zxhxx 19 часов назад
Сommit
600e0826b5
100 измененных файлов с 6644 добавлено и 0 удалено
  1. 50 0
      .example.env
  2. 3 0
      .gitattributes
  3. 13 0
      .gitignore
  4. 42 0
      .travis.yml
  5. 21 0
      LICENSE
  6. 144 0
      README.md
  7. 1 0
      app/.htaccess
  8. 22 0
      app/AppService.php
  9. 94 0
      app/BaseController.php
  10. 58 0
      app/ExceptionHandle.php
  11. 8 0
      app/Request.php
  12. 36 0
      app/admin/config/admin.php
  13. 31 0
      app/admin/config/route.php
  14. 232 0
      app/admin/controller/Ajax.php
  15. 160 0
      app/admin/controller/Index.php
  16. 93 0
      app/admin/controller/Login.php
  17. 21 0
      app/admin/controller/mall/Cate.php
  18. 135 0
      app/admin/controller/mall/Goods.php
  19. 179 0
      app/admin/controller/system/Admin.php
  20. 71 0
      app/admin/controller/system/Auth.php
  21. 69 0
      app/admin/controller/system/Config.php
  22. 155 0
      app/admin/controller/system/CurdGenerate.php
  23. 138 0
      app/admin/controller/system/Log.php
  24. 191 0
      app/admin/controller/system/Menu.php
  25. 106 0
      app/admin/controller/system/Node.php
  26. 27 0
      app/admin/controller/system/Quick.php
  27. 22 0
      app/admin/controller/system/Uploadfile.php
  28. 0 0
      app/admin/entity/.keep
  29. 12 0
      app/admin/entity/Test.php
  30. 5 0
      app/admin/middleware.php
  31. 43 0
      app/admin/middleware/CheckAuth.php
  32. 21 0
      app/admin/middleware/CheckInstall.php
  33. 60 0
      app/admin/middleware/CheckLogin.php
  34. 45 0
      app/admin/middleware/RateLimiting.php
  35. 104 0
      app/admin/middleware/SystemLog.php
  36. 18 0
      app/admin/model/MallCate.php
  37. 32 0
      app/admin/model/MallGoods.php
  38. 36 0
      app/admin/model/SystemAdmin.php
  39. 61 0
      app/admin/model/SystemAuth.php
  40. 10 0
      app/admin/model/SystemAuthNode.php
  41. 10 0
      app/admin/model/SystemConfig.php
  42. 29 0
      app/admin/model/SystemLog.php
  43. 68 0
      app/admin/model/SystemMenu.php
  44. 35 0
      app/admin/model/SystemNode.php
  45. 17 0
      app/admin/model/SystemQuick.php
  46. 10 0
      app/admin/model/SystemUploadfile.php
  47. 20 0
      app/admin/service/ConfigService.php
  48. 24 0
      app/admin/service/NodeService.php
  49. 128 0
      app/admin/service/SystemLogService.php
  50. 50 0
      app/admin/service/TriggerService.php
  51. 226 0
      app/admin/service/UploadService.php
  52. 21 0
      app/admin/service/annotation/ControllerAnnotation.php
  53. 19 0
      app/admin/service/annotation/MiddlewareAnnotation.php
  54. 25 0
      app/admin/service/annotation/NodeAnnotation.php
  55. 155 0
      app/admin/service/auth/Node.php
  56. 166 0
      app/admin/service/console/CliEcho.php
  57. 1575 0
      app/admin/service/curd/BuildCurd.php
  58. 8 0
      app/admin/service/curd/exceptions/CurdException.php
  59. 8 0
      app/admin/service/curd/exceptions/FileException.php
  60. 8 0
      app/admin/service/curd/exceptions/TableException.php
  61. 28 0
      app/admin/service/curd/templates/controller/controller.code
  62. 21 0
      app/admin/service/curd/templates/controller/indexMethod.code
  63. 2 0
      app/admin/service/curd/templates/controller/select.code
  64. 23 0
      app/admin/service/curd/templates/model/model.code
  65. 5 0
      app/admin/service/curd/templates/model/relation.code
  66. 5 0
      app/admin/service/curd/templates/model/relationSelect.code
  67. 5 0
      app/admin/service/curd/templates/model/select.code
  68. 91 0
      app/admin/service/curd/templates/static/js.code
  69. 10 0
      app/admin/service/curd/templates/view/form.code
  70. 16 0
      app/admin/service/curd/templates/view/index.code
  71. 7 0
      app/admin/service/curd/templates/view/module/checkbox.code
  72. 3 0
      app/admin/service/curd/templates/view/module/checkboxInput.code
  73. 7 0
      app/admin/service/curd/templates/view/module/date.code
  74. 8 0
      app/admin/service/curd/templates/view/module/editor.code
  75. 11 0
      app/admin/service/curd/templates/view/module/file.code
  76. 11 0
      app/admin/service/curd/templates/view/module/files.code
  77. 11 0
      app/admin/service/curd/templates/view/module/image.code
  78. 11 0
      app/admin/service/curd/templates/view/module/images.code
  79. 7 0
      app/admin/service/curd/templates/view/module/input.code
  80. 4 0
      app/admin/service/curd/templates/view/module/option.code
  81. 7 0
      app/admin/service/curd/templates/view/module/radio.code
  82. 3 0
      app/admin/service/curd/templates/view/module/radioInput.code
  83. 9 0
      app/admin/service/curd/templates/view/module/select.code
  84. 7 0
      app/admin/service/curd/templates/view/module/sort.code
  85. 7 0
      app/admin/service/curd/templates/view/module/textarea.code
  86. 13 0
      app/admin/service/curd/templates/view/recycle.code
  87. 108 0
      app/admin/service/tool/CommonTool.php
  88. 203 0
      app/admin/traits/Curd.php
  89. 58 0
      app/admin/view/index/edit_admin.html
  90. 38 0
      app/admin/view/index/edit_password.html
  91. 120 0
      app/admin/view/index/index.html
  92. 45 0
      app/admin/view/index/set2fa.html
  93. 218 0
      app/admin/view/index/welcome.html
  94. 42 0
      app/admin/view/layout/default.html
  95. 27 0
      app/admin/view/layout/editor.html
  96. 55 0
      app/admin/view/login/index.html
  97. 43 0
      app/admin/view/mall/cate/add.html
  98. 43 0
      app/admin/view/mall/cate/edit.html
  99. 10 0
      app/admin/view/mall/cate/index.html
  100. 131 0
      app/admin/view/mall/goods/add.html

+ 50 - 0
.example.env

@@ -0,0 +1,50 @@
+APP_DEBUG=true
+
+# 后台系统日志开关
+APP_ADMIN_SYSTEM_LOG=true
+
+DEFAULT_TIMEZONE=Asia/Shanghai
+
+DB_TYPE=mysql
+DB_HOST=127.0.0.1
+DB_NAME=easyadmin8
+DB_USER=root
+DB_PASS=root
+DB_PORT=3306
+DB_CHARSET=utf8mb4
+DB_PREFIX=ea8_
+
+# 限流器开关 若启动需要配置 Redis 服务
+RATE_LIMITING_STATUS=false
+
+# Redis配置
+REDIS_HOST=127.0.0.1
+REDIS_PORT=6379
+REDIS_PASSWORD=
+REDIS_PREFIX=
+REDIS_DATABASE=0
+
+# 后台配置项组
+[EASYADMIN]
+
+# 后台地址后缀名称
+ADMIN=admin
+
+# 后台登录验证码开关
+CAPTCHA=false
+
+# 是否为演示环境
+IS_DEMO=false
+
+# CDN配置项组
+CDN=
+EXAMPLE=true
+
+# 是否开启CSRF过滤 
+IS_CSRF=false
+
+# 静态文件路径前缀
+STATIC_PATH=/static
+
+# OSS静态文件路径前缀
+OSS_STATIC_PREFIX=static_easyadmin

+ 3 - 0
.gitattributes

@@ -0,0 +1,3 @@
+*.js linguist-language=PHP
+*.css linguist-language=PHP
+*.html linguist-language=PHP

+ 13 - 0
.gitignore

@@ -0,0 +1,13 @@
+*.log
+.env
+composer.phar
+composer.lock
+.DS_Store
+Thumbs.db
+/.idea
+/.vscode
+/.settings
+/.buildpath
+/.project
+*.txt
+/public/*.txt

+ 42 - 0
.travis.yml

@@ -0,0 +1,42 @@
+sudo: false
+
+language: php
+
+branches:
+  only:
+    - stable
+
+cache:
+  directories:
+    - $HOME/.composer/cache
+
+before_install:
+  - composer self-update
+
+install:
+  - composer install --no-dev --no-interaction --ignore-platform-reqs
+  - zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Core.zip .
+  - composer require --update-no-dev --no-interaction "topthink/think-image:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-migration:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-captcha:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-mongo:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-worker:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-helper:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-queue:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-angular:^1.0"
+  - composer require --dev --update-no-dev --no-interaction "topthink/think-testing:^1.0"
+  - zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Full.zip .
+
+script:
+  - php think unit
+
+deploy:
+  provider: releases
+  api_key:
+    secure: TSF6bnl2JYN72UQOORAJYL+CqIryP2gHVKt6grfveQ7d9rleAEoxlq6PWxbvTI4jZ5nrPpUcBUpWIJHNgVcs+bzLFtyh5THaLqm39uCgBbrW7M8rI26L8sBh/6nsdtGgdeQrO/cLu31QoTzbwuz1WfAVoCdCkOSZeXyT/CclH99qV6RYyQYqaD2wpRjrhA5O4fSsEkiPVuk0GaOogFlrQHx+C+lHnf6pa1KxEoN1A0UxxVfGX6K4y5g4WQDO5zT4bLeubkWOXK0G51XSvACDOZVIyLdjApaOFTwamPcD3S1tfvuxRWWvsCD5ljFvb2kSmx5BIBNwN80MzuBmrGIC27XLGOxyMerwKxB6DskNUO9PflKHDPI61DRq0FTy1fv70SFMSiAtUv9aJRT41NQh9iJJ0vC8dl+xcxrWIjU1GG6+l/ZcRqVx9V1VuGQsLKndGhja7SQ+X1slHl76fRq223sMOql7MFCd0vvvxVQ2V39CcFKao/LB1aPH3VhODDEyxwx6aXoTznvC/QPepgWsHOWQzKj9ftsgDbsNiyFlXL4cu8DWUty6rQy8zT2b4O8b1xjcwSUCsy+auEjBamzQkMJFNlZAIUrukL/NbUhQU37TAbwsFyz7X0E/u/VMle/nBCNAzgkMwAUjiHM6FqrKKBRWFbPrSIixjfjkCnrMEPw=
+  file:
+    - ThinkPHP_Core.zip
+    - ThinkPHP_Full.zip
+  skip_cleanup: true
+  on:
+    tags: true

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 EasyAdmin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 144 - 0
README.md

@@ -0,0 +1,144 @@
+<div align="center" dir="auto">
+<img alt="log" src="public/static/common/images/logo-8.png" />
+
+<p>
+<img src="https://img.shields.io/badge/php-%3E=8.1.0-brightgreen.svg?style=for-the-badge&logo=php&colorB=ff69b4" alt="php">
+<img src="https://img.shields.io/badge/mysql-%3E=5.7-brightgreen.svg?style=for-the-badge&logo=mysql&colorB=blue" alt="MySQL">
+<img src="https://img.shields.io/badge/thinkphp-%3E=8.0.0-brightgreen.svg?style=for-the-badge&logo=thinkphp" alt="ThinkPHP">
+<img src="https://img.shields.io/badge/layui-%3E=2.9.0-brightgreen.svg?style=for-the-badge&logo=layui&colorB=orange" alt="layui">
+<img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge&logo=license&colorB=purple" alt="License">
+</p>
+
+</div>
+
+## `EasyAdmin8`所有版本 (当前项目为`ThinkPHP`版本)
+
+|          |                                 Github                                 |                                 Gitee                                 |
+|----------|:----------------------------------------------------------------------:|:---------------------------------------------------------------------:|
+| ThinkPHP |         [EasyAdmin8](https://github.com/EasyAdmin8/EasyAdmin8)         |         [EasyAdmin8](https://gitee.com/EasyAdmin8/EasyAdmin8)         |
+| Laravel  | [EasyAdmin8-Laravel](https://github.com/EasyAdmin8/EasyAdmin8-Laravel) | [EasyAdmin8-Laravel](https://gitee.com/EasyAdmin8/EasyAdmin8-Laravel) |
+| webman   |  [EasyAdmin8-webman](https://github.com/EasyAdmin8/EasyAdmin8-webman)  |  [EasyAdmin8-webman](https://gitee.com/EasyAdmin8/EasyAdmin8-webman)  |
+
+## 项目介绍
+
+> `EasyAdmin8` 在 [`EasyAdmin`](https://gitee.com/zhongshaofa/easyadmin) 的基础上更新 ThinkPHP 框架到 8.1+ ,PHP 最低版本要求不低于 8.1
+> 
+> 2025年起 `PHP` 版本要求提升到 `8.1+`, 如果需要 `8.0` 到分支 `v8.0` 中下载
+>
+> ThinkPHP v8.1+ 和 Layui v2.9.x 的快速开发的后台管理系统。
+>
+> 项目地址:[http://easyadmin8.top](http://easyadmin8.top)
+>
+> 演示地址:[http://thinkphp.easyadmin8.top/admin](http://thinkphp.easyadmin8.top/admin)
+>
+> 如果您之前已经用过 `FastAdmin` 或者 `EasyAdmin` , 那么入手 `EasyAdmin8` 将会更加轻松
+>
+>【如果不能访问,可以自行本地搭建预览或参考下方界面预览图】
+
+## 大版本更新记录:
+
+[更新记录](log.md)
+
+## 安装教程
+
+> EasyAdmin8 使用 Composer 来管理项目依赖。因此,在使用 EasyAdmin8 之前,请确保你的机器已经安装了 Composer。
+
+### 通过一键安装命令
+
+```
+if [ -f /usr/bin/curl ];then curl -sSO https://easyadmin8.top/auto-install-EasyAdmin8.sh;else wget -O auto-install-EasyAdmin8.sh https://easyadmin8.top/auto-install-EasyAdmin8.sh;fi;bash auto-install-EasyAdmin8.sh
+```
+
+### 通过`git`下载安装包,`composer`安装依赖包
+
+```
+1.下载安装包
+
+  git clone https://github.com/EasyAdmin8/EasyAdmin8
+
+  或者
+
+  git clone https://gitee.com/EasyAdmin8/EasyAdmin8
+
+2.安装依赖包(确保 PHP 版本 >= 8.1)
+
+  在根目录下 composer install ,如果有报错信息可以使用命令 composer install --ignore-platform-reqs
+  
+3. 拷贝 .example.env 文件重命名为 .env ,命令 cp .example.env .env ,修改数据库账号密码参数
+
+4.配置伪静态(以 Nginx 为例)
+  
+  location / {
+    if ( !-e $request_filename){
+        rewrite ^/(.*)$ /index.php?s=$1 last;
+        break;
+    }
+  }
+
+```
+
+## CURD命令大全
+
+> 参考 [CURD命令大全](https://edocs.easyadmin8.top/curd/command.html)
+
+## 常见问题
+
+> 参考 [常见问题](https://easyadmin8.top/guide/question.html)
+
+## 界面预览
+
+![EasyAdmin8-01](public/static/common/images/easyadmin8-01.png)
+![EasyAdmin8-02](public/static/common/images/easyadmin8-02.png)
+![EasyAdmin8-03](public/static/common/images/easyadmin8-03.png)
+
+## 交流群
+
+<center>
+
+![EasyAdmin8-ThinkPHP 交流群](public/static/common/images/EasyAdmin8-ThinkPHP.png)
+
+</center>
+
+## 相关文档
+
+* [ThinkPHP 8.1](https://doc.thinkphp.cn)
+
+* [EasyAdmin](http://easyadmin.99php.cn/docs)
+
+* [Layui 2.9.x](https://layui.dev/docs)
+
+* [Layuimini](https://github.com/zhongshaofa/layuimini)
+
+* [Annotations](https://github.com/doctrine/annotations)
+
+* [Jquery](https://github.com/jquery/jquery)
+
+* [RequireJs](https://github.com/requirejs/requirejs)
+
+* [CKEditor](https://github.com/ckeditor/ckeditor4)
+
+* [Echarts](https://github.com/apache/incubator-echarts)
+
+* [UEditorPlus](https://github.com/modstart-lib/ueditor-plus)
+
+* [wangEditor](https://github.com/wangeditor-team/wangEditor)
+
+## 免责声明
+
+> 所有协议遵循 [`EasyAdmin`](https://gitee.com/zhongshaofa/easyadmin)
+>
+> 任何用户在使用 `EasyAdmin8` 后台框架前,请您仔细阅读并透彻理解本声明。您可以选择不使用`EasyAdmin8`后台框架,若您一旦使用`EasyAdmin8`后台框架,您的使用行为即被视为对本声明全部内容的认可和接受。
+
+* `EasyAdmin8`后台框架是一款开源免费的后台快速开发框架 ,主要用于更便捷地开发后台管理;其尊重并保护所有用户的个人隐私权,不窃取任何用户计算机中的信息。更不具备用户数据存储等网络传输功能。
+
+* 您承诺秉着合法、合理的原则使用`EasyAdmin8`后台框架,不利用`EasyAdmin8`后台框架进行任何违法、侵害他人合法利益等恶意的行为,亦不将`EasyAdmin8`后台框架运用于任何违反我国法律法规的 Web 平台。
+
+* 任何单位或个人因下载使用`EasyAdmin8`后台框架而产生的任何意外、疏忽、合约毁坏、诽谤、版权或知识产权侵犯及其造成的损失 (包括但不限于直接、间接、附带或衍生的损失等),本开源项目不承担任何法律责任。
+
+* 用户明确并同意本声明条款列举的全部内容,对使用`EasyAdmin8`后台框架可能存在的风险和相关后果将完全由用户自行承担,本开源项目不承担任何法律责任。
+
+* 任何单位或个人在阅读本免责声明后,应在《MIT 开源许可证》所允许的范围内进行合法的发布、传播和使用`EasyAdmin8`后台框架等行为,若违反本免责声明条款或违反法律法规所造成的法律责任(包括但不限于民事赔偿和刑事责任),由违约者自行承担。
+
+* 如果本声明的任何部分被认为无效或不可执行,其余部分仍具有完全效力。不可执行的部分声明,并不构成我们放弃执行该声明的权利。
+
+* 本开源项目有权随时对本声明条款及附件内容进行单方面的变更,并以消息推送、网页公告等方式予以公布,公布后立即自动生效,无需另行单独通知;若您在本声明内容公告变更后继续使用的,表示您已充分阅读、理解并接受修改后的声明内容。

+ 1 - 0
app/.htaccess

@@ -0,0 +1 @@
+deny from all

+ 22 - 0
app/AppService.php

@@ -0,0 +1,22 @@
+<?php
+declare (strict_types = 1);
+
+namespace app;
+
+use think\Service;
+
+/**
+ * 应用服务类
+ */
+class AppService extends Service
+{
+    public function register()
+    {
+        // 服务注册
+    }
+
+    public function boot()
+    {
+        // 服务启动
+    }
+}

+ 94 - 0
app/BaseController.php

@@ -0,0 +1,94 @@
+<?php
+declare (strict_types = 1);
+
+namespace app;
+
+use think\App;
+use think\exception\ValidateException;
+use think\Validate;
+
+/**
+ * 控制器基础类
+ */
+abstract class BaseController
+{
+    /**
+     * Request实例
+     * @var \think\Request
+     */
+    protected $request;
+
+    /**
+     * 应用实例
+     * @var \think\App
+     */
+    protected $app;
+
+    /**
+     * 是否批量验证
+     * @var bool
+     */
+    protected $batchValidate = false;
+
+    /**
+     * 控制器中间件
+     * @var array
+     */
+    protected $middleware = [];
+
+    /**
+     * 构造方法
+     * @access public
+     * @param  App  $app  应用对象
+     */
+    public function __construct(App $app)
+    {
+        $this->app     = $app;
+        $this->request = $this->app->request;
+
+        // 控制器初始化
+        $this->initialize();
+    }
+
+    // 初始化
+    protected function initialize()
+    {}
+
+    /**
+     * 验证数据
+     * @access protected
+     * @param  array        $data     数据
+     * @param  string|array $validate 验证器名或者验证规则数组
+     * @param  array        $message  提示信息
+     * @param  bool         $batch    是否批量验证
+     * @return array|string|true
+     * @throws ValidateException
+     */
+    protected function validate(array $data, string|array $validate, array $message = [], bool $batch = false)
+    {
+        if (is_array($validate)) {
+            $v = new Validate();
+            $v->rule($validate);
+        } else {
+            if (strpos($validate, '.')) {
+                // 支持场景
+                [$validate, $scene] = explode('.', $validate);
+            }
+            $class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
+            $v     = new $class();
+            if (!empty($scene)) {
+                $v->scene($scene);
+            }
+        }
+
+        $v->message($message);
+
+        // 是否批量验证
+        if ($batch || $this->batchValidate) {
+            $v->batch(true);
+        }
+
+        return $v->failException(true)->check($data);
+    }
+
+}

+ 58 - 0
app/ExceptionHandle.php

@@ -0,0 +1,58 @@
+<?php
+namespace app;
+
+use think\db\exception\DataNotFoundException;
+use think\db\exception\ModelNotFoundException;
+use think\exception\Handle;
+use think\exception\HttpException;
+use think\exception\HttpResponseException;
+use think\exception\ValidateException;
+use think\Response;
+use Throwable;
+
+/**
+ * 应用异常处理类
+ */
+class ExceptionHandle extends Handle
+{
+    /**
+     * 不需要记录信息(日志)的异常类列表
+     * @var array
+     */
+    protected $ignoreReport = [
+        HttpException::class,
+        HttpResponseException::class,
+        ModelNotFoundException::class,
+        DataNotFoundException::class,
+        ValidateException::class,
+    ];
+
+    /**
+     * 记录异常信息(包括日志或者其它方式记录)
+     *
+     * @access public
+     * @param  Throwable $exception
+     * @return void
+     */
+    public function report(Throwable $exception): void
+    {
+        // 使用内置的方式记录异常日志
+        parent::report($exception);
+    }
+
+    /**
+     * Render an exception into an HTTP response.
+     *
+     * @access public
+     * @param \think\Request   $request
+     * @param Throwable $e
+     * @return Response
+     */
+    public function render($request, Throwable $e): Response
+    {
+        // 添加自定义异常处理机制
+
+        // 其他错误交给系统处理
+        return parent::render($request, $e);
+    }
+}

+ 8 - 0
app/Request.php

@@ -0,0 +1,8 @@
+<?php
+namespace app;
+
+// 应用请求对象类
+class Request extends \think\Request
+{
+
+}

+ 36 - 0
app/admin/config/admin.php

@@ -0,0 +1,36 @@
+<?php
+
+return [
+    // 后台路径地址 默认 admin
+    'alias_name'         => env('EASYADMIN.ADMIN'),
+
+    // 不需要验证权限的控制器
+    'no_auth_controller' => [
+        'ajax',
+        'login',
+        'index',
+    ],
+
+    // 不需要验证权限的节点
+    'no_auth_node'       => [
+        'login/index',
+        'login/out',
+    ],
+
+    //上传类型
+    'upload_types'       => [
+        'local' => '本地存储',
+        'oss'   => '阿里云oss',
+        'cos'   => '腾讯云cos',
+        'qnoss' => '七牛云'
+    ],
+
+    // 默认编辑器
+    'editor_types'       => [
+        'ueditor'    => '百度编辑器(不建议使用)',
+        'ckeditor'   => 'CK编辑器',
+        'wangEditor' => 'wangEditor(推荐使用)',
+        'EasyMDE'    => 'EasyMDE(markdown)',
+    ],
+
+];

+ 31 - 0
app/admin/config/route.php

@@ -0,0 +1,31 @@
+<?php
+
+use app\admin\middleware\CheckInstall;
+use app\admin\middleware\CheckLogin;
+use app\admin\middleware\CheckAuth;
+use app\admin\middleware\SystemLog;
+use app\admin\middleware\RateLimiting;
+
+// 你可以在这里继续写你需要的路由
+
+
+// +----------------------------------------------------------------------
+// | 这里只是路由的中间件
+// | 至于为什么要把中间件配置写在这里呢??? Why???
+// | 因为 ThinkPHP官方最新版本 已经不支持在中间件获取 controller 和 action 了
+// +----------------------------------------------------------------------
+
+return [
+    'middleware' => [
+        // 限流中间件
+        RateLimiting::class,
+        // 判断是否已经安装后台系统
+        CheckInstall::class,
+        // 检测是否登录
+        CheckLogin::class,
+        // 操作日志
+        SystemLog::class,
+        // 验证节点权限
+        CheckAuth::class,
+    ],
+];

+ 232 - 0
app/admin/controller/Ajax.php

@@ -0,0 +1,232 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\admin\model\SystemUploadfile;
+use app\admin\service\UploadService;
+use app\common\controller\AdminController;
+use app\common\service\MenuService;
+use app\Request;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\db\Query;
+use think\facade\Cache;
+use think\response\Json;
+
+class Ajax extends AdminController
+{
+
+    /**
+     * 初始化后台接口地址
+     * @return Json
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function initAdmin(): Json
+    {
+        $cacheData = Cache::get('initAdmin_' . $this->adminUid);
+        if (!empty($cacheData)) {
+            return json($cacheData);
+        }
+        $menuService = new MenuService($this->adminUid);
+        $data        = [
+            'logoInfo' => [
+                'title' => sysConfig('site', 'logo_title'),
+                'image' => sysConfig('site', 'logo_image'),
+                'href'  => __url('index/index'),
+            ],
+            'homeInfo' => $menuService->getHomeInfo(),
+            'menuInfo' => $menuService->getMenuTree(),
+        ];
+        Cache::tag('initAdmin')->set('initAdmin_' . $this->adminUid, $data);
+        return json($data);
+    }
+
+    /**
+     * 清理缓存接口
+     */
+    public function clearCache(): void
+    {
+        Cache::clear();
+        $this->success('清理缓存成功');
+    }
+
+    /**
+     * 上传文件
+     * @param Request $request
+     * @return Json|null
+     */
+    public function upload(Request $request): Json|null
+    {
+        $this->isDemo && $this->error('演示环境下不允许修改');
+        $this->checkPostRequest();
+        $type         = $request->param('type', '');
+        $data         = [
+            'upload_type' => $request->post('upload_type'),
+            'file'        => $request->file($type == 'editor' ? 'upload' : 'file'),
+        ];
+        $uploadConfig = sysConfig('upload');
+        empty($data['upload_type']) && $data['upload_type'] = $uploadConfig['upload_type'];
+        $rule = [
+            'upload_type|指定上传类型有误' => "in:{$uploadConfig['upload_allow_type']}",
+            'file|文件'                    => "require|file|fileExt:{$uploadConfig['upload_allow_ext']}|fileSize:{$uploadConfig['upload_allow_size']}",
+        ];
+        $this->validate($data, $rule);
+        $upload_type = $data['upload_type'];
+        try {
+            $upload = UploadService::instance()->setConfig($uploadConfig)->$upload_type($data['file'], $type);
+        }catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+        $code = $upload['code'] ?? 0;
+        if ($code == 0) {
+            $this->error($upload['data'] ?? '');
+        }else {
+            if ($type == 'editor') {
+                return json(
+                    [
+                        'error'    => ['message' => '上传成功', 'number' => 201,],
+                        'fileName' => '',
+                        'uploaded' => 1,
+                        'url'      => $upload['data']['url'] ?? '',
+                    ]
+                );
+            }else {
+                $this->success('上传成功', $upload['data'] ?? '');
+            }
+        }
+    }
+
+    /**
+     * 获取上传文件列表
+     * @param Request $request
+     * @return Json
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getUploadFiles(Request $request): Json
+    {
+        $get   = $request->get();
+        $page  = !empty($get['page']) ? $get['page'] : 1;
+        $limit = !empty($get['limit']) ? $get['limit'] : 10;
+        $title = !empty($get['title']) ? $get['title'] : null;
+        $count = SystemUploadfile::where(function(Query $query) use ($title) {
+            !empty($title) && $query->where('original_name', 'like', "%{$title}%");
+        })
+            ->count();
+        $list  = SystemUploadfile::where(function(Query $query) use ($title) {
+            !empty($title) && $query->where('original_name', 'like', "%{$title}%");
+        })
+            ->page($page, $limit)
+            ->order($this->sort)
+            ->select()->toArray();
+        $data  = [
+            'code'  => 0,
+            'msg'   => '',
+            'count' => $count,
+            'data'  => $list,
+        ];
+        return json($data);
+    }
+
+    /**
+     * 百度编辑器上传
+     * @param Request $request
+     * @return Json
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function uploadUEditor(Request $request): Json
+    {
+        $uploadConfig      = sysConfig('upload');
+        $upload_allow_size = $uploadConfig['upload_allow_size'];
+        $_upload_allow_ext = explode(',', $uploadConfig['upload_allow_ext']);
+        $upload_allow_ext  = [];
+        array_map(function($value) use (&$upload_allow_ext) {
+            $upload_allow_ext[] = '.' . $value;
+        }, $_upload_allow_ext);
+        $config      = [
+            // 上传图片配置项
+            "imageActionName"         => "image",
+            "imageFieldName"          => "file",
+            "imageMaxSize"            => $upload_allow_size,
+            "imageAllowFiles"         => $upload_allow_ext,
+            "imageCompressEnable"     => true,
+            "imageCompressBorder"     => 5000,
+            "imageInsertAlign"        => "none",
+            "imageUrlPrefix"          => "",
+            // 列出图片
+            "imageManagerActionName"  => "listImage",
+            "imageManagerListSize"    => 20,
+            "imageManagerUrlPrefix"   => "",
+            "imageManagerInsertAlign" => "none",
+            "imageManagerAllowFiles"  => $upload_allow_ext,
+            // 上传 video
+            "videoActionName"         => "video",
+            "videoFieldName"          => "file",
+            "videoUrlPrefix"          => "",
+            "videoMaxSize"            => $upload_allow_size,
+            "videoAllowFiles"         => $upload_allow_ext,
+            // 上传 附件
+            "fileActionName"          => "attachment",
+            "fileFieldName"           => "file",
+            "fileMaxSize"             => $upload_allow_size,
+            "fileAllowFiles"          => $upload_allow_ext,
+        ];
+        $action      = $request->param('action/s', '');
+        $file        = $request->file('file');
+        $upload_type = $uploadConfig['upload_type'];
+        switch ($action) {
+            case 'image':
+            case 'attachment':
+            case 'video':
+                if ($this->isDemo) return json(['state' => '演示环境下不允许修改']);
+                try {
+                    $upload = UploadService::instance()->setConfig($uploadConfig)->$upload_type($file);
+                    $code   = $upload['code'] ?? 0;
+                    if ($code == 0) {
+                        return json(['state' => $upload['data'] ?? '上传错误信息']);
+                    }else {
+                        return json(['state' => 'SUCCESS', 'url' => $upload['data']['url'] ?? '']);
+                    }
+                }catch (\Exception $e) {
+                    $this->error($e->getMessage());
+                }
+                break;
+            case 'listImage':
+                $list   = (new SystemUploadfile())->order($this->sort)->limit(100)->field('url')->select()->toArray();
+                $result = [
+                    "state" => "SUCCESS",
+                    "list"  => $list,
+                    "total" => 0,
+                    "start" => 0,
+                ];
+                return json($result);
+            default:
+                return json($config);
+        }
+    }
+
+    public function composerInfo(): Json
+    {
+        $lockFilePath = root_path() . '/composer.lock';
+        $list         = [];
+        if (file_exists($lockFilePath)) {
+            $lockFileContent = file_get_contents($lockFilePath);
+            if ($lockFileContent !== false) {
+                $lockData = json_decode($lockFileContent, true);
+                if (!empty($lockData['packages'])) {
+                    foreach ($lockData['packages'] as $package) {
+                        $list[] = ['name' => $package['name'], 'version' => $package['version']];
+                    }
+                }
+            }
+        }
+        $this->success('success', $list);
+    }
+
+}

+ 160 - 0
app/admin/controller/Index.php

@@ -0,0 +1,160 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\admin\model\SystemAdmin;
+use app\admin\model\SystemQuick;
+use app\common\controller\AdminController;
+use app\Request;
+use Exception;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\facade\Db;
+
+class Index extends AdminController
+{
+
+    /**
+     * 后台主页
+     * @param Request $request
+     * @return string
+     */
+    public function index(Request $request): string
+    {
+        return $this->fetch('', ['admin' => $request->adminUserInfo,]);
+    }
+
+    /**
+     * 后台欢迎页
+     * @return string
+     * @throws Exception
+     */
+    public function welcome(): string
+    {
+        $tpVersion    = \think\facade\App::version();
+        $mysqlVersion = Db::query("select version() as version")[0]['version'] ?? '未知';
+        $phpVersion   = phpversion();
+        $versions     = compact('tpVersion', 'mysqlVersion', 'phpVersion');
+        $quick_list   = SystemQuick::field('id,title,icon,href')
+            ->where(['status' => 1])->order('sort', 'desc')->limit(50)->select()->toArray();
+        $quicks       = array_chunk($quick_list, 8);
+        $this->assign(compact('quicks', 'versions'));
+        return $this->fetch();
+    }
+
+    /**
+     * 修改管理员信息
+     * @param Request $request
+     * @return string
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function editAdmin(Request $request): string
+    {
+        $id  = $this->adminUid;
+        $row = (new SystemAdmin())
+            ->withoutField('password')
+            ->find($id);
+        empty($row) && $this->error('用户信息不存在');
+        if ($request->isPost()) {
+            $post = $request->post();
+            $this->isDemo && $this->error('演示环境下不允许修改');
+            $rule = [];
+            $this->validate($post, $rule);
+            try {
+                $login_type = $post['login_type'] ?? 1;
+                if ($login_type == 2) {
+                    $ga_secret = (new SystemAdmin())->where('id', $id)->value('ga_secret');
+                    if (empty($ga_secret)) $this->error('请先绑定谷歌验证器');
+                }
+                $save = $row->allowField(['head_img', 'phone', 'remark', 'update_time', 'login_type'])->save($post);
+            }catch (\PDOException $e) {
+                $this->error('保存失败');
+            }
+            $save ? $this->success('保存成功') : $this->error('保存失败');
+        }
+        $this->assign('row', $row);
+        $notes = (new SystemAdmin())->notes;
+        $this->assign('notes', $notes);
+        return $this->fetch();
+    }
+
+    /**
+     * 修改密码
+     * @param Request $request
+     * @return string
+     */
+    public function editPassword(Request $request): string
+    {
+        $id  = $this->adminUid;
+        $row = (new SystemAdmin())
+            ->withoutField('password')
+            ->find($id);
+        if (!$row) {
+            $this->error('用户信息不存在');
+        }
+        if ($request->isPost()) {
+            $post = $request->post();
+            $this->isDemo && $this->error('演示环境下不允许修改');
+            $rule = [
+                'password|登录密码'       => 'require',
+                'password_again|确认密码' => 'require',
+            ];
+            $this->validate($post, $rule);
+            if ($post['password'] != $post['password_again']) {
+                $this->error('两次密码输入不一致');
+            }
+
+            try {
+                $save = $row->save([
+                    'password' => password_hash($post['password'], PASSWORD_DEFAULT),
+                ]);
+            }catch (Exception $e) {
+                $this->error('保存失败');
+            }
+            if ($save) {
+                $this->success('保存成功');
+            }else {
+                $this->error('保存失败');
+            }
+        }
+        $this->assign('row', $row);
+        return $this->fetch();
+    }
+
+    /**
+     * 设置谷歌验证码
+     * @param Request $request
+     * @return string
+     * @throws Exception
+     */
+    public function set2fa(Request $request): string
+    {
+        $id  = $this->adminUid;
+        $row = (new SystemAdmin())->withoutField('password')->find($id);
+        if (!$row) $this->error('用户信息不存在');
+        // You can see: https://gitee.com/wolf-code/authenticator
+        $ga = new \Wolfcode\Authenticator\google\PHPGangstaGoogleAuthenticator();
+        if (!$request->isAjax()) {
+            $old_secret = $row->ga_secret;
+            $secret     = $ga->createSecret(32);
+            $ga_title   = $this->isDemo ? 'EasyAdmin8演示环境' : '可自定义修改显示标题';
+            $dataUri    = $ga->getQRCode($ga_title, $secret);
+            $this->assign(compact('row', 'dataUri', 'old_secret', 'secret'));
+            return $this->fetch();
+        }
+        $this->isDemo && $this->error('演示环境下不允许修改');
+        $post      = $request->post();
+        $ga_secret = $post['ga_secret'] ?? '';
+        $ga_code   = $post['ga_code'] ?? '';
+        if (empty($ga_code)) $this->error('请输入验证码');
+        if (!$ga->verifyCode($ga_secret, $ga_code)) $this->error('验证码错误');
+        $row->ga_secret  = $ga_secret;
+        $row->login_type = 2;
+        $row->save();
+        $this->success('操作成功');
+    }
+
+}

+ 93 - 0
app/admin/controller/Login.php

@@ -0,0 +1,93 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\admin\model\SystemAdmin;
+use app\common\controller\AdminController;
+use app\common\utils\Helper;
+use think\captcha\facade\Captcha;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use app\Request;
+use think\Response;
+use Wolfcode\RateLimiting\Attributes\RateLimitingMiddleware;
+
+class Login extends AdminController
+{
+
+    protected bool $ignoreLogin = true;
+
+    public function initialize(): void
+    {
+        parent::initialize();
+        $action = $this->request->action();
+        if (!empty($this->adminUid) && !in_array($action, ['out'])) {
+            $adminModuleName = config('admin.alias_name');
+            $this->success('已登录,无需再次登录', [], __url("@{$adminModuleName}"));
+        }
+    }
+
+    /**
+     * 用户登录
+     * @param Request $request
+     * @return string
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    #[RateLimitingMiddleware(key: [Helper::class, 'getIp'], seconds: 1, limit: 1, message: '请求过于频繁')]
+    public function index(Request $request): string
+    {
+        $captcha = env('EASYADMIN.CAPTCHA', 1);
+        if (!$request->isPost()) return $this->fetch('', compact('captcha'));
+        $post = $request->post();
+        $rule = [
+            'username|用户名'         => 'require',
+            'password|密码'           => 'require',
+            'keep_login|是否保持登录' => 'require',
+        ];
+        $captcha == 1 && $rule['captcha|验证码'] = 'require|captcha';
+        $this->validate($post, $rule);
+        $admin = SystemAdmin::where(['username' => $post['username']])->find();
+        if (empty($admin)) {
+            $this->error('用户不存在');
+        }
+        if (!password_verify($post['password'], $admin->password)) {
+            $this->error('密码输入有误');
+        }
+        if ($admin->status == 0) {
+            $this->error('账号已被禁用');
+        }
+        if ($admin->login_type == 2) {
+            if (empty($post['ga_code'])) $this->error('请输入谷歌验证码', ['is_ga_code' => true]);
+            $ga = new \Wolfcode\Authenticator\google\PHPGangstaGoogleAuthenticator();
+            if (!$ga->verifyCode($admin->ga_secret, $post['ga_code'])) $this->error('谷歌验证码错误');;
+        }
+        $admin->login_num += 1;
+        $admin->save();
+        $admin = $admin->toArray();
+        unset($admin['password']);
+        $admin['expire_time'] = $post['keep_login'] == 1 ? 0 : time() + 7200;
+        session('admin', $admin);
+        $this->success('登录成功');
+    }
+
+    /**
+     * 用户退出
+     */
+    public function out(): void
+    {
+        session('admin', null);
+        $this->success('退出登录成功');
+    }
+
+    /**
+     * 验证码
+     * @return Response
+     */
+    public function captcha(): Response
+    {
+        return Captcha::instance()->create();
+    }
+}

+ 21 - 0
app/admin/controller/mall/Cate.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace app\admin\controller\mall;
+
+use app\admin\model\MallCate;
+use app\common\controller\AdminController;
+use app\admin\service\annotation\ControllerAnnotation;
+use app\admin\service\annotation\NodeAnnotation;
+use think\App;
+
+#[ControllerAnnotation(title: '商品分类管理')]
+class Cate extends AdminController
+{
+
+    public function __construct(App $app)
+    {
+        parent::__construct($app);
+        self::$model = MallCate::class;
+    }
+
+}

+ 135 - 0
app/admin/controller/mall/Goods.php

@@ -0,0 +1,135 @@
+<?php
+
+namespace app\admin\controller\mall;
+
+use app\admin\model\MallCate;
+use app\admin\model\MallGoods;
+use app\admin\service\annotation\MiddlewareAnnotation;
+use app\common\controller\AdminController;
+use app\admin\service\annotation\ControllerAnnotation;
+use app\admin\service\annotation\NodeAnnotation;
+use app\Request;
+use think\App;
+use think\response\Json;
+use Wolfcode\Ai\Enum\AiType;
+use Wolfcode\Ai\Service\AiChatService;
+
+#[ControllerAnnotation(title: '商城商品管理')]
+class Goods extends AdminController
+{
+
+    #[NodeAnnotation(ignore: ['export'])] // 过滤不需要生成的权限节点 默认 CURD 中会自动生成部分节点 可以在此处过滤
+    protected array $ignoreNode;
+
+    public function __construct(App $app)
+    {
+        parent::__construct($app);
+        self::$model = new MallGoods();
+        $this->assign('cate', MallCate::column('title', 'id'));
+    }
+
+    #[NodeAnnotation(title: '列表', auth: true)]
+    public function index(Request $request): Json|string
+    {
+        if ($request->isAjax()) {
+            if (input('selectFields')) return $this->selectList();
+            list($page, $limit, $where) = $this->buildTableParams();
+            $count = self::$model::where($where)->count();
+            $list  = self::$model::with(['cate'])->where($where)->page($page, $limit)->order($this->sort)->select()->toArray();
+            $data  = [
+                'code'  => 0,
+                'msg'   => '',
+                'count' => $count,
+                'data'  => $list,
+            ];
+            return json($data);
+        }
+        return $this->fetch();
+    }
+
+    #[NodeAnnotation(title: '入库', auth: true)]
+    public function stock(Request $request, $id): string
+    {
+        $row = self::$model::find($id);
+        empty($row) && $this->error('数据不存在');
+        if ($request->isPost()) {
+            $post = $request->post();
+            $rule = [];
+            $this->validate($post, $rule);
+            try {
+                $post['total_stock'] = $row->total_stock + $post['stock'];
+                $post['stock']       = $row->stock + $post['stock'];
+                $save                = $row->save($post);
+            }catch (\Exception $e) {
+                $this->error('保存失败');
+            }
+            $save ? $this->success('保存成功') : $this->error('保存失败');
+        }
+        $this->assign('row', $row);
+        return $this->fetch();
+    }
+
+    #[MiddlewareAnnotation(ignore: MiddlewareAnnotation::IGNORE_LOGIN)]
+    public function no_check_login(Request $request): string
+    {
+        return '这里演示方法不需要经过登录验证';
+    }
+
+
+    #[NodeAnnotation(title: 'AI优化', auth: true)]
+    public function aiOptimization(Request $request): void
+    {
+        $message = $request->post('message');
+        if (empty($message)) $this->error('请输入内容');
+
+        // 演示环境下 默认返回的内容
+        if ($this->isDemo) {
+            $content = <<<EOF
+演示环境中 默认返回的内容
+            
+我来帮你优化这个标题,让它更有吸引力且更符合电商平台的搜索逻辑:
+        
+"商务男士高端定制马克杯 | 办公室精英必备 | 优质陶瓷防烫手柄"
+        
+这个优化后的标题:
+1. 突出了目标用户群体(商务男士)
+2. 强调了产品定位(高端定制)
+3. 点明了使用场景(办公室)
+4. 添加了材质和功能特点(优质陶瓷、防烫手柄)
+5. 使用了吸引人的关键词(精英必备)
+        
+这样的标题不仅更具体,也更容易被搜索引擎识别,同时能精准触达目标客户群。您觉得这个版本如何?
+EOF;
+            $choices = [['message' => [
+                'role'    => 'assistant',
+                'content' => $content,
+            ]]];
+            $this->success('success', compact('choices'));
+        }
+
+        try {
+            $result  = AiChatService::instance()
+                // 当使用推理模型时,可能存在超时的情况,所以需要设置超时时间为 0
+                // ->setTimeLimit(0)
+                // 请替换为您需要的模型类型
+                ->setAiType(AiType::QWEN)
+                // 如果需要指定模型的 API 地址,可自行设置
+                // ->setAiUrl('https://xxx.com')
+                // 请替换为您的模型
+                ->setAiModel('qwen-plus')
+                // 请替换为您的 API KEY
+                ->setAiKey('sk-1234567890')
+                // 此内容会作为系统提示,会影响到回答的内容 当前仅作为测试使用
+                ->setSystemContent('你现在是一位资深的海外电商产品经理')
+                ->chat($message);
+            $choices = $result['choices'];
+        }catch (\Throwable $exception) {
+            $choices = [['message' => [
+                'role'    => 'assistant',
+                'content' => $exception->getMessage(),
+            ]]];
+        }
+        $this->success('success', compact('choices'));
+    }
+
+}

+ 179 - 0
app/admin/controller/system/Admin.php

@@ -0,0 +1,179 @@
+<?php
+
+namespace app\admin\controller\system;
+
+use app\admin\model\SystemAdmin;
+use app\admin\service\TriggerService;
+use app\common\constants\AdminConstant;
+use app\common\controller\AdminController;
+use app\admin\service\annotation\ControllerAnnotation;
+use app\admin\service\annotation\NodeAnnotation;
+use app\Request;
+use think\App;
+use think\response\Json;
+
+#[ControllerAnnotation(title: '管理员管理')]
+class Admin extends AdminController
+{
+
+    protected array $sort = [
+        'sort' => 'desc',
+        'id'   => 'desc',
+    ];
+
+    public function __construct(App $app)
+    {
+        parent::__construct($app);
+        self::$model = SystemAdmin::class;
+        $this->assign('auth_list', self::$model::getAuthList());
+    }
+
+    #[NodeAnnotation(title: '列表', auth: true)]
+    public function index(Request $request): Json|string
+    {
+        if ($request->isAjax()) {
+            if (input('selectFields')) {
+                return $this->selectList();
+            }
+            list($page, $limit, $where) = $this->buildTableParams();
+            $count = self::$model::where($where)->count();
+            $list  = self::$model::withoutField('password')
+                ->where($where)
+                ->page($page, $limit)
+                ->order($this->sort)
+                ->select()->toArray();
+            $data  = [
+                'code'  => 0,
+                'msg'   => '',
+                'count' => $count,
+                'data'  => $list,
+            ];
+            return json($data);
+        }
+        return $this->fetch();
+    }
+
+    #[NodeAnnotation(title: '添加', auth: true)]
+    public function add(Request $request): string
+    {
+        if ($request->isPost()) {
+            $post             = $request->post();
+            $authIds          = $request->post('auth_ids', []);
+            $post['auth_ids'] = implode(',', array_keys($authIds));
+            $rule             = [];
+            $this->validate($post, $rule);
+            if (empty($post['password'])) $post['password'] = '123456';
+            $post['password'] = password_hash($post['password'],PASSWORD_DEFAULT);
+            try {
+                $save = self::$model::create($post);
+            }catch (\Exception $e) {
+                $this->error('保存失败' . $e->getMessage());
+            }
+            $save ? $this->success('保存成功') : $this->error('保存失败');
+        }
+        return $this->fetch();
+    }
+
+    #[NodeAnnotation(title: '编辑', auth: true)]
+    public function edit(Request $request, $id = 0): string
+    {
+        $row = self::$model::find($id);
+        empty($row) && $this->error('数据不存在');
+        if ($request->isPost()) {
+            $post             = $request->post();
+            $authIds          = $request->post('auth_ids', []);
+            $post['auth_ids'] = implode(',', array_keys($authIds));
+            $rule             = [];
+            $this->validate($post, $rule);
+            try {
+                $save = $row->save($post);
+                TriggerService::updateMenu($id);
+            }catch (\Exception $e) {
+                $this->error('保存失败' . $e->getMessage());
+            }
+            $save ? $this->success('保存成功') : $this->error('保存失败');
+        }
+        $this->assign('row', $row);
+        return $this->fetch();
+    }
+
+    #[NodeAnnotation(title: '设置密码', auth: true)]
+    public function password(Request $request, $id): string
+    {
+        $row = self::$model::find($id);
+        empty($row) && $this->error('数据不存在');
+        if ($request->isAjax()) {
+            $post = $request->post();
+            $rule = [
+                'password|登录密码'       => 'require',
+                'password_again|确认密码' => 'require',
+            ];
+            $this->validate($post, $rule);
+            if ($post['password'] != $post['password_again']) {
+                $this->error('两次密码输入不一致');
+            }
+            try {
+                $save = $row->save([
+                    'password' => password_hash($post['password'], PASSWORD_DEFAULT),
+                ]);
+            }catch (\Exception $e) {
+                $this->error('保存失败');
+            }
+            $save ? $this->success('保存成功') : $this->error('保存失败');
+        }
+        $this->assign('row', $row);
+        return $this->fetch();
+    }
+
+    #[NodeAnnotation(title: '删除', auth: true)]
+    public function delete(Request $request): void
+    {
+        $this->checkPostRequest();
+        $id  = $request->param('id');
+        $row = self::$model::whereIn('id', $id)->select();
+        $row->isEmpty() && $this->error('数据不存在');
+        $id == AdminConstant::SUPER_ADMIN_ID && $this->error('超级管理员不允许修改');
+        if (is_array($id)) {
+            if (in_array(AdminConstant::SUPER_ADMIN_ID, $id)) {
+                $this->error('超级管理员不允许修改');
+            }
+        }
+        try {
+            $save = $row->delete();
+        }catch (\Exception $e) {
+            $this->error('删除失败');
+        }
+        $save ? $this->success('删除成功') : $this->error('删除失败');
+    }
+
+    #[NodeAnnotation(title: '属性修改', auth: true)]
+    public function modify(Request $request): void
+    {
+        $this->checkPostRequest();
+        $post = $request->post();
+        $rule = [
+            'id|ID'      => 'require',
+            'field|字段' => 'require',
+            'value|值'   => 'require',
+        ];
+        $this->validate($post, $rule);
+        if (!in_array($post['field'], $this->allowModifyFields)) {
+            $this->error('该字段不允许修改:' . $post['field']);
+        }
+        if ($post['id'] == AdminConstant::SUPER_ADMIN_ID && $post['field'] == 'status') {
+            $this->error('超级管理员状态不允许修改');
+        }
+        $row = self::$model::find($post['id']);
+        empty($row) && $this->error('数据不存在');
+        try {
+            $row->save([
+                $post['field'] => $post['value'],
+            ]);
+        }catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+        $this->success('保存成功');
+    }
+
+
+}

+ 71 - 0
app/admin/controller/system/Auth.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace app\admin\controller\system;
+
+use app\admin\model\SystemAuth;
+use app\admin\model\SystemAuthNode;
+use app\admin\service\TriggerService;
+use app\common\controller\AdminController;
+use app\admin\service\annotation\ControllerAnnotation;
+use app\admin\service\annotation\NodeAnnotation;
+use app\Request;
+use think\App;
+
+#[ControllerAnnotation(title: '角色权限管理', auth: true)]
+class Auth extends AdminController
+{
+
+    protected array $sort = [
+        'sort' => 'desc',
+        'id'   => 'desc',
+    ];
+
+    public function __construct(App $app)
+    {
+        parent::__construct($app);
+        self::$model = SystemAuth::class;
+    }
+
+    #[NodeAnnotation(title: '授权', auth: true)]
+    public function authorize(Request $request, $id): string
+    {
+        $row = self::$model::find($id);
+        empty($row) && $this->error('数据不存在');
+        if ($request->isAjax()) {
+            $list = self::$model::getAuthorizeNodeListByAdminId($id);
+            $this->success('获取成功', $list);
+        }
+        $this->assign('row', $row);
+        return $this->fetch();
+    }
+
+    #[NodeAnnotation(title: '授权保存', auth: true)]
+    public function saveAuthorize(Request $request): void
+    {
+        $this->checkPostRequest();
+        $id   = $request->post('id');
+        $node = $request->post('node', "[]");
+        $node = json_decode($node, true);
+        $row  = self::$model::find($id);
+        empty($row) && $this->error('数据不存在');
+        try {
+            $authNode = new SystemAuthNode();
+            $authNode->where('auth_id', $id)->delete();
+            if (!empty($node)) {
+                $saveAll = [];
+                foreach ($node as $vo) {
+                    $saveAll[] = [
+                        'auth_id' => $id,
+                        'node_id' => $vo,
+                    ];
+                }
+                $authNode->saveAll($saveAll);
+            }
+            TriggerService::updateMenu();
+        }catch (\Exception $e) {
+            $this->error('保存失败');
+        }
+        $this->success('保存成功');
+    }
+
+}

+ 69 - 0
app/admin/controller/system/Config.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace app\admin\controller\system;
+
+use app\admin\model\SystemConfig;
+use app\admin\service\TriggerService;
+use app\common\controller\AdminController;
+use app\admin\service\annotation\ControllerAnnotation;
+use app\admin\service\annotation\NodeAnnotation;
+use app\Request;
+use think\App;
+use think\facade\Cache;
+use think\response\Json;
+
+#[ControllerAnnotation(title: '系统配置管理')]
+class Config extends AdminController
+{
+
+    public function __construct(App $app)
+    {
+        parent::__construct($app);
+        self::$model = SystemConfig::class;
+        $this->assign('upload_types', config('admin.upload_types'));
+        $this->assign('editor_types', config('admin.editor_types'));
+    }
+
+    #[NodeAnnotation(title: '列表', auth: true)]
+    public function index(Request $request): Json|string
+    {
+        return $this->fetch();
+    }
+
+    #[NodeAnnotation(title: '保存', auth: true)]
+    public function save(Request $request): void
+    {
+        $this->checkPostRequest();
+        $post         = $request->post();
+        $notAddFields = ['_token', 'file', 'group'];
+        try {
+            $group = $post['group'] ?? '';
+            if (empty($group)) $this->error('保存失败');
+            if ($group == 'upload') {
+                $upload_types = config('admin.upload_types');
+                // 兼容旧版本
+                self::$model::where('name', 'upload_allow_type')->update(['value' => implode(',', array_keys($upload_types))]);
+            }
+            foreach ($post as $key => $val) {
+                if (in_array($key, $notAddFields)) continue;
+                if (self::$model::where('name', $key)->count()) {
+                    self::$model::where('name', $key)->update(['value' => $val,]);
+                }else {
+                    self::$model::create(
+                        [
+                            'name'  => $key,
+                            'value' => $val,
+                            'group' => $group,
+                        ]);
+                }
+                if (Cache::has($key)) Cache::set($key, $val);
+            }
+            TriggerService::updateMenu();
+            TriggerService::updateSysConfig();
+        }catch (\Exception $e) {
+            $this->error('保存失败' . $e->getMessage());
+        }
+        $this->success('保存成功');
+    }
+
+}

+ 155 - 0
app/admin/controller/system/CurdGenerate.php

@@ -0,0 +1,155 @@
+<?php
+
+namespace app\admin\controller\system;
+
+use app\admin\service\curd\BuildCurd;
+use app\common\controller\AdminController;
+use app\admin\service\annotation\ControllerAnnotation;
+use app\admin\service\annotation\NodeAnnotation;
+use app\Request;
+use think\db\exception\PDOException;
+use think\exception\FileException;
+use think\facade\Console;
+use think\facade\Db;
+use think\helper\Str;
+use think\response\Json;
+
+#[ControllerAnnotation(title: 'CURD可视化管理')]
+class CurdGenerate extends AdminController
+{
+    #[NodeAnnotation(title: '列表', auth: true)]
+    public function index(Request $request): Json|string
+    {
+        return $this->fetch();
+    }
+
+    #[NodeAnnotation(title: '操作', auth: true)]
+    public function save(Request $request, string $type = ''): ?Json
+    {
+        if (!$request->isAjax()) $this->error();
+        switch ($type) {
+            case "search":
+                $tb_prefix = $request->param('tb_prefix/s', '');
+                $tb_name   = $request->param('tb_name/s', '');
+                if (empty($tb_name)) $this->error('参数错误');
+
+                try {
+                    $list = Db::query("SHOW FULL COLUMNS FROM {$tb_prefix}{$tb_name}");
+                    $data = [];
+                    foreach ($list as $value) {
+                        $data[] = [
+                            'name'  => $value['Field'],
+                            'type'  => $value['Type'],
+                            'key'   => $value['Key'],
+                            'extra' => $value['Extra'],
+                            'null'  => $value['Null'],
+                            'desc'  => $value['Comment'],
+                        ];
+                    }
+                    $this->success('查询成功', compact('data', 'list'));
+                }catch (PDOException $exception) {
+                    $this->error($exception->getMessage());
+                }
+                break;
+            case "add":
+                $tb_prefix = $request->param('tb_prefix/s', '');
+                $tb_name   = $request->param('tb_name/s', '');
+                if (empty($tb_name)) $this->error('参数错误');
+
+                $tb_fields = $request->param('tb_fields');
+                $force     = $request->post('force/d', 0);
+                try {
+                    $build = (new BuildCurd())->setTablePrefix($tb_prefix)->setTable($tb_name);
+                    $build->setForce($force); // 强制覆盖
+                    // 新增字段类型
+                    if ($tb_fields) {
+                        foreach ($tb_fields as $tk => $tf) {
+                            if (empty($tf)) continue;
+                            $tf = array_values($tf);
+                            switch ($tk) {
+                                case 'ignore':
+                                    $build->setIgnoreFields($tf, true);
+                                    break;
+                                case 'select':
+                                    $build->setSelectFields($tf, true);
+                                    break;
+                                case 'radio':
+                                    $build->setRadioFieldSuffix($tf, true);
+                                    break;
+                                case 'checkbox':
+                                    $build->setCheckboxFieldSuffix($tf, true);
+                                    break;
+                                case 'image':
+                                    $build->setImageFieldSuffix($tf, true);
+                                    break;
+                                case 'images':
+                                    $build->setImagesFieldSuffix($tf, true);
+                                    break;
+                                case 'date':
+                                    $build->setDateFieldSuffix($tf, true);
+                                    break;
+                                case 'datetime':
+                                    $build->setDatetimeFieldSuffix($tf, true);
+                                    break;
+                                case 'editor':
+                                    $build->setEditorFields($tf, true);
+                                    break;
+                                default:
+                                    break;
+                            }
+                        }
+                    }
+                    $build    = $build->render();
+                    $fileList = $build->getFileList();
+                    if (empty($fileList)) $this->error('这里什么都没有');
+                    $result = $build->create();
+                    $_file  = $result[0] ?? '';
+                    $link   = '';
+                    if (!empty($_file)) {
+                        $_fileExp        = explode(DIRECTORY_SEPARATOR, $_file);
+                        $_fileExp_last   = array_slice($_fileExp, -2);
+                        $_fileExp_last_0 = $_fileExp_last[0] . '.';
+                        if ($_fileExp_last[0] == 'controller') $_fileExp_last_0 = '';
+                        $link = '/' . config('admin.alias_name') . '/' . $_fileExp_last_0 . Str::snake(explode('.php', end($_fileExp_last))[0] ?? '') . '/index';
+                    }
+                    $this->success('生成成功', compact('result', 'link'));
+                }catch (FileException $exception) {
+                    return json(['code' => -1, 'msg' => $exception->getMessage()]);
+                }
+                break;
+            case "delete":
+                $tb_prefix = $request->param('tb_prefix/s', '');
+                $tb_name   = $request->param('tb_name/s', '');
+                if (empty($tb_name)) $this->error('参数错误');
+
+                try {
+                    $build    = (new BuildCurd())->setTablePrefix($tb_prefix)->setTable($tb_name);
+                    $build    = $build->render();
+                    $fileList = $build->getFileList();
+                    if (empty($fileList)) $this->error('这里什么都没有');
+                    $result = $build->delete();
+                    $this->success('删除自动生成CURD文件成功', compact('result'));
+                }catch (FileException $exception) {
+                    return json(['code' => -1, 'msg' => $exception->getMessage()]);
+                }
+                break;
+            case 'console':
+                $command = $request->post('command', '');
+                if (empty($command)) $this->error('请输入命令');
+                $commandExp = explode(' ', $command);
+                $commandExp = array_values(array_filter($commandExp));
+                try {
+
+                    $output = Console::call('curd', [...$commandExp]);
+                }catch (\Throwable $exception) {
+                    $this->error($exception->getMessage() . $exception->getLine());
+                }
+                if (empty($output)) $this->error('设置错误');
+                $this->success($output->fetch());
+                break;
+            default:
+                $this->error('参数错误');
+                break;
+        }
+    }
+}

+ 138 - 0
app/admin/controller/system/Log.php

@@ -0,0 +1,138 @@
+<?php
+
+namespace app\admin\controller\system;
+
+use app\admin\model\SystemLog;
+use app\admin\service\annotation\MiddlewareAnnotation;
+use app\admin\service\tool\CommonTool;
+use app\common\controller\AdminController;
+use app\admin\service\annotation\ControllerAnnotation;
+use app\admin\service\annotation\NodeAnnotation;
+use app\Request;
+use jianyan\excel\Excel;
+use think\App;
+use think\db\exception\DbException;
+use think\db\exception\PDOException;
+use think\facade\Db;
+use think\response\Json;
+
+#[ControllerAnnotation(title: '操作日志管理')]
+class Log extends AdminController
+{
+    public function __construct(App $app)
+    {
+        parent::__construct($app);
+        self::$model = SystemLog::class;
+    }
+
+    #[NodeAnnotation(title: '列表', auth: true)]
+    public function index(Request $request): Json|string
+    {
+        if ($request->isAjax()) {
+            if (input('selectFields')) {
+                return $this->selectList();
+            }
+            [$page, $limit, $where, $excludeFields] = $this->buildTableParams(['month']);
+            $month = !empty($excludeFields['month']) ? date('Ym', strtotime($excludeFields['month'])) : date('Ym');
+            $model = (new self::$model)->setSuffix("_$month")->with('admin')->where($where);
+            try {
+                $count = $model->count();
+                $list  = $model->page($page, $limit)->order($this->sort)->select();
+            }catch (PDOException|DbException $exception) {
+                $count = 0;
+                $list  = [];
+            }
+            $data = [
+                'code'  => 0,
+                'msg'   => '',
+                'count' => $count,
+                'data'  => $list,
+            ];
+            return json($data);
+        }
+        return $this->fetch();
+    }
+
+    #[NodeAnnotation(title: '导出', auth: true)]
+    public function export()
+    {
+        if (env('EASYADMIN.IS_DEMO', false)) {
+            $this->error('演示环境下不允许操作');
+        }
+        [$page, $limit, $where, $excludeFields] = $this->buildTableParams(['month']);
+        $month     = !empty($excludeFields['month']) ? date('Ym', strtotime($excludeFields['month'])) : date('Ym');
+        $tableName = (new self::$model)->setSuffix("_$month")->getName();
+        $tableName = CommonTool::humpToLine(lcfirst($tableName));
+        $prefix    = config('database.connections.mysql.prefix');
+        $dbList    = Db::query("show full columns from {$prefix}{$tableName}");
+        $header    = [];
+        foreach ($dbList as $vo) {
+            $comment = !empty($vo['Comment']) ? $vo['Comment'] : $vo['Field'];
+            if (!in_array($vo['Field'], $this->noExportFields)) {
+                $header[] = [$comment, $vo['Field']];
+            }
+        }
+        $model = (new self::$model)->setSuffix("_$month")->with('admin')->where($where);
+        try {
+            $list = $model
+                ->limit(10000)
+                ->order('id', 'desc')
+                ->select()
+                ->toArray();
+            foreach ($list as &$vo) {
+                $vo['content']  = json_encode($vo['content'], JSON_UNESCAPED_UNICODE);
+                $vo['response'] = json_encode($vo['response'], JSON_UNESCAPED_UNICODE);
+            }
+            exportExcel($header, $list, '操作日志');
+        }catch (\Throwable $exception) {
+            $this->error($exception->getMessage());
+        }
+    }
+
+
+    #[NodeAnnotation(title: '删除指定日志', auth: true)]
+    public function deleteMonthLog(Request $request)
+    {
+        if (!$request->isAjax()) {
+            return $this->fetch();
+        }
+
+        if ($this->isDemo) $this->error('演示环境下不允许操作');
+
+        $monthsAgo = $request->param('month/d', 0);
+        if ($monthsAgo < 1) $this->error('月份错误');
+
+        $currentDate = new \DateTime();
+        $currentDate->modify("-$monthsAgo months");
+
+        $dbPrefix   = env('DB_PREFIX');
+        $dbLike     = "{$dbPrefix}system_log_";
+        $tables     = Db::query("SHOW TABLES LIKE '$dbLike%'");
+        $threshold  = date('Ym', strtotime("-$monthsAgo month"));
+        $tableNames = [];
+        try {
+            foreach ($tables as $table) {
+                $tableName = current($table);
+                if (!preg_match("/^$dbLike\d{6}$/", $tableName)) continue;
+                $datePart   = substr($tableName, -6);
+                $issetTable = Db::query("SHOW TABLES LIKE '$tableName'");
+                if (!$issetTable) continue;
+                if ($datePart - $threshold <= 0) {
+                    Db::execute("DROP TABLE `$tableName`");
+                    $tableNames[] = $tableName;
+                }
+            }
+        }catch (PDOException) {
+        }
+        if (empty($tableNames)) $this->error('没有需要删除的表');
+        $this->success('操作成功 - 共删除 ' . count($tableNames) . ' 张表<br/>' . implode('<br>', $tableNames));
+    }
+
+    #[MiddlewareAnnotation(ignore: MiddlewareAnnotation::IGNORE_LOG)]
+    #[NodeAnnotation(title: '框架日志', auth: true, ignore: NodeAnnotation::IGNORE_NODE)]
+    public function record(): Json|string
+    {
+        return (new \Wolfcode\PhpLogviewer\thinkphp\LogViewer())->fetch();
+    }
+
+}

+ 191 - 0
app/admin/controller/system/Menu.php

@@ -0,0 +1,191 @@
+<?php
+
+namespace app\admin\controller\system;
+
+use app\admin\model\SystemMenu;
+use app\admin\model\SystemNode;
+use app\admin\service\TriggerService;
+use app\common\constants\MenuConstant;
+use app\admin\service\annotation\ControllerAnnotation;
+use app\admin\service\annotation\NodeAnnotation;
+use app\common\controller\AdminController;
+use app\Request;
+use think\App;
+use think\response\Json;
+
+#[ControllerAnnotation(title: '菜单管理')]
+class Menu extends AdminController
+{
+
+    protected array $sort = [
+        'sort' => 'desc',
+        'id'   => 'asc',
+    ];
+
+    public function __construct(App $app)
+    {
+        parent::__construct($app);
+        self::$model = SystemMenu::class;
+    }
+
+    #[NodeAnnotation(title: '列表', auth: true)]
+    public function index(Request $request): Json|string
+    {
+        if ($request->isAjax()) {
+            if (input('selectFields')) {
+                return $this->selectList();
+            }
+            $count = self::$model::count();
+            $list  = self::$model::order($this->sort)->select()->toArray();
+            $data  = [
+                'code'  => 0,
+                'msg'   => '',
+                'count' => $count,
+                'data'  => $list,
+            ];
+            return json($data);
+        }
+        return $this->fetch();
+    }
+
+    #[NodeAnnotation(title: '添加', auth: true)]
+    public function add(Request $request): string
+    {
+        $id     = $request->param('id');
+        $homeId = self::$model::where(['pid' => MenuConstant::HOME_PID,])->value('id');
+        if ($id == $homeId) {
+            $this->error('首页不能添加子菜单');
+        }
+        if ($request->isPost()) {
+            $post = $request->post();
+            $rule = [
+                'pid|上级菜单'   => 'require',
+                'title|菜单名称' => 'require',
+                'icon|菜单图标'  => 'require',
+            ];
+            $this->validate($post, $rule);
+            try {
+                $save = self::$model::create($post);
+            }catch (\Exception $e) {
+                $this->error('保存失败');
+            }
+            if ($save) {
+                TriggerService::updateMenu();
+                $this->success('保存成功');
+            }else {
+                $this->error('保存失败');
+            }
+        }
+        $pidMenuList = self::$model::getPidMenuList();
+        $this->assign('id', $id);
+        $this->assign('pidMenuList', $pidMenuList);
+        return $this->fetch();
+    }
+
+    #[NodeAnnotation(title: '编辑', auth: true)]
+    public function edit(Request $request, $id = 0): string
+    {
+        $row = self::$model::find($id);
+        empty($row) && $this->error('数据不存在');
+        if ($request->isPost()) {
+            $post = $request->post();
+            $rule = [
+                'pid|上级菜单'   => 'require',
+                'title|菜单名称' => 'require',
+                'icon|菜单图标'  => 'require',
+            ];
+            $this->validate($post, $rule);
+            if ($row->pid == MenuConstant::HOME_PID) $post['pid'] = MenuConstant::HOME_PID;
+            try {
+                $save = $row->save($post);
+            }catch (\Exception $e) {
+                $this->error('保存失败');
+            }
+            if (!empty($save)) {
+                TriggerService::updateMenu();
+                $this->success('保存成功');
+            }else {
+                $this->error('保存失败');
+            }
+        }
+        $pidMenuList = self::$model::getPidMenuList();
+        $this->assign([
+            'id'          => $id,
+            'pidMenuList' => $pidMenuList,
+            'row'         => $row,
+        ]);
+        return $this->fetch();
+    }
+
+    #[NodeAnnotation(title: '删除', auth: true)]
+    public function delete(Request $request): void
+    {
+        $this->checkPostRequest();
+        $id  = $request->param('id');
+        $row = self::$model::whereIn('id', $id)->select();
+        empty($row) && $this->error('数据不存在');
+        try {
+            $save = $row->delete();
+        }catch (\Exception $e) {
+            $this->error('删除失败');
+        }
+        if ($save) {
+            TriggerService::updateMenu();
+            $this->success('删除成功');
+        }else {
+            $this->error('删除失败');
+        }
+    }
+
+    #[NodeAnnotation(title: '属性修改', auth: true)]
+    public function modify(Request $request): void
+    {
+        $this->checkPostRequest();
+        $post = $request->post();
+        $rule = [
+            'id|ID'      => 'require',
+            'field|字段' => 'require',
+            'value|值'   => 'require',
+        ];
+        $this->validate($post, $rule);
+        $row = self::$model::find($post['id']);
+        if (!$row) {
+            $this->error('数据不存在');
+        }
+        if (!in_array($post['field'], $this->allowModifyFields)) {
+            $this->error('该字段不允许修改:' . $post['field']);
+        }
+        $homeId = self::$model::where([
+            'pid' => MenuConstant::HOME_PID,
+        ])
+            ->value('id');
+        if ($post['id'] == $homeId && $post['field'] == 'status') {
+            $this->error('首页状态不允许关闭');
+        }
+        try {
+            $row->save([
+                $post['field'] => $post['value'],
+            ]);
+        }catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+        TriggerService::updateMenu();
+        $this->success('保存成功');
+    }
+
+    #[NodeAnnotation(title: '添加菜单提示', auth: true)]
+    public function getMenuTips(): Json
+    {
+        $node = input('get.keywords');
+        $list = SystemNode::whereLike('node', "%{$node}%")
+            ->field('node,title')
+            ->limit(10)
+            ->select()->toArray();
+        return json([
+            'code'    => 0,
+            'content' => $list,
+            'type'    => 'success',
+        ]);
+    }
+
+}

+ 106 - 0
app/admin/controller/system/Node.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace app\admin\controller\system;
+
+use app\admin\model\SystemNode;
+use app\admin\service\TriggerService;
+use app\common\controller\AdminController;
+use app\admin\service\annotation\ControllerAnnotation;
+use app\admin\service\annotation\NodeAnnotation;
+use app\admin\service\NodeService;
+use app\Request;
+use think\App;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\response\Json;
+
+#[ControllerAnnotation(title: '系统节点管理')]
+class Node extends AdminController
+{
+
+    public function __construct(App $app)
+    {
+        parent::__construct($app);
+        self::$model = SystemNode::class;
+    }
+
+    #[NodeAnnotation(title: '列表', auth: true)]
+    public function index(Request $request): Json|string
+    {
+        if ($request->isAjax()) {
+            if (input('selectFields')) {
+                return $this->selectList();
+            }
+            $count = self::$model::count();
+            $list  = self::$model::getNodeTreeList();
+            $data  = [
+                'code'  => 0,
+                'msg'   => '',
+                'count' => $count,
+                'data'  => $list,
+            ];
+            return json($data);
+        }
+        return $this->fetch();
+    }
+
+    #[NodeAnnotation(title: '系统节点更新', auth: true)]
+    public function refreshNode($force = 0): void
+    {
+
+        $this->checkPostRequest();
+        $nodeList = (new NodeService())->getNodeList();
+        empty($nodeList) && $this->error('暂无需要更新的系统节点');
+
+        try {
+            if ($force == 1) {
+                $updateNodeList = self::$model::whereIn('node', array_column($nodeList, 'node'))->select();
+                $formatNodeList = array_format_key($nodeList, 'node');
+                foreach ($updateNodeList as $vo) {
+                    isset($formatNodeList[$vo['node']])
+                    && self::$model::where('id', $vo['id'])->update(
+                        [
+                            'title'   => $formatNodeList[$vo['node']]['title'],
+                            'is_auth' => $formatNodeList[$vo['node']]['is_auth'],
+                        ]
+                    );
+                }
+            }
+            $existNodeList = self::$model::field('node,title,type,is_auth')->select();
+            foreach ($nodeList as $key => $vo) {
+                foreach ($existNodeList as $v) {
+                    if ($vo['node'] == $v->node) {
+                        unset($nodeList[$key]);
+                        break;
+                    }
+                }
+            }
+            if (!empty($nodeList)) {
+                (new self::$model)->saveAll($nodeList);
+                TriggerService::updateNode();
+            }
+        }catch (\Exception $e) {
+            $this->error('节点更新失败');
+        }
+        $this->success('节点更新成功');
+    }
+
+    #[NodeAnnotation(title: '清除失效节点', auth: true)]
+    public function clearNode(): void
+    {
+        $this->checkPostRequest();
+        $nodeList = (new NodeService())->getNodeList();
+        try {
+            $existNodeList  = self::$model::field('id,node,title,type,is_auth')->select()->toArray();
+            $formatNodeList = array_format_key($nodeList, 'node');
+            foreach ($existNodeList as $vo) {
+                !isset($formatNodeList[$vo['node']]) && self::$model::where('id', $vo['id'])->delete();
+            }
+            TriggerService::updateNode();
+        }catch (\Exception $e) {
+            $this->error('节点更新失败');
+        }
+        $this->success('节点更新成功');
+    }
+}

+ 27 - 0
app/admin/controller/system/Quick.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace app\admin\controller\system;
+
+
+use app\admin\model\SystemQuick;
+use app\common\controller\AdminController;
+use app\admin\service\annotation\ControllerAnnotation;
+use app\admin\service\annotation\NodeAnnotation;
+use think\App;
+
+#[ControllerAnnotation(title: '快捷入口管理')]
+class Quick extends AdminController
+{
+
+    protected array $sort = [
+        'sort' => 'desc',
+        'id'   => 'desc',
+    ];
+
+    public function __construct(App $app)
+    {
+        parent::__construct($app);
+        self::$model = SystemQuick::class;
+    }
+
+}

+ 22 - 0
app/admin/controller/system/Uploadfile.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace app\admin\controller\system;
+
+use app\admin\model\SystemUploadfile;
+use app\common\controller\AdminController;
+use app\admin\service\annotation\ControllerAnnotation;
+use app\admin\service\annotation\NodeAnnotation;
+use think\App;
+
+#[ControllerAnnotation(title: '上传文件管理')]
+class Uploadfile extends AdminController
+{
+
+    public function __construct(App $app)
+    {
+        parent::__construct($app);
+        self::$model = SystemUploadfile::class;
+        $this->assign('upload_types', config('admin.upload_types'));
+    }
+
+}

+ 0 - 0
app/admin/entity/.keep


+ 12 - 0
app/admin/entity/Test.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace app\admin\entity;
+
+use app\common\entity\BaseEntity;
+
+/**
+ * ThinkORM 4.0 实体模型案例
+ * 可与 Model 并存 或者 单独使用
+ * @package app\admin\entity
+ */
+class Test extends BaseEntity {}

+ 5 - 0
app/admin/middleware.php

@@ -0,0 +1,5 @@
+<?php
+
+return [
+    // ...
+];

+ 43 - 0
app/admin/middleware/CheckAuth.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace app\admin\middleware;
+
+use app\common\service\AuthService;
+use app\common\traits\JumpTrait;
+use app\Request;
+use Closure;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+
+class CheckAuth
+{
+    use JumpTrait;
+
+    /**
+     * @throws ModelNotFoundException
+     * @throws DbException
+     * @throws DataNotFoundException
+     */
+    public function handle(Request $request, Closure $next)
+    {
+        $adminUserInfo = $request->adminUserInfo;
+        if (empty($adminUserInfo)) return $next($request);
+        $adminConfig = config('admin');
+        $adminId     = $adminUserInfo['id'];
+
+        $authService       = app(AuthService::class, ['adminId' => $adminId]);
+        $currentNode       = $authService->getCurrentNode();
+        $currentController = parse_name($request->controller());
+
+        if (!in_array($currentController, $adminConfig['no_auth_controller']) && !in_array($currentNode, $adminConfig['no_auth_node'])) {
+            $check = $authService->checkNode($currentNode);
+            !$check && $this->error('无权限访问');
+            // 判断是否为演示环境
+            if (env('EASYADMIN.IS_DEMO', false) && $request->isPost()) {
+                if (!in_array($currentNode, ['system.log/record', 'mall.goods/aiOptimization'])) $this->error('演示环境下不允许修改');
+            }
+        }
+        return $next($request);
+    }
+}

+ 21 - 0
app/admin/middleware/CheckInstall.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace app\admin\middleware;
+
+use app\common\traits\JumpTrait;
+use app\Request;
+use Closure;
+
+class CheckInstall
+{
+    use JumpTrait;
+
+    public function handle(Request $request, Closure $next)
+    {
+        $controller = $request->controller();
+        if (!is_file(root_path() . 'config' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'lock' . DIRECTORY_SEPARATOR . 'install.lock')) {
+            if ($controller != 'Install') return redirect('/install');
+        }
+        return $next($request);
+    }
+}

+ 60 - 0
app/admin/middleware/CheckLogin.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace app\admin\middleware;
+
+use app\common\traits\JumpTrait;
+use app\Request;
+use Closure;
+use ReflectionClass;
+use ReflectionException;
+use app\admin\service\annotation\MiddlewareAnnotation;
+
+class CheckLogin
+{
+    use JumpTrait;
+
+    /**
+     * @throws ReflectionException
+     */
+    public function handle(Request $request, Closure $next)
+    {
+        $controller = $request->controller();
+        if (empty($controller)) return $next($request);
+        if (str_contains($controller, '.')) $controller = str_replace('.', '\\', $controller);
+        $action          = $request->action();
+        $controllerClass = 'app\\admin\\controller\\' . $controller;
+        $classObj        = new ReflectionClass($controllerClass);
+        $properties      = $classObj->getDefaultProperties();
+        // 整个控制器是否忽略登录
+        $ignoreLogin   = $properties['ignoreLogin'] ?? false;
+        $adminUserInfo = session('admin');
+        if (!$ignoreLogin) {
+            $noNeedCheck = $properties['noNeedCheck'] ?? [];
+            if (in_array($action, $noNeedCheck)) {
+                return $next($request);
+            }
+            try {
+                $reflectionMethod = new \ReflectionMethod($controllerClass, $action);
+                $attributes       = $reflectionMethod->getAttributes(MiddlewareAnnotation::class);
+                foreach ($attributes as $attribute) {
+                    $annotation = $attribute->newInstance();
+                    $_ignore    = (array)$annotation->ignore;
+                    // 控制器中的某个方法忽略登录
+                    if (in_array('LOGIN', $_ignore)) return $next($request);
+                }
+            }catch (\Throwable) {
+            }
+            if (empty($adminUserInfo)) {
+                return redirect(__url('login/index'));
+            }
+            // 判断是否登录过期
+            $expireTime = $adminUserInfo['expire_time'];
+            if ($expireTime !== 0 && time() > $expireTime) {
+                session('admin', null);
+                $this->error('登录已过期,请重新登录', [], __url(env('EASYADMIN.ADMIN') . '/login/index'));
+            }
+        }
+        $request->adminUserInfo = $adminUserInfo ?: [];
+        return $next($request);
+    }
+}

+ 45 - 0
app/admin/middleware/RateLimiting.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace app\admin\middleware;
+
+use app\common\traits\JumpTrait;
+use app\Request;
+use Closure;
+use Wolfcode\RateLimiting\Bootstrap;
+
+class RateLimiting
+{
+    use JumpTrait;
+
+    /**
+     * 启用限流器需要开启Redis
+     * @param Request $request
+     * @param Closure $next
+     * @return mixed
+     */
+    public function handle(Request $request, Closure $next): mixed
+    {
+        // 是否启用限流器
+        if (!env('RATE_LIMITING_STATUS', false)) return $next($request);
+        if ($request->method() == 'GET') return $next($request);
+        $controller      = $request->controller();
+        $module          = app('http')->getName();
+        $appNamespace    = config('app.app_namespace');
+        $controllerClass = "app\\{$module}\\controller\\{$controller}{$appNamespace}";
+        $controllerClass = str_replace('.', '\\', $controllerClass);
+        $action          = $request->action();
+        try {
+            Bootstrap::init($controllerClass, $action, [
+                # Redis 相关配置
+                'host'     => env('REDIS_HOST', '127.0.0.1'),
+                'port'     => (int)env('REDIS_PORT', 6379),
+                'password' => env('REDIS_PASSWORD', ''),
+                'prefix'   => env('REDIS_PREFIX', ''),
+                'database' => (int)env('REDIS_DATABASE', 0),
+            ]);
+        }catch (\Throwable $exception) {
+            $this->error($exception->getMessage());
+        }
+        return $next($request);
+    }
+}

+ 104 - 0
app/admin/middleware/SystemLog.php

@@ -0,0 +1,104 @@
+<?php
+
+namespace app\admin\middleware;
+
+use app\admin\service\annotation\ControllerAnnotation;
+use app\admin\service\annotation\MiddlewareAnnotation;
+use app\admin\service\annotation\NodeAnnotation;
+use app\admin\service\SystemLogService;
+use app\common\traits\JumpTrait;
+use app\Request;
+use Closure;
+use ReflectionException;
+
+class SystemLog
+{
+    use JumpTrait;
+
+    /**
+     * 敏感信息字段,日志记录时需要加密
+     * @var array
+     */
+    protected array $sensitiveParams = [
+        'password',
+        'password_again',
+        'phone',
+        'mobile',
+    ];
+
+    /**
+     * @throws ReflectionException
+     */
+    public function handle(Request $request, Closure $next)
+    {
+        $response = $next($request);
+        if (!env('APP_ADMIN_SYSTEM_LOG', true)) return $response;
+        $params = $request->param();
+        if (isset($params['s'])) unset($params['s']);
+        foreach ($params as $key => $val) {
+            in_array($key, $this->sensitiveParams) && $params[$key] = "***********";
+        }
+        $method = strtolower($request->method());
+        $url    = $request->url();
+
+        if (env('APP_DEBUG')) {
+            trace(['url' => $url, 'method' => $method, 'params' => $params,], 'requestDebugInfo');
+        }
+        if ($request->isAjax()) {
+            if (in_array($method, ['post', 'put', 'delete'])) {
+
+                $title = '';
+                try {
+                    $pathInfo    = $request->pathinfo();
+                    $pathInfoExp = explode('/', $pathInfo);
+                    $_action     = end($pathInfoExp) ?? '';
+                    $pathInfoExp = explode('.', $pathInfoExp[0] ?? '');
+                    $_name       = $pathInfoExp[0] ?? '';
+                    $_controller = ucfirst($pathInfoExp[1] ?? '');
+                    $className   = $_controller ? "app\admin\controller\\{$_name}\\{$_controller}" : "app\admin\controller\\{$_name}";
+                    if ($_name && $_action) {
+                        $reflectionMethod = new \ReflectionMethod($className, $_action);
+                        $attributes       = $reflectionMethod->getAttributes(MiddlewareAnnotation::class);
+                        foreach ($attributes as $attribute) {
+                            $annotation = $attribute->newInstance();
+                            $_ignore    = (array)$annotation->ignore;
+                            if (in_array('log', array_map('strtolower', $_ignore))) return $response;
+                        }
+                        $controllerTitle      = $nodeTitle = '';
+                        $controllerAttributes = (new \ReflectionClass($className))->getAttributes(ControllerAnnotation::class);
+                        $actionAttributes     = $reflectionMethod->getAttributes(NodeAnnotation::class);
+                        foreach ($controllerAttributes as $controllerAttribute) {
+                            $controllerAnnotation = $controllerAttribute->newInstance();
+                            $controllerTitle      = $controllerAnnotation->title ?? '';
+                        }
+                        foreach ($actionAttributes as $actionAttribute) {
+                            $actionAnnotation = $actionAttribute->newInstance();
+                            $nodeTitle        = $actionAnnotation->title ?? '';
+                        }
+                        $title = $controllerTitle . ' - ' . $nodeTitle;
+                    }
+                }catch (\Throwable $exception) {
+                }
+
+                $ip = $request->ip();
+                // 限制记录的响应内容,避免过大
+                $_response = json_encode($response->getData(), JSON_UNESCAPED_UNICODE);
+                $_response = mb_substr($_response, 0, 3000, 'utf-8');
+
+                $data = [
+                    'admin_id'    => session('admin.id'),
+                    'title'       => $title,
+                    'url'         => $url,
+                    'method'      => $method,
+                    'ip'          => $ip,
+                    'content'     => json_encode($params, JSON_UNESCAPED_UNICODE),
+                    'response'    => $_response,
+                    'useragent'   => $request->server('HTTP_USER_AGENT'),
+                    'create_time' => time(),
+                ];
+                SystemLogService::instance()->save($data);
+            }
+        }
+        return $response;
+    }
+}

+ 18 - 0
app/admin/model/MallCate.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace app\admin\model;
+
+
+use app\common\model\TimeModel;
+
+class MallCate extends TimeModel
+{
+
+    protected function getOptions(): array
+    {
+        return [
+            'deleteTime' => 'delete_time',
+        ];
+    }
+
+}

+ 32 - 0
app/admin/model/MallGoods.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace app\admin\model;
+
+use app\common\model\TimeModel;
+use think\model\relation\BelongsTo;
+use think\model\relation\HasOne;
+
+class MallGoods extends TimeModel
+{
+    protected function getOptions(): array
+    {
+        return [
+            'deleteTime' => 'delete_time',
+        ];
+    }
+
+    // * +++++++++++++++++++++++++++
+    // | 以下两种写法适用于 with 关联
+    // * +++++++++++++++++++++++++
+
+    //    public function cate(): BelongsTo
+    //    {
+    //        return $this->belongsTo('app\admin\model\MallCate', 'cate_id', 'id');
+    //    }
+
+    public function cate(): HasOne
+    {
+        return $this->hasOne(MallCate::class, 'id', 'cate_id');
+    }
+
+}

+ 36 - 0
app/admin/model/SystemAdmin.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace app\admin\model;
+
+
+use app\common\model\TimeModel;
+
+class SystemAdmin extends TimeModel
+{
+
+    protected function getOptions(): array
+    {
+        return [
+            'deleteTime' => 'delete_time',
+        ];
+    }
+
+    public array $notes = [
+        'login_type' => [
+            1 => '密码登录',
+            2 => '密码 + 谷歌验证码登录'
+        ],
+    ];
+
+    public static function getAuthIdsAttr($value): array
+    {
+        if (!$value) return [];
+        return explode(',', $value);
+    }
+
+    public static function getAuthList(): array
+    {
+        return SystemAuth::where('status', 1)->column('title', 'id');
+    }
+
+}

+ 61 - 0
app/admin/model/SystemAuth.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace app\admin\model;
+
+use app\common\model\TimeModel;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+
+class SystemAuth extends TimeModel
+{
+
+    protected function getOptions(): array
+    {
+        return [
+            'deleteTime' => 'delete_time',
+        ];
+    }
+
+    /**
+     * 根据角色ID获取授权节点
+     * @param $authId
+     * @return array
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public static function getAuthorizeNodeListByAdminId($authId): array
+    {
+        $checkNodeList = (new SystemAuthNode())
+            ->where('auth_id', $authId)
+            ->column('node_id');
+        $systemNode    = new SystemNode();
+        $nodeList      = $systemNode
+            ->where('is_auth', 1)
+            ->field('id,node,title,type,is_auth')
+            ->select()
+            ->toArray();
+        $newNodeList   = [];
+        foreach ($nodeList as $vo) {
+            if ($vo['type'] == 1) {
+                $vo            = array_merge($vo, ['field' => 'node', 'spread' => true]);
+                $vo['checked'] = false;
+                $vo['title']   = "{$vo['title']}【{$vo['node']}】";
+                $children      = [];
+                foreach ($nodeList as $v) {
+                    if ($v['type'] == 2 && strpos($v['node'], $vo['node'] . '/') !== false) {
+                        $v            = array_merge($v, ['field' => 'node', 'spread' => true]);
+                        $v['checked'] = in_array($v['id'], $checkNodeList) ? true : false;
+                        $v['title']   = "{$v['title']}【{$v['node']}】";
+                        $children[]   = $v;
+                    }
+                }
+                !empty($children) && $vo['children'] = $children;
+                $newNodeList[] = $vo;
+            }
+        }
+        return $newNodeList;
+    }
+
+}

+ 10 - 0
app/admin/model/SystemAuthNode.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace app\admin\model;
+
+use app\common\model\TimeModel;
+
+class SystemAuthNode extends TimeModel
+{
+
+}

+ 10 - 0
app/admin/model/SystemConfig.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace app\admin\model;
+
+use app\common\model\TimeModel;
+
+class SystemConfig extends TimeModel
+{
+
+}

+ 29 - 0
app/admin/model/SystemLog.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace app\admin\model;
+
+use app\admin\service\SystemLogService;
+use app\common\model\TimeModel;
+use think\model\relation\BelongsTo;
+
+class SystemLog extends TimeModel
+{
+
+    protected array $type = [
+        'content'  => 'json',
+        'response' => 'json',
+    ];
+
+    protected function init(): void
+    {
+        SystemLogService::instance()->detectTable();
+    }
+
+
+    public function admin(): BelongsTo
+    {
+        return $this->belongsTo('app\admin\model\SystemAdmin', 'admin_id', 'id');
+    }
+
+
+}

+ 68 - 0
app/admin/model/SystemMenu.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace app\admin\model;
+
+use app\common\constants\MenuConstant;
+use app\common\model\TimeModel;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+
+class SystemMenu extends TimeModel
+{
+    protected function getOptions(): array
+    {
+        return [
+            'deleteTime' => 'delete_time',
+        ];
+    }
+
+
+    /**
+     * @throws ModelNotFoundException
+     * @throws DbException
+     * @throws DataNotFoundException
+     */
+    public static function getPidMenuList(): array
+    {
+        $list = self::field('id,pid,title')->where([
+            ['pid', '<>', MenuConstant::HOME_PID],
+            ['status', '=', 1],
+        ])->select()->toArray();
+
+        $pidMenuList = self::buildPidMenu(0, $list);
+        return array_merge([[
+            'id'    => 0,
+            'pid'   => 0,
+            'title' => '顶级菜单',
+        ]], $pidMenuList);
+    }
+
+    protected static function buildPidMenu($pid, $list, $level = 0): array
+    {
+        $newList = [];
+        foreach ($list as $vo) {
+            if ($vo['pid'] == $pid) {
+                $level++;
+                foreach ($newList as $v) {
+                    if ($vo['pid'] == $v['pid'] && isset($v['level'])) {
+                        $level = $v['level'];
+                        break;
+                    }
+                }
+                $vo['level'] = $level;
+                if ($level > 1) {
+                    $repeatString = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
+                    $markString   = str_repeat("{$repeatString}├{$repeatString}", $level - 1);
+                    $vo['title']  = $markString . $vo['title'];
+                }
+                $newList[] = $vo;
+                $childList = self::buildPidMenu($vo['id'], $list, $level);
+                !empty($childList) && $newList = array_merge($newList, $childList);
+            }
+
+        }
+        return $newList;
+    }
+
+}

+ 35 - 0
app/admin/model/SystemNode.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace app\admin\model;
+
+use app\common\model\TimeModel;
+
+class SystemNode extends TimeModel
+{
+
+    public static function getNodeTreeList(): array
+    {
+        $list = self::select()->toArray();
+        return self::buildNodeTree($list);
+    }
+
+    protected static function buildNodeTree($list): array
+    {
+        $newList      = [];
+        $repeatString = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
+        foreach ($list as $vo) {
+            if ($vo['type'] == 1) {
+                $newList[] = $vo;
+                foreach ($list as $v) {
+                    if ($v['type'] == 2 && str_contains($v['node'], $vo['node'] . '/')) {
+                        $v['node'] = "{$repeatString}├{$repeatString}" . $v['node'];
+                        $newList[] = $v;
+                    }
+                }
+            }
+        }
+        return $newList;
+    }
+
+
+}

+ 17 - 0
app/admin/model/SystemQuick.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace app\admin\model;
+
+use app\common\model\TimeModel;
+
+class SystemQuick extends TimeModel
+{
+
+    protected function getOptions(): array
+    {
+        return [
+            'deleteTime' => 'delete_time',
+        ];
+    }
+
+}

+ 10 - 0
app/admin/model/SystemUploadfile.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace app\admin\model;
+
+use app\common\model\TimeModel;
+
+class SystemUploadfile extends TimeModel
+{
+
+}

+ 20 - 0
app/admin/service/ConfigService.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace app\admin\service;
+
+use think\facade\Cache;
+
+class ConfigService
+{
+
+    public static function getVersion()
+    {
+        $version = cache('site_version');
+        if (empty($version)) {
+            $version = sysConfig('site', 'site_version');
+            cache('site_version', $version);
+        }
+        return $version;
+    }
+
+}

+ 24 - 0
app/admin/service/NodeService.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace app\admin\service;
+
+
+use app\admin\service\auth\Node;
+
+class NodeService
+{
+
+    /**
+     * 获取节点服务
+     * @return array
+     * @throws \Doctrine\Common\Annotations\AnnotationException
+     * @throws \ReflectionException
+     */
+    public function getNodeList()
+    {
+        $basePath      = base_path() . 'admin' . DIRECTORY_SEPARATOR . 'controller';
+        $baseNamespace = "app\admin\controller";
+        $nodeList      = (new Node($basePath, $baseNamespace))->getNodeList();
+        return $nodeList;
+    }
+}

+ 128 - 0
app/admin/service/SystemLogService.php

@@ -0,0 +1,128 @@
+<?php
+
+namespace app\admin\service;
+
+use think\facade\Cache;
+use think\facade\Db;
+use think\facade\Config;
+use think\facade\Env;
+
+/**
+ * 系统日志表
+ * Class SystemLogService
+ * @package app\admin\service
+ */
+class SystemLogService
+{
+
+    protected static ?SystemLogService $instance = null;
+
+    /**
+     * 表前缀
+     * @var string
+     */
+    protected string $tablePrefix;
+
+    /**
+     * 表后缀
+     * @var string
+     */
+    protected string $tableSuffix;
+
+    /**
+     * 表名
+     * @var string
+     */
+    protected string $tableName;
+
+    /**
+     * 构造方法
+     * SystemLogService constructor.
+     */
+    protected function __construct()
+    {
+        $this->tablePrefix = Config::get('database.connections.mysql.prefix');
+        $this->tableSuffix = date('Ym', time());
+        $this->tableName   = "{$this->tablePrefix}system_log_{$this->tableSuffix}";
+    }
+
+    /**
+     * 获取实例对象
+     * @return SystemLogService
+     */
+    public static function instance(): SystemLogService
+    {
+        if (is_null(self::$instance)) {
+            self::$instance = new static();
+        }
+        return self::$instance;
+    }
+
+
+    /**
+     * 保存数据
+     * @param $data
+     * @return bool|string
+     */
+    public function save($data): bool|string
+    {
+        Db::startTrans();
+        try {
+            $this->detectTable();
+            Db::table($this->tableName)->strict(false)->insert($data);
+            Db::commit();
+        }catch (\Exception $e) {
+            Db::rollback();
+            return $e->getMessage();
+        }
+        return true;
+    }
+
+    /**
+     * 检测数据表
+     * @return bool
+     */
+    public function detectTable(): bool
+    {
+        $_key = "system_log_{$this->tableName}_table";
+        // 手动删除日志表时候 记得清除缓存
+        $isset = Cache::get($_key);
+        if ($isset) return true;
+        $check = Db::query("show tables like '{$this->tableName}'");
+        if (empty($check)) {
+            $sql = $this->getCreateSql();
+            Db::execute($sql);
+        }
+        Cache::set($_key, !empty($check));
+        return true;
+    }
+
+    public function getAllTableList()
+    {
+
+    }
+
+    /**
+     * 根据后缀获取创建表的sql
+     * @return string
+     */
+    protected function getCreateSql(): string
+    {
+        return <<<EOT
+CREATE TABLE `{$this->tableName}` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `admin_id` int(10) unsigned DEFAULT '0' COMMENT '管理员ID',
+  `url` varchar(1500) NOT NULL DEFAULT '' COMMENT '操作页面',
+  `method` varchar(50) NOT NULL COMMENT '请求方法',
+  `title` varchar(100) DEFAULT '' COMMENT '日志标题',
+  `content` json NOT NULL COMMENT '请求数据',
+  `response` json DEFAULT NULL COMMENT '回调数据',
+  `ip` varchar(50) NOT NULL DEFAULT '' COMMENT 'IP',
+  `useragent` varchar(255) DEFAULT '' COMMENT 'User-Agent',
+  `create_time` int(10) DEFAULT NULL COMMENT '操作时间',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='后台操作日志表 - {$this->tableSuffix}';
+EOT;
+    }
+
+}

+ 50 - 0
app/admin/service/TriggerService.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace app\admin\service;
+
+use think\facade\Cache;
+
+class TriggerService
+{
+
+    /**
+     * 更新菜单缓存
+     * @param null $adminId
+     * @return bool
+     */
+    public static function updateMenu($adminId = null)
+    {
+        if(empty($adminId)){
+            Cache::tag('initAdmin')->clear();
+        }else{
+            Cache::delete('initAdmin_' . $adminId);
+        }
+        return true;
+    }
+
+    /**
+     * 更新节点缓存
+     * @param null $adminId
+     * @return bool
+     */
+    public static function updateNode($adminId = null)
+    {
+        if(empty($adminId)){
+            Cache::tag('authNode')->clear();
+        }else{
+            Cache::delete('allAuthNode_' . $adminId);
+        }
+        return true;
+    }
+
+    /**
+     * 更新系统设置缓存
+     * @return bool
+     */
+    public static function updateSysConfig(): bool
+    {
+        Cache::tag('sysConfig')->clear();
+        return true;
+    }
+
+}

+ 226 - 0
app/admin/service/UploadService.php

@@ -0,0 +1,226 @@
+<?php
+
+namespace app\admin\service;
+
+use app\admin\model\SystemUploadfile;
+use OSS\Core\OssException;
+use OSS\Credentials\EnvironmentVariableCredentialsProvider;
+use OSS\OssClient;
+use think\facade\Env;
+use think\file\UploadedFile;
+use think\helper\Str;
+use Qcloud\Cos\Client;
+use Exception;
+use Qiniu\Storage\UploadManager;
+use Qiniu\Auth;
+
+class UploadService
+{
+    public static ?UploadService $_instance = null;
+    protected array              $options   = [];
+    private array                $saveData;
+
+    public static function instance(): ?UploadService
+    {
+        if (!static::$_instance) static::$_instance = new static();
+        return static::$_instance;
+    }
+
+    /**
+     * @param array $options
+     * @return $this
+     */
+    public function setConfig(array $options = []): UploadService
+    {
+        $this->options = $options;
+        return $this;
+    }
+
+    /**
+     * @return array
+     */
+    public function getConfig(): array
+    {
+        return $this->options;
+    }
+
+    /**
+     * @param UploadedFile $file
+     * @param string $base_path
+     * @return string
+     */
+    protected function setFilePath(UploadedFile $file, string $base_path = ''): string
+    {
+        $path = date('Ymd') . '/' . Str::random(3) . time() . Str::random() . '.' . $file->extension();
+        return $base_path . $path;
+    }
+
+    /**
+     * @param UploadedFile $file
+     * @return UploadService
+     */
+    protected function setSaveData(UploadedFile $file): static
+    {
+        $options        = $this->options;
+        $data           = [
+            'upload_type'   => $options['upload_type'],
+            'original_name' => $file->getOriginalName(),
+            'mime_type'     => $file->getMime(),
+            'file_size'     => $file->getSize(),
+            'file_ext'      => strtolower($file->extension()),
+            'create_time'   => time(),
+        ];
+        $this->saveData = $data;
+        return $this;
+    }
+
+    /**
+     * 本地存储
+     *
+     * @param UploadedFile $file
+     * @param string $type
+     * @return array
+     */
+    public function local(UploadedFile $file, string $type = ''): array
+    {
+        if ($file->isValid()) {
+            $base_path = '/storage/' . date('Ymd') . '/';
+            // 上传文件的目标文件夹
+            $destinationPath = public_path() . $base_path;
+            $this->setSaveData($file);
+            // 将文件移动到目标文件夹中
+            $move = $file->move($destinationPath, Str::random(3) . time() . Str::random() . session('admin.id') . '.' . $file->extension());
+            $url  = $base_path . $move->getFilename();
+            $data = ['url' => $url];
+            $this->save($url);
+            return ['code' => 1, 'data' => $data];
+        }
+        $data = '上传失败';
+        return ['code' => 0, 'data' => $data];
+    }
+
+    /**
+     * 阿里云OSS
+     *
+     * @param UploadedFile $file
+     * @param string $type
+     * @return array
+     */
+    public function oss(UploadedFile $file, string $type = ''): array
+    {
+        $config          = $this->getConfig();
+        $accessKeyId     = $config['oss_access_key_id'];
+        $accessKeySecret = $config['oss_access_key_secret'];
+        $endpoint        = $config['oss_endpoint'];
+        $bucket          = $config['oss_bucket'];
+        // 升级 aliyuncs/oss-sdk-php 到 v2.7.2 以上, 使用签名 v4 版本
+        putenv('OSS_ACCESS_KEY_ID=' . $accessKeyId);
+        putenv('OSS_ACCESS_KEY_SECRET=' . $accessKeySecret);
+        $region   = str_replace(['http://oss-', 'https://oss-', 'oss-'], '', explode('.aliyuncs.com', $endpoint)[0] ?? '');
+        $provider = new EnvironmentVariableCredentialsProvider();
+        $args     = [
+            "provider"         => $provider,
+            "endpoint"         => $endpoint,
+            "signatureVersion" => OssClient::OSS_SIGNATURE_VERSION_V4,
+            "region"           => $region
+        ];
+        if ($file->isValid()) {
+            $object = $this->setFilePath($file, Env::get('EASYADMIN.OSS_STATIC_PREFIX', 'easyadmin8') . '/');
+            try {
+                $ossClient       = new OssClient($args);
+                $_rs             = $ossClient->putObject($bucket, $object, file_get_contents($file->getRealPath()));
+                $oss_request_url = $_rs['oss-request-url'] ?? '';
+                if (empty($oss_request_url)) return ['code' => 0, 'data' => '上传至OSS失败'];
+                $oss_request_url = str_replace('http://', 'https://', $oss_request_url);
+                $this->setSaveData($file);
+            } catch (OssException $e) {
+                return ['code' => 0, 'data' => $e->getMessage()];
+            }
+            $data = ['url' => $oss_request_url];
+            $this->save($oss_request_url);
+            return ['code' => 1, 'data' => $data];
+        }
+        $data = '上传失败';
+        return ['code' => 0, 'data' => $data];
+    }
+
+    /**
+     * 腾讯云cos
+     *
+     * @param UploadedFile $file
+     * @param string $type
+     * @return array
+     */
+    public function cos(UploadedFile $file, string $type = ''): array
+    {
+        $config    = $this->getConfig();
+        $secretId  = $config['cos_secret_id'];              //替换为用户的 secretId,请登录访问管理控制台进行查看和管理,https://console.cloud.tencent.com/cam/capi
+        $secretKey = $config['cos_secret_key'];             //替换为用户的 secretKey,请登录访问管理控制台进行查看和管理,https://console.cloud.tencent.com/cam/capi
+        $region    = $config['cos_region'];                 //替换为用户的 region,已创建桶归属的region可以在控制台查看,https://console.cloud.tencent.com/cos5/bucket
+        if ($file->isValid()) {
+            $cosClient = new Client(
+                [
+                    'region'      => $region,
+                    'schema'      => 'http',
+                    'credentials' => ['secretId' => $secretId, 'secretKey' => $secretKey,
+                    ],
+                ]);
+            try {
+                $object   = $this->setFilePath($file, Env::get('EASYADMIN.OSS_STATIC_PREFIX', 'easyadmin8') . '/');
+                $result   = $cosClient->upload(
+                    $config['cos_bucket'],         //存储桶名称,由BucketName-Appid 组成,可以在COS控制台查看 https://console.cloud.tencent.com/cos5/bucket
+                    $object,                       //此处的 key 为对象键
+                    file_get_contents($file->getRealPath())
+                );
+                $location = $result['Location'] ?? '';
+                if (empty($location)) return ['code' => 0, 'data' => '上传至COS失败'];
+                $location = 'https://' . $location;
+                $this->setSaveData($file);
+            }catch (Exception $e) {
+                return ['code' => 0, 'data' => $e->getMessage()];
+            }
+            $data = ['url' => $location];
+            $this->save($location);
+            return ['code' => 1, 'data' => $data];
+        }
+        $data = '上传失败';
+        return ['code' => 0, 'data' => $data];
+    }
+
+    /**
+     * 七牛云
+     *
+     * @param UploadedFile $file
+     * @param string $type
+     * @return array
+     * @throws Exception
+     */
+    public function qnoss(UploadedFile $file, string $type = ''): array
+    {
+        if (!$file->isValid()) return ['code' => 1, 'data' => '上传验证失败'];
+        $uploadMgr = new UploadManager();
+        $config    = $this->getConfig();
+        $accessKey = $config['qnoss_access_key'];
+        $secretKey = $config['qnoss_secret_key'];
+        $bucket    = $config['qnoss_bucket'];
+        $domain    = $config['qnoss_domain'];
+        $auth      = new Auth($accessKey, $secretKey);
+        $token     = $auth->uploadToken($bucket);
+        $object    = $this->setFilePath($file, Env::get('EASYADMIN.OSS_STATIC_PREFIX', 'easyadmin8') . '/');
+        list($ret, $error) = $uploadMgr->putFile($token, $object, $file->getRealPath());
+        if (empty($ret)) return ['code' => 0, 'data' => $error->getResponse()->error ?? '上传失败,请检查七牛云相关参数配置'];
+        $url  = $domain . "/" . $ret['key'];
+        $data = ['url' => $url];
+        $this->setSaveData($file);
+        $this->save($url);
+        return ['code' => 1, 'data' => $data];
+    }
+
+    protected function save(string $url = ''): bool
+    {
+        $data                = $this->saveData;
+        $data['url']         = $url;
+        $data['upload_time'] = time();
+        return (new SystemUploadfile())->save($data);
+    }
+}

+ 21 - 0
app/admin/service/annotation/ControllerAnnotation.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace app\admin\service\annotation;
+
+use Attribute;
+
+/**
+ * controller 节点注解类
+ */
+#[Attribute]
+final class ControllerAnnotation
+{
+    /**
+     * @param string $title
+     * @param bool $auth 是否需要权限
+     * @param string|array $ignore
+     */
+    public function __construct(public string $title = '', public bool $auth = true, public string|array $ignore = '')
+    {
+    }
+}

+ 19 - 0
app/admin/service/annotation/MiddlewareAnnotation.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace app\admin\service\annotation;
+
+use Attribute;
+
+#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
+final class MiddlewareAnnotation
+{
+    /** 过滤日志 */
+    const IGNORE_LOG = 'LOG';
+
+    /** 免登录 */
+    const IGNORE_LOGIN = 'LOGIN';
+
+    public function __construct(public string $type = '', public string|array $ignore = '')
+    {
+    }
+}

+ 25 - 0
app/admin/service/annotation/NodeAnnotation.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace app\admin\service\annotation;
+
+use Attribute;
+
+/**
+ * action 节点注解类
+ */
+#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD| Attribute::TARGET_PROPERTY)]
+final class NodeAnnotation
+{
+    /** 过滤节点 */
+    const IGNORE_NODE = 'NODE';
+
+    /**
+     * @param string $title
+     * @param bool $auth 是否需要权限
+     * @param string|array $ignore
+     */
+    public function __construct(public string $title = '', public bool $auth = true, public string|array $ignore = '')
+    {
+    }
+
+}

+ 155 - 0
app/admin/service/auth/Node.php

@@ -0,0 +1,155 @@
+<?php
+
+namespace app\admin\service\auth;
+
+use Doctrine\Common\Annotations\AnnotationException;
+use Doctrine\Common\Annotations\AnnotationReader;
+use Doctrine\Common\Annotations\AnnotationRegistry;
+use Doctrine\Common\Annotations\DocParser;
+use app\admin\service\annotation\ControllerAnnotation;
+use app\admin\service\annotation\NodeAnnotation;
+use app\admin\service\tool\CommonTool;
+use ReflectionException;
+
+/**
+ * 节点处理类
+ * Class Node
+ * @package EasyAdmin\auth
+ */
+class Node
+{
+
+    /**
+     * @var string 当前文件夹
+     */
+    protected string $basePath;
+
+    /**
+     * @var string 命名空间前缀
+     */
+    protected string $baseNamespace;
+
+    /**
+     * 构造方法
+     * Node constructor.
+     * @param string $basePath 读取的文件夹
+     * @param string $baseNamespace 读取的命名空间前缀
+     */
+    public function __construct(string $basePath, string $baseNamespace)
+    {
+        $this->basePath      = $basePath;
+        $this->baseNamespace = $baseNamespace;
+        return $this;
+    }
+
+    /**
+     * 获取所有节点
+     * @return array
+     * @throws AnnotationException
+     * @throws ReflectionException
+     */
+    public function getNodeList(): array
+    {
+        list($nodeList, $controllerList) = [[], $this->getControllerList()];
+
+        if (!empty($controllerList)) {
+            AnnotationRegistry::loadAnnotationClass('class_exists');
+            $parser = new DocParser();
+            $parser->setIgnoreNotImportedAnnotations(true);
+            $reader = new AnnotationReader($parser);
+
+            foreach ($controllerList as $controllerFormat => $controller) {
+
+                // 获取类和方法的注释信息
+                $reflectionClass = new \ReflectionClass($controller);
+                $methods         = $reflectionClass->getMethods();
+                $actionList      = [];
+
+                // 遍历读取所有方法的注释的参数信息
+                foreach ($methods as $method) {
+
+                    // 忽略掉不需要的节点
+                    $property           = $reflectionClass->getProperty('ignoreNode');
+                    $propertyAttributes = $property->getAttributes(NodeAnnotation::class);
+                    if (!empty($propertyAttributes[0])) {
+                        $propertyAttribute = $propertyAttributes[0]->newInstance();
+                        if (in_array($method->name, $propertyAttribute->ignore)) continue;
+                    }
+
+                    $attributes = $reflectionClass->getMethod($method->name)->getAttributes(NodeAnnotation::class);
+                    foreach ($attributes as $attribute) {
+                        $annotation = $attribute->newInstance();
+                        if (!empty($annotation->ignore)) if (strtolower($annotation->ignore) == 'node') continue;
+                        $actionList[] = [
+                            'node'    => $controllerFormat . '/' . $method->name,
+                            'title'   => $annotation->title ?? null,
+                            'is_auth' => $annotation->auth ?? false,
+                            'type'    => 2,
+                        ];
+                    }
+                }
+                // 方法非空才读取控制器注解
+                if (!empty($actionList)) {
+                    // 读取Controller的注解
+                    $attributes = $reflectionClass->getAttributes(ControllerAnnotation::class);
+                    foreach ($attributes as $attribute) {
+                        $controllerAnnotation = $attribute->newInstance();
+                        $nodeList[]           = [
+                            'node'    => $controllerFormat,
+                            'title'   => $controllerAnnotation->title ?? null,
+                            'is_auth' => $controllerAnnotation->auth ?? false,
+                            'type'    => 1,
+                        ];
+                    }
+                    $nodeList = array_merge($nodeList, $actionList);
+                }
+
+            }
+        }
+        return $nodeList;
+    }
+
+    /**
+     * 获取所有控制器
+     * @return array
+     */
+    public function getControllerList(): array
+    {
+        return $this->readControllerFiles($this->basePath);
+    }
+
+    /**
+     * 遍历读取控制器文件
+     * @param $path
+     * @return array
+     */
+    protected function readControllerFiles($path): array
+    {
+        list($list, $temp_list, $dirExplode) = [[], scandir($path), explode($this->basePath, $path)];
+        $middleDir = !empty($dirExplode[1]) ? str_replace('/', '\\', substr($dirExplode[1], 1)) . "\\" : '';
+
+        foreach ($temp_list as $file) {
+            // 排除根目录和没有开启注解的模块
+            if ($file == ".." || $file == ".") {
+                continue;
+            }
+            if (is_dir($path . DIRECTORY_SEPARATOR . $file)) {
+                // 子文件夹,进行递归
+                $childFiles = $this->readControllerFiles($path . DIRECTORY_SEPARATOR . $file);
+                $list       = array_merge($childFiles, $list);
+            }else {
+                // 判断是不是控制器
+                $fileExplodeArray = explode('.', $file);
+                if (count($fileExplodeArray) != 2 || end($fileExplodeArray) != 'php') {
+                    continue;
+                }
+                // 根目录下的文件
+                $className               = str_replace('.php', '', $file);
+                $controllerFormat        = str_replace('\\', '.', $middleDir) . CommonTool::humpToLine(lcfirst($className));
+                $list[$controllerFormat] = "{$this->baseNamespace}\\{$middleDir}" . $className;
+            }
+        }
+        return $list;
+    }
+
+}

+ 166 - 0
app/admin/service/console/CliEcho.php

@@ -0,0 +1,166 @@
+<?php
+
+namespace app\admin\service\console;
+
+class CliEcho
+{
+
+    private array $foreground_colors = [];
+
+    private array $background_colors = [];
+
+    private static array $foregroundColors = [
+        'black'        => '0;30',
+        'dark_gray'    => '1;30',
+        'blue'         => '0;34',
+        'light_blue'   => '1;34',
+        'green'        => '0;32',
+        'light_green'  => '1;32',
+        'cyan'         => '0;36',
+        'light_cyan'   => '1;36',
+        'red'          => '0;31',
+        'light_red'    => '1;31',
+        'purple'       => '0;35',
+        'light_purple' => '1;35',
+        'brown'        => '0;33',
+        'yellow'       => '1;33',
+        'light_gray'   => '0;37',
+        'white'        => '1;37',
+    ];
+
+    private static $backgroundColors = [
+        'black'      => '40',
+        'red'        => '41',
+        'green'      => '42',
+        'yellow'     => '43',
+        'blue'       => '44',
+        'magenta'    => '45',
+        'cyan'       => '46',
+        'light_gray' => '47',
+    ];
+
+    public function __construct()
+    {
+        // Set up shell colors
+        $this->foreground_colors['black']        = '0;30';
+        $this->foreground_colors['dark_gray']    = '1;30';
+        $this->foreground_colors['blue']         = '0;34';
+        $this->foreground_colors['light_blue']   = '1;34';
+        $this->foreground_colors['green']        = '0;32';
+        $this->foreground_colors['light_green']  = '1;32';
+        $this->foreground_colors['cyan']         = '0;36';
+        $this->foreground_colors['light_cyan']   = '1;36';
+        $this->foreground_colors['red']          = '0;31';
+        $this->foreground_colors['light_red']    = '1;31';
+        $this->foreground_colors['purple']       = '0;35';
+        $this->foreground_colors['light_purple'] = '1;35';
+        $this->foreground_colors['brown']        = '0;33';
+        $this->foreground_colors['yellow']       = '1;33';
+        $this->foreground_colors['light_gray']   = '0;37';
+        $this->foreground_colors['white']        = '1;37';
+        $this->background_colors['black']        = '40';
+        $this->background_colors['red']          = '41';
+        $this->background_colors['green']        = '42';
+        $this->background_colors['yellow']       = '43';
+        $this->background_colors['blue']         = '44';
+        $this->background_colors['magenta']      = '45';
+        $this->background_colors['cyan']         = '46';
+        $this->background_colors['light_gray']   = '47';
+    }
+
+    // Returns colored string
+    public function getColoredString($string, $foreground_color = null, $background_color = null, $new_line = false): string
+    {
+        $colored_string = '';
+        // Check if given foreground color found
+        if (isset($this->foreground_colors[$foreground_color])) {
+            $colored_string .= "\033[" . $this->foreground_colors[$foreground_color] . 'm';
+        }
+        // Check if given background color found
+        if (isset($this->background_colors[$background_color])) {
+            $colored_string .= "\033[" . $this->background_colors[$background_color] . 'm';
+        }
+        // Add string and end coloring
+        $colored_string .= $string . "\033[0m";
+        return $new_line ? $colored_string . PHP_EOL : $colored_string;
+    }
+
+    // Returns all foreground color names
+    public function getForegroundColors(): array
+    {
+        return array_keys($this->foreground_colors);
+    }
+
+    // Returns all background color names
+    public function getBackgroundColors(): array
+    {
+        return array_keys($this->background_colors);
+    }
+
+    /**
+     * 获取带颜色的文字.
+     *
+     * @param string $string black|dark_gray|blue|light_blue|green|light_green|cyan|light_cyan|red|light_red|purple|brown|yellow|light_gray|white
+     * @param string|null $foregroundColor 前景颜色 black|red|green|yellow|blue|magenta|cyan|light_gray
+     * @param string|null $backgroundColor 背景颜色 同$foregroundColor
+     *
+     * @return string
+     */
+    public static function initColoredString(
+        string  $string,
+        ?string $foregroundColor = null,
+        ?string $backgroundColor = null
+    ): string
+    {
+        $coloredString = '';
+        if (isset(static::$foregroundColors[$foregroundColor])) {
+            $coloredString .= "\033[" . static::$foregroundColors[$foregroundColor] . 'm';
+        }
+        if (isset(static::$backgroundColors[$backgroundColor])) {
+            $coloredString .= "\033[" . static::$backgroundColors[$backgroundColor] . 'm';
+        }
+        $coloredString .= $string . "\033[0m";
+        return $coloredString;
+    }
+
+    /**
+     * 输出提示信息.
+     *
+     * @param $msg
+     */
+    public static function notice($msg): void
+    {
+        fwrite(STDOUT, self::initColoredString($msg, 'light_gray') . PHP_EOL);
+    }
+
+    /**
+     * 输出错误信息.
+     *
+     * @param $msg
+     */
+    public static function error($msg): void
+    {
+        fwrite(STDERR, self::initColoredString($msg, 'white', 'red') . PHP_EOL);
+    }
+
+    /**
+     * 输出警告信息.
+     *
+     * @param $msg
+     */
+    public static function warn($msg): void
+    {
+        fwrite(STDOUT, self::initColoredString($msg, 'red', 'yellow') . PHP_EOL);
+    }
+
+    /**
+     * 输出成功信息.
+     *
+     * @param $msg
+     */
+    public static function success($msg): void
+    {
+        fwrite(STDOUT, self::initColoredString($msg, 'light_cyan') . PHP_EOL);
+    }
+
+}

+ 1575 - 0
app/admin/service/curd/BuildCurd.php

@@ -0,0 +1,1575 @@
+<?php
+
+namespace app\admin\service\curd;
+
+use app\admin\service\curd\exceptions\TableException;
+use app\admin\service\tool\CommonTool;
+use Exception;
+use think\exception\FileException;
+use think\facade\Db;
+
+/**
+ * 快速构建系统CURD
+ * Class BuildCurd
+ * @package EasyAdmin\curd
+ */
+class BuildCurd
+{
+
+    /**
+     * 当前目录
+     * @var string
+     */
+    protected string $dir;
+
+    /**
+     * 应用目录
+     * @var string
+     */
+    protected string $rootDir;
+
+    /**
+     * 分隔符
+     * @var string
+     */
+    protected string $DS = DIRECTORY_SEPARATOR;
+
+    /**
+     * 数据库名
+     * @var string
+     */
+    protected mixed $dbName;
+
+    /**
+     *  表前缀
+     * @var string
+     */
+    protected mixed $tablePrefix = 'ea';
+
+    /**
+     * 主表
+     * @var string
+     */
+    protected string $table;
+
+    /**
+     * 表注释名
+     * @var string
+     */
+    protected string $tableComment;
+
+    /**
+     * 主表列信息
+     * @var array
+     */
+    protected array $tableColumns;
+
+    /**
+     * 数据列表可见字段
+     * @var string
+     */
+    protected string $fields;
+
+    /**
+     * 是否软删除模式
+     * @var bool
+     */
+    protected bool $delete = false;
+
+    /**
+     * 是否强制覆盖
+     * @var bool
+     */
+    protected bool $force = false;
+
+    /**
+     * 关联模型
+     * @var array
+     */
+    protected array $relationArray = [];
+
+    /**
+     * 控制器对应的URL
+     * @var string
+     */
+    protected string $controllerUrl;
+
+    /**
+     * 生成的控制器名
+     * @var string
+     */
+    protected string $controllerFilename;
+
+
+    /**
+     * 控制器命名
+     * @var string
+     */
+    protected string $controllerName;
+
+    /**
+     * 控制器命名空间
+     * @var string
+     */
+    protected string $controllerNamespace;
+
+    /**
+     * 视图名
+     * @var string
+     */
+    protected string $viewFilename;
+
+    /**
+     * js文件名
+     * @var string
+     */
+    protected string $jsFilename;
+
+    /**
+     * 生成的模型文件名
+     * @var string
+     */
+    protected string $modelFilename;
+
+    /**
+     * 主表模型命名
+     * @var string
+     */
+    protected string $modelName;
+
+    /**
+     * 复选框字段后缀
+     * @var array
+     */
+    protected array $checkboxFieldSuffix = ['checkbox'];
+
+    /**
+     * 单选框字段后缀
+     * @var array
+     */
+    protected array $radioFieldSuffix = ['radio'];
+
+    /**
+     * 下拉字段后缀
+     * @var array
+     */
+    protected array $selectFieldSuffix = ['select'];
+
+    /**
+     * 单图片字段后缀
+     * @var array
+     */
+    protected array $imageFieldSuffix = ['image', 'logo', 'photo', 'icon'];
+
+    /**
+     * 多图片字段后缀
+     * @var array
+     */
+    protected array $imagesFieldSuffix = ['images', 'photos', 'icons'];
+
+    /**
+     * 单文件字段后缀
+     * @var array
+     */
+    protected array $fileFieldSuffix = ['file'];
+
+    /**
+     * 多文件字段后缀
+     * @var array
+     */
+    protected array $filesFieldSuffix = ['files'];
+
+    /**
+     * 日期字段后缀
+     * @var array
+     */
+    protected array $dateFieldSuffix = ['date', 'time'];
+    /**
+     * 日期时间字段后缀
+     * @var array
+     */
+    protected array $datetimeFieldSuffix = ['datetime'];
+
+    /**
+     * 开关组件字段
+     * @var array
+     */
+    protected array $switchFields = [];
+
+    /**
+     * 下拉选择字段
+     * @var array
+     */
+    protected array $selectFields = ['select'];
+
+    /**
+     * 单选字段
+     * @var array
+     */
+    protected array $radioFields = ['radio'];
+
+    /**
+     * 复选字段
+     * @var array
+     */
+    protected array $checkboxFields = ['checkbox'];
+
+    /**
+     * 富文本字段
+     * @var array
+     */
+    protected array $editorFields = [];
+
+    /**
+     * 排序字段
+     * @var array
+     */
+    protected array $sortFields = [];
+
+    /**
+     * 忽略字段
+     * @var array
+     */
+    protected array $ignoreFields = ['update_time', 'delete_time'];
+
+    /**
+     * 外键字段
+     * @var array
+     */
+    protected array $foreignKeyFields = [];
+
+    /**
+     * 相关生成文件
+     * @var array
+     */
+    protected array $fileList = [];
+
+    /**
+     * 表单类型
+     * @var array
+     */
+    protected array $formTypeArray = ['text', 'image', 'images', 'file', 'files', 'select', 'switch', 'date', 'editor', 'textarea', 'checkbox', 'radio'];
+
+    /**
+     * 初始化
+     * BuildCurd constructor.
+     */
+    public function __construct()
+    {
+        $this->tablePrefix = config('database.connections.mysql.prefix');
+        $this->dbName      = config('database.connections.mysql.database');
+        $this->dir         = __DIR__;
+        $this->rootDir     = root_path();
+        return $this;
+    }
+
+    public function setTablePrefix($prefix): static
+    {
+        $this->tablePrefix = $prefix;
+        return $this;
+    }
+
+    /**
+     * 设置主表
+     * @param $table
+     * @return $this
+     * @throws TableException
+     */
+    public function setTable($table): static
+    {
+        $this->table = $table;
+        try {
+
+            // 获取表列注释
+            $columns = Db::query("SHOW FULL COLUMNS FROM {$this->tablePrefix}{$this->table}");
+            foreach ($columns as $vo) {
+                $colum = [
+                    'type'     => $vo['Type'],
+                    'comment'  => !empty($vo['Comment']) ? $vo['Comment'] : $vo['Field'],
+                    'required' => $vo['Null'] == "NO",
+                    'default'  => $vo['Default'],
+                ];
+
+                // 格式化列数据
+                $this->buildColum($colum);
+
+                $this->tableColumns[$vo['Field']] = $colum;
+
+                if ($vo['Field'] == 'delete_time') {
+                    $this->delete = true;
+                }
+            }
+            $this->tableComment = $this->table;
+        } catch (Exception $e) {
+            throw new TableException($e->getMessage());
+        }
+
+        // 初始化默认控制器名
+        $nodeArray = explode('_', $this->table);
+        if (count($nodeArray) == 1) {
+            $this->controllerFilename = ucfirst($nodeArray[0]);
+        } else {
+            foreach ($nodeArray as $k => $v) {
+                if ($k == 0) {
+                    $this->controllerFilename = "{$v}{$this->DS}";
+                } else {
+                    $this->controllerFilename .= ucfirst($v);
+                }
+            }
+        }
+
+        // 初始化默认模型名
+        $this->modelFilename = ucfirst(CommonTool::lineToHump($this->table));
+
+        $this->buildViewJsUrl();
+
+        // 构建数据
+        $this->buildStructure();
+
+        return $this;
+    }
+
+    /**
+     * 设置关联表
+     * @param $relationTable
+     * @param $foreignKey
+     * @param null $primaryKey
+     * @param null $modelFilename
+     * @param array $onlyShowFields
+     * @param null $bindSelectField
+     * @return $this
+     * @throws TableException
+     */
+    public function setRelation($relationTable, $foreignKey, $primaryKey = null, $modelFilename = null, array $onlyShowFields = [], $bindSelectField = null): static
+    {
+        if (!isset($this->tableColumns[$foreignKey])) {
+            throw new TableException("主表不存在外键字段:{$foreignKey}");
+        }
+        if (!empty($modelFilename)) {
+            $modelFilename = str_replace('/', $this->DS, $modelFilename);
+        }
+        try {
+            $columns       = Db::query("SHOW FULL COLUMNS FROM {$this->tablePrefix}{$relationTable}");
+            $formatColumns = [];
+            $delete        = false;
+            if (!empty($bindSelectField) && !in_array($bindSelectField, array_column($columns, 'Field'))) {
+                throw new TableException("关联表{$relationTable}不存在该字段: {$bindSelectField}");
+            }
+            $onlyFields = [];
+            foreach ($columns as $vo) {
+                if (empty($primaryKey) && $vo['Key'] == 'PRI') {
+                    $primaryKey = $vo['Field'];
+                }
+                if (!empty($onlyShowFields) && !in_array($vo['Field'], $onlyShowFields)) {
+                    continue;
+                }
+                if (!empty($onlyShowFields)) $onlyFields[] = $vo['Field'];
+                $colum = [
+                    'type'    => $vo['Type'],
+                    'comment' => $vo['Comment'],
+                    'default' => $vo['Default'],
+                ];
+
+                $this->buildColum($colum);
+
+                $formatColumns[$vo['Field']] = $colum;
+                if ($vo['Field'] == 'delete_time') {
+                    $delete = true;
+                }
+            }
+
+            $modelFilename = empty($modelFilename) ? ucfirst(CommonTool::lineToHump($relationTable)) : $modelFilename;
+            $modelArray    = explode($this->DS, $modelFilename);
+            $modelName     = array_pop($modelArray);
+
+            $relation = [
+                'modelFilename'   => $modelFilename,
+                'modelName'       => $modelName,
+                'foreignKey'      => $foreignKey,
+                'primaryKey'      => $primaryKey,
+                'bindSelectField' => $bindSelectField,
+                'delete'          => $delete,
+                'tableColumns'    => $formatColumns,
+                'onlyFields'      => $onlyFields,
+            ];
+            if (!empty($bindSelectField)) {
+                $relationArray                                      = explode('\\', $modelFilename);
+                $this->tableColumns[$foreignKey]['bindSelectField'] = $bindSelectField;
+                $this->tableColumns[$foreignKey]['bindRelation']    = lcfirst(end($relationArray)) . ucfirst($bindSelectField);
+            }
+            $this->relationArray[$relationTable] = $relation;
+            $this->selectFields[]                = $foreignKey;
+        } catch (Exception $e) {
+            throw new TableException($e->getMessage());
+        }
+        return $this;
+    }
+
+    /**
+     * 设置控制器名
+     * @param $controllerFilename
+     * @return $this
+     */
+    public function setControllerFilename($controllerFilename): static
+    {
+        $this->controllerFilename = str_replace('/', $this->DS, $controllerFilename);
+        $this->buildViewJsUrl();
+        return $this;
+    }
+
+    /**
+     * 设置模型名
+     * @param $modelFilename
+     * @return $this
+     */
+    public function setModelFilename($modelFilename): static
+    {
+        $this->modelFilename = str_replace('/', $this->DS, $modelFilename);
+        $this->buildViewJsUrl();
+        return $this;
+    }
+
+    /**
+     * 设置显示字段
+     * @param $fields
+     * @return $this
+     */
+    public function setFields($fields): static
+    {
+        $this->fields = $fields;
+        return $this;
+    }
+
+    /**
+     * 设置删除模式
+     * @param $delete
+     * @return $this
+     */
+    public function setDelete($delete): static
+    {
+        $this->delete = $delete;
+        return $this;
+    }
+
+    /**
+     * 设置是否强制替换
+     * @param $force
+     * @return $this
+     */
+    public function setForce($force): static
+    {
+        $this->force = $force;
+        return $this;
+    }
+
+    /**
+     * 设置复选框字段后缀
+     * @param $array
+     * @return $this
+     */
+    public function setCheckboxFieldSuffix($array, $replace = false): static
+    {
+        $this->checkboxFieldSuffix = $replace ? $array : array_merge($this->checkboxFieldSuffix, $array);
+        return $this;
+    }
+
+    /**
+     * 设置单选框字段后缀
+     * @param $array
+     * @return $this
+     */
+    public function setRadioFieldSuffix($array, $replace = false): static
+    {
+        $this->radioFieldSuffix = $replace ? $array : array_merge($this->radioFieldSuffix, $array);
+        return $this;
+    }
+
+    /**
+     * 设置单图片字段后缀
+     * @param $array
+     * @return $this
+     */
+    public function setImageFieldSuffix($array, $replace = false): static
+    {
+        $this->imageFieldSuffix = $replace ? $array : array_merge($this->imageFieldSuffix, $array);
+        return $this;
+    }
+
+    /**
+     * 设置多图片字段后缀
+     * @param $array
+     * @return $this
+     */
+    public function setImagesFieldSuffix($array, $replace = false): static
+    {
+        $this->imagesFieldSuffix = $replace ? $array : array_merge($this->imagesFieldSuffix, $array);
+        return $this;
+    }
+
+    /**
+     * 设置单文件字段后缀
+     * @param $array
+     * @return $this
+     */
+    public function setFileFieldSuffix($array, $replace = false): static
+    {
+        $this->fileFieldSuffix = $replace ? $array : array_merge($this->fileFieldSuffix, $array);
+        return $this;
+    }
+
+    /**
+     * 设置多文件字段后缀
+     * @param $array
+     * @return $this
+     */
+    public function setFilesFieldSuffix($array, $replace = false): static
+    {
+        $this->filesFieldSuffix = $replace ? $array : array_merge($this->filesFieldSuffix, $array);
+        return $this;
+    }
+
+    /**
+     * 设置日期字段后缀
+     * @param $array
+     * @return $this
+     */
+    public function setDateFieldSuffix($array, $replace = false): static
+    {
+        $this->dateFieldSuffix = $replace ? $array : array_merge($this->dateFieldSuffix, $array);
+        return $this;
+    }
+
+    /**
+     * 设置日期时间字段后缀
+     * @param $array
+     * @return $this
+     */
+    public function setDatetimeFieldSuffix($array, $replace = false): static
+    {
+        $this->datetimeFieldSuffix = $replace ? $array : array_merge($this->datetimeFieldSuffix, $array);
+        return $this;
+    }
+
+    /**
+     * 设置开关字段
+     * @param $array
+     * @return $this
+     */
+    public function setSwitchFields($array, $replace = false): static
+    {
+        $this->switchFields = $replace ? $array : array_merge($this->switchFields, $array);
+        return $this;
+    }
+
+    /**
+     * 设置下拉选择字段
+     * @param $array
+     * @return $this
+     */
+    public function setSelectFields($array, $replace = false): static
+    {
+        $this->selectFields = $replace ? $array : array_merge($this->selectFields, $array);
+        return $this;
+    }
+
+    /**
+     * 设置排序字段
+     * @param $array
+     * @return $this
+     */
+    public function setSortFields($array, $replace = false): static
+    {
+        $this->sortFields = $replace ? $array : array_merge($this->sortFields, $array);
+        return $this;
+    }
+
+    /**
+     * 设置忽略字段
+     * @param $array
+     * @return $this
+     */
+    public function setIgnoreFields($array, $replace = false): static
+    {
+        $this->ignoreFields = $replace ? $array : array_merge($this->ignoreFields, $array);
+        return $this;
+    }
+
+    public function setEditorFields($array, $replace = false): static
+    {
+        $this->editorFields = $replace ? $array : array_merge($this->editorFields, $array);
+        return $this;
+    }
+
+    /**
+     * 获取相关的文件
+     * @return array
+     */
+    public function getFileList(): array
+    {
+        return $this->fileList;
+    }
+
+    /**
+     * 构建基础视图、JS、URL
+     * @return $this
+     */
+    protected function buildViewJsUrl(): static
+    {
+        $nodeArray   = explode($this->DS, $this->controllerFilename);
+        $formatArray = [];
+        foreach ($nodeArray as $vo) {
+            $formatArray[] = CommonTool::humpToLine(lcfirst($vo));
+        }
+        $this->controllerUrl = implode('.', $formatArray);
+        $this->viewFilename  = implode($this->DS, $formatArray);
+        $this->jsFilename    = $this->viewFilename;
+
+        // 控制器命名空间
+        $namespaceArray            = $nodeArray;
+        $this->controllerName      = array_pop($namespaceArray);
+        $namespaceSuffix           = implode('\\', $namespaceArray);
+        $this->controllerNamespace = empty($namespaceSuffix) ? "app\admin\controller" : "app\admin\controller\\{$namespaceSuffix}";
+
+        // 主表模型命名
+        $modelArray = explode($this->DS, $this->modelFilename);
+
+        $this->modelName = array_pop($modelArray);
+
+        return $this;
+    }
+
+    /**
+     * 构建字段
+     * @return $this
+     */
+    protected function buildStructure(): static
+    {
+        foreach ($this->tableColumns as $key => $val) {
+
+            // 排序
+            if ($key == 'sort') {
+                $this->sortFields[] = $key;
+            }
+
+            // 富文本
+            if (in_array($key, ['describe', 'content', 'details'])) {
+                $this->editorFields[] = $key;
+            }
+        }
+        return $this;
+    }
+
+    /**
+     * 构建必填
+     * @param $require
+     * @return string
+     */
+    protected function buildRequiredHtml($require): string
+    {
+        return $require ? 'lay-verify="required"' : "";
+    }
+
+    /**
+     * 构建初始化字段信息
+     * @param $colum
+     * @return array
+     */
+    protected function buildColum(&$colum): array
+    {
+
+        $string = $colum['comment'];
+
+        $colum['define'] = json_encode([1 => '系统自动生成A', 2 => '请自行修改B'], JSON_UNESCAPED_UNICODE);
+
+        // 处理定义类型
+        preg_match('/{[\s\S]*?}/i', $string, $formTypeMatch);
+        if (!empty($formTypeMatch) && isset($formTypeMatch[0])) {
+            $colum['comment'] = str_replace($formTypeMatch[0], '', $colum['comment']);
+            $formType         = trim(str_replace('}', '', str_replace('{', '', $formTypeMatch[0])));
+            $_formType        = $this->checkCommentFormType($formType);
+            if ($_formType) {
+                $colum['formType'] = $_formType;
+            }
+        }
+
+        // 处理默认定义
+        preg_match('/\([\s\S]*?\)/i', $string, $defineMatch);
+        if (!empty($formTypeMatch) && isset($defineMatch[0])) {
+            $colum['comment'] = str_replace($defineMatch[0], '', $colum['comment']);
+            if (isset($colum['formType']) && in_array($colum['formType'], ['images', 'files', 'select', 'switch', 'radio', 'checkbox', 'date'])) {
+                $define = str_replace(')', '', str_replace('(', '', $defineMatch[0]));
+                if (in_array($colum['formType'], ['select', 'switch', 'radio', 'checkbox'])) {
+                    $formatDefine = [];
+                    $explodeArray = explode(',', $define);
+                    foreach ($explodeArray as $vo) {
+                        $voExplodeArray = explode(':', $vo);
+                        if (count($voExplodeArray) == 2) {
+                            $formatDefine[trim($voExplodeArray[0])] = trim($voExplodeArray[1]);
+                        }
+                    }
+                    !empty($formatDefine) && $colum['define'] = $formatDefine;
+                } else {
+                    $colum['define'] = $define;
+                }
+            }
+        }
+
+        $colum['comment'] = trim($colum['comment']);
+
+        return $colum;
+    }
+
+    /**
+     * 构建下拉控制器
+     * @param $field
+     * @return mixed
+     */
+    protected function buildSelectController($field): mixed
+    {
+        $field      = CommonTool::lineToHump(ucfirst($field));
+        $name       = "get{$field}List";
+        $selectCode = CommonTool::replaceTemplate(
+            $this->getTemplate("controller{$this->DS}select"),
+            [
+                'name' => $name,
+            ]
+        );
+        return $selectCode;
+    }
+
+    /**
+     * 构架下拉模型
+     * @param $field
+     * @param $array
+     * @return mixed
+     */
+    protected function buildSelectModel($field, $array): mixed
+    {
+        $field  = CommonTool::lineToHump(ucfirst($field));
+        $name   = "get{$field}List";
+        $values = '[';
+        foreach ($array as $k => $v) {
+            $values .= "'{$k}'=>'{$v}',";
+        }
+        $values     .= ']';
+        $selectCode = CommonTool::replaceTemplate(
+            $this->getTemplate("model{$this->DS}select"),
+            [
+                'name'   => $name,
+                'values' => $values,
+            ]
+        );
+        return $selectCode;
+    }
+
+    /**
+     * 构架关联下拉模型
+     * @param $relation
+     * @param $filed
+     * @return mixed
+     */
+    protected function buildRelationSelectModel($relation, $field): mixed
+    {
+        $relationArray = explode('\\', $relation);
+        $name          = end($relationArray);
+        $name          = "get{$name}List";
+        $selectCode    = CommonTool::replaceTemplate(
+            $this->getTemplate("model{$this->DS}relationSelect"),
+            [
+                'name'     => "notes['$field']",
+                'relation' => $relation,
+                'values'   => $field,
+            ]
+        );
+        return $selectCode;
+    }
+
+    /**
+     * 构建下拉框视图
+     * @param $field
+     * @param string $select
+     * @return mixed
+     */
+    protected function buildOptionView($field, string $select = '')
+    {
+        //        $field = CommonTool::lineToHump(ucfirst($field));
+        //        $name  = "get{$field}List";
+        return CommonTool::replaceTemplate(
+            $this->getTemplate("view{$this->DS}module{$this->DS}option"),
+            [
+                'name'   => "notes['$field']",
+                'select' => $select,
+            ]
+        );
+    }
+
+    /**
+     * 构建单选框视图
+     * @param $field
+     * @param string $select
+     * @return mixed
+     */
+    protected function buildRadioView($field, string $select = ''): mixed
+    {
+        //        $formatField = CommonTool::lineToHump(ucfirst($field));
+        //        $name        = "get{$formatField}List";
+        return CommonTool::replaceTemplate(
+            $this->getTemplate("view{$this->DS}module{$this->DS}radioInput"),
+            [
+                'field'  => $field,
+                'name'   => "notes['$field']",
+                'select' => $select,
+            ]
+        );
+    }
+
+    /**
+     * 构建多选框视图
+     * @param $field
+     * @param string $select
+     * @return mixed
+     */
+    protected function buildCheckboxView($field, string $select = ''): mixed
+    {
+        //        $formatField = CommonTool::lineToHump(ucfirst($field));
+        //        $name        = "get{$formatField}List";
+        return CommonTool::replaceTemplate(
+            $this->getTemplate("view{$this->DS}module{$this->DS}checkboxInput"),
+            [
+                'field'  => $field,
+                'name'   => "notes['$field']",
+                'select' => $select,
+            ]
+        );
+    }
+
+    /**
+     * 初始化
+     * @return $this
+     */
+    public function render(): static
+    {
+
+        // 初始化数据
+        $this->renderData();
+
+        // 控制器
+        $this->renderController();
+
+        // 模型
+        $this->renderModel();
+
+        // 视图
+        $this->renderView();
+
+        // JS
+        $this->renderJs();
+
+        return $this;
+    }
+
+    /**
+     * 初始化数据
+     * @return $this
+     */
+    protected function renderData(): static
+    {
+
+        // 主表
+        foreach ($this->tableColumns as $field => $val) {
+
+
+            // 过滤字段
+            if (in_array($field, $this->ignoreFields)) {
+                unset($this->tableColumns[$field]);
+                continue;
+            }
+
+            $this->tableColumns[$field]['formType'] = $this->tableColumns[$field]['formType'] ?? 'text';
+
+            // 判断图片
+            if ($this->checkContain($field, $this->imageFieldSuffix)) {
+                $this->tableColumns[$field]['formType'] = 'image';
+                continue;
+            }
+            if ($this->checkContain($field, $this->imagesFieldSuffix)) {
+                $this->tableColumns[$field]['formType'] = 'images';
+                continue;
+            }
+
+            // 判断文件
+            if ($this->checkContain($field, $this->fileFieldSuffix)) {
+                $this->tableColumns[$field]['formType'] = 'file';
+                continue;
+            }
+            if ($this->checkContain($field, $this->filesFieldSuffix)) {
+                $this->tableColumns[$field]['formType'] = 'files';
+                continue;
+            }
+
+            // 判断日期
+            if ($this->checkContain($field, $this->dateFieldSuffix)) {
+                $this->tableColumns[$field]['formType'] = 'date';
+                continue;
+            }
+
+            // 判断日期时间
+            if ($this->checkContain($field, $this->datetimeFieldSuffix)) {
+                $this->tableColumns[$field]['formType'] = 'datetime';
+                continue;
+            }
+
+            if (in_array($field, $this->radioFields) || $this->checkContain($field, $this->radioFieldSuffix)) {
+                $this->tableColumns[$field]['formType'] = 'radio';
+                continue;
+            }
+
+            if (in_array($field, $this->checkboxFields) || $this->checkContain($field, $this->checkboxFieldSuffix)) {
+                $this->tableColumns[$field]['formType'] = 'checkbox';
+                continue;
+            }
+
+            // 判断开关
+            if (in_array($field, $this->switchFields)) {
+                $this->tableColumns[$field]['formType'] = 'switch';
+                continue;
+            }
+
+
+            // 判断富文本
+            if (in_array($field, $this->editorFields) || in_array($val['type'], ['text', 'tinytext', 'mediumtext', 'longtext'])) {
+                $this->tableColumns[$field]['formType'] = 'editor';
+                continue;
+            }
+
+            // 判断排序
+            if (in_array($field, $this->sortFields)) {
+                $this->tableColumns[$field]['formType'] = 'sort';
+                continue;
+            }
+
+            // 判断下拉选择
+            if (in_array($field, $this->selectFields)) {
+                $this->tableColumns[$field]['formType'] = 'select';
+                continue;
+            }
+        }
+
+        // 关联表
+        foreach ($this->relationArray as $table => $tableVal) {
+            foreach ($tableVal['tableColumns'] as $field => $val) {
+
+                // 过滤字段
+                if (in_array($field, $this->ignoreFields)) {
+                    unset($this->relationArray[$table]['tableColumns'][$field]);
+                    continue;
+                }
+
+                // 判断是否已初始化
+                if (isset($this->relationArray[$table]['tableColumns'][$field]['formType'])) {
+                    continue;
+                }
+
+                // 判断图片
+                if ($this->checkContain($field, $this->imageFieldSuffix)) {
+                    $this->relationArray[$table]['tableColumns'][$field]['formType'] = 'image';
+                    continue;
+                }
+                if ($this->checkContain($field, $this->imagesFieldSuffix)) {
+                    $this->relationArray[$table]['tableColumns'][$field]['formType'] = 'images';
+                    continue;
+                }
+
+                // 判断文件
+                if ($this->checkContain($field, $this->fileFieldSuffix)) {
+                    $this->relationArray[$table]['tableColumns'][$field]['formType'] = 'file';
+                    continue;
+                }
+                if ($this->checkContain($field, $this->filesFieldSuffix)) {
+                    $this->relationArray[$table]['tableColumns'][$field]['formType'] = 'files';
+                    continue;
+                }
+
+                // 判断时间
+                if ($this->checkContain($field, $this->dateFieldSuffix)) {
+                    $this->relationArray[$table]['tableColumns'][$field]['formType'] = 'date';
+                    continue;
+                }
+
+                // 判断开关
+                if (in_array($field, $this->switchFields)) {
+                    $this->relationArray[$table]['tableColumns'][$field]['formType'] = 'switch';
+                    continue;
+                }
+
+                // 判断富文本
+                if (in_array($field, $this->editorFields) || in_array($val['type'], ['text', 'tinytext', 'mediumtext', 'longtext'])) {
+                    $this->relationArray[$table]['tableColumns'][$field]['formType'] = 'editor';
+                    continue;
+                }
+
+                // 判断排序
+                if (in_array($field, $this->sortFields)) {
+                    $this->relationArray[$table]['tableColumns'][$field]['formType'] = 'sort';
+                    continue;
+                }
+
+                // 判断下拉选择
+                if (in_array($field, $this->selectFields)) {
+                    $this->relationArray[$table]['tableColumns'][$field]['formType'] = 'select';
+                    continue;
+                }
+
+                $this->relationArray[$table]['tableColumns'][$field]['formType'] = 'text';
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * 初始化控制器
+     * @return $this
+     */
+    protected function renderController(): static
+    {
+        $controllerFile    = "{$this->rootDir}app{$this->DS}admin{$this->DS}controller{$this->DS}{$this->controllerFilename}.php";
+        $constructRelation = '';
+        if (empty($this->relationArray)) {
+            $controllerIndexMethod = '';
+        } else {
+            $relationCode = '';
+            foreach ($this->relationArray as $key => $val) {
+                $relation     = CommonTool::lineToHump($key);
+                $relationCode = "withJoin('{$relation}', 'LEFT')";
+                if (!empty($val['bindSelectField']) && !empty($val['primaryKey'])) {
+                    $constructRelation = '$notes["' . lcfirst($val['foreignKey']) . '"] = \app\admin\model\\' . $val['modelFilename'] . '::column("' . $val['bindSelectField'] . '", "' . $val['primaryKey'] . '");';
+                }
+            }
+            $controllerIndexMethod = CommonTool::replaceTemplate(
+                $this->getTemplate("controller{$this->DS}indexMethod"),
+                [
+                    'relationIndexMethod' => $relationCode,
+                ]
+            );
+        }
+        $selectList = '';
+        //        foreach ($this->relationArray as $relation) {
+        //            if (!empty($relation['bindSelectField'])) {
+        //                $relationArray = explode('\\', $relation['modelFilename']);
+        //                $selectList    .= $this->buildSelectController(end($relationArray));
+        //            }
+        //        }
+        //        foreach ($this->tableColumns as $field => $val) {
+        //            if (isset($val['formType']) && in_array($val['formType'], ['select', 'switch', 'radio', 'checkbox']) && isset($val['define'])) {
+        //                $selectList .= $this->buildSelectController($field);
+        //            }
+        //        }
+
+        $modelFilenameExtend = str_replace($this->DS, '\\', $this->modelFilename);
+
+        $controllerValue                 = CommonTool::replaceTemplate(
+            $this->getTemplate("controller{$this->DS}controller"),
+            [
+                'controllerName'       => $this->controllerName,
+                'controllerNamespace'  => $this->controllerNamespace,
+                'controllerAnnotation' => $this->tableComment,
+                'modelFilename'        => "\app\admin\model\\{$modelFilenameExtend}",
+                'indexMethod'          => $controllerIndexMethod,
+                'selectList'           => $selectList,
+                'constructRelation'    => $constructRelation,
+            ]
+        );
+        $this->fileList[$controllerFile] = $controllerValue;
+        return $this;
+    }
+
+    /**
+     * 初始化模型
+     * @return $this
+     */
+    protected function renderModel(): static
+    {
+        // 主表模型
+        $modelFile    = "{$this->rootDir}app{$this->DS}admin{$this->DS}model{$this->DS}{$this->modelFilename}.php";
+        $relationList = '';
+        if (!empty($this->relationArray)) {
+            foreach ($this->relationArray as $key => $val) {
+                $relation     = CommonTool::lineToHump($key);
+                $relationCode = CommonTool::replaceTemplate(
+                    $this->getTemplate("model{$this->DS}relation"),
+                    [
+                        'relationMethod' => $relation,
+                        'relationModel'  => "{$val['modelFilename']}::class",
+                        'foreignKey'     => $val['foreignKey'],
+                        'primaryKey'     => $val['primaryKey'],
+                        'relationFields' => empty($val['onlyFields']) ? "" : "->field('{$val['primaryKey']}," . implode(',', $val['onlyFields']) . "')",
+                    ]);
+                $relationList .= $relationCode;
+            }
+        }
+
+        $selectList = '';
+        foreach ($this->relationArray as $relation) {
+            if (!empty($relation['bindSelectField'])) {
+                $selectList .= $this->buildRelationSelectModel($relation['modelFilename'], $relation['bindSelectField']);
+            }
+        }
+        $selectArrays = [];
+        foreach ($this->tableColumns as $field => $val) {
+            if (isset($val['formType']) && in_array($val['formType'], ['select', 'switch', 'radio', 'checkbox']) && isset($val['define'])) {
+                $selectArrays += [$field => is_array($val['define']) ? $val['define'] : json_decode($val['define'], true)];
+            }
+        }
+        $extendNamespaceArray = explode($this->DS, $this->modelFilename);
+        $extendNamespace      = null;
+        if (count($extendNamespaceArray) > 1) {
+            array_pop($extendNamespaceArray);
+            $extendNamespace = '\\' . implode('\\', $extendNamespaceArray);
+        }
+        $modelValue = CommonTool::replaceTemplate(
+            $this->getTemplate("model{$this->DS}model"),
+            [
+                'modelName'      => $this->modelName,
+                'modelNamespace' => "app\admin\model{$extendNamespace}",
+                'prefix_table'   => $this->tablePrefix == config('database.connections.mysql.prefix') ? "" : $this->tablePrefix . $this->table,
+                'table'          => $this->table,
+                'deleteTime'     => $this->delete ? '"delete_time"' : 'false',
+                'relationList'   => $relationList,
+                //                'selectList'     => $selectList,
+                'selectArrays'   => CommonTool::replaceArrayString(var_export($selectArrays, true)),
+            ]
+        );
+
+
+        $this->fileList[$modelFile] = $modelValue;
+
+        // 关联模型
+        foreach ($this->relationArray as $key => $val) {
+            $relationModelFile = "{$this->rootDir}app{$this->DS}admin{$this->DS}model{$this->DS}{$val['modelFilename']}.php";
+
+            // todo 判断关联模型文件是否存在, 存在就不重新生成文件, 防止关联模型文件被覆盖
+            $relationModelClass = "\\app\\admin\\model\\{$val['modelFilename']}";
+            if (class_exists($relationModelClass) && method_exists(new $relationModelClass, 'getName')) {
+                $tableName = (new $relationModelClass)->getName();
+                if (CommonTool::humpToLine(lcfirst($tableName)) == CommonTool::humpToLine(lcfirst($key))) {
+                    continue;
+                }
+            }
+
+            $extendNamespaceArray = explode($this->DS, $val['modelFilename']);
+            $extendNamespace      = null;
+            if (count($extendNamespaceArray) > 1) {
+                array_pop($extendNamespaceArray);
+                $extendNamespace = '\\' . implode('\\', $extendNamespaceArray);
+            }
+
+            $relationModelValue                 = CommonTool::replaceTemplate(
+                $this->getTemplate("model{$this->DS}model"),
+                [
+                    'modelName'      => $val['modelName'],
+                    'modelNamespace' => "app\admin\model{$extendNamespace}",
+                    'prefix_table'   => $this->tablePrefix == config('database.connections.mysql.prefix') ? "" : $this->tablePrefix . $this->table,
+                    'table'          => $key,
+                    'deleteTime'     => $val['delete'] ? '"delete_time"' : 'false',
+                    'relationList'   => '',
+                    'selectList'     => '',
+                    'selectArrays'   => "[]",
+                ]
+            );
+            $this->fileList[$relationModelFile] = $relationModelValue;
+        }
+        return $this;
+    }
+
+    /**
+     * 初始化视图
+     * @return $this
+     */
+    protected function renderView(): static
+    {
+        // 列表页面
+        $viewIndexFile                  = "{$this->rootDir}app{$this->DS}admin{$this->DS}view{$this->DS}{$this->viewFilename}{$this->DS}index.html";
+        $viewIndexValue                 = CommonTool::replaceTemplate(
+            $this->getTemplate("view{$this->DS}index"),
+            [
+                'controllerUrl' => $this->controllerUrl,
+                'notesScript'   => $this->formatNotesScript(),
+            ]
+        );
+        $this->fileList[$viewIndexFile] = $viewIndexValue;
+
+        // 添加页面
+        $viewAddFile = "{$this->rootDir}app{$this->DS}admin{$this->DS}view{$this->DS}{$this->viewFilename}{$this->DS}add.html";
+        $addFormList = '';
+        foreach ($this->tableColumns as $field => $val) {
+
+            if (in_array($field, ['id', 'create_time'])) {
+                continue;
+            }
+
+            $templateFile = "view{$this->DS}module{$this->DS}input";
+            $define       = '';
+
+            // 根据formType去获取具体模板
+            if ($val['formType'] == 'image') {
+                $templateFile = "view{$this->DS}module{$this->DS}image";
+            } elseif ($val['formType'] == 'images') {
+                $templateFile = "view{$this->DS}module{$this->DS}images";
+                $define       = $val['define'] ?? '|';
+                if (strlen($define) > 5) $define = '|';
+            } elseif ($val['formType'] == 'file') {
+                $templateFile = "view{$this->DS}module{$this->DS}file";
+            } elseif ($val['formType'] == 'files') {
+                $templateFile = "view{$this->DS}module{$this->DS}files";
+                $define       = $val['define'] ?? '|';
+            } elseif ($val['formType'] == 'editor') {
+                $templateFile   = "view{$this->DS}module{$this->DS}editor";
+                $val['default'] = '""';
+            } elseif ($val['formType'] == 'date') {
+                $templateFile = "view{$this->DS}module{$this->DS}date";
+                $define       = 'date';
+            } elseif ($val['formType'] == 'datetime') {
+                $templateFile = "view{$this->DS}module{$this->DS}date";
+                $define       = 'datetime';
+            } elseif ($val['formType'] == 'radio') {
+                $templateFile = "view{$this->DS}module{$this->DS}radio";
+                if (!empty($val['define'])) {
+                    $define = $this->buildRadioView($field, '');
+                }
+            } elseif ($val['formType'] == 'checkbox') {
+                $templateFile = "view{$this->DS}module{$this->DS}checkbox";
+                if (!empty($val['define'])) {
+                    $define = $this->buildCheckboxView($field, '');
+                }
+            } elseif ($val['formType'] == 'select') {
+                $templateFile = "view{$this->DS}module{$this->DS}select";
+                if (isset($val['bindRelation'])) {
+                    $define = $this->buildOptionView($field);
+                } elseif (!empty($val['define'])) {
+                    $define = $this->buildOptionView($field);
+                }
+            } elseif ($field == 'remark' || $val['formType'] == 'textarea') {
+                $templateFile = "view{$this->DS}module{$this->DS}textarea";
+            } elseif ($field == 'sort') {
+                $templateFile = "view{$this->DS}module{$this->DS}sort";
+            }
+            $addFormList .= CommonTool::replaceTemplate(
+                $this->getTemplate($templateFile),
+                [
+                    'comment'  => $val['comment'],
+                    'field'    => $field,
+                    'required' => $this->buildRequiredHtml($val['required']),
+                    'value'    => $val['default'],
+                    'define'   => $define,
+                ]
+            );
+        }
+        $viewAddValue                 = CommonTool::replaceTemplate(
+            $this->getTemplate("view{$this->DS}form"),
+            [
+                'formList' => $addFormList,
+            ]
+        );
+        $this->fileList[$viewAddFile] = $viewAddValue;
+
+
+        // 编辑页面
+        $viewEditFile = "{$this->rootDir}app{$this->DS}admin{$this->DS}view{$this->DS}{$this->viewFilename}{$this->DS}edit.html";
+        $editFormList = '';
+        foreach ($this->tableColumns as $field => $val) {
+
+            if (in_array($field, ['id', 'create_time'])) {
+                continue;
+            }
+
+            $templateFile = "view{$this->DS}module{$this->DS}input";
+
+            $define = '';
+            $value  = '{$row.' . $field . '|default=\'\'}';
+
+            // 根据formType去获取具体模板
+            if ($val['formType'] == 'image') {
+                $templateFile = "view{$this->DS}module{$this->DS}image";
+            } elseif ($val['formType'] == 'images') {
+                $templateFile = "view{$this->DS}module{$this->DS}images";
+            } elseif ($val['formType'] == 'file') {
+                $templateFile = "view{$this->DS}module{$this->DS}file";
+            } elseif ($val['formType'] == 'files') {
+                $templateFile = "view{$this->DS}module{$this->DS}files";
+            } elseif ($val['formType'] == 'editor') {
+                $templateFile = "view{$this->DS}module{$this->DS}editor";
+                $value        = '$row["' . $field . '"]';
+            } elseif ($val['formType'] == 'date') {
+                $templateFile = "view{$this->DS}module{$this->DS}date";
+                $define       = 'date';
+            } elseif ($val['formType'] == 'datetime') {
+                $templateFile = "view{$this->DS}module{$this->DS}date";
+                $define       = 'datetime';
+            } elseif ($val['formType'] == 'radio') {
+                $templateFile = "view{$this->DS}module{$this->DS}radio";
+                if (!empty($val['define'])) {
+                    $define = $this->buildRadioView($field, '{if $row.' . $field . '==$k}checked{/if}');
+                }
+            } elseif ($val['formType'] == 'checkbox') {
+                $templateFile = "view{$this->DS}module{$this->DS}checkbox";
+                if (!empty($val['define'])) {
+                    $define = $this->buildCheckboxView($field, '{if $row.' . $field . '==$k}checked{/if}');
+                }
+            } elseif ($val['formType'] == 'select') {
+                $templateFile = "view{$this->DS}module{$this->DS}select";
+                if (isset($val['bindRelation'])) {
+                    $define = $this->buildOptionView($field, '{if $row.' . $field . '==$k}selected{/if}');
+                } elseif (!empty($val['define'])) {
+                    $define = $this->buildOptionView($field, '{if $row.' . $field . '==$k}selected{/if}');
+                }
+            } elseif ($field == 'remark' || $val['formType'] == 'textarea') {
+                $templateFile = "view{$this->DS}module{$this->DS}textarea";
+                $value        = '{$row.' . $field . '|raw|default=\'\'}';
+            } elseif ($field == 'sort') {
+                $templateFile = "view{$this->DS}module{$this->DS}sort";
+            }
+            $editFormList .= CommonTool::replaceTemplate(
+                $this->getTemplate($templateFile),
+                [
+                    'comment'  => $val['comment'],
+                    'field'    => $field,
+                    'required' => $this->buildRequiredHtml($val['required']),
+                    'value'    => $value,
+                    'define'   => $define,
+                ]
+            );
+        }
+        $viewEditValue                 = CommonTool::replaceTemplate(
+            $this->getTemplate("view{$this->DS}form"),
+            [
+                'formList' => $editFormList,
+            ]
+        );
+        $this->fileList[$viewEditFile] = $viewEditValue;
+
+        $viewRecycleFile                  = "{$this->rootDir}app{$this->DS}admin{$this->DS}view{$this->DS}{$this->viewFilename}{$this->DS}recycle.html";
+        $viewRecycleValue                 = CommonTool::replaceTemplate(
+            $this->getTemplate("view{$this->DS}recycle"),
+            [
+                'controllerUrl' => $this->controllerUrl,
+                'notesScript'   => $this->formatNotesScript(),
+            ]
+        );
+        $this->fileList[$viewRecycleFile] = $viewRecycleValue;
+        return $this;
+    }
+
+    /**
+     * 初始化JS
+     * @return $this
+     */
+    protected function renderJs(): static
+    {
+        $jsFile = "{$this->rootDir}public{$this->DS}static{$this->DS}admin{$this->DS}js{$this->DS}{$this->jsFilename}.js";
+
+        $indexCols = "    {type: 'checkbox'},\r";
+
+        // 主表字段
+        foreach ($this->tableColumns as $field => $val) {
+
+            if ($val['formType'] == 'image') {
+                $templateValue = "{field: '{$field}', title: '{$val['comment']}', templet: ea.table.image}";
+            } elseif ($val['formType'] == 'datetime') {
+                $templateValue = "{field: '{$field}', search: 'range', title: '{$val['comment']}'}";
+            } elseif ($val['formType'] == 'images') {
+                continue;
+            } elseif ($val['formType'] == 'file') {
+                $templateValue = "{field: '{$field}', title: '{$val['comment']}', templet: ea.table.url}";
+            } elseif ($val['formType'] == 'files') {
+                continue;
+            } elseif ($val['formType'] == 'editor') {
+                continue;
+            } elseif (in_array($field, $this->switchFields)) {
+                if (!empty($val['define'])) {
+                    $templateValue = "{field: '{$field}', search: 'select', selectList: notes?.{$field} || {}, title: '{$val['comment']}', templet: ea.table.switch}";
+                } else {
+                    $templateValue = "{field: '{$field}', title: '{$val['comment']}', templet: ea.table.switch}";
+                }
+            } elseif (in_array($val['formType'], ['select', 'checkbox', 'radio', 'switch'])) {
+                if (!empty($val['define'])) {
+                    $templateValue = "{field: '{$field}', search: 'select', selectList: notes?.{$field} || {}, title: '{$val['comment']}'}";
+                } else {
+                    $templateValue = "{field: '{$field}', title: '{$val['comment']}'}";
+                }
+            } elseif ($field == 'remark') {
+                $templateValue = "{field: '{$field}', title: '{$val['comment']}', templet: ea.table.text}";
+            } elseif (in_array($field, $this->sortFields)) {
+                $templateValue = "{field: '{$field}', title: '{$val['comment']}', edit: 'text'}";
+            } else {
+                $templateValue = "{field: '{$field}', title: '{$val['comment']}'}";
+            }
+            $indexCols .= $this->formatColsRow("{$templateValue},\r");
+        }
+
+        // 关联表
+        foreach ($this->relationArray as $table => $tableVal) {
+            $table = CommonTool::humpToLine($table);
+            foreach ($tableVal['tableColumns'] as $field => $val) {
+                if ($val['formType'] == 'image') {
+                    $templateValue = "{field: '{$table}.{$field}', title: '{$val['comment']}', templet: ea.table.image}";
+                } elseif ($val['formType'] == 'images') {
+                    continue;
+                } elseif ($val['formType'] == 'file') {
+                    $templateValue = "{field: '{$table}.{$field}', title: '{$val['comment']}', templet: ea.table.url}";
+                } elseif ($val['formType'] == 'files') {
+                    continue;
+                } elseif ($val['formType'] == 'editor') {
+                    continue;
+                } elseif ($val['formType'] == 'select') {
+                    $templateValue = "{field: '{$table}.{$field}', title: '{$val['comment']}'}";
+                } elseif ($field == 'remark') {
+                    $templateValue = "{field: '{$table}.{$field}', title: '{$val['comment']}', templet: ea.table.text}";
+                } elseif (in_array($field, $this->switchFields)) {
+                    $templateValue = "{field: '{$table}.{$field}', title: '{$val['comment']}', templet: ea.table.switch}";
+                } elseif (in_array($field, $this->sortFields)) {
+                    $templateValue = "{field: '{$table}.{$field}', title: '{$val['comment']}', edit: 'text'}";
+                } else {
+                    $templateValue = "{field: '{$table}.{$field}', title: '{$val['comment']}'}";
+                }
+
+                if ($templateValue) $indexCols .= $this->formatColsRow("{$templateValue},\r");
+            }
+        }
+
+        $recycleCols = $indexCols;
+        $indexCols   .= $this->formatColsRow("{width: 250, title: '操作', templet: ea.table.tool},\r");
+
+        $jsValue                 = CommonTool::replaceTemplate(
+            $this->getTemplate("static{$this->DS}js"),
+            [
+                'controllerUrl' => $this->controllerUrl,
+                'indexCols'     => $indexCols,
+                'recycleCols'   => $recycleCols,
+            ]
+        );
+        $this->fileList[$jsFile] = $jsValue;
+        return $this;
+    }
+
+    /**
+     * 检测文件
+     * @return $this
+     */
+    protected function check(): static
+    {
+        // 是否强制性
+        if ($this->force) {
+            return $this;
+        }
+        foreach ($this->fileList as $key => $val) {
+            if (is_file($key)) {
+                throw new FileException("文件已存在:{$key}");
+            }
+        }
+        return $this;
+    }
+
+    /**
+     * 开始生成
+     * @return array
+     */
+    public function create(): array
+    {
+        $this->check();
+        foreach ($this->fileList as $key => $val) {
+
+            // 判断文件夹是否存在,不存在就创建
+            $fileArray = explode($this->DS, $key);
+            array_pop($fileArray);
+            $fileDir = implode($this->DS, $fileArray);
+            if (!is_dir($fileDir)) {
+                mkdir($fileDir, 0775, true);
+            }
+
+            // 写入
+            file_put_contents($key, $val);
+        }
+        return array_keys($this->fileList);
+    }
+
+    /**
+     * 开始删除
+     * @return array
+     */
+    public function delete(): array
+    {
+        $deleteFile = [];
+        foreach ($this->fileList as $key => $val) {
+            if (is_file($key)) {
+                unlink($key);
+                $deleteFile[] = $key;
+            }
+        }
+        return $deleteFile;
+    }
+
+    /**
+     * 检测字段后缀
+     * @param $string
+     * @param $array
+     * @return bool
+     */
+    protected function checkContain($string, $array): bool
+    {
+        foreach ($array as $vo) {
+            if (str_starts_with($vo, $string)) {
+                return true;
+            }
+            if (str_ends_with($vo, $string)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 格式化表单行
+     * @param $value
+     * @return string
+     */
+    protected function formatColsRow($value): string
+    {
+        return "                    {$value}";
+    }
+
+    /**
+     * 获取对应的模板信息
+     * @param $name
+     * @return false|string
+     */
+    protected function getTemplate($name): bool|string
+    {
+        return file_get_contents("{$this->dir}{$this->DS}templates{$this->DS}{$name}.code");
+    }
+
+    /**
+     * 检测字段注释归类的类型
+     * @param string $formType
+     * @return string|null
+     */
+    protected function checkCommentFormType(string $formType = ''): ?string
+    {
+        $classProperties = get_class_vars(get_class($this));
+        foreach ($classProperties as $property => $classProperty) {
+            if (empty($property)) continue;
+            if (str_ends_with($property, 'FieldSuffix')) {
+                if (in_array($formType, $this->$property)) {
+                    return $this->$property[0] ?? '';
+                }
+            }
+        }
+        return '';
+    }
+
+
+    protected function formatNotesScript(): string
+    {
+        return '    let notes = JSON.parse(\'{$notes|json_encode=256|raw}\');';
+    }
+}

+ 8 - 0
app/admin/service/curd/exceptions/CurdException.php

@@ -0,0 +1,8 @@
+<?php
+declare(strict_types = 1);
+
+namespace app\admin\service\curd\exceptions;
+
+class CurdException extends \Exception
+{
+}

+ 8 - 0
app/admin/service/curd/exceptions/FileException.php

@@ -0,0 +1,8 @@
+<?php
+declare(strict_types = 1);
+
+namespace app\admin\service\curd\exceptions;
+
+class FileException extends \Exception
+{
+}

+ 8 - 0
app/admin/service/curd/exceptions/TableException.php

@@ -0,0 +1,8 @@
+<?php
+declare(strict_types = 1);
+
+namespace app\admin\service\curd\exceptions;
+
+class TableException extends \Exception
+{
+}

+ 28 - 0
app/admin/service/curd/templates/controller/controller.code

@@ -0,0 +1,28 @@
+<?php
+
+namespace {{controllerNamespace}};
+
+use app\common\controller\AdminController;
+use app\admin\service\annotation\ControllerAnnotation;
+use app\admin\service\annotation\NodeAnnotation;
+use think\App;
+
+#[ControllerAnnotation(title: '{{controllerAnnotation}}')]
+class {{controllerName}} extends AdminController
+{
+
+    private array $notes;
+
+    public function __construct(App $app)
+    {
+        parent::__construct($app);
+        self::$model = new {{modelFilename}}();
+        $notes = self::$model::$notes;
+        {{constructRelation}}
+        $this->notes =$notes;
+        $this->assign(compact('notes'));
+    }
+
+    {{indexMethod}}
+
+}

+ 21 - 0
app/admin/service/curd/templates/controller/indexMethod.code

@@ -0,0 +1,21 @@
+
+    #[NodeAnnotation(title: '列表', auth: true)]
+    public function index(\app\Request $request): \think\response\Json|string
+    {
+        if ($request->isAjax()) {
+            if (input('selectFields')) {
+                return $this->selectList();
+            }
+            list($page, $limit, $where) = $this->buildTableParams();
+            $count = self::$model::where($where)->{{relationIndexMethod}}->count();
+            $list  = self::$model::where($where)->{{relationIndexMethod}}->page($page, $limit)->order($this->sort)->select()->toArray();
+            $data  = [
+                'code'  => 0,
+                'msg'   => '',
+                'count' => $count,
+                'data'  => $list,
+            ];
+            return json($data);
+        }
+        return $this->fetch();
+    }

+ 2 - 0
app/admin/service/curd/templates/controller/select.code

@@ -0,0 +1,2 @@
+
+        $this->assign('{{name}}', $this->model->{{name}}());

+ 23 - 0
app/admin/service/curd/templates/model/model.code

@@ -0,0 +1,23 @@
+<?php
+
+namespace {{modelNamespace}};
+
+use app\common\model\TimeModel;
+
+class {{modelName}} extends TimeModel
+{
+
+    protected function getOptions(): array
+    {
+        return [
+            'name'       => "{{table}}",
+            'table'      => "{{prefix_table}}",
+            'deleteTime' => {{deleteTime}},
+        ];
+    }
+
+    public static array $notes = {{selectArrays}};
+
+    {{relationList}}
+
+}

+ 5 - 0
app/admin/service/curd/templates/model/relation.code

@@ -0,0 +1,5 @@
+
+    public function {{relationMethod}}()
+    {
+        return $this->belongsTo({{relationModel}}, '{{foreignKey}}', '{{primaryKey}}'){{relationFields}};
+    }

+ 5 - 0
app/admin/service/curd/templates/model/relationSelect.code

@@ -0,0 +1,5 @@
+
+    public function {{name}}()
+    {
+        return \app\admin\model\{{relation}}::column('{{values}}', 'id');
+    }

+ 5 - 0
app/admin/service/curd/templates/model/select.code

@@ -0,0 +1,5 @@
+
+    public function {{name}}()
+    {
+        return {{values}};
+    }

+ 91 - 0
app/admin/service/curd/templates/static/js.code

@@ -0,0 +1,91 @@
+define(["jquery", "easy-admin"], function ($, ea) {
+
+    var init = {
+        table_elem: '#currentTable',
+        table_render_id: 'currentTableRenderId',
+        index_url: '{{controllerUrl}}/index',
+        add_url: '{{controllerUrl}}/add',
+        edit_url: '{{controllerUrl}}/edit',
+        delete_url: '{{controllerUrl}}/delete',
+        export_url: '{{controllerUrl}}/export',
+        modify_url: '{{controllerUrl}}/modify',
+        recycle_url: '{{controllerUrl}}/recycle',
+    };
+
+    return {
+
+        index: function () {
+            ea.table.render({
+                init: init,
+                cols: [[
+                {{indexCols}}
+                ]],
+            });
+
+            ea.listen();
+        },
+        add: function () {
+            ea.listen();
+        },
+        edit: function () {
+            ea.listen();
+        },
+        recycle: function () {
+            init.index_url = init.recycle_url;
+            ea.table.render({
+                init: init,
+                toolbar: ['refresh',
+                    [{
+                        class: 'layui-btn layui-btn-sm',
+                        method: 'get',
+                        field: 'id',
+                        icon: 'fa fa-refresh',
+                        text: '全部恢复',
+                        title: '确定恢复?',
+                        auth: 'recycle',
+                        url: init.recycle_url + '?type=restore',
+                        checkbox: true
+                    }, {
+                        class: 'layui-btn layui-btn-danger layui-btn-sm',
+                        method: 'get',
+                        field: 'id',
+                        icon: 'fa fa-delete',
+                        text: '彻底删除',
+                        title: '确定彻底删除?',
+                        auth: 'recycle',
+                        url: init.recycle_url + '?type=delete',
+                        checkbox: true
+                    }], 'export',
+                ],
+                cols: [[
+                {{recycleCols}}
+                    {
+                        width: 250,
+                        title: '操作',
+                        templet: ea.table.tool,
+                        operat: [
+                            [{
+                                title: '确认恢复?',
+                                text: '恢复数据',
+                                filed: 'id',
+                                url: init.recycle_url + '?type=restore',
+                                method: 'get',
+                                auth: 'recycle',
+                                class: 'layui-btn layui-btn-xs layui-btn-success',
+                            }, {
+                                title: '想好了吗?',
+                                text: '彻底删除',
+                                filed: 'id',
+                                method: 'get',
+                                url: init.recycle_url + '?type=delete',
+                                auth: 'recycle',
+                                class: 'layui-btn layui-btn-xs layui-btn-normal layui-bg-red',
+                            }]]
+                    }
+                ]],
+            });
+
+            ea.listen();
+        },
+    };
+});

+ 10 - 0
app/admin/service/curd/templates/view/form.code

@@ -0,0 +1,10 @@
+<div class="layuimini-container">
+    <form id="app-form" class="layui-form layuimini-form">
+        {{formList}}
+        <div class="hr-line"></div>
+        <div class="layui-form-item text-center">
+            <button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
+            <button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
+        </div>
+    </form>
+</div>

+ 16 - 0
app/admin/service/curd/templates/view/index.code

@@ -0,0 +1,16 @@
+<div class="layuimini-container">
+    <div class="layuimini-main">
+        <table id="currentTable" class="layui-table layui-hide"
+               data-auth-add="{:auth('{{controllerUrl}}/add')}"
+               data-auth-edit="{:auth('{{controllerUrl}}/edit')}"
+               data-auth-delete="{:auth('{{controllerUrl}}/delete')}"
+               data-auth-recycle="{:auth('{{controllerUrl}}/recycle')}"
+               lay-filter="currentTable">
+               <!-- searchTableShow="false" 隐藏搜索框 -->
+        </table>
+    </div>
+</div>
+
+<script>
+{{notesScript}}
+</script>

+ 7 - 0
app/admin/service/curd/templates/view/module/checkbox.code

@@ -0,0 +1,7 @@
+
+        <div class="layui-form-item">
+            <label class="layui-form-label">{{comment}}</label>
+            <div class="layui-input-block">
+{{define}}
+            </div>
+        </div>

+ 3 - 0
app/admin/service/curd/templates/view/module/checkboxInput.code

@@ -0,0 +1,3 @@
+                {foreach ${{name}} as $k=>$v}
+                <input type="checkbox" name="{{field}}[]" value="{$k}" lay-skin="primary" title="{$v}" {{select}}>
+                {/foreach}

+ 7 - 0
app/admin/service/curd/templates/view/module/date.code

@@ -0,0 +1,7 @@
+
+        <div class="layui-form-item">
+            <label class="layui-form-label">{{comment}}</label>
+            <div class="layui-input-block">
+                <input type="text" name="{{field}}" data-date="" data-date-type="{{define}}" class="layui-input" {{required}} placeholder="请输入{{comment}}" value="{{value}}">
+            </div>
+        </div>

+ 8 - 0
app/admin/service/curd/templates/view/module/editor.code

@@ -0,0 +1,8 @@
+
+        <div class="layui-form-item">
+            <label class="layui-form-label">{{comment}}</label>
+            <div class="layui-input-block">
+                {:editor_textarea({{value}},"{{field}}","{{comment}}")}
+
+            </div>
+        </div>

+ 11 - 0
app/admin/service/curd/templates/view/module/file.code

@@ -0,0 +1,11 @@
+
+        <div class="layui-form-item">
+            <label class="layui-form-label required">{{comment}}</label>
+            <div class="layui-input-block layuimini-upload">
+                <input name="{{field}}" class="layui-input layui-col-xs6" {{required}}  placeholder="请上传{{comment}}" value="{{value}}">
+                <div class="layuimini-upload-btn">
+                    <span><a class="layui-btn" data-upload="{{field}}" data-upload-number="one" data-upload-exts="*" data-upload-icon="file"><i class="fa fa-upload"></i> 上传</a></span>
+                    <span><a class="layui-btn layui-btn-normal" id="select_{{field}}" data-upload-select="{{field}}" data-upload-number="one" data-upload-mimetype="*"><i class="fa fa-list"></i> 选择</a></span>
+                </div>
+            </div>
+        </div>

+ 11 - 0
app/admin/service/curd/templates/view/module/files.code

@@ -0,0 +1,11 @@
+
+        <div class="layui-form-item">
+            <label class="layui-form-label required">{{comment}}</label>
+            <div class="layui-input-block layuimini-upload">
+                <input name="{{field}}" class="layui-input layui-col-xs6" {{required}}  placeholder="请上传{{comment}}" value="{{value}}">
+                <div class="layuimini-upload-btn">
+                    <span><a class="layui-btn" data-upload="{{field}}" data-upload-number="more" data-upload-exts="*" data-upload-icon="file"><i class="fa fa-upload" data-upload-sign="{{define}}"></i> 上传</a></span>
+                    <span><a class="layui-btn layui-btn-normal" id="select_{{field}}" data-upload-select="{{field}}" data-upload-number="more" data-upload-mimetype="*" data-upload-sign="{{define}}"><i class="fa fa-list"></i> 选择</a></span>
+                </div>
+            </div>
+        </div>

+ 11 - 0
app/admin/service/curd/templates/view/module/image.code

@@ -0,0 +1,11 @@
+
+        <div class="layui-form-item">
+            <label class="layui-form-label required">{{comment}}</label>
+            <div class="layui-input-block layuimini-upload">
+                <input name="{{field}}" class="layui-input layui-col-xs6" {{required}}  placeholder="请上传{{comment}}" value="{{value}}">
+                <div class="layuimini-upload-btn">
+                    <span><a class="layui-btn" data-upload="{{field}}" data-upload-number="one" data-upload-exts="png|jpg|ico|jpeg" data-upload-icon="image"><i class="fa fa-upload"></i> 上传</a></span>
+                    <span><a class="layui-btn layui-btn-normal" id="select_{{field}}" data-upload-select="{{field}}" data-upload-number="one" data-upload-mimetype="image/*"><i class="fa fa-list"></i> 选择</a></span>
+                </div>
+            </div>
+        </div>

+ 11 - 0
app/admin/service/curd/templates/view/module/images.code

@@ -0,0 +1,11 @@
+
+        <div class="layui-form-item">
+            <label class="layui-form-label required">{{comment}}</label>
+            <div class="layui-input-block layuimini-upload">
+                <input name="{{field}}" class="layui-input layui-col-xs6" {{required}}  placeholder="请上传{{comment}}" value="{{value}}">
+                <div class="layuimini-upload-btn">
+                    <span><a class="layui-btn" data-upload="{{field}}" data-upload-number="more" data-upload-exts="png|jpg|ico|jpeg" data-upload-icon="image" data-upload-sign="{{define}}"><i class="fa fa-upload"></i> 上传</a></span>
+                    <span><a class="layui-btn layui-btn-normal" id="select_{{field}}" data-upload-select="{{field}}" data-upload-number="more" data-upload-mimetype="image/*" data-upload-sign="{{define}}"><i class="fa fa-list"></i> 选择</a></span>
+                </div>
+            </div>
+        </div>

+ 7 - 0
app/admin/service/curd/templates/view/module/input.code

@@ -0,0 +1,7 @@
+
+        <div class="layui-form-item">
+            <label class="layui-form-label">{{comment}}</label>
+            <div class="layui-input-block">
+                <input type="text" name="{{field}}" class="layui-input" {{required}} placeholder="请输入{{comment}}" value="{{value}}">
+            </div>
+        </div>

+ 4 - 0
app/admin/service/curd/templates/view/module/option.code

@@ -0,0 +1,4 @@
+                    <option value=''></option>
+                    {foreach ${{name}} as $k=>$v}
+                    <option value='{$k}' {{select}}>{$v}</option>
+                    {/foreach}

+ 7 - 0
app/admin/service/curd/templates/view/module/radio.code

@@ -0,0 +1,7 @@
+
+        <div class="layui-form-item">
+            <label class="layui-form-label">{{comment}}</label>
+            <div class="layui-input-block">
+{{define}}
+            </div>
+        </div>

+ 3 - 0
app/admin/service/curd/templates/view/module/radioInput.code

@@ -0,0 +1,3 @@
+                {foreach ${{name}} as $k=>$v}
+                <input type="radio" name="{{field}}" value="{$k}" title="{$v}" {{select}}>
+                {/foreach}

+ 9 - 0
app/admin/service/curd/templates/view/module/select.code

@@ -0,0 +1,9 @@
+
+        <div class="layui-form-item">
+            <label class="layui-form-label">{{comment}}</label>
+            <div class="layui-input-block">
+                <select name="{{field}}" {{required}}>
+{{define}}
+                </select>
+            </div>
+        </div>

+ 7 - 0
app/admin/service/curd/templates/view/module/sort.code

@@ -0,0 +1,7 @@
+
+        <div class="layui-form-item">
+            <label class="layui-form-label">{{comment}}</label>
+            <div class="layui-input-block">
+                <input type="number" name="{{field}}" class="layui-input" lay-affix="number" {{required}} placeholder="请输入{{comment}}" value="{{value}}">
+            </div>
+        </div>

+ 7 - 0
app/admin/service/curd/templates/view/module/textarea.code

@@ -0,0 +1,7 @@
+
+        <div class="layui-form-item layui-form-text">
+            <label class="layui-form-label">{{comment}}</label>
+            <div class="layui-input-block">
+                <textarea name="{{field}}" class="layui-textarea" {{required}} placeholder="请输入{{comment}}">{{value}}</textarea>
+            </div>
+        </div>

+ 13 - 0
app/admin/service/curd/templates/view/recycle.code

@@ -0,0 +1,13 @@
+<div class="layuimini-container">
+    <div class="layuimini-main">
+        <table id="currentTable" class="layui-table layui-hide"
+               data-auth-recycle="{:auth('{{controllerUrl}}/recycle')}"
+               lay-filter="currentTable">
+               <!-- searchTableShow="false" 隐藏搜索框 -->
+        </table>
+    </div>
+</div>
+
+<script>
+{{notesScript}}
+</script>

+ 108 - 0
app/admin/service/tool/CommonTool.php

@@ -0,0 +1,108 @@
+<?php
+
+namespace app\admin\service\tool;
+
+class CommonTool
+{
+
+    /**
+     * 下划线转驼峰
+     * @param $str
+     * @return null|string|string[]
+     */
+    public static function lineToHump($str)
+    {
+        $str = preg_replace_callback('/([-_]+([a-z]{1}))/i', function ($matches) {
+            return strtoupper($matches[2]);
+        }, $str);
+        return $str;
+    }
+
+    /**
+     * 驼峰转下划线
+     * @param $str
+     * @return null|string|string[]
+     */
+    public static function humpToLine($str)
+    {
+        $str = preg_replace_callback('/([A-Z]{1})/', function ($matches) {
+            return '_' . strtolower($matches[0]);
+        }, $str);
+        return $str;
+    }
+
+    /**
+     * 获取真实IP
+     * @return mixed
+     */
+    public static function getRealIp()
+    {
+        $ip = $_SERVER['REMOTE_ADDR'];
+        if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
+            foreach ($matches[0] as $xip) {
+                if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) {
+                    $ip = $xip;
+                    break;
+                }
+            }
+        }elseif (isset($_SERVER['HTTP_CLIENT_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CLIENT_IP'])) {
+            $ip = $_SERVER['HTTP_CLIENT_IP'];
+        }elseif (isset($_SERVER['HTTP_CF_CONNECTING_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CF_CONNECTING_IP'])) {
+            $ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
+        }elseif (isset($_SERVER['HTTP_X_REAL_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_X_REAL_IP'])) {
+            $ip = $_SERVER['HTTP_X_REAL_IP'];
+        }
+        return $ip;
+    }
+
+    /**
+     * 读取文件夹下的所有文件
+     * @param $path
+     * @param $basePath
+     * @return array|mixed
+     */
+    public static function readDirAllFiles($path, $basePath = '')
+    {
+        list($list, $temp_list) = [[], scandir($path)];
+        empty($basePath) && $basePath = $path;
+        foreach ($temp_list as $file) {
+            if ($file != ".." && $file != ".") {
+                if (is_dir($path . DIRECTORY_SEPARATOR . $file)) {
+                    $childFiles = self::readDirAllFiles($path . DIRECTORY_SEPARATOR . $file, $basePath);
+                    $list       = array_merge($childFiles, $list);
+                }else {
+                    $filePath        = $path . DIRECTORY_SEPARATOR . $file;
+                    $fileName        = str_replace($basePath . DIRECTORY_SEPARATOR, '', $filePath);
+                    $list[$fileName] = $filePath;
+                }
+            }
+        }
+        return $list;
+    }
+
+    /**
+     * 模板值替换
+     * @param $string
+     * @param $array
+     * @return mixed
+     */
+    public static function replaceTemplate($string, $array)
+    {
+        foreach ($array as $key => $val) {
+            if (is_null($val)) $val = '';
+            $string = str_replace("{{" . $key . "}}", $val, $string);
+        }
+        return $string;
+    }
+
+
+    public static function replaceArrayString(?string $arrayString): string
+    {
+        $arrayString = str_replace('array (', '[', $arrayString);
+        $arrayString = str_replace(')', ']', $arrayString);
+        $arrayString = str_replace('=> 
+  [', '=> [', $arrayString);
+        return $arrayString;
+    }
+    
+}

+ 203 - 0
app/admin/traits/Curd.php

@@ -0,0 +1,203 @@
+<?php
+
+namespace app\admin\traits;
+
+use app\admin\service\annotation\NodeAnnotation;
+use app\admin\service\tool\CommonTool;
+use app\Request;
+use think\db\exception\PDOException;
+use think\facade\Db;
+use think\response\Json;
+
+/**
+ * 后台CURD复用
+ * Trait Curd
+ * @package app\admin\traits
+ */
+trait Curd
+{
+
+    #[NodeAnnotation(title: '列表', auth: true)]
+    public function index(Request $request): Json|string
+    {
+        if ($request->isAjax()) {
+            if (input('selectFields')) {
+                return $this->selectList();
+            }
+            list($page, $limit, $where) = $this->buildTableParams();
+            $count = self::$model::where($where)->count();
+            $list  = self::$model::where($where)->page($page, $limit)->order($this->sort)->select()->toArray();
+            $data  = [
+                'code'  => 0,
+                'msg'   => '',
+                'count' => $count,
+                'data'  => $list,
+            ];
+            return json($data);
+        }
+        return $this->fetch();
+    }
+
+    #[NodeAnnotation(title: '添加', auth: true)]
+    public function add(Request $request): string
+    {
+        if ($request->isPost()) {
+            $post = $request->post();
+            $rule = [];
+            $this->validate($post, $rule);
+            try {
+                Db::transaction(function() use ($post, &$save) {
+                    $save = self::$model::create($post);
+                });
+            }catch (\Exception $e) {
+                $this->error('新增失败:' . $e->getMessage());
+            }
+            $save ? $this->success('新增成功') : $this->error('新增失败');
+        }
+        return $this->fetch();
+    }
+
+    #[NodeAnnotation(title: '编辑', auth: true)]
+    public function edit(Request $request, $id = 0): string
+    {
+        $row = self::$model::find($id);
+        empty($row) && $this->error('数据不存在');
+        if ($request->isPost()) {
+            $post = $request->post();
+            $rule = [];
+            $this->validate($post, $rule);
+            try {
+                Db::transaction(function() use ($post, $row, &$save) {
+                    $save = $row->save($post);
+                });
+            }catch (\Exception $e) {
+                $this->error('保存失败');
+            }
+            $save ? $this->success('保存成功') : $this->error('保存失败');
+        }
+        $this->assign('row', $row);
+        return $this->fetch();
+    }
+
+    #[NodeAnnotation(title: '删除', auth: true)]
+    public function delete(Request $request): void
+    {
+        // 如果不是id作为主键 请在对应的控制器中覆盖重写
+        $id = $request->param('id', []);
+        $this->checkPostRequest();
+        $row = self::$model::whereIn('id', $id)->select();
+        $row->isEmpty() && $this->error('数据不存在');
+        try {
+            $save = $row->delete();
+        }catch (\Exception $e) {
+            $this->error('删除失败');
+        }
+        $save ? $this->success('删除成功') : $this->error('删除失败');
+    }
+
+    #[NodeAnnotation(title: '导出', auth: true)]
+    public function export()
+    {
+        if (env('EASYADMIN.IS_DEMO', false)) {
+            $this->error('演示环境下不允许操作');
+        }
+        list($page, $limit, $where) = $this->buildTableParams();
+        $tableName = (new self::$model)->getName();
+        $tableName = CommonTool::humpToLine(lcfirst($tableName));
+        $prefix    = config('database.connections.mysql.prefix');
+        $dbList    = Db::query("show full columns from {$prefix}{$tableName}");
+        $header    = [];
+        foreach ($dbList as $vo) {
+            $comment = !empty($vo['Comment']) ? $vo['Comment'] : $vo['Field'];
+            if (!in_array($vo['Field'], $this->noExportFields)) {
+                $header[] = [$comment, $vo['Field']];
+            }
+        }
+        $list = self::$model::where($where)
+            ->limit(100000)
+            ->order($this->sort)
+            ->select()
+            ->toArray();
+        try {
+            exportExcel($header, $list);
+        }catch (\Throwable $exception) {
+            $this->error('导出失败: ' . $exception->getMessage() . PHP_EOL . $exception->getFile() . PHP_EOL . $exception->getLine());
+        }
+    }
+
+    #[NodeAnnotation(title: '属性修改', auth: true)]
+    public function modify(Request $request): void
+    {
+        $this->checkPostRequest();
+        $post = $request->post();
+        $rule = [
+            'id|ID'      => 'require',
+            'field|字段' => 'require',
+            'value|值'   => 'require',
+        ];
+        $this->validate($post, $rule);
+        $row = self::$model::find($post['id']);
+        if (!$row) {
+            $this->error('数据不存在');
+        }
+        if (!in_array($post['field'], $this->allowModifyFields)) {
+            $this->error('该字段不允许修改:' . $post['field']);
+        }
+        try {
+            Db::transaction(function() use ($post, $row) {
+                $row->save([
+                    $post['field'] => $post['value'],
+                ]);
+            });
+        }catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+        $this->success('保存成功');
+    }
+
+    #[NodeAnnotation(title: '回收站', auth: true)]
+    public function recycle(Request $request): Json|string
+    {
+        if (!$request->isAjax()) {
+            return $this->fetch();
+        }
+        $id              = $request->param('id', []);
+        $type            = $request->param('type', '');
+        $deleteTimeField = (new self::$model)->getOption('deleteTime'); // 获取软删除字段
+        $defaultErrorMsg = 'Model 中未设置软删除 deleteTime 对应字段 或 数据表中不存在该字段';
+        if (!$deleteTimeField) $this->success($defaultErrorMsg);
+        switch ($type) {
+            case 'restore':
+                self::$model::withTrashed()->whereIn('id', $id)->strict(false)->update([$deleteTimeField => null, 'update_time' => time()]);
+                $this->success('success');
+                break;
+            case 'delete':
+                self::$model::destroy($id, true);
+                $this->success('success');
+                break;
+            default:
+                list($page, $limit, $where) = $this->buildTableParams();
+                try {
+                    $count = self::$model::withTrashed()->where($where)->whereNotNull($deleteTimeField)->count();
+                    $list  = self::$model::withTrashed()->where($where)->page($page, $limit)->order($this->sort)->whereNotNull($deleteTimeField)->select()->toArray();
+                    $data  = [
+                        'code'  => 0,
+                        'msg'   => '',
+                        'count' => $count,
+                        'data'  => $list,
+                    ];
+                } catch (\Throwable $e) {
+                    $error = $e->getMessage();
+                    if ($e instanceof PDOException) $error .= '<br>' . $defaultErrorMsg;
+                    $data = [
+                        'code'  => -1,
+                        'msg'   => $error,
+                        'count' => 0,
+                        'data'  => [],
+                    ];
+                }
+                return json($data);
+        }
+
+    }
+}

+ 58 - 0
app/admin/view/index/edit_admin.html

@@ -0,0 +1,58 @@
+<div class="layuimini-container">
+    <div class="layuimini-main">
+
+        <form id="app-form" class="layui-form layuimini-form">
+
+            <div class="layui-form-item">
+                <label class="layui-form-label required">用户头像</label>
+                <div class="layui-input-block layuimini-upload">
+                    <input name="head_img" class="layui-input layui-col-xs6" lay-reqtext="请上传用户头像" placeholder="请上传用户头像" value="{$row.head_img|default=''}">
+                    <div class="layuimini-upload-btn">
+                        <span><a class="layui-btn" data-upload="head_img" data-upload-number="one" data-upload-exts="png|jpg|ico|jpeg" data-upload-mimetype="image/*"><i class="fa fa-upload"></i> 上传</a></span>
+                        <span><a class="layui-btn layui-btn-normal" id="select_head_img" data-upload-select="head_img" data-upload-number="one"><i class="fa fa-list"></i> 选择</a></span>
+                    </div>
+                </div>
+            </div>
+
+            <div class="layui-form-item">
+                <label class="layui-form-label required">登录账户</label>
+                <div class="layui-input-block">
+                    <input type="text" name="username" class="layui-input" readonly value="{$row.username|default=''}">
+                    <tip>填写登录账户。</tip>
+                </div>
+            </div>
+
+            <div class="layui-form-item">
+                <label class="layui-form-label">用户手机</label>
+                <div class="layui-input-block">
+                    <input type="text" name="phone" class="layui-input" lay-reqtext="请输入用户手机" placeholder="请输入用户手机" value="{$row.phone|default=''}">
+                    <tip>填写用户手机。</tip>
+                </div>
+            </div>
+
+            <div class="layui-form-item">
+                <label class="layui-form-label">登录方式</label>
+                <div class="layui-input-block">
+                    {foreach notes.login_type as $key=>$val}
+                    <input type="radio" name="login_type" lay-skin="primary" title="{$val}" value="{$key}" lay-filter="loginType-filter" {if $key==$row.login_type}checked=""{/if}>
+                    {/foreach}
+                </div>
+            </div>
+
+            <div class="layui-form-item layui-form-text">
+                <label class="layui-form-label">备注信息</label>
+                <div class="layui-input-block">
+                    <textarea name="remark" class="layui-textarea" placeholder="请输入备注信息">{$row.remark|default=''}</textarea>
+                </div>
+            </div>
+
+            <div class="hr-line"></div>
+            <div class="layui-form-item text-center">
+                <button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
+                <button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
+            </div>
+
+        </form>
+
+    </div>
+</div>

+ 38 - 0
app/admin/view/index/edit_password.html

@@ -0,0 +1,38 @@
+<div class="layuimini-container">
+    <div class="layuimini-main">
+
+        <form id="app-form" class="layui-form layuimini-form">
+
+            <div class="layui-form-item">
+                <label class="layui-form-label required">登录账户</label>
+                <div class="layui-input-block">
+                    <input type="text" name="username" class="layui-input" readonly value="{$row.username|default=''}">
+                </div>
+            </div>
+
+            <div class="layui-form-item">
+                <label class="layui-form-label">登录密码</label>
+                <div class="layui-input-block">
+                    <input type="password" name="password" class="layui-input" lay-verify="required"  lay-reqtext="请输入登录密码" placeholder="请输入登录密码" value="">
+                    <tip>填写登录密码。</tip>
+                </div>
+            </div>
+
+            <div class="layui-form-item">
+                <label class="layui-form-label">确认密码</label>
+                <div class="layui-input-block">
+                    <input type="password" name="password_again" class="layui-input" lay-verify="required"  lay-reqtext="请输入确认密码" placeholder="请输入确认密码" value="">
+                    <tip>填写再次登录密码。</tip>
+                </div>
+            </div>
+
+            <div class="hr-line"></div>
+            <div class="layui-form-item text-center">
+                <button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
+                <button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
+            </div>
+
+        </form>
+
+    </div>
+</div>

+ 120 - 0
app/admin/view/index/index.html

@@ -0,0 +1,120 @@
+<link rel="stylesheet" href="/static/plugs/lay-module/layuimini/layuimini.css?v={$version}" media="all">
+<link rel="stylesheet" href="/static/plugs/lay-module/layuimini/themes/default.css?v={$version}" media="all">
+<style id="layuimini-bg-color"></style>
+<div class="layui-layout-body layuimini-all">
+    <div class="layui-layout layui-layout-admin">
+
+        <div class="layui-header header">
+            <div class="layui-logo layuimini-logo"></div>
+
+            <div class="layuimini-header-content">
+                <a>
+                    <div class="layuimini-tool"><i title="展开" class="fa fa-outdent" data-side-fold="1"></i></div>
+                </a>
+
+                <!--电脑端头部菜单-->
+                <ul class="layui-nav layui-layout-left layuimini-header-menu layuimini-menu-header-pc layuimini-pc-show">
+                </ul>
+
+                <!--手机端头部菜单-->
+                <ul class="layui-nav layui-layout-left layuimini-header-menu layuimini-mobile-show">
+                    <li class="layui-nav-item">
+                        <a href="javascript:;"><i class="fa fa-list-ul"></i> 选择模块</a>
+                        <dl class="layui-nav-child layuimini-menu-header-mobile">
+                        </dl>
+                    </li>
+                </ul>
+
+                <ul class="layui-nav layui-layout-right">
+                    <li class="layui-nav-item" lay-unselect>
+                        <a href="http://easyadmin8.top" target="_blank"><i class="fa fa-home"></i></a>
+                    </li>
+                    <li class="layui-nav-item" lay-unselect>
+                        <a href="javascript:;" data-refresh="刷新"><i class="fa fa-refresh"></i></a>
+                    </li>
+                    <li class="layui-nav-item" lay-unselect>
+                        <a href="javascript:;" data-clear="清理" class="layuimini-clear"><i class="fa fa-trash"></i></a>
+                    </li>
+                    <li class="layui-nav-item mobile layui-hide-xs" lay-unselect>
+                        <a href="javascript:;" data-check-screen="full"><i class="fa fa-arrows-alt"></i></a>
+                    </li>
+                    <li class="layui-nav-item mobile layui-hide-xs" lay-unselect>
+                        <div class="layui-form ws-header-theme" lay-filter="header-theme">
+                            <input type="checkbox" name="theme-mode" lay-filter="header-theme-mode" lay-skin="switch">
+                            <div lay-checkbox>
+                                <i class="layui-icon layui-icon-moon"></i> |
+                                <i class="layui-icon layui-icon-light"></i>
+                            </div>
+                        </div>
+                    </li>
+                    <li class="layui-nav-item layuimini-setting">
+                        <a href="javascript:;">
+                            <img src="{:session('admin.head_img')}" class="layui-nav-img" width="50" height="50">
+                            <cite class="adminName">{:session('admin.username')}</cite>
+                            <span class="layui-nav-more"></span>
+                        </a>
+                        <dl class="layui-nav-child">
+                            <dd>
+                                <a href="javascript:;" layuimini-content-href="{:__url('index/editAdmin')}" data-title="基本资料" data-icon="fa fa-gears">基本资料<span class="layui-badge-dot"></span></a>
+                            </dd>
+                            <dd>
+                                <a href="javascript:;" layuimini-content-href="{:__url('index/editPassword')}" data-title="修改密码" data-icon="fa fa-gears">修改密码</a>
+                            </dd>
+                            <dd>
+                                <hr>
+                            </dd>
+                            <dd>
+                                <a href="javascript:;" class="login-out">退出登录</a>
+                            </dd>
+                        </dl>
+                    </li>
+                    <li class="layui-nav-item layuimini-select-bgcolor" lay-unselect>
+                        <a href="javascript:;" data-bgcolor="配色方案"><i class="fa fa-ellipsis-v"></i></a>
+                    </li>
+                </ul>
+            </div>
+        </div>
+
+        <!--无限极左侧菜单-->
+        <div class="layui-side layui-bg-black layuimini-menu-left">
+        </div>
+
+        <!--初始化加载层-->
+        <div class="layuimini-loader">
+            <div class="layuimini-loader-inner"></div>
+        </div>
+
+        <!--手机端遮罩层-->
+        <div class="layuimini-make"></div>
+
+        <!-- 移动导航 -->
+        <div class="layuimini-site-mobile"><i class="layui-icon"></i></div>
+
+        <div class="layui-body">
+            <div class="layuimini-tab layui-tabs-rollTool layui-tabs" lay-filter="layuiminiTab" id="layuiminiTab">
+                <ul class="layui-tabs-header">
+                    <li class="layui-this" id="layuiminiHomeTabId" lay-id=""></li>
+                </ul>
+                <div class="layui-tab-control">
+                    <li class="layuimini-tab-roll-left layui-icon layui-icon-left"></li>
+                    <li class="layuimini-tab-roll-right layui-icon layui-icon-right"></li>
+                    <li class="layui-tab-tool layui-icon layui-icon-down">
+                        <ul class="layui-nav close-box">
+                            <li class="layui-nav-item">
+                                <a href="javascript:;"><span class="layui-nav-more"></span></a>
+                                <dl class="layui-nav-child">
+                                    <dd><a href="javascript:;" layuimini-tab-close="current">关 闭 当 前</a></dd>
+                                    <dd><a href="javascript:;" layuimini-tab-close="other">关 闭 其 他</a></dd>
+                                    <dd><a href="javascript:;" layuimini-tab-close="all">关 闭 全 部</a></dd>
+                                </dl>
+                            </li>
+                        </ul>
+                    </li>
+                </div>
+                <div class="layui-tabs-body">
+                    <div id="layuiminiHomeTabIframe" class="layui-tab-item layui-tabs-item layui-show"></div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>

+ 45 - 0
app/admin/view/index/set2fa.html

@@ -0,0 +1,45 @@
+<div class="layuimini-container">
+    <form id="app-form" class="layui-form layuimini-form" autocomplete="off">
+        {if $old_secret}
+        <div class="layui-card">
+            <div class="layui-card-header">提示</div>
+            <div class="layui-card-body">
+                当前账号已经绑定过了 谷歌验证码 ,如果重新保存将替换
+            </div>
+        </div>
+        {/if}
+        <div class="layui-form-item">
+            <label class="layui-form-label required">验证秘钥</label>
+            <div class="layui-input-block">
+                <input type="text" name="ga_secret" class="layui-input" value="{$secret}" readonly disabled>
+            </div>
+        </div>
+
+        <div class="layui-form-item">
+            <label class="layui-form-label required">二维码</label>
+            <div class="layui-input-block">
+                <img src="{$dataUri}" alt="二维码" style="width: 200px;height: 200px">
+                <div class="layui-text layui-font-cyan layui-font-12">
+                    使用&nbsp;
+                    <a href="https://2fas.com" target="_blank"><span class="layui-text layui-font-blue">2FAS</span></a>
+                    &nbsp;或者&nbsp;
+                    <a href="https://cn.bing.com/search?q=Google+Authenticator" target="_blank"><span class="layui-text layui-font-blue">Google Authenticator</span></a>
+                    &nbsp;APP 扫描二维码 后 输入验证码 进行绑定
+                </div>
+            </div>
+        </div>
+        <div class="layui-form-item">
+            <label class="layui-form-label required">谷歌验证码</label>
+            <div class="layui-input-block">
+                <input type="text" name="ga_code" class="layui-input" maxlength="6" lay-verify="required" placeholder="扫描二维码,输入验证码" value="">
+            </div>
+        </div>
+        <div class=" hr-line">
+        </div>
+        <div class="layui-form-item text-center">
+            <button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
+            <button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
+        </div>
+
+    </form>
+</div>

+ 218 - 0
app/admin/view/index/welcome.html

@@ -0,0 +1,218 @@
+<link rel="stylesheet" href="/static/admin/css/welcome.css?v={$version}" media="all">
+<div class="layui-layout layui-padding-2">
+    <div class="layui-layout-admin">
+        <div class="layui-row layui-col-space10">
+            <div class="layui-col-md8 ">
+                <div class="layui-row layui-col-space10">
+                    <div class="layui-col-md6 ">
+                        <div class="layui-card">
+                            <div class="layui-card-header"><i class="fa fa-warning icon"></i>数据统计</div>
+                            <div class="layui-card-body">
+                                <div class="welcome-module">
+                                    <div class="layui-row layui-col-space10">
+                                        <div class="layui-col-xs6">
+                                            <div class="layui-panel">
+                                                <div class="layui-card-body">
+                                                    <span class="layui-badge layui-bg-cyan fa-pull-right ">实时</span>
+                                                    <div class="panel-content">
+                                                        <h5>用户统计</h5>
+                                                        <h2>1234</h2>
+                                                        <h6>记录数</h6>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                        </div>
+                                        <div class="layui-col-xs6">
+                                            <div class="layui-panel">
+                                                <div class="layui-card-body">
+                                                    <span class="layui-badge layui-bg-purple fa-pull-right ">实时</span>
+                                                    <div class="panel-content">
+                                                        <h5>商品统计</h5>
+                                                        <h2>1234</h2>
+                                                        <h6>记录数</h6>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                        </div>
+                                        <div class="layui-col-xs6">
+                                            <div class="layui-panel">
+                                                <div class="layui-card-body ">
+                                                    <span class="layui-badge layui-bg-orange fa-pull-right ">实时</span>
+                                                    <div class="panel-content">
+                                                        <h5>浏览统计</h5>
+                                                        <h2>1234</h2>
+                                                        <h6>记录数</h6>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                        </div>
+                                        <div class="layui-col-xs6">
+                                            <div class="layui-panel">
+                                                <div class="layui-card-body ">
+                                                    <span class="layui-badge layui-bg-red fa-pull-right ">实时</span>
+                                                    <div class="panel-content">
+                                                        <h5>订单统计</h5>
+                                                        <h2>1234</h2>
+                                                        <h6>记录数</h6>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="layui-col-md6 ">
+                        <div class="layui-card">
+                            <div class="layui-card-header"><i class="fa fa-credit-card icon icon-blue"></i>快捷入口</div>
+                            <div class="layui-card-body">
+                                <div class="welcome-module">
+                                    <div class="layui-row layui-col-space10">
+                                        <div class="swiper mySwiper">
+                                            <div class="swiper-wrapper">
+                                                {foreach $quicks as $value}
+
+                                                <div class="swiper-slide">
+                                                    {foreach $value as $vo}
+
+                                                    <div class="layui-col-xs3 layuimini-qiuck-module">
+                                                        <a layuimini-content-href="{:url($vo['href'])}" data-title="{$vo['title']}">
+                                                            <i class="{$vo['icon']|raw}"></i>
+                                                            <cite>{$vo['title']}</cite>
+                                                        </a>
+                                                    </div>
+                                                    {/foreach}
+
+                                                </div>
+                                                {/foreach}
+
+                                            </div>
+                                        </div>
+                                        <div class="swiper-pagination"></div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+
+                    </div>
+                    <div class="layui-col-md12 ">
+                        <div class="layui-card">
+                            <div class="layui-card-header"><i class="fa fa-line-chart icon"></i>报表统计</div>
+                            <div class="layui-card-body">
+                                <div id="echarts-records" style="width: 100%;min-height:500px"></div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="layui-col-md4 ">
+
+                <div class="layui-card">
+                    <div class="layui-card-header"><i class="fa fa-fire icon"></i>版本信息</div>
+                    <div class="layui-card-body layui-text">
+                        <table class="layui-table">
+                            <colgroup>
+                                <col width="150">
+                                <col>
+                            </colgroup>
+                            <tbody>
+                            <tr>
+                                <td>框架名称</td>
+                                <td>
+                                    <button type="button" class="layui-btn layui-btn-xs layui-btn-primary">EasyAdmin8</button>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>ThinkPHP版本</td>
+                                <td>
+                                    <button type="button" class="layui-btn layui-btn-xs layui-btn-primary">{$versions.tpVersion|default='-'}</button>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>PHP版本</td>
+                                <td>
+                                    <button type="button" class="layui-btn layui-btn-xs layui-btn-primary">{$versions.phpVersion|default='-'}</button>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>MySQL版本</td>
+                                <td>
+                                    <button type="button" class="layui-btn layui-btn-xs layui-btn-primary">{$versions.mysqlVersion|default='-'}</button>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>Layui版本</td>
+                                <td>
+                                    <button type="button" class="layui-btn layui-btn-xs layui-btn-primary" id="layui-version">-</button>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>DEBUG模式</td>
+                                <td>
+                                    <span class="layui-badge {:env('APP_DEBUG')?'layui-bg-cyan':'layui-bg-gray'}">
+                                        {:env('APP_DEBUG')?'开启中':'已关闭'}
+                                    </span>
+                                    <span class="layui-badge layui-bg-gray">建议线上环境关闭 APP_DEBUG</span>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>composer信息</td>
+                                <td>
+                                    <button type="button" class="layui-btn layui-btn-xs layui-bg-cyan" lay-on="showComposerInfo">点击查看</button>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>主要特色</td>
+                                <td>
+                                    <span class="layui-btn layui-btn-xs layui-btn-primary layui-border">零门槛</span>
+                                    <span class="layui-btn layui-btn-xs layui-btn-primary layui-border">响应式</span>
+                                    <span class="layui-btn layui-btn-xs layui-btn-primary layui-border">清爽</span>
+                                    <span class="layui-btn layui-btn-xs layui-btn-primary layui-border">极简</span>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>Gitee</td>
+                                <td>
+                                    <div class="layui-btn-container">
+                                        <a href='https://gitee.com/wolf18/easyAdmin8' target="_blank">
+                                            <img src='https://gitee.com/wolf18/easyAdmin8/badge/star.svg?theme=dark' alt='star'/>
+                                        </a>
+                                    </div>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>Github</td>
+                                <td>
+                                    <a href="https://github.com/wolf-leo/easyAdmin8" target="_blank">
+                                        <i class="layui-icon layui-icon-github layui-font-20 layui-font-gray layui-text"></i>
+                                    </a>
+                                </td>
+                            </tr>
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
+
+                <div class="layui-card">
+                    <div class="layui-card-header"><i class="fa fa-paper-plane-o icon"></i>作者心语</div>
+                    <div class="layui-card-body layui-text">
+                        <p>
+                            本模板基于layui2.x以及font-awesome-6.x进行实现。
+                            <a class="layui-btn layui-btn-xs layui-btn-danger" style="vertical-align: baseline;" target="_blank" href="http://layui.dev/docs">layui文档</a>
+                        </p>
+                        <hr>
+                        <p class="layui-font-red">备注:此后台框架永久开源,但请勿进行出售或者上传到任何素材网站,否则将追究相应的责任。</p>
+                        <hr>
+                        <div class="layui-card-header"><i class="fa fa-qq icon"></i>QQ交流群</div>
+                        <div class="layui-card-body">
+                            <img src="/static/common/images/EasyAdmin8-ThinkPHP.png" width="145">
+                        </div>
+                    </div>
+                </div>
+
+            </div>
+        </div>
+    </div>
+</div>

+ 42 - 0
app/admin/view/layout/default.html

@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>{:sysConfig('site','site_name')}</title>
+    <meta name="renderer" content="webkit">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+    <link rel="icon" href="{:sysConfig('site', 'site_ico')}" type="image/x-icon">
+    <!--[if lt IE 9]>
+    <script src="https://cdn.staticfile.org/html5shiv/r29/html5.min.js"></script>
+    <script src="https://cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script>
+    <![endif]-->
+    <link rel="stylesheet" href="/static/admin/css/public.css?v={$version}" media="all">
+    <link rel="stylesheet" href="" id="layuicss-theme-dark" media="all">
+    <script>
+        window.CONFIG = {
+            ADMIN: "{$adminModuleName|default='admin'}",
+            CONTROLLER_JS_PATH: "{$thisControllerJsPath|default=''}",
+            ACTION: "{$thisAction|default=''}",
+            AUTOLOAD_JS: "{$autoloadJs|default='false'}",
+            IS_SUPER_ADMIN: "{$isSuperAdmin|default='false'}",
+            VERSION: "{$version|default='1.0.0'}",
+            CSRF_TOKEN: "{:token()}",
+            ADMIN_UPLOAD_URL: "{$adminUploadUrl|default=''}",
+            IFRAME_OPEN_TOP: "{$iframeOpenTop|default=0}",
+            MAX_FILE_SIZE: "{$maxFileSize|default=2 * 1024 * 1024}",
+            EDITOR_TYPE: "{$adminEditor|default='wangEditor'}",
+        };
+    </script>
+    <script src="/static/plugs/layui-v2.x/layui.js" charset="utf-8"></script>
+    <script src="/static/plugs/require-2.3.6/require.js" charset="utf-8"></script>
+    <script src="/static/config-admin.js?v={$version}" charset="utf-8"></script>
+    <script src="/static/common/js/admin.js?v={$version}" charset="utf-8"></script>
+    {include file="layout/editor" /}
+</head>
+<body>
+
+{__CONTENT__}
+
+</body>
+</html>

+ 27 - 0
app/admin/view/layout/editor.html

@@ -0,0 +1,27 @@
+    {switch $adminEditor}
+    {case ckeditor}
+
+    <script src="/static/plugs/ckeditor4/ckeditor.js?v={$version}" charset="utf-8"></script>
+    {/case}
+
+    {case wangEditor}
+
+    <link rel="stylesheet" href="/static/plugs/wangEditor/dist/style.css?v={$version}">
+    <script src="/static/plugs/wangEditor/dist/index.js?v={$version}"></script>
+    {/case}
+
+
+    {case EasyMDE}
+
+    <link rel="stylesheet" href="/static/plugs/easymde/easymde.min.css?v={$version}">
+    <script src="/static/plugs/easymde/easymde.min.js?v={$version}"></script>
+    {/case}
+
+    {default /}
+
+    <script src="/static/plugs/ueditor/ueditor.config.js?v={$version}" charset="utf-8"></script>
+    <script src="/static/plugs/ueditor/ueditor.all.js?v={$version}" charset="utf-8"></script>
+    <script src="/static/plugs/ueditor/lang/zh-cn/zh-cn.js?v={$version}" charset="utf-8"></script>
+    <script src="/static/plugs/ueditor/third-party/codemirror/codemirror.js?v={$version}" charset="utf-8"></script>
+    <script src="/static/plugs/ueditor/third-party/zeroclipboard/zeroclipboard.js?v={$version}" charset="utf-8"></script>
+    {/switch}

+ 55 - 0
app/admin/view/login/index.html

@@ -0,0 +1,55 @@
+<link rel="stylesheet" href="/static/admin/css/login.css?v={$version}" media="all">
+<div class="container">
+    <div class="main-body">
+        <div class="login-main">
+            <div class="login-top">
+                <span>{:sysConfig('site','site_name')}</span>
+                <span class="bg1"></span>
+                <span class="bg2"></span>
+            </div>
+            <form class="layui-form login-bottom">
+                <div class="demo {if !$isDemo}layui-hide{/if}">用户名:admin 密码:123456</div>
+                <div class="center">
+
+                    <div class="item">
+                        <span class="icon icon-2"></span>
+                        <input type="text" name="username" lay-verify="required" placeholder="请输入登录账号" maxlength="24"/>
+                    </div>
+
+                    <div class="item">
+                        <span class="icon icon-3"></span>
+                        <input type="password" name="password" lay-verify="required" placeholder="请输入密码" maxlength="20">
+                        <span class="bind-password icon icon-4"></span>
+                    </div>
+
+                    <div class="item layui-hide" id="gaCode">
+                        <span class="icon icon-3"></span>
+                        <input type="text" name="ga_code" placeholder="谷歌验证码" maxlength="6">
+                    </div>
+
+                    {if $captcha == 1}
+                    <div id="validatePanel" class="item" style="width: 137px;">
+                        <input type="text" name="captcha" placeholder="请输入验证码" maxlength="4">
+                        <img id="refreshCaptcha" class="validateImg" src="{:url('login/captcha')}" onclick="this.src='{:url(\'login/captcha\')}?seed='+Math.random()">
+                    </div>
+                    {/if}
+
+                </div>
+                <div class="tip">
+                    <span class="icon-nocheck"></span>
+                    <span class="login-tip">保持登录</span>
+                    <a href="javascript:" class="forget-password">忘记密码?</a>
+                </div>
+                <div class="layui-form-item" style="text-align:center; width:100%;height:100%;margin:0px;">
+                    <button type="button" class="login-btn" lay-submit>立即登录</button>
+                </div>
+            </form>
+        </div>
+    </div>
+    <div class="footer">
+        {:sysConfig('site','site_copyright')}<span class="padding-5">|</span><a target="_blank" href="http://www.miitbeian.gov.cn">{:sysConfig('site','site_beian')}</a>
+    </div>
+</div>
+<script>
+    let backgroundUrl = "{:sysConfig('site','admin_background')}"
+</script>

+ 43 - 0
app/admin/view/mall/cate/add.html

@@ -0,0 +1,43 @@
+<div class="layuimini-container">
+    <form id="app-form" class="layui-form layuimini-form">
+
+        <div class="layui-form-item">
+            <label class="layui-form-label">分类名称</label>
+            <div class="layui-input-block">
+                <input type="text" name="title" class="layui-input" lay-verify="required" placeholder="请输入分类名称" value="">
+            </div>
+        </div>
+
+        <div class="layui-form-item">
+            <label class="layui-form-label required">分类图片</label>
+            <div class="layui-input-block layuimini-upload">
+                <input name="image" class="layui-input layui-col-xs6" lay-verify="required"  placeholder="请上传分类图片" value="">
+                <div class="layuimini-upload-btn">
+                    <span><a class="layui-btn" data-upload="image" data-upload-number="one" data-upload-exts="png|jpg|ico|jpeg" data-upload-icon="image" data-upload-mimetype="image/*"><i class="fa fa-upload"></i> 上传</a></span>
+                    <span><a class="layui-btn layui-btn-normal" id="select_image" data-upload-select="image" data-upload-number="one"><i class="fa fa-list"></i> 选择</a></span>
+                </div>
+            </div>
+        </div>
+
+        <div class="layui-form-item">
+            <label class="layui-form-label">分类排序</label>
+            <div class="layui-input-block">
+                <input type="number" name="sort" class="layui-input" lay-affix="number" placeholder="请输入分类排序" value="0">
+            </div>
+        </div>
+
+        <div class="layui-form-item layui-form-text">
+            <label class="layui-form-label">备注信息</label>
+            <div class="layui-input-block">
+                <textarea name="remark" class="layui-textarea" placeholder="请输入备注信息"></textarea>
+            </div>
+        </div>
+
+        <div class="hr-line"></div>
+        <div class="layui-form-item text-center">
+            <button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
+            <button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
+        </div>
+
+    </form>
+</div>

+ 43 - 0
app/admin/view/mall/cate/edit.html

@@ -0,0 +1,43 @@
+<div class="layuimini-container">
+    <form id="app-form" class="layui-form layuimini-form">
+
+        <div class="layui-form-item">
+            <label class="layui-form-label">分类名称</label>
+            <div class="layui-input-block">
+                <input type="text" name="title" class="layui-input" lay-verify="required" placeholder="请输入分类名称" value="{$row.title|default=''}">
+            </div>
+        </div>
+
+        <div class="layui-form-item">
+            <label class="layui-form-label required">分类图片</label>
+            <div class="layui-input-block layuimini-upload">
+                <input name="image" class="layui-input layui-col-xs6" lay-verify="required" lay-reqtext="请上传分类图片" placeholder="请上传分类图片" value="{$row.image|default=''}">
+                <div class="layuimini-upload-btn">
+                    <span><a class="layui-btn" data-upload="image" data-upload-number="one" data-upload-exts="png|jpg|ico|jpeg" data-upload-icon="image" data-upload-mimetype="image/*"><i class="fa fa-upload"></i> 上传</a></span>
+                    <span><a class="layui-btn layui-btn-normal" id="select_image" data-upload-select="image" data-upload-number="one"><i class="fa fa-list"></i> 选择</a></span>
+                </div>
+            </div>
+        </div>
+
+        <div class="layui-form-item">
+            <label class="layui-form-label">分类排序</label>
+            <div class="layui-input-block">
+                <input type="number" name="sort" class="layui-input" lay-affix="number" placeholder="请输入分类排序" value="{$row.sort|default=''}">
+            </div>
+        </div>
+
+        <div class="layui-form-item layui-form-text">
+            <label class="layui-form-label">备注信息</label>
+            <div class="layui-input-block">
+                <textarea name="remark" class="layui-textarea" placeholder="请输入备注信息">{$row.remark|default=''}</textarea>
+            </div>
+        </div>
+
+        <div class="hr-line"></div>
+        <div class="layui-form-item text-center">
+            <button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
+            <button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
+        </div>
+
+    </form>
+</div>

+ 10 - 0
app/admin/view/mall/cate/index.html

@@ -0,0 +1,10 @@
+<div class="layuimini-container">
+    <div class="layuimini-main">
+        <table id="currentTable" class="layui-table layui-hide"
+               data-auth-add="{:auth('mall.cate/add')}"
+               data-auth-edit="{:auth('mall.cate/edit')}"
+               data-auth-delete="{:auth('mall.cate/delete')}"
+               lay-filter="currentTable">
+        </table>
+    </div>
+</div>

+ 131 - 0
app/admin/view/mall/goods/add.html

@@ -0,0 +1,131 @@
+<div class="layuimini-container">
+    <form id="app-form" class="layui-form layuimini-form layui-form-pane">
+
+        <div class="layui-row">
+            <div class="layui-col-xl5 layui-col-lg5 layui-col-md12 layui-col-sm12 layui-col-xs12">
+                <!-- 可以使用该方式 推荐写法-->
+                <div class="layui-form-item">
+                    <label class="layui-form-label">商品分类</label>
+                    <div class="layui-input-block">
+                        <select name="cate_id" lay-verify="required" data-select="{:url('mall.cate/index')}" data-fields="id,title">
+                        </select>
+                    </div>
+                </div>
+
+                <!--也可以使用该方式-->
+                <div class="layui-form-item">
+                    <label class="layui-form-label">商品分类2</label>
+                    <div class="layui-input-block">
+                        <select name="cate_id" lay-verify="required">
+                            {volist name='cate' id='vo'}
+                            <option value="{$key}">{$vo}</option>
+                            {/volist}
+                        </select>
+                    </div>
+                </div>
+
+                <!-- 展现形式不同的写法-->
+                <div class="layui-form-item">
+                    <label class="layui-form-label">商品分类3</label>
+                    <div data-show="switchSelect" data-list='{$cate|json_encode|raw}' data-name="cate_id" data-value="" data-target="radio"></div>
+                </div>
+
+                <div class="layui-form-item">
+                    <div class="layui-row">
+                        <label class="layui-form-label required">商品标题</label>
+                        <div class="layui-input-block layui-col-space5">
+                            <div class="layui-col-xs10">
+                                <div class="layui-input-wrap">
+                                    <input type="text" name="title" class="layui-input" lay-verify="required" placeholder="请输入商品标题" value="">
+                                </div>
+                            </div>
+                            <div class="layui-col-xs2">
+                                <button class="layui-btn layui-bg-purple layui-btn-fluid" type="button" lay-on="AiOptimization">AI优化</button>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="layui-form-item">
+                    <label class="layui-form-label required">商品LOGO</label>
+                    <div class="layui-input-block layuimini-upload">
+                        <input name="logo" class="layui-input layui-col-xs6" lay-verify="required" placeholder="请上传分类图片" value="">
+                        <div class="layuimini-upload-btn">
+                            <span><a class="layui-btn" data-upload="logo" data-upload-number="one" data-upload-exts="png|jpg|ico|jpeg" data-upload-icon="image" data-upload-mimetype="image/*"><i class="fa fa-upload"></i> 上传</a></span>
+                            <span><a class="layui-btn layui-btn-normal" id="select_logo" data-upload-select="logo" data-upload-number="one"><i class="fa fa-list"></i> 选择</a></span>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="layui-form-item">
+                    <label class="layui-form-label required">商品图片</label>
+                    <div class="layui-input-block layuimini-upload">
+                        <input name="images" class="layui-input layui-col-xs6" lay-verify="required" placeholder="请上传商品图片" value="">
+                        <div class="layuimini-upload-btn">
+                            <span><a class="layui-btn" data-upload="images" data-upload-number="more" data-upload-exts="png|jpg|ico|jpeg" data-upload-icon="image" data-upload-mimetype="image/*"><i class="fa fa-upload"></i> 上传</a></span>
+                            <span><a class="layui-btn layui-btn-normal" id="select_images" data-upload-select="images" data-upload-number="more"><i class="fa fa-list"></i> 选择</a></span>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="layui-form-item">
+                    <label class="layui-form-label">市场价格</label>
+                    <div class="layui-input-block">
+                        <input type="text" name="market_price" class="layui-input" lay-verify="required" placeholder="请输入市场价格" value="0">
+                    </div>
+                </div>
+
+                <div class="layui-form-item">
+                    <label class="layui-form-label">折扣价格</label>
+                    <div class="layui-input-block">
+                        <input type="text" name="discount_price" class="layui-input" lay-verify="required" placeholder="请输入折扣价格" value="0">
+                    </div>
+                </div>
+
+                <div class="layui-form-item">
+                    <label class="layui-form-label">虚拟销量</label>
+                    <div class="layui-input-block">
+                        <input type="text" name="virtual_sales" class="layui-input" lay-verify="required" placeholder="请输入虚拟销量" value="0">
+                    </div>
+                </div>
+
+                <div class="layui-form-item">
+                    <label class="layui-form-label">分类排序</label>
+                    <div class="layui-input-block">
+                        <input type="number" name="sort" class="layui-input" lay-affix="number" placeholder="请输入分类排序" value="0">
+                    </div>
+                </div>
+
+                <!-- 文档:https://xm-select.com/file/xm-select/v1.2.4/#/basic/use -->
+                <div class="layui-form-item">
+                    <label class="layui-form-label">模拟多选</label>
+                    <div class="layui-input-block">
+                        <div id="demo1" class="xm-select-demo"></div>
+                    </div>
+                </div>
+
+                <div class="layui-form-item layui-form-text">
+                    <label class="layui-form-label">备注信息</label>
+                    <div class="layui-input-block">
+                        <textarea name="remark" class="layui-textarea" placeholder="请输入备注信息"></textarea>
+                    </div>
+                </div>
+            </div>
+            <div class="layui-col-xl7 layui-col-lg7 layui-col-md12 layui-col-sm12 layui-col-xs12">
+                <div class="layui-form-item layui-form-text">
+                    <label class="layui-form-label">商品描述</label>
+                    <div class="layui-input-block">
+                        {:editor_textarea('','describe')}
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="hr-line"></div>
+        <div class="layui-form-item text-center">
+            <button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
+            <button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
+        </div>
+
+    </form>
+</div>

Некоторые файлы не были показаны из-за большого количества измененных файлов