├── .editorconfig ├── .env.example ├── .gitattributes ├── .gitee └── ISSUE_TEMPLATE.zh-CN.md ├── .gitignore ├── .php-cs-fixer.dist.php ├── .workflow ├── branch-pipeline.yml ├── master-pipeline.yml └── pr-pipeline.yml ├── LICENSE.txt ├── README-en.md ├── README.md ├── app ├── Console │ └── Kernel.php ├── Events │ ├── Create.php │ └── Test.php ├── Exceptions │ └── Handler.php ├── Http │ ├── Controllers │ │ └── Controller.php │ ├── Kernel.php │ └── Middleware │ │ ├── Authenticate.php │ │ ├── EncryptCookies.php │ │ ├── PreventRequestsDuringMaintenance.php │ │ ├── RedirectIfAuthenticated.php │ │ ├── TrimStrings.php │ │ ├── TrustHosts.php │ │ ├── TrustProxies.php │ │ ├── ValidateSignature.php │ │ └── VerifyCsrfToken.php ├── Listeners │ ├── Command.php │ ├── RouteMatched.php │ └── test.php ├── Models │ ├── Modules │ │ └── Users │ │ │ └── Models │ │ │ └── CatchController.php │ └── User.php └── Providers │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ ├── BroadcastServiceProvider.php │ ├── EventServiceProvider.php │ └── RouteServiceProvider.php ├── artisan ├── bootstrap ├── app.php └── cache │ ├── packages.php │ └── services.php ├── composer.json ├── config ├── app.php ├── auth.php ├── broadcasting.php ├── cache.php ├── cors.php ├── database.php ├── filesystems.php ├── hashing.php ├── logging.php ├── mail.php ├── queue.php ├── services.php ├── session.php └── view.php ├── database ├── .gitignore ├── factories │ └── UserFactory.php ├── migrations │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ └── 2019_12_14_000001_create_personal_access_tokens_table.php └── seeders │ ├── DatabaseSeeder.php │ └── good.php ├── docker-compose.yml ├── lang └── en │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php ├── modules ├── Common │ ├── Http │ │ └── Controllers │ │ │ ├── OptionController.php │ │ │ └── UploadController.php │ ├── Providers │ │ └── CommonServiceProvider.php │ ├── README.md │ ├── Repository │ │ └── Options │ │ │ ├── Components.php │ │ │ ├── Controllers.php │ │ │ ├── DataRange.php │ │ │ ├── Factory.php │ │ │ ├── Modules.php │ │ │ ├── OptionInterface.php │ │ │ ├── Schemas.php │ │ │ └── Status.php │ ├── Support │ │ └── Upload │ │ │ ├── OssUpload.php │ │ │ ├── Uploader.php │ │ │ └── Uses │ │ │ ├── LocalUpload.php │ │ │ └── Upload.php │ ├── config │ │ └── upload.php │ └── routes │ │ └── route.php ├── Develop │ ├── Http │ │ └── Controllers │ │ │ ├── GenerateController.php │ │ │ ├── ModuleController.php │ │ │ └── SchemaController.php │ ├── Listeners │ │ ├── CreatedListener.php │ │ └── DeletedListener.php │ ├── Models │ │ └── Schemas.php │ ├── Providers │ │ ├── DevelopServiceProvider.php │ │ └── Install.php │ ├── Support │ │ ├── Generate │ │ │ ├── Create │ │ │ │ ├── Controller.php │ │ │ │ ├── Creator.php │ │ │ │ ├── FrontForm.php │ │ │ │ ├── FrontTable.php │ │ │ │ ├── Model.php │ │ │ │ ├── Request.php │ │ │ │ ├── Route.php │ │ │ │ └── Schema.php │ │ │ ├── Generator.php │ │ │ ├── Module.php │ │ │ └── stubs │ │ │ │ ├── controller.stub │ │ │ │ ├── migration.stub │ │ │ │ ├── model.stub │ │ │ │ ├── provider.stub │ │ │ │ ├── request.stub │ │ │ │ ├── route.stub │ │ │ │ └── vue │ │ │ │ ├── form.stub │ │ │ │ ├── formItems │ │ │ │ ├── cascader.stub │ │ │ │ ├── date.stub │ │ │ │ ├── datetime.stub │ │ │ │ ├── input-number.stub │ │ │ │ ├── input.stub │ │ │ │ ├── radio.stub │ │ │ │ ├── rate.stub │ │ │ │ ├── select.stub │ │ │ │ ├── switch.stub │ │ │ │ ├── textarea.stub │ │ │ │ ├── tree-select.stub │ │ │ │ ├── tree.stub │ │ │ │ └── upload.stub │ │ │ │ └── table.stub │ │ └── ModuleInstall.php │ ├── database │ │ └── migrations │ │ │ └── Schemas.php │ └── routes │ │ └── route.php ├── Permissions │ ├── .DS_Store │ ├── Enums │ │ ├── DataRange.php │ │ ├── MenuStatus.php │ │ └── MenuType.php │ ├── Exceptions │ │ └── PermissionForbidden.php │ ├── Http │ │ ├── Controllers │ │ │ ├── DepartmentsController.php │ │ │ ├── JobsController.php │ │ │ ├── PermissionsController.php │ │ │ └── RolesController.php │ │ └── Requests │ │ │ └── RoleRequest.php │ ├── Installer.php │ ├── Middlewares │ │ └── PermissionGate.php │ ├── Models │ │ ├── Departments.php │ │ ├── Jobs.php │ │ ├── Permissions.php │ │ ├── Roles.php │ │ └── Traits │ │ │ └── DataRange.php │ ├── Providers │ │ └── PermissionsServiceProvider.php │ ├── database │ │ ├── migrations │ │ │ ├── 2022_12_05_084442_create_roles.php │ │ │ ├── 2022_12_06_110551_create_jobs.php │ │ │ ├── 2022_12_07_075441_create_departments.php │ │ │ ├── 2022_12_07_103318_create_permissions.php │ │ │ ├── 2022_12_10_061840_create_user_has_roles.php │ │ │ ├── 2022_12_10_061857_create_role_has_permissions.php │ │ │ ├── 2022_12_10_061919_create_user_has_jobs.php │ │ │ ├── 2022_12_10_061928_create_role_has_departments.php │ │ │ ├── 2023_03_01_142043_create_remove_is_inner.php │ │ │ └── 2023_04_17_141652_create_update_permissions.php │ │ └── seeder │ │ │ └── PermissionsMenusSeeder.php │ └── routes │ │ └── route.php ├── System │ ├── Http │ │ └── Controllers │ │ │ ├── DictionaryController.php │ │ │ └── DictionaryValuesController.php │ ├── Installer.php │ ├── LICENSE.txt │ ├── Models │ │ ├── Dictionary.php │ │ └── DictionaryValues.php │ ├── Providers │ │ └── SystemServiceProvider.php │ ├── README.md │ ├── database │ │ ├── migrations │ │ │ ├── 2023_05_08_043214_create_system_dictionary.php │ │ │ └── 2023_05_08_043740_create_system_dictionary_values.php │ │ └── seeder │ │ │ └── SystemMenusSeeder.php │ └── routes │ │ └── route.php └── User │ ├── Events │ └── Login.php │ ├── Http │ ├── Controllers │ │ ├── AuthController.php │ │ └── UserController.php │ └── Requests │ │ └── UserRequest.php │ ├── Listeners │ └── Login.php │ ├── Middlewares │ └── OperatingMiddleware.php │ ├── Models │ ├── LogLogin.php │ ├── LogOperate.php │ ├── Observers │ │ └── UsersObserver.php │ ├── Traits │ │ └── UserRelations.php │ └── User.php │ ├── Providers │ └── UserServiceProvider.php │ ├── database │ ├── migrations │ │ ├── 2022_12_04_060250_create_users.php │ │ ├── 2022_12_04_062539_create_log_login.php │ │ └── 2022_12_17_031519_create_log_operate.php │ └── seeder │ │ └── User.php │ └── routes │ └── route.php ├── phpunit.xml ├── public ├── .htaccess ├── admin.html ├── favicon.ico ├── index.php └── robots.txt ├── resources └── views │ └── welcome.blade.php ├── routes ├── api.php ├── channels.php ├── console.php └── web.php ├── storage ├── app │ ├── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore └── wechat.png /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=CatchAdmin后台管理系统 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | 7 | LOG_CHANNEL=stack 8 | LOG_DEPRECATIONS_CHANNEL=null 9 | LOG_LEVEL=debug 10 | 11 | DB_CONNECTION=mysql 12 | DB_HOST=127.0.0.1 13 | DB_PORT=3306 14 | DB_DATABASE=laravel 15 | DB_USERNAME=root 16 | DB_PASSWORD= 17 | DB_PREFIX= 18 | 19 | BROADCAST_DRIVER=log 20 | CACHE_DRIVER=file 21 | FILESYSTEM_DISK=local 22 | QUEUE_CONNECTION=sync 23 | SESSION_DRIVER=file 24 | SESSION_LIFETIME=120 25 | 26 | MEMCACHED_HOST=127.0.0.1 27 | 28 | REDIS_HOST=127.0.0.1 29 | REDIS_PASSWORD=null 30 | REDIS_PORT=6379 31 | 32 | MAIL_MAILER=smtp 33 | MAIL_HOST=mailhog 34 | MAIL_PORT=1025 35 | MAIL_USERNAME=null 36 | MAIL_PASSWORD=null 37 | MAIL_ENCRYPTION=null 38 | MAIL_FROM_ADDRESS="hello@example.com" 39 | MAIL_FROM_NAME="${APP_NAME}" 40 | 41 | AWS_ACCESS_KEY_ID= 42 | AWS_SECRET_ACCESS_KEY= 43 | AWS_DEFAULT_REGION=us-east-1 44 | AWS_BUCKET= 45 | AWS_USE_PATH_STYLE_ENDPOINT=false 46 | 47 | PUSHER_APP_ID= 48 | PUSHER_APP_KEY= 49 | PUSHER_APP_SECRET= 50 | PUSHER_HOST= 51 | PUSHER_PORT=443 52 | PUSHER_SCHEME=https 53 | PUSHER_APP_CLUSTER=mt1 54 | 55 | VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 56 | VITE_PUSHER_HOST="${PUSHER_HOST}" 57 | VITE_PUSHER_PORT="${PUSHER_PORT}" 58 | VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" 59 | VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 60 | VITE_BASE_URL=${APP_URL}/api/ 61 | VITE_APP_NAME=${APP_NAME} 62 | 63 | ALIOSS_BUCKET= 64 | ALIOSS_ACCESS_ID= 65 | ALIOSS_ACCESS_SECRET= 66 | ALIOSS_ENDPOINT= 67 | ALIOSS_UPLOAD_DIR= 68 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /.gitee/ISSUE_TEMPLATE.zh-CN.md: -------------------------------------------------------------------------------- 1 | # 环境 2 | - 操作系统: 3 | - php 版本: 4 | - Laravel 版本: 5 | - Mysql 版本: 6 | - web 服务器: 7 | 8 | # 问题 9 | - 问题描述: 10 | - 问题截图: 11 | 12 | # 结果 13 | - 实际结果: 14 | - 预期结果: 15 | 16 | # 分析 17 | - 所做的尝试: 18 | - 19 | - 20 | 21 | # 方案: 22 | - 解决方案: 23 | 24 | > 请在问题解决后关闭 issue 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /public/hot 2 | /public/storage 3 | /storage/*.key 4 | /vendor 5 | /fixer 6 | /catch 7 | .env 8 | nohup.out 9 | .env.backup 10 | .env.production 11 | .phpunit.result.cache 12 | .php-cs-fixer.cache 13 | Homestead.json 14 | Homestead.yaml 15 | composer.lock 16 | /.fleet 17 | /.idea 18 | /.vscode 19 | /web 20 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | exclude('packages') 11 | //// ->notPath('./packages/test.php') 12 | // in 配置需要规则的目录 13 | ->in([ 14 | __DIR__.DIRECTORY_SEPARATOR.'app', 15 | 16 | __DIR__.DIRECTORY_SEPARATOR.'catch', 17 | 18 | __DIR__.DIRECTORY_SEPARATOR.'modules', 19 | ]) 20 | // 排除 . 开头的文件 21 | ->ignoreDotFiles(true) 22 | // vcs 文件 23 | ->ignoreVCS(true); 24 | 25 | $config = new Config(); 26 | 27 | return $config->setRules([ 28 | '@PSR1' => true, // psr1 29 | 30 | '@PSR2' => true, // psr2 规范 31 | 32 | '@PSR12' => true, // psr12 规范 33 | 34 | 'binary_operator_spaces' => true, // 二元操作符号空格 $a=1 => $a = 1; 35 | 36 | 'array_syntax' => [ 37 | 'syntax' => 'short', // array('1') => ['1'] 38 | ], 39 | 40 | 'no_trailing_comma_in_singleline_array' => true, // -$a = array('sample', ); => $a = array('sample'); 41 | 42 | 'trim_array_spaces' => true, // array( 'a', 'b' ); => array('a', 'b') 43 | 44 | 'single_trait_insert_per_statement' => false, 45 | 46 | 'standardize_not_equals' => true, // "!=" => "<>" 47 | 48 | 'magic_constant_casing' => true, // __dir__ => __DIR__ 49 | 50 | 'native_function_casing' => true, // STRLEN($str); => strlen($str); 51 | 52 | 'cast_spaces' => true, // (int)$b => (int) $b 53 | 54 | 'simplified_if_return' => true, // if ($foo) { return true; } return false; => return (bool) ($foo) ; 55 | 56 | 'no_unused_imports' => true, // use \DateTime; -use \Exception; => use \DateTime; 57 | 58 | 'not_operator_with_successor_space' => true, // if (!$bar) => if (! $bar) 59 | 60 | /** 61 | * // function example($b) { 62 | if ($b) { 63 | return; 64 | } 65 | - return; 66 | */ 67 | 'no_useless_return' => true, 68 | 69 | /** 70 | * function a() { 71 | - $a = 1; 72 | - return $a; 73 | + return 1; 74 | */ 75 | 'return_assignment' => true, 76 | 77 | /** 78 | - true, 82 | 83 | /** 84 | * $foo = [ 85 | - 'bar' => [ 86 | - 'baz' => true, 87 | - ], 88 | + 'bar' => [ 89 | + 'baz' => true, 90 | + ], 91 | */ 92 | 'array_indentation' => true, 93 | 94 | /** 95 | * -$sample = $b [ 'a' ] [ 'b' ]; 96 | +$sample = $b['a']['b']; 97 | */ 98 | 'no_spaces_around_offset' => true, 99 | 100 | 'concat_space' => true, // $a.$b => $a . $b 101 | ])->setFinder($finder); 102 | -------------------------------------------------------------------------------- /.workflow/branch-pipeline.yml: -------------------------------------------------------------------------------- 1 | version: '1.0' 2 | name: branch-pipeline 3 | displayName: BranchPipeline 4 | stages: 5 | - stage: 6 | name: compile 7 | displayName: 编译 8 | steps: 9 | - step: build@php 10 | name: build_php 11 | displayName: PHP 构建 12 | # 支持5.0、7.0、7.1、7.2、7.3、7.4、8.0、8.1八个版本 13 | phpVersion: 8.0 14 | # 构建命令 15 | commands: 16 | - php --version 17 | # 非必填字段,开启后表示将构建产物暂存,但不会上传到制品库中,7天后自动清除 18 | artifacts: 19 | # 构建产物名字,作为产物的唯一标识可向下传递,支持自定义,默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址 20 | - name: BUILD_ARTIFACT 21 | # 构建产物获取路径,是指代码编译完毕之后构建物的所在路径 22 | path: 23 | - ./ 24 | - step: publish@general_artifacts 25 | name: publish_general_artifacts 26 | displayName: 上传制品 27 | # 上游构建任务定义的产物名,默认BUILD_ARTIFACT 28 | dependArtifact: BUILD_ARTIFACT 29 | # 上传到制品库时的制品命名,默认output 30 | artifactName: output 31 | dependsOn: build_php 32 | - stage: 33 | name: release 34 | displayName: 发布 35 | steps: 36 | - step: publish@release_artifacts 37 | name: publish_release_artifacts 38 | displayName: '发布' 39 | # 上游上传制品任务的产出 40 | dependArtifact: output 41 | # 发布制品版本号 42 | version: '1.0.0.0' 43 | # 是否开启版本号自增,默认开启 44 | autoIncrement: true 45 | triggers: 46 | push: 47 | branches: 48 | exclude: 49 | - master 50 | include: 51 | - .* 52 | -------------------------------------------------------------------------------- /.workflow/master-pipeline.yml: -------------------------------------------------------------------------------- 1 | version: '1.0' 2 | name: master-pipeline 3 | displayName: MasterPipeline 4 | stages: 5 | - stage: 6 | name: compile 7 | displayName: 编译 8 | steps: 9 | - step: build@php 10 | name: build_php 11 | displayName: PHP 构建 12 | # 支持5.0、7.0、7.1、7.2、7.3、7.4、8.0、8.1八个版本 13 | phpVersion: 8.0 14 | # 构建命令 15 | commands: 16 | - php --version 17 | # 非必填字段,开启后表示将构建产物暂存,但不会上传到制品库中,7天后自动清除 18 | artifacts: 19 | # 构建产物名字,作为产物的唯一标识可向下传递,支持自定义,默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址 20 | - name: BUILD_ARTIFACT 21 | # 构建产物获取路径,是指代码编译完毕之后构建物的所在路径 22 | path: 23 | - ./ 24 | - step: publish@general_artifacts 25 | name: publish_general_artifacts 26 | displayName: 上传制品 27 | # 上游构建任务定义的产物名,默认BUILD_ARTIFACT 28 | dependArtifact: BUILD_ARTIFACT 29 | # 上传到制品库时的制品命名,默认output 30 | artifactName: output 31 | dependsOn: build_php 32 | - stage: 33 | name: release 34 | displayName: 发布 35 | steps: 36 | - step: publish@release_artifacts 37 | name: publish_release_artifacts 38 | displayName: '发布' 39 | # 上游上传制品任务的产出 40 | dependArtifact: output 41 | # 发布制品版本号 42 | version: '1.0.0.0' 43 | # 是否开启版本号自增,默认开启 44 | autoIncrement: true 45 | triggers: 46 | push: 47 | branches: 48 | include: 49 | - master 50 | -------------------------------------------------------------------------------- /.workflow/pr-pipeline.yml: -------------------------------------------------------------------------------- 1 | version: '1.0' 2 | name: pr-pipeline 3 | displayName: PRPipeline 4 | stages: 5 | - stage: 6 | name: compile 7 | displayName: 编译 8 | steps: 9 | - step: build@php 10 | name: build_php 11 | displayName: PHP 构建 12 | # 支持5.0、7.0、7.1、7.2、7.3、7.4、8.0、8.1八个版本 13 | phpVersion: 8.0 14 | # 构建命令 15 | commands: 16 | - php --version 17 | # 非必填字段,开启后表示将构建产物暂存,但不会上传到制品库中,7天后自动清除 18 | artifacts: 19 | # 构建产物名字,作为产物的唯一标识可向下传递,支持自定义,默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址 20 | - name: BUILD_ARTIFACT 21 | # 构建产物获取路径,是指代码编译完毕之后构建物的所在路径 22 | path: 23 | - ./ 24 | - step: publish@general_artifacts 25 | name: publish_general_artifacts 26 | displayName: 上传制品 27 | # 上游构建任务定义的产物名,默认BUILD_ARTIFACT 28 | dependArtifact: BUILD_ARTIFACT 29 | # 上传到制品库时的制品命名,默认output 30 | artifactName: output 31 | dependsOn: build_php 32 | triggers: 33 | pr: 34 | branches: 35 | include: 36 | - master 37 | -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 | ## Introduce 2 | `CatchAdmin` is a background management system based on secondary development of [Laravel](https://laravel.com) and [Element Plus](https://element-plus.org). The `Laravel` community also has many excellent background management systems, such as `Nova`, an official product, which is of course charged, and `Filament` based on `Livewire` is free, and `Laravel Admin` has to be said. `CatchAdmin` still adopts the traditional front-end and back-end separation strategy, and the `Laravel` framework is only exported as `Api`. Coupling between management system modules is minimized. Each module has independent controllers, routes, models, and data tables. In terms of development, the influence between modules is minimized as much as possible, which reduces the difficulty of development. Based on `CatchAdmin`, systems such as `CMS`, `CRM`, `OA`, etc. can be developed. It also encapsulates many practical tools to enhance the development experience. 3 | 4 | [Chinese](./README.md)|[English](./README-en.md) 5 | 6 | ## Function 7 | - [x] User management Background 8 | - [x] Department Management Configure the company's department structure, support tree structure 9 | - [x] Position Management Configure the position of background users 10 | - [x] Menu Management Configure system menus, buttons, etc. 11 | - [x] Role management Configure user roles and assign permissions 12 | - [x] Operation log Background user operation records 13 | - [x] Login log The login record of background system users 14 | - [x] Code Generation Generate CURD operations on the API side 15 | - [x] Schema management Generate table structure 16 | - [x] module management system 17 | 18 | ## Project Address 19 | - [github catch admin](https://github.com/jaguarjack/catch-admin) 20 | - 21 | ## Document Address 22 | - [Document Address](https://catchadmin.com/docs/3.0/intro) 23 | 24 | ## Preview 25 | ![zRrjNd.png](https://i.imgtg.com/2023/02/16/dASpg.png) 26 | ![zRsAEQ.png](https://i.imgtg.com/2023/02/16/dAsKK.png) 27 | ![zRsUv6.png](https://i.imgtg.com/2023/02/16/dA0fB.png) 28 | ![zRsV4s.png](https://i.imgtg.com/2023/02/16/dAd5s.png) 29 | 30 | ## Demo Address 31 | [demo address](https://v3.catchadmin.com) 32 | - Account: `catch@admin.com` 33 | - Password: `catchadmin` 34 | 35 | ## Sponsorship 36 | If the project helps you, or saves you development time at work. If you can, you can support the `Catchadmin` project, thank you very much 🙏 37 | support 38 | 39 | ## Specification 40 | ###PHP 41 | Use fixer for code checking, please refer to the specifications of the `.php-cs-fixer.dist.php` file in the root directory for details, and the following two steps are required 42 | ```shell 43 | mkdir path 44 | ``` 45 | ```shell 46 | composer require --working-dir=path friendsofphp/php-cs-fixer 47 | ``` 48 | After the installation is complete, you can use 49 | ```shell 50 | composer cs 51 | ``` 52 | Format the code, this command will directly modify the file to complete the correction, if you only need to check whether the format is correct, then use 53 | ```shell 54 | composer cs-diff 55 | ``` 56 | 57 | ## Thanks 🙏 58 | > Ranked in no particular order 59 | 60 | - [Laravel](https://laravel.com) 61 | - [Vue](https://cn.vuejs.org/) 62 | - [ElementPlus](https://element-plus.org) 63 | - [Docusaurus](https://docusaurus.com) 64 | - [JetBrains](https://www.jetbrains.com/) 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 介绍 2 | ### 这是 catchadmin 完全分离的版本 3 | `CatchAdmin`是一款基于[Laravel](https://laravel.com)和[Element Plus](https://element-plus.org)二次开发而成后台管理系统。`Laravel` 社区也有许多非常优秀的后台管理系统,例如 `Nova`, 官方出品,当然是收费的,免费的有基于 `Livewire` 的 `Filament`,还有不得不说的 `Laravel Admin`。`CatchAdmin` 还是采用传统的前后端分离策略,`Laravel` 框架仅仅作为 `Api` 输出。将管理系统模块之间的耦合降到了最低限度。每个模块之间都有独立的控制器,路由,模型,数据表。在开发上尽可能将模块之间的影响降到最低,降低了开发上的难度。基于 `CatchAdmin `可以开发 `CMS`,`CRM`,`OA` 等 等系统。也封装了很多实用的工具,提升开发体验。 4 | 5 | ## 前端项目 6 | [catchadmin-vue](https://gitee.com/catchadmin/catch-admin-vue) 7 | 8 | ## Laravel 入门教程 9 | [Laravel 免费入门教程](https://laravel-study.catchadmin.com) 10 | 11 | [中文](./README.md)|[英文](./README-en.md) 12 | ## 其他版本 13 | - [tp8 新版本](https://gitee.com/catchamin/catchadmin-tp) 14 | - [webman 高性能版本](https://gitee.com/catchamin/catchadmin-webman) 15 | 16 | ## 新功能 17 | - [动态表单](https://catchadmin.com/docs/3.0/front/catch-form) 18 | - [动态表格](https://catchadmin.com/docs/3.0/front/catch-table) 19 | 20 | ## 专业版 21 | [专业版本官方地址](https://license.catchadmin.com) 22 | 23 | 首先感谢一直以来对 `CatchAdmin` 开源项目的支持和使用。作为一名开源工作者,我一直致力于开发出功能强大且易于使用的后台管理系统,以帮助您简化业务流程和提升工作效率。然而,由于某些原因,我不得不做出一些调整。为了能够继续开发和维护这个项目,我将推出一款付费的后台管理系统,以确保我能够持续为您提供高质量的服务和支持。 24 | 25 | 专业版本不会在开源版本做一些破坏性变更,所以当您从开源版本切换到专业版本,不会有任何开发心智负担。但是使用专业版本会有新的组件来配合您的工作。 26 | 27 | 我深信,付费后台管理系统将为您带来更多的价值和便利,帮助您提升工作效率 28 | 29 | ## 功能 30 | - [x] 用户管理 后台用户管理 31 | - [x] 部门管理 配置公司的部门结构,支持树形结构 32 | - [x] 岗位管理 配置后台用户的职务 33 | - [x] 菜单管理 配置系统菜单,按钮等等 34 | - [x] 角色管理 配置用户担当的角色,分配权限 35 | - [x] 操作日志 后台用户操作记录 36 | - [x] 登录日志 后台系统用户的登录记录 37 | - [x] 代码生成 生成 API 端的 CURD 操作 38 | - [x] Schema 管理 生成表结构 39 | - [x] 模块管理 系统模块管理 40 | 41 | 42 | ## 讨论 43 | - 可以提 `ISSUE`,请按照 `issue` 模板提问 44 | - 加入 Q 群 `302266230` 暗号 `catchadmin`。 45 | - 加微信入群,新建🆕 46 | 47 | 48 | 49 | ## 项目地址 50 | - [github catchadmin](https://github.com/jaguarjack/catch-admin) 51 | ## 文档地址 52 | - [文档地址](https://catchadmin.com/docs/3.0/intro) 53 | ## 预览 54 | 55 | ![zRrjNd.png](https://i.imgtg.com/2023/02/16/dASpg.png) 56 | ![zRsAEQ.png](https://i.imgtg.com/2023/02/16/dAsKK.png) 57 | ![zRsUv6.png](https://i.imgtg.com/2023/02/16/dA0fB.png) 58 | ![zRsV4s.png](https://i.imgtg.com/2023/02/16/dAd5s.png) 59 | 60 | ## 体验地址 61 | [demo 地址](https://v3.catchadmin.com) 62 | - 账户: `catch@admin.com` 63 | - 密码: `catchadmin` 64 | 65 | ## 视频教程(😂记得一键三连哦) 66 | - [catchadmin 安装](https://www.bilibili.com/video/BV1eY411v71J/) 67 | - [catchadmin 开发之模块创建](https://www.bilibili.com/video/BV1jP41127aW/) 68 | - [catchadmin 之快速开发](https://www.bilibili.com/video/BV1Qh4y1J7eB/) 69 | 70 | ## 规范 71 | ### PHP 72 | 使用 fixer 进行代码检查, 具体请查看根目录下 `.php-cs-fixer.dist.php` 文件的规范,还需要进行以下两步骤 73 | ```shell 74 | mkdir path 75 | ``` 76 | ```shell 77 | composer require --working-dir=path friendsofphp/php-cs-fixer 78 | ``` 79 | 安装完成之后可以使用 80 | ```shell 81 | composer cs 82 | ``` 83 | 进行代码格式化,这个命令会直接修改文件完成修正,如果只需要查看格式是否正确,那么使用 84 | ```shell 85 | composer cs-diff 86 | ``` 87 | 88 | ## 感谢🙏 89 | > 排名不分先后 90 | 91 | - [Laravel](https://laravel.com) 92 | - [Vue](https://cn.vuejs.org/) 93 | - [ElementPlus](https://element-plus.org) 94 | - [VitePress](https://vitepress.dev/zh/) 95 | - [JetBrains](https://www.jetbrains.com/) 96 | 97 | 98 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('inspire')->hourly(); 19 | } 20 | 21 | /** 22 | * Register the commands for the application. 23 | * 24 | * @return void 25 | */ 26 | protected function commands() 27 | { 28 | $this->load(__DIR__.'/Commands'); 29 | 30 | require base_path('routes/console.php'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Events/Create.php: -------------------------------------------------------------------------------- 1 | , \Psr\Log\LogLevel::*> 19 | */ 20 | protected $levels = [ 21 | // 22 | ]; 23 | 24 | /** 25 | * A list of the exception types that are not reported. 26 | * 27 | * @var array> 28 | */ 29 | protected $dontReport = [ 30 | // 31 | ]; 32 | 33 | /** 34 | * A list of the inputs that are never flashed to the session on validation exceptions. 35 | * 36 | * @var array 37 | */ 38 | protected $dontFlash = [ 39 | 'current_password', 40 | 'password', 41 | 'password_confirmation', 42 | ]; 43 | 44 | /** 45 | * Register the exception handling callbacks for the application. 46 | * 47 | * @return void 48 | */ 49 | public function register() 50 | { 51 | $this->reportable(function (Throwable $e) { 52 | // 53 | }); 54 | } 55 | 56 | 57 | /** 58 | * render 59 | * 60 | * @param $request 61 | * @param Throwable $e 62 | * @return JsonResponse|Response 63 | * @throws Throwable 64 | */ 65 | public function render($request, Throwable $e): JsonResponse|Response 66 | { 67 | $message = $e->getMessage(); 68 | 69 | if (method_exists($e, 'getStatusCode')) { 70 | if ($e->getStatusCode() == Response::HTTP_NOT_FOUND) { 71 | $message = '路由未找到或未注册'; 72 | } 73 | } 74 | 75 | $e = new FailedException($message ?: 'Server Error', $e instanceof CatchException ? $e->getCode() : Code::FAILED); 76 | 77 | $response = parent::render($request, $e); 78 | 79 | $response->header('Access-Control-Allow-Origin', '*'); 80 | $response->header('Access-Control-Allow-Methods', '*'); 81 | $response->header('Access-Control-Allow-Headers', '*'); 82 | 83 | return $response; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $middleware = [ 17 | // \App\Http\Middleware\TrustHosts::class, 18 | \App\Http\Middleware\TrustProxies::class, 19 | \Illuminate\Http\Middleware\HandleCors::class, 20 | \App\Http\Middleware\PreventRequestsDuringMaintenance::class, 21 | \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, 22 | \App\Http\Middleware\TrimStrings::class, 23 | \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, 24 | ]; 25 | 26 | /** 27 | * The application's route middleware groups. 28 | * 29 | * @var array> 30 | */ 31 | protected $middlewareGroups = [ 32 | 'web' => [ 33 | \App\Http\Middleware\EncryptCookies::class, 34 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 35 | \Illuminate\Session\Middleware\StartSession::class, 36 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 37 | \App\Http\Middleware\VerifyCsrfToken::class, 38 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 39 | ], 40 | 41 | 'api' => [ 42 | // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 43 | 'throttle:api', 44 | \Illuminate\Routing\Middleware\SubstituteBindings::class 45 | ], 46 | ]; 47 | 48 | /** 49 | * The application's route middleware. 50 | * 51 | * These middleware may be assigned to groups or used individually. 52 | * 53 | * @var array 54 | */ 55 | protected $routeMiddleware = [ 56 | 'auth' => \App\Http\Middleware\Authenticate::class, 57 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 58 | 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, 59 | 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 60 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 61 | 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 62 | 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 63 | 'signed' => \App\Http\Middleware\ValidateSignature::class, 64 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 65 | 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 66 | ]; 67 | } 68 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 18 | return route('login'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/PreventRequestsDuringMaintenance.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 26 | return redirect(RouteServiceProvider::HOME); 27 | } 28 | } 29 | 30 | return $next($request); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'current_password', 16 | 'password', 17 | 'password_confirmation', 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustHosts.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function hosts() 15 | { 16 | return [ 17 | $this->allSubdomainsOfApplicationUrl(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | |string|null 14 | */ 15 | protected $proxies; 16 | 17 | /** 18 | * The headers that should be used to detect proxies. 19 | * 20 | * @var int 21 | */ 22 | protected $headers = 23 | Request::HEADER_X_FORWARDED_FOR | 24 | Request::HEADER_X_FORWARDED_HOST | 25 | Request::HEADER_X_FORWARDED_PORT | 26 | Request::HEADER_X_FORWARDED_PROTO | 27 | Request::HEADER_X_FORWARDED_AWS_ELB; 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Middleware/ValidateSignature.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 'fbclid', 16 | // 'utm_campaign', 17 | // 'utm_content', 18 | // 'utm_medium', 19 | // 'utm_source', 20 | // 'utm_term', 21 | ]; 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Listeners/Command.php: -------------------------------------------------------------------------------- 1 | command); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Listeners/RouteMatched.php: -------------------------------------------------------------------------------- 1 | route); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Listeners/test.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected $fillable = [ 22 | 'name', 23 | 'email', 24 | 'password', 25 | ]; 26 | 27 | /** 28 | * The attributes that should be hidden for serialization. 29 | * 30 | * @var array 31 | */ 32 | protected $hidden = [ 33 | 'password', 34 | 'remember_token', 35 | ]; 36 | 37 | /** 38 | * The attributes that should be cast. 39 | * 40 | * @var array 41 | */ 42 | protected $casts = [ 43 | 'email_verified_at' => 'datetime', 44 | ]; 45 | } 46 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected $policies = [ 16 | // 'App\Models\Model' => 'App\Policies\ModelPolicy', 17 | ]; 18 | 19 | /** 20 | * Register any authentication / authorization services. 21 | * 22 | * @return void 23 | */ 24 | public function boot() 25 | { 26 | $this->registerPolicies(); 27 | 28 | // 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | > 19 | */ 20 | protected $listen = [ 21 | Registered::class => [ 22 | SendEmailVerificationNotification::class, 23 | ], 24 | 25 | RouteMatched::class => [ 26 | \App\Listeners\RouteMatched::class 27 | ], 28 | 29 | CommandFinished::class => [ 30 | Command::class 31 | ] 32 | ]; 33 | 34 | /** 35 | * Register any events for your application. 36 | * 37 | * @return void 38 | */ 39 | public function boot() 40 | { 41 | // 42 | } 43 | 44 | /** 45 | * Determine if events and listeners should be automatically discovered. 46 | * 47 | * @return bool 48 | */ 49 | public function shouldDiscoverEvents() 50 | { 51 | return false; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | configureRateLimiting(); 32 | 33 | $this->routes(function () { 34 | Route::middleware('api') 35 | ->prefix('api') 36 | ->group(base_path('routes/api.php')); 37 | 38 | Route::middleware('web') 39 | ->group(base_path('routes/web.php')); 40 | }); 41 | 42 | $this->booted(function(){ 43 | $this->app->booted(function (){ 44 | if (file_exists('loadCachedAdminRoutes')) { 45 | loadCachedAdminRoutes(); 46 | } 47 | }); 48 | }); 49 | } 50 | 51 | /** 52 | * Configure the rate limiters for the application. 53 | * 54 | * @return void 55 | */ 56 | protected function configureRateLimiting() 57 | { 58 | RateLimiter::for('api', function (Request $request) { 59 | return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /bootstrap/cache/packages.php: -------------------------------------------------------------------------------- 1 | 3 | array ( 4 | 'providers' => 5 | array ( 6 | 0 => 'Laravel\\Tinker\\TinkerServiceProvider', 7 | ), 8 | ), 9 | 'nesbot/carbon' => 10 | array ( 11 | 'providers' => 12 | array ( 13 | 0 => 'Carbon\\Laravel\\ServiceProvider', 14 | ), 15 | ), 16 | 'nunomaduro/collision' => 17 | array ( 18 | 'providers' => 19 | array ( 20 | 0 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', 21 | ), 22 | ), 23 | 'nunomaduro/termwind' => 24 | array ( 25 | 'providers' => 26 | array ( 27 | 0 => 'Termwind\\Laravel\\TermwindServiceProvider', 28 | ), 29 | ), 30 | 'pestphp/pest' => 31 | array ( 32 | 'providers' => 33 | array ( 34 | 0 => 'Pest\\Laravel\\PestServiceProvider', 35 | ), 36 | ), 37 | 'tymon/jwt-auth' => 38 | array ( 39 | 'aliases' => 40 | array ( 41 | 'JWTAuth' => 'Tymon\\JWTAuth\\Facades\\JWTAuth', 42 | 'JWTFactory' => 'Tymon\\JWTAuth\\Facades\\JWTFactory', 43 | ), 44 | 'providers' => 45 | array ( 46 | 0 => 'Tymon\\JWTAuth\\Providers\\LaravelServiceProvider', 47 | ), 48 | ), 49 | ); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jaguarjack/catchadmin", 3 | "type": "project", 4 | "description": "The CatchAdmin Background management", 5 | "keywords": [ 6 | "framework", 7 | "catchadmin", 8 | "management", 9 | "permissions", 10 | "modules" 11 | ], 12 | "license": "MIT", 13 | "require": { 14 | "php": "^8.2", 15 | "ext-pdo": "*", 16 | "ext-zip": "*", 17 | "laravel/framework": "^v12.8.1", 18 | "laravel/tinker": "^v2.10.1", 19 | "catchadmin/core": "^0.4.5" 20 | }, 21 | "require-dev": { 22 | "fakerphp/faker": "^v1.24.1", 23 | "mockery/mockery": "^1.6.12", 24 | "pestphp/pest": "^v3.7.2" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "App\\": "app/", 29 | "Database\\Factories\\": "database/factories/", 30 | "Database\\Seeders\\": "database/seeders/", 31 | "Modules\\": "modules", 32 | "\\": "" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Tests\\": "tests/" 38 | } 39 | }, 40 | "scripts": { 41 | "post-autoload-dump": [ 42 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 43 | "@php artisan package:discover --ansi", 44 | "@composer dump-autoload --no-scripts" 45 | ], 46 | "post-update-cmd": [ 47 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force" 48 | ], 49 | "post-root-package-install": [ 50 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 51 | ], 52 | "post-create-project-cmd": [ 53 | "@php artisan key:generate --ansi" 54 | ], 55 | "dev": [ 56 | "Composer\\Config::disableProcessTimeout", 57 | "npx concurrently -c \"#93c5fd,#c4b5fd\" \"cd web && yarn dev\" \"php artisan serve\" --names='vite,server'" 58 | ], 59 | "cs-diff": "./fixer/vendor/bin/php-cs-fixer fix --dry-run --diff", 60 | "cs": "./fixer/vendor/bin/php-cs-fixer fix" 61 | }, 62 | "extra": { 63 | "laravel": { 64 | "dont-discover": [] 65 | } 66 | }, 67 | "config": { 68 | "optimize-autoloader": true, 69 | "preferred-install": "dist", 70 | "sort-packages": true, 71 | "allow-plugins": { 72 | "pestphp/pest-plugin": true 73 | } 74 | }, 75 | "minimum-stability": "dev", 76 | "prefer-stable": true 77 | } -------------------------------------------------------------------------------- /config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_DRIVER', 'null'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Broadcast Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the broadcast connections that will be used 26 | | to broadcast events to other systems or over websockets. Samples of 27 | | each available type of connection are provided inside this array. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'pusher' => [ 34 | 'driver' => 'pusher', 35 | 'key' => env('PUSHER_APP_KEY'), 36 | 'secret' => env('PUSHER_APP_SECRET'), 37 | 'app_id' => env('PUSHER_APP_ID'), 38 | 'options' => [ 39 | 'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', 40 | 'port' => env('PUSHER_PORT', 443), 41 | 'scheme' => env('PUSHER_SCHEME', 'https'), 42 | 'encrypted' => true, 43 | 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', 44 | ], 45 | 'client_options' => [ 46 | // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html 47 | ], 48 | ], 49 | 50 | 'ably' => [ 51 | 'driver' => 'ably', 52 | 'key' => env('ABLY_KEY'), 53 | ], 54 | 55 | 'redis' => [ 56 | 'driver' => 'redis', 57 | 'connection' => 'default', 58 | ], 59 | 60 | 'log' => [ 61 | 'driver' => 'log', 62 | ], 63 | 64 | 'null' => [ 65 | 'driver' => 'null', 66 | ], 67 | 68 | ], 69 | 70 | ]; 71 | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_DRIVER', 'file'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Cache Stores 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the cache "stores" for your application as 26 | | well as their drivers. You may even define multiple stores for the 27 | | same cache driver to group types of items stored in your caches. 28 | | 29 | | Supported drivers: "apc", "array", "database", "file", 30 | | "memcached", "redis", "dynamodb", "octane", "null" 31 | | 32 | */ 33 | 34 | 'stores' => [ 35 | 36 | 'apc' => [ 37 | 'driver' => 'apc', 38 | ], 39 | 40 | 'array' => [ 41 | 'driver' => 'array', 42 | 'serialize' => false, 43 | ], 44 | 45 | 'database' => [ 46 | 'driver' => 'database', 47 | 'table' => 'cache', 48 | 'connection' => null, 49 | 'lock_connection' => null, 50 | ], 51 | 52 | 'file' => [ 53 | 'driver' => 'file', 54 | 'path' => storage_path('framework/cache/data'), 55 | ], 56 | 57 | 'memcached' => [ 58 | 'driver' => 'memcached', 59 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 60 | 'sasl' => [ 61 | env('MEMCACHED_USERNAME'), 62 | env('MEMCACHED_PASSWORD'), 63 | ], 64 | 'options' => [ 65 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 66 | ], 67 | 'servers' => [ 68 | [ 69 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 70 | 'port' => env('MEMCACHED_PORT', 11211), 71 | 'weight' => 100, 72 | ], 73 | ], 74 | ], 75 | 76 | 'redis' => [ 77 | 'driver' => 'redis', 78 | 'connection' => 'cache', 79 | 'lock_connection' => 'default', 80 | ], 81 | 82 | 'dynamodb' => [ 83 | 'driver' => 'dynamodb', 84 | 'key' => env('AWS_ACCESS_KEY_ID'), 85 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 86 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 87 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 88 | 'endpoint' => env('DYNAMODB_ENDPOINT'), 89 | ], 90 | 91 | 'octane' => [ 92 | 'driver' => 'octane', 93 | ], 94 | 95 | ], 96 | 97 | /* 98 | |-------------------------------------------------------------------------- 99 | | Cache Key Prefix 100 | |-------------------------------------------------------------------------- 101 | | 102 | | When utilizing the APC, database, memcached, Redis, or DynamoDB cache 103 | | stores there might be other applications using the same cache. For 104 | | that reason, you may prefix every cache key to avoid collisions. 105 | | 106 | */ 107 | 108 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), 109 | 110 | ]; 111 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['api/*', 'sanctum/csrf-cookie'], 19 | 20 | 'allowed_methods' => ['*'], 21 | 22 | 'allowed_origins' => ['*'], 23 | 24 | 'allowed_origins_patterns' => [], 25 | 26 | 'allowed_headers' => ['*'], 27 | 28 | 'exposed_headers' => [], 29 | 30 | 'max_age' => 0, 31 | 32 | 'supports_credentials' => false, 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DISK', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure as many filesystem "disks" as you wish, and you 24 | | may even configure multiple disks of the same driver. Defaults have 25 | | been set up for each driver as an example of the required values. 26 | | 27 | | Supported Drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app'), 36 | 'throw' => false, 37 | ], 38 | 39 | 'public' => [ 40 | 'driver' => 'local', 41 | 'root' => storage_path('app/public'), 42 | 'url' => env('APP_URL').'/storage', 43 | 'visibility' => 'public', 44 | 'throw' => false, 45 | ], 46 | 47 | 's3' => [ 48 | 'driver' => 's3', 49 | 'key' => env('AWS_ACCESS_KEY_ID'), 50 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 51 | 'region' => env('AWS_DEFAULT_REGION'), 52 | 'bucket' => env('AWS_BUCKET'), 53 | 'url' => env('AWS_URL'), 54 | 'endpoint' => env('AWS_ENDPOINT'), 55 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 56 | 'throw' => false, 57 | ], 58 | 59 | ], 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Symbolic Links 64 | |-------------------------------------------------------------------------- 65 | | 66 | | Here you may configure the symbolic links that will be created when the 67 | | `storage:link` Artisan command is executed. The array keys should be 68 | | the locations of the links and the values should be their targets. 69 | | 70 | */ 71 | 72 | 'links' => [ 73 | public_path('storage') => storage_path('app/public'), 74 | 75 | // 创建 storage 对应的软连接 76 | public_path('uploads') => storage_path('uploads') 77 | ], 78 | 79 | ]; 80 | -------------------------------------------------------------------------------- /config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Bcrypt Options 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify the configuration options that should be used when 26 | | passwords are hashed using the Bcrypt algorithm. This will allow you 27 | | to control the amount of time it takes to hash the given password. 28 | | 29 | */ 30 | 31 | 'bcrypt' => [ 32 | 'rounds' => env('BCRYPT_ROUNDS', 10), 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Argon Options 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here you may specify the configuration options that should be used when 41 | | passwords are hashed using the Argon algorithm. These will allow you 42 | | to control the amount of time it takes to hash the given password. 43 | | 44 | */ 45 | 46 | 'argon' => [ 47 | 'memory' => 65536, 48 | 'threads' => 1, 49 | 'time' => 4, 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'sync'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection information for each server that 24 | | is used by your application. A default configuration has been added 25 | | for each back-end shipped with Laravel. You are free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'table' => 'jobs', 40 | 'queue' => 'default', 41 | 'retry_after' => 90, 42 | 'after_commit' => false, 43 | ], 44 | 45 | 'beanstalkd' => [ 46 | 'driver' => 'beanstalkd', 47 | 'host' => 'localhost', 48 | 'queue' => 'default', 49 | 'retry_after' => 90, 50 | 'block_for' => 0, 51 | 'after_commit' => false, 52 | ], 53 | 54 | 'sqs' => [ 55 | 'driver' => 'sqs', 56 | 'key' => env('AWS_ACCESS_KEY_ID'), 57 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 58 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 59 | 'queue' => env('SQS_QUEUE', 'default'), 60 | 'suffix' => env('SQS_SUFFIX'), 61 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 62 | 'after_commit' => false, 63 | ], 64 | 65 | 'redis' => [ 66 | 'driver' => 'redis', 67 | 'connection' => 'default', 68 | 'queue' => env('REDIS_QUEUE', 'default'), 69 | 'retry_after' => 90, 70 | 'block_for' => null, 71 | 'after_commit' => false, 72 | ], 73 | 74 | ], 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | Failed Queue Jobs 79 | |-------------------------------------------------------------------------- 80 | | 81 | | These options configure the behavior of failed queue job logging so you 82 | | can control which database and table are used to store the jobs that 83 | | have failed. You may change them to any database / table you wish. 84 | | 85 | */ 86 | 87 | 'failed' => [ 88 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 89 | 'database' => env('DB_CONNECTION', 'mysql'), 90 | 'table' => 'failed_jobs', 91 | ], 92 | 93 | ]; 94 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 21 | 'scheme' => 'https', 22 | ], 23 | 24 | 'postmark' => [ 25 | 'token' => env('POSTMARK_TOKEN'), 26 | ], 27 | 28 | 'ses' => [ 29 | 'key' => env('AWS_ACCESS_KEY_ID'), 30 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 31 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 32 | ], 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class UserFactory extends Factory 12 | { 13 | /** 14 | * Define the model's default state. 15 | * 16 | * @return array 17 | */ 18 | public function definition() 19 | { 20 | return [ 21 | 'name' => fake()->name(), 22 | 'email' => fake()->unique()->safeEmail(), 23 | 'email_verified_at' => now(), 24 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 25 | 'remember_token' => Str::random(10), 26 | ]; 27 | } 28 | 29 | /** 30 | * Indicate that the model's email address should be unverified. 31 | * 32 | * @return static 33 | */ 34 | public function unverified() 35 | { 36 | return $this->state(fn (array $attributes) => [ 37 | 'email_verified_at' => null, 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 19 | $table->string('uuid')->unique(); 20 | $table->text('connection'); 21 | $table->text('queue'); 22 | $table->longText('payload'); 23 | $table->longText('exception'); 24 | $table->timestamp('failed_at')->useCurrent(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('failed_jobs'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->morphs('tokenable'); 19 | $table->string('name'); 20 | $table->string('token', 64)->unique(); 21 | $table->text('abilities')->nullable(); 22 | $table->timestamp('last_used_at')->nullable(); 23 | $table->timestamp('expires_at')->nullable(); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('personal_access_tokens'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 18 | 19 | // \App\Models\User::factory()->create([ 20 | // 'name' => 'Test User', 21 | // 'email' => 'test@example.com', 22 | // ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/seeders/good.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'password' => 'The provided password is incorrect.', 18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset!', 17 | 'sent' => 'We have emailed your password reset link!', 18 | 'throttled' => 'Please wait before retrying.', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that email address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /modules/Common/Http/Controllers/OptionController.php: -------------------------------------------------------------------------------- 1 | make($name)->get(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /modules/Common/Http/Controllers/UploadController.php: -------------------------------------------------------------------------------- 1 | upload($request->file('file')); 22 | } 23 | 24 | /** 25 | * image 26 | * 27 | * @param Request $request 28 | * @param Uploader $uploader 29 | * @return array 30 | */ 31 | public function image(Request $request, Uploader $uploader): array 32 | { 33 | return $uploader->upload($request->file('image')); 34 | } 35 | 36 | 37 | /** 38 | * oss upload 39 | * 40 | * @param OssUpload $ossUpload 41 | * @return array 42 | */ 43 | public function oss(OssUpload $ossUpload): array 44 | { 45 | return $ossUpload->config(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /modules/Common/Providers/CommonServiceProvider.php: -------------------------------------------------------------------------------- 1 | 'layout', 16 | 'value' => '/layout/index.vue', 17 | ], 18 | ]; 19 | 20 | public function get(): array 21 | { 22 | try { 23 | $viewRootPath = config('catch.views_path'); 24 | 25 | if ($module = request()->get('module')) { 26 | if (!File::exists($viewRootPath . $module . DIRECTORY_SEPARATOR)) { 27 | return []; 28 | } 29 | 30 | $components = File::allFiles($viewRootPath . $module . DIRECTORY_SEPARATOR); 31 | 32 | foreach ($components as $component) { 33 | // 过滤非 vue 文件 34 | if ($component->getExtension() !== 'vue') { 35 | continue; 36 | } 37 | 38 | $_component = Str::of($component->getPathname()) 39 | ->replace($viewRootPath, '') 40 | ->explode(DIRECTORY_SEPARATOR); 41 | 42 | $_component->shift(1); 43 | 44 | $this->components[] = [ 45 | 'label' => Str::of($_component->implode('/'))->replace('.vue', ''), 46 | 47 | 'value' => Str::of($component)->replace($viewRootPath, '')->prepend('/'), 48 | ]; 49 | } 50 | } 51 | 52 | return $this->components; 53 | } catch (\Throwable $exception) { 54 | return []; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /modules/Common/Repository/Options/Controllers.php: -------------------------------------------------------------------------------- 1 | get('module')) { 16 | $controllerFiles = File::glob(CatchAdmin::getModuleControllerPath($module).'*.php'); 17 | 18 | foreach ($controllerFiles as $controllerFile) { 19 | $controllers[] = [ 20 | 'label' => Str::of(File::name($controllerFile))->lcfirst()->remove('Controller'), 21 | 22 | 'value' => Str::of(File::name($controllerFile))->lcfirst()->remove('Controller'), 23 | ]; 24 | } 25 | } 26 | 27 | return $controllers; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /modules/Common/Repository/Options/DataRange.php: -------------------------------------------------------------------------------- 1 | DataRangeEnum::All_Data->name(), 14 | 'value' => DataRangeEnum::All_Data->value() 15 | ], 16 | 17 | [ 18 | 'label' => DataRangeEnum::Personal_Choose->name(), 19 | 'value' => DataRangeEnum::Personal_Choose->value() 20 | ], 21 | 22 | [ 23 | 'label' => DataRangeEnum::Personal_Data->name(), 24 | 'value' => DataRangeEnum::Personal_Data->value() 25 | ], 26 | 27 | [ 28 | 'label' => DataRangeEnum::Department_Data->name(), 29 | 'value' => DataRangeEnum::Department_Data->value() 30 | ], 31 | 32 | [ 33 | 'label' => DataRangeEnum::Department_DOWN_Data->name(), 34 | 'value' => DataRangeEnum::Department_DOWN_Data->value() 35 | ] 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/Common/Repository/Options/Factory.php: -------------------------------------------------------------------------------- 1 | ucfirst()->toString(); 19 | 20 | $class = new $className(); 21 | 22 | if (! $class instanceof OptionInterface) { 23 | throw new Exception('option must be implement [OptionInterface]'); 24 | } 25 | 26 | return $class; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /modules/Common/Repository/Options/Modules.php: -------------------------------------------------------------------------------- 1 | all([]) 14 | 15 | ->each(function ($module) use (&$modules) { 16 | $modules[] = [ 17 | 'label' => $module['title'], 18 | 19 | 'value' => $module['name'] 20 | ]; 21 | }); 22 | 23 | return $modules; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /modules/Common/Repository/Options/OptionInterface.php: -------------------------------------------------------------------------------- 1 | getDatabaseName(); 17 | $tablePrefix = $connection->getTablePrefix(); 18 | 19 | foreach (Schema::getTables($databaseName) as $table) { 20 | $tableName = Str::of($table['name'])->replaceStart($tablePrefix, ''); 21 | 22 | $options[] = [ 23 | 'label' => $tableName . "\t\t\t\t" . $table['comment'], 24 | 'value' => $tableName, 25 | ]; 26 | } 27 | 28 | return $options; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /modules/Common/Repository/Options/Status.php: -------------------------------------------------------------------------------- 1 | StatusEnum::Enable->name(), 14 | 'value' => StatusEnum::Enable->value() 15 | ], 16 | 17 | [ 18 | 'label' => StatusEnum::Disable->name(), 19 | 'value' => StatusEnum::Disable->value() 20 | ] 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /modules/Common/Support/Upload/OssUpload.php: -------------------------------------------------------------------------------- 1 | accessKeySecret = config('common.upload.oss.access_secret'); 29 | 30 | $this->accessKeyId = config('common.upload.oss.access_id'); 31 | 32 | $this->dir = date('Y-m-d') . '/'; 33 | 34 | $this->endpoint = config('common.upload.oss.endpoint'); 35 | 36 | $this->maxSize = config('common.upload.max_size'); 37 | 38 | $this->bucket = config('common.upload.oss.bucket'); 39 | } 40 | 41 | /** 42 | * config 43 | * 44 | * @return array 45 | */ 46 | public function config(): array 47 | { 48 | return [ 49 | 'bucket' => $this->bucket, 50 | 51 | 'accessKeyId' => $this->accessKeyId, 52 | 53 | 'host' => sprintf('https://%s.%s', $this->bucket, $this->endpoint), 54 | 55 | 'policy' => $this->policy(), 56 | 57 | 'signature' => $this->signature(), 58 | 59 | 'expire' => $this->getExpiration(), 60 | 61 | 'dir' => $this->dir, 62 | 63 | 'url' => $this->endpoint . $this->dir 64 | ]; 65 | } 66 | 67 | /** 68 | * 69 | * @return string 70 | */ 71 | protected function policy(): string 72 | { 73 | return base64_encode(json_encode([ 74 | 'expiration' => $this->getExpiration(), 75 | 'conditions' => [ 76 | ['starts-with', '$key', $this->dir], 77 | ['content-length-range', 0, $this->maxSize] 78 | ] 79 | ])); 80 | } 81 | 82 | /** 83 | * 84 | * @return string 85 | */ 86 | protected function signature(): string 87 | { 88 | return base64_encode( 89 | \hash_hmac('sha1', $this->policy(), $this->accessKeySecret, true) 90 | ); 91 | } 92 | 93 | /** 94 | * 95 | * @return string 96 | */ 97 | protected function getExpiration(): string 98 | { 99 | return date('Y-m-d\TH:i:s\Z', time() + $this->expire); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /modules/Common/Support/Upload/Uploader.php: -------------------------------------------------------------------------------- 1 | getDriver()->setUploadedFile($file)->upload(); 30 | } catch (\Exception $exception) { 31 | throw new FailedException($exception->getMessage()); 32 | } 33 | } 34 | 35 | 36 | /** 37 | * get driver 38 | * 39 | */ 40 | public function getDriver() 41 | { 42 | $drivers = $this->getDrivers(); 43 | 44 | $driver = $drivers[$this->driver] ?? null; 45 | 46 | if (! $driver) { 47 | throw new FailedException('Upload Driver Not Found'); 48 | } 49 | 50 | return app($driver); 51 | } 52 | 53 | 54 | /** 55 | * set driver 56 | * 57 | * @param string $driver 58 | * @return $this 59 | */ 60 | public function setDriver(string $driver): static 61 | { 62 | $this->driver = $driver; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * get drivers 69 | * 70 | * @return string[] 71 | */ 72 | public function getDrivers(): array 73 | { 74 | return [ 75 | 'local' => LocalUpload::class 76 | ]; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /modules/Common/Support/Upload/Uses/LocalUpload.php: -------------------------------------------------------------------------------- 1 | addUrl($this->getUploadPath()); 19 | } 20 | 21 | /** 22 | * app url 23 | * 24 | * @param $path 25 | * @return mixed 26 | */ 27 | protected function addUrl($path): mixed 28 | { 29 | $path['path'] = config('app.url') . '/'. 30 | 31 | Str::of($path['path'])->replace('\\', '/')->toString(); 32 | 33 | return $path; 34 | } 35 | 36 | 37 | /** 38 | * local upload 39 | * 40 | * @return string 41 | */ 42 | protected function localUpload(): string 43 | { 44 | $this->checkSize(); 45 | 46 | $storePath = 'uploads' . DIRECTORY_SEPARATOR . $this->getUploadedFileMimeType() . DIRECTORY_SEPARATOR . date('Y-m-d', time()); 47 | 48 | $filename = $this->generateImageName($this->getUploadedFileExt()); 49 | 50 | Storage::build([ 51 | 'driver' => 'local', 52 | 'root' => $storePath 53 | ])->put($filename, $this->file->getContent()); 54 | 55 | return $storePath . DIRECTORY_SEPARATOR . $filename; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /modules/Common/config/upload.php: -------------------------------------------------------------------------------- 1 | 10 * 1024 * 1024, 5 | 6 | // oss 配置 7 | 'oss' => [ 8 | 'bucket' => env('ALIOSS_BUCKET'), 9 | 10 | 'access_id' => env('ALIOSS_ACCESS_ID'), 11 | 12 | 'access_secret' => env('ALIOSS_ACCESS_SECRET'), 13 | 14 | 'endpoint' => env('ALIOSS_ENDPOINT'), 15 | 16 | 'dir' => env('ALIOSS_UPLOAD_DIR') 17 | ], 18 | ]; 19 | -------------------------------------------------------------------------------- /modules/Common/routes/route.php: -------------------------------------------------------------------------------- 1 | group(function (){ 21 | Route::post('upload/file', 'file'); 22 | Route::post('upload/image', 'image'); 23 | // get oss signature 24 | Route::get('upload/oss', 'oss'); 25 | }); 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /modules/Develop/Http/Controllers/GenerateController.php: -------------------------------------------------------------------------------- 1 | setParams($request->all())->generate(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /modules/Develop/Http/Controllers/ModuleController.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 26 | } 27 | 28 | /** 29 | * index 30 | * 31 | * @param Request $request 32 | * @return Collection 33 | */ 34 | public function index(Request $request): Collection 35 | { 36 | return $this->repository->all($request->all()); 37 | } 38 | 39 | /** 40 | * store 41 | * 42 | * @param Request $request 43 | * @return bool|int 44 | */ 45 | public function store(Request $request): bool|int 46 | { 47 | return $this->repository->create($request->all()); 48 | } 49 | 50 | /** 51 | * show 52 | * 53 | * @param string $name 54 | * @return Collection 55 | * @throws \Exception 56 | */ 57 | public function show(mixed $name): Collection 58 | { 59 | return $this->repository->show($name); 60 | } 61 | 62 | /** 63 | * update 64 | * 65 | * @param $name 66 | * @param Request $request 67 | * @return bool|int 68 | */ 69 | public function update($name, Request $request): bool|int 70 | { 71 | return $this->repository->update($name, $request->all()); 72 | } 73 | 74 | 75 | /** 76 | * update 77 | * 78 | * @param $name 79 | * @return bool|int 80 | */ 81 | public function enable($name): bool|int 82 | { 83 | return $this->repository->disOrEnable($name); 84 | } 85 | 86 | /** 87 | * destroy 88 | * 89 | * @param $name 90 | * @return bool|int 91 | * @throws \Exception 92 | */ 93 | public function destroy($name): bool|int 94 | { 95 | return $this->repository->delete($name); 96 | } 97 | 98 | /** 99 | * install 100 | * 101 | * @param Request $request 102 | * @param ModuleRepositoryInterface $moduleRepository 103 | * @return true 104 | */ 105 | public function install(Request $request, ModuleRepositoryInterface $moduleRepository) 106 | { 107 | if ($moduleRepository->all()->pluck('name')->contains($request->get('title'))) { 108 | throw new FailedException('模块已安装,无法再次安装'); 109 | } 110 | 111 | $moduleInstall = new ModuleInstall($request->get('type')); 112 | 113 | $moduleInstall->install($request->all()); 114 | 115 | return true; 116 | } 117 | 118 | /** 119 | * upload 120 | * 121 | * @param Request $request 122 | * @return string 123 | */ 124 | public function upload(Request $request) 125 | { $file = $request->file('file'); 126 | 127 | Storage::build([ 128 | 'driver' => 'local', 129 | 'root' => storage_path('app') 130 | ])->put($file->getClientOriginalName(), $file->getContent()); 131 | 132 | return storage_path('app') . DIRECTORY_SEPARATOR . $file->getClientOriginalName(); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /modules/Develop/Http/Controllers/SchemaController.php: -------------------------------------------------------------------------------- 1 | schemas->getList(); 25 | } 26 | 27 | /** 28 | * store 29 | * 30 | * @param Request $request 31 | * @throws \Exception 32 | * @return bool 33 | */ 34 | public function store(Request $request) 35 | { 36 | return $this->schemas->storeBy($request->all()); 37 | } 38 | 39 | /** 40 | * show 41 | * 42 | * @param $id 43 | * @return mixed 44 | */ 45 | public function show($id) 46 | { 47 | return $this->schemas->show($id); 48 | } 49 | 50 | 51 | /** 52 | * destroy 53 | * 54 | * @param $id 55 | * @return bool|null 56 | */ 57 | public function destroy($id) 58 | { 59 | return $this->schemas->deleteBy($id); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /modules/Develop/Listeners/CreatedListener.php: -------------------------------------------------------------------------------- 1 | module; 29 | 30 | (new Module( 31 | $module['path'], 32 | $module['dirs']['controllers'], 33 | $module['dirs']['models'], 34 | $module['dirs']['requests'], 35 | $module['dirs']['database'] 36 | ) 37 | )->create(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /modules/Develop/Listeners/DeletedListener.php: -------------------------------------------------------------------------------- 1 | module['path']); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /modules/Develop/Models/Schemas.php: -------------------------------------------------------------------------------- 1 | 'like', 'name' => 'like']; 35 | 36 | /** 37 | * @var string[] 38 | */ 39 | protected $casts = [ 40 | 'is_soft_delete' => Status::class 41 | ]; 42 | 43 | /** 44 | * 45 | * @param array $data 46 | * @return boolean 47 | * @throws Exception 48 | */ 49 | public function storeBy(array $data): bool 50 | { 51 | // 从已有 schema 中选择 52 | if (isset($data['schema_name'])) { 53 | $columns = SchemaFacade::getColumnListing($data['schema_name']); 54 | 55 | return parent::storeBy([ 56 | 'module' => $data['module'], 57 | 'name' => $data['schema_name'], 58 | 'columns' => implode(',', $columns), 59 | 'is_soft_delete' => isset($columns['deleted_at']) ? Status::Enable : Status::Disable, 60 | ]); 61 | } 62 | 63 | $schema = $data['schema']; 64 | 65 | $structures = $data['structures']; 66 | 67 | $schemaId = parent::storeBy([ 68 | 'module' => $schema['module'], 69 | 70 | 'name' => $schema['name'], 71 | 72 | 'columns' => implode(',', array_column($structures, 'field')), 73 | 74 | 'is_soft_delete' => $schema['deleted_at'] ? Status::Enable : Status::Disable 75 | ], true); 76 | 77 | try { 78 | $schemaCreate = new Schema($schema['name'], $schema['engine'], $schema['charset'], $schema['collection'], $schema['comment']); 79 | 80 | $schemaCreate->setStructures($structures) 81 | ->setModule($schema['module']) 82 | ->setCreatedAt($schema['created_at']) 83 | ->setCreatorId($schema['creator_id']) 84 | ->setUpdatedAt($schema['updated_at']) 85 | ->setDeletedAt($schema['deleted_at']) 86 | ->create(); 87 | } catch (Exception $e) { 88 | parent::deleteBy($schemaId, true); 89 | 90 | throw $e; 91 | } 92 | 93 | return true; 94 | } 95 | 96 | 97 | /** 98 | * @param $id 99 | * @return Model 100 | */ 101 | public function show($id): Model 102 | { 103 | $schema = parent::firstBy($id); 104 | 105 | foreach (SchemaFacade::getColumns($schema->name) as $column) { 106 | $columns[] = [ 107 | 'name' => $column['name'], 108 | 'type' => $column['type_name'], 109 | 'nullable' => $column['nullable'], 110 | 'default' => $column['default'], 111 | 'comment' => $column['comment'], 112 | ]; 113 | } 114 | 115 | $schema->columns = $columns; 116 | 117 | return $schema; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /modules/Develop/Providers/DevelopServiceProvider.php: -------------------------------------------------------------------------------- 1 | CreatedListener::class, 16 | 17 | Deleted::class => DeletedListener::class 18 | ]; 19 | 20 | /** 21 | * route path 22 | * 23 | * @return string|array 24 | */ 25 | public function moduleName(): string|array 26 | { 27 | // TODO: Implement path() method. 28 | return 'develop'; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /modules/Develop/Providers/Install.php: -------------------------------------------------------------------------------- 1 | module).$this->getControllerName().$this->ext; 41 | } 42 | 43 | public function getContent(): string|bool 44 | { 45 | // TODO: Implement getContent() method. 46 | return Str::of(File::get($this->getControllerStub()))->replace($this->replace, [ 47 | $this->getControllerNamespace(), 48 | 49 | $this->getUses(), 50 | 51 | $this->getControllerName(), 52 | 53 | $this->model, 54 | 55 | $this->request ?: 'Request' 56 | ])->toString(); 57 | } 58 | 59 | /** 60 | * get controller name 61 | * 62 | * @return string 63 | */ 64 | protected function getControllerName(): string 65 | { 66 | return Str::of($this->controller)->whenContains('Controller', function ($value) { 67 | return Str::of($value)->ucfirst(); 68 | }, function ($value) { 69 | return Str::of($value)->append('Controller')->ucfirst(); 70 | })->toString(); 71 | } 72 | 73 | /** 74 | * get uses 75 | * 76 | * @return string 77 | */ 78 | protected function getUses(): string 79 | { 80 | return Str::of('use ') 81 | ->append(CatchAdmin::getModuleModelNamespace($this->module).$this->model) 82 | ->append(';') 83 | ->newLine() 84 | ->append('use ') 85 | ->when($this->request, function ($str) { 86 | return $str->append(CatchAdmin::getModuleRequestNamespace($this->module).$this->request); 87 | }, function ($str) { 88 | return $str->append("Illuminate\Http\Request"); 89 | })->append(';')->newLine()->toString(); 90 | } 91 | 92 | /** 93 | * get controller stub 94 | * 95 | * @return string 96 | */ 97 | protected function getControllerStub(): string 98 | { 99 | return dirname(__DIR__).DIRECTORY_SEPARATOR.'stubs'.DIRECTORY_SEPARATOR.'controller.stub'; 100 | } 101 | 102 | /** 103 | * get controller namespace 104 | * 105 | * @return string 106 | */ 107 | protected function getControllerNamespace(): string 108 | { 109 | return Str::of(CatchAdmin::getModuleControllerNamespace($this->module))->rtrim('\\')->append(';')->toString(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/Create/Creator.php: -------------------------------------------------------------------------------- 1 | put(); 39 | } 40 | 41 | /** 42 | * the file which content put in 43 | * 44 | * @return string 45 | */ 46 | abstract public function getFile(): string; 47 | 48 | /** 49 | * get content 50 | * @return string|bool 51 | */ 52 | abstract public function getContent(): string|bool; 53 | 54 | /** 55 | * @return string|bool 56 | * @throws FileNotFoundException 57 | */ 58 | protected function put(): string|bool 59 | { 60 | if (! $this->getContent()) { 61 | return false; 62 | } 63 | 64 | $this->file = $this->getFile(); 65 | 66 | File::put($this->file, $this->getContent()); 67 | 68 | if (File::exists($this->file)) { 69 | return $this->file; 70 | } 71 | 72 | throw new FileNotFoundException("create [$this->file] failed"); 73 | } 74 | 75 | 76 | /** 77 | * set ext 78 | * 79 | * @param string $ext 80 | * @return $this 81 | */ 82 | protected function setExt(string $ext): static 83 | { 84 | $this->ext = $ext; 85 | 86 | return $this; 87 | } 88 | 89 | 90 | /** 91 | * set module 92 | * 93 | * @param string $module 94 | * @return $this 95 | */ 96 | public function setModule(string $module): static 97 | { 98 | $this->module = $module; 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * get file 105 | * 106 | * @return string 107 | */ 108 | public function getGenerateFile(): string 109 | { 110 | return $this->file; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/Create/Request.php: -------------------------------------------------------------------------------- 1 | module).$this->getRequestName().$this->ext; 37 | } 38 | 39 | /** 40 | * get content 41 | * 42 | * @return string|bool 43 | */ 44 | public function getContent(): string|bool 45 | { 46 | $rule = $this->getRulesString(); 47 | 48 | if (! $rule) { 49 | return false; 50 | } 51 | 52 | return Str::of( 53 | File::get(dirname(__DIR__).DIRECTORY_SEPARATOR.'stubs'.DIRECTORY_SEPARATOR.'request.stub') 54 | )->replace($this->replace, [$this->getNamespace(), $this->getRequestName(), $rule])->toString(); 55 | } 56 | 57 | /** 58 | * get namespace 59 | * 60 | * @return string 61 | */ 62 | protected function getNamespace(): string 63 | { 64 | return Str::of(CatchAdmin::getModuleRequestNamespace($this->module))->rtrim('\\')->append(';')->toString(); 65 | } 66 | 67 | /** 68 | * get request name 69 | * 70 | * @return ?string 71 | */ 72 | public function getRequestName(): ?string 73 | { 74 | if ($this->getRules()) { 75 | return Str::of($this->controller)->remove('Controller')->append('Request')->ucfirst()->toString(); 76 | } 77 | 78 | return null; 79 | } 80 | 81 | /** 82 | * get rule 83 | * 84 | * @return string|bool 85 | */ 86 | public function getRulesString(): string|bool 87 | { 88 | $rules = $this->getRules(); 89 | 90 | if (! count($rules)) { 91 | return false; 92 | } 93 | 94 | $rule = Str::of(''); 95 | 96 | foreach ($rules as $field => $validates) { 97 | $rule = $rule->append("'{$field}'") 98 | ->append(' => ') 99 | ->append('\'') 100 | ->append(Arr::join($validates, '|')) 101 | ->append('\',') 102 | ->newLine(); 103 | } 104 | 105 | return $rule->toString(); 106 | } 107 | 108 | /** 109 | * get rules 110 | * 111 | * @return array 112 | */ 113 | protected function getRules(): array 114 | { 115 | $rules = []; 116 | 117 | foreach ($this->structures as $structure) { 118 | if ($structure['field'] && count($structure['validates'])) { 119 | $rules[$structure['field']] = $structure['validates']; 120 | } 121 | } 122 | 123 | return $rules; 124 | } 125 | 126 | /** 127 | * set structures 128 | * 129 | * @param array $structures 130 | * @return $this 131 | */ 132 | public function setStructures(array $structures): static 133 | { 134 | $this->structures = $structures; 135 | 136 | return $this; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/Create/Route.php: -------------------------------------------------------------------------------- 1 | module); 31 | } 32 | 33 | /** 34 | * get content 35 | * 36 | * @return string 37 | */ 38 | public function getContent(): string 39 | { 40 | // route 主要添加两个点 41 | // use Controller 42 | // 添加路由 43 | $route = Str::of(''); 44 | 45 | $originContent = File::get(CatchAdmin::getModuleRoutePath($this->module)); 46 | 47 | // 如果已经有 controller,就不再追加路由 48 | if (Str::of($originContent)->contains($this->getUserController())) { 49 | return $originContent; 50 | } 51 | 52 | File::lines(CatchAdmin::getModuleRoutePath($this->module)) 53 | ->each(function ($line) use (&$route) { 54 | if (Str::of($line)->contains('Route::prefix')) { 55 | $route = $route->trim(PHP_EOL) 56 | ->newLine() 57 | ->append($this->getUserController()) 58 | ->append(';') 59 | ->newLine(2) 60 | ->append($line) 61 | ->newLine(); 62 | } else { 63 | $route = $route->append($line)->newLine(); 64 | } 65 | }); 66 | 67 | $apiResource = "Route::apiResource('{api}', {controller}::class);"; 68 | 69 | return Str::of($route->toString())->replace( 70 | ['{module}', '//next'], 71 | [ 72 | lcfirst($this->module), 73 | Str::of($apiResource)->replace(['{api}', '{controller}'], [$this->getApiString(), $this->getControllerName()]) 74 | ->prepend("\t") 75 | ->prepend(PHP_EOL) 76 | ->newLine()->append("\t//next")] 77 | )->toString(); 78 | } 79 | 80 | /** 81 | * get api 82 | * 83 | * @return string 84 | */ 85 | public function getApiString(): string 86 | { 87 | return Str::of($this->getControllerName())->remove('Controller')->snake('_')->replace('_', '/')->toString(); 88 | } 89 | 90 | /** 91 | * get api route 92 | * 93 | * @return string 94 | */ 95 | public function getApiRute(): string 96 | { 97 | return lcfirst($this->module).'/'.$this->getApiString(); 98 | } 99 | 100 | /** 101 | * use controller 102 | * 103 | * @return string 104 | */ 105 | protected function getUserController(): string 106 | { 107 | return 'use '.CatchAdmin::getModuleControllerNamespace($this->module).$this->getControllerName(); 108 | } 109 | 110 | /** 111 | * get controller name 112 | * 113 | * @return string 114 | */ 115 | protected function getControllerName(): string 116 | { 117 | return Str::of($this->controller)->whenContains('Controller', function ($value) { 118 | return Str::of($value)->ucfirst(); 119 | }, function ($value) { 120 | return Str::of($value)->append('Controller')->ucfirst(); 121 | })->toString(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/Module.php: -------------------------------------------------------------------------------- 1 | controller) { 28 | CatchAdmin::getModuleControllerPath($this->module); 29 | } 30 | 31 | if ($this->models) { 32 | CatchAdmin::getModuleModelPath($this->module); 33 | } 34 | 35 | if ($this->requests) { 36 | CatchAdmin::getModuleRequestPath($this->module); 37 | } 38 | 39 | if ($this->database) { 40 | CatchAdmin::getModuleMigrationPath($this->module); 41 | CatchAdmin::getModuleSeederPath($this->module); 42 | } 43 | 44 | $this->createProvider(); 45 | 46 | $this->createRoute(); 47 | } 48 | 49 | 50 | /** 51 | * delete 52 | * 53 | * @return void 54 | */ 55 | public function delete(): void 56 | { 57 | } 58 | 59 | /** 60 | * create provider 61 | * 62 | * @return void 63 | */ 64 | protected function createProvider(): void 65 | { 66 | CatchAdmin::getModuleProviderPath($this->module); 67 | 68 | File::put( 69 | CatchAdmin::getModuleProviderPath($this->module).sprintf('%sServiceProvider.php', ucfirst($this->module)), 70 | Str::of( 71 | File::get(__DIR__.DIRECTORY_SEPARATOR.'stubs'.DIRECTORY_SEPARATOR.'provider.stub') 72 | )->replace(['{Module}', '{module}'], [ucfirst($this->module), $this->module]) 73 | ); 74 | } 75 | 76 | 77 | /** 78 | * create route 79 | * 80 | * @return void 81 | */ 82 | protected function createRoute(): void 83 | { 84 | $content = Str::of( 85 | File::get(__DIR__.DIRECTORY_SEPARATOR.'stubs'.DIRECTORY_SEPARATOR.'route.stub') 86 | )->replace(['{module}'], [lcfirst($this->module)]); 87 | 88 | File::put( 89 | CatchAdmin::getModuleRoutePath($this->module), 90 | $content 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/stubs/controller.stub: -------------------------------------------------------------------------------- 1 | model->getList(); 21 | } 22 | 23 | /** 24 | * @param Request $request 25 | * @return mixed 26 | */ 27 | public function store({request} $request) 28 | { 29 | return $this->model->storeBy($request->all()); 30 | } 31 | 32 | /** 33 | * @param $id 34 | * @return mixed 35 | */ 36 | public function show($id) 37 | { 38 | return $this->model->firstBy($id); 39 | } 40 | 41 | /** 42 | * @param Request $request 43 | * @param $id 44 | * @return mixed 45 | */ 46 | public function update($id, {request} $request) 47 | { 48 | return $this->model->updateBy($id, $request->all()); 49 | } 50 | 51 | /** 52 | * @param $id 53 | * @return mixed 54 | */ 55 | public function destroy($id) 56 | { 57 | return $this->model->deleteBy($id); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/stubs/migration.stub: -------------------------------------------------------------------------------- 1 | group(function(){ 6 | //next 7 | }); -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/stubs/vue/form.stub: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/stubs/vue/formItems/cascader.stub: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/stubs/vue/formItems/date.stub: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/stubs/vue/formItems/datetime.stub: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/stubs/vue/formItems/input-number.stub: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/stubs/vue/formItems/input.stub: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/stubs/vue/formItems/radio.stub: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ item.label }} 4 | 5 | 6 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/stubs/vue/formItems/rate.stub: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/stubs/vue/formItems/select.stub: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/stubs/vue/formItems/switch.stub: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/stubs/vue/formItems/textarea.stub: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/stubs/vue/formItems/tree-select.stub: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/stubs/vue/formItems/tree.stub: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/stubs/vue/formItems/upload.stub: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /modules/Develop/Support/Generate/stubs/vue/table.stub: -------------------------------------------------------------------------------- 1 | 27 | 28 | 48 | -------------------------------------------------------------------------------- /modules/Develop/Support/ModuleInstall.php: -------------------------------------------------------------------------------- 1 | type === self::NORMAL_INSTALL) { 28 | $this->installWithTitle($params['title']); 29 | } 30 | 31 | if ($this->type == self::ZIP_INSTALL) { 32 | $this->installWithZip($params['title'], $params['file']); 33 | } 34 | } catch (\Exception $e) { 35 | if ($this->type == self::ZIP_INSTALL) { 36 | CatchAdmin::deleteModulePath($params['title']); 37 | } 38 | 39 | throw new FailedException('安装失败: ' . $e->getMessage()); 40 | } 41 | } 42 | 43 | /** 44 | * 45 | * @param string $title 46 | */ 47 | protected function installWithTitle(string $title): void 48 | { 49 | try { 50 | $installer = CatchAdmin::getModuleInstaller($title); 51 | 52 | $installer->install(); 53 | } catch (\Exception|\Throwable $e) { 54 | // CatchAdmin::deleteModulePath($title); 55 | 56 | throw new FailedException('安装失败: ' . $e->getMessage()); 57 | } 58 | } 59 | 60 | /** 61 | * get 62 | * 63 | * @param string $title 64 | * @param string $zip 65 | */ 66 | protected function installWithZip(string $title, string $zip): void 67 | { 68 | $zipRepository = Zipper::make($zip)->getRepository(); 69 | 70 | $zipRepository->getArchive()->extractTo(CatchAdmin::getModulePath($title)); 71 | 72 | $this->installWithTitle($title); 73 | 74 | File::delete($zip); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /modules/Develop/database/migrations/Schemas.php: -------------------------------------------------------------------------------- 1 | increments('id'); 12 | 13 | $table->string('module')->nullable(false)->comment('模块名称'); 14 | 15 | $table->string('name')->nullable(false)->comment('schema 名称'); 16 | 17 | $table->string('columns', 2000)->nullable(false)->comment('字段'); 18 | 19 | $table->boolean('is_soft_delete')->default(1)->comment('1 是 2 否'); 20 | 21 | $table->createdAt(); 22 | 23 | $table->updatedAt(); 24 | 25 | $table->deletedAt(); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /modules/Develop/routes/route.php: -------------------------------------------------------------------------------- 1 | only(['index', 'show', 'store', 'destroy']); 17 | -------------------------------------------------------------------------------- /modules/Permissions/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaguarJack/catch-admin/3ca6cea6ad073754a5134685e54d5b75ece561a7/modules/Permissions/.DS_Store -------------------------------------------------------------------------------- /modules/Permissions/Enums/DataRange.php: -------------------------------------------------------------------------------- 1 | 1, 21 | self::Personal_Choose => 2, 22 | self::Personal_Data => 3, 23 | self::Department_Data => 4, 24 | self::Department_DOWN_Data => 5, 25 | }; 26 | } 27 | 28 | public function name(): string 29 | { 30 | // TODO: Implement name() method. 31 | return match ($this) { 32 | self::All_Data => '全部数据', 33 | self::Personal_Choose => '自定义数据', 34 | self::Personal_Data => '本人数据', 35 | self::Department_Data => '部门数据', 36 | self::Department_DOWN_Data => '部门及以下数据', 37 | }; 38 | } 39 | 40 | /** 41 | * assert value 42 | * 43 | * @param int $value 44 | * @return bool 45 | */ 46 | public function assert(int $value): bool 47 | { 48 | return $this->value === $value; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /modules/Permissions/Enums/MenuStatus.php: -------------------------------------------------------------------------------- 1 | 1, 17 | self::Hidden => 2, 18 | }; 19 | } 20 | 21 | public function name(): string 22 | { 23 | // TODO: Implement name() method. 24 | return match ($this) { 25 | self::Show => '显示', 26 | self::Hidden => '隐藏', 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /modules/Permissions/Enums/MenuType.php: -------------------------------------------------------------------------------- 1 | 1, 18 | self::Menu => 2, 19 | self::Action => 3, 20 | }; 21 | } 22 | 23 | public function name(): string 24 | { 25 | // TODO: Implement name() method. 26 | return match ($this) { 27 | self::Top => '目录类型', 28 | self::Menu => '菜单类型', 29 | self::Action => '按钮类型', 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /modules/Permissions/Exceptions/PermissionForbidden.php: -------------------------------------------------------------------------------- 1 | model->getList(); 24 | } 25 | 26 | public function store(Request $request) 27 | { 28 | return $this->model->storeBy($request->all()); 29 | } 30 | 31 | public function show($id) 32 | { 33 | return $this->model->firstBy($id); 34 | } 35 | 36 | public function update($id, Request $request) 37 | { 38 | return $this->model->updateBy($id, $request->all()); 39 | } 40 | 41 | public function destroy($id) 42 | { 43 | return $this->model->deleteBy($id); 44 | } 45 | 46 | public function enable($id) 47 | { 48 | return $this->model->toggleBy($id); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /modules/Permissions/Http/Controllers/JobsController.php: -------------------------------------------------------------------------------- 1 | model->getList(); 25 | } 26 | 27 | public function store(Request $request) 28 | { 29 | return $this->model->storeBy($request->all()); 30 | } 31 | 32 | public function show($id) 33 | { 34 | return $this->model->firstBy($id); 35 | } 36 | 37 | public function update($id, Request $request) 38 | { 39 | return $this->model->updateBy($id, $request->all()); 40 | } 41 | 42 | public function destroy($id) 43 | { 44 | return $this->model->deleteBy($id); 45 | } 46 | 47 | public function enable($id) 48 | { 49 | return $this->model->toggleBy($id); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /modules/Permissions/Http/Controllers/PermissionsController.php: -------------------------------------------------------------------------------- 1 | get('from') == 'role') { 31 | return $this->model->setBeforeGetList(function ($query){ 32 | return $query->orderByDesc('sort'); 33 | })->getList(); 34 | } 35 | 36 | return $this->model->setBeforeGetList(function ($query) { 37 | return $query->with('actions')->whereIn('type', [MenuType::Top->value(), MenuType::Menu->value()])->orderByDesc('sort'); 38 | })->getList(); 39 | } 40 | 41 | /** 42 | * 43 | * @param Request $request 44 | * @return mixed 45 | * @throws \ReflectionException 46 | */ 47 | public function store(Request $request) 48 | { 49 | return $this->model->storeBy($request->all()); 50 | } 51 | 52 | /** 53 | * 54 | * @param $id 55 | * @return Model|null 56 | */ 57 | public function show($id): ?Model 58 | { 59 | return $this->model->firstBy($id); 60 | } 61 | 62 | /** 63 | * 64 | * @param $id 65 | * @param Request $request 66 | * @return mixed 67 | */ 68 | public function update($id, Request $request): mixed 69 | { 70 | return $this->model->updateBy($id, $request->all()); 71 | } 72 | 73 | /** 74 | * 75 | * @param $id 76 | * @return mixed 77 | */ 78 | public function destroy($id) 79 | { 80 | if ($this->model->where($this->model->getParentIdColumn(), $id)->first()) { 81 | throw new FailedException('无法进行删除,请先删除子级'); 82 | } 83 | 84 | return $this->model->deleteBy($id); 85 | } 86 | 87 | /** 88 | * enable 89 | * 90 | * @param $id 91 | * @return bool 92 | */ 93 | public function enable($id): bool 94 | { 95 | return $this->model->toggleBy($id, 'hidden'); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /modules/Permissions/Http/Controllers/RolesController.php: -------------------------------------------------------------------------------- 1 | model->setBeforeGetList(function ($query) { 33 | return $query->with(['permissions' => function ($query) { 34 | $query->select('id'); 35 | }]); 36 | })->getList(); 37 | } 38 | 39 | /** 40 | * 41 | * @param RoleRequest $request 42 | * @return bool 43 | */ 44 | public function store(RoleRequest $request) 45 | { 46 | $data = $request->all(); 47 | if (!isset($data['data_range'])) { 48 | $data['data_range'] = 0; 49 | } else { 50 | $data['data_range'] = (int)$data['data_range']; 51 | if (!DataRange::Personal_Choose->assert($data['data_range'])) { 52 | $data['departments'] = []; 53 | } 54 | } 55 | 56 | return $this->model->storeBy($data); 57 | } 58 | 59 | /** 60 | * 61 | * @param $id 62 | * @param Request $request 63 | * @return Model|null 64 | */ 65 | public function show($id, Request $request) 66 | { 67 | $role = $this->model->firstBy($id); 68 | 69 | if ($request->has('from') && $request->get('from') == 'parent_role') { 70 | $role->setAttribute('permissions', $role->permissions()->get()->toTree()); 71 | } else { 72 | $role->setAttribute('permissions', $role->permissions()->get()->pluck('id')); 73 | } 74 | 75 | $role->setAttribute('departments', $role->departments()->pluck('id')); 76 | 77 | return $role; 78 | } 79 | 80 | /** 81 | * 82 | * @param $id 83 | * @param RoleRequest $request 84 | * @return mixed 85 | */ 86 | public function update($id, RoleRequest $request) 87 | { 88 | $data = $request->all(); 89 | $data['data_range'] = (int) $data['data_range']; 90 | if (!DataRange::Personal_Choose->assert($data['data_range'])) { 91 | $data['departments'] = []; 92 | } 93 | 94 | return $this->model->updateBy($id, $data); 95 | } 96 | 97 | /** 98 | * @param $id 99 | * @return bool|null 100 | */ 101 | public function destroy($id) 102 | { 103 | if ($this->model->where($this->model->getParentIdColumn(), $id)->first()) { 104 | throw new FailedException('请先删除子角色'); 105 | } 106 | 107 | return $this->model->deleteBy($id); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /modules/Permissions/Http/Requests/RoleRequest.php: -------------------------------------------------------------------------------- 1 | [ 20 | 'required', 21 | Rule::unique('roles')->where(function ($query) { 22 | return $query->when($this->get('id'), function ($query){ 23 | $query->where('id', '<>', $this->get('id')); 24 | })->where('deleted_at', 0); 25 | }) 26 | ], 27 | 28 | 'identify' => [ 29 | 'required', 30 | 'alpha', 31 | Rule::unique('roles')->where(function ($query) { 32 | return $query->when($this->get('id'), function ($query){ 33 | $query->where('id', '<>', $this->get('id')); 34 | })->where('deleted_at', 0); 35 | }) 36 | ] 37 | ]; 38 | } 39 | 40 | 41 | /** 42 | * messages 43 | * 44 | * @return string[] 45 | */ 46 | public function messages(): array 47 | { 48 | return [ 49 | 'role_name.required' => '角色名称必须填写', 50 | 51 | 'role_name.unique' => '角色名称已存在', 52 | 53 | 'identify.required' => '角色标识必须填写', 54 | 55 | 'identify.alpha' => '角色标识只允许字母组成', 56 | 57 | 'identify.unique' => '角色标识已存在', 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /modules/Permissions/Installer.php: -------------------------------------------------------------------------------- 1 | '权限管理', 15 | 'name' => 'permissions', 16 | 'path' => 'permissions', 17 | 'keywords' => '权限, 角色, 部门', 18 | 'description' => '权限管理模块', 19 | 'provider' => PermissionsServiceProvider::class 20 | ]; 21 | } 22 | 23 | protected function requirePackages(): void 24 | { 25 | // TODO: Implement requirePackages() method. 26 | } 27 | 28 | protected function removePackages(): void 29 | { 30 | // TODO: Implement removePackages() method. 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /modules/Permissions/Middlewares/PermissionGate.php: -------------------------------------------------------------------------------- 1 | isMethod('get')) { 14 | return $next($request); 15 | } 16 | 17 | /* @var User $user */ 18 | $user = $request->user(getGuardName()); 19 | 20 | if (! $user->can()) { 21 | throw new PermissionForbidden(); 22 | } 23 | 24 | return $next($request); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /modules/Permissions/Models/Departments.php: -------------------------------------------------------------------------------- 1 | 'like', 46 | 'status' => '=', 47 | ]; 48 | 49 | protected bool $asTree = true; 50 | 51 | 52 | /** 53 | * 54 | * @param int|array $id 55 | * @return array 56 | */ 57 | public function findFollowDepartments(int|array $id): array 58 | { 59 | if (!is_array($id)) { 60 | $id = [$id]; 61 | } 62 | 63 | $followDepartmentIds = $this->whereIn($this->getParentIdColumn(), $id)->pluck('id')->toArray(); 64 | 65 | if (! empty($followDepartmentIds)) { 66 | $followDepartmentIds = array_merge($followDepartmentIds, $this->findFollowDepartments($followDepartmentIds)); 67 | } 68 | 69 | return $followDepartmentIds; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /modules/Permissions/Models/Jobs.php: -------------------------------------------------------------------------------- 1 | 'like' 42 | ]; 43 | } 44 | -------------------------------------------------------------------------------- /modules/Permissions/Models/Roles.php: -------------------------------------------------------------------------------- 1 | 'like', 50 | 51 | 'id' => '<>' 52 | ]; 53 | 54 | protected bool $asTree = true; 55 | 56 | 57 | /** 58 | * 59 | * @return BelongsToMany 60 | */ 61 | public function permissions(): BelongsToMany 62 | { 63 | return $this->belongsToMany(Permissions::class, 'role_has_permissions', 'role_id', 'permission_id'); 64 | } 65 | 66 | 67 | /** 68 | * departments 69 | * 70 | * @return BelongsToMany 71 | */ 72 | public function departments(): BelongsToMany 73 | { 74 | return $this->belongsToMany(Departments::class, 'role_has_departments', 'role_id', 'department_id'); 75 | } 76 | 77 | /** 78 | * get role's permissions 79 | * @return Collection 80 | */ 81 | public function getPermissions(): Collection 82 | { 83 | return $this->permissions()->get(); 84 | } 85 | 86 | /** 87 | * get role's departments 88 | * @return Collection 89 | */ 90 | public function getDepartments(): Collection 91 | { 92 | return $this->departments()->get(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /modules/Permissions/Models/Traits/DataRange.php: -------------------------------------------------------------------------------- 1 | user(); 27 | 28 | if ($currenUser->isSuperAdmin()) { 29 | return $query; 30 | } 31 | 32 | $userIds = $this->getDepartmentUserIdsBy($roles, $currenUser); 33 | 34 | if ($userIds->isEmpty()) { 35 | return $query; 36 | } 37 | 38 | return $query->whereIn($this->aliasField('creator_id'), $userIds); 39 | } 40 | 41 | /** 42 | * get department ids 43 | * 44 | * @param array $roles 45 | * @param $currentUser 46 | * @return Collection 47 | */ 48 | public function getDepartmentUserIdsBy(array $roles, $currentUser): Collection 49 | { 50 | $userIds = Collection::make(); 51 | 52 | if (empty($roles)) { 53 | $roles = $currentUser->roles()->get(); 54 | } 55 | 56 | /* @var Roles $role */ 57 | foreach ($roles as $role) { 58 | if (DataRangeEnum::All_Data->assert($role->data_range)) { 59 | return Collection::make(); 60 | } 61 | 62 | if (DataRangeEnum::Personal_Choose->assert($role->data_range)) { 63 | $userIds = $userIds->merge($this->getUserIdsByDepartmentId($role->departments()->pluck('id'))); 64 | } 65 | 66 | if (DataRangeEnum::Personal_Data->assert($role->data_range)) { 67 | $userIds = $userIds->push($currentUser->id); 68 | } 69 | 70 | if (DataRangeEnum::Department_Data->assert($role->data_range)) { 71 | $userIds = $userIds->merge( 72 | $this->getUserIdsByDepartmentId([$currentUser->department_id]) 73 | ); 74 | } 75 | 76 | if (DataRangeEnum::Department_DOWN_Data->assert($role->data_range)) { 77 | $departmentsId = [$currentUser->department_id]; 78 | 79 | $departmentModel = new Departments(); 80 | 81 | $departmentIds = $departmentModel->findFollowDepartments($departmentsId); 82 | 83 | $userIds = $userIds->merge($this->getUserIdsByDepartmentId($departmentIds))->push($currentUser->id); 84 | } 85 | } 86 | 87 | return $userIds->unique(); 88 | } 89 | 90 | 91 | /** 92 | * get user ids by department is 93 | * 94 | * @param array|Collection $departmentIds 95 | * @return Collection 96 | */ 97 | protected function getUserIdsByDepartmentId(array|Collection $departmentIds): Collection 98 | { 99 | $userModel = app(getAuthUserModel()); 100 | 101 | return $userModel->whereIn('department_id', $departmentIds)->pluck('id'); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /modules/Permissions/Providers/PermissionsServiceProvider.php: -------------------------------------------------------------------------------- 1 | increments('id'); 21 | $table->string('role_name', 30)->comment('角色名称'); 22 | $table->string('identify', 30)->nullable()->comment('角色的标识,用英文表示'); 23 | $table->integer('parent_id')->default(0)->comment('父级ID'); 24 | $table->string('description')->nullable()->comment('角色描述'); 25 | $table->smallInteger('data_range')->default(0)->comment('1 全部数据 2 自定义数据 3 仅本人数据 4 部门数据 5 部门及以下数据'); 26 | $table->creatorId(); 27 | $table->createdAt(); 28 | $table->updatedAt(); 29 | $table->deletedAt(); 30 | 31 | $table->engine = 'InnoDB'; 32 | $table->comment('角色表'); 33 | }); 34 | } 35 | 36 | /** 37 | * Reverse the migrations. 38 | * 39 | * @return void 40 | */ 41 | public function down() 42 | { 43 | Schema::dropIfExists('roles'); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /modules/Permissions/database/migrations/2022_12_06_110551_create_jobs.php: -------------------------------------------------------------------------------- 1 | increments('id'); 21 | $table->string('job_name', 50)->comment('岗位名称'); 22 | $table->string('coding', 30)->nullable()->comment('创建人ID'); 23 | $table->tinyInteger('status')->default('1')->comment('1 正常 2 停用'); 24 | $table->integer('sort')->default('1')->comment('排序'); 25 | $table->string('description', 1000)->nullable()->comment('岗位描述'); 26 | $table->creatorId(); 27 | $table->createdAt(); 28 | $table->updatedAt(); 29 | $table->deletedAt(); 30 | 31 | $table->engine = 'InnoDB'; 32 | $table->comment('岗位表'); 33 | }); 34 | } 35 | 36 | /** 37 | * Reverse the migrations. 38 | * 39 | * @return void 40 | */ 41 | public function down() 42 | { 43 | Schema::dropIfExists('jobs'); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /modules/Permissions/database/migrations/2022_12_07_075441_create_departments.php: -------------------------------------------------------------------------------- 1 | increments('id'); 21 | $table->integer('parent_id')->default(0)->comment('父级ID'); 22 | $table->string('department_name')->comment('部门名称'); 23 | $table->string('principal')->nullable()->comment('负责人'); 24 | $table->string('mobile', 30)->nullable()->comment('负责人联系方式'); 25 | $table->string('email', 50)->nullable()->comment('邮箱'); 26 | $table->smallInteger('status')->default(1)->comment('1 正常 2 停用'); 27 | $table->integer('sort')->default(1)->comment('排序'); 28 | $table->creatorId(); 29 | $table->createdAt(); 30 | $table->updatedAt(); 31 | $table->deletedAt(); 32 | 33 | $table->engine = 'InnoDB'; 34 | $table->comment('部门表'); 35 | }); 36 | } 37 | 38 | /** 39 | * Reverse the migrations. 40 | * 41 | * @return void 42 | */ 43 | public function down() 44 | { 45 | Schema::dropIfExists('departments'); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /modules/Permissions/database/migrations/2022_12_07_103318_create_permissions.php: -------------------------------------------------------------------------------- 1 | increments('id'); 21 | $table->integer('parent_id')->default(0)->comment('父级菜单'); 22 | $table->string('permission_name', 50)->comment('菜单名称'); 23 | $table->string('route', 50)->comment('前端路由'); 24 | $table->string('icon', 50)->comment('菜单 icon'); 25 | $table->string('module', 50)->comment('所属模块'); 26 | $table->string('permission_mark', 100)->default('')->comment('权限标识,使用 @ 分割'); 27 | $table->string('component')->comment('组件'); 28 | $table->string('redirect')->nullable()->comment('跳转地址'); 29 | $table->tinyInteger('keepalive')->default(1)->comment('1 缓存 2 不缓存'); 30 | $table->tinyInteger('type')->default(1)->comment('1 目录 2 菜单 3 按钮'); 31 | $table->tinyInteger('hidden')->default(1)->comment('1 显示 2 隐藏'); 32 | $table->integer('sort')->default(1)->comment('排序'); 33 | $table->creatorId(); 34 | $table->createdAt(); 35 | $table->updatedAt(); 36 | $table->deletedAt(); 37 | 38 | $table->index(['module', 'permission_mark']); 39 | 40 | $table->engine = 'InnoDB'; 41 | $table->comment('权限表'); 42 | }); 43 | } 44 | 45 | /** 46 | * Reverse the migrations. 47 | * 48 | * @return void 49 | */ 50 | public function down() 51 | { 52 | Schema::dropIfExists('permissions'); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /modules/Permissions/database/migrations/2022_12_10_061840_create_user_has_roles.php: -------------------------------------------------------------------------------- 1 | integer('user_id')->comment('users primary key'); 17 | 18 | $table->integer('role_id')->comment('roles primary key'); 19 | 20 | $table->comment('user relate roles'); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down(): void 30 | { 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /modules/Permissions/database/migrations/2022_12_10_061857_create_role_has_permissions.php: -------------------------------------------------------------------------------- 1 | integer('role_id')->comment('roles primary key'); 17 | 18 | $table->integer('permission_id')->comment('permissions primary key'); 19 | 20 | $table->comment('role relate permissions'); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down(): void 30 | { 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /modules/Permissions/database/migrations/2022_12_10_061919_create_user_has_jobs.php: -------------------------------------------------------------------------------- 1 | integer('user_id')->comment('users primary key'); 17 | 18 | $table->integer('job_id')->comment('jobs primary key'); 19 | 20 | $table->comment('user relate jobs'); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down(): void 30 | { 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /modules/Permissions/database/migrations/2022_12_10_061928_create_role_has_departments.php: -------------------------------------------------------------------------------- 1 | integer('role_id')->comment('roles primary key'); 17 | 18 | $table->integer('department_id')->comment('departments primary key'); 19 | 20 | $table->comment('role relate departments'); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down(): void 30 | { 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /modules/Permissions/database/migrations/2023_03_01_142043_create_remove_is_inner.php: -------------------------------------------------------------------------------- 1 | removeColumn('is_inner'); 19 | } 20 | 21 | $table->string('active_menu')->default('')->comment('访问内页时,需要激活的菜单栏')->after('sort'); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down(): void 31 | { 32 | 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /modules/Permissions/database/migrations/2023_04_17_141652_create_update_permissions.php: -------------------------------------------------------------------------------- 1 | string('icon')->default('')->change(); 19 | $table->string('component')->default('')->change(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down(): void 29 | { 30 | 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /modules/Permissions/routes/route.php: -------------------------------------------------------------------------------- 1 | group(function () { 10 | Route::apiResource('roles', RolesController::class); 11 | 12 | Route::apiResource('jobs', JobsController::class); 13 | Route::put('jobs/enable/{id}', [JobsController::class, 'enable']); 14 | 15 | Route::apiResource('departments', DepartmentsController::class); 16 | Route::put('departments/enable/{id}', [DepartmentsController::class, 'enable']); 17 | 18 | Route::apiResource('permissions', PermissionsController::class); 19 | Route::put('permissions/enable/{id}', [PermissionsController::class, 'enable']); 20 | //next 21 | }); 22 | -------------------------------------------------------------------------------- /modules/System/Http/Controllers/DictionaryController.php: -------------------------------------------------------------------------------- 1 | model->getList(); 23 | } 24 | 25 | /** 26 | * @param Request $request 27 | * @return mixed 28 | */ 29 | public function store(Request $request) 30 | { 31 | return $this->model->storeBy($request->all()); 32 | } 33 | 34 | /** 35 | * @param $id 36 | * @return mixed 37 | */ 38 | public function show($id) 39 | { 40 | return $this->model->firstBy($id); 41 | } 42 | 43 | /** 44 | * @param Request $request 45 | * @param $id 46 | * @return mixed 47 | */ 48 | public function update($id, Request $request) 49 | { 50 | return $this->model->updateBy($id, $request->all()); 51 | } 52 | 53 | /** 54 | * @param $id 55 | * @return mixed 56 | */ 57 | public function destroy($id) 58 | { 59 | $dictionary = $this->model->find($id); 60 | 61 | if ($this->model->deleteBy($id)) { 62 | return $dictionary->values()->delete(); 63 | } 64 | 65 | return false; 66 | } 67 | 68 | public function enable($id) 69 | { 70 | return $this->model->toggleBy($id); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /modules/System/Http/Controllers/DictionaryValuesController.php: -------------------------------------------------------------------------------- 1 | model->getList(); 23 | } 24 | 25 | /** 26 | * @param Request $request 27 | * @return mixed 28 | */ 29 | public function store(Request $request) 30 | { 31 | return $this->model->storeBy($request->all()); 32 | } 33 | 34 | /** 35 | * @param $id 36 | * @return mixed 37 | */ 38 | public function show($id) 39 | { 40 | return $this->model->firstBy($id); 41 | } 42 | 43 | /** 44 | * @param Request $request 45 | * @param $id 46 | * @return mixed 47 | */ 48 | public function update($id, Request $request) 49 | { 50 | return $this->model->updateBy($id, $request->all()); 51 | } 52 | 53 | /** 54 | * @param $id 55 | * @return mixed 56 | */ 57 | public function destroy($id) 58 | { 59 | return $this->model->deleteBy($id); 60 | } 61 | 62 | public function enable($id) 63 | { 64 | return $this->model->toggleBy($id); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /modules/System/Installer.php: -------------------------------------------------------------------------------- 1 | '系统管理', 15 | 'name' => 'system', 16 | 'path' => 'system', 17 | 'keywords' => '系统管理, system', 18 | 'description' => '系统管理模块', 19 | 'provider' => SystemServiceProvider::class 20 | ]; 21 | } 22 | 23 | protected function requirePackages(): void 24 | { 25 | // TODO: Implement requirePackages() method. 26 | } 27 | 28 | protected function removePackages(): void 29 | { 30 | // TODO: Implement removePackages() method. 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /modules/System/Models/Dictionary.php: -------------------------------------------------------------------------------- 1 | 'like', 42 | 'key' => 'like', 43 | 'status' => '=', 44 | ]; 45 | 46 | 47 | /** 48 | * 字典值集合 49 | * 50 | * @return HasMany 51 | */ 52 | public function values(): HasMany 53 | { 54 | return $this->hasMany(DictionaryValues::class, 'dic_id', 'id'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /modules/System/Models/DictionaryValues.php: -------------------------------------------------------------------------------- 1 | '=', 43 | 'label' => 'like', 44 | 'status' => '=', 45 | ]; 46 | } 47 | -------------------------------------------------------------------------------- /modules/System/Providers/SystemServiceProvider.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name', 100)->comment('字典名称'); 19 | $table->string('key')->comment('字典 key'); 20 | $table->tinyInteger('status')->default(1)->comment('状态 1 启用 2 禁用'); 21 | $table->string('description', 1000)->comment('备注')->default(''); 22 | $table->creatorId(); 23 | $table->createdAt(); 24 | $table->updatedAt(); 25 | $table->deletedAt(); 26 | 27 | $table->engine='InnoDB'; 28 | $table->comment('字段管理'); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | * 35 | * @return void 36 | */ 37 | public function down() 38 | { 39 | Schema::dropIfExists('system_dictionary'); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /modules/System/database/migrations/2023_05_08_043740_create_system_dictionary_values.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->integer('dic_id')->comment('字典ID'); 19 | $table->string('label')->comment('值名称'); 20 | $table->tinyInteger('value')->comment('对应值'); 21 | $table->integer('sort')->default(0)->comment('排序'); 22 | $table->tinyInteger('status')->default(1)->comment('状态 1 正常 2 禁用'); 23 | $table->string('description', 1000)->comment('描述')->default(''); 24 | $table->creatorId(); 25 | $table->createdAt(); 26 | $table->updatedAt(); 27 | $table->deletedAt(); 28 | 29 | $table->engine='InnoDB'; 30 | $table->comment('字典对应值'); 31 | }); 32 | } 33 | 34 | /** 35 | * Reverse the migrations. 36 | * 37 | * @return void 38 | */ 39 | public function down() 40 | { 41 | Schema::dropIfExists('system_dictionary_values'); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /modules/System/routes/route.php: -------------------------------------------------------------------------------- 1 | group(function(){ 8 | 9 | Route::apiResource('dictionary', DictionaryController::class); 10 | Route::put('dictionary/enable/{id}', [DictionaryController::class, 'enable']); 11 | 12 | Route::apiResource('dic/values', DictionaryValuesController::class); 13 | Route::put('dic/values/enable/{id}', [DictionaryValuesController::class, 'enable']); 14 | 15 | //next 16 | }); 17 | 18 | -------------------------------------------------------------------------------- /modules/User/Events/Login.php: -------------------------------------------------------------------------------- 1 | where('email', $request->get('email'))->first(); 24 | 25 | Event::dispatch(new Login($request, $user ? ($user->isDisabled() ? null : $user) : null)); 26 | 27 | if ($user) { 28 | if ($user->isDisabled()) { 29 | throw new FailedException('账号被禁用,请联系管理员'); 30 | } 31 | 32 | if (Hash::check($request->get('password'), $user->password)) { 33 | $token = $user->createToken('token')->plainTextToken; 34 | return compact('token'); 35 | } 36 | } 37 | 38 | throw new FailedException('登录失败!请检查邮箱或者密码'); 39 | } 40 | 41 | 42 | /** 43 | * logout 44 | * 45 | * @return array 46 | */ 47 | public function logout(): array 48 | { 49 | /* @var User $user */ 50 | $user = Auth::guard(getGuardName())->user(); 51 | 52 | $user->currentAccessToken()->delete(); 53 | 54 | return []; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /modules/User/Http/Requests/UserRequest.php: -------------------------------------------------------------------------------- 1 | [ 20 | 'required', 21 | Rule::unique('users')->where(function ($query) { 22 | return $query->when($this->get('id'), function ($query){ 23 | $query->where('id', '<>', $this->get('id')); 24 | })->where('deleted_at', 0); 25 | }) 26 | ], 27 | ]; 28 | } 29 | 30 | 31 | /** 32 | * messages 33 | * 34 | * @return string[] 35 | */ 36 | public function messages(): array 37 | { 38 | return [ 39 | 'email.required' => '邮箱必须填写', 40 | 41 | 'email.unique' => '邮箱已存在', 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /modules/User/Listeners/Login.php: -------------------------------------------------------------------------------- 1 | request; 25 | 26 | $this->log($request, (bool) $event->user); 27 | 28 | if ($event->user) { 29 | $event->user->login_ip = $request->ip(); 30 | $event->user->login_at = time(); 31 | $event->user->remember_token = null; 32 | $event->user->save(); 33 | } 34 | } 35 | 36 | /** 37 | * login log 38 | * 39 | * @param Request $request 40 | * @param int $isSuccess 41 | * @return void 42 | */ 43 | protected function log(Request $request, int $isSuccess): void 44 | { 45 | LogLogin::insert([ 46 | 'account' => $request->get('email'), 47 | 'login_ip' => $request->ip(), 48 | 'browser' => $this->getBrowserFrom(Str::of($request->userAgent())), 49 | 'platform' => $this->getPlatformFrom(Str::of($request->userAgent())), 50 | 'login_at' => time(), 51 | 'status' => $isSuccess ? Status::Enable : Status::Disable 52 | ]); 53 | } 54 | 55 | 56 | /** 57 | * get platform 58 | * 59 | * @param Stringable $userAgent 60 | * @return string 61 | */ 62 | protected function getBrowserFrom(Stringable $userAgent): string 63 | { 64 | return match (true) { 65 | $userAgent->contains('MSIE', true) => 'IE', 66 | $userAgent->contains('Firefox', true) => 'Firefox', 67 | $userAgent->contains('Chrome', true) => 'Chrome', 68 | $userAgent->contains('Opera', true) => 'Opera', 69 | $userAgent->contains('Safari', true) => 'Safari', 70 | default => 'unknown' 71 | }; 72 | } 73 | 74 | 75 | /** 76 | * get os name 77 | * 78 | * @param Stringable $userAgent 79 | * @return string 80 | */ 81 | protected function getPlatformFrom(Stringable $userAgent): string 82 | { 83 | return match (true) { 84 | $userAgent->contains('win', true) => 'Windows', 85 | $userAgent->contains('mac', true) => 'Mac OS', 86 | $userAgent->contains('linux', true) => 'Linux', 87 | $userAgent->contains('iphone', true) => 'iphone', 88 | $userAgent->contains('android', true) => 'Android', 89 | default => 'unknown' 90 | }; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /modules/User/Middlewares/OperatingMiddleware.php: -------------------------------------------------------------------------------- 1 | log($request, $response); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /modules/User/Models/LogLogin.php: -------------------------------------------------------------------------------- 1 | 'datetime:Y-m-d H:i' 23 | ]; 24 | 25 | /** 26 | * 27 | * @param ?string $email 28 | * @return LengthAwarePaginator 29 | * @throws ContainerExceptionInterface 30 | * @throws NotFoundExceptionInterface 31 | */ 32 | public function getUserLogBy(?string $email): LengthAwarePaginator 33 | { 34 | return static::when($email, function ($query) use ($email){ 35 | $query->where('account', $email); 36 | }) 37 | ->orderByDesc('id') 38 | ->paginate(request()->get('limit', 10)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /modules/User/Models/LogOperate.php: -------------------------------------------------------------------------------- 1 | user(); 45 | 46 | $userModel = getAuthUserModel(); 47 | 48 | if (! $user instanceof $userModel) { 49 | return; 50 | } 51 | 52 | [$module, $controller, $action] = CatchAdmin::parseFromRouteAction(); 53 | 54 | $requestStartAt = app(Kernel::class)->requestStartedAt()->getPreciseTimestamp(3); 55 | $params = $request->all(); 56 | // 如果参数过长则不记录 57 | if (!empty($params)) { 58 | if (strlen(\json_encode($params, JSON_UNESCAPED_UNICODE)) > 5000) { 59 | $params = []; 60 | } 61 | } 62 | 63 | $timeTaken = intval(microtime(true) * 1000 - $requestStartAt); 64 | $this->storeBy([ 65 | 'module' => $module, 66 | 'action' => $controller . '@' . $action, 67 | 'creator_id' => $user->id, 68 | 'http_method' => $request->method(), 69 | 'http_code' => $response->getStatusCode(), 70 | 'start_at' => intval($requestStartAt/1000), 71 | 'time_taken' => $timeTaken, 72 | 'ip' => $request->ip(), 73 | 'params' => \json_encode($params, JSON_UNESCAPED_UNICODE), 74 | 'created_at' => time() 75 | ]); 76 | } 77 | 78 | /** 79 | * 80 | * @return Attribute 81 | */ 82 | protected function timeTaken(): Attribute 83 | { 84 | return Attribute::make( 85 | get: fn ($value) => $value > 1000 ? intval($value/1000) . 's' : $value . 'ms', 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /modules/User/Models/Observers/UsersObserver.php: -------------------------------------------------------------------------------- 1 | 'like', 42 | 'email' => 'like', 43 | 'status' => '=', 44 | ]; 45 | 46 | /** 47 | * @var string 48 | */ 49 | protected $table = 'users'; 50 | 51 | protected array $fields = ['id', 'username', 'email', 'avatar', 'creator_id', 'status', 'department_id', 'created_at']; 52 | 53 | /** 54 | * @var array|string[] 55 | */ 56 | protected array $form = ['username', 'email', 'password', 'department_id']; 57 | 58 | /** 59 | * @var array|string[] 60 | */ 61 | protected array $formRelations = ['roles', 'jobs']; 62 | 63 | /** 64 | * password 65 | * 66 | * @return Attribute 67 | */ 68 | protected function password(): Attribute 69 | { 70 | return new Attribute( 71 | // get: fn($value) => '', 72 | set: fn ($value) => bcrypt($value), 73 | ); 74 | } 75 | 76 | protected function DepartmentId(): Attribute 77 | { 78 | return new Attribute( 79 | get: fn($value) => $value ? : null, 80 | set: fn($value) => $value ? : 0 81 | ); 82 | } 83 | 84 | /** 85 | * is super admin 86 | * 87 | * @return bool 88 | */ 89 | public function isSuperAdmin(): bool 90 | { 91 | return $this->{$this->primaryKey} == config('catch.super_admin'); 92 | } 93 | 94 | /** 95 | * update 96 | * @param $id 97 | * @param array $data 98 | * @return mixed 99 | */ 100 | public function updateBy($id, array $data): mixed 101 | { 102 | if (empty($data['password'])) { 103 | unset($data['password']); 104 | } 105 | 106 | return parent::updateBy($id, $data); 107 | } 108 | 109 | public function isDisabled(): bool 110 | { 111 | 112 | return $this->status == Status::Disable->value; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /modules/User/Providers/UserServiceProvider.php: -------------------------------------------------------------------------------- 1 | LoginListener::class 15 | ]; 16 | 17 | /** 18 | * route path 19 | * 20 | * @return string|array 21 | */ 22 | public function moduleName(): string|array 23 | { 24 | // TODO: Implement path() method. 25 | return 'user'; 26 | } 27 | 28 | /** 29 | * @return string[] 30 | */ 31 | protected function middlewares(): array 32 | { 33 | return [OperatingMiddleware::class]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /modules/User/database/migrations/2022_12_04_060250_create_users.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | 18 | $table->string('username')->comment('昵称'); 19 | 20 | $table->string('password')->comment('密码'); 21 | 22 | $table->string('email')->comment('邮箱'); 23 | 24 | $table->string('avatar')->nullable()->comment('头像'); 25 | 26 | $table->string('remember_token', 1000)->nullable()->comment('token'); 27 | 28 | $table->integer('department_id')->default(0)->comment('部门ID'); 29 | 30 | $table->integer('creator_id')->default(0); 31 | 32 | $table->status(); 33 | 34 | $table->string('login_ip')->nullable()->comment('登录IP'); 35 | 36 | $table->integer('login_at')->default(0)->comment('登录时间'); 37 | 38 | $table->createdAt(); 39 | 40 | $table->updatedAt(); 41 | 42 | $table->deletedAt(); 43 | 44 | $table->comment('用户表'); 45 | }); 46 | } 47 | 48 | /** 49 | * Reverse the migrations. 50 | * 51 | * @return void 52 | */ 53 | public function down(): void 54 | { 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /modules/User/database/migrations/2022_12_04_062539_create_log_login.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | 18 | $table->string('account')->comment('登录账户'); 19 | 20 | $table->string('login_ip')->comment('登录的IP'); 21 | 22 | $table->string('browser')->comment('浏览器'); 23 | 24 | $table->string('platform')->comment('平台'); 25 | 26 | $table->integer('login_at')->comment('平台'); 27 | 28 | $table->status(); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | * 35 | * @return void 36 | */ 37 | public function down(): void 38 | { 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /modules/User/database/migrations/2022_12_17_031519_create_log_operate.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | $table->string('module', 50)->comment('操作'); 18 | $table->string('action', 50)->comment('操作'); 19 | $table->text('params')->comment('参数'); 20 | $table->string('ip')->comment('ip 地址'); 21 | $table->string('http_method', 10)->comment('http 请求方式'); 22 | $table->smallInteger('http_code')->comment('http status code'); 23 | $table->unsignedInteger('start_at')->comment('请求开始时间'); 24 | $table->smallInteger('time_taken')->comment('请求消耗时间/ms'); 25 | $table->creatorId(); 26 | $table->createdAt(); 27 | 28 | $table->engine = 'InnoDB'; 29 | $table->comment('操作日志'); 30 | }); 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | * 36 | * @return void 37 | */ 38 | public function down() 39 | { 40 | Schema::dropIfExists('log_operate'); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /modules/User/database/seeder/User.php: -------------------------------------------------------------------------------- 1 | 'catchadmin', 17 | 18 | 'email' => 'catch@admin.com', 19 | 20 | 'password' => 'catchadmin', 21 | 22 | 'creator_id' => 1, 23 | 24 | 'department_id' => 0 25 | ]); 26 | 27 | $user->save(); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /modules/User/routes/route.php: -------------------------------------------------------------------------------- 1 | withoutMiddleware(config('catch.route.middlewares')); 9 | Route::post('logout', [AuthController::class, 'logout'])->withoutMiddleware(config('catch.route.middlewares')); 10 | 11 | // users route 12 | Route::apiResource('users', UserController::class); 13 | Route::put('users/enable/{id}', [UserController::class, 'enable']); 14 | Route::match(['post', 'get'], 'user/online', [UserController::class, 'online']); 15 | Route::get('user/login/log', [UserController::class, 'loginLog']); 16 | Route::get('user/operate/log', [UserController::class, 'operateLog']); 17 | Route::get('user/operate/log', [UserController::class, 'operateLog']); 18 | Route::get('user/export', [UserController::class, 'export']); 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | ./tests/Feature 13 | 14 | 15 | 16 | 17 | ./app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /public/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%- title %> 8 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaguarJack/catch-admin/3ca6cea6ad073754a5134685e54d5b75ece561a7/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class); 50 | 51 | $response = $kernel->handle( 52 | $request = Request::capture() 53 | )->send(); 54 | 55 | $kernel->terminate($request, $response); 56 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /resources/views/welcome.blade.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaguarJack/catch-admin/3ca6cea6ad073754a5134685e54d5b75ece561a7/resources/views/welcome.blade.php -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 18 | }); 19 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 19 | })->purpose('Display an inspiring quote'); 20 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 |