瀏覽代碼

no message

Ryuiso 1 年之前
父節點
當前提交
9900aaa60c
共有 100 個文件被更改,包括 8192 次插入99 次删除
  1. 4 0
      .gitattributes
  2. 62 97
      .gitignore
  3. 42 0
      .travis.yml
  4. 350 0
      CHANGELOG.md
  5. 201 0
      LICENSE
  6. 132 2
      README.md
  7. 1 0
      app/.htaccess
  8. 22 0
      app/AppService.php
  9. 96 0
      app/BaseController.php
  10. 59 0
      app/ExceptionHandle.php
  11. 9 0
      app/Request.php
  12. 903 0
      app/admin/buildadmin.sql
  13. 84 0
      app/admin/common.php
  14. 107 0
      app/admin/controller/Ajax.php
  15. 15 0
      app/admin/controller/Dashboard.php
  16. 118 0
      app/admin/controller/Index.php
  17. 142 0
      app/admin/controller/Module.php
  18. 17 0
      app/admin/controller/Terminal.php
  19. 35 0
      app/admin/controller/aerocraft/FlightPlan.php
  20. 257 0
      app/admin/controller/auth/Admin.php
  21. 52 0
      app/admin/controller/auth/AdminLog.php
  22. 343 0
      app/admin/controller/auth/Group.php
  23. 224 0
      app/admin/controller/auth/Menu.php
  24. 798 0
      app/admin/controller/crud/Crud.php
  25. 36 0
      app/admin/controller/crud/Log.php
  26. 12 0
      app/admin/controller/demo/NeoHome.php
  27. 16 0
      app/admin/controller/demo/NeoVideo.php
  28. 87 0
      app/admin/controller/routine/AdminInfo.php
  29. 64 0
      app/admin/controller/routine/Attachment.php
  30. 176 0
      app/admin/controller/routine/Config.php
  31. 173 0
      app/admin/controller/security/DataRecycle.php
  32. 91 0
      app/admin/controller/security/DataRecycleLog.php
  33. 230 0
      app/admin/controller/security/SensitiveData.php
  34. 102 0
      app/admin/controller/security/SensitiveDataLog.php
  35. 155 0
      app/admin/controller/user/Group.php
  36. 109 0
      app/admin/controller/user/Identity.php
  37. 43 0
      app/admin/controller/user/MoneyLog.php
  38. 79 0
      app/admin/controller/user/Pilot.php
  39. 210 0
      app/admin/controller/user/Rule.php
  40. 43 0
      app/admin/controller/user/ScoreLog.php
  41. 145 0
      app/admin/controller/user/User.php
  42. 16 0
      app/admin/event.php
  43. 38 0
      app/admin/lang/en.php
  44. 8 0
      app/admin/lang/en/ajax.php
  45. 5 0
      app/admin/lang/en/auth/admin.php
  46. 6 0
      app/admin/lang/en/auth/group.php
  47. 6 0
      app/admin/lang/en/auth/menu.php
  48. 4 0
      app/admin/lang/en/dashboard.php
  49. 9 0
      app/admin/lang/en/index.php
  50. 6 0
      app/admin/lang/en/routine/admininfo.php
  51. 5 0
      app/admin/lang/en/routine/attachment.php
  52. 22 0
      app/admin/lang/en/routine/config.php
  53. 7 0
      app/admin/lang/en/security/datarecycle.php
  54. 4 0
      app/admin/lang/en/security/datarecyclelog.php
  55. 8 0
      app/admin/lang/en/security/sensitivedata.php
  56. 4 0
      app/admin/lang/en/security/sensitivedatalog.php
  57. 8 0
      app/admin/lang/en/user/moneylog.php
  58. 7 0
      app/admin/lang/en/user/pilot.php
  59. 8 0
      app/admin/lang/en/user/scorelog.php
  60. 59 0
      app/admin/lang/zh-cn.php
  61. 8 0
      app/admin/lang/zh-cn/ajax.php
  62. 6 0
      app/admin/lang/zh-cn/auth/admin.php
  63. 9 0
      app/admin/lang/zh-cn/auth/group.php
  64. 6 0
      app/admin/lang/zh-cn/auth/menu.php
  65. 4 0
      app/admin/lang/zh-cn/dashboard.php
  66. 9 0
      app/admin/lang/zh-cn/index.php
  67. 21 0
      app/admin/lang/zh-cn/module.php
  68. 6 0
      app/admin/lang/zh-cn/routine/admininfo.php
  69. 5 0
      app/admin/lang/zh-cn/routine/attachment.php
  70. 23 0
      app/admin/lang/zh-cn/routine/config.php
  71. 7 0
      app/admin/lang/zh-cn/security/datarecycle.php
  72. 4 0
      app/admin/lang/zh-cn/security/datarecyclelog.php
  73. 8 0
      app/admin/lang/zh-cn/security/sensitivedata.php
  74. 4 0
      app/admin/lang/zh-cn/security/sensitivedatalog.php
  75. 8 0
      app/admin/lang/zh-cn/user/moneylog.php
  76. 7 0
      app/admin/lang/zh-cn/user/pilot.php
  77. 8 0
      app/admin/lang/zh-cn/user/scorelog.php
  78. 458 0
      app/admin/library/Auth.php
  79. 959 0
      app/admin/library/crud/Helper.php
  80. 60 0
      app/admin/library/crud/stubs/html/form.stub
  81. 66 0
      app/admin/library/crud/stubs/html/index.stub
  82. 24 0
      app/admin/library/crud/stubs/mixins/controller/controller.stub
  83. 27 0
      app/admin/library/crud/stubs/mixins/controller/index.stub
  84. 6 0
      app/admin/library/crud/stubs/mixins/controller/initialize.stub
  85. 12 0
      app/admin/library/crud/stubs/mixins/model/afterInsert.stub
  86. 5 0
      app/admin/library/crud/stubs/mixins/model/beforeInsert.stub
  87. 5 0
      app/admin/library/crud/stubs/mixins/model/belongsTo.stub
  88. 7 0
      app/admin/library/crud/stubs/mixins/model/getters/cityNames.stub
  89. 5 0
      app/admin/library/crud/stubs/mixins/model/getters/float.stub
  90. 5 0
      app/admin/library/crud/stubs/mixins/model/getters/htmlDecode.stub
  91. 5 0
      app/admin/library/crud/stubs/mixins/model/getters/jsonDecode.stub
  92. 7 0
      app/admin/library/crud/stubs/mixins/model/getters/remoteSelectLabels.stub
  93. 5 0
      app/admin/library/crud/stubs/mixins/model/getters/string.stub
  94. 9 0
      app/admin/library/crud/stubs/mixins/model/getters/stringToArray.stub
  95. 2 0
      app/admin/library/crud/stubs/mixins/model/mixins/beforeInsertWithSnowflake.stub
  96. 18 0
      app/admin/library/crud/stubs/mixins/model/model.stub
  97. 5 0
      app/admin/library/crud/stubs/mixins/model/setters/arrayToString.stub
  98. 5 0
      app/admin/library/crud/stubs/mixins/model/setters/time.stub
  99. 31 0
      app/admin/library/crud/stubs/mixins/validate/validate.stub
  100. 249 0
      app/admin/library/traits/Backend.php

+ 4 - 0
.gitattributes

@@ -0,0 +1,4 @@
+* text=auto eol=lf
+
+# Windows
+*.bat text eol=crlf

+ 62 - 97
.gitignore

@@ -1,99 +1,64 @@
-# ---> JetBrains
-# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
-
-*.iml
-
-## Directory-based project format:
-.idea/
-# if you remove the above rule, at least ignore the following:
-
-# User-specific stuff:
-# .idea/workspace.xml
-# .idea/tasks.xml
-# .idea/dictionaries
-
-# Sensitive or high-churn files:
-# .idea/dataSources.ids
-# .idea/dataSources.xml
-# .idea/sqlDataSources.xml
-# .idea/dynamic.xml
-# .idea/uiDesigner.xml
-
-# Gradle:
-# .idea/gradle.xml
-# .idea/libraries
-
-# Mongo Explorer plugin:
-# .idea/mongoSettings.xml
-
-## File-based project format:
-*.ipr
-*.iws
-
-## Plugin-specific files:
-
-# IntelliJ
-/out/
-
-# mpeltonen/sbt-idea plugin
-.idea_modules/
-
-# JIRA plugin
-atlassian-ide-plugin.xml
-
-# Crashlytics plugin (for Android Studio and IntelliJ)
-com_crashlytics_export_strings.xml
-crashlytics.properties
-crashlytics-build.properties
-
-# ---> VisualStudioCode
-.settings
-
-
-# ---> macOS
-.DS_Store
-.AppleDouble
-.LSOverride
-
-# Icon must end with two \r
-Icon
-
-
-# Thumbnails
-._*
-
-# Files that might appear in the root of a volume
-.DocumentRevisions-V100
-.fseventsd
-.Spotlight-V100
-.TemporaryItems
-.Trashes
-.VolumeIcon.icns
-
-# Directories potentially created on remote AFP share
-.AppleDB
-.AppleDesktop
-Network Trash Folder
-Temporary Items
-.apdisk
-
-# ---> Windows
-# Windows image file caches
-Thumbs.db
-ehthumbs.db
-
-# Folder config file
+# 通过 Git 部署项目至线上时建议删除的忽略规则
+/vendor
+/modules
+/public/*.lock
+/public/index.html
+/public/assets
+
+# 通过 Git 部署项目至线上时可以考虑删除的忽略规则
+/public/storage/*
+composer.lock
+pnpm-lock.yaml
+package-lock.json
+yarn.lock
+
+# common
+/nbproject
+/runtime/*
+/install
+node_modules
+dist
+dist-ssr
+.DS_Store
+/.env
 Desktop.ini
 Desktop.ini
 
 
-# Recycle Bin used on file shares
-$RECYCLE.BIN/
-
-# Windows Installer files
-*.cab
-*.msi
-*.msm
-*.msp
-
-# Windows shortcuts
-*.lnk
-
+# 部署到线上时,如果您已经删除了 /modules 的忽略规则,那么以下规则可以帮您排除多余的模块文件(已知的多余/重复文件)
+# 提示:在不修改以下规则的前提下,您可以在无需排除的目录中建立一个名为 .gitkeep 的文件,作为列外(逃生舱)
+# 请勿在已经忽略以下文件[夹]的环境下卸载模块,该操作将导致未知异常!
+/modules/ebak
+/modules/*/app
+/modules/*/config
+/modules/*/extend
+/modules/*/public
+/modules/*/vendor
+/modules/*/web
+/modules/*/web-nuxt
+/modules/*/install.sql
+/modules/*/config.json
+/modules/*/LICENSE
+/modules/*/README.md
+/modules/*/uniapp.zip
+
+# Log files
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+!/web/.vscode
+
+# Other
+*.css.map
+*.local
+!.gitkeep
+.svn

+ 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

+ 350 - 0
CHANGELOG.md

@@ -0,0 +1,350 @@
+### [BuildAdmin 更新日志](https://gitee.com/wonderful-code/buildadmin)
+
+🔥🔥基于 Vue3.x setup + ThinkPHP6 + TypeScript + Vite + Pinia + Element Plus等流行技术栈的后台管理系统,自适应多端、支持CRUD代码生成、自带WEB终端、同时提供Web和Server端、内置全局数据回收站和字段级数据修改保护、自动注册路由、无限子级权限管理等,无需授权即可免费商用,希望能帮助大家实现快速开发。
+
+## v1.1.4-Release
+### 新增
+- 模块安装增加依赖模块检测
+- 新的依赖管理类
+- 通过模块市场为`WebNuxt工程`安装模块的实现
+
+## v1.1.3-Release
+### 新增
+- `WebNuxt`工程发布,可通过模块市场安装,亦可直接访问[代码仓库](https://gitee.com/wonderful-code/build-admin-nuxt)
+- 增加可选的管理员和会员单点登录功能
+- 增加直接登录会员账号的方法
+- 新增双栏布局效果,顶部栏加左侧栏同时存在
+- 确保无需登录的接口不会抛出token过期的异常
+- 增加表格普通侧边按钮类型
+- 增加根据当前路由路径快捷获取语言翻译的函数
+- 后台模块管理增加我的模块按钮
+
+### 修复/重构
+- 远程下拉增加信息提示框
+- 文件上传失败则不在上传列表显示
+- 调整composer依赖
+- 可视化CRUD生成的语言包代码按需加载实现
+- 优化数据行拖拽排序的逻辑
+- 优化数据行侧边按钮的类型定义
+- 模块封面图片开启懒加载
+- 修改管理员日志的data字段类型为longtext
+- 修复添加窗口中存在富文本字段时可能无法关闭的问题
+- 修复管理员无权限时跳回首页或被注销的问题
+- 修复表格行侧边 confirmButton 按钮 disabled 无效的问题
+- 修复从历史记录开始时,远程下拉参数无法选择的问题
+- 修复菜单规则只添加为菜单时无法打开的问题
+- 修复从数据表开始时字段分析可能出错的问题
+- 修复行侧边按钮 disabledTip 属性无效的问题
+- 修复前台iframe菜单无法打开的问题
+- 修复远程下拉监听值为`null、undefined`时报错的问题
+- 修复后台因为管理员模型登录时间获取器导致登录判断报错问题
+
+## v1.1.2-Release
+- 此版本有一些不兼容更新,建议在更新前参考:[v1.1.2不兼容更新](https://wonderful-code.gitee.io/guide/other/incompatibleUpdate/v112.html)
+- 页面组件与页面语言包全部**按需加载**,大幅减少首屏加载大小
+- 更新系统前端的所有可更新依赖到最新稳定版本
+- 可视化CRUD增加字段名称检查
+- 禁止管理员自己删除自己
+- `isAdminApp`方法支持传递`path`进行判断
+- `mixins`代码移入到新建的组件内统一管理
+- 修复可视化CRUD生成的代码中`-1`没有加引号的问题
+- 修复后台单栏布局只有一个菜单时菜单不显示的问题
+- 修复模块发布新版本不能减少旧版本模块文件的问题
+- 修复模块更新脚本因未加载而不能执行的问题
+
+## v1.1.1-Release
+### 新增
+- 增加前台会员中心埋点(配合模块为会员中心增加功能)
+- 编程式添加会员菜单规则支持
+
+### 优化
+- 默认关闭监听SQL
+- 服务端返回302时自动删除前端的用户token
+- 系统配置保存时只效验和提交当前页的表单数据
+- 优化用户信息显示
+- 优化`getTableFieldList`接口
+- 统一接口响应数据`key`的命名规范
+- 默认不再允许上传pdf格式的文件
+- `Token::check`方法增加过期不抛出异常时的逻辑
+
+### 修复/重构
+- 修复模块下载安装时解压目录名可能错误的问题
+- 文件后缀名大写时无法上传的问题
+- 修复关联表名带下划线生成的代码出错
+- 修复上传组件一处类型检查错误
+- 会员中心的用户名默认不再禁止修改
+- 会员修改绑定信息时账户验证通过的token在使用后立即删除
+- 自定义排序字段,模型onAfterInsert方法生成错误
+- 修复生成三级以上的菜单规则时,无法为非超管分配权限的问题
+- 修复可视化CRUD删除字段时可能出现报错的问题
+- 去除多余的会员菜单规则
+- 模块市场中与官网相关的URL修改
+- 修复预览图片宽高较大时超出对话框的问题
+- 修复公共搜索只有一个输入框时会触发表单的默认行为的问题
+- 其他细节
+
+## v1.1.0-Release
+### 新增
+- **可视化CRUD新增多种快捷组件,并修复已知问题**
+- 模块可以在启用和禁用脚本内备份配置数据和运行文件
+- 模块支持向main.ts和App.vue添加代码
+- 新增会员修改绑定信息(手机号、邮箱)支持
+- 文件图片上传增加隐藏附件选择器的选项
+- 远程下拉组件增加 label 格式化函数的属性
+- 增加颜色选择器(baInput)
+- 完善上传组件的onChange等事件
+
+### 优化
+- 优化后台登录页面自适应效果
+- 优化首页和会员中心菜单样式
+- 优化终端警告信息显示效果
+- 优化账户名验证错误时的提示消息
+- 详情弹窗可以点击弹窗外部进行关闭
+- 禁止管理员向自己的角色组添加其他管理员
+- 其他细节...
+
+### 修复/重构
+- 修复后台编辑弹窗缩放后显示异常的问题
+- 修复在第一个tab右击菜单中关闭全部tab时报错的问题 #10
+- 修复远程下拉可能出现已聚焦却无选项的问题
+- 修复添加管理员和会员时可能出现表单验证信息的问题
+- 修复模块管理中会员登录态过期后不自动注销的问题
+- 修复系统配置中的数字输入框编辑可能无法保存的问题
+- 修复系统配置中的上传组件从附件选择器中选择附件保存无效的问题
+- 增加vue-qr依赖
+- 增加忽略Desktop.ini
+
+## v1.0.9-Release
+- **新增可视化CRUD**
+- 去除原命令行CRUD代码生成功能(已打包为模块,按需下载)
+- 添加表单颜色选择器和表格内的颜色渲染方式
+- 侧边按钮增加 disabled 判定方法和按钮额外属性
+- 增加获取数据表字段的辅助函数
+- 增加获取一个目录所有文件的辅助函数
+- 后台手机端自适应优化
+- 公共搜索输入框可一键清空
+- 远程下拉默认值优化
+- 优化版本类/扩展类
+- 优化树状表格
+- `DELETE`请求的body改为query以兼容域名CNAME解析
+- 在main.ts导入display.css而不是分散导入
+- 修复url带参跳转时表格可能报错的问题
+- 修复只添加为路由的菜单规则不能刷新的问题
+- 修复验权时可能出现错误的问题
+- 修复Linux下删除空文件夹可能失败的问题
+- 修复自建模块处于未安装状态时显示异常的问题
+- 会员切换登录注册时重置表单项 !70
+- 会员切换到注册表单时清理用户名
+- 管理员分组的上级分组禁止为自身
+- 模块管理用户信息弹窗数据更新
+- 本地模块更新日志显示异常的问题
+
+## v1.0.8-Release
+- **ThinkPHP发布6.1.0版本安全更新**,修正了序列化漏洞问题和优化多语言判断机制。
+- 去除`lodash`依赖改用`lodash-es`(后者同时为`Element plus`的依赖,与框架更契合,包体积更小)
+- 修复跨域代理示例的规则错误的问题
+- 合并打包css文件、增加分包配置示例
+- 完善工具函数注释、优化相关代码
+- 模块详情展示效果优化
+
+PS: 框架对`TP`的版本限定为`^6.0.0`,针对tp本次安全更新,git包的开发者可以直接`composer update`,若没更新到`v6.1.0`请更换`composer`源,`BuildAdmin`发新版本主要是为了更新完整包和资源包。
+
+## v1.0.7-Release
+- 富文本编辑器通过模块市场按需安装(框架不再内置),以方便选择不同的编辑器
+- **增加附件资源库**
+- 前台用户登录状态检测优化
+- 事件监听优化
+- 附件管理优化
+- 单元格图片预览弹窗可以通过点击遮罩层关闭
+- 自定义表格页码相关优化
+- 搜索事件Data的类型定义优化
+- 修复特殊类型文件上传时可能被限制的问题
+- 优化敏感数据修改监听的逻辑
+- 修复 typescript-eslint 依赖可能安装失败的问题
+- 优化表单密码验证规则
+
+## v1.0.6-Release
+- Table组件增加多个插槽位,提供`el-table-column`支持
+- 增加WEB端文件上传扩展文件
+- 增加文件上传前的类型与大小检查
+- 增加文件单位转字节的函数
+- 增加系统配置管理类
+- 新增以编程的方式删除依赖的功能
+- 新增模块安装时对互斥模块的检测
+- 增加多个系统预置事件定义
+- 增加发送邮件接口
+- 增加发送短信接口
+- 增加手机验证账户验证方式
+- 增加responseType json 以外类型的处理逻辑
+- 增加编程式添加系统配置中的快捷配置入口的方法
+- 增加清理浏览器缓存的快捷按钮
+- 升级element-plus版本到2.2.17
+- 优化表单验证
+- 优化表格的单元格渲染
+- 优化多处类型定义
+- 优化后端数据库字段读取函数
+- 优化数据管理中数据表和控制器列表的加载
+- 优化控制台页面暗黑模式下的文字颜色
+- 优化模块安装时对互斥模块的检测
+- 优化上传组件
+- WEB端语言包文件无限层级读取
+- 表格顶部菜单按钮图标在暗黑模式时的样式优化
+- 禁用模块时可以选择保留一些由模块添加的依赖项
+- 模块状态不为已安装时不定义AppInit事件
+- 资源完整路径处理时加入上传文件cdnurl的判断
+- Table组件不再使用事件巴士监听相关事件
+- 删除文件不存在的附件记录前额外检查是否是本地存储
+- 附件管理删除记录时同时删除文件,并提供友好提示信息
+- 去除Table组件的action事件
+- 去除TableHeader组件的action事件
+- 输入组件帮助信息显示效果优化
+- 修复对表格第三次排序时(取消排序时)失效的问题
+- 修复部分后台功能缓存设置不生效的问题
+- 修复多选远程下拉选择一次面板就收缩和无右侧箭头的问题
+- 修复菜单规则管理中图标选择器在窗口关闭后残留的问题
+- 修复图标选择器选取图标后无法再次显示的问题
+- 模块安装器去除等待热更新步骤
+- 修复预设表格页码或单页加载数量无效的问题
+- 修复主动添加的系统配置不能删除的问题、格式化代码
+- 修复模块依赖冲突检测可能异常的问题
+- 修复安装云存储模块后,本地上传模块时被上传到云存储的问题
+- 修复用户修改头像时顶栏和侧栏的头像图片可能404的问题
+- 修复模块依赖冲突时,模块的启用脚本不执行的问题
+- 修复模块安装完成后异常的显示了`模块已安装`的错误弹窗
+- 管理员管理和会员管理接口中的敏感信息剔除
+- 移除多余的IE相关判断
+- 其他优化...
+
+## v1.0.5-Release Preview
+- 新增**模块市场**,一键安装某个功能、单页或是纯前端技术栈的学习案例项目等等,随时随地为系统添砖加瓦,系统能够自动维护`package.json`和`composer.json`并通过内置终端自动完成模块所需依赖的安装。
+- 新增前后台**暗黑模式**支持
+- 安装器不再要求数据表前缀必填、安装验证逻辑优化
+- 终端原`popen`实现改为`proc_open`
+- 重新实现图片文件上传组件
+- 单元格渲染为 tags 时支持effect、size等属性
+- url的点击事件增加当前行数据的参数
+- 为管理员管理功能开启数据限制
+- 后台Iframe相关多个细节完善
+- 生成代码文件中的缩进改为空格而不是tab
+- 访问后端接口时,不再必须通过index.php入口文件
+- 放行所有options请求
+- 修复顶部菜单columnDisplay和comSearch同时不存在时,仍然会残留一个div边框的问题
+- 修复菜单规则管理中无法直接开关规则的问题
+- 修复单选远程下拉清理输入框值后无法再读取全部远程数据的问题
+- 修复axios封装在showCodeMessage=false时请求无后续处理的问题
+- 修复表字段名称为length时CRUD生成语言包报错
+- 修复删除菜单规则时未同时删除子级菜单的问题
+- 修复角色组的资料可被越权修改的问题
+- 修复触发到API请求节流时报错为跨域的问题
+- 修复表格顶部下拉菜单复选框和按钮组占位
+- 修复已上传文件丢失后,无法再次上传的问题
+- 修复有默认值的情况多文件同时上传时文件列表错乱的问题
+- 修复隐藏菜单情况刷新页面再展开菜单会导致顶部tab异常的问题
+- 修复后台菜单折叠状态刷新后丢失的问题
+- 修复管理员昵称过长时首次登录昵称被换行的问题
+- 修复登录页面管理员头像位置自适应异常的问题
+- 其他细节...
+
+## v1.0.3-Release
+- 完善英文语言包
+- 公共搜索增加远程下拉组件支持
+- 增加数据权限控制支持:不同管理员只可以查看有权数据行 的权限控制功能
+- 自动识别表主键并添加到生成的模型属性
+- 后台终端按钮只为超级管理员显示
+- 关联表指定远程select下拉字段
+- 增加表格快速搜索字段是否存在的检测
+- 增加以type为后缀的enum等类型字段可被生成为单选框
+- 站点系统配置缓存支持
+- 增加会员中心开关
+- 会员注册时通过API获取可用的验证方式、会员注册验证邮件实现
+- 完善会员规则管理
+- 表格公共搜索->对开关组件状态的搜索优化
+- 公共搜索显示状态可通过baTable实例控制
+- 验证码类支持到php8.1
+- 去除file_list后缀的字段生成为多文件上传组件(与下拉组件后缀存在冲突)
+- 优化角色组权限分配
+- 优化默认管理员分组拥有的权限节点
+- 数据回收和敏感数据规则中,不再使用带前缀的表全名
+- 安装器`npm install`失败自动重试一次
+- 安装器增加检测当前端口是否是8000
+- 安装器完成页面增加重新安装按钮 (只清理缓存,不会删除install.lock)
+- 修复敏感数据规则管理中删除敏感字段时的显示异常问题
+- 修复表格时间字段未提供值时显示为当前时间的问题
+- 修复管理员个人资料表单中的签名无法被重置的问题
+- 修复后端默认应用不存在的问题
+- 修复字段类型为char(1)时,生成的单选框无字典数据
+- 修复数据表主键不为ID时编辑表单无法保存、表格无法排序等问题
+- 修复顶栏标签全屏时,取消全屏的按钮会遮挡表格顶部操作按钮的问题
+- 修复前后台路由规则名称重复时可能导致错误跳转问题
+- 修复手机号验证正则无法识别部分已知号码的问题
+- 修复系统配置中的禁止访问IP和时区配置项无效的问题
+- 修复系统配置中富文本编辑器层级过高和无法编辑的问题
+- 修复系统配置中时间和城市类型的输入组件无法正常录入值的问题
+- 修复数据表没有注释时不生成菜单规则的问题
+- 修复表格右侧无buttons,且要初始化排序时会报错的问题
+- 修复单元格渲染为tag时值为0等无法显示的问题
+- 修复images字段名称后缀不能生成为图片上传组件的问题
+- 修复管理员日志权限控制不完善的问题
+- 修复管理员可通过后台使自己部分权限丢失的问题
+- 修复管理员分组被禁用后还可以被远程select选择的问题
+- 修复删除管理员时没有同时删除管理员的分组数据的问题
+- 修复远程下拉搜索结果无法选中的问题、同时优化下拉选项面板显示逻辑
+- 修复菜单规则和会员分组被禁用后在远程select中依然可以选择的问题
+- 修复重复安装系统时.env-example被多次写入数据库资料的问题
+- 修复数据安全监听中表不存在时的日志记录异常
+- 其他细节优化
+
+## v1.0.2-Release
+- **增加前台会员中心**
+- 安装器增加NPM源自动设置选项
+- CRUD:增加tinyint(1)类型的字段在符合条件下自动生成为单选框
+- baInput:单选/复选框/下拉框默认值传递数字支持
+- baInput:优化年份选择器
+- baInput:文件上传组件增加预览响应
+- web端布局(layouts)内的目录结构调整
+- 增加跨域代理配置示例,提供给有需要的小伙伴(感谢@ttdms)
+- 增加邮件发送类、增加phpMailer依赖、系统邮件配置增加测试邮件发送功能
+- 后台右侧菜单增加清理缓存按钮
+- 会员余额以分为单位保存到数据库,并在模型层做转换处理
+- 附件管理增加上传会员字段
+- 优化富文本编辑器滚动条样式、通用弹窗表单增加圆角
+- 更新wangeditor依赖版本到5.1.1
+- 增加会员资料的状态商店、优化后台登录状态判断逻辑
+- 表格开关类型字段的公共搜索使用下拉框渲染
+- 重构了站点首页
+- 更新font-awesome的资源地址到国内CDN
+- 去除build:online命令,使用build代替
+- 修复关闭管理员登录验证码后,登录任然报错验证码不存在的问题
+- 修复富文本编辑器上传文件时提示未配置上传URL的问题
+- 修复表格中的tag和url在无值时任然显示组件的问题
+- 修复侧边菜单栏的非激活菜单项的图标颜色不符合直觉的问题
+- 修复CRUD生成的代码在添加数据时权重字段无效的问题
+- 修复部分日志记录没有标题的问题
+- 修复已在后台或会员中心再跳转到模块首页时会卡在loading页面的问题
+- 修复系统配置编辑时提示变量名不能为空的问题
+- 修复后台表格右侧字段下拉没有高度限定的问题、修复一处样式缺失
+- 修复管理员注销时偶尔需要权限的问题
+- 修复默认的数据回收规则配置不完整的问题
+- 修复表格顶部的批量操作按钮在未选择数据时依然可点击的问题
+- 修复表格内tag在公共搜索中被渲染为下拉框的问题
+- 修复管理员登录页面编译后可能存在的username未定义报错
+
+## v1.0.1-Release
+- 增加终端配置功能
+- 终端增加是否运行于安装服务下的检测
+- FormItem增加额外的块级输入提示选项
+- 优化管理分组权限节点选择时的样式
+- 语言包整理
+- 额外暴露i18n实例,实现在非setup中使用语言翻译
+- 新增站点配置状态store
+- 修复bug、完善README
+
+## v1.0.0-beta
+**公共测试版本**
+- 内置WEB终端
+- 一键CRUD
+- Pinia
+- 可视化配置+动态加载路由
+- 细粒度权限控制
+- 数据修改保护、数据全局回收
+- ...

+ 201 - 0
LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 妙码生花
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 132 - 2
README.md

@@ -1,3 +1,133 @@
-# zy.uas.admin
+<br />
+<div align="center">
+    <img src="https://wonderful-code.gitee.io/images/readme/logo-title.png" alt="" />
+    <br />
+    <a href="https://buildadmin.com/" target="_blank">官网</a> | <a href="https://demo.buildadmin.com/" target="_blank">演示站</a> |
+    <a href="https://wonderful-code.gitee.io/" target="_blank">文档</a> |
+    <a href="https://jq.qq.com/?_wv=1027&k=c8a7iSk8" target="_blank">加群</a> |
+    <a href="https://wonderful-code.gitee.io/guide/" target="_blank">视频介绍</a> |
+    <a href="https://gitee.com/wonderful-code/buildadmin" target="_blank">Gitee仓库</a> |
+    <a href="https://github.com/build-admin/BuildAdmin" target="_blank">GitHub仓库</a>
+</div>
+<br />
+<p align="center">
+    <a href="https://www.thinkphp.cn/" target="_blank">
+        <img src="https://img.shields.io/badge/ThinkPHP-%3E6.0-brightgreen?color=91aac3&labelColor=439EFD" alt="vue">
+    </a>
+    <a href="https://v3.vuejs.org/" target="_blank">
+        <img src="https://img.shields.io/badge/Vue-%3E3.x-brightgreen?color=91aac3&labelColor=439EFD" alt="vue">
+    </a>
+    <a href="https://element-plus.gitee.io/#/zh-CN/component/changelog" target="_blank">
+        <img src="https://img.shields.io/badge/Element--Plus-%3E2.2-brightgreen?color=91aac3&labelColor=439EFD" alt="element plus">
+    </a>
+    <a href="https://www.tslang.cn/" target="_blank">
+        <img src="https://img.shields.io/badge/TypeScript-%3E4.9-blue?color=91aac3&labelColor=439EFD" alt="typescript">
+    </a>
+    <a href="https://vitejs.dev/" target="_blank">
+        <img src="https://img.shields.io/badge/Vite-%3E4.0-blue?color=91aac3&labelColor=439EFD" alt="vite">
+    </a>
+    <a href="https://pinia.vuejs.org/" target="_blank">
+        <img src="https://img.shields.io/badge/Pinia-%3E2.0-blue?color=91aac3&labelColor=439EFD" alt="vite">
+    </a>
+    <a href="https://gitee.com/wonderful-code/buildadmin/blob/master/LICENSE" target="_blank">
+        <img src="https://img.shields.io/badge/Apache2.0-license-blue?color=91aac3&labelColor=439EFD" alt="license">
+    </a>
+</p>
 
 
-管理系统
+<br>
+<div align="center">
+  <img src="https://wonderful-code.gitee.io/images/readme/index.gif" />
+</div>
+<br>
+
+### 介绍
+🌈 基于 Vue3.x setup + ThinkPHP6 + TypeScript + Vite + Pinia + Element Plus等流行技术栈的后台管理系统,自适应多端、可视化CRUD代码生成、自带WEB终端、同时提供Web和Server端、内置全局数据回收站和字段级数据修改保护、自动注册路由、无限子级权限管理等,无需授权即可免费商用,希望能帮助大家实现快速开发。
+
+### 主要特性
+**🚀 CRUD代码生成:**
+图形化拖拽生成后台增删改查代码,自动创建数据表;大气且实用的表格,多达22种表单组件支持,行拖拽排序,受权限控制的编辑和删除等等,并支持关联表,可为您节省大量开发时间。
+
+**💥 内置WEB终端:**
+我们内置了一个WEB终端以实现一些理想中的功能,比如:虽然是基于vue3的系统,但你在安装本系统时,并不需要手动执行`npm install`和`npm build`命令。且后续本终端将为您提供更多方便、快捷的服务。
+
+**👍 流行且稳定的技术栈:**
+除了基于`ThinkPHP6`前后端分离架构外,我们的`Vue3`使用了`Setup`、状态管理使用`Pinia`、并使用了`TypeScript`、`Vite`等可以为你的知识面添砖加瓦的技术栈。
+
+**🎨 模块市场:**
+一键安装数据导出、短信发送、云存储、单页或是纯前端技术栈的学习案例项目等等,随时随地为系统添砖加瓦,系统能够自动维护`package.json`和`composer.json`并通过内置终端自动完成模块所需依赖的安装,若您愿意成为模块开发者,模块可以:覆盖系统任何文件或为系统新增文件,您的模块经由官方审核即可上架。
+
+**🔀 前后端分离:**
+`web`文件夹内包含:`干净`(不含后端代码)、`完整`(所有前端代码文件均在此内) 的前端代码文件,对前端开发者友好,作为纯前端开发者,您可以将BAdmin当做学习与资源的社群,本系统可为您准备好案例和模板等所需要的环境,而您只需专注于学习或工作,不需要会任何后端代码!(邀您:[和我们一起](https://jq.qq.com/?_wv=1027&k=c8a7iSk8) )
+
+**🌴 数据回收与反悔:**
+内置全局数据回收站,并且提供字段级数据修改记录和修改对比,随时回滚和还原,安全且无感。
+
+**✨ 高颜值:**
+提供三种布局模式,其中默认布局使用无边框设计风格,它并没有强行填满屏幕的每一个缝然后使用边框线进行分隔,所有的功能版块,都像是悬浮在屏幕上的,同时又将屏幕空间及其合理的利用了。
+
+**🔐 权限验证:**
+可视化的管理权限,然后根据权限动态的注册路由、菜单、页面、按钮(权限节点)、支持无限父子级权限分组、前后端搭配鉴权,自由分派页面和按钮权限。
+
+**📝 未来可期:**
+我们正在持续维护系统,并着手开发更多基础设施模块,按需一键安装,甚至提供开箱即用的各行业完整应用。
+
+**🧱 一举多得:**
+后台自适应PC、平板、手机等多种场景的支持,轻松应对各种需求。
+
+**💖 其他杂项:**
+角色组/管理员/管理员日志、 会员/会员组/会员余额、积分日志、系统配置/控制台/附件管理/个人资料管理等等、更多特性等你探索...
+
+### 安装使用
+💫 我们提供了完善的文档,对于熟悉`ThinkPHP`和`Vue`的用户,请使用大佬版:[快速上手](https://wonderful-code.gitee.io/guide/install/start.html) ,对于新人朋友,我们额外准备了各个操作系统的从零开始套餐:[Windows从零到一](https://wonderful-code.gitee.io/guide/install/windows.html) | [Linux从零到一](https://wonderful-code.gitee.io/guide/install/linux-bt.html) | [MacBook安装引导](https://wonderful-code.gitee.io/guide/install/macBook.html)
+
+### 联系我们
+- [演示站](https://demo.buildadmin.com/) 账户:`admin`,密码:`123456`(演示站数据无法修改,请下载源码安装体验全部功能)
+- [文档:wonderful-code.gitee.io](https://wonderful-code.gitee.io/)
+- 加群:[687903819(已满)](https://jq.qq.com/?_wv=1027&k=QwtXa14c)、[751852082](https://jq.qq.com/?_wv=1027&k=c8a7iSk8)
+- [Gitee仓库](https://gitee.com/wonderful-code/buildadmin)、[GitHub仓库](https://github.com/build-admin/BuildAdmin)
+- [备用文档:doc.buildadmin.com](https://doc.buildadmin.com/)
+- [官网](https://buildadmin.com/)、[官方邮箱 hi@buildadmin.com](mailto:hi@buildadmin.com)
+
+### 项目预览
+|  |  |
+|---------------------|---------------------|
+|![登录](https://wonderful-code.gitee.io/images/readme/login.gif)|![控制台](https://wonderful-code.gitee.io/images/readme/dashboard.png)|
+|![布局配置](https://wonderful-code.gitee.io/images/readme/layout.png)|![表格](https://wonderful-code.gitee.io/images/readme/admin.png)|
+|![表单](https://wonderful-code.gitee.io/images/readme/user.png)|![系统配置](https://wonderful-code.gitee.io/images/readme/config.png)|
+|![数据回收规则](https://wonderful-code.gitee.io/images/readme/data-recycle.png)|![数据回收日志](https://wonderful-code.gitee.io/images/readme/data-recycle-log.png)|
+|![敏感数据](https://wonderful-code.gitee.io/images/readme/sensitive-data.png)|![菜单](https://wonderful-code.gitee.io/images/readme/menu.png)|
+|![单栏布局](https://wonderful-code.gitee.io/images/readme/layout-3.png)|![经典布局](https://wonderful-code.gitee.io/images/readme/layout-2.png)|
+
+### 特别鸣谢
+💕 感谢巨人提供肩膀,排名不分先后
+- [Thinkphp](http://www.thinkphp.cn/)
+- [FastAdmin](https://gitee.com/karson/fastadmin)
+- [Vue](https://github.com/vuejs/core)
+- [vue-next-admin](https://gitee.com/lyt-top/vue-next-admin)
+- [Element Plus](https://github.com/element-plus/element-plus)
+- [TypeScript](https://github.com/microsoft/TypeScript)
+- [vue-router](https://github.com/vuejs/vue-router-next)
+- [vite](https://github.com/vitejs/vite)
+- [Pinia](https://github.com/vuejs/pinia)
+- [Axios](https://github.com/axios/axios)
+- [nprogress](https://github.com/rstacruz/nprogress)
+- [screenfull](https://github.com/sindresorhus/screenfull.js)
+- [mitt](https://github.com/developit/mitt)
+- [sass](https://github.com/sass/sass)
+- [wangEditor](https://github.com/wangeditor-team/wangEditor)
+- [echarts](https://github.com/apache/echarts)
+- [vueuse](https://github.com/vueuse/vueuse)
+- [lodash](https://github.com/lodash/lodash)
+- [eslint](https://github.com/eslint/eslint)
+- [prettier](https://github.com/prettier/prettier)
+- [vuepress](https://github.com/vuejs/vuepress)
+- [countUp](https://github.com/inorganik/countUp.js)
+- [Sortable](https://github.com/SortableJS/Sortable)
+- [v-code-diff](https://github.com/Shimada666/v-code-diff)
+
+### 版权信息
+🔐 BuildAdmin 遵循`Apache2.0`开源协议发布,提供无需授权的免费使用。\
+本项目包含的第三方源码和二进制文件之版权信息另行标注。
+
+### 支持项目
+💕 无需捐赠,如果觉得项目不错,或者已经在使用了,希望你可以去 [Github](https://github.com/build-admin/BuildAdmin) 或者 [Gitee](https://gitee.com/wonderful-code/buildadmin) 帮我们点个 ⭐ Star,这将是对我们极大的鼓励与支持。

+ 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()
+    {
+        // 服务启动
+    }
+}

+ 96 - 0
app/BaseController.php

@@ -0,0 +1,96 @@
+<?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 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->request->controllerPath = str_replace('.', '/', $this->request->controller(true));
+
+        // 控制器初始化
+        $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, $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();
+        }
+
+        return $v->failException()->check($data);
+    }
+
+}

+ 59 - 0
app/ExceptionHandle.php

@@ -0,0 +1,59 @@
+<?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);
+    }
+}

+ 9 - 0
app/Request.php

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

+ 903 - 0
app/admin/buildadmin.sql

@@ -0,0 +1,903 @@
+/*
+ BuildAdmin Install SQL
+ Date: 2022-05-13
+*/
+
+SET
+    FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for `__PREFIX__admin`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__admin`;
+CREATE TABLE `__PREFIX__admin`
+(
+    `id`            int(10) unsigned                          NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `username`      varchar(20) COLLATE utf8mb4_unicode_ci             DEFAULT '' COMMENT '用户名',
+    `nickname`      varchar(50) COLLATE utf8mb4_unicode_ci             DEFAULT '' COMMENT '昵称',
+    `avatar`        varchar(255) COLLATE utf8mb4_unicode_ci            DEFAULT '' COMMENT '头像',
+    `email`         varchar(100) COLLATE utf8mb4_unicode_ci            DEFAULT '' COMMENT '邮箱',
+    `mobile`        varchar(11) COLLATE utf8mb4_unicode_ci             DEFAULT '' COMMENT '手机',
+    `loginfailure`  tinyint(1) unsigned                       NOT NULL DEFAULT '0' COMMENT '登录失败次数',
+    `lastlogintime` int(10)                                            DEFAULT NULL COMMENT '登录时间',
+    `lastloginip`   varchar(50) COLLATE utf8mb4_unicode_ci             DEFAULT NULL COMMENT '登录IP',
+    `password`      varchar(32) COLLATE utf8mb4_unicode_ci             DEFAULT '' COMMENT '密码',
+    `salt`          varchar(30) COLLATE utf8mb4_unicode_ci             DEFAULT '' COMMENT '密码盐',
+    `motto`         varchar(255) COLLATE utf8mb4_unicode_ci   NOT NULL DEFAULT '' COMMENT '签名',
+    `createtime`    int(10)                                            DEFAULT NULL COMMENT '创建时间',
+    `updatetime`    int(10)                                            DEFAULT NULL COMMENT '更新时间',
+    `status`        enum ('1','0') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '1' COMMENT '状态:0=禁用,1=启用',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `username` (`username`) USING BTREE
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='管理员表';
+
+-- ----------------------------
+-- Records of __PREFIX__admin
+-- ----------------------------
+BEGIN;
+INSERT INTO `__PREFIX__admin`
+VALUES ('1', 'admin', 'Admin', '', 'admin@buildadmin.com', '18888888888', '0', '1652166427', '127.0.0.1',
+        'dc82034ba4108148cbefd980b6b63371', 'kWlGDm9qAVB8MjbX', '', '1645876529', '1652166427', '1');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for `__PREFIX__admin_group`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__admin_group`;
+CREATE TABLE `__PREFIX__admin_group`
+(
+    `id`         int(10) unsigned                          NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `pid`        int(10) unsigned                          NOT NULL DEFAULT '0' COMMENT '上级分组',
+    `name`       varchar(100) COLLATE utf8mb4_unicode_ci            DEFAULT '' COMMENT '组名',
+    `rules`      text COLLATE utf8mb4_unicode_ci           NOT NULL COMMENT '权限规则ID',
+    `createtime` int(10)                                            DEFAULT NULL COMMENT '创建时间',
+    `updatetime` int(10)                                            DEFAULT NULL COMMENT '更新时间',
+    `status`     enum ('1','0') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '1' COMMENT '状态:0=禁用,1=启用',
+    PRIMARY KEY (`id`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='管理分组表';
+
+-- ----------------------------
+-- Records of __PREFIX__admin_group
+-- ----------------------------
+BEGIN;
+INSERT INTO `__PREFIX__admin_group`
+VALUES ('1', '0', '超级管理组', '*', '1645876529', '1647805864', '1');
+INSERT INTO `__PREFIX__admin_group`
+VALUES ('2', '1', '一级管理员',
+        '1,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,77,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76',
+        '1645876529', '1658197123', '1');
+INSERT INTO `__PREFIX__admin_group`
+VALUES ('3', '2', '二级管理员', '21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43', '1645876529',
+        '1658197143', '1');
+INSERT INTO `__PREFIX__admin_group`
+VALUES ('4', '3', '三级管理员', '55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75', '1645876529',
+        '1658197162', '1');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for `__PREFIX__admin_group_access`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__admin_group_access`;
+CREATE TABLE `__PREFIX__admin_group_access`
+(
+    `uid`      int(10) unsigned NOT NULL COMMENT '管理员ID',
+    `group_id` int(10) unsigned NOT NULL COMMENT '分组ID',
+    UNIQUE KEY `uid_group_id` (`uid`, `group_id`),
+    KEY `uid` (`uid`),
+    KEY `group_id` (`group_id`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='管理权限分组表';
+
+-- ----------------------------
+-- Records of __PREFIX__admin_group_access
+-- ----------------------------
+BEGIN;
+INSERT INTO `__PREFIX__admin_group_access`
+VALUES ('1', '1');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for `__PREFIX__admin_log`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__admin_log`;
+CREATE TABLE `__PREFIX__admin_log`
+(
+    `id`         int(10) unsigned                    NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `admin_id`   int(10) unsigned                    NOT NULL DEFAULT '0' COMMENT '管理员ID',
+    `username`   varchar(30) COLLATE utf8mb4_unicode_ci       DEFAULT '' COMMENT '管理员用户名',
+    `url`        varchar(1500) COLLATE utf8mb4_unicode_ci     DEFAULT '' COMMENT '操作Url',
+    `title`      varchar(100) COLLATE utf8mb4_unicode_ci      DEFAULT '' COMMENT '日志标题',
+    `data`       longtext COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '请求数据',
+    `ip`         varchar(50) COLLATE utf8mb4_unicode_ci       DEFAULT '' COMMENT 'IP',
+    `useragent`  varchar(255) COLLATE utf8mb4_unicode_ci      DEFAULT '' COMMENT 'User-Agent',
+    `createtime` int(10)                                      DEFAULT NULL COMMENT '操作时间',
+    PRIMARY KEY (`id`),
+    KEY `name` (`username`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='管理员日志表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__area`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__area`;
+CREATE TABLE `__PREFIX__area`
+(
+    `id`        int(10) NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `pid`       int(10)                                 DEFAULT NULL COMMENT '父id',
+    `shortname` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '简称',
+    `name`      varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '名称',
+    `mergename` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '全称',
+    `level`     tinyint(4)                              DEFAULT NULL COMMENT '层级:1=省,2=市,3=区/县',
+    `pinyin`    varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '拼音',
+    `code`      varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '长途区号',
+    `zip`       varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '邮编',
+    `first`     varchar(50) COLLATE utf8mb4_unicode_ci  DEFAULT NULL COMMENT '首字母',
+    `lng`       varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '经度',
+    `lat`       varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '纬度',
+    PRIMARY KEY (`id`),
+    KEY `pid` (`pid`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='地区表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__attachment`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__attachment`;
+CREATE TABLE `__PREFIX__attachment`
+(
+    `id`             int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `topic`          varchar(20)      NOT NULL DEFAULT '' COMMENT '细目',
+    `admin_id`       int(10) unsigned NOT NULL DEFAULT '0' COMMENT '上传管理员ID',
+    `user_id`        int(10) unsigned NOT NULL DEFAULT '0' COMMENT '上传用户ID',
+    `url`            varchar(255)     NOT NULL DEFAULT '' COMMENT '物理路径',
+    `width`          int(10) unsigned NOT NULL DEFAULT '0' COMMENT '宽度',
+    `height`         int(10) unsigned NOT NULL DEFAULT '0' COMMENT '高度',
+    `name`           varchar(100)     NOT NULL DEFAULT '' COMMENT '原始名称',
+    `size`           int(10) unsigned NOT NULL DEFAULT '0' COMMENT '大小',
+    `mimetype`       varchar(100)     NOT NULL DEFAULT '' COMMENT 'mime类型',
+    `quote`          int(10) unsigned NOT NULL DEFAULT '0' COMMENT '上传(引用)次数',
+    `storage`        varchar(50)      NOT NULL DEFAULT '' COMMENT '存储方式',
+    `sha1`           varchar(40)      NOT NULL DEFAULT '' COMMENT 'sha1编码',
+    `createtime`     int(10) unsigned          DEFAULT NULL COMMENT '上传时间',
+    `lastuploadtime` int(10) unsigned          DEFAULT NULL COMMENT '最后上传时间',
+    PRIMARY KEY (`id`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='附件表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__captcha`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__captcha`;
+CREATE TABLE `__PREFIX__captcha`
+(
+    `key`        varchar(32) NOT NULL DEFAULT '' COMMENT '验证码Key',
+    `code`       varchar(32) NOT NULL DEFAULT '' COMMENT '验证码(加密后的,用于验证)',
+    `captcha`    varchar(6)  NOT NULL DEFAULT '' COMMENT '验证码(供UniApp安卓二次生成图片)',
+    `createtime` int(10) unsigned     DEFAULT NULL COMMENT '创建时间',
+    `expiretime` int(10) unsigned     DEFAULT NULL COMMENT '过期时间',
+    PRIMARY KEY (`key`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='验证码表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__config`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__config`;
+CREATE TABLE `__PREFIX__config`
+(
+    `id`        int(10) unsigned NOT NULL AUTO_INCREMENT,
+    `name`      varchar(30) COLLATE utf8mb4_unicode_ci  DEFAULT '' COMMENT '变量名',
+    `group`     varchar(30) COLLATE utf8mb4_unicode_ci  DEFAULT '' COMMENT '分组',
+    `title`     varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '变量标题',
+    `tip`       varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '变量描述',
+    `type`      varchar(30) COLLATE utf8mb4_unicode_ci  DEFAULT '' COMMENT '类型:string,number,radio,checkbox,switch,textarea,array,datetime,date,select,selects',
+    `value`     text COLLATE utf8mb4_unicode_ci COMMENT '变量值',
+    `content`   text COLLATE utf8mb4_unicode_ci COMMENT '字典数据',
+    `rule`      varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '验证规则',
+    `extend`    varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '扩展属性',
+    `allow_del` tinyint(1)       NOT NULL               DEFAULT '0' COMMENT '允许删除:0=否,1=是',
+    `weigh`     int(10)          NOT NULL               DEFAULT '0' COMMENT '权重',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `name` (`name`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='系统配置';
+
+-- ----------------------------
+-- Records of __PREFIX__config
+-- ----------------------------
+BEGIN;
+INSERT INTO `__PREFIX__config`
+VALUES ('1', 'config_group', 'basics', 'Config group', '', 'array',
+        '[{\"key\":\"basics\",\"value\":\"Basics\"},{\"key\":\"mail\",\"value\":\"Mail\"},{\"key\":\"config_quick_entrance\",\"value\":\"Config Quick entrance\"}]',
+        null, 'required', '', '0', '-1');
+INSERT INTO `__PREFIX__config`
+VALUES ('2', 'site_name', 'basics', 'Site Name', '站点名称', 'string', '站点名称', null, 'required', '', '0', '99');
+INSERT INTO `__PREFIX__config`
+VALUES ('3', 'record_number', 'basics', 'Record number', '域名备案号', 'string', '渝ICP备8888888号-1', null, '', '',
+        '0', '0');
+INSERT INTO `__PREFIX__config`
+VALUES ('4', 'version', 'basics', 'Version number', '系统版本号', 'string', 'v1.0.0', null, 'required', '', '0', '0');
+INSERT INTO `__PREFIX__config`
+VALUES ('5', 'time_zone', 'basics', 'time zone', '', 'string', 'Asia/Shanghai', null, 'required', '', '0', '0');
+INSERT INTO `__PREFIX__config`
+VALUES ('6', 'no_access_ip', 'basics', 'No access ip', '禁止访问站点的ip列表,一行一个', 'textarea', '', null, '', '',
+        '0', '0');
+INSERT INTO `__PREFIX__config`
+VALUES ('7', 'smtp_server', 'mail', 'smtp server', '', 'string', 'smtp.qq.com', null, '', '', '0', '99');
+INSERT INTO `__PREFIX__config`
+VALUES ('8', 'smtp_port', 'mail', 'smtp port', '', 'string', '465', null, '', '', '0', '0');
+INSERT INTO `__PREFIX__config`
+VALUES ('9', 'smtp_user', 'mail', 'smtp user', '', 'string', '', null, '', '', '0', '0');
+INSERT INTO `__PREFIX__config`
+VALUES ('10', 'smtp_pass', 'mail', 'smtp pass', '', 'string', '', null, '', '', '0', '0');
+INSERT INTO `__PREFIX__config`
+VALUES ('11', 'smtp_verification', 'mail', 'smtp verification', '', 'select', 'SSL',
+        '{\"SSL\":\"SSL\",\"TLS\":\"TLS\"}', '', '', '0', '0');
+INSERT INTO `__PREFIX__config`
+VALUES ('12', 'smtp_sender_mail', 'mail', 'smtp sender mail', '', 'string', '', null, 'email', '', '0', '0');
+INSERT INTO `__PREFIX__config`
+VALUES ('13', 'config_quick_entrance', 'config_quick_entrance', 'Config Quick entrance', '', 'array',
+        '[{\"key\":\"\\u6570\\u636e\\u56de\\u6536\\u89c4\\u5219\\u914d\\u7f6e\",\"value\":\"\\/admin\\/security\\/dataRecycle\"},{\"key\":\"\\u654f\\u611f\\u6570\\u636e\\u89c4\\u5219\\u914d\\u7f6e\",\"value\":\"\\/admin\\/security\\/sensitiveData\"}]',
+        null, '', '', '0', '0');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for `__PREFIX__menu_rule`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__menu_rule`;
+CREATE TABLE `__PREFIX__menu_rule`
+(
+    `id`         int(10) unsigned                                                          NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `pid`        int(10) unsigned                                                          NOT NULL DEFAULT '0' COMMENT '上级菜单',
+    `type`       enum ('menu_dir','menu','button') COLLATE utf8mb4_unicode_ci              NOT NULL DEFAULT 'menu' COMMENT '类型:menu_dir=菜单目录,menu=菜单项,button=页面按钮',
+    `title`      varchar(50) COLLATE utf8mb4_unicode_ci                                    NOT NULL DEFAULT '' COMMENT '标题',
+    `name`       varchar(50) COLLATE utf8mb4_unicode_ci                                    NOT NULL DEFAULT '' COMMENT '规则名称',
+    `path`       varchar(100) COLLATE utf8mb4_unicode_ci                                   NOT NULL DEFAULT '' COMMENT '路由路径',
+    `icon`       varchar(50) COLLATE utf8mb4_unicode_ci                                    NOT NULL DEFAULT '' COMMENT '图标',
+    `menu_type`  enum ('tab','link','iframe') COLLATE utf8mb4_unicode_ci                            DEFAULT NULL COMMENT '菜单类型:tab=选项卡,link=链接,iframe=Iframe',
+    `url`        varchar(255) COLLATE utf8mb4_unicode_ci                                   NOT NULL DEFAULT '' COMMENT 'Url',
+    `component`  varchar(100) COLLATE utf8mb4_unicode_ci                                   NOT NULL DEFAULT '' COMMENT '组件路径',
+    `keepalive`  tinyint(1) unsigned                                                       NOT NULL DEFAULT '0' COMMENT '缓存:0=关闭,1=开启',
+    `extend`     enum ('none','add_rules_only','add_menu_only') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'none' COMMENT '扩展属性:none=无,add_rules_only=只添加为路由,add_menu_only=只添加为菜单',
+    `remark`     varchar(255) COLLATE utf8mb4_unicode_ci                                   NOT NULL DEFAULT '' COMMENT '备注',
+    `weigh`      int(10)                                                                   NOT NULL DEFAULT '0' COMMENT '权重(排序)',
+    `status`     enum ('1','0') COLLATE utf8mb4_unicode_ci                                 NOT NULL DEFAULT '1' COMMENT '状态:0=禁用,1=启用',
+    `updatetime` int(10)                                                                            DEFAULT NULL COMMENT '更新时间',
+    `createtime` int(10)                                                                            DEFAULT NULL COMMENT '创建时间',
+    PRIMARY KEY (`id`),
+    KEY `pid` (`pid`),
+    KEY `weigh` (`weigh`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='菜单和权限规则表';
+
+-- ----------------------------
+-- Records of __PREFIX__menu_rule
+-- ----------------------------
+BEGIN;
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('1', '0', 'menu', '控制台', 'dashboard/dashboard', 'dashboard', 'fa fa-dashboard', 'tab', '',
+        '/src/views/backend/dashboard.vue', '1', 'none', 'remark_text', '999', '1', '1651926966', '1646889188');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('2', '0', 'menu_dir', '权限管理', 'auth', 'auth', 'fa fa-group', null, '', '', '0', 'none', '', '100', '1',
+        '1648948034', '1645876529');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('3', '2', 'menu', '角色组管理', 'auth/group', 'auth/group', 'fa fa-group', 'tab', '',
+        '/src/views/backend/auth/group/index.vue', '1', 'none', '', '99', '1', '1648162157', '1646927597');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('4', '3', 'button', '查看', 'auth/group/index', '', '', null, '', '', '0', 'none', '', '99', '1', '1648065864',
+        '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('5', '3', 'button', '添加', 'auth/group/add', '', '', null, '', '', '0', 'none', '', '99', '1', '1648065864',
+        '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('6', '3', 'button', '编辑', 'auth/group/edit', '', '', null, '', '', '0', 'none', '', '99', '1', '1648065864',
+        '1647806129');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('7', '3', 'button', '删除', 'auth/group/del', '', '', null, '', '', '0', 'none', '', '99', '1', '1648065864',
+        '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('8', '2', 'menu', '管理员管理', 'auth/admin', 'auth/admin', 'el-icon-UserFilled', 'tab', '',
+        '/src/views/backend/auth/admin/index.vue', '1', 'none', '', '98', '1', '1648067239', '1647549566');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('9', '8', 'button', '查看', 'auth/admin/index', '', '', null, '', '', '0', 'none', '', '98', '1', '1648065864',
+        '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('10', '8', 'button', '添加', 'auth/admin/add', '', '', null, '', '', '0', 'none', '', '98', '1', '1648065864',
+        '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('11', '8', 'button', '编辑', 'auth/admin/edit', '', '', null, '', '', '0', 'none', '', '98', '1', '1648065864',
+        '1647806129');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('12', '8', 'button', '删除', 'auth/admin/del', '', '', null, '', '', '0', 'none', '', '98', '1', '1648065864',
+        '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('13', '2', 'menu', '菜单规则管理', 'auth/menu', 'auth/menu', 'el-icon-Grid', 'tab', '',
+        '/src/views/backend/auth/menu/index.vue', '1', 'none', '', '97', '1', '1648133759', '1645876529');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('14', '13', 'button', '查看', 'auth/menu/index', '', '', null, '', '', '0', 'none', '', '97', '1', '1648065864',
+        '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('15', '13', 'button', '添加', 'auth/menu/add', '', '', null, '', '', '0', 'none', '', '97', '1', '1648065864',
+        '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('16', '13', 'button', '编辑', 'auth/menu/edit', '', '', null, '', '', '0', 'none', '', '97', '1', '1648065864',
+        '1647806129');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('17', '13', 'button', '删除', 'auth/menu/del', '', '', null, '', '', '0', 'none', '', '97', '1', '1648065864',
+        '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('18', '13', 'button', '快速排序', 'auth/menu/sortable', '', '', null, '', '', '0', 'none', '', '97', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('19', '2', 'menu', '管理员日志管理', 'auth/adminLog', 'auth/adminLog', 'el-icon-List', 'tab', '',
+        '/src/views/backend/auth/adminLog/index.vue', '1', 'none', '', '96', '1', '1648067241', '1647963918');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('20', '19', 'button', '查看', 'auth/adminLog/index', '', '', null, '', '', '0', 'none', '', '96', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('21', '0', 'menu_dir', '会员管理', 'user', 'user', 'fa fa-drivers-license', null, '', '', '0', 'none', '', '95',
+        '1', '1648947448', '1648049553');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('22', '21', 'menu', '会员管理', 'user/user', 'user/user', 'fa fa-user', 'tab', '',
+        '/src/views/backend/user/user/index.vue', '1', 'none', '', '94', '1', '1648255019', '1648049712');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('23', '22', 'button', '查看', 'user/user/index', '', '', null, '', '', '0', 'none', '', '94', '1', '1648065864',
+        '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('24', '22', 'button', '添加', 'user/user/add', '', '', null, '', '', '0', 'none', '', '94', '1', '1648065864',
+        '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('25', '22', 'button', '编辑', 'user/user/edit', '', '', null, '', '', '0', 'none', '', '94', '1', '1648065864',
+        '1647806129');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('26', '22', 'button', '删除', 'user/user/del', '', '', null, '', '', '0', 'none', '', '94', '1', '1648065864',
+        '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('27', '21', 'menu', '会员分组管理', 'user/group', 'user/group', 'fa fa-group', 'tab', '',
+        '/src/views/backend/user/group/index.vue', '1', 'none', '', '93', '1', '1648067248', '1648051141');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('28', '27', 'button', '查看', 'user/group/index', '', '', null, '', '', '0', 'none', '', '93', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('29', '27', 'button', '添加', 'user/group/add', '', '', null, '', '', '0', 'none', '', '93', '1', '1648065864',
+        '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('30', '27', 'button', '编辑', 'user/group/edit', '', '', null, '', '', '0', 'none', '', '93', '1', '1648065864',
+        '1647806129');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('31', '27', 'button', '删除', 'user/group/del', '', '', null, '', '', '0', 'none', '', '93', '1', '1648065864',
+        '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('32', '21', 'menu', '会员规则管理', 'user/rule', 'user/rule', 'fa fa-th-list', 'tab', '',
+        '/src/views/backend/user/rule/index.vue', '1', 'none', '', '92', '1', '1648067247', '1648051207');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('33', '32', 'button', '查看', 'user/rule/index', '', '', null, '', '', '0', 'none', '', '92', '1', '1648065864',
+        '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('34', '32', 'button', '添加', 'user/rule/add', '', '', null, '', '', '0', 'none', '', '92', '1', '1648065864',
+        '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('35', '32', 'button', '编辑', 'user/rule/edit', '', '', null, '', '', '0', 'none', '', '92', '1', '1648065864',
+        '1647806129');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('36', '32', 'button', '删除', 'user/rule/del', '', '', null, '', '', '0', 'none', '', '92', '1', '1648065864',
+        '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('37', '32', 'button', '快速排序', 'user/rule/sortable', '', '', null, '', '', '0', 'none', '', '92', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('38', '21', 'menu', '会员余额管理', 'user/moneyLog', 'user/moneyLog', 'el-icon-Money', 'tab', '',
+        '/src/views/backend/user/moneyLog/index.vue', '0', 'none', '', '91', '1', '1648437356', '1648052587');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('39', '38', 'button', '查看', 'user/moneyLog/index', '', '', null, '', '', '0', 'none', '', '91', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('40', '38', 'button', '添加', 'user/moneyLog/add', '', '', null, '', '', '0', 'none', '', '91', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('41', '21', 'menu', '会员积分管理', 'user/scoreLog', 'user/scoreLog', 'el-icon-Discount', 'tab', '',
+        '/src/views/backend/user/scoreLog/index.vue', '1', 'none', '', '90', '1', '1648067246', '1648052689');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('42', '41', 'button', '查看', 'user/scoreLog/index', '', '', null, '', '', '0', 'none', '', '90', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('43', '41', 'button', '添加', 'user/scoreLog/add', '', '', null, '', '', '0', 'none', '', '90', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('44', '0', 'menu_dir', '常规管理', 'routine', 'routine', 'fa fa-cogs', null, '', '', '0', 'none', '', '89', '1',
+        '1648133739', '1645876529');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('45', '44', 'menu', '系统配置', 'routine/config', 'routine/config', 'el-icon-Tools', 'tab', '',
+        '/src/views/backend/routine/config/index.vue', '1', 'none', '', '88', '1', '1648781089', '1648053389');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('46', '45', 'button', '查看', 'routine/config/index', '', '', null, '', '', '0', 'none', '', '88', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('47', '45', 'button', '编辑', 'routine/config/edit', '', '', null, '', '', '0', 'none', '', '88', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('48', '44', 'menu', '附件管理', 'routine/attachment', 'routine/attachment', 'fa fa-folder', 'tab', '',
+        '/src/views/backend/routine/attachment/index.vue', '1', 'none', 'remark_text', '87', '1', '1648067228',
+        '1647105410');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('49', '48', 'button', '查看', 'routine/attachment/index', '', '', null, '', '', '0', 'none', '', '87', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('50', '48', 'button', '编辑', 'routine/attachment/edit', '', '', null, '', '', '0', 'none', '', '87', '1',
+        '1648065864', '1647806129');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('51', '48', 'button', '删除', 'routine/attachment/del', '', '', null, '', '', '0', 'none', '', '87', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('52', '44', 'menu', '个人资料', 'routine/adminInfo', 'routine/adminInfo', 'fa fa-user', 'tab', '',
+        '/src/views/backend/routine/adminInfo.vue', '1', 'none', '', '86', '1', '1648067229', '1645876529');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('53', '52', 'button', '查看', 'routine/adminInfo/index', '', '', null, '', '', '0', 'none', '', '86', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('54', '52', 'button', '编辑', 'routine/adminInfo/edit', '', '', null, '', '', '0', 'none', '', '86', '1',
+        '1648065864', '1647806129');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('55', '0', 'menu_dir', '数据安全管理', 'security', 'security', 'fa fa-shield', null, '', '', '0', 'none', '',
+        '85', '1', '1649853629', '1648948025');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('56', '55', 'menu', '数据回收站', 'security/dataRecycleLog', 'security/dataRecycleLog', 'fa fa-database', 'tab',
+        '', '/src/views/backend/security/dataRecycleLog/index.vue', '1', 'none', '', '84', '1', '1651603319',
+        '1648948283');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('57', '56', 'button', '查看', 'security/dataRecycleLog/index', '', '', null, '', '', '0', 'none', '', '84', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('58', '56', 'button', '删除', 'security/dataRecycleLog/del', '', '', null, '', '', '0', 'none', '', '84', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('59', '56', 'button', '还原', 'security/dataRecycleLog/restore', '', '', null, '', '', '0', 'none', '', '84',
+        '1', '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('60', '56', 'button', '查看详情', 'security/dataRecycleLog/info', '', '', null, '', '', '0', 'none', '', '84',
+        '1', '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('61', '55', 'menu', '敏感数据修改记录', 'security/sensitiveDataLog', 'security/sensitiveDataLog',
+        'fa fa-expeditedssl', 'tab', '', '/src/views/backend/security/sensitiveDataLog/index.vue', '1', 'none', '',
+        '83', '1', '1649112262', '1649059604');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('62', '61', 'button', '查看', 'security/sensitiveDataLog/index', '', '', null, '', '', '0', 'none', '', '83',
+        '1', '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('63', '61', 'button', '删除', 'security/sensitiveDataLog/del', '', '', null, '', '', '0', 'none', '', '83', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('64', '61', 'button', '回滚', 'security/sensitiveDataLog/rollback', '', '', null, '', '', '0', 'none', '', '83',
+        '1', '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('65', '61', 'button', '查看详情', 'security/sensitiveDataLog/info', '', '', null, '', '', '0', 'none', '', '83',
+        '1', '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('66', '55', 'menu', '数据回收规则管理', 'security/dataRecycle', 'security/dataRecycle', 'fa fa-database', 'tab',
+        '', '/src/views/backend/security/dataRecycle/index.vue', '1', 'none',
+        '在此定义需要回收的数据,实现数据自动统一回收', '82', '1', '1651603319', '1648948215');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('67', '66', 'button', '查看', 'security/dataRecycle/index', '', '', null, '', '', '0', 'none', '', '82', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('68', '66', 'button', '添加', 'security/dataRecycle/add', '', '', null, '', '', '0', 'none', '', '82', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('69', '66', 'button', '编辑', 'security/dataRecycle/edit', '', '', null, '', '', '0', 'none', '', '82', '1',
+        '1648065864', '1647806129');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('70', '66', 'button', '删除', 'security/dataRecycle/del', '', '', null, '', '', '0', 'none', '', '82', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('71', '55', 'menu', '敏感字段规则管理', 'security/sensitiveData', 'security/sensitiveData',
+        'fa fa-expeditedssl', 'tab', '', '/src/views/backend/security/sensitiveData/index.vue', '1', 'none',
+        '在此定义需要保护的敏感字段,随后系统将自动监听该字段的修改操作,并提供了敏感字段的修改回滚功能', '81', '1',
+        '1649112263', '1649005119');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('72', '71', 'button', '查看', 'security/sensitiveData/index', '', '', null, '', '', '0', 'none', '', '81', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('73', '71', 'button', '添加', 'security/sensitiveData/add', '', '', null, '', '', '0', 'none', '', '81', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('74', '71', 'button', '编辑', 'security/sensitiveData/edit', '', '', null, '', '', '0', 'none', '', '81', '1',
+        '1648065864', '1647806129');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('75', '71', 'button', '删除', 'security/sensitiveData/del', '', '', null, '', '', '0', 'none', '', '81', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('76', '0', 'menu', 'BuildAdmin', 'buildadmin/buildadmin', 'buildadmin', 'local-logo', 'link',
+        'https://doc.buildadmin.com', '', '0', 'none', '', '0', '0', '1651926977', '1648947396');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('77', '45', 'button', '添加', 'routine/config/add', '', '', null, '', '', '0', 'none', '', '88', '1',
+        '1655375826', '1655375812');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('78', '0', 'menu', '模块市场', 'moduleStore/moduleStore', 'moduleStore', 'el-icon-GoodsFilled', 'tab', '',
+        '/src/views/backend/module/index.vue', '1', 'none', '', '86', '1', '1661317584', '1661317424');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('79', '78', 'button', '查看', 'moduleStore/moduleStore/index', '', '', null, '', '', '0', 'none', '', '1', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('80', '78', 'button', '安装', 'moduleStore/moduleStore/install', '', '', null, '', '', '0', 'none', '', '2',
+        '1', '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('81', '78', 'button', '调整状态', 'moduleStore/moduleStore/changeState', '', '', null, '', '', '0', 'none', '',
+        '3', '1', '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('82', '78', 'button', '卸载', 'moduleStore/moduleStore/uninstall', '', '', null, '', '', '0', 'none', '', '4',
+        '1', '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('83', '78', 'button', '更新', 'moduleStore/moduleStore/update', '', '', null, '', '', '0', 'none', '', '5', '1',
+        '1648065864', '1647806112');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('84', '0', 'menu', 'CRUD代码生成', 'crud/crud', 'crud/crud', 'fa fa-code', 'tab', '',
+        '/src/views/backend/crud/index.vue', '1', 'none', '', '80', '1', '1668848266', '1668848266');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('85', '84', 'button', '查看', 'crud/crud/index', '', '', null, '', '', '0', 'none', '', '3', '1', '1668848809',
+        '1668848770');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('86', '84', 'button', '生成', 'crud/crud/generate', '', '', null, '', '', '0', 'none', '', '2', '1',
+        '1668848809', '1668848770');
+INSERT INTO `__PREFIX__menu_rule`
+VALUES ('87', '84', 'button', '删除', 'crud/crud/delete', '', '', null, '', '', '0', 'none', '', '1', '1', '1668848921',
+        '1668848921');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for `__PREFIX__security_data_recycle`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__security_data_recycle`;
+CREATE TABLE `__PREFIX__security_data_recycle`
+(
+    `id`            int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `name`          varchar(50)      NOT NULL DEFAULT '' COMMENT '规则名称',
+    `controller`    varchar(100)     NOT NULL DEFAULT '' COMMENT '控制器',
+    `controller_as` varchar(100)     NOT NULL DEFAULT '' COMMENT '控制器别名',
+    `data_table`    varchar(100)     NOT NULL DEFAULT '' COMMENT '对应数据表',
+    `primary_key`   varchar(50)      NOT NULL DEFAULT '' COMMENT '数据表主键',
+    `status`        enum ('1','0')   NOT NULL DEFAULT '0' COMMENT '状态:0=禁用,1=启用',
+    `updatetime`    int(10) unsigned          DEFAULT NULL COMMENT '修改时间',
+    `createtime`    int(10) unsigned          DEFAULT NULL COMMENT '创建时间',
+    PRIMARY KEY (`id`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='回收规则表';
+
+-- ----------------------------
+-- Records of __PREFIX__security_data_recycle
+-- ----------------------------
+BEGIN;
+INSERT INTO `__PREFIX__security_data_recycle`
+VALUES ('1', '管理员', 'auth/Admin.php', 'auth/admin', 'admin', 'id', '1', '1648958789', '1648958789');
+INSERT INTO `__PREFIX__security_data_recycle`
+VALUES ('2', '管理员日志', 'auth/AdminLog.php', 'auth/adminlog', 'admin_log', 'id', '1', '1648967082', '1648958964');
+INSERT INTO `__PREFIX__security_data_recycle`
+VALUES ('3', '菜单规则', 'auth/Menu.php', 'auth/menu', 'menu_rule', 'id', '1', '1648959494', '1648959494');
+INSERT INTO `__PREFIX__security_data_recycle`
+VALUES ('4', '系统配置项', 'routine/Config.php', 'routine/config', 'config', 'id', '1', '1648959518', '1648959510');
+INSERT INTO `__PREFIX__security_data_recycle`
+VALUES ('5', '会员', 'user/User.php', 'user/user', 'user', 'id', '1', '1649097966', '1648959540');
+INSERT INTO `__PREFIX__security_data_recycle`
+VALUES ('6', '数据回收规则', 'security/DataRecycle.php', 'security/datarecycle', 'security_data_recycle', 'id', '1',
+        '1648965759', '1648959655');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for `__PREFIX__security_data_recycle_log`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__security_data_recycle_log`;
+CREATE TABLE `__PREFIX__security_data_recycle_log`
+(
+    `id`          int(11) unsigned    NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `admin_id`    int(11) unsigned    NOT NULL DEFAULT '0' COMMENT '操作管理员',
+    `recycle_id`  int(11) unsigned    NOT NULL DEFAULT '0' COMMENT '回收规则ID',
+    `data`        text COMMENT '回收的数据',
+    `data_table`  varchar(100)        NOT NULL DEFAULT '' COMMENT '数据表',
+    `primary_key` varchar(50)         NOT NULL DEFAULT '' COMMENT '数据表主键',
+    `is_restore`  tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否已还原:0=否,1=是',
+    `ip`          varchar(50)         NOT NULL DEFAULT '' COMMENT '操作者IP',
+    `useragent`   varchar(255)        NOT NULL DEFAULT '' COMMENT 'User Agent',
+    `createtime`  int(10) unsigned             DEFAULT NULL COMMENT '删除时间',
+    PRIMARY KEY (`id`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='数据回收记录表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__security_sensitive_data`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__security_sensitive_data`;
+CREATE TABLE `__PREFIX__security_sensitive_data`
+(
+    `id`            int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `name`          varchar(50)      NOT NULL DEFAULT '' COMMENT '规则名称',
+    `controller`    varchar(100)     NOT NULL DEFAULT '' COMMENT '控制器',
+    `controller_as` varchar(100)     NOT NULL DEFAULT '' COMMENT '处理后的控制器名',
+    `data_table`    varchar(100)     NOT NULL DEFAULT '' COMMENT '对应数据表',
+    `primary_key`   varchar(50)      NOT NULL DEFAULT '' COMMENT '数据表主键字段',
+    `data_fields`   text             NOT NULL COMMENT '敏感数据字段',
+    `status`        enum ('1','0')   NOT NULL DEFAULT '0' COMMENT '状态:0=关闭,1=启用',
+    `updatetime`    int(10) unsigned          DEFAULT NULL COMMENT '修改时间',
+    `createtime`    int(10) unsigned          DEFAULT NULL COMMENT '创建时间',
+    PRIMARY KEY (`id`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='敏感数据表';
+
+-- ----------------------------
+-- Records of __PREFIX__security_sensitive_data
+-- ----------------------------
+BEGIN;
+INSERT INTO `__PREFIX__security_sensitive_data`
+VALUES ('1', '管理员数据', 'auth/Admin.php', 'auth/admin', 'admin', 'id',
+        '{\"username\":\"用户名\",\"mobile\":\"手机\",\"password\":\"密码\",\"status\":\"状态\"}', '1', '1649047890',
+        '1649045180');
+INSERT INTO `__PREFIX__security_sensitive_data`
+VALUES ('2', '会员数据', 'user/User.php', 'user/user', 'user', 'id',
+        '{\"username\":\"用户名\",\"mobile\":\"手机号\",\"password\":\"密码\",\"status\":\"状态\",\"email\":\"邮箱地址\"}',
+        '1', '1649058989', '1649045243');
+INSERT INTO `__PREFIX__security_sensitive_data`
+VALUES ('3', '管理员权限', 'auth/Group.php', 'auth/group', 'admin_group', 'id', '{\"rules\":\"权限规则ID\"}', '1',
+        '1649047866', '1649047271');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for `__PREFIX__security_sensitive_data_log`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__security_sensitive_data_log`;
+CREATE TABLE `__PREFIX__security_sensitive_data_log`
+(
+    `id`           int(11) unsigned    NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `admin_id`     int(11) unsigned    NOT NULL DEFAULT '0' COMMENT '管理员',
+    `sensitive_id` int(11) unsigned    NOT NULL DEFAULT '0' COMMENT '敏感数据规则ID',
+    `data_table`   varchar(100)        NOT NULL DEFAULT '' COMMENT '所在数据表',
+    `primary_key`  varchar(50)         NOT NULL DEFAULT '' COMMENT '数据表主键',
+    `data_field`   varchar(50)         NOT NULL DEFAULT '' COMMENT '被修改字段',
+    `data_comment` varchar(50)         NOT NULL DEFAULT '' COMMENT '被修改项',
+    `id_value`     varchar(11)         NOT NULL DEFAULT '' COMMENT '被修改项主键值',
+    `before`       text COMMENT '修改前',
+    `after`        text COMMENT '修改后',
+    `ip`           varchar(50)         NOT NULL DEFAULT '' COMMENT '操作者IP',
+    `useragent`    varchar(255)        NOT NULL DEFAULT '' COMMENT 'User Agent',
+    `is_rollback`  tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否已回滚:0=否,1=是',
+    `createtime`   int(10) unsigned             DEFAULT NULL COMMENT '修改时间',
+    PRIMARY KEY (`id`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='敏感数据修改记录';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__test_build`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__test_build`;
+CREATE TABLE `__PREFIX__test_build`
+(
+    `id`            int(10) unsigned      NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `title`         varchar(100)                   DEFAULT NULL COMMENT '标题',
+    `keyword_rows`  varchar(100)                   DEFAULT NULL COMMENT '关键词',
+    `content`       text                  NOT NULL COMMENT '内容',
+    `views`         int(10) unsigned      NOT NULL DEFAULT '0' COMMENT '浏览量',
+    `likes`         mediumint(9) unsigned NOT NULL DEFAULT '0' COMMENT '有帮助数',
+    `dislikes`      mediumint(9) unsigned NOT NULL DEFAULT '0' COMMENT '无帮助数',
+    `note_textarea` varchar(100)                   DEFAULT NULL COMMENT '备注',
+    `status`        enum ('1','0')        NOT NULL DEFAULT '1' COMMENT '状态:0=隐藏,1=正常',
+    `weigh`         int(10)               NOT NULL DEFAULT '0' COMMENT '权重',
+    `updatetime`    int(10) unsigned               DEFAULT NULL COMMENT '更新时间',
+    `createtime`    int(10) unsigned               DEFAULT NULL COMMENT '创建时间',
+    PRIMARY KEY (`id`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4 COMMENT ='知识库表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__token`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__token`;
+CREATE TABLE `__PREFIX__token`
+(
+    `token`      varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Token',
+    `type`       varchar(15)                            NOT NULL COMMENT '类型',
+    `user_id`    int(10) unsigned                       NOT NULL DEFAULT '0' COMMENT '用户ID',
+    `createtime` int(10)                                         DEFAULT NULL COMMENT '创建时间',
+    `expiretime` int(10)                                         DEFAULT NULL COMMENT '过期时间',
+    PRIMARY KEY (`token`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='用户Token表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__user`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__user`;
+CREATE TABLE `__PREFIX__user`
+(
+    `id`                  int(10) unsigned                              NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `group_id`            int(10) unsigned                              NOT NULL DEFAULT '1' COMMENT '用户组ID',
+    `username`            varchar(32) COLLATE utf8mb4_unicode_ci                 DEFAULT '' COMMENT '用户名',
+    `mobile`              varchar(11) COLLATE utf8mb4_unicode_ci                 DEFAULT '' COMMENT '手机号',
+    `email`               varchar(100) COLLATE utf8mb4_unicode_ci                DEFAULT '' COMMENT '邮箱地址',
+    `nickname`            varchar(30) COLLATE utf8mb4_unicode_ci                 DEFAULT '' COMMENT '昵称',
+    `verification_status` enum ('0','1','2') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0' COMMENT '认证状态:0=未审核,1=已审核,2=已拒绝',
+    `group_name`          varchar(30) COLLATE utf8mb4_unicode_ci                 DEFAULT NULL COMMENT '用户组名称',
+    `real_name`           varchar(30) COLLATE utf8mb4_unicode_ci                 DEFAULT NULL COMMENT '真实姓名',
+    `identity`            char(18) COLLATE utf8mb4_unicode_ci                    DEFAULT NULL COMMENT '身份证号',
+    `identity_img`        blob                                                   DEFAULT NULL COMMENT '证件照',
+    `aerocraft_sn`        text COLLATE utf8mb4_unicode_ci                        DEFAULT NULL COMMENT '飞机编码',
+    `aerocraft_img`       blob                                                   DEFAULT NULL COMMENT '飞机照片',
+    `pilot_license`       blob                                                   DEFAULT NULL COMMENT '飞行证书',
+    `city`                varchar(30) COLLATE utf8mb4_unicode_ci                 DEFAULT NULL COMMENT '城市',
+    `address`             varchar(255) COLLATE utf8mb4_unicode_ci                DEFAULT NULL COMMENT '地址',
+    `avatar`              varchar(255) COLLATE utf8mb4_unicode_ci                DEFAULT '' COMMENT '头像',
+    `gender`              enum ('0','1','2') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0' COMMENT '性别:0=未知,1=男,2=女',
+    `birthday`            date                                                   DEFAULT NULL COMMENT '生日',
+    `money`               int(10) unsigned                              NOT NULL DEFAULT '0' COMMENT '余额',
+    `score`               int(10)                                       NOT NULL DEFAULT '0' COMMENT '积分',
+    `lastlogintime`       int(10)                                                DEFAULT NULL COMMENT '上次登录时间',
+    `lastloginip`         varchar(50) COLLATE utf8mb4_unicode_ci                 DEFAULT NULL COMMENT '登录IP',
+    `loginfailure`        tinyint(1) unsigned                           NOT NULL DEFAULT '0' COMMENT '失败次数',
+    `joinip`              varchar(50) COLLATE utf8mb4_unicode_ci                 DEFAULT NULL COMMENT '加入IP',
+    `jointime`            int(10)                                                DEFAULT NULL COMMENT '加入时间',
+    `motto`               varchar(100) COLLATE utf8mb4_unicode_ci                DEFAULT NULL COMMENT '签名',
+    `password`            varchar(32) COLLATE utf8mb4_unicode_ci                 DEFAULT '' COMMENT '密码',
+    `salt`                varchar(30) COLLATE utf8mb4_unicode_ci                 DEFAULT '' COMMENT '密码盐',
+    `status`              varchar(30) COLLATE utf8mb4_unicode_ci                 DEFAULT '' COMMENT '状态',
+    `updatetime`          int(10)                                                DEFAULT NULL COMMENT '更新时间',
+    `createtime`          int(10)                                                DEFAULT NULL COMMENT '创建时间',
+    `deletetime`          int(10)                                                DEFAULT NULL COMMENT '删除时间',
+    PRIMARY KEY (`id`),
+    KEY `username` (`username`),
+    KEY `email` (`email`),
+    KEY `mobile` (`mobile`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='会员表';
+
+-- ----------------------------
+-- Records of __PREFIX__user
+-- ----------------------------
+BEGIN;
+INSERT INTO `__PREFIX__user`
+VALUES ('1', '1', 'user', '18888888888', 'user@buildadmin.com', 'User', '0', '默认游客', '张', '48881000**********', null, 'sn****************', null, null, '上海', null, '', '1', '2022-05-13', '0', '0', '1648156017', '127.0.0.1', '0', null, '1648156017', null, null, null, 'enable', '1650731874', '1648156017', null);
+COMMIT;
+
+-- ----------------------------
+-- Table structure for `__PREFIX__user_group`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__user_group`;
+CREATE TABLE `__PREFIX__user_group`
+(
+    `id`         int(10) unsigned                NOT NULL AUTO_INCREMENT,
+    `name`       varchar(50) COLLATE utf8mb4_unicode_ci    DEFAULT '' COMMENT '用户组名',
+    `rules`      text COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '权限节点',
+    `status`     enum ('1','0') COLLATE utf8mb4_unicode_ci DEFAULT '1' COMMENT '状态:0=禁用,1=启用',
+    `updatetime` int(10)                                   DEFAULT NULL COMMENT '更新时间',
+    `createtime` int(10)                                   DEFAULT NULL COMMENT '添加时间',
+    `deletetime` int(10)                                   DEFAULT NULL COMMENT '删除时间',
+    PRIMARY KEY (`id`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='用户组表';
+
+-- ----------------------------
+-- Records of __PREFIX__user_group
+-- ----------------------------
+BEGIN;
+INSERT INTO `__PREFIX__user_group`
+VALUES ('1', '默认游客', '*', '1', '1648167137', '1648167095', null);
+COMMIT;
+
+-- ----------------------------
+-- Table structure for `__PREFIX__user_money_log`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__user_money_log`;
+CREATE TABLE `__PREFIX__user_money_log`
+(
+    `id`         int(10) unsigned NOT NULL AUTO_INCREMENT,
+    `user_id`    int(10) unsigned NOT NULL               DEFAULT '0' COMMENT '会员ID',
+    `money`      int(10)          NOT NULL               DEFAULT '0' COMMENT '变更余额',
+    `before`     int(10)          NOT NULL               DEFAULT '0' COMMENT '变更前余额',
+    `after`      int(10)          NOT NULL               DEFAULT '0' COMMENT '变更后余额',
+    `memo`       varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '备注',
+    `createtime` int(10) unsigned                        DEFAULT NULL COMMENT '创建时间',
+    PRIMARY KEY (`id`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='会员余额变动表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__user_rule`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__user_rule`;
+CREATE TABLE `__PREFIX__user_rule`
+(
+    `id`         int(10) unsigned                                                          NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `pid`        int(10) unsigned                                                          NOT NULL DEFAULT '0' COMMENT '上级菜单',
+    `type`       enum ('route','menu_dir','menu') COLLATE utf8mb4_unicode_ci               NOT NULL DEFAULT 'menu' COMMENT '类型:route=路由,menu_dir=菜单目录,menu=菜单项',
+    `title`      varchar(50) COLLATE utf8mb4_unicode_ci                                    NOT NULL DEFAULT '' COMMENT '标题',
+    `name`       varchar(50) COLLATE utf8mb4_unicode_ci                                    NOT NULL DEFAULT '' COMMENT '规则名称',
+    `path`       varchar(100) COLLATE utf8mb4_unicode_ci                                   NOT NULL DEFAULT '' COMMENT '路由路径',
+    `icon`       varchar(50) COLLATE utf8mb4_unicode_ci                                    NOT NULL DEFAULT '' COMMENT '图标',
+    `menu_type`  enum ('tab','link','iframe') COLLATE utf8mb4_unicode_ci                   NOT NULL DEFAULT 'tab' COMMENT '菜单类型:tab=选项卡,link=链接,iframe=Iframe',
+    `url`        varchar(255) COLLATE utf8mb4_unicode_ci                                   NOT NULL DEFAULT '' COMMENT 'Url',
+    `component`  varchar(100) COLLATE utf8mb4_unicode_ci                                   NOT NULL DEFAULT '' COMMENT '组件路径',
+    `extend`     enum ('none','add_rules_only','add_menu_only') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'none' COMMENT '扩展属性:none=无,add_rules_only=只添加为路由,add_menu_only=只添加为菜单',
+    `remark`     varchar(255) COLLATE utf8mb4_unicode_ci                                   NOT NULL DEFAULT '' COMMENT '备注',
+    `weigh`      int(10)                                                                   NOT NULL DEFAULT '0' COMMENT '权重(排序)',
+    `status`     enum ('1','0') COLLATE utf8mb4_unicode_ci                                 NOT NULL DEFAULT '1' COMMENT '状态:0=禁用,1=启用',
+    `updatetime` int(10)                                                                            DEFAULT NULL COMMENT '更新时间',
+    `createtime` int(10)                                                                            DEFAULT NULL COMMENT '创建时间',
+    PRIMARY KEY (`id`),
+    KEY `pid` (`pid`),
+    KEY `weigh` (`weigh`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='会员权限规则表';
+
+-- ----------------------------
+-- Records of __PREFIX__user_rule
+-- ----------------------------
+BEGIN;
+INSERT INTO `__PREFIX__user_rule`
+VALUES ('1', '0', 'menu_dir', '我的账户', 'account', 'account', 'fa fa-user-circle', 'tab', '', '', 'none', '', '98',
+        '1', '1655970295', '1648156017');
+INSERT INTO `__PREFIX__user_rule`
+VALUES ('2', '1', 'menu', '账户概览', 'account/overview', 'account/overview', 'fa fa-home', 'tab', '',
+        '/src/views/frontend/user/account/overview.vue', 'none', '', '99', '1', '1655879438', '1655820267');
+INSERT INTO `__PREFIX__user_rule`
+VALUES ('3', '1', 'menu', '个人资料', 'account/profile', 'account/profile', 'fa fa-user-circle-o', 'tab', '',
+        '/src/views/frontend/user/account/profile.vue', 'none', '', '98', '1', '1655972096', '1655820365');
+INSERT INTO `__PREFIX__user_rule`
+VALUES ('4', '1', 'menu', '修改密码', 'account/changePassword', 'account/changePassword', 'fa fa-shield', 'tab', '',
+        '/src/views/frontend/user/account/changePassword.vue', 'none', '', '97', '1', '1655980365', '1655820461');
+INSERT INTO `__PREFIX__user_rule`
+VALUES ('5', '1', 'menu', '积分记录', 'account/integral', 'account/integral', 'fa fa-tag', 'tab', '',
+        '/src/views/frontend/user/account/integral.vue', 'none', '', '96', '1', '1655985356', '1655820507');
+INSERT INTO `__PREFIX__user_rule`
+VALUES ('6', '1', 'menu', '余额记录', 'account/balance', 'account/balance', 'fa fa-money', 'tab', '',
+        '/src/views/frontend/user/account/balance.vue', 'none', '', '95', '1', '1655985373', '1655820593');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for `__PREFIX__user_score_log`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__user_score_log`;
+CREATE TABLE `__PREFIX__user_score_log`
+(
+    `id`         int(10) unsigned NOT NULL AUTO_INCREMENT,
+    `user_id`    int(10) unsigned NOT NULL               DEFAULT '0' COMMENT '会员ID',
+    `score`      int(10)          NOT NULL               DEFAULT '0' COMMENT '变更积分',
+    `before`     int(10)          NOT NULL               DEFAULT '0' COMMENT '变更前积分',
+    `after`      int(10)          NOT NULL               DEFAULT '0' COMMENT '变更后积分',
+    `memo`       varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '备注',
+    `createtime` int(10) unsigned                        DEFAULT NULL COMMENT '创建时间',
+    PRIMARY KEY (`id`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='会员积分变动表';
+
+-- ----------------------------
+-- Table structure for `__PREFIX__crud_log`
+-- ----------------------------
+DROP TABLE IF EXISTS `__PREFIX__crud_log`;
+CREATE TABLE `__PREFIX__crud_log`
+(
+    `id`          int(10) unsigned                        NOT NULL AUTO_INCREMENT COMMENT 'ID',
+    `table_name`  varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '数据表名',
+    `table`       text COLLATE utf8mb4_unicode_ci COMMENT '数据表数据',
+    `fields`      text COLLATE utf8mb4_unicode_ci COMMENT '字段数据',
+    `status`      enum ('delete','success','error','start') COLLATE utf8mb4_unicode_ci DEFAULT 'start' COMMENT '状态:delete=已删除,success=成功,error=失败,start=生成中',
+    `create_time` bigint(16)                                                           DEFAULT NULL COMMENT '创建时间',
+    PRIMARY KEY (`id`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4
+  COLLATE = utf8mb4_unicode_ci COMMENT ='crud记录';

+ 84 - 0
app/admin/common.php

@@ -0,0 +1,84 @@
+<?php
+
+use think\facade\Db;
+
+if (!function_exists('get_controller_list')) {
+    function get_controller_list($app = 'admin'): array
+    {
+        $controllerDir = root_path() . 'app' . DIRECTORY_SEPARATOR . $app . DIRECTORY_SEPARATOR . 'controller' . DIRECTORY_SEPARATOR;
+        return get_dir_files($controllerDir);
+    }
+}
+
+if (!function_exists('get_dir_files')) {
+    function get_dir_files($dir): array
+    {
+        $files = new RecursiveIteratorIterator(
+            new RecursiveDirectoryIterator($dir), RecursiveIteratorIterator::LEAVES_ONLY
+        );
+
+        $fileList = [];
+        foreach ($files as $file) {
+            if (!$file->isDir() && $file->getExtension() == 'php') {
+                $filePath        = $file->getRealPath();
+                $name            = str_replace($dir, '', $filePath);
+                $name            = str_replace(DIRECTORY_SEPARATOR, "/", $name);
+                $fileList[$name] = $name;
+            }
+        }
+        return $fileList;
+    }
+}
+
+if (!function_exists('get_table_list')) {
+    function get_table_list(): array
+    {
+        $tableList = [];
+        $database  = config('database.connections.mysql.database');
+        $tables    = Db::query("SELECT TABLE_NAME,TABLE_COMMENT FROM information_schema.TABLES WHERE table_schema = ? ", [$database]);
+        foreach ($tables as $row) {
+            $tableList[$row['TABLE_NAME']] = $row['TABLE_NAME'] . ($row['TABLE_COMMENT'] ? ' - ' . str_replace('表', '', $row['TABLE_COMMENT']) : '');
+        }
+        return $tableList;
+    }
+}
+
+if (!function_exists('get_table_fields')) {
+    function get_table_fields($table, $onlyCleanComment = false): array
+    {
+        if (!$table) return [];
+
+        $dbname = config('database.connections.mysql.database');
+        $prefix = config('database.connections.mysql.prefix');
+
+        // 从数据库中获取表字段信息
+        $sql        = "SELECT * FROM `information_schema`.`columns` "
+            . "WHERE TABLE_SCHEMA = ? AND table_name = ? "
+            . "ORDER BY ORDINAL_POSITION";
+        $columnList = Db::query($sql, [$dbname, $table]);
+        if (!$columnList) {
+            $columnList = Db::query($sql, [$dbname, $prefix . $table]);
+        }
+
+        $fieldList = [];
+        foreach ($columnList as $item) {
+            if ($onlyCleanComment) {
+                $fieldList[$item['COLUMN_NAME']] = '';
+                if ($item['COLUMN_COMMENT']) {
+                    $comment                         = explode(':', $item['COLUMN_COMMENT']);
+                    $fieldList[$item['COLUMN_NAME']] = $comment[0];
+                }
+                continue;
+            }
+            $fieldList[$item['COLUMN_NAME']] = $item;
+        }
+        return $fieldList;
+    }
+}
+
+if (!function_exists('mb_ucfirst')) {
+    function mb_ucfirst($string): string
+    {
+        return mb_strtoupper(mb_substr($string, 0, 1)) . mb_strtolower(mb_substr($string, 1));
+    }
+}

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

@@ -0,0 +1,107 @@
+<?php
+
+namespace app\admin\controller;
+
+use ba\Terminal;
+use think\Exception;
+use think\facade\Db;
+use think\facade\Cache;
+use think\facade\Event;
+use app\admin\model\AdminLog;
+use app\common\library\Upload;
+use think\exception\FileException;
+use app\common\controller\Backend;
+
+class Ajax extends Backend
+{
+    protected $noNeedPermission = ['*'];
+
+    public function initialize()
+    {
+        parent::initialize();
+    }
+
+    public function upload()
+    {
+        AdminLog::setTitle(__('upload'));
+        $file = $this->request->file('file');
+        try {
+            $upload     = new Upload($file);
+            $attachment = $upload->upload(null, $this->auth->id);
+            unset($attachment['createtime'], $attachment['quote']);
+        } catch (Exception|FileException $e) {
+            $this->error($e->getMessage());
+        }
+
+        $this->success(__('File uploaded successfully'), [
+            'file' => $attachment ?? []
+        ]);
+    }
+
+    public function area()
+    {
+        $this->success('', get_area());
+    }
+
+    public function buildSuffixSvg()
+    {
+        $suffix     = $this->request->param('suffix', 'file');
+        $background = $this->request->param('background');
+        $content    = build_suffix_svg((string)$suffix, (string)$background);
+        return response($content, 200, ['Content-Length' => strlen($content)])->contentType('image/svg+xml');
+    }
+
+    public function getTablePk($table = null)
+    {
+        if (!$table) {
+            $this->error(__('Parameter error'));
+        }
+        $tablePk = Db::query("SHOW TABLE STATUS LIKE '{$table}'", [], true);
+        if (!$tablePk) {
+            $table   = config('database.connections.mysql.prefix') . $table;
+            $tablePk = Db::query("SHOW TABLE STATUS LIKE '{$table}'", [], true);
+            if (!$tablePk) {
+                $this->error(__('Data table does not exist'));
+            }
+        }
+        $tablePk = Db::table($table)->getPk();
+        $this->success('', ['pk' => $tablePk]);
+    }
+
+    public function getTableFieldList()
+    {
+        $table = $this->request->param('table');
+        $clean = $this->request->param('clean', true);
+        if (!$table) {
+            $this->error(__('Parameter error'));
+        }
+
+        $tablePk = Db::name($table)->getPk();
+        $this->success('', [
+            'pk'        => $tablePk,
+            'fieldList' => get_table_fields($table, $clean),
+        ]);
+    }
+
+    public function changeTerminalConfig()
+    {
+        AdminLog::setTitle(__('changeTerminalConfig'));
+        if (Terminal::changeTerminalConfig()) {
+            $this->success();
+        } else {
+            $this->error(__('Failed to modify the terminal configuration. Please modify the configuration file manually:%s', ['/config/buildadmin.php']));
+        }
+    }
+
+    public function clearCache()
+    {
+        $type = $this->request->post('type');
+        if ($type == 'tp' || $type == 'all') {
+            Cache::clear();
+        } else {
+            $this->error(__('Parameter error'));
+        }
+        Event::trigger('cacheClearAfter', $this->app);
+        $this->success(__('Cache cleaned~'));
+    }
+}

+ 15 - 0
app/admin/controller/Dashboard.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+
+class Dashboard extends Backend
+{
+    public function dashboard()
+    {
+        $this->success('', [
+            'remark' => get_route_remark()
+        ]);
+    }
+}

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

@@ -0,0 +1,118 @@
+<?php
+declare (strict_types=1);
+
+namespace app\admin\controller;
+
+use app\common\facade\Token;
+use ba\Captcha;
+use think\facade\Config;
+use think\facade\Validate;
+use app\common\controller\Backend;
+use app\admin\model\AdminLog;
+
+class Index extends Backend
+{
+    protected $noNeedLogin      = ['logout', 'login'];
+    protected $noNeedPermission = ['index'];
+
+    public function index()
+    {
+        $adminInfo          = $this->auth->getInfo();
+        $adminInfo['super'] = $this->auth->isSuperAdmin();
+        unset($adminInfo['token'], $adminInfo['refreshToken']);
+
+        $menus = $this->auth->getMenus();
+        if (!$menus) {
+            $this->error(__('No background menu, please contact super administrator!'));
+        }
+        $this->success('', [
+            'adminInfo'  => $adminInfo,
+            'menus'      => $menus,
+            'siteConfig' => [
+                'siteName' => get_sys_config('site_name'),
+                'version'  => get_sys_config('version'),
+                'cdnUrl'   => full_url(),
+                'apiUrl'   => Config::get('buildadmin.api_url'),
+                'upload'   => get_upload_config(),
+            ],
+            'terminal'   => [
+                'installServicePort' => Config::get('terminal.install_service_port'),
+                'npmPackageManager'  => Config::get('terminal.npm_package_manager'),
+            ]
+        ]);
+    }
+
+    public function login()
+    {
+        // 检查登录态
+        if ($this->auth->isLogin()) {
+            $this->success(__('You have already logged in. There is no need to log in again~'), [
+                'routePath' => '/admin'
+            ], 302);
+        }
+
+        $captchaSwitch = Config::get('buildadmin.admin_login_captcha');
+
+        // 检查提交
+        if ($this->request->isPost()) {
+            $username = $this->request->post('username');
+            $password = $this->request->post('password');
+            $keep     = $this->request->post('keep');
+
+            $rule = [
+                'username|' . __('Username') => 'require|length:3,30',
+                'password|' . __('Password') => 'require|regex:^(?!.*[&<>"\'\n\r]).{6,32}$',
+            ];
+            $data = [
+                'username' => $username,
+                'password' => $password,
+            ];
+            if ($captchaSwitch) {
+                $rule['captcha|' . __('Captcha')]     = 'require|length:4,6';
+                $rule['captchaId|' . __('CaptchaId')] = 'require';
+
+                $data['captcha']   = $this->request->post('captcha');
+                $data['captchaId'] = $this->request->post('captcha_id');
+            }
+            $validate = Validate::rule($rule);
+            if (!$validate->check($data)) {
+                $this->error($validate->getError());
+            }
+
+            if ($captchaSwitch) {
+                $captchaObj = new Captcha();
+                if (!$captchaObj->check($data['captcha'], $data['captchaId'])) {
+                    $this->error(__('Please enter the correct verification code'));
+                }
+            }
+
+            AdminLog::setTitle(__('Login'));
+
+            $res = $this->auth->login($username, $password, (bool)$keep);
+            if ($res === true) {
+                $this->success(__('Login succeeded!'), [
+                    'userInfo'  => $this->auth->getInfo(),
+                    'routePath' => '/admin'
+                ]);
+            } else {
+                $msg = $this->auth->getError();
+                $msg = $msg ?: __('Incorrect user name or password!');
+                $this->error($msg);
+            }
+        }
+
+        $this->success('', [
+            'captcha' => $captchaSwitch
+        ]);
+    }
+
+    public function logout()
+    {
+        if ($this->request->isPost()) {
+            $refreshToken = $this->request->post('refresh_token', '');
+            if ($refreshToken) Token::delete((string)$refreshToken);
+            $this->auth->logout();
+            $this->success();
+        }
+    }
+}

+ 142 - 0
app/admin/controller/Module.php

@@ -0,0 +1,142 @@
+<?php
+
+namespace app\admin\controller;
+
+use ba\module\Server;
+use think\Exception;
+use ba\module\Manage;
+use ba\module\moduleException;
+use app\common\controller\Backend;
+
+class Module extends Backend
+{
+    protected $noNeedPermission = ['state', 'dependentInstallComplete'];
+
+    public function index()
+    {
+        $this->success('', [
+            'installed' => Server::installedList(root_path() . 'modules' . DIRECTORY_SEPARATOR),
+        ]);
+    }
+
+    public function state()
+    {
+        $uid = $this->request->get("uid/s", '');
+        if (!$uid) {
+            $this->error(__('Parameter error'));
+        }
+        $this->success('', [
+            'state' => Manage::instance($uid)->getInstallState()
+        ]);
+    }
+
+    public function install()
+    {
+        $uid     = $this->request->get("uid/s", '');
+        $token   = $this->request->get("token/s", '');
+        $orderId = $this->request->get("order_id/d", 0);
+        if (!$uid) {
+            $this->error(__('Parameter error'));
+        }
+        $res = [];
+        try {
+            $res = Manage::instance($uid)->install($token, $orderId);
+        } catch (moduleException $e) {
+            $this->error(__($e->getMessage()), $e->getData(), $e->getCode());
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()));
+        }
+        $this->success('', [
+            'data' => $res,
+        ]);
+    }
+
+    public function dependentInstallComplete()
+    {
+        $uid = $this->request->get("uid/s", '');
+        if (!$uid) {
+            $this->error(__('Parameter error'));
+        }
+        try {
+            Manage::instance($uid)->dependentInstallComplete('all');
+        } catch (moduleException $e) {
+            $this->error(__($e->getMessage()), $e->getData(), $e->getCode());
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()));
+        }
+        $this->success();
+    }
+
+    public function changeState()
+    {
+        $uid   = $this->request->post("uid/s", '');
+        $state = $this->request->post("state/b", false);
+        if (!$uid) {
+            $this->error(__('Parameter error'));
+        }
+        $info = [];
+        try {
+            $info = Manage::instance($uid)->changeState($state);
+        } catch (moduleException $e) {
+            $this->error(__($e->getMessage()), $e->getData(), $e->getCode());
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()));
+        }
+        $this->success('', [
+            'info' => $info,
+        ]);
+    }
+
+    public function uninstall()
+    {
+        $uid = $this->request->get("uid/s", '');
+        if (!$uid) {
+            $this->error(__('Parameter error'));
+        }
+        try {
+            Manage::instance($uid)->uninstall();
+        } catch (moduleException $e) {
+            $this->error(__($e->getMessage()), $e->getData(), $e->getCode());
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()));
+        }
+        $this->success();
+    }
+
+    public function update()
+    {
+        $uid     = $this->request->get("uid/s", '');
+        $token   = $this->request->get("token/s", '');
+        $orderId = $this->request->get("order_id/d", 0);
+        if (!$token || !$uid) {
+            $this->error(__('Parameter error'));
+        }
+        try {
+            Manage::instance($uid)->update($token, $orderId);
+        } catch (moduleException $e) {
+            $this->error(__($e->getMessage()), $e->getData(), $e->getCode());
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()));
+        }
+        $this->success();
+    }
+
+    public function upload()
+    {
+        $file = $this->request->get("file/s", '');
+        if (!$file) {
+            $this->error(__('Parameter error'));
+        }
+        $info = [];
+        try {
+            $info = Manage::instance()->upload($file);
+        } catch (moduleException $e) {
+            $this->error(__($e->getMessage()), $e->getData(), $e->getCode());
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()));
+        }
+        $this->success('', [
+            'info' => $info
+        ]);
+    }
+}

+ 17 - 0
app/admin/controller/Terminal.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace app\admin\controller;
+
+use ba\Terminal as T;
+
+/**
+ * WEB终端
+ * Content-Type: text/event-stream
+ */
+class Terminal
+{
+    public function index()
+    {
+        T::instance()->exec();
+    }
+}

+ 35 - 0
app/admin/controller/aerocraft/FlightPlan.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace app\admin\controller\aerocraft;
+
+use app\common\controller\Backend;
+
+/**
+ * 飞行计划管理
+ *
+ */
+class FlightPlan extends Backend
+{
+    /**
+     * FlightPlan模型对象
+     * @var \app\admin\model\FlightPlan
+     */
+    protected $model = null;
+    
+    protected $defaultSortField = 'applyfor_no,desc';
+
+    protected $preExcludeFields = ['id', 'applyfor_no', 'username', 'group_id', 'updatetime', 'createtime', 'deletetime'];
+
+    protected $quickSearchField = ['id'];
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new \app\admin\model\FlightPlan;
+    }
+
+
+    /**
+     * 若需重写查看、编辑、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写
+     */
+}

+ 257 - 0
app/admin/controller/auth/Admin.php

@@ -0,0 +1,257 @@
+<?php
+
+namespace app\admin\controller\auth;
+
+use ba\Random;
+use Exception;
+use app\common\controller\Backend;
+use app\admin\model\Admin as AdminModel;
+use think\db\exception\PDOException;
+use think\exception\ValidateException;
+use think\facade\Db;
+
+class Admin extends Backend
+{
+    /**
+     * @var AdminModel
+     */
+    protected $model = null;
+
+    protected $preExcludeFields = ['createtime', 'updatetime', 'password', 'salt', 'loginfailure', 'lastlogintime', 'lastloginip'];
+
+    protected $quickSearchField = ['username', 'nickname'];
+
+    /**
+     * 开启数据限制
+     */
+    protected $dataLimit = 'allAuthAndOthers';
+
+    protected $dataLimitField = 'id';
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new AdminModel();
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        $this->request->filter(['strip_tags', 'trim']);
+        if ($this->request->param('select')) {
+            $this->select();
+        }
+
+        list($where, $alias, $limit, $order) = $this->queryBuilder();
+        $res = $this->model
+            ->withoutField('loginfailure,password,salt')
+            ->withJoin($this->withJoinTable, $this->withJoinType)
+            ->alias($alias)
+            ->where($where)
+            ->order($order)
+            ->paginate($limit);
+
+        $this->success('', [
+            'list'   => $res->items(),
+            'total'  => $res->total(),
+            'remark' => get_route_remark(),
+        ]);
+    }
+
+    public function add()
+    {
+        if ($this->request->isPost()) {
+            $data = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            /**
+             * 由于有密码字段-对方法进行重写
+             * 数据验证
+             */
+            if ($this->modelValidate) {
+                try {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    $validate = new $validate;
+                    $validate->scene('add')->check($data);
+                } catch (ValidateException $e) {
+                    $this->error($e->getMessage());
+                }
+            }
+
+            $salt   = Random::build('alnum', 16);
+            $passwd = encrypt_password($data['password'], $salt);
+
+            $data   = $this->excludeFields($data);
+            $result = false;
+            if ($data['group_arr']) $this->checkGroupAuth($data['group_arr']);
+            Db::startTrans();
+            try {
+                $data['salt']     = $salt;
+                $data['password'] = $passwd;
+                $result           = $this->model->save($data);
+                if ($data['group_arr']) {
+                    $groupAccess = [];
+                    foreach ($data['group_arr'] as $datum) {
+                        $groupAccess[] = [
+                            'uid'      => $this->model->id,
+                            'group_id' => $datum,
+                        ];
+                    }
+                    Db::name('admin_group_access')->insertAll($groupAccess);
+                }
+                Db::commit();
+            } catch (ValidateException|PDOException|Exception $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('Added successfully'));
+            } else {
+                $this->error(__('No rows were added'));
+            }
+        }
+
+        $this->error(__('Parameter error'));
+    }
+
+    public function edit($id = null)
+    {
+        $row = $this->model->find($id);
+        if (!$row) {
+            $this->error(__('Record not found'));
+        }
+
+        $dataLimitAdminIds = $this->getDataLimitAdminIds();
+        if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
+            $this->error(__('You have no permission'));
+        }
+
+        if ($this->request->isPost()) {
+            $data = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            /**
+             * 由于有密码字段-对方法进行重写
+             * 数据验证
+             */
+            if ($this->modelValidate) {
+                try {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    $validate = new $validate;
+                    $validate->scene('edit')->check($data);
+                } catch (ValidateException $e) {
+                    $this->error($e->getMessage());
+                }
+            }
+
+            if ($this->auth->id == $data['id'] && $data['status'] == '0') {
+                $this->error(__('Please use another administrator account to disable the current account!'));
+            }
+
+            if (isset($data['password']) && $data['password']) {
+                $this->model->resetPassword($data['id'], $data['password']);
+            }
+
+            $groupAccess = [];
+            if ($data['group_arr']) {
+                $checkGroups = [];
+                foreach ($data['group_arr'] as $datum) {
+                    if (!in_array($datum, $row->group_arr)) {
+                        $checkGroups[] = $datum;
+                    }
+                    $groupAccess[] = [
+                        'uid'      => $id,
+                        'group_id' => $datum,
+                    ];
+                }
+                $this->checkGroupAuth($checkGroups);
+            }
+
+            Db::name('admin_group_access')
+                ->where('uid', $id)
+                ->delete();
+
+            $data   = $this->excludeFields($data);
+            $result = false;
+            Db::startTrans();
+            try {
+                $result = $row->save($data);
+                if ($groupAccess) Db::name('admin_group_access')->insertAll($groupAccess);
+                Db::commit();
+            } catch (PDOException|Exception $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('Update successful'));
+            } else {
+                $this->error(__('No rows updated'));
+            }
+        }
+
+        unset($row['salt'], $row['loginfailure']);
+        $row['password'] = '';
+        $this->success('', [
+            'row' => $row
+        ]);
+    }
+
+    /**
+     * 删除
+     * @param null $ids
+     */
+    public function del($ids = null)
+    {
+        if (!$this->request->isDelete() || !$ids) {
+            $this->error(__('Parameter error'));
+        }
+
+        $dataLimitAdminIds = $this->getDataLimitAdminIds();
+        if ($dataLimitAdminIds) {
+            $this->model->where($this->dataLimitField, 'in', $dataLimitAdminIds);
+        }
+
+        $pk    = $this->model->getPk();
+        $data  = $this->model->where($pk, 'in', $ids)->select();
+        $count = 0;
+        Db::startTrans();
+        try {
+            foreach ($data as $v) {
+                if ($v->id != $this->auth->id) {
+                    $count += $v->delete();
+                    Db::name('admin_group_access')
+                        ->where('uid', $v['id'])
+                        ->delete();
+                }
+            }
+            Db::commit();
+        } catch (PDOException|Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        if ($count) {
+            $this->success(__('Deleted successfully'));
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+
+    public function checkGroupAuth(array $groups)
+    {
+        if ($this->auth->isSuperAdmin()) {
+            return;
+        }
+        $authGroups = $this->auth->getAllAuthGroups('allAuthAndOthers');
+        foreach ($groups as $group) {
+            if (!in_array($group, $authGroups)) {
+                $this->error(__('You have no permission to add an administrator to this group!'));
+            }
+        }
+    }
+}

+ 52 - 0
app/admin/controller/auth/AdminLog.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace app\admin\controller\auth;
+
+use app\common\controller\Backend;
+use app\admin\model\AdminLog as AdminLogModel;
+
+class AdminLog extends Backend
+{
+    /**
+     * @var AdminLogModel
+     */
+    protected $model = null;
+
+    protected $preExcludeFields = ['createtime', 'admin_id', 'username'];
+
+    protected $quickSearchField = ['title'];
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new AdminLogModel();
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        $this->request->filter(['strip_tags', 'trim']);
+        if ($this->request->param('select')) {
+            $this->select();
+        }
+
+        list($where, $alias, $limit, $order) = $this->queryBuilder();
+        if (!$this->auth->isSuperAdmin()) {
+            $where[] = ['admin_id', '=', $this->auth->id];
+        }
+        $res = $this->model
+            ->withJoin($this->withJoinTable, $this->withJoinType)
+            ->alias($alias)
+            ->where($where)
+            ->order($order)
+            ->paginate($limit);
+
+        $this->success('', [
+            'list'   => $res->items(),
+            'total'  => $res->total(),
+            'remark' => get_route_remark(),
+        ]);
+    }
+}

+ 343 - 0
app/admin/controller/auth/Group.php

@@ -0,0 +1,343 @@
+<?php
+
+namespace app\admin\controller\auth;
+
+use ba\Tree;
+use Exception;
+use think\facade\Db;
+use app\admin\model\MenuRule;
+use app\admin\model\AdminGroup;
+use app\common\controller\Backend;
+use think\db\exception\PDOException;
+use think\exception\ValidateException;
+
+class Group extends Backend
+{
+    /**
+     * 修改、删除分组时对操作管理员进行鉴权
+     * 本管理功能部分场景对数据权限有要求,修改此值请额外确定以下的 absoluteAuth 实现的功能
+     * allAuthAndOthers=管理员拥有该分组所有权限并拥有额外权限时允许
+     */
+    protected $authMethod = 'allAuthAndOthers';
+
+    /**
+     * @var AdminGroup
+     */
+    protected $model = null;
+
+    protected $preExcludeFields = ['createtime', 'updatetime'];
+
+    protected $quickSearchField = 'name';
+
+    /**
+     * @var Tree
+     */
+    protected $tree = null;
+
+    /**
+     * 远程select初始化传值
+     * @var array
+     */
+    protected $initValue;
+
+    /**
+     * 搜索关键词
+     * @var array
+     */
+    protected $keyword = false;
+
+    /**
+     * 是否组装Tree
+     * @var bool
+     */
+    protected $assembleTree;
+
+    /**
+     * 登录管理员的角色组
+     */
+    protected $adminGroups = [];
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new AdminGroup();
+        $this->tree  = Tree::instance();
+
+        $isTree          = $this->request->param('isTree', true);
+        $this->initValue = $this->request->get("initValue/a", '');
+        $this->keyword   = $this->request->request("quick_search");
+
+        // 有初始化值时不组装树状(初始化出来的值更好看)
+        $this->assembleTree = $isTree && !$this->initValue;
+
+        $this->adminGroups = Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id');
+    }
+
+    public function index()
+    {
+        if ($this->request->param('select')) {
+            $this->select();
+        }
+
+        $this->success('', [
+            'list'   => $this->getGroups(),
+            'remark' => get_route_remark(),
+            'group'  => $this->adminGroups,
+        ]);
+    }
+
+    public function add()
+    {
+        if ($this->request->isPost()) {
+            $data = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            $data = $this->excludeFields($data);
+            if (is_array($data['rules']) && $data['rules']) {
+                $rules      = MenuRule::select();
+                $superAdmin = true;
+                foreach ($rules as $rule) {
+                    if (!in_array($rule['id'], $data['rules'])) {
+                        $superAdmin = false;
+                    }
+                }
+
+                if ($superAdmin) {
+                    $data['rules'] = '*';
+                } else {
+                    // 禁止添加`拥有自己全部权限`的分组
+                    if (!array_diff($this->auth->getRuleIds(), $data['rules'])) {
+                        $this->error(__('Role group has all your rights, please contact the upper administrator to add or do not need to add!'));
+                    }
+                    $data['rules'] = implode(',', $data['rules']);
+                }
+            } else {
+                unset($data['rules']);
+            }
+
+            $result = false;
+            Db::startTrans();
+            try {
+                // 模型验证
+                if ($this->modelValidate) {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    if (class_exists($validate)) {
+                        $validate = new $validate;
+                        $validate->scene('add')->check($data);
+                    }
+                }
+                $result = $this->model->save($data);
+                Db::commit();
+            } catch (ValidateException|Exception|PDOException $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('Added successfully'));
+            } else {
+                $this->error(__('No rows were added'));
+            }
+        }
+
+        $this->error(__('Parameter error'));
+    }
+
+    public function edit($id = null)
+    {
+        $row = $this->model->find($id);
+        if (!$row) {
+            $this->error(__('Record not found'));
+        }
+
+        $this->checkAuth($id);
+
+        if ($this->request->isPost()) {
+            $data = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            $adminGroup = Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id');
+            if (in_array($data['id'], $adminGroup)) {
+                $this->error(__('You cannot modify your own management group!'));
+            }
+
+            $data = $this->excludeFields($data);
+            if (is_array($data['rules']) && $data['rules']) {
+                $rules      = MenuRule::select();
+                $superAdmin = true;
+                foreach ($rules as $rule) {
+                    if (!in_array($rule['id'], $data['rules'])) {
+                        $superAdmin = false;
+                    }
+                }
+
+                if ($superAdmin) {
+                    $data['rules'] = '*';
+                } else {
+                    // 禁止添加`拥有自己全部权限`的分组
+                    if (!array_diff($this->auth->getRuleIds(), $data['rules'])) {
+                        $this->error(__('Role group has all your rights, please contact the upper administrator to add or do not need to add!'));
+                    }
+                    $data['rules'] = implode(',', $data['rules']);
+                }
+            } else {
+                unset($data['rules']);
+            }
+
+            $result = false;
+            Db::startTrans();
+            try {
+                // 模型验证
+                if ($this->modelValidate) {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    if (class_exists($validate)) {
+                        $validate = new $validate;
+                        $validate->scene('edit')->check($data);
+                    }
+                }
+                $result = $row->save($data);
+                Db::commit();
+            } catch (ValidateException|Exception|PDOException $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('Update successful'));
+            } else {
+                $this->error(__('No rows updated'));
+            }
+        }
+
+        // 读取所有pid,全部从节点数组移除,父级选择状态由子级决定
+        $pids  = MenuRule::field('pid')
+            ->distinct(true)
+            ->where('id', 'in', $row->rules)
+            ->select()->toArray();
+        $rules = $row->rules ? explode(',', $row->rules) : [];
+        foreach ($pids as $item) {
+            $ruKey = array_search($item['pid'], $rules);
+            if ($ruKey !== false) {
+                unset($rules[$ruKey]);
+            }
+        }
+        $row->rules = array_values($rules);
+        $this->success('', [
+            'row' => $row
+        ]);
+    }
+
+    /**
+     * 删除
+     * @param array $ids
+     */
+    public function del(array $ids = [])
+    {
+        if (!$this->request->isDelete() || !$ids) {
+            $this->error(__('Parameter error'));
+        }
+
+        $pk   = $this->model->getPk();
+        $data = $this->model->where($pk, 'in', $ids)->select();
+        foreach ($data as $v) {
+            $this->checkAuth($v->id);
+        }
+        $subData = $this->model->where('pid', 'in', $ids)->column('pid', 'id');
+        foreach ($subData as $key => $subDatum) {
+            if (!in_array($key, $ids)) {
+                $this->error(__('Please delete the child element first, or use batch deletion'));
+            }
+        }
+
+        $adminGroup = Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id');
+        $count      = 0;
+        Db::startTrans();
+        try {
+            foreach ($data as $v) {
+                if (!in_array($v['id'], $adminGroup)) {
+                    $count += $v->delete();
+                }
+            }
+            Db::commit();
+        } catch (PDOException|Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        if ($count) {
+            $this->success(__('Deleted successfully'));
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+
+    public function select()
+    {
+        $data = $this->getGroups([['status', '=', 1]]);
+
+        if ($this->assembleTree) {
+            $data = $this->tree->assembleTree($this->tree->getTreeArray($data));
+        }
+        $this->success('', [
+            'options' => $data
+        ]);
+    }
+
+    public function getGroups($where = []): array
+    {
+        $pk      = $this->model->getPk();
+        $initKey = $this->request->get("initKey/s", $pk);
+
+        // 下拉选择时只获取:拥有所有权限并且有额外权限的分组
+        $absoluteAuth = $this->request->get('absoluteAuth/b', false);
+
+        if ($this->keyword) {
+            $keyword = explode(' ', $this->keyword);
+            foreach ($keyword as $item) {
+                $where[] = [$this->quickSearchField, 'like', '%' . $item . '%'];
+            }
+        }
+
+        if ($this->initValue) {
+            $where[] = [$initKey, 'in', $this->initValue];
+        }
+
+        if (!$this->auth->isSuperAdmin()) {
+            $authGroups = $this->auth->getAllAuthGroups($this->authMethod);
+            if (!$absoluteAuth) $authGroups = array_merge($this->adminGroups, $authGroups);
+            $where[] = ['id', 'in', $authGroups];
+        }
+        $data = $this->model->where($where)->select()->toArray();
+
+        // 获取第一个权限的名称供列表显示-s
+        foreach ($data as &$datum) {
+            if ($datum['rules']) {
+                if ($datum['rules'] == '*') {
+                    $datum['rules'] = __('Super administrator');
+                } else {
+                    $rules = explode(',', $datum['rules']);
+                    if ($rules) {
+                        $rulesFirstTitle = MenuRule::where('id', $rules[0])->value('title');
+                        $datum['rules']  = count($rules) == 1 ? $rulesFirstTitle : $rulesFirstTitle . '等 ' . count($rules) . ' 项';
+                    }
+                }
+            } else {
+                $datum['rules'] = __('No permission');
+            }
+        }
+        // 获取第一个权限的名称供列表显示-e
+
+        // 如果要求树状,此处先组装好 children
+        return $this->assembleTree ? $this->tree->assembleChild($data) : $data;
+    }
+
+    public function checkAuth($groupId)
+    {
+        $authGroups = $this->auth->getAllAuthGroups($this->authMethod);
+        if (!$this->auth->isSuperAdmin() && !in_array($groupId, $authGroups)) {
+            $this->error(__($this->authMethod == 'allAuth' ? 'You need to have all permissions of this group to operate this group~' : 'You need to have all the permissions of the group and have additional permissions before you can operate the group~'));
+        }
+    }
+
+}

+ 224 - 0
app/admin/controller/auth/Menu.php

@@ -0,0 +1,224 @@
+<?php
+
+namespace app\admin\controller\auth;
+
+use ba\Tree;
+use Exception;
+use think\facade\Db;
+use app\admin\model\MenuRule;
+use app\common\controller\Backend;
+use think\db\exception\PDOException;
+use think\exception\ValidateException;
+
+class Menu extends Backend
+{
+    /**
+     * @var MenuRule
+     */
+    protected $model = null;
+
+    /**
+     * @var Tree
+     */
+    protected $tree = null;
+
+    protected $preExcludeFields = ['createtime', 'updatetime'];
+
+    protected $quickSearchField = 'title';
+
+    /**
+     * 远程select初始化传值
+     * @var array
+     */
+    protected $initValue;
+
+    /**
+     * 搜索关键词
+     * @var array
+     */
+    protected $keyword = false;
+
+    /**
+     * 是否组装Tree
+     * @var bool
+     */
+    protected $assembleTree;
+
+    protected $modelValidate = false;
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new MenuRule();
+        $this->tree  = Tree::instance();
+
+        $isTree          = $this->request->param('isTree', true);
+        $this->initValue = $this->request->get("initValue/a", '');
+        $this->keyword   = $this->request->request("quick_search");
+
+        // 有初始化值时不组装树状(初始化出来的值更好看)
+        $this->assembleTree = $isTree && !$this->initValue;
+    }
+
+    public function index()
+    {
+        if ($this->request->param('select')) {
+            $this->select();
+        }
+
+        $this->success('', [
+            'list'   => $this->getMenus(),
+            'remark' => get_route_remark(),
+        ]);
+    }
+
+    /**
+     * 编辑
+     */
+    public function edit()
+    {
+        $id  = $this->request->param($this->model->getPk());
+        $row = $this->model->find($id);
+        if (!$row) {
+            $this->error(__('Record not found'));
+        }
+
+        $dataLimitAdminIds = $this->getDataLimitAdminIds();
+        if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
+            $this->error(__('You have no permission'));
+        }
+
+        if ($this->request->isPost()) {
+            $data = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            $data   = $this->excludeFields($data);
+            $result = false;
+            Db::startTrans();
+            try {
+                // 模型验证
+                if ($this->modelValidate) {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    if (class_exists($validate)) {
+                        $validate = new $validate;
+                        if ($this->modelSceneValidate) $validate->scene('edit');
+                        $validate->check($data);
+                    }
+                }
+                if (isset($data['pid']) && $data['pid'] > 0) {
+                    // 满足意图并消除副作用
+                    $parent = $this->model->where('id', $data['pid'])->find();
+                    if ($parent['pid'] == $row['id']) {
+                        $parent->pid = 0;
+                        $parent->save();
+                    }
+                }
+                $result = $row->save($data);
+                Db::commit();
+            } catch (ValidateException|Exception|PDOException $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('Update successful'));
+            } else {
+                $this->error(__('No rows updated'));
+            }
+        }
+
+        $this->success('', [
+            'row' => $row
+        ]);
+    }
+
+    /**
+     * 删除
+     * @param array $ids
+     */
+    public function del(array $ids = [])
+    {
+        if (!$this->request->isDelete() || !$ids) {
+            $this->error(__('Parameter error'));
+        }
+
+        $dataLimitAdminIds = $this->getDataLimitAdminIds();
+        if ($dataLimitAdminIds) {
+            $this->model->where($this->dataLimitField, 'in', $dataLimitAdminIds);
+        }
+
+        $pk      = $this->model->getPk();
+        $data    = $this->model->where($pk, 'in', $ids)->select();
+        $subData = $this->model->where('pid', 'in', $ids)->column('pid', 'id');
+        foreach ($subData as $key => $subDatum) {
+            if (!in_array($key, $ids)) {
+                $this->error(__('Please delete the child element first, or use batch deletion'));
+            }
+        }
+        $count = 0;
+        Db::startTrans();
+        try {
+            foreach ($data as $v) {
+                $count += $v->delete();
+            }
+            Db::commit();
+        } catch (PDOException|Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        if ($count) {
+            $this->success(__('Deleted successfully'));
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+
+    /**
+     * 重写select方法
+     */
+    public function select()
+    {
+        $data = $this->getMenus([['type', 'in', ['menu_dir', 'menu']], ['status', '=', '1']]);
+
+        if ($this->assembleTree) {
+            $data = $this->tree->assembleTree($this->tree->getTreeArray($data, 'title'));
+        }
+        $this->success('', [
+            'options' => $data
+        ]);
+    }
+
+    protected function getMenus($where = []): array
+    {
+        $pk      = $this->model->getPk();
+        $initKey = $this->request->get("initKey/s", $pk);
+
+        $ids = $this->auth->getRuleIds();
+
+        // 如果没有 * 则只获取用户拥有的规则
+        if (!in_array('*', $ids)) {
+            $where[] = ['id', 'in', $ids];
+        }
+
+        if ($this->keyword) {
+            $keyword = explode(' ', $this->keyword);
+            foreach ($keyword as $item) {
+                $where[] = [$this->quickSearchField, 'like', '%' . $item . '%'];
+            }
+        }
+
+        if ($this->initValue) {
+            $where[] = [$initKey, 'in', $this->initValue];
+        }
+
+        // 读取用户组所有权限规则
+        $rules = $this->model
+            ->where($where)
+            ->order('weigh desc,id asc')
+            ->select()->toArray();
+
+        // 如果要求树状,此处先组装好 children
+        return $this->assembleTree ? $this->tree->assembleChild($rules) : $rules;
+    }
+}

+ 798 - 0
app/admin/controller/crud/Crud.php

@@ -0,0 +1,798 @@
+<?php
+
+namespace app\admin\controller\crud;
+
+use think\Exception;
+use think\facade\Db;
+use app\admin\model\CrudLog;
+use app\common\library\Menu;
+use app\common\controller\Backend;
+use app\admin\library\crud\Helper;
+use think\db\exception\PDOException;
+
+class Crud extends Backend
+{
+    /**
+     * 模型文件数据
+     */
+    protected $modelData = [];
+
+    /**
+     * 控制器文件数据
+     */
+    protected $controllerData = [];
+
+    /**
+     * index.vue文件数据
+     */
+    protected $indexVueData = [];
+
+    /**
+     * form.vue文件数据
+     */
+    protected $formVueData = [];
+
+    /**
+     * 语言翻译前缀
+     */
+    protected $webTranslate = '';
+
+    /**
+     * 语言包数据
+     */
+    protected $langTsData = [];
+
+    /**
+     * 当designType为以下值时:
+     * 1. 出入库字符串到数组转换
+     * 2. 默认值转数组
+     */
+    protected $dtStringToArray = ['checkbox', 'selects', 'remoteSelects', 'city', 'images', 'files'];
+
+    protected $noNeedPermission = ['logStart', 'getFileData', 'parseFieldData', 'generateCheck', 'databaseList'];
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->request->filter(['trim']);
+    }
+
+    public function generate()
+    {
+        $table  = $this->request->post('table', []);
+        $fields = $this->request->post('fields', []);
+
+        if (!$table || !$fields || !isset($table['name']) || !$table['name']) {
+            $this->error(__('Parameter error'));
+        }
+
+        try {
+            // 记录日志
+            $crudLogId = Helper::recordCrudStatus([
+                'table'  => $table,
+                'fields' => $fields,
+                'status' => 'start',
+            ]);
+
+            // 表存在则删除
+            Helper::delTable($table['name']);
+
+            // 创建表
+            [$tablePk] = Helper::createTable($table['name'], $table['comment'] ?? '', $fields);
+
+            // 表名称
+            $tableName = Helper::getTableName($table['name'], false);
+
+            // 表注释
+            $tableComment = mb_substr($table['comment'], -1) == '表' ? mb_substr($table['comment'], 0, -1) . '管理' : $table['comment'];
+
+            // 生成文件信息解析
+            $modelFile      = Helper::parseNameData('admin', $tableName, '', 'model', $table['modelFile']);
+            $validateFile   = Helper::parseNameData('admin', $tableName, '', 'validate', $table['validateFile']);
+            $controllerFile = Helper::parseNameData('admin', $tableName, '', 'controller', $table['controllerFile']);
+            $webViewsDir    = Helper::parseWebDirNameData($tableName, '', 'views', $table['webViewsDir']);
+            $webLangDir     = Helper::parseWebDirNameData($tableName, '', 'lang', $table['webViewsDir']);
+
+            // 语言翻译前缀
+            $this->webTranslate = implode('.', $webLangDir['lang']) . '.';
+
+            // 快速搜索字段
+            if (!in_array($tablePk, $table['quickSearchField'])) {
+                $table['quickSearchField'][] = $tablePk;
+            }
+            $quickSearchFieldZhCnTitle = [];
+
+            // 模型数据
+            $this->modelData['append']             = [];
+            $this->modelData['methods']            = [];
+            $this->modelData['fieldType']          = [];
+            $this->modelData['createTime']         = '';
+            $this->modelData['updateTime']         = '';
+            $this->modelData['beforeInsertMixins'] = [];
+            $this->modelData['beforeInsert']       = '';
+            $this->modelData['afterInsert']        = '';
+            $this->modelData['name']               = $tableName;
+            $this->modelData['className']          = $modelFile['lastName'];
+            $this->modelData['namespace']          = $modelFile['namespace'];
+            $this->modelData['relationMethodList'] = [];
+
+            // 控制器数据
+            $this->controllerData['attr']           = [];
+            $this->controllerData['methods']        = [];
+            $this->controllerData['filterRule']     = '';
+            $this->controllerData['className']      = $controllerFile['lastName'];
+            $this->controllerData['namespace']      = $controllerFile['namespace'];
+            $this->controllerData['tableComment']   = $tableComment;
+            $this->controllerData['modelName']      = $modelFile['lastName'];
+            $this->controllerData['modelNamespace'] = $modelFile['namespace'];
+
+            // index.vue数据
+            $this->indexVueData['enableDragSort']        = false;
+            $this->indexVueData['defaultItems']          = [];
+            $this->indexVueData['tableColumn']           = [
+                [
+                    'type'     => 'selection',
+                    'align'    => 'center',
+                    'operator' => 'false',
+                ],
+            ];
+            $this->indexVueData['dblClickNotEditColumn'] = ['undefined'];
+            $this->indexVueData['optButtons']            = ['edit', 'delete'];
+            $this->indexVueData['defaultOrder']          = '';
+
+            // form.vue数据
+            $this->formVueData['bigDialog']  = 'false';
+            $this->formVueData['formFields'] = [];
+
+            // 语言包数据
+            $this->langTsData = [
+                'en'    => [],
+                'zh-cn' => [],
+            ];
+
+            // 简化的字段数据
+            $fieldsMap = [];
+
+            foreach ($fields as $key => $field) {
+
+                $fieldsMap[$field['name']] = $field['designType'];
+
+                // 分析字段
+                Helper::analyseField($field);
+
+                Helper::getDictData($this->langTsData['en'], $field, 'en');
+                Helper::getDictData($this->langTsData['zh-cn'], $field, 'zh-cn');
+
+                // 快速搜索字段
+                if (in_array($field['name'], $table['quickSearchField'])) {
+                    $quickSearchFieldZhCnTitle[] = $this->langTsData['zh-cn'][$field['name']] ?? $field['name'];
+                }
+
+                // 不允许双击编辑的字段
+                if ($field['designType'] == 'switch') {
+                    $this->indexVueData['dblClickNotEditColumn'][] = $field['name'];
+                }
+
+                // 列字典数据
+                $columnDict = $this->getColumnDict($field);
+
+                // 表单项
+                if (in_array($field['name'], $table['formFields'])) {
+                    $this->formVueData['formFields'][] = $this->getFormField($field, $columnDict);
+                }
+
+                // 表格列
+                if (in_array($field['name'], $table['columnFields'])) {
+                    $this->indexVueData['tableColumn'][] = $this->getTableColumn($field, $columnDict);
+                }
+
+                // 关联表数据解析
+                if (in_array($field['designType'], ['remoteSelect', 'remoteSelects'])) {
+                    $this->parseJoinData($field);
+                }
+
+                // 模型方法
+                $this->parseModelMethods($field, $this->modelData);
+
+                // 控制器/模型等文件的一些杂项属性解析
+                $this->parseSundryData($field, $table);
+
+                if (!in_array($field['name'], $table['formFields'])) {
+                    $this->controllerData['attr']['preExcludeFields'][] = $field['name'];
+                }
+            }
+
+            // 快速搜索提示
+            $this->langTsData['en']['quick Search Fields']    = implode(',', $table['quickSearchField']);
+            $this->langTsData['zh-cn']['quick Search Fields'] = implode('、', $quickSearchFieldZhCnTitle);
+            $this->controllerData['attr']['quickSearchField'] = $table['quickSearchField'];
+
+            // 开启字段排序
+            $weighKey = array_search('weigh', $fieldsMap);
+            if ($weighKey !== false) {
+                $this->indexVueData['enableDragSort'] = true;
+                $this->modelData['afterInsert']       = Helper::assembleStub('mixins/model/afterInsert', [
+                    'field' => $weighKey
+                ]);
+            }
+
+            // 表格的操作列
+            $this->indexVueData['tableColumn'][] = [
+                'label'    => "t('operate')",
+                'align'    => 'center',
+                'width'    => $this->indexVueData['enableDragSort'] ? 140 : 100,
+                'render'   => 'buttons',
+                'buttons'  => 'optButtons',
+                'operator' => 'false',
+            ];
+            if ($this->indexVueData['enableDragSort']) {
+                array_unshift($this->indexVueData['optButtons'], 'weigh-sort');
+            }
+
+            // 写入语言包代码
+            Helper::writeWebLangFile($this->langTsData, $webLangDir);
+
+            // 写入模型代码
+            Helper::writeModelFile($tablePk, $fieldsMap, $this->modelData, $modelFile);
+
+            // 写入控制器代码
+            Helper::writeControllerFile($this->controllerData, $controllerFile);
+
+            // 写入验证器代码
+            $validateContent = Helper::assembleStub('mixins/validate/validate', [
+                'namespace' => $validateFile['namespace'],
+                'className' => $validateFile['lastName'],
+            ]);
+            Helper::writeFile($validateFile['parseFile'], $validateContent);
+
+            // 写入index.vue代码
+            $this->indexVueData['tablePk']      = $tablePk;
+            $this->indexVueData['webTranslate'] = $this->webTranslate;
+            Helper::writeIndexFile($this->indexVueData, $webViewsDir, $controllerFile);
+
+            // 写入form.vue代码
+            Helper::writeFormFile($this->formVueData, $webViewsDir, $fields, $this->webTranslate);
+
+            // 生成菜单
+            Helper::createMenu($webViewsDir, $tableComment);
+
+            Helper::recordCrudStatus([
+                'id'     => $crudLogId,
+                'status' => 'success',
+            ]);
+        } catch (PDOException|Exception $e) {
+            Helper::recordCrudStatus([
+                'id'     => $crudLogId,
+                'status' => 'error',
+            ]);
+            if (env('app_debug', false)) throw $e;
+            $this->error($e->getMessage());
+        }
+
+        $this->success();
+    }
+
+    public function logStart()
+    {
+        $id   = $this->request->post('id');
+        $info = CrudLog::find($id)->toArray();
+        if (!$info) {
+            $this->error(__('Record not found'));
+        }
+        $this->success('', [
+            'table'  => $info['table'],
+            'fields' => $info['fields']
+        ]);
+    }
+
+    public function delete()
+    {
+        $id   = $this->request->post('id');
+        $info = CrudLog::find($id)->toArray();
+        if (!$info) {
+            $this->error(__('Record not found'));
+        }
+        $webLangDir = Helper::parseWebDirNameData($info['table']['name'], '', 'lang', $info['table']['webViewsDir']);
+        $files      = [
+            $webLangDir['en'] . '.ts',
+            $webLangDir['zh-cn'] . '.ts',
+            $info['table']['webViewsDir'] . '/' . 'index.vue',
+            $info['table']['webViewsDir'] . '/' . 'popupForm.vue',
+            $info['table']['controllerFile'],
+            $info['table']['modelFile'],
+            $info['table']['validateFile'],
+        ];
+        try {
+            foreach ($files as &$file) {
+                $file = path_transform(root_path() . $file);
+                if (file_exists($file)) {
+                    unlink($file);
+                }
+                del_empty_dir(dirname($file));
+            }
+
+            // 删除菜单
+            Menu::delete(Helper::getMenuName($webLangDir), true);
+
+            Helper::recordCrudStatus([
+                'id'     => $id,
+                'status' => 'delete',
+            ]);
+        } catch (Exception $e) {
+            $this->error($e->getMessage());
+        }
+        $this->success(__('Deleted successfully'));
+    }
+
+    public function getFileData()
+    {
+        $table = $this->request->get('table');
+
+        if (!$table) {
+            $this->error(__('Parameter error'));
+        }
+
+        try {
+            $modelFile      = Helper::parseNameData('admin', $table, '', 'model');
+            $validateFile   = Helper::parseNameData('admin', $table, '', 'validate');
+            $controllerFile = Helper::parseNameData('admin', $table, '', 'controller');
+            $webViewsDir    = Helper::parseWebDirNameData($table, '', 'views');
+        } catch (Exception $e) {
+            $this->error($e->getMessage());
+        }
+
+        // 模型和控制器文件和文件列表
+        $adminModelFiles      = get_dir_files(root_path() . 'app' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR . 'model' . DIRECTORY_SEPARATOR);
+        $commonModelFiles     = get_dir_files(root_path() . 'app' . DIRECTORY_SEPARATOR . 'common' . DIRECTORY_SEPARATOR . 'model' . DIRECTORY_SEPARATOR);
+        $adminControllerFiles = get_controller_list();
+
+        $modelFileList   = [];
+        $controllerFiles = [];
+        foreach ($adminModelFiles as $item) {
+            $item                 = path_transform('app/admin/model/' . $item);
+            $modelFileList[$item] = $item;
+        }
+        foreach ($commonModelFiles as $item) {
+            $item                 = path_transform('app/common/model/' . $item);
+            $modelFileList[$item] = $item;
+        }
+
+        $outExcludeController = [
+            'Addon.php',
+            'Ajax.php',
+            'Dashboard.php',
+            'Index.php',
+            'Module.php',
+            'Terminal.php',
+            'routine/AdminInfo.php',
+            'routine/Config.php',
+        ];
+        foreach ($adminControllerFiles as $item) {
+            if (in_array($item, $outExcludeController)) {
+                continue;
+            }
+            $item                   = path_transform('app/admin/controller/' . $item);
+            $controllerFiles[$item] = $item;
+        }
+
+        $this->success('', [
+            'modelFile'          => $modelFile['rootFileName'],
+            'controllerFile'     => $controllerFile['rootFileName'],
+            'validateFile'       => $validateFile['rootFileName'],
+            'controllerFileList' => $controllerFiles,
+            'modelFileList'      => $modelFileList,
+            'webViewsDir'        => $webViewsDir['views'],
+        ]);
+    }
+
+    public function parseFieldData()
+    {
+        $type  = $this->request->post('type');
+        $sql   = $this->request->post('sql');
+        $table = $this->request->post('table');
+        if ($type == 'db') {
+            $sql       = 'SELECT * FROM `information_schema`.`tables` '
+                . 'WHERE TABLE_SCHEMA = ? AND table_name = ?';
+            $tableInfo = Db::query($sql, [config('database.connections.mysql.database'), Helper::getTableName($table)]);
+            if (!$tableInfo) {
+                $this->error(__('Record not found'));
+            }
+            $this->success('', [
+                'columns' => Helper::parseTableColumns($table),
+                'comment' => $tableInfo[0]['TABLE_COMMENT'] ?? '',
+            ]);
+        } elseif ($type == 'sql') {
+            // TODO
+        }
+
+    }
+
+    public function generateCheck()
+    {
+        $table          = $this->request->post('table');
+        $controllerFile = $this->request->post('controllerFile', '');
+
+        if (!$table) {
+            $this->error(__('Parameter error'));
+        }
+
+        try {
+            if (!$controllerFile) {
+                $controllerFile = Helper::parseNameData('admin', $table, '', 'controller')['rootFileName'];
+            }
+        } catch (Exception $e) {
+            $this->error($e->getMessage());
+        }
+
+        $tableList       = get_table_list();
+        $tableExist      = array_key_exists(Helper::getTableName($table), $tableList);
+        $controllerExist = file_exists(root_path() . $controllerFile);
+
+        if ($controllerExist || $tableExist) {
+            $this->error('', [
+                'table'      => $tableExist,
+                'controller' => $controllerExist,
+            ], -1);
+        }
+        $this->success();
+    }
+
+    public function databaseList()
+    {
+        $tablePrefix     = config('database.connections.mysql.prefix');
+        $outExcludeTable = [
+            // 功能表
+            'area',
+            'token',
+            'captcha',
+            'admin_group_access',
+            'config',
+            'admin_log',
+            // 不建议生成crud的表
+            'user_money_log',
+            'user_score_log',
+        ];
+
+        $outTables = [];
+        $tables    = get_table_list();
+        $pattern   = '/^' . $tablePrefix . '/i';
+        foreach ($tables as $table => $tableComment) {
+            $table = preg_replace($pattern, '', $table);
+            if (!in_array($table, $outExcludeTable)) {
+                $outTables[$table] = $tableComment;
+            }
+        }
+        $this->success('', [
+            'dbs' => $outTables,
+        ]);
+    }
+
+    /**
+     * 关联表数据解析
+     * @param $field
+     * @throws Exception
+     */
+    private function parseJoinData($field)
+    {
+        $dictEn   = [];
+        $dictZhCn = [];
+
+        if ($field['form']['relation-fields'] && $field['form']['remote-table']) {
+            $columns        = Helper::parseTableColumns($field['form']['remote-table'], true);
+            $relationFields = explode(',', $field['form']['relation-fields']);
+            $tableName      = Helper::getTableName($field['form']['remote-table'], false);
+            $relationMethod = parse_name($tableName, 1, false);
+
+            // 建立关联模型代码文件
+            if (!$field['form']['remote-model'] || !file_exists(root_path() . $field['form']['remote-model'])) {
+                $joinModelFile = Helper::parseNameData('admin', $tableName, '', 'model', $field['form']['remote-model']);
+                if (!file_exists(root_path() . $joinModelFile['rootFileName'])) {
+                    $joinModelData['append']             = [];
+                    $joinModelData['methods']            = [];
+                    $joinModelData['fieldType']          = [];
+                    $joinModelData['createTime']         = '';
+                    $joinModelData['updateTime']         = '';
+                    $joinModelData['beforeInsertMixins'] = [];
+                    $joinModelData['beforeInsert']       = '';
+                    $joinModelData['afterInsert']        = '';
+                    $joinModelData['name']               = $tableName;
+                    $joinModelData['className']          = $joinModelFile['lastName'];
+                    $joinModelData['namespace']          = $joinModelFile['namespace'];
+                    $joinTablePk                         = 'id';
+                    $joinFieldsMap                       = [];
+                    foreach ($columns as $column) {
+                        $joinFieldsMap[$column['name']] = $column['designType'];
+                        $this->parseModelMethods($column, $joinModelData);
+                        if ($column['primaryKey']) $joinTablePk = $column['name'];
+                    }
+                    $weighKey = array_search('weigh', $joinFieldsMap);
+                    if ($weighKey !== false) {
+                        $joinModelData['afterInsert'] = Helper::assembleStub('mixins/model/afterInsert', [
+                            'field' => $joinFieldsMap[$weighKey]
+                        ]);
+                    }
+                    Helper::writeModelFile($joinTablePk, $joinFieldsMap, $joinModelData, $joinModelFile);
+                }
+                $field['form']['remote-model'] = $joinModelFile['rootFileName'];
+            }
+
+            if ($field['designType'] == 'remoteSelect') {
+                // 关联预载入方法
+                $this->controllerData['attr']['withJoinTable'][$tableName] = $relationMethod;
+
+                // 模型方法代码
+                $relationData                                      = [
+                    'relationMethod'     => $relationMethod,
+                    'relationMode'       => 'belongsTo',
+                    'relationPrimaryKey' => $field['form']['remote-pk'] ?? 'id',
+                    'relationForeignKey' => $field['name'],
+                    'relationClassName'  => str_replace(['.php', '/'], ['', '\\'], '\\' . $field['form']['remote-model']) . "::class",
+                ];
+                $this->modelData['relationMethodList'][$tableName] = Helper::assembleStub('mixins/model/belongsTo', $relationData);
+
+                // 查询时显示的字段
+                if ($relationFields) {
+                    $this->controllerData['relationVisibleFieldList'][$relationData['relationMethod']] = $relationFields;
+                }
+            } elseif ($field['designType'] == 'remoteSelects') {
+                $this->modelData['append'][]  = parse_name($tableName, 1, false);
+                $this->modelData['methods'][] = Helper::assembleStub('mixins/model/getters/remoteSelectLabels', [
+                    'field'          => parse_name($tableName, 1),
+                    'className'      => str_replace(['.php', '/'], ['', '\\'], '\\' . $field['form']['remote-model']),
+                    'primaryKey'     => $field['form']['remote-pk'] ?? 'id',
+                    'foreignKey'     => $field['name'],
+                    'labelFieldName' => $field['form']['remote-field'] ?? 'name',
+                ]);
+            }
+
+            foreach ($relationFields as $relationField) {
+                $relationFieldPrefix     = $relationMethod . '.';
+                $relationFieldLangPrefix = strtolower($tableName) . '__';
+                if (array_key_exists($relationField, $columns)) {
+                    Helper::getDictData($dictEn, $columns[$relationField], 'en', $relationFieldLangPrefix);
+                    Helper::getDictData($dictZhCn, $columns[$relationField], 'zh-cn', $relationFieldLangPrefix);
+                }
+
+                // 不允许双击编辑的字段
+                if ($columns[$relationField]['designType'] == 'switch') {
+                    $this->indexVueData['dblClickNotEditColumn'][] = $field['name'];
+                }
+
+                // 列字典数据
+                $columnDict = $this->getColumnDict($columns[$relationField], $relationFieldLangPrefix);
+
+                // 表格列
+                $columns[$relationField]['table']['render']   = 'tags';
+                $columns[$relationField]['table']['operator'] = 'LIKE';
+                $columns[$relationField]['designType']        = $field['designType'];
+                $this->indexVueData['tableColumn'][]          = $this->getTableColumn($columns[$relationField], $columnDict, $relationFieldPrefix, $relationFieldLangPrefix);
+            }
+        }
+        $this->langTsData['en']    = array_merge($this->langTsData['en'], $dictEn);
+        $this->langTsData['zh-cn'] = array_merge($this->langTsData['zh-cn'], $dictZhCn);
+    }
+
+    /**
+     * 解析模型方法(设置器、获取器等)
+     */
+    private function parseModelMethods($field, &$modelData)
+    {
+        // fieldType
+        if ($field['designType'] == 'array') {
+            $modelData['fieldType'][$field['name']] = 'json';
+        } elseif (!in_array($field['name'], ['create_time', 'update_time', 'updatetime', 'createtime']) && $field['designType'] == 'datetime' && (in_array($field['type'], ['int', 'bigint']))) {
+            $modelData['fieldType'][$field['name']] = 'timestamp:Y-m-d H:i:s';
+        }
+
+        // beforeInsertMixins
+        if ($field['designType'] == 'spk') {
+            $modelData['beforeInsertMixins']['snowflake'] = Helper::assembleStub('mixins/model/mixins/beforeInsertWithSnowflake', []);
+        }
+
+        // methods
+        $fieldName = parse_name($field['name'], 1);
+        if (in_array($field['designType'], $this->dtStringToArray)) {
+            $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/stringToArray', [
+                'field' => $fieldName
+            ]);
+            $modelData['methods'][] = Helper::assembleStub('mixins/model/setters/arrayToString', [
+                'field' => $fieldName
+            ]);
+        } elseif ($field['designType'] == 'array') {
+            $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/jsonDecode', [
+                'field' => $fieldName
+            ]);
+        } elseif ($field['designType'] == 'time') {
+            $modelData['methods'][] = Helper::assembleStub('mixins/model/setters/time', [
+                'field' => $fieldName
+            ]);
+        } elseif ($field['designType'] == 'editor') {
+            $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/htmlDecode', [
+                'field' => $fieldName
+            ]);
+        } elseif ($field['designType'] == 'spk') {
+            $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/string', [
+                'field' => $fieldName
+            ]);
+        } elseif ($field['originalDesignType'] == 'float') {
+            $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/float', [
+                'field' => $fieldName
+            ]);
+        }
+
+        if ($field['designType'] == 'city') {
+            $modelData['append'][]  = $field['name'] . '_text';
+            $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/cityNames', [
+                'field'             => $fieldName . 'Text',
+                'originalFieldName' => $field['name'],
+            ]);
+        }
+    }
+
+    /**
+     * 控制器/模型等文件的一些杂项属性解析
+     */
+    private function parseSundryData($field, $table)
+    {
+        if ($field['designType'] == 'editor') {
+            $this->formVueData['bigDialog']     = 'true'; // form 使用较宽的 Dialog
+            $this->controllerData['filterRule'] = "\n" . Helper::tab(2) . '$this->request->filter(\'trim,htmlspecialchars\');';// 修改变量过滤规则
+        }
+
+        // 默认排序字段
+        if ($table['defaultSortField'] && $table['defaultSortType']) {
+            $defaultSortField = "{$table['defaultSortField']},{$table['defaultSortType']}";
+            if ($defaultSortField == 'id,desc') {
+                $this->controllerData['attr']['defaultSortField'] = '';
+            } else {
+                $this->controllerData['attr']['defaultSortField'] = $defaultSortField;
+                $this->indexVueData['defaultOrder']               = Helper::buildDefaultOrder($table['defaultSortField'], $table['defaultSortType']);
+            }
+        }
+    }
+
+    private function getFormField($field, $columnDict): array
+    {
+        // 表单项属性
+        $formField = [
+            ':label'  => 't(\'' . $this->webTranslate . $field['name'] . '\')',
+            'type'    => $field['designType'],
+            'v-model' => 'baTable.form.items!.' . $field['name'],
+            'prop'    => $field['name'],
+        ];
+
+        // 不同输入框的属性处理
+        if ($columnDict || in_array($field['designType'], ['radio', 'checkbox', 'select', 'selects'])) {
+            $formField[':data'] = [
+                'content' => $columnDict,
+            ];
+        } elseif ($field['designType'] == 'textarea') {
+            $formField[':input-attr']['rows'] = (int)($field['form']['rows'] ?? 3);
+            $formField['@keyup.enter.stop']   = '';
+            $formField['@keyup.ctrl.enter']   = 'baTable.onSubmit(formRef)';
+        } elseif ($field['designType'] == 'remoteSelect' || $field['designType'] == 'remoteSelects') {
+            $formField[':input-attr']['pk']         = Helper::getTableName($field['form']['remote-table'], false) . '.' . ($field['form']['remote-pk'] ?? 'id');
+            $formField[':input-attr']['field']      = $field['form']['remote-field'] ?? 'name';
+            $formField[':input-attr']['remote-url'] = $this->getRemoteSelectUrl($field);
+            if ($field['designType'] == 'remoteSelects') {
+                $formField['type']                    = 'remoteSelect';
+                $formField[':input-attr']['multiple'] = 'true';
+            }
+        } elseif ($field['designType'] == 'number') {
+            $formField[':input-attr']['step'] = (int)($field['form']['step'] ?? 1);
+            $formField['v-model.number']      = $formField['v-model'];
+            unset($formField['v-model']);
+        } elseif ($field['designType'] == 'icon') {
+            $formField[':input-attr']['placement'] = 'top';
+        } elseif ($field['designType'] == 'editor') {
+            $formField['@keyup.enter.stop'] = '';
+            $formField['@keyup.ctrl.enter'] = 'baTable.onSubmit(formRef)';
+        }
+
+        // placeholder
+        if (!in_array($field['designType'], ['image', 'images', 'file', 'files', 'switch'])) {
+            if (in_array($field['designType'], ['radio', 'checkbox', 'datetime', 'year', 'date', 'time', 'select', 'selects', 'remoteSelect', 'remoteSelects', 'city', 'icon'])) {
+                $formField[':placeholder'] = "t('Please select field', { field: t('" . $this->webTranslate . $field['name'] . "') })";
+            } else {
+                $formField[':placeholder'] = "t('Please input field', { field: t('" . $this->webTranslate . $field['name'] . "') })";
+            }
+        }
+
+        // 默认值
+        if ($field['default'] && $field['default'] != 'empty string') {
+            $this->indexVueData['defaultItems'][$field['name']] = $field['default'];
+        }
+        if ($field['default'] == 'null') {
+            $this->indexVueData['defaultItems'][$field['name']] = null;
+        } elseif ($field['default'] == '0' && in_array($field['designType'], ['radio', 'checkbox', 'select', 'selects'])) {
+            // 防止为`0`时无法设置上默认值
+            $this->indexVueData['defaultItems'][$field['name']] = '0';
+        }
+        if ($field['designType'] == 'array') {
+            $this->indexVueData['defaultItems'][$field['name']] = "[]";
+        } elseif (in_array($field['designType'], $this->dtStringToArray) && stripos($field['default'], ',') !== false) {
+            $this->indexVueData['defaultItems'][$field['name']] = Helper::buildSimpleArray(explode(',', $field['default']));
+        } elseif (in_array($field['designType'], ['weigh', 'number', 'float'])) {
+            $this->indexVueData['defaultItems'][$field['name']] = (float)$field['default'];
+        }
+        return $formField;
+    }
+
+    private function getRemoteSelectUrl($field): string
+    {
+        if ($field['form']['remote-url']) return $field['form']['remote-url'];
+        $url = '';
+        if ($field['form']['remote-controller']) {
+            $pathArr      = [];
+            $controller   = explode(DIRECTORY_SEPARATOR, $field['form']['remote-controller']);
+            $controller   = str_replace('.php', '', $controller);
+            $redundantDir = [
+                'app'        => 0,
+                'admin'      => 1,
+                'controller' => 2,
+            ];
+            foreach ($controller as $key => $item) {
+                if (!array_key_exists($item, $redundantDir) || $key !== $redundantDir[$item]) {
+                    $pathArr[] = $item;
+                }
+            }
+            $url = count($pathArr) > 1 ? implode('.', $pathArr) : $pathArr[0];
+            $url = '/admin/' . $url . '/index';
+        }
+        return $url;
+    }
+
+    private function getTableColumn($field, $columnDict, $fieldNamePrefix = '', $translationPrefix = ''): array
+    {
+        $column = [
+            'label' => "t('" . $this->webTranslate . $translationPrefix . $field['name'] . "')",
+            'prop'  => $fieldNamePrefix . $field['name'] . ($field['designType'] == 'city' ? '_text' : ''),
+            'align' => 'center',
+        ];
+
+        // 模糊搜索增加一个placeholder
+        if (isset($field['table']['operator']) && $field['table']['operator'] == 'LIKE') {
+            $column['operatorPlaceholder'] = "t('Fuzzy query')";
+        }
+
+        // 合并前端预设的字段表格属性
+        if (isset($field['table']) && $field['table']) {
+            $column = array_merge($column, $field['table']);
+        }
+
+        // 需要值替换的渲染类型
+        $columnReplaceValue = ['tag', 'tags', 'switch'];
+        if (!in_array($field['designType'], ['remoteSelect', 'remoteSelects']) && ($columnDict || (isset($field['table']['render']) && in_array($field['table']['render'], $columnReplaceValue)))) {
+            $column['replaceValue'] = $columnDict;
+        }
+
+        if (isset($column['render']) && $column['render'] == 'none') {
+            unset($column['render']);
+        }
+        return $column;
+    }
+
+    private function getColumnDict($column, $translationPrefix = ''): array
+    {
+        $dict = [];
+        // 确保字典中无翻译也可以识别到该值
+        if (in_array($column['type'], ['enum', 'set'])) {
+            $dataType   = str_replace(' ', '', $column['dataType']);
+            $columnData = substr($dataType, stripos($dataType, '(') + 1, -1);
+            $columnData = explode(',', str_replace(["'", '"'], '', $columnData));
+            foreach ($columnData as $columnDatum) {
+                $dict[$columnDatum] = $column['name'] . ' ' . $columnDatum;
+            }
+        }
+        $dictData = [];
+        Helper::getDictData($dictData, $column, 'zh-cn', $translationPrefix);
+        if ($dictData) {
+            unset($dictData[$translationPrefix . $column['name']]);
+            foreach ($dictData as $key => $item) {
+                $keyName        = str_replace($translationPrefix . $column['name'] . ' ', '', $key);
+                $dict[$keyName] = "t('" . $this->webTranslate . $key . "')";
+            }
+        }
+        return $dict;
+    }
+}

+ 36 - 0
app/admin/controller/crud/Log.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace app\admin\controller\crud;
+
+use app\admin\model\CrudLog;
+use app\common\controller\Backend;
+
+/**
+ * crud记录
+ *
+ */
+class Log extends Backend
+{
+    /**
+     * Log模型对象
+     * @var CrudLog
+     */
+    protected $model = null;
+
+    protected $preExcludeFields = ['id', 'create_time'];
+
+    protected $quickSearchField = ['id', 'table_name'];
+
+    protected $noNeedPermission = ['index'];
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new CrudLog;
+
+        if (!$this->auth->check('crud/crud/index')) {
+            $this->error(__('You have no permission'), [], 401);
+        }
+    }
+
+}

+ 12 - 0
app/admin/controller/demo/NeoHome.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace app\admin\controller\demo;
+
+use app\common\controller\Backend;
+
+class NeoHome extends Backend
+{
+    public function index()
+    {
+    }
+}

+ 16 - 0
app/admin/controller/demo/NeoVideo.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace app\admin\controller\demo;
+
+use app\common\controller\Backend;
+
+class NeoVideo extends Backend
+{
+    public function index()
+    {
+        $this->success('', [
+            'remark' => get_route_remark()
+        ]);
+
+    }
+}

+ 87 - 0
app/admin/controller/routine/AdminInfo.php

@@ -0,0 +1,87 @@
+<?php
+
+namespace app\admin\controller\routine;
+
+use Exception;
+use app\common\controller\Backend;
+use think\db\exception\PDOException;
+use think\exception\ValidateException;
+use think\facade\Db;
+
+class AdminInfo extends Backend
+{
+    protected $model = null;
+
+    // 排除字段
+    protected $preExcludeFields = ['username', 'lastlogintime', 'password', 'salt', 'status'];
+    // 输出字段
+    protected $authAllowFields = ['id', 'username', 'nickname', 'avatar', 'email', 'mobile', 'motto', 'lastlogintime'];
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->auth->setAllowFields($this->authAllowFields);
+        $this->model = $this->auth->getAdmin();
+    }
+
+    public function index()
+    {
+        $info = $this->auth->getInfo();
+        $this->success('', [
+            'info' => $info
+        ]);
+    }
+
+    public function edit($id = null)
+    {
+        $row = $this->model->find($id);
+        if (!$row) {
+            $this->error(__('Record not found'));
+        }
+
+        if ($this->request->isPost()) {
+            $data = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            if (isset($data['avatar']) && $data['avatar']) {
+                $row->avatar = $data['avatar'];
+                if ($row->save()) {
+                    $this->success(__('Avatar modified successfully!'));
+                }
+            }
+
+            // 数据验证
+            if ($this->modelValidate) {
+                try {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    $validate = new $validate;
+                    $validate->scene('info')->check($data);
+                } catch (ValidateException $e) {
+                    $this->error($e->getMessage());
+                }
+            }
+
+            if (isset($data['password']) && $data['password']) {
+                $this->model->resetPassword($this->auth->id, $data['password']);
+            }
+
+            $data   = $this->excludeFields($data);
+            $result = false;
+            Db::startTrans();
+            try {
+                $result = $row->save($data);
+                Db::commit();
+            } catch (PDOException|Exception $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('Update successful'));
+            } else {
+                $this->error(__('No rows updated'));
+            }
+        }
+    }
+}

+ 64 - 0
app/admin/controller/routine/Attachment.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace app\admin\controller\routine;
+
+use Exception;
+use think\facade\Event;
+use app\common\controller\Backend;
+use app\common\model\Attachment as AttachmentModel;
+use think\db\exception\PDOException;
+
+class Attachment extends Backend
+{
+    protected $model = null;
+
+    protected $quickSearchField = 'name';
+
+    protected $withJoinTable = ['admin', 'user'];
+
+    protected $defaultSortField = 'lastuploadtime,desc';
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new AttachmentModel();
+    }
+
+    /**
+     * 删除
+     * @param array $ids
+     */
+    public function del(array $ids = [])
+    {
+        if (!$this->request->isDelete() || !$ids) {
+            $this->error(__('Parameter error'));
+        }
+
+        $dataLimitAdminIds = $this->getDataLimitAdminIds();
+        if ($dataLimitAdminIds) {
+            $this->model->where($this->dataLimitField, 'in', $dataLimitAdminIds);
+        }
+
+        $pk    = $this->model->getPk();
+        $data  = $this->model->where($pk, 'in', $ids)->select();
+        $count = 0;
+        try {
+            foreach ($data as $v) {
+                Event::trigger('AttachmentDel', $v);
+                $filePath = path_transform(public_path() . ltrim($v->url, '/'));
+                if (file_exists($filePath)) {
+                    unlink($filePath);
+                    del_empty_dir(dirname($filePath));
+                }
+                $count += $v->delete();
+            }
+        } catch (PDOException|Exception $e) {
+            $this->error(__('%d records and files have been deleted', [$count]) . $e->getMessage());
+        }
+        if ($count) {
+            $this->success(__('%d records and files have been deleted', [$count]));
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+}

+ 176 - 0
app/admin/controller/routine/Config.php

@@ -0,0 +1,176 @@
+<?php
+
+namespace app\admin\controller\routine;
+
+use app\common\controller\Backend;
+use app\admin\model\Config as ConfigModel;
+use think\db\exception\PDOException;
+use think\exception\ValidateException;
+use think\facade\Cache;
+use think\facade\Db;
+use Exception;
+use app\common\library\Email;
+use PHPMailer\PHPMailer\PHPMailer;
+use PHPMailer\PHPMailer\Exception as PHPMailerException;
+
+class Config extends Backend
+{
+    protected $model = null;
+
+    protected $noNeedLogin = ['index'];
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new ConfigModel();
+    }
+
+    public function index()
+    {
+        $configGroup = get_sys_config('config_group');
+        $config      = $this->model->order('weigh desc')->select()->toArray();
+        $list        = [];
+        foreach ($config as $item) {
+            $item['title']                  = __($item['title']);
+            $list[$item['group']]['list'][] = $item;
+        }
+        foreach ($configGroup as $item) {
+            $list[$item['key']]['name']  = $item['key'];
+            $list[$item['key']]['title'] = __($item['value']);
+
+            $newConfigGroup[$item['key']] = $list[$item['key']]['title'];
+        }
+
+        $this->success('', [
+            'list'          => $list,
+            'remark'        => get_route_remark(),
+            'configGroup'   => $newConfigGroup ?? [],
+            'quickEntrance' => get_sys_config('config_quick_entrance'),
+        ]);
+    }
+
+    /**
+     * 编辑
+     * @param null $id
+     */
+    public function edit($id = null)
+    {
+        $all = $this->model->select();
+        foreach ($all as $item) {
+            if ($item['type'] == 'editor') {
+                $this->request->filter('trim,htmlspecialchars');
+                break;
+            }
+        }
+        if ($this->request->isPost()) {
+            $this->modelValidate = false;
+            $data                = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            $data = $this->excludeFields($data);
+
+            $configValue = [];
+            foreach ($all as $item) {
+                if (array_key_exists($item->name, $data)) {
+                    $configValue[] = [
+                        'id'    => $item->id,
+                        'type'  => $item->getData('type'),
+                        'value' => $data[$item->name]
+                    ];
+                }
+            }
+
+            $result = false;
+            Db::startTrans();
+            try {
+                // 模型验证
+                if ($this->modelValidate) {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    if (class_exists($validate)) {
+                        $validate = new $validate;
+                        if ($this->modelSceneValidate) $validate->scene('edit');
+                        $validate->check($data);
+                    }
+                }
+                $result = $this->model->saveAll($configValue);
+                Cache::tag(ConfigModel::$cacheTag)->clear();
+                Db::commit();
+            } catch (ValidateException|Exception|PDOException $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('The current page configuration item was updated successfully'));
+            } else {
+                $this->error(__('No rows updated'));
+            }
+
+        }
+    }
+
+    public function add()
+    {
+        if ($this->request->isPost()) {
+            $data = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            $data   = $this->excludeFields($data);
+            $result = false;
+            Db::startTrans();
+            try {
+                // 模型验证
+                if ($this->modelValidate) {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    if (class_exists($validate)) {
+                        $validate = new $validate;
+                        if ($this->modelSceneValidate) $validate->scene('add');
+                        $validate->check($data);
+                    }
+                }
+                $result = $this->model->save($data);
+                Cache::tag(ConfigModel::$cacheTag)->clear();
+                Db::commit();
+            } catch (ValidateException|Exception|PDOException $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('Added successfully'));
+            } else {
+                $this->error(__('No rows were added'));
+            }
+        }
+
+        $this->error(__('Parameter error'));
+    }
+
+    public function sendTestMail()
+    {
+        $data = $this->request->post();
+        $mail = new Email();
+        try {
+            $mail->Host       = $data['smtp_server'];
+            $mail->SMTPAuth   = true;
+            $mail->Username   = $data['smtp_user'];
+            $mail->Password   = $data['smtp_pass'];
+            $mail->SMTPSecure = $data['smtp_verification'] == 'SSL' ? PHPMailer::ENCRYPTION_SMTPS : PHPMailer::ENCRYPTION_STARTTLS;
+            $mail->Port       = $data['smtp_port'];
+
+            $mail->setFrom($data['smtp_sender_mail'], $data['smtp_user']);
+
+            $mail->isSMTP();
+            $mail->addAddress($data['testMail']);
+            $mail->isHTML();
+            $mail->setSubject(__('This is a test email') . '-' . get_sys_config('site_name'));
+            $mail->Body = __('Congratulations, receiving this email means that your email service has been configured correctly');
+            $mail->send();
+        } catch (PHPMailerException $e) {
+            $this->error($mail->ErrorInfo);
+        }
+        $this->success(__('Test mail sent successfully~'));
+    }
+}

+ 173 - 0
app/admin/controller/security/DataRecycle.php

@@ -0,0 +1,173 @@
+<?php
+
+namespace app\admin\controller\security;
+
+use app\common\controller\Backend;
+use app\admin\model\DataRecycle as DataRecycleModel;
+use think\db\exception\PDOException;
+use think\exception\ValidateException;
+use think\facade\Db;
+use Exception;
+
+class DataRecycle extends Backend
+{
+    protected $model = null;
+
+    // 排除字段
+    protected $preExcludeFields = ['updatetime', 'createtime'];
+
+    protected $quickSearchField = 'name';
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new DataRecycleModel();
+    }
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if ($this->request->isPost()) {
+            $data = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            $data                  = $this->excludeFields($data);
+            $data['controller_as'] = str_ireplace('.php', '', $data['controller'] ?? '');
+            $data['controller_as'] = strtolower(str_ireplace(['\\', '.'], '/', $data['controller_as']));
+
+            $result = false;
+            Db::startTrans();
+            try {
+                // 模型验证
+                if ($this->modelValidate) {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    if (class_exists($validate)) {
+                        $validate = new $validate;
+                        if ($this->modelSceneValidate) $validate->scene('add');
+                        $validate->check($data);
+                    }
+                }
+                $result = $this->model->save($data);
+                Db::commit();
+            } catch (ValidateException|Exception|PDOException $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('Added successfully'));
+            } else {
+                $this->error(__('No rows were added'));
+            }
+        }
+
+        // 放在add方法内,就不需要额外添加权限节点了
+        $this->success('', [
+            'tables'      => $this->getTableList(),
+            'controllers' => $this->getControllerList(),
+        ]);
+    }
+
+    /**
+     * 编辑
+     * @param null $id
+     */
+    public function edit($id = null)
+    {
+        $row = $this->model->find($id);
+        if (!$row) {
+            $this->error(__('Record not found'));
+        }
+
+        if ($this->request->isPost()) {
+            $data = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            $data                  = $this->excludeFields($data);
+            $data['controller_as'] = str_ireplace('.php', '', $data['controller'] ?? '');
+            $data['controller_as'] = strtolower(str_ireplace(['\\', '.'], '/', $data['controller_as']));
+
+            $result = false;
+            Db::startTrans();
+            try {
+                // 模型验证
+                if ($this->modelValidate) {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    if (class_exists($validate)) {
+                        $validate = new $validate;
+                        if ($this->modelSceneValidate) $validate->scene('edit');
+                        $validate->check($data);
+                    }
+                }
+                $result = $row->save($data);
+                Db::commit();
+            } catch (ValidateException|Exception|PDOException $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('Update successful'));
+            } else {
+                $this->error(__('No rows updated'));
+            }
+        }
+
+        $this->success('', [
+            'row' => $row
+        ]);
+    }
+
+    protected function getControllerList()
+    {
+        $outExcludeController = [
+            'Addon.php',
+            'Ajax.php',
+            'Module.php',
+            'Terminal.php',
+            'Dashboard.php',
+            'Index.php',
+            'routine/AdminInfo.php',
+            'user/MoneyLog.php',
+            'user/ScoreLog.php',
+        ];
+        $outControllers       = [];
+        $controllers          = get_controller_list();
+        foreach ($controllers as $key => $controller) {
+            if (!in_array($controller, $outExcludeController)) {
+                $outControllers[$key] = $controller;
+            }
+        }
+        return $outControllers;
+    }
+
+    protected function getTableList()
+    {
+        $tablePrefix     = config('database.connections.mysql.prefix');
+        $outExcludeTable = [
+            // 功能表
+            'area',
+            'token',
+            'captcha',
+            'admin_group_access',
+            // 无删除功能
+            'user_money_log',
+            'user_score_log',
+        ];
+
+        $outTables = [];
+        $tables    = get_table_list();
+        $pattern   = '/^' . $tablePrefix . '/i';
+        foreach ($tables as $table => $tableComment) {
+            $table = preg_replace($pattern, '', $table);
+            if (!in_array($table, $outExcludeTable)) {
+                $outTables[$table] = $tableComment;
+            }
+        }
+        return $outTables;
+    }
+}

+ 91 - 0
app/admin/controller/security/DataRecycleLog.php

@@ -0,0 +1,91 @@
+<?php
+
+namespace app\admin\controller\security;
+
+use app\common\controller\Backend;
+use app\admin\model\DataRecycleLog as DataRecycleLogModel;
+use think\db\exception\PDOException;
+use Exception;
+use think\facade\Db;
+
+class DataRecycleLog extends Backend
+{
+    protected $model = null;
+
+    // 排除字段
+    protected $preExcludeFields = [];
+
+    protected $quickSearchField = 'recycle.name';
+
+    protected $withJoinTable = ['recycle', 'admin'];
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new DataRecycleLogModel();
+    }
+
+    public function restore($ids = null)
+    {
+        $data = $this->model->where('id', 'in', $ids)->select();
+        if (!$data) {
+            $this->error(__('Record not found'));
+        }
+
+        $count = 0;
+        Db::startTrans();
+        try {
+            foreach ($data as $row) {
+                $recycleData = json_decode($row->data, true);
+                if (is_array($recycleData) && Db::name($row->data_table)->insert($recycleData)) {
+                    $row->delete();
+                    $count++;
+                }
+            }
+            Db::commit();
+        } catch (PDOException|Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+
+        if ($count) {
+            $this->success();
+        } else {
+            $this->error(__('No rows were restore'));
+        }
+    }
+
+    public function info($id = null)
+    {
+        $row = $this->model
+            ->withJoin($this->withJoinTable, $this->withJoinType)
+            ->where('data_recycle_log.id', $id)
+            ->find();
+        if (!$row) {
+            $this->error(__('Record not found'));
+        }
+        $data = $this->jsonToArray($row->data);
+        if (is_array($data)) {
+            foreach ($data as $key => $item) {
+                $data[$key] = $this->jsonToArray($item);
+            }
+        }
+        $row->data = $data;
+
+        $this->success('', [
+            'row' => $row
+        ]);
+    }
+
+    protected function jsonToArray($value = '')
+    {
+        if (!is_string($value)) {
+            return $value;
+        }
+        $data = json_decode($value, true);
+        if (($data && is_object($data)) || (is_array($data) && !empty($data))) {
+            return $data;
+        }
+        return $value;
+    }
+}

+ 230 - 0
app/admin/controller/security/SensitiveData.php

@@ -0,0 +1,230 @@
+<?php
+
+namespace app\admin\controller\security;
+
+use app\common\controller\Backend;
+use app\admin\model\SensitiveData as SensitiveDataModel;
+use think\db\exception\PDOException;
+use think\exception\ValidateException;
+use think\facade\Db;
+use Exception;
+
+class SensitiveData extends Backend
+{
+    protected $model = null;
+
+    // 排除字段
+    protected $preExcludeFields = ['updatetime', 'createtime'];
+
+    protected $quickSearchField = 'controller';
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new SensitiveDataModel();
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        $this->request->filter(['strip_tags', 'trim']);
+        if ($this->request->param('select')) {
+            $this->select();
+        }
+
+        list($where, $alias, $limit, $order) = $this->queryBuilder();
+        $res = $this->model
+            ->withJoin($this->withJoinTable, $this->withJoinType)
+            ->alias($alias)
+            ->where($where)
+            ->order($order)
+            ->paginate($limit);
+
+        foreach ($res->items() as $item) {
+            if ($item->data_fields) {
+                $fields = [];
+                foreach ($item->data_fields as $key => $field) {
+                    $fields[] = $field ?: $key;
+                }
+                $item->data_fields = $fields;
+            }
+        }
+
+        $this->success('', [
+            'list'   => $res->items(),
+            'total'  => $res->total(),
+            'remark' => get_route_remark(),
+        ]);
+    }
+
+    /**
+     * 添加重写
+     */
+    public function add()
+    {
+        if ($this->request->isPost()) {
+            $data = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            $data                  = $this->excludeFields($data);
+            $data['controller_as'] = str_ireplace('.php', '', $data['controller'] ?? '');
+            $data['controller_as'] = strtolower(str_ireplace(['\\', '.'], '/', $data['controller_as']));
+
+            $result = false;
+            Db::startTrans();
+            try {
+                // 模型验证
+                if ($this->modelValidate) {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    if (class_exists($validate)) {
+                        $validate = new $validate;
+                        if ($this->modelSceneValidate) $validate->scene('add');
+                        $validate->check($data);
+                    }
+                }
+
+                if (is_array($data['fields'])) {
+                    $data['data_fields'] = [];
+                    foreach ($data['fields'] as $field) {
+                        $data['data_fields'][$field['name']] = $field['value'];
+                    }
+                }
+
+                $result = $this->model->save($data);
+                Db::commit();
+            } catch (ValidateException|Exception|PDOException $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('Added successfully'));
+            } else {
+                $this->error(__('No rows were added'));
+            }
+        }
+
+        // 放在add方法内,就不需要额外添加权限节点了
+        $this->success('', [
+            'tables'      => $this->getTableList(),
+            'controllers' => $this->getControllerList(),
+        ]);
+    }
+
+    /**
+     * 编辑重写
+     * @param null $id
+     */
+    public function edit($id = null)
+    {
+        $row = $this->model->find($id);
+        if (!$row) {
+            $this->error(__('Record not found'));
+        }
+
+        if ($this->request->isPost()) {
+            $data = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            $data                  = $this->excludeFields($data);
+            $data['controller_as'] = str_ireplace('.php', '', $data['controller'] ?? '');
+            $data['controller_as'] = strtolower(str_ireplace(['\\', '.'], '/', $data['controller_as']));
+
+            $result = false;
+            Db::startTrans();
+            try {
+                // 模型验证
+                if ($this->modelValidate) {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    if (class_exists($validate)) {
+                        $validate = new $validate;
+                        if ($this->modelSceneValidate) $validate->scene('edit');
+                        $validate->check($data);
+                    }
+                }
+
+                if (is_array($data['fields'])) {
+                    $data['data_fields'] = [];
+                    foreach ($data['fields'] as $field) {
+                        $data['data_fields'][$field['name']] = $field['value'];
+                    }
+                }
+
+                $result = $row->save($data);
+                Db::commit();
+            } catch (ValidateException|Exception|PDOException $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('Update successful'));
+            } else {
+                $this->error(__('No rows updated'));
+            }
+        }
+
+        $this->success('', [
+            'row'         => $row,
+            'tables'      => $this->getTableList(),
+            'controllers' => $this->getControllerList(),
+        ]);
+    }
+
+    protected function getControllerList()
+    {
+        $outExcludeController = [
+            'Addon.php',
+            'Ajax.php',
+            'Dashboard.php',
+            'Index.php',
+            'Module.php',
+            'Terminal.php',
+            'auth/AdminLog.php',
+            'routine/AdminInfo.php',
+            'routine/Config.php',
+            'user/MoneyLog.php',
+            'user/ScoreLog.php',
+        ];
+        $outControllers       = [];
+        $controllers          = get_controller_list();
+        foreach ($controllers as $key => $controller) {
+            if (!in_array($controller, $outExcludeController)) {
+                $outControllers[$key] = $controller;
+            }
+        }
+        return $outControllers;
+    }
+
+    protected function getTableList()
+    {
+        $tablePrefix     = config('database.connections.mysql.prefix');
+        $outExcludeTable = [
+            // 功能表
+            'area',
+            'token',
+            'captcha',
+            'admin_group_access',
+            'config',
+            // 无编辑功能
+            'admin_log',
+            'user_money_log',
+            'user_score_log',
+        ];
+
+        $outTables = [];
+        $tables    = get_table_list();
+        $pattern   = '/^' . $tablePrefix . '/i';
+        foreach ($tables as $table => $tableComment) {
+            $table = preg_replace($pattern, '', $table);
+            if (!in_array($table, $outExcludeTable)) {
+                $outTables[$table] = $tableComment;
+            }
+        }
+        return $outTables;
+    }
+}

+ 102 - 0
app/admin/controller/security/SensitiveDataLog.php

@@ -0,0 +1,102 @@
+<?php
+
+namespace app\admin\controller\security;
+
+use app\common\controller\Backend;
+use app\admin\model\SensitiveDataLog as SensitiveDataLogModel;
+use think\db\exception\PDOException;
+use think\facade\Db;
+use Exception;
+
+class SensitiveDataLog extends Backend
+{
+    protected $model = null;
+
+    // 排除字段
+    protected $preExcludeFields = [];
+
+    protected $quickSearchField = 'sensitive.name';
+
+    protected $withJoinTable = ['sensitive', 'admin'];
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new SensitiveDataLogModel();
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        $this->request->filter(['strip_tags', 'trim']);
+        if ($this->request->param('select')) {
+            $this->select();
+        }
+
+        list($where, $alias, $limit, $order) = $this->queryBuilder();
+        $res = $this->model
+            ->withJoin($this->withJoinTable, $this->withJoinType)
+            ->alias($alias)
+            ->where($where)
+            ->order($order)
+            ->paginate($limit);
+
+        foreach ($res->items() as $item) {
+            $item->id_value = $item['primary_key'] . '=' . $item->id_value;
+        }
+
+        $this->success('', [
+            'list'   => $res->items(),
+            'total'  => $res->total(),
+            'remark' => get_route_remark(),
+        ]);
+    }
+
+    public function info($id = null)
+    {
+        $row = $this->model
+            ->withJoin($this->withJoinTable, $this->withJoinType)
+            ->where('sensitive_data_log.id', $id)
+            ->find();
+        if (!$row) {
+            $this->error(__('Record not found'));
+        }
+
+        $this->success('', [
+            'row' => $row
+        ]);
+    }
+
+    public function rollback($ids = null)
+    {
+        $data = $this->model->where('id', 'in', $ids)->select();
+        if (!$data) {
+            $this->error(__('Record not found'));
+        }
+
+        $count = 0;
+        Db::startTrans();
+        try {
+            foreach ($data as $row) {
+                if (Db::name($row->data_table)->where($row->primary_key, $row->id_value)->update([
+                    $row->data_field => $row->before
+                ])) {
+                    $row->delete();
+                    $count++;
+                }
+            }
+            Db::commit();
+        } catch (PDOException|Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+
+        if ($count) {
+            $this->success();
+        } else {
+            $this->error(__('No rows were rollback'));
+        }
+    }
+}

+ 155 - 0
app/admin/controller/user/Group.php

@@ -0,0 +1,155 @@
+<?php
+
+namespace app\admin\controller\user;
+
+use Exception;
+use think\facade\Db;
+use app\admin\model\UserRule;
+use app\admin\model\UserGroup;
+use app\common\controller\Backend;
+use think\db\exception\PDOException;
+use think\exception\ValidateException;
+
+class Group extends Backend
+{
+    protected $model = null;
+
+    // 排除字段
+    protected $preExcludeFields = ['updatetime', 'createtime'];
+
+    protected $quickSearchField = 'name';
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new UserGroup();
+    }
+
+    public function add()
+    {
+        if ($this->request->isPost()) {
+            $data = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            $data = $this->excludeFields($data);
+            if (is_array($data['rules']) && $data['rules']) {
+                $rules = UserRule::select();
+                $super = true;
+                foreach ($rules as $rule) {
+                    if (!in_array($rule['id'], $data['rules'])) {
+                        $super = false;
+                    }
+                }
+
+                if ($super) {
+                    $data['rules'] = '*';
+                } else {
+                    $data['rules'] = implode(',', $data['rules']);
+                }
+            } else {
+                unset($data['rules']);
+            }
+
+            $result = false;
+            Db::startTrans();
+            try {
+                // 模型验证
+                if ($this->modelValidate) {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    if (class_exists($validate)) {
+                        $validate = new $validate;
+                        $validate->scene('add')->check($data);
+                    }
+                }
+                $result = $this->model->save($data);
+                Db::commit();
+            } catch (ValidateException|Exception|PDOException $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('Added successfully'));
+            } else {
+                $this->error(__('No rows were added'));
+            }
+        }
+
+        $this->error(__('Parameter error'));
+    }
+
+    public function edit($id = null)
+    {
+        $row = $this->model->find($id);
+        if (!$row) {
+            $this->error(__('Record not found'));
+        }
+
+        if ($this->request->isPost()) {
+            $data = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            $data = $this->excludeFields($data);
+            if (is_array($data['rules']) && $data['rules']) {
+                $rules = UserRule::select();
+                $super = true;
+                foreach ($rules as $rule) {
+                    if (!in_array($rule['id'], $data['rules'])) {
+                        $super = false;
+                    }
+                }
+
+                if ($super) {
+                    $data['rules'] = '*';
+                } else {
+                    $data['rules'] = implode(',', $data['rules']);
+                }
+            } else {
+                unset($data['rules']);
+            }
+
+            $result = false;
+            Db::startTrans();
+            try {
+                // 模型验证
+                if ($this->modelValidate) {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    if (class_exists($validate)) {
+                        $validate = new $validate;
+                        $validate->scene('edit')->check($data);
+                    }
+                }
+                $result = $row->save($data);
+                Db::commit();
+            } catch (ValidateException|Exception|PDOException $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('Update successful'));
+            } else {
+                $this->error(__('No rows updated'));
+            }
+        }
+
+        // 读取所有pid,全部从节点数组移除,父级选择状态由子级决定
+        $pids  = UserRule::field('pid')
+            ->distinct(true)
+            ->where('id', 'in', $row->rules)
+            ->select()->toArray();
+        $rules = $row->rules ? explode(',', $row->rules) : [];
+        foreach ($pids as $item) {
+            $ruKey = array_search($item['pid'], $rules);
+            if ($ruKey !== false) {
+                unset($rules[$ruKey]);
+            }
+        }
+        $row->rules = array_values($rules);
+        $this->success('', [
+            'row' => $row
+        ]);
+    }
+}

+ 109 - 0
app/admin/controller/user/Identity.php

@@ -0,0 +1,109 @@
+<?php
+
+namespace app\admin\controller\user;
+
+use Exception;
+use ba\Random;
+use think\facade\Db;
+use app\common\controller\Backend;
+use app\admin\model\User as UserModel;
+use think\db\exception\PDOException;
+use think\exception\ValidateException;
+
+class Identity extends Backend
+{
+    protected $model = null;
+    
+    protected $withJoinTable = ['group'];
+    
+    // 排除字段
+    protected $preExcludeFields = ['lastlogintime', 'loginfailure', 'password', 'salt', 'money', 'score'];
+    
+    protected $quickSearchField = ['username', 'nickname', 'id'];
+    
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new UserModel();
+    }
+    
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        $this->request->filter(['strip_tags', 'trim']);
+        // if ($this->request->param('select')) {
+        //     $this->select();
+        // }
+        
+        list($where, $alias, $limit, $order) = $this->queryBuilder();
+        $res = $this->model
+            ->withoutField('password,salt')
+            ->withJoin($this->withJoinTable, $this->withJoinType)
+            ->alias($alias)
+            ->where($where)
+            ->where(['real_name_status' => [0, 2]])
+            ->order($order)
+            ->paginate($limit);
+        
+        // var_dump($res);
+        $this->success('', [
+            'list'   => $res->items(),
+            'total'  => $res->total(),
+            'remark' => get_route_remark(),
+        ]);
+    }
+    
+    public function edit($id = null)
+    {
+        $row = $this->model->find($id);
+        if (!$row) {
+            $this->error(__('Record not found'));
+        }
+        
+        // 重构:不需要修改密码,所以不需要这个字段
+        // $row->password = '';
+        
+        if ($this->request->isPost()) {
+            $password = $this->request->post('password', '');
+            if ($password) {
+                $this->model->resetPassword($id, $password);
+            }
+            parent::edit();
+        }
+        
+        unset($row->salt);
+        $row->password = '';
+        $this->success('', [
+            'row' => $row
+        ]);
+    }
+    
+    /**
+     * 重写select
+     */
+    // public function select()
+    // {
+    //     $this->request->filter(['strip_tags', 'trim']);
+    //
+    //     list($where, $alias, $limit, $order) = $this->queryBuilder();
+    //     $res = $this->model
+    //         ->withJoin($this->withJoinTable, $this->withJoinType)
+    //         ->alias($alias)
+    //         // ->where($where)
+    //         ->where(['real_name_status' => 1])
+    //         ->order($order)
+    //         ->paginate($limit);
+    //     // var_dump($res);die;
+    //     foreach ($res as $re) {
+    //         $re->nickname_text = $re->username . '(ID:' . $re->id . ')';
+    //     }
+    //
+    //     $this->success('', [
+    //         'list'   => $res->items(),
+    //         'total'  => $res->total(),
+    //         'remark' => get_route_remark(),
+    //     ]);
+    // }
+}

+ 43 - 0
app/admin/controller/user/MoneyLog.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace app\admin\controller\user;
+
+use app\common\controller\Backend;
+use app\admin\model\UserMoneyLog;
+use app\admin\model\User;
+
+class MoneyLog extends Backend
+{
+    protected $model = null;
+
+    protected $withJoinTable = ['user'];
+
+    // 排除字段
+    protected $preExcludeFields = ['createtime'];
+
+    protected $quickSearchField = ['user.username', 'user.nickname'];
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new UserMoneyLog();
+    }
+
+    /**
+     * 添加
+     */
+    public function add($userId = 0)
+    {
+        if ($this->request->isPost()) {
+            parent::add();
+        }
+
+        $user = User::where('id', (int)$userId)->find();
+        if (!$user) {
+            $this->error(__("The user can't find it"));
+        }
+        $this->success('', [
+            'user' => $user
+        ]);
+    }
+}

+ 79 - 0
app/admin/controller/user/Pilot.php

@@ -0,0 +1,79 @@
+<?php
+
+namespace app\admin\controller\user;
+
+use app\common\controller\Backend;
+use app\admin\model\User as UserModel;
+
+class Pilot extends Backend
+{
+    protected $model = null;
+    
+    protected $withJoinTable = ['group'];
+    
+    // 排除字段
+    protected $preExcludeFields = ['lastlogintime', 'loginfailure', 'password', 'salt', 'money', 'score'];
+    
+    protected $quickSearchField = ['username', 'nickname', 'id'];
+    
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new UserModel();
+    }
+    
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        $this->request->filter(['strip_tags', 'trim']);
+        if ($this->request->param('select')) {
+            $this->select();
+        }
+        
+        list($where, $alias, $limit, $order) = $this->queryBuilder();
+        $res = $this->model
+            ->withoutField('password,salt')
+            ->withJoin($this->withJoinTable, $this->withJoinType)
+            ->alias($alias)
+            ->where($where)
+            ->where(['group_id' => 2, 'real_name_status' => 1, 'certified' => 1])
+            ->order($order)
+            ->paginate($limit);
+        
+        $this->success('', [
+            'list'   => $res->items(),
+            'total'  => $res->total(),
+            'remark' => get_route_remark(),
+        ]);
+    }
+    
+    /**
+     * 加载为select(远程下拉选择框)数据,默认还是走$this->index()方法
+     * 必要时请在对应控制器类中重写
+     */
+    public function select()
+    {
+        $this->request->filter(['strip_tags', 'trim']);
+        
+        list($where, $alias, $limit, $order) = $this->queryBuilder();
+        $res = $this->model
+            ->withJoin($this->withJoinTable, $this->withJoinType)
+            ->alias($alias)
+            ->where($where)
+            ->order($order)
+            ->paginate($limit);
+        
+        foreach ($res as $re) {
+            $re->nickname_text = $re->username . '(ID:' . $re->id . ')';
+        }
+        
+        $this->success('', [
+            'list'   => $res->items(),
+            'total'  => $res->total(),
+            'remark' => get_route_remark(),
+        ]);
+    }
+    
+}

+ 210 - 0
app/admin/controller/user/Rule.php

@@ -0,0 +1,210 @@
+<?php
+
+namespace app\admin\controller\user;
+
+use ba\Tree;
+use Exception;
+use think\facade\Db;
+use app\admin\model\UserRule;
+use app\common\controller\Backend;
+use think\db\exception\PDOException;
+use think\exception\ValidateException;
+
+class Rule extends Backend
+{
+    /**
+     * @var UserRule
+     */
+    protected $model = null;
+
+    /**
+     * @var Tree
+     */
+    protected $tree = null;
+
+    protected $noNeedLogin = ['index'];
+
+    protected $preExcludeFields = ['createtime', 'updatetime'];
+
+    protected $quickSearchField = 'title';
+
+    /**
+     * 远程select初始化传值
+     * @var array
+     */
+    protected $initValue;
+
+    /**
+     * 是否组装Tree
+     * @var bool
+     */
+    protected $assembleTree;
+
+    /**
+     * 搜索关键词
+     * @var array
+     */
+    protected $keyword = false;
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new UserRule();
+        $this->tree  = Tree::instance();
+
+        $isTree          = $this->request->param('isTree', true);
+        $this->initValue = $this->request->get("initValue/a", '');
+        $this->keyword   = $this->request->request("quick_search");
+
+        // 有初始化值时不组装树状(初始化出来的值更好看)
+        $this->assembleTree = $isTree && !$this->initValue;
+    }
+
+    public function index()
+    {
+        if ($this->request->param('select')) {
+            $this->select();
+        }
+
+        $this->success('', [
+            'list'   => $this->getRules(),
+            'remark' => get_route_remark(),
+        ]);
+    }
+
+    /**
+     * 编辑
+     */
+    public function edit()
+    {
+        $id  = $this->request->param($this->model->getPk());
+        $row = $this->model->find($id);
+        if (!$row) {
+            $this->error(__('Record not found'));
+        }
+
+        $dataLimitAdminIds = $this->getDataLimitAdminIds();
+        if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
+            $this->error(__('You have no permission'));
+        }
+
+        if ($this->request->isPost()) {
+            $data = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            $data   = $this->excludeFields($data);
+            $result = false;
+            Db::startTrans();
+            try {
+                // 模型验证
+                if ($this->modelValidate) {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    if (class_exists($validate)) {
+                        $validate = new $validate;
+                        if ($this->modelSceneValidate) $validate->scene('edit');
+                        $validate->check($data);
+                    }
+                }
+                if (isset($data['pid']) && $data['pid'] > 0) {
+                    // 满足意图并消除副作用
+                    $parent = $this->model->where('id', $data['pid'])->find();
+                    if ($parent['pid'] == $row['id']) {
+                        $parent->pid = 0;
+                        $parent->save();
+                    }
+                }
+                $result = $row->save($data);
+                Db::commit();
+            } catch (ValidateException|Exception|PDOException $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('Update successful'));
+            } else {
+                $this->error(__('No rows updated'));
+            }
+
+        }
+
+        $this->success('', [
+            'row' => $row
+        ]);
+    }
+
+    /**
+     * 删除
+     * @param array $ids
+     */
+    public function del(array $ids = [])
+    {
+        if (!$this->request->isDelete() || !$ids) {
+            $this->error(__('Parameter error'));
+        }
+
+        $dataLimitAdminIds = $this->getDataLimitAdminIds();
+        if ($dataLimitAdminIds) {
+            $this->model->where($this->dataLimitField, 'in', $dataLimitAdminIds);
+        }
+
+        $pk      = $this->model->getPk();
+        $data    = $this->model->where($pk, 'in', $ids)->select();
+        $subData = $this->model->where('pid', 'in', $ids)->column('pid', 'id');
+        foreach ($subData as $key => $subDatum) {
+            if (!in_array($key, $ids)) {
+                $this->error(__('Please delete the child element first, or use batch deletion'));
+            }
+        }
+        $count = 0;
+        Db::startTrans();
+        try {
+            foreach ($data as $v) {
+                $count += $v->delete();
+            }
+            Db::commit();
+        } catch (PDOException|Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        if ($count) {
+            $this->success(__('Deleted successfully'));
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+
+    public function select()
+    {
+        $data = $this->getRules([['status', '=', '1']]);
+
+        if ($this->assembleTree) {
+            $data = $this->tree->assembleTree($this->tree->getTreeArray($data, 'title'));
+        }
+        $this->success('', [
+            'options' => $data
+        ]);
+    }
+
+    public function getRules($where = []): array
+    {
+        $pk      = $this->model->getPk();
+        $initKey = $this->request->get("initKey/s", $pk);
+
+        if ($this->keyword) {
+            $keyword = explode(' ', $this->keyword);
+            foreach ($keyword as $item) {
+                $where[] = [$this->quickSearchField, 'like', '%' . $item . '%'];
+            }
+        }
+
+        if ($this->initValue) {
+            $where[] = [$initKey, 'in', $this->initValue];
+        }
+
+        $data = $this->model->where($where)->order('weigh desc,id asc')->select()->toArray();
+        return $this->assembleTree ? $this->tree->assembleChild($data) : $data;
+    }
+
+}

+ 43 - 0
app/admin/controller/user/ScoreLog.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace app\admin\controller\user;
+
+use app\common\controller\Backend;
+use app\admin\model\UserScoreLog;
+use app\admin\model\User;
+
+class ScoreLog extends Backend
+{
+    protected $model = null;
+
+    protected $withJoinTable = ['user'];
+
+    // 排除字段
+    protected $preExcludeFields = ['createtime'];
+
+    protected $quickSearchField = ['user.username', 'user.nickname'];
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new UserScoreLog();
+    }
+
+    /**
+     * 添加
+     */
+    public function add($userId = 0)
+    {
+        if ($this->request->isPost()) {
+            parent::add();
+        }
+
+        $user = User::where('id', (int)$userId)->find();
+        if (!$user) {
+            $this->error(__("The user can't find it"));
+        }
+        $this->success('', [
+            'user' => $user
+        ]);
+    }
+}

+ 145 - 0
app/admin/controller/user/User.php

@@ -0,0 +1,145 @@
+<?php
+
+namespace app\admin\controller\user;
+
+use Exception;
+use ba\Random;
+use think\facade\Db;
+use app\common\controller\Backend;
+use app\admin\model\User as UserModel;
+use think\db\exception\PDOException;
+use think\exception\ValidateException;
+
+class User extends Backend
+{
+    protected $model = null;
+
+    protected $withJoinTable = ['group'];
+
+    // 排除字段
+    protected $preExcludeFields = ['lastlogintime', 'loginfailure', 'password', 'salt'];
+
+    protected $quickSearchField = ['username', 'nickname', 'id'];
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new UserModel();
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        $this->request->filter(['strip_tags', 'trim']);
+        if ($this->request->param('select')) {
+            $this->select();
+        }
+
+        list($where, $alias, $limit, $order) = $this->queryBuilder();
+        $res = $this->model
+            ->withoutField('password,salt')
+            ->withJoin($this->withJoinTable, $this->withJoinType)
+            ->alias($alias)
+            ->where($where)
+            ->order($order)
+            ->paginate($limit);
+
+        $this->success('', [
+            'list'   => $res->items(),
+            'total'  => $res->total(),
+            'remark' => get_route_remark(),
+        ]);
+    }
+
+    public function add()
+    {
+        if ($this->request->isPost()) {
+            $data = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            $salt   = Random::build('alnum', 16);
+            $passwd = encrypt_password($data['password'], $salt);
+
+            $data   = $this->excludeFields($data);
+            $result = false;
+            Db::startTrans();
+            try {
+                $data['salt']     = $salt;
+                $data['password'] = $passwd;
+                // 模型验证
+                if ($this->modelValidate) {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    if (class_exists($validate)) {
+                        $validate = new $validate;
+                        if ($this->modelSceneValidate) $validate->scene('add');
+                        $validate->check($data);
+                    }
+                }
+                $result = $this->model->save($data);
+                Db::commit();
+            } catch (ValidateException|Exception|PDOException $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('Added successfully'));
+            } else {
+                $this->error(__('No rows were added'));
+            }
+        }
+
+        $this->error(__('Parameter error'));
+    }
+
+    public function edit($id = null)
+    {
+        $row = $this->model->find($id);
+        if (!$row) {
+            $this->error(__('Record not found'));
+        }
+
+        if ($this->request->isPost()) {
+            $password = $this->request->post('password', '');
+            if ($password) {
+                $this->model->resetPassword($id, $password);
+            }
+            parent::edit();
+        }
+
+        unset($row->salt);
+        $row->password = '';
+        $this->success('', [
+            'row' => $row
+        ]);
+    }
+
+    /**
+     * 重写select
+     */
+    public function select()
+    {
+        $this->request->filter(['strip_tags', 'trim']);
+
+        list($where, $alias, $limit, $order) = $this->queryBuilder();
+        $res = $this->model
+            ->withJoin($this->withJoinTable, $this->withJoinType)
+            ->alias($alias)
+            ->where($where)
+            ->order($order)
+            ->paginate($limit);
+
+        foreach ($res as $re) {
+            $re->nickname_text = $re->username . '(ID:' . $re->id . ')';
+        }
+
+        $this->success('', [
+            'list'   => $res->items(),
+            'total'  => $res->total(),
+            'remark' => get_route_remark(),
+        ]);
+    }
+}

+ 16 - 0
app/admin/event.php

@@ -0,0 +1,16 @@
+<?php
+// 事件定义文件
+return [
+    'bind'      => [
+    ],
+    'listen'    => [
+        'AppInit'     => [],
+        'HttpRun'     => [],
+        'HttpEnd'     => [],
+        'LogLevel'    => [],
+        'LogWrite'    => [],
+        'backendInit' => [app\common\event\Security::class],
+    ],
+    'subscribe' => [
+    ],
+];

+ 38 - 0
app/admin/lang/en.php

@@ -0,0 +1,38 @@
+<?php
+return [
+    'Please login first'                                                                  => 'Please login first',
+    'You have no permission'                                                              => 'You have no permission to operate',
+    'Username'                                                                            => 'Username',
+    'Password'                                                                            => 'Password',
+    'Nickname'                                                                            => 'Nickname',
+    'Email'                                                                               => 'Email',
+    'Mobile'                                                                              => 'Mobile Number',
+    'Captcha'                                                                             => 'Captcha',
+    'CaptchaId'                                                                           => 'Captcha Id',
+    'Please enter the correct verification code'                                          => 'Please enter the correct Captcha!',
+    'Parameter %s can not be empty'                                                       => 'Parameter %s can not be empty',
+    'Record not found'                                                                    => 'Record not found',
+    'No rows were added'                                                                  => 'No rows were added',
+    'No rows were deleted'                                                                => 'No rows were deleted',
+    'No rows updated'                                                                     => 'No rows updated',
+    'Update successful'                                                                   => 'Update successful!',
+    'Added successfully'                                                                  => 'Added successfully!',
+    'Deleted successfully'                                                                => 'Deleted successfully!',
+    'Parameter error'                                                                     => 'Parameter error!',
+    'Invalid collation because the weights of the two targets are equal'                  => 'Invalid sort, because these two target weight is equal.',
+    'File uploaded successfully'                                                          => 'File uploaded successfully',
+    'No files were uploaded'                                                              => 'No files were uploaded',
+    'The uploaded file format is not allowed'                                             => 'The uploaded file format is no allowance.',
+    'The uploaded image file is not a valid image'                                        => 'The uploaded image file is not a valid image',
+    'The uploaded file is too large (%sMiB), Maximum file size:%sMiB'                     => 'The uploaded file is too large (%sMiB), maximum file size:%sMiB',
+    'No files have been uploaded or the file size exceeds the upload limit of the server' => 'No files have been uploaded or the file size exceeds the server upload limit.',
+    'Unknown'                                                                             => 'Unknown',
+    'Account not exist'                                                                   => 'Account does not exist',
+    'Account disabled'                                                                    => 'Account is disabled',
+    'Token login failed'                                                                  => 'Token login failed',
+    'Username is incorrect'                                                               => 'Incorrect username',
+    'Please try again after 1 day'                                                        => 'The number of login failures exceeds the limit, please try again after 24 hours',
+    'Password is incorrect'                                                               => 'Wrong password',
+    'You are not logged in'                                                               => 'You are not logged in',
+    'Cache cleaned~'                                                                      => 'The cache has been cleaned up, please refresh the backgroun.',
+];

+ 8 - 0
app/admin/lang/en/ajax.php

@@ -0,0 +1,8 @@
+<?php
+return [
+    'Failed to switch package manager. Please modify the configuration file manually:%s'            => 'Failed to switch package manager, please modify the configuration file manually:%s',
+    'Failed to modify the terminal configuration. Please modify the configuration file manually:%s' => 'Failed to modify the terminal configuration, please modify the configuration file manually:%s',
+    'upload'                                                                                        => 'Upload files',
+    'changeTerminalConfig'                                                                          => 'Modify terminal configuration',
+    'Data table does not exist'                                                                     => 'Data table does not exist',
+];

+ 5 - 0
app/admin/lang/en/auth/admin.php

@@ -0,0 +1,5 @@
+<?php
+return [
+    'Group Name Arr'                                                           => 'Administrator Grouping ',
+    'Please use another administrator account to disable the current account!' => 'Disable the current account, please use another administrator account!',
+];

+ 6 - 0
app/admin/lang/en/auth/group.php

@@ -0,0 +1,6 @@
+<?php
+return [
+    'Super administrator'                          => 'Super administrator',
+    'No permission'                                => 'No permission',
+    'You cannot modify your own management group!' => 'You cannot modify your own management group!',
+];

+ 6 - 0
app/admin/lang/en/auth/menu.php

@@ -0,0 +1,6 @@
+<?php
+return [
+    'type'  => 'Rule type',
+    'title' => 'Rule title',
+    'name'  => 'Rule name',
+];

+ 4 - 0
app/admin/lang/en/dashboard.php

@@ -0,0 +1,4 @@
+<?php
+return [
+    'remark_text' => "Open source equals mutual assistance, and needs everyone's support. There are many ways to support it, such as using, recommending, writing tutorials, protecting the ecology, contributing code, answering questions, sharing experiences, donation, sponsorship and so on. Welcome to join us!",
+];

+ 9 - 0
app/admin/lang/en/index.php

@@ -0,0 +1,9 @@
+<?php
+return [
+    'No background menu, please contact super administrator!'       => 'No background menu, please contact the super administrator!',
+    'You have already logged in. There is no need to log in again~' => 'You have already logged in, no need to log in again.',
+    'Login succeeded!'                                              => 'Login successful!',
+    'Incorrect user name or password!'                              => 'Incorrect username or password!',
+    'Login'                                                         => 'Login',
+    'Logout'                                                        => 'Logout',
+];

+ 6 - 0
app/admin/lang/en/routine/admininfo.php

@@ -0,0 +1,6 @@
+<?php
+return [
+    'Please input correct username' => 'Please enter the correct username',
+    'Please input correct password' => 'Please enter the correct password',
+    'Avatar modified successfully!' => 'Profile picture modified successfully!',
+];

+ 5 - 0
app/admin/lang/en/routine/attachment.php

@@ -0,0 +1,5 @@
+<?php
+return [
+    '%d records and files have been deleted' => '%d records and files have been deleted',
+    'remark_text'                            => 'When the same file is uploaded multiple times, only one copy will be saved to the disk and an attachment record will be added; Deleting an attachment record will automatically delete the corresponding file!',
+];

+ 22 - 0
app/admin/lang/en/routine/config.php

@@ -0,0 +1,22 @@
+<?php
+return [
+    'Basics'                                                                                            => 'Basic configuration',
+    'Mail'                                                                                              => 'Mail configuration',
+    'Config group'                                                                                      => 'Configure grouping',
+    'Site Name'                                                                                         => 'Site name',
+    'Config Quick entrance'                                                                             => 'Quick configuration entrance',
+    'Record number'                                                                                     => 'Record Number',
+    'Version number'                                                                                    => 'Version Number',
+    'time zone'                                                                                         => 'Time zone',
+    'No access ip'                                                                                      => 'No access IP',
+    'smtp server'                                                                                       => 'SMTP server',
+    'smtp port'                                                                                         => 'SMTP port',
+    'smtp user'                                                                                         => 'SMTP username',
+    'smtp pass'                                                                                         => 'SMTP password',
+    'smtp verification'                                                                                 => 'SMTP verification mode',
+    'smtp sender mail'                                                                                  => 'SMTP sender mailbox',
+    'Variable name'                                                                                     => 'variable name',
+    'Test mail sent successfully~'                                                                      => 'Test message sent successfully',
+    'This is a test email'                                                                              => 'This is a test email',
+    'Congratulations, receiving this email means that your email service has been configured correctly' => "Congratulations, when you receive this email, it means that your mail service is configures correctly. This is the email subject, <b>you can use HtmlL!</b> in the main body.",
+];

+ 7 - 0
app/admin/lang/en/security/datarecycle.php

@@ -0,0 +1,7 @@
+<?php
+return [
+    'Name'        => 'Rule Name',
+    'Controller'  => 'Controller',
+    'Data Table'  => 'Corresponding data table',
+    'Primary Key' => 'Data table primary key',
+];

+ 4 - 0
app/admin/lang/en/security/datarecyclelog.php

@@ -0,0 +1,4 @@
+<?php
+return [
+    'No rows were restore' => 'No records have been restored',
+];

+ 8 - 0
app/admin/lang/en/security/sensitivedata.php

@@ -0,0 +1,8 @@
+<?php
+return [
+    'Name'        => 'Rule name',
+    'Controller'  => 'Controller',
+    'Data Table'  => 'Corresponding data table',
+    'Primary Key' => 'Data table primary key',
+    'Data Fields' => 'Sensitive data fields',
+];

+ 4 - 0
app/admin/lang/en/security/sensitivedatalog.php

@@ -0,0 +1,4 @@
+<?php
+return [
+    'No rows were rollback' => 'No records have been roll-back',
+];

+ 8 - 0
app/admin/lang/en/user/moneylog.php

@@ -0,0 +1,8 @@
+<?php
+return [
+    'user_id'                     => 'User',
+    'money'                       => 'Change amount',
+    'memo'                        => 'Change Notes',
+    "The user can't find it"      => "User does not exist",
+    'Change note cannot be blank' => 'Change Notes cannot be empty',
+];

+ 7 - 0
app/admin/lang/en/user/pilot.php

@@ -0,0 +1,7 @@
+<?php
+/**
+ * @datetime 2023/4/17 18:09
+ */
+return [
+    'remark_text'                            => 'Authenticated users: '
+];

+ 8 - 0
app/admin/lang/en/user/scorelog.php

@@ -0,0 +1,8 @@
+<?php
+return [
+    'user_id'                     => 'User',
+    'score'                       => 'Change points',
+    'memo'                        => 'Change Notes',
+    "The user can't find it"      => 'User does not exist',
+    'Change note cannot be blank' => 'Change notes cannot be empty',
+];

+ 59 - 0
app/admin/lang/zh-cn.php

@@ -0,0 +1,59 @@
+<?php
+return [
+    'Please login first'                                                                  => '请先登录!',
+    'You have no permission'                                                              => '没有权限操作!',
+    'Username'                                                                            => '用户名',
+    'Password'                                                                            => '密码',
+    'Nickname'                                                                            => '昵称',
+    'Email'                                                                               => '邮箱',
+    'Mobile'                                                                              => '手机号',
+    'Captcha'                                                                             => '验证码',
+    'CaptchaId'                                                                           => '验证码ID',
+    'Please enter the correct verification code'                                          => '请输入正确的验证码!',
+    'Parameter %s can not be empty'                                                       => '参数%s不能为空',
+    'Record not found'                                                                    => '记录未找到',
+    'No rows were added'                                                                  => '未添加任何行',
+    'No rows were deleted'                                                                => '未删除任何行',
+    'No rows updated'                                                                     => '未更新任何行',
+    'Update successful'                                                                   => '更新成功!',
+    'Added successfully'                                                                  => '添加成功!',
+    'Deleted successfully'                                                                => '删除成功!',
+    'Parameter error'                                                                     => '参数错误!',
+    'Invalid collation because the weights of the two targets are equal'                  => '无效排序整理,因为两个目标权重是相等的',
+    'File uploaded successfully'                                                          => '文件上传成功!',
+    'No files were uploaded'                                                              => '没有文件被上传',
+    'The uploaded file format is not allowed'                                             => '上传的文件格式未被允许',
+    'The uploaded image file is not a valid image'                                        => '上传的图片文件不是有效的图像',
+    'The uploaded file is too large (%sMiB), Maximum file size:%sMiB'                     => '上传的文件太大(%sM),最大文件大小:%sM',
+    'No files have been uploaded or the file size exceeds the upload limit of the server' => '没有文件被上传或文件大小超出服务器上传限制!',
+    'Unknown'                                                                             => '未知',
+    // 权限类语言包-s
+    'Account not exist'                                                                   => '帐户不存在',
+    'Account disabled'                                                                    => '帐户已禁用',
+    'Token login failed'                                                                  => '令牌登录失败',
+    'Username is incorrect'                                                               => '用户名不正确',
+    'Please try again after 1 day'                                                        => '登录失败次数超限,请在1天后再试',
+    'Password is incorrect'                                                               => '密码不正确',
+    'You are not logged in'                                                               => '你没有登录',
+    // 权限类语言包-e
+    // 时间格式化-s
+    '%d second%s ago'                                                                     => '%d秒前',
+    '%d minute%s ago'                                                                     => '%d分钟前',
+    '%d hour%s ago'                                                                       => '%d小时前',
+    '%d day%s ago'                                                                        => '%d天前',
+    '%d week%s ago'                                                                       => '%d周前',
+    '%d month%s ago'                                                                      => '%d月前',
+    '%d year%s ago'                                                                       => '%d年前',
+    '%d second%s after'                                                                   => '%d秒后',
+    '%d minute%s after'                                                                   => '%d分钟后',
+    '%d hour%s after'                                                                     => '%d小时后',
+    '%d day%s after'                                                                      => '%d天后',
+    '%d week%s after'                                                                     => '%d周后',
+    '%d month%s after'                                                                    => '%d月后',
+    '%d year%s after'                                                                     => '%d年后',
+    // 时间格式化-e
+    'Cache cleaned~'                                                                      => '缓存已清理,请刷新后台~',
+    'Please delete the child element first, or use batch deletion'                        => '请首先删除子元素,或使用批量删除!',
+    'Configuration write failed: %s'                                                      => '配置写入失败:%s',
+    'Token expiration'                                                                    => '登录态过期,请重新登录!',
+];

+ 8 - 0
app/admin/lang/zh-cn/ajax.php

@@ -0,0 +1,8 @@
+<?php
+return [
+    'Failed to switch package manager. Please modify the configuration file manually:%s'            => '包管理器切换失败,请手动修改配置文件:%s',
+    'Failed to modify the terminal configuration. Please modify the configuration file manually:%s' => '修改终端配置失败,请手动修改配置文件:%s',
+    'upload'                                                                                        => '上传文件',
+    'changeTerminalConfig'                                                                          => '修改终端配置',
+    'Data table does not exist'                                                                     => '数据表不存在~',
+];

+ 6 - 0
app/admin/lang/zh-cn/auth/admin.php

@@ -0,0 +1,6 @@
+<?php
+return [
+    'Group Name Arr'                                                           => '管理员分组',
+    'Please use another administrator account to disable the current account!' => '请使用另外的管理员账户禁用当前账户!',
+    'You have no permission to add an administrator to this group!'            => '您没有权限向此分组添加管理员!',
+];

+ 9 - 0
app/admin/lang/zh-cn/auth/group.php

@@ -0,0 +1,9 @@
+<?php
+return [
+    'Super administrator'                                                                                                 => '超级管理员',
+    'No permission'                                                                                                       => '无权限',
+    'You cannot modify your own management group!'                                                                        => '不能修改自己所在的管理组!',
+    'You need to have all permissions of this group to operate this group~'                                               => '您需要拥有该分组的所有权限才可以操作该分组~',
+    'You need to have all the permissions of the group and have additional permissions before you can operate the group~' => '您需要拥有该分组的所有权限且还有额外权限时,才可以操作该分组~',
+    'Role group has all your rights, please contact the upper administrator to add or do not need to add!'                => '角色组拥有您的全部权限,请联系上级管理员添加或无需添加!',
+];

+ 6 - 0
app/admin/lang/zh-cn/auth/menu.php

@@ -0,0 +1,6 @@
+<?php
+return [
+    'type'  => '规则类型',
+    'title' => '规则标题',
+    'name'  => '规则名称',
+];

+ 4 - 0
app/admin/lang/zh-cn/dashboard.php

@@ -0,0 +1,4 @@
+<?php
+return [
+    'remark_text' => '开源等于互助;开源需要大家一起来支持,支持的方式有很多种,比如使用、推荐、写教程、保护生态、贡献代码、回答问题、分享经验、打赏赞助等;欢迎您加入我们!',
+];

+ 9 - 0
app/admin/lang/zh-cn/index.php

@@ -0,0 +1,9 @@
+<?php
+return [
+    'No background menu, please contact super administrator!'       => '无后台菜单,请联系超级管理员!',
+    'You have already logged in. There is no need to log in again~' => '您已经登录过了,无需重复登录~',
+    'Login succeeded!'                                              => '登录成功!',
+    'Incorrect user name or password!'                              => '用户名或密码不正确!',
+    'Login'                                                         => '登录',
+    'Logout'                                                        => '注销登录',
+];

+ 21 - 0
app/admin/lang/zh-cn/module.php

@@ -0,0 +1,21 @@
+<?php
+return [
+    'Order not found'                                           => '订单找不到啦!',
+    'Module already exists'                                     => '模块已存在!',
+    'package download failed'                                   => '包下载失败!',
+    'No permission to write temporary files'                    => '没有权限写入临时文件!',
+    'Zip file not found'                                        => '找不到压缩包文件',
+    'Unable to open the zip file'                               => '无法打开压缩包文件',
+    'Unable to extract ZIP file'                                => '无法提取ZIP文件',
+    'Unable to package zip file'                                => '无法打包zip文件',
+    'Basic configuration of the Module is incomplete'           => '模块基础配置不完整',
+    'Module package file does not exist'                        => '模块包文件不存在',
+    'Module file conflicts'                                     => '模块文件存在冲突,请手动处理!',
+    'Configuration file has no write permission'                => '配置文件无写入权限',
+    'The current state of the module cannot be set to disabled' => '模块当前状态无法设定为禁用',
+    'The current state of the module cannot be set to enabled'  => '模块当前状态无法设定为启用',
+    'Module file updated'                                       => '模块文件有更新',
+    'Please disable the module first'                           => '请先禁用模块',
+    'Please disable the module before updating'                 => '更新前请先禁用模块',
+    'The directory required by the module is occupied'          => '模块所需目录已被占用',
+];

+ 6 - 0
app/admin/lang/zh-cn/routine/admininfo.php

@@ -0,0 +1,6 @@
+<?php
+return [
+    'Please input correct username' => '请输入正确的用户名',
+    'Please input correct password' => '请输入正确的密码',
+    'Avatar modified successfully!' => '头像修改成功!',
+];

+ 5 - 0
app/admin/lang/zh-cn/routine/attachment.php

@@ -0,0 +1,5 @@
+<?php
+return [
+    '%d records and files have been deleted' => '删除了%d条记录和文件',
+    'remark_text'                            => '同一文件被多次上传时,只会保存一份至磁盘和增加一条附件记录;删除附件记录,将自动删除对应文件!'
+];

+ 23 - 0
app/admin/lang/zh-cn/routine/config.php

@@ -0,0 +1,23 @@
+<?php
+return [
+    'Basics'                                                                                            => '基础配置',
+    'Mail'                                                                                              => '邮件配置',
+    'Config group'                                                                                      => '配置分组',
+    'Site Name'                                                                                         => '站点名称',
+    'Config Quick entrance'                                                                             => '快捷配置入口',
+    'Record number'                                                                                     => '备案号',
+    'Version number'                                                                                    => '版本号',
+    'time zone'                                                                                         => '时区',
+    'No access ip'                                                                                      => '禁止访问IP',
+    'smtp server'                                                                                       => 'SMTP 服务器',
+    'smtp port'                                                                                         => 'SMTP 端口',
+    'smtp user'                                                                                         => 'SMTP 用户名',
+    'smtp pass'                                                                                         => 'SMTP 密码',
+    'smtp verification'                                                                                 => 'SMTP 验证方式',
+    'smtp sender mail'                                                                                  => 'SMTP 发件人邮箱',
+    'Variable name'                                                                                     => '变量名',
+    'Test mail sent successfully~'                                                                      => '测试邮件发送成功~',
+    'This is a test email'                                                                              => '这是一封测试邮件',
+    'Congratulations, receiving this email means that your email service has been configured correctly' => '恭喜您,收到此邮件代表您的邮件服务已配置正确;这是邮件主体 <b>在主体中可以使用Html!</b>',
+    'The current page configuration item was updated successfully'                                      => '当前页配置项更新成功!',
+];

+ 7 - 0
app/admin/lang/zh-cn/security/datarecycle.php

@@ -0,0 +1,7 @@
+<?php
+return [
+    'Name'        => '规则名称',
+    'Controller'  => '控制器',
+    'Data Table'  => '对应数据表',
+    'Primary Key' => '数据表主键',
+];

+ 4 - 0
app/admin/lang/zh-cn/security/datarecyclelog.php

@@ -0,0 +1,4 @@
+<?php
+return [
+    'No rows were restore' => '没有记录被还原',
+];

+ 8 - 0
app/admin/lang/zh-cn/security/sensitivedata.php

@@ -0,0 +1,8 @@
+<?php
+return [
+    'Name'        => '规则名称',
+    'Controller'  => '控制器',
+    'Data Table'  => '对应数据表',
+    'Primary Key' => '数据表主键',
+    'Data Fields' => '敏感数据字段',
+];

+ 4 - 0
app/admin/lang/zh-cn/security/sensitivedatalog.php

@@ -0,0 +1,4 @@
+<?php
+return [
+    'No rows were rollback' => '没有记录被回滚',
+];

+ 8 - 0
app/admin/lang/zh-cn/user/moneylog.php

@@ -0,0 +1,8 @@
+<?php
+return [
+    'user_id'                     => '用户',
+    'money'                       => '变更金额',
+    'memo'                        => '变更备注',
+    "The user can't find it"      => '用户找不到啦',
+    'Change note cannot be blank' => '变更备注不能为空',
+];

+ 7 - 0
app/admin/lang/zh-cn/user/pilot.php

@@ -0,0 +1,7 @@
+<?php
+/**
+ * @datetime 2023/4/17 18:09
+ */
+return [
+    'remark_text'                            => '已认证用户:不仅是已通过实名认证,且还具备通过提交无人机飞行员资质证书,接受平台飞行资质审核并通过的专业用户。'
+];

+ 8 - 0
app/admin/lang/zh-cn/user/scorelog.php

@@ -0,0 +1,8 @@
+<?php
+return [
+    'user_id'                     => '用户',
+    'score'                       => '变更积分',
+    'memo'                        => '变更备注',
+    "The user can't find it"      => '用户找不到啦',
+    'Change note cannot be blank' => '变更备注不能为空',
+];

+ 458 - 0
app/admin/library/Auth.php

@@ -0,0 +1,458 @@
+<?php
+
+namespace app\admin\library;
+
+use ba\Random;
+use think\Exception;
+use think\facade\Db;
+use think\facade\Config;
+use app\admin\model\Admin;
+use app\common\facade\Token;
+use app\admin\model\AdminGroup;
+use think\db\exception\DbException;
+use think\db\exception\PDOException;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\ModelNotFoundException;
+
+/**
+ * 管理员权限类
+ */
+class Auth extends \ba\Auth
+{
+    /**
+     * @var Auth 对象实例
+     */
+    protected static $instance;
+
+    /**
+     * @var bool 是否登录
+     */
+    protected $logined = false;
+    /**
+     * @var string 错误消息
+     */
+    protected $error = '';
+    /**
+     * @var Admin Model实例
+     */
+    protected $model = null;
+    /**
+     * @var string 令牌
+     */
+    protected $token = '';
+    /**
+     * @var string 刷新令牌
+     */
+    protected $refreshToken = '';
+    /**
+     * @var int 令牌默认有效期
+     */
+    protected $keeptime = 86400;
+    /**
+     * @var string[] 允许输出的字段
+     */
+    protected $allowFields = ['id', 'username', 'nickname', 'avatar', 'lastlogintime'];
+
+    public function __construct(array $config = [])
+    {
+        parent::__construct($config);
+    }
+
+    /**
+     * 魔术方法-管理员信息字段
+     * @param $name
+     * @return null|string 字段信息
+     */
+    public function __get($name)
+    {
+        return $this->model ? $this->model->$name : null;
+    }
+
+    /**
+     * 初始化
+     * @access public
+     * @param array $options 参数
+     * @return Auth
+     */
+    public static function instance(array $options = []): Auth
+    {
+        if (is_null(self::$instance)) {
+            self::$instance = new static($options);
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * 根据Token初始化管理员登录态
+     * @param $token
+     * @return bool
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function init($token): bool
+    {
+        if ($this->logined) {
+            return true;
+        }
+        if ($this->error) {
+            return false;
+        }
+        $tokenData = Token::get($token);
+        if (!$tokenData) {
+            return false;
+        }
+        $userId = intval($tokenData['user_id']);
+        if ($userId > 0) {
+            $this->model = Admin::where('id', $userId)->find();
+            if (!$this->model) {
+                $this->setError('Account not exist');
+                return false;
+            }
+            if ($this->model['status'] != '1') {
+                $this->setError('Account disabled');
+                return false;
+            }
+            $this->token = $token;
+            $this->loginSuccessful();
+            return true;
+        } else {
+            $this->setError('Token login failed');
+            return false;
+        }
+    }
+
+    /**
+     * 管理员登录
+     * @param string $username
+     * @param string $password
+     * @param bool   $keeptime
+     * @return bool
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function login(string $username, string $password, bool $keeptime = false): bool
+    {
+        $this->model = Admin::where('username', $username)->find();
+        if (!$this->model) {
+            $this->setError('Username is incorrect');
+            return false;
+        }
+        if ($this->model['status'] == '0') {
+            $this->setError('Account disabled');
+            return false;
+        }
+        $adminLoginRetry = Config::get('buildadmin.admin_login_retry');
+        if ($adminLoginRetry && $this->model->loginfailure >= $adminLoginRetry && time() - $this->model->getData('lastlogintime') < 86400) {
+            $this->setError('Please try again after 1 day');
+            return false;
+        }
+        if ($this->model->password != encrypt_password($password, $this->model->salt)) {
+            $this->loginFailed();
+            $this->setError('Password is incorrect');
+            return false;
+        }
+        if (Config::get('buildadmin.admin_sso')) {
+            Token::clear('admin', $this->model->id);
+            Token::clear('admin-refresh', $this->model->id);
+        }
+
+        if ($keeptime) {
+            $this->setRefreshToken(2592000);
+        }
+        $this->loginSuccessful();
+        return true;
+    }
+
+    /**
+     * 设置刷新Token
+     * @param int $keeptime
+     */
+    public function setRefreshToken(int $keeptime = 0)
+    {
+        $this->refreshToken = Random::uuid();
+        Token::set($this->refreshToken, 'admin-refresh', $this->model->id, $keeptime);
+    }
+
+    /**
+     * 管理员登录成功
+     * @return bool
+     */
+    public function loginSuccessful(): bool
+    {
+        if (!$this->model) {
+            return false;
+        }
+        Db::startTrans();
+        try {
+            $this->model->loginfailure  = 0;
+            $this->model->lastlogintime = time();
+            $this->model->lastloginip   = request()->ip();
+            $this->model->save();
+            $this->logined = true;
+
+            if (!$this->token) {
+                $this->token = Random::uuid();
+                Token::set($this->token, 'admin', $this->model->id, $this->keeptime);
+            }
+            Db::commit();
+        } catch (PDOException|Exception $e) {
+            Db::rollback();
+            $this->setError($e->getMessage());
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 管理员登录失败
+     * @return bool
+     */
+    public function loginFailed(): bool
+    {
+        if (!$this->model) {
+            return false;
+        }
+        Db::startTrans();
+        try {
+            $this->model->loginfailure++;
+            $this->model->lastlogintime = time();
+            $this->model->lastloginip   = request()->ip();
+            $this->model->save();
+
+            $this->token   = '';
+            $this->model   = null;
+            $this->logined = false;
+            Db::commit();
+        } catch (PDOException|Exception $e) {
+            Db::rollback();
+            $this->setError($e->getMessage());
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 退出登录
+     * @return bool
+     */
+    public function logout(): bool
+    {
+        if (!$this->logined) {
+            $this->setError('You are not logged in');
+            return false;
+        }
+        $this->logined = false;
+        Token::delete($this->token);
+        $this->token = '';
+        return true;
+    }
+
+    /**
+     * 是否登录
+     * @return bool
+     */
+    public function isLogin(): bool
+    {
+        return $this->logined;
+    }
+
+    /**
+     * 获取管理员模型
+     * @return Admin
+     */
+    public function getAdmin(): Admin
+    {
+        return $this->model;
+    }
+
+    /**
+     * 获取管理员Token
+     * @return string
+     */
+    public function getToken(): string
+    {
+        return $this->token;
+    }
+
+    /**
+     * 获取管理员刷新Token
+     * @return string
+     */
+    public function getRefreshToken(): string
+    {
+        return $this->refreshToken;
+    }
+
+    /**
+     * 获取管理员信息 - 只输出允许输出的字段
+     * @return array
+     */
+    public function getInfo(): array
+    {
+        if (!$this->model) {
+            return [];
+        }
+        $info                 = $this->model->toArray();
+        $info                 = array_intersect_key($info, array_flip($this->getAllowFields()));
+        $info['token']        = $this->getToken();
+        $info['refreshToken'] = $this->getRefreshToken();
+        return $info;
+    }
+
+    /**
+     * 获取允许输出字段
+     * @return string[]
+     */
+    public function getAllowFields(): array
+    {
+        return $this->allowFields;
+    }
+
+    /**
+     * 设置允许输出字段
+     * @param $fields
+     */
+    public function setAllowFields($fields)
+    {
+        $this->allowFields = $fields;
+    }
+
+    /**
+     * 设置Token有效期
+     * @param int $keeptime
+     */
+    public function setKeeptime(int $keeptime = 0)
+    {
+        $this->keeptime = $keeptime;
+    }
+
+    public function check(string $name, int $uid = 0, string $relation = 'or', string $mode = 'url'): bool
+    {
+        return parent::check($name, $uid ?: $this->id, $relation, $mode);
+    }
+
+    public function getGroups(int $uid = 0): array
+    {
+        return parent::getGroups($uid ?: $this->id);
+    }
+
+    public function getRuleList(int $uid = 0): array
+    {
+        return parent::getRuleList($uid ?: $this->id);
+    }
+
+    public function getRuleIds(int $uid = 0): array
+    {
+        return parent::getRuleIds($uid ?: $this->id);
+    }
+
+    public function getMenus(int $uid = 0): array
+    {
+        return parent::getMenus($uid ?: $this->id);
+    }
+
+    public function isSuperAdmin(): bool
+    {
+        return in_array('*', $this->getRuleIds());
+    }
+
+    /**
+     * 获取管理员所在分组的所有子级分组
+     * @return array
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getAdminChildGroups(): array
+    {
+        $groupIds = Db::name('admin_group_access')
+            ->where('uid', $this->id)
+            ->select();
+        $children = [];
+        foreach ($groupIds as $group) {
+            $this->getGroupChildGroups($group['group_id'], $children);
+        }
+        return array_unique($children);
+    }
+
+    public function getGroupChildGroups($groupId, &$children)
+    {
+        $childrenTemp = AdminGroup::where('pid', $groupId)->where('status', '1')->select();
+        foreach ($childrenTemp as $item) {
+            $children[] = $item['id'];
+            $this->getGroupChildGroups($item['id'], $children);
+        }
+    }
+
+    /**
+     * 获取分组内的管理员
+     * @param array $groups
+     * @return array 管理员数组
+     */
+    public function getGroupAdmins(array $groups): array
+    {
+        return Db::name('admin_group_access')
+            ->where('group_id', 'in', $groups)
+            ->column('uid');
+    }
+
+    /**
+     * 获取拥有"所有权限"的分组
+     * @param string $dataLimit 数据权限
+     * @return array 分组数组
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public function getAllAuthGroups(string $dataLimit): array
+    {
+        // 当前管理员拥有的权限
+        $rules         = $this->getRuleIds();
+        $allAuthGroups = [];
+        $groups        = AdminGroup::where('status', '1')->select();
+        foreach ($groups as $group) {
+            if ($group['rules'] == '*') {
+                continue;
+            }
+            $groupRules = explode(',', $group['rules']);
+
+            // 及时break, array_diff 等没有 in_array 快
+            $all = true;
+            foreach ($groupRules as $groupRule) {
+                if (!in_array($groupRule, $rules)) {
+                    $all = false;
+                    break;
+                }
+            }
+            if ($all) {
+                if ($dataLimit == 'allAuth' || ($dataLimit == 'allAuthAndOthers' && array_diff($rules, $groupRules))) {
+                    $allAuthGroups[] = $group['id'];
+                }
+            }
+        }
+        return $allAuthGroups;
+    }
+
+    /**
+     * 设置错误消息
+     * @param $error
+     * @return $this
+     */
+    public function setError($error): Auth
+    {
+        $this->error = $error;
+        return $this;
+    }
+
+    /**
+     * 获取错误消息
+     * @return float|int|string
+     */
+    public function getError()
+    {
+        return $this->error ? __($this->error) : '';
+    }
+}

+ 959 - 0
app/admin/library/crud/Helper.php

@@ -0,0 +1,959 @@
+<?php
+
+namespace app\admin\library\crud;
+
+use think\Exception;
+use think\facade\Db;
+use app\common\library\Menu;
+use app\admin\model\MenuRule;
+use app\admin\model\CrudLog;
+
+class Helper
+{
+    /**
+     * 内部保留词
+     */
+    protected static $reservedKeywords = [
+        'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'final', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'isset', 'list', 'namespace', 'new', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use', 'var', 'while', 'xor', 'yield'
+    ];
+
+    /**
+     * 预设控制器和模型文件位置
+     */
+    protected static $parseNamePresets = [
+        'controller' => [
+            'user'        => ['user', 'user'],
+            'admin'       => ['auth', 'admin'],
+            'admin_group' => ['auth', 'group'],
+            'attachment'  => ['routine', 'attachment'],
+            'menu_rule'   => ['auth', 'menu'],
+        ],
+        'model'      => [],
+        'validate'   => [],
+    ];
+
+    /**
+     * 子级菜单数组(权限节点)
+     * @var string
+     */
+    protected static $menuChildren = [
+        ['type' => 'button', 'title' => '查看', 'name' => '/index', 'status' => '1'],
+        ['type' => 'button', 'title' => '添加', 'name' => '/add', 'status' => '1'],
+        ['type' => 'button', 'title' => '编辑', 'name' => '/edit', 'status' => '1'],
+        ['type' => 'button', 'title' => '删除', 'name' => '/del', 'status' => '1'],
+        ['type' => 'button', 'title' => '快速排序', 'name' => '/sortable', 'status' => '1'],
+    ];
+
+    /**
+     * 输入框类型的识别规则
+     */
+    protected static $inputTypeRule = [
+        // 开关组件
+        [
+            'type'   => ['tinyint', 'int', 'enum'],
+            'suffix' => ['switch', 'toggle'],
+            'value'  => 'switch',
+        ],
+        [
+            'column_type' => ['tinyint(1)', 'char(1)', 'tinyint(1) unsigned'],
+            'suffix'      => ['switch', 'toggle'],
+            'value'       => 'switch',
+        ],
+        // 富文本-识别规则和textarea重合,优先识别为富文本
+        [
+            'type'   => ['longtext', 'text', 'mediumtext', 'smalltext', 'tinytext', 'bigtext'],
+            'suffix' => ['content', 'editor'],
+            'value'  => 'editor',
+        ],
+        // textarea
+        [
+            'type'   => ['varchar'],
+            'suffix' => ['textarea', 'multiline', 'rows'],
+            'value'  => 'textarea',
+        ],
+        // Array
+        [
+            'suffix' => ['array'],
+            'value'  => 'array',
+        ],
+        // 时间选择器-字段类型为int同时以['time', 'datetime']结尾
+        [
+            'type'   => ['int'],
+            'suffix' => ['time', 'datetime'],
+            'value'  => 'timestamp',
+        ],
+        [
+            'type'  => ['datetime', 'timestamp'],
+            'value' => 'datetime',
+        ],
+        [
+            'type'  => ['date'],
+            'value' => 'date',
+        ],
+        [
+            'type'  => ['year'],
+            'value' => 'year',
+        ],
+        [
+            'type'  => ['time'],
+            'value' => 'time',
+        ],
+        // 单选select
+        [
+            'suffix' => ['select', 'list', 'data'],
+            'value'  => 'select',
+        ],
+        // 多选select
+        [
+            'suffix' => ['selects', 'multi', 'lists'],
+            'value'  => 'selects',
+        ],
+        // 远程select
+        [
+            'suffix' => ['_id'],
+            'value'  => 'remoteSelect',
+        ],
+        // 远程selects
+        [
+            'suffix' => ['_ids'],
+            'value'  => 'remoteSelects',
+        ],
+        // 城市选择器
+        [
+            'suffix' => ['city'],
+            'value'  => 'city',
+        ],
+        // 单图上传
+        [
+            'suffix' => ['image', 'avatar'],
+            'value'  => 'image',
+        ],
+        // 多图上传
+        [
+            'suffix' => ['images', 'avatars'],
+            'value'  => 'images',
+        ],
+        // 文件上传
+        [
+            'suffix' => ['file'],
+            'value'  => 'file',
+        ],
+        // 多文件上传
+        [
+            'suffix' => ['files'],
+            'value'  => 'files',
+        ],
+        // icon选择器
+        [
+            'suffix' => ['icon'],
+            'value'  => 'icon',
+        ],
+        // 单选框
+        [
+            'column_type' => ['tinyint(1)', 'char(1)', 'tinyint(1) unsigned'],
+            'suffix'      => ['status', 'state', 'type'],
+            'value'       => 'radio',
+        ],
+        // 数字输入框
+        [
+            'suffix' => ['number', 'int', 'num'],
+            'value'  => 'number',
+        ],
+        [
+            'type'  => ['bigint', 'int', 'mediumint', 'smallint', 'tinyint', 'decimal', 'double', 'float'],
+            'value' => 'number',
+        ],
+        // 富文本-低权重
+        [
+            'type'  => ['longtext', 'text', 'mediumtext', 'smalltext', 'tinytext', 'bigtext'],
+            'value' => 'textarea',
+        ],
+        // 单选框-低权重
+        [
+            'type'  => ['enum'],
+            'value' => 'radio',
+        ],
+        // 多选框
+        [
+            'type'  => ['set'],
+            'value' => 'checkbox',
+        ],
+        // 颜色选择器
+        [
+            'suffix' => ['color'],
+            'value'  => 'color',
+        ],
+    ];
+
+    /**
+     * 预设WEB端文件位置
+     */
+    protected static $parseWebDirPresets = [
+        'lang'  => [],
+        'views' => [
+            'user'        => ['user', 'user'],
+            'admin'       => ['auth', 'admin'],
+            'admin_group' => ['auth', 'group'],
+            'attachment'  => ['routine', 'attachment'],
+            'menu_rule'   => ['auth', 'menu'],
+        ],
+    ];
+
+    /**
+     * 添加时间字段
+     * @var string
+     */
+    protected static $createTimeField = 'create_time';
+
+    /**
+     * 更新时间字段
+     * @var string
+     */
+    protected static $updateTimeField = 'update_time';
+
+    public static function getDictData(&$dict, $field, $lang, $translationPrefix = ''): array
+    {
+        if (!$field['comment']) return [];
+        $comment = str_replace([',', ':'], [',', ':'], $field['comment']);
+        if (stripos($comment, ':') !== false && stripos($comment, ',') && stripos($comment, '=') !== false) {
+            [$fieldTitle, $item] = explode(':', $comment);
+            $dict[$translationPrefix . $field['name']] = $lang == 'en' ? $field['name'] : $fieldTitle;
+            foreach (explode(',', $item) as $v) {
+                $valArr = explode('=', $v);
+                if (count($valArr) == 2) {
+                    [$key, $value] = $valArr;
+                    $dict[$translationPrefix . $field['name'] . ' ' . $key] = $lang == 'en' ? $field['name'] . ' ' . $key : $value;
+                }
+            }
+        } else {
+            $dict[$translationPrefix . $field['name']] = $lang == 'en' ? $field['name'] : $comment;
+        }
+        return $dict;
+    }
+
+    public static function recordCrudStatus(array $data)
+    {
+        if (isset($data['id'])) {
+            return CrudLog::where('id', $data['id'])
+                ->update([
+                    'status' => $data['status'],
+                ]);
+        }
+        $log = CrudLog::create([
+            'table_name' => $data['table']['name'],
+            'table'      => $data['table'],
+            'fields'     => $data['fields'],
+            'status'     => $data['status'],
+        ]);
+        return $log->id;
+    }
+
+    public static function createTable($name, $comment, $fields): array
+    {
+        $fieldType = [
+            'variableLength'  => ['blob', 'date', 'enum', 'geometry', 'geometrycollection', 'json', 'linestring', 'longblob', 'longtext', 'mediumblob', 'mediumtext', 'multilinestring', 'multipoint', 'multipolygon', 'point', 'polygon', 'set', 'text', 'tinyblob', 'tinytext', 'year'],
+            'fixedLength'     => ['int', 'bigint', 'binary', 'bit', 'char', 'datetime', 'mediumint', 'smallint', 'time', 'timestamp', 'tinyint', 'varbinary', 'varchar'],
+            'decimal'         => ['decimal', 'double', 'float'],
+            'supportUnsigned' => ['int', 'tinyint', 'smallint', 'mediumint', 'integer', 'bigint', 'real', 'double', 'float', 'decimal', 'numeric'],
+        ];
+        $name      = self::getTableName($name);
+        $sql       = "CREATE TABLE IF NOT EXISTS `$name` (" . PHP_EOL;
+        $pk        = '';
+        foreach ($fields as $field) {
+            $fieldConciseType = self::analyseFieldType($field);
+            // 组装dateType
+            if (!isset($field['dataType']) || !$field['dataType']) {
+                if (!$field['type']) {
+                    continue;
+                }
+                if (in_array($field['type'], $fieldType['fixedLength'])) {
+                    $field['dataType'] = "{$field['type']}({$field['length']})";
+                } elseif (in_array($field['type'], $fieldType['decimal'])) {
+                    $field['dataType'] = "{$field['type']}({$field['length']},{$field['precision']})";
+                } elseif (in_array($field['type'], $fieldType['variableLength'])) {
+                    $field['dataType'] = $field['type'];
+                } else {
+                    $field['dataType'] = $field['precision'] ? "{$field['type']}({$field['length']},{$field['precision']})" : "{$field['type']}({$field['length']})";
+                }
+            }
+            $unsigned      = ($field['unsigned'] && in_array($fieldConciseType, $fieldType['supportUnsigned'])) ? ' UNSIGNED' : '';
+            $null          = $field['null'] ? ' NULL' : ' NOT NULL';
+            $autoIncrement = $field['autoIncrement'] ? ' AUTO_INCREMENT' : '';
+            $default       = '';
+            if (strtolower((string)$field['default']) == 'null') {
+                $default = ' DEFAULT NULL';
+            } elseif ($field['default'] == '0') {
+                $default = " DEFAULT '0'";
+            } elseif ($field['default'] == 'empty string') {
+                $default = " DEFAULT ''";
+            } elseif ($field['default']) {
+                $default = " DEFAULT '{$field['default']}'";
+            }
+            $fieldComment = $field['comment'] ? " COMMENT '{$field['comment']}'" : '';
+            $sql          .= "`{$field['name']}` {$field['dataType']}$unsigned$null$autoIncrement$default$fieldComment ," . PHP_EOL;
+            if ($field['primaryKey']) {
+                $pk = $field['name'];
+            }
+        }
+        $sql .= "PRIMARY KEY (`$pk`)" . PHP_EOL . ") ";
+        $sql .= "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='$comment'";
+        Db::execute($sql);
+        return [$pk];
+    }
+
+    public static function parseNameData($app, $table, $name, $type, $value = ''): array
+    {
+        $pathArr = [];
+        if ($value) {
+            $value        = str_replace('.php', '', $value);
+            $value        = str_replace(['.', '/', '\\', '_'], '/', $value);
+            $pathArrTemp  = explode('/', $value);
+            $redundantDir = [
+                'app' => 0,
+                $app  => 1,
+                $type => 2,
+            ];
+            foreach ($pathArrTemp as $key => $item) {
+                if (!array_key_exists($item, $redundantDir) || $key !== $redundantDir[$item]) {
+                    $pathArr[] = $item;
+                }
+            }
+            $originalLastName = array_pop($pathArr);
+            $lastName         = ucfirst($originalLastName);
+        } else {
+            if (!$name) {
+                if (isset(self::$parseNamePresets[$type]) && array_key_exists($table, self::$parseNamePresets[$type])) {
+                    $pathArr          = self::$parseNamePresets[$type][$table];
+                    $originalLastName = array_pop($pathArr);
+                    $lastName         = ucfirst($originalLastName);
+                } else {
+                    $originalLastName = $lastName = parse_name($table, 1);
+                }
+            } else {
+                $name             = str_replace(['.', '/', '\\', '_'], '/', $name);
+                $pathArr          = explode('/', $name);
+                $originalLastName = array_pop($pathArr);
+                $lastName         = ucfirst($originalLastName);
+            }
+        }
+
+        // 类名不能为内部关键字
+        if (in_array(strtolower($originalLastName), self::$reservedKeywords)) {
+            throw new Exception('Unable to use internal variable:' . $lastName);
+        }
+
+        $appDir       = app()->getBasePath() . $app . DIRECTORY_SEPARATOR;
+        $namespace    = "app\\$app\\$type" . ($pathArr ? '\\' . implode('\\', $pathArr) : '');
+        $parseFile    = $appDir . $type . DIRECTORY_SEPARATOR . ($pathArr ? implode(DIRECTORY_SEPARATOR, $pathArr) . DIRECTORY_SEPARATOR : '') . $lastName . '.php';
+        $rootFileName = $namespace . "/$lastName" . '.php';
+
+        return [
+            'lastName'         => $lastName,
+            'originalLastName' => $originalLastName,
+            'path'             => $pathArr,
+            'namespace'        => $namespace,
+            'parseFile'        => path_transform($parseFile),
+            'rootFileName'     => path_transform($rootFileName),
+        ];
+    }
+
+    public static function parseWebDirNameData($table, $name, $type, $value = ''): array
+    {
+        $pathArr = [];
+        if ($value) {
+            $value        = str_replace(['.', '/', '\\', '_'], '/', $value);
+            $pathArrTemp  = explode('/', $value);
+            $redundantDir = [
+                'web'     => 0,
+                'src'     => 1,
+                'views'   => 2,
+                'lang'    => 2,
+                'backend' => 3,
+                'pages'   => 3,
+                'en'      => 4,
+                'zh-cn'   => 4,
+            ];
+            foreach ($pathArrTemp as $key => $item) {
+                if (!array_key_exists($item, $redundantDir) || $key !== $redundantDir[$item]) {
+                    $pathArr[] = $item;
+                }
+            }
+            $originalLastName = array_pop($pathArr);
+            $lastName         = lcfirst($originalLastName);
+        } else {
+            if (!$name) {
+                if (array_key_exists($table, self::$parseWebDirPresets[$type])) {
+                    $pathArr          = self::$parseWebDirPresets[$type][$table];
+                    $originalLastName = array_pop($pathArr);
+                    $lastName         = lcfirst($originalLastName);
+                } else {
+                    $originalLastName = $lastName = parse_name($table, 1, false);
+                }
+            } else {
+                $name             = str_replace(['.', '/', '\\', '_'], '/', $name);
+                $pathArr          = explode('/', $name);
+                $originalLastName = array_pop($pathArr);
+                $lastName         = lcfirst($originalLastName);
+            }
+        }
+
+        $webDir['path']             = $pathArr;
+        $webDir['lastName']         = $lastName;
+        $webDir['originalLastName'] = $originalLastName;
+        if ($type == 'views') {
+            $webDir['views'] = "web/src/views/backend" . ($pathArr ? '/' . implode('/', $pathArr) : '') . "/$lastName";
+        } elseif ($type == 'lang') {
+            $webDir['lang'] = array_merge($pathArr, [$lastName]);
+            $langDir        = ['en', 'zh-cn'];
+            foreach ($langDir as $item) {
+                $webDir[$item] = "web/src/lang/backend/$item" . ($pathArr ? '/' . implode('/', $pathArr) : '') . "/$lastName";
+            }
+        }
+        foreach ($webDir as &$item) {
+            if (is_string($item)) $item = path_transform($item);
+        }
+        return $webDir;
+    }
+
+    public static function getTableName(string $table, $fullName = true): string
+    {
+        $tablePrefix = config('database.connections.mysql.prefix');
+        $pattern     = '/^' . $tablePrefix . '/i';
+        return ($fullName ? $tablePrefix : '') . (preg_replace($pattern, '', $table));
+    }
+
+    /**
+     * 获取菜单name、path
+     * @param array $webDir
+     * @return string
+     */
+    public static function getMenuName(array $webDir): string
+    {
+        return ($webDir['path'] ? implode('/', $webDir['path']) . '/' : '') . $webDir['originalLastName'];
+    }
+
+    /**
+     * 获取基础模板文件路径
+     * @param string $name
+     * @return string
+     */
+    public static function getStubFilePath(string $name): string
+    {
+        return app_path() . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . 'crud' . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . path_transform($name) . '.stub';
+    }
+
+    /**
+     * 组装模板
+     * @param string $name
+     * @param array  $data
+     * @param bool   $escape
+     * @return string
+     */
+    public static function assembleStub(string $name, array $data, bool $escape = false): string
+    {
+        foreach ($data as &$datum) {
+            $datum = is_array($datum) ? implode(PHP_EOL, $datum) : $datum;
+        }
+        $search = $replace = [];
+        foreach ($data as $k => $v) {
+            $search[]  = "{%$k%}";
+            $replace[] = $v;
+        }
+        $stubPath    = self::getStubFilePath($name);
+        $stubContent = file_get_contents($stubPath);
+        $content     = str_replace($search, $replace, $stubContent);
+        return $escape ? self::escape($content) : $content;
+    }
+
+    /**
+     * 获取转义编码后的值
+     * @param string|array $value
+     * @return string
+     */
+    public static function escape($value): string
+    {
+        if (is_array($value)) {
+            $value = json_encode($value, JSON_UNESCAPED_UNICODE);
+        }
+        return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false);
+    }
+
+    public static function tab(int $num = 1): string
+    {
+        return str_pad('', 4 * $num);
+    }
+
+    /**
+     * 删除数据表
+     */
+    public static function delTable(string $table)
+    {
+        $sql = 'DROP TABLE IF EXISTS `' . self::getTableName($table) . '`';
+        Db::execute($sql);
+    }
+
+    /**
+     * 根据数据表解析字段数据
+     */
+    public static function parseTableColumns(string $table, bool $analyseField = false): array
+    {
+        // 从数据库中获取表字段信息
+        $sql = 'SELECT * FROM `information_schema`.`columns` '
+            . 'WHERE TABLE_SCHEMA = ? AND table_name = ? '
+            . 'ORDER BY ORDINAL_POSITION';
+
+        $columns     = [];
+        $tableColumn = Db::query($sql, [config('database.connections.mysql.database'), self::getTableName($table)]);
+        foreach ($tableColumn as $item) {
+            $isNullAble = $item['IS_NULLABLE'] == 'YES';
+            $column     = [
+                'name'          => $item['COLUMN_NAME'],
+                'type'          => $item['DATA_TYPE'],
+                'dataType'      => stripos($item['COLUMN_TYPE'], '(') !== false ? substr_replace($item['COLUMN_TYPE'], '', stripos($item['COLUMN_TYPE'], ')') + 1) : $item['COLUMN_TYPE'],
+                'default'       => ($isNullAble && is_null($item['COLUMN_DEFAULT'])) ? 'null' : $item['COLUMN_DEFAULT'],
+                'null'          => $isNullAble,
+                'primaryKey'    => $item['COLUMN_KEY'] == 'PRI',
+                'unsigned'      => (bool)stripos($item['COLUMN_TYPE'], 'unsigned'),
+                'autoIncrement' => stripos($item['EXTRA'], 'auto_increment') !== false,
+                'comment'       => $item['COLUMN_COMMENT'],
+                'designType'    => self::getTableColumnsDataType($item),
+                'table'         => [],
+                'form'          => [],
+            ];
+            if ($analyseField) {
+                self::analyseField($column);
+            } else {
+                self::handleTableColumn($column);
+            }
+            $columns[$item['COLUMN_NAME']] = $column;
+        }
+        return $columns;
+    }
+
+    /**
+     * 解析到的表字段的额外处理
+     */
+    public static function handleTableColumn(&$column)
+    {
+        // 预留
+    }
+
+    public static function analyseFieldType($field): string
+    {
+        $dataType = (isset($field['dataType']) && $field['dataType']) ? $field['dataType'] : $field['type'];
+        if (stripos($dataType, '(') !== false) {
+            $typeName = explode('(', $dataType);
+            return trim($typeName[0]);
+        }
+        return trim($dataType);
+    }
+
+    /**
+     * 分析字段
+     */
+    public static function analyseField(&$field)
+    {
+        $field['type']               = self::analyseFieldType($field);
+        $field['originalDesignType'] = $field['designType'];
+
+        // 表单项类型转换对照表
+        $designTypeComparison = [
+            'pk'        => 'string',
+            'weigh'     => 'number',
+            'timestamp' => 'datetime',
+            'float'     => 'number',
+        ];
+        if (array_key_exists($field['designType'], $designTypeComparison)) {
+            $field['designType'] = $designTypeComparison[$field['designType']];
+        }
+
+        // 是否开启了多选
+        $supportMultipleComparison = ['select', 'image', 'file', 'remoteSelect'];
+        if (in_array($field['designType'], $supportMultipleComparison)) {
+            $multiKey = $field['designType'] == 'remoteSelect' ? 'select-multi' : $field['designType'] . '-multi';
+            if (isset($field['form'][$multiKey]) && $field['form'][$multiKey]) {
+                $field['designType'] = $field['designType'] . 's';
+            }
+        }
+    }
+
+    public static function getTableColumnsDataType($column)
+    {
+        if (stripos($column['COLUMN_NAME'], 'id') !== false && stripos($column['EXTRA'], 'auto_increment') !== false) {
+            return 'pk';
+        } elseif ($column['COLUMN_NAME'] == 'weigh') {
+            return 'weigh';
+        } elseif (in_array($column['COLUMN_NAME'], ['createtime', 'updatetime', 'create_time', 'update_time'])) {
+            return 'timestamp';
+        }
+        foreach (self::$inputTypeRule as $item) {
+            $typeBool       = true;
+            $suffixBool     = true;
+            $columnTypeBool = true;
+            if (isset($item['type']) && $item['type'] && !in_array($column['DATA_TYPE'], $item['type'])) {
+                $typeBool = false;
+            }
+            if (isset($item['suffix']) && $item['suffix']) {
+                $suffixBool = self::isMatchSuffix($column['COLUMN_NAME'], $item['suffix']);
+            }
+            if (isset($item['column_type']) && $item['column_type'] && !in_array($column['COLUMN_TYPE'], $item['column_type'])) {
+                $columnTypeBool = false;
+            }
+            if ($typeBool && $suffixBool && $columnTypeBool) {
+                return $item['value'];
+            }
+        }
+        return 'string';
+    }
+
+    /**
+     * 判断是否符合指定后缀
+     *
+     * @param string $field     字段名称
+     * @param mixed  $suffixArr 后缀
+     * @return bool
+     */
+    protected static function isMatchSuffix(string $field, $suffixArr): bool
+    {
+        $suffixArr = is_array($suffixArr) ? $suffixArr : explode(',', $suffixArr);
+        foreach ($suffixArr as $v) {
+            if (preg_match("/$v$/i", $field)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static function createMenu($webViewsDir, $tableComment)
+    {
+        $menuName = self::getMenuName($webViewsDir);
+        if (!MenuRule::where('name', $menuName)->value('id')) {
+            $pid = 0;
+            foreach ($webViewsDir['path'] as $item) {
+                $pMenu = MenuRule::where('name', $item)->value('id');
+                if ($pMenu) {
+                    $pid = $pMenu;
+                    continue;
+                }
+                $menu = [
+                    'pid'   => $pid,
+                    'type'  => 'menu_dir',
+                    'title' => $item,
+                    'name'  => $item,
+                    'path'  => $item,
+                ];
+                $menu = MenuRule::create($menu);
+                $pid  = $menu->id;
+            }
+
+            // 建立菜单
+            foreach (self::$menuChildren as &$item) {
+                $item['name'] = $menuName . $item['name'];
+            }
+            $componentPath = str_replace(['\\', 'web/src'], ['/', '/src'], $webViewsDir['views'] . '/' . 'index.vue');
+            Menu::create([
+                [
+                    'type'      => 'menu',
+                    'title'     => $tableComment ?: $webViewsDir['originalLastName'],
+                    'name'      => $menuName,
+                    'path'      => $menuName,
+                    'menu_type' => 'tab',
+                    'component' => $componentPath,
+                    'children'  => self::$menuChildren,
+                ]
+            ], $pid);
+        }
+    }
+
+    public static function writeWebLangFile($langData, $webLangDir)
+    {
+        foreach ($langData as $lang => $langDatum) {
+            $langTsContent = '';
+            foreach ($langDatum as $key => $item) {
+                $quote         = self::getQuote($item);
+                $keyStr        = self::formatObjectKey($key);
+                $langTsContent .= self::tab() . $keyStr . ": $quote$item$quote,\n";
+            }
+            $langTsContent = "export default {\n" . $langTsContent . "}\n";
+            self::writeFile(root_path() . $webLangDir[$lang] . '.ts', $langTsContent);
+        }
+    }
+
+    public static function writeFile($path, $content)
+    {
+        $path = path_transform($path);
+        if (!is_dir(dirname($path))) {
+            mkdir(dirname($path), 0755, true);
+        }
+        return file_put_contents($path, $content);
+    }
+
+    public static function buildModelAppend($append): string
+    {
+        if (!$append) return '';
+        $append = self::buildFormatSimpleArray($append);
+        return "\n" . self::tab() . "// 追加属性" . "\n" . self::tab() . "protected \$append = $append;\n";
+    }
+
+    public static function buildModelFieldType(array $fieldType): string
+    {
+        if (!$fieldType) return '';
+        $maxStrLang = 0;
+        foreach ($fieldType as $key => $item) {
+            $strLang    = strlen($key);
+            $maxStrLang = max($strLang, $maxStrLang);
+        }
+
+        $str = '';
+        foreach ($fieldType as $key => $item) {
+            $str .= self::tab(2) . "'$key'" . str_pad('=>', ($maxStrLang - strlen($key) + 3), ' ', STR_PAD_LEFT) . " '$item',\n";
+        }
+        return "\n" . self::tab() . "// 字段类型转换" . "\n" . self::tab() . "protected \$type = [\n" . rtrim($str, "\n") . "\n" . self::tab() . "];\n";
+    }
+
+    public static function writeModelFile(string $tablePk, array $fieldsMap, array $modelData, array $modelFile)
+    {
+        $modelData['pk']                 = $tablePk == 'id' ? '' : "\n" . self::tab() . "// 表主键\n" . self::tab() . 'protected $pk = ' . "'$tablePk';\n" . self::tab();
+        $modelData['autoWriteTimestamp'] = array_key_exists(self::$createTimeField, $fieldsMap) || array_key_exists(self::$updateTimeField, $fieldsMap) ? 'true' : 'false';
+        if ($modelData['autoWriteTimestamp'] == 'true') {
+            $modelData['createTime'] = array_key_exists(self::$createTimeField, $fieldsMap) ? '' : "\n" . self::tab() . "protected \$createTime = false;";
+            $modelData['updateTime'] = array_key_exists(self::$updateTimeField, $fieldsMap) ? '' : "\n" . self::tab() . "protected \$updateTime = false;";
+        }
+        $modelMethodList        = isset($modelData['relationMethodList']) ? array_merge($modelData['methods'], $modelData['relationMethodList']) : $modelData['methods'];
+        $modelData['methods']   = $modelMethodList ? "\n" . implode("\n", $modelMethodList) : '';
+        $modelData['append']    = self::buildModelAppend($modelData['append']);
+        $modelData['fieldType'] = self::buildModelFieldType($modelData['fieldType']);
+
+        // 生成雪花ID?
+        if (isset($modelData['beforeInsertMixins']['snowflake'])) {
+            // beforeInsert 组装
+            $modelData['beforeInsert'] = Helper::assembleStub('mixins/model/beforeInsert', [
+                'setSnowFlakeIdCode' => $modelData['beforeInsertMixins']['snowflake']
+            ]);
+        }
+        if ($modelData['afterInsert'] && $modelData['beforeInsert']) {
+            $modelData['afterInsert'] = "\n" . $modelData['afterInsert'];
+        }
+
+        $modelFileContent = self::assembleStub('mixins/model/model', $modelData);
+        self::writeFile($modelFile['parseFile'], $modelFileContent);
+    }
+
+    public static function writeControllerFile(array $controllerData, array $controllerFile)
+    {
+        if (isset($controllerData['relationVisibleFieldList']) && $controllerData['relationVisibleFieldList']) {
+            $relationVisibleFields = '$res->visible([';
+            foreach ($controllerData['relationVisibleFieldList'] as $cKey => $controllerDatum) {
+                $relationVisibleFields .= "'$cKey' => ['" . implode("','", $controllerDatum) . "'],";
+            }
+            $relationVisibleFields = rtrim($relationVisibleFields, ',');
+            $relationVisibleFields .= ']);';
+            // 重写index
+            $controllerData['methods']['index'] = self::assembleStub('mixins/controller/index', [
+                'relationVisibleFields' => $relationVisibleFields
+            ]);
+            unset($controllerData['relationVisibleFieldList']);
+        }
+        $controllerAttr = '';
+        foreach ($controllerData['attr'] as $key => $item) {
+            if (is_array($item)) {
+                $controllerAttr .= "\n" . self::tab() . "protected \$$key = ['" . implode("', '", $item) . "'];\n";
+            } elseif ($item) {
+                $controllerAttr .= "\n" . self::tab() . "protected \$$key = '$item';\n";
+            }
+        }
+        $controllerData['attr']       = $controllerAttr;
+        $controllerData['initialize'] = self::assembleStub('mixins/controller/initialize', [
+            'modelNamespace' => $controllerData['modelNamespace'],
+            'modelName'      => $controllerData['modelName'],
+            'filterRule'     => $controllerData['filterRule'],
+        ]);
+        $contentFileContent           = self::assembleStub('mixins/controller/controller', $controllerData);
+        self::writeFile($controllerFile['parseFile'], $contentFileContent);
+    }
+
+    public static function writeFormFile($formVueData, $webViewsDir, $fields, $webTranslate)
+    {
+        $fieldHtml                = "\n";
+        $formVueData['bigDialog'] = $formVueData['bigDialog'] ? "\n" . self::tab(2) . 'width="50%"' : '';
+        foreach ($formVueData['formFields'] as $field) {
+            $fieldHtml .= self::tab(5) . "<FormItem";
+            foreach ($field as $key => $attr) {
+                if (is_array($attr)) {
+                    $fieldHtml .= ' ' . $key . '="' . self::getJsonFromArray($attr) . '"';
+                } else {
+                    $fieldHtml .= ' ' . $key . '="' . $attr . '"';
+                }
+            }
+            $fieldHtml .= " />\n";
+        }
+        $formVueData['formFields'] = rtrim($fieldHtml, "\n");
+
+        // 表单验证规则
+        $formValidatorRules = [];
+        foreach ($fields as $field) {
+            if (isset($field['form']['validator'])) {
+                foreach ($field['form']['validator'] as $item) {
+                    $message = '';
+                    if (isset($field['form']['validatorMsg']) && $field['form']['validatorMsg']) {
+                        $message = ", message: '{$field['form']['validatorMsg']}'";
+                    }
+                    $formValidatorRules[$field['name']][] = "buildValidatorData({ name: '$item', title: t('$webTranslate{$field['name']}')$message })";
+                }
+            }
+        }
+        $formVueData['formItemRules'] = self::buildFormValidatorRules($formValidatorRules);
+        $formVueContent               = self::assembleStub('html/form', $formVueData);
+        self::writeFile(root_path() . $webViewsDir['views'] . '/' . 'popupForm.vue', $formVueContent);
+    }
+
+    public static function buildFormValidatorRules($formValidatorRules): string
+    {
+        $rulesHtml = "";
+        foreach ($formValidatorRules as $key => $formItemRule) {
+            $rulesArrHtml = '';
+            foreach ($formItemRule as $item) {
+                $rulesArrHtml .= $item . ', ';
+            }
+            $rulesHtml .= self::tab() . $key . ': [' . rtrim($rulesArrHtml, ', ') . "],\n";
+        }
+        return $rulesHtml ? "\n" . $rulesHtml : '';
+    }
+
+    public static function writeIndexFile($indexVueData, $webViewsDir, $controllerFile)
+    {
+        $indexVueData['optButtons']            = self::buildSimpleArray($indexVueData['optButtons']);
+        $indexVueData['defaultItems']          = self::getJsonFromArray($indexVueData['defaultItems']);
+        $indexVueData['tableColumn']           = self::buildTableColumn($indexVueData['tableColumn']);
+        $indexVueData['dblClickNotEditColumn'] = self::buildSimpleArray($indexVueData['dblClickNotEditColumn']);
+        $controllerFile['path'][]              = $controllerFile['originalLastName'];
+        $indexVueData['controllerUrl']         = '\'/admin/' . ($controllerFile['path'] ? implode('.', $controllerFile['path']) : '') . '/\'';
+        $indexVueData['componentName']         = ($webViewsDir['path'] ? implode('/', $webViewsDir['path']) . '/' : '') . $webViewsDir['originalLastName'];
+        $indexVueContent                       = self::assembleStub('html/index', $indexVueData);
+        self::writeFile(root_path() . $webViewsDir['views'] . '/' . 'index.vue', $indexVueContent);
+    }
+
+    public static function buildTableColumn($tableColumnList): string
+    {
+        $columnJson = '';
+        foreach ($tableColumnList as $column) {
+            $columnJson .= self::tab(3) . '{';
+            foreach ($column as $key => $item) {
+                $columnJson .= self::buildTableColumnKey($key, $item);
+            }
+            $columnJson = rtrim($columnJson, ',');
+            $columnJson .= ' }' . ",\n";
+        }
+        return rtrim($columnJson, "\n");
+    }
+
+    public static function buildTableColumnKey($key, $item): string
+    {
+        $key = self::formatObjectKey($key);
+        if (is_array($item)) {
+            $itemJson = ' ' . $key . ': {';
+            foreach ($item as $ik => $iitem) {
+                $itemJson .= self::buildTableColumnKey($ik, $iitem);
+            }
+            $itemJson = rtrim($itemJson, ',');
+            $itemJson .= ' }';
+        } else {
+            if ($item === 'false') {
+                $itemJson = ' ' . $key . ': false,';
+            } elseif (in_array($key, ['label', 'width', 'buttons'], true) || strpos($item, "t('") === 0 || strpos($item, "t(\"") === 0) {
+                $itemJson = ' ' . $key . ': ' . $item . ',';
+            } else {
+                $itemJson = ' ' . $key . ': \'' . $item . '\',';
+            }
+        }
+        return $itemJson;
+    }
+
+    public static function formatObjectKey(string $keyName): string
+    {
+        if (preg_match("/^[a-zA-Z0-9_]+$/", $keyName)) {
+            return $keyName;
+        } else {
+            $quote = self::getQuote($keyName);
+            return "$quote$keyName$quote";
+        }
+    }
+
+    public static function getQuote(string $value): string
+    {
+        return stripos($value, "'") === false ? "'" : '"';
+    }
+
+    public static function buildFormatSimpleArray($arr, int $tab = 2): string
+    {
+        if (!$arr) return '[]';
+        $str = '[' . PHP_EOL;
+        foreach ($arr as $item) {
+            if ($item == 'undefined' || $item == 'false' || is_numeric($item)) {
+                $str .= self::tab($tab) . $item . ',' . PHP_EOL;
+            } else {
+                $quote = self::getQuote($item);
+                $str   .= self::tab($tab) . "$quote$item$quote," . PHP_EOL;
+            }
+        }
+        return $str . self::tab($tab - 1) . ']';
+    }
+
+    public static function buildSimpleArray($arr): string
+    {
+        if (!$arr) return '[]';
+        $str = '';
+        foreach ($arr as $item) {
+            if ($item == 'undefined' || $item == 'false' || is_numeric($item)) {
+                $str .= $item . ', ';
+            } else {
+                $quote = self::getQuote($item);
+                $str   .= "$quote$item$quote, ";
+            }
+        }
+        return '[' . rtrim($str, ", ") . ']';
+    }
+
+    public static function buildDefaultOrder(string $field, string $type): string
+    {
+        if ($field && $type) {
+            $defaultOrderStub = [
+                'prop'  => $field,
+                'order' => $type,
+            ];
+            $defaultOrderStub = self::getJsonFromArray($defaultOrderStub);
+            if ($defaultOrderStub) {
+                return "\n" . self::tab(2) . "defaultOrder: " . $defaultOrderStub . ',';
+            }
+        }
+        return '';
+    }
+
+    public static function getJsonFromArray($arr)
+    {
+        if (is_array($arr)) {
+            $jsonStr = '';
+            foreach ($arr as $key => $item) {
+                $keyStr = ' ' . self::formatObjectKey($key) . ': ';
+                if (is_array($item)) {
+                    $jsonStr .= $keyStr . self::getJsonFromArray($item) . ',';
+                } elseif ($item === 'false' || $item === 'true') {
+                    $jsonStr .= $keyStr . ($item === 'false' ? 'false' : 'true') . ',';
+                } elseif ($item === null) {
+                    $jsonStr .= $keyStr . 'null,';
+                } elseif (strpos($item, "t('") === 0 || strpos($item, "t(\"") === 0 || $item == '[]' || in_array(gettype($item), ['integer', 'double'])) {
+                    $jsonStr .= $keyStr . $item . ',';
+                } elseif (isset($item[0]) && $item[0] == '[' && substr($item, -1, 1) == ']') {
+                    $jsonStr .= $keyStr . $item . ',';
+                } else {
+                    $quote   = self::getQuote($item);
+                    $jsonStr .= $keyStr . "$quote$item$quote,";
+                }
+            }
+            return $jsonStr ? '{' . rtrim($jsonStr, ',') . ' }' : '{}';
+        } else {
+            return $arr;
+        }
+    }
+
+}

+ 60 - 0
app/admin/library/crud/stubs/html/form.stub

@@ -0,0 +1,60 @@
+<template>
+    <!-- 对话框表单 -->
+    <el-dialog
+        class="ba-operate-dialog"
+        :close-on-click-modal="false"
+        :model-value="['add', 'edit'].includes(baTable.form.operate!)"
+        @close="baTable.toggleForm"{%bigDialog%}
+    >
+        <template #header>
+            <div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
+                {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
+            </div>
+        </template>
+        <el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
+            <div
+                class="ba-operate-form"
+                :class="'ba-' + baTable.form.operate + '-form'"
+                :style="'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
+            >
+                <el-form
+                    v-if="!baTable.form.loading"
+                    ref="formRef"
+                    @submit.prevent=""
+                    @keyup.enter="baTable.onSubmit(formRef)"
+                    :model="baTable.form.items"
+                    label-position="right"
+                    :label-width="baTable.form.labelWidth + 'px'"
+                    :rules="rules"
+                >{%formFields%}
+                </el-form>
+            </div>
+        </el-scrollbar>
+        <template #footer>
+            <div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
+                <el-button @click="baTable.toggleForm('')">{{ t('Cancel') }}</el-button>
+                <el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
+                    {{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
+                </el-button>
+            </div>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, inject } from 'vue'
+import { useI18n } from 'vue-i18n'
+import type baTableClass from '/@/utils/baTable'
+import FormItem from '/@/components/formItem/index.vue'
+import type { ElForm, FormItemRule } from 'element-plus'
+import { buildValidatorData } from '/@/utils/validate'
+
+const formRef = ref<InstanceType<typeof ElForm>>()
+const baTable = inject('baTable') as baTableClass
+
+const { t } = useI18n()
+
+const rules: Partial<Record<string, FormItemRule[]>> = reactive({{%formItemRules%}})
+</script>
+
+<style scoped lang="scss"></style>

+ 66 - 0
app/admin/library/crud/stubs/html/index.stub

@@ -0,0 +1,66 @@
+<template>
+    <div class="default-main ba-table-box">
+        <el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
+
+        <!-- 表格顶部菜单 -->
+        <TableHeader
+            :buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
+            :quick-search-placeholder="t('quick Search Placeholder', { fields: t('{%webTranslate%}quick Search Fields') })"
+        />
+
+        <!-- 表格 -->
+        <!-- 要使用`el-table`组件原有的属性,直接加在Table标签上即可 -->
+        <Table ref="tableRef" />
+
+        <!-- 表单 -->
+        <PopupForm />
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref, provide, onMounted } from 'vue'
+import baTableClass from '/@/utils/baTable'
+import { defaultOptButtons } from '/@/components/table'
+import { baTableApi } from '/@/api/common'
+import { useI18n } from 'vue-i18n'
+import PopupForm from './popupForm.vue'
+import Table from '/@/components/table/index.vue'
+import TableHeader from '/@/components/table/header/index.vue'
+
+const { t } = useI18n()
+const tableRef = ref()
+const optButtons = defaultOptButtons({%optButtons%})
+const baTable = new baTableClass(
+    new baTableApi({%controllerUrl%}),
+    {
+        pk: '{%tablePk%}',
+        column: [
+{%tableColumn%}
+        ],
+        dblClickNotEditColumn: {%dblClickNotEditColumn%},{%defaultOrder%}
+    },
+    {
+        defaultItems: {%defaultItems%},
+    }
+)
+
+provide('baTable', baTable)
+
+onMounted(() => {
+    baTable.table.ref = tableRef.value
+    baTable.mount()
+    baTable.getIndex()?.then(() => {
+        baTable.initSort()
+        baTable.dragSort()
+    })
+})
+</script>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+export default defineComponent({
+    name: '{%componentName%}',
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 24 - 0
app/admin/library/crud/stubs/mixins/controller/controller.stub

@@ -0,0 +1,24 @@
+<?php
+
+namespace {%namespace%};
+
+use app\common\controller\Backend;
+
+/**
+ * {%tableComment%}
+ *
+ */
+class {%className%} extends Backend
+{
+    /**
+     * {%modelName%}模型对象
+     * @var \{%modelNamespace%}\{%modelName%}
+     */
+    protected $model = null;
+    {%attr%}{%initialize%}
+{%methods%}
+
+    /**
+     * 若需重写查看、编辑、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应的方法至此进行重写
+     */
+}

+ 27 - 0
app/admin/library/crud/stubs/mixins/controller/index.stub

@@ -0,0 +1,27 @@
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        $this->request->filter(['strip_tags', 'trim']);
+        // 如果是select则转发到select方法,若select未重写,其实还是继续执行index
+        if ($this->request->param('select')) {
+            $this->select();
+        }
+
+        list($where, $alias, $limit, $order) = $this->queryBuilder();
+        $res = $this->model
+            ->withJoin($this->withJoinTable, $this->withJoinType)
+            ->alias($alias)
+            ->where($where)
+            ->order($order)
+            ->paginate($limit);
+        {%relationVisibleFields%}
+
+        $this->success('', [
+            'list'   => $res->items(),
+            'total'  => $res->total(),
+            'remark' => get_route_remark(),
+        ]);
+    }

+ 6 - 0
app/admin/library/crud/stubs/mixins/controller/initialize.stub

@@ -0,0 +1,6 @@
+
+    public function initialize()
+    {
+        parent::initialize();
+        $this->model = new \{%modelNamespace%}\{%modelName%};{%filterRule%}
+    }

+ 12 - 0
app/admin/library/crud/stubs/mixins/model/afterInsert.stub

@@ -0,0 +1,12 @@
+
+    protected static function onAfterInsert($model)
+    {
+        if ($model->{%field%} == 0) {
+            $pk = $model->getPk();
+            if (strlen($model[$pk]) >= 19) {
+                $model->where($pk, $model[$pk])->update(['{%field%}' => $model->count()]);
+            } else {
+                $model->where($pk, $model[$pk])->update(['{%field%}' => $model[$pk]]);
+            }
+        }
+    }

+ 5 - 0
app/admin/library/crud/stubs/mixins/model/beforeInsert.stub

@@ -0,0 +1,5 @@
+
+    protected static function onBeforeInsert($model)
+    {
+{%setSnowFlakeIdCode%}
+    }

+ 5 - 0
app/admin/library/crud/stubs/mixins/model/belongsTo.stub

@@ -0,0 +1,5 @@
+
+    public function {%relationMethod%}()
+    {
+        return $this->{%relationMode%}({%relationClassName%}, '{%relationForeignKey%}', '{%relationPrimaryKey%}');
+    }

+ 7 - 0
app/admin/library/crud/stubs/mixins/model/getters/cityNames.stub

@@ -0,0 +1,7 @@
+
+    public function get{%field%}Attr($value, $row): string
+    {
+        if ($row['{%originalFieldName%}'] === '' || $row['{%originalFieldName%}'] === null) return '';
+        $cityNames = \think\facade\Db::name('area')->whereIn('id', $row['{%originalFieldName%}'])->column('name');
+        return $cityNames ? implode(',', $cityNames) : '';
+    }

+ 5 - 0
app/admin/library/crud/stubs/mixins/model/getters/float.stub

@@ -0,0 +1,5 @@
+
+    public function get{%field%}Attr($value): float
+    {
+        return (float)$value;
+    }

+ 5 - 0
app/admin/library/crud/stubs/mixins/model/getters/htmlDecode.stub

@@ -0,0 +1,5 @@
+
+    public function get{%field%}Attr($value): string
+    {
+        return !$value ? '' : htmlspecialchars_decode($value);
+    }

+ 5 - 0
app/admin/library/crud/stubs/mixins/model/getters/jsonDecode.stub

@@ -0,0 +1,5 @@
+
+    public function get{%field%}Attr($value): array
+    {
+        return !$value ? [] : json_decode($value, true);
+    }

+ 7 - 0
app/admin/library/crud/stubs/mixins/model/getters/remoteSelectLabels.stub

@@ -0,0 +1,7 @@
+
+    public function get{%field%}Attr($value, $row): array
+    {
+        return [
+            '{%labelFieldName%}' => {%className%}::whereIn('{%primaryKey%}', $row['{%foreignKey%}'])->column('{%labelFieldName%}'),
+        ];
+    }

+ 5 - 0
app/admin/library/crud/stubs/mixins/model/getters/string.stub

@@ -0,0 +1,5 @@
+
+    public function get{%field%}Attr($value): string
+    {
+        return (string)$value;
+    }

+ 9 - 0
app/admin/library/crud/stubs/mixins/model/getters/stringToArray.stub

@@ -0,0 +1,9 @@
+
+    public function get{%field%}Attr($value): array
+    {
+        if ($value === '' || $value === null) return [];
+        if (!is_array($value)) {
+            return explode(',', $value);
+        }
+        return $value;
+    }

+ 2 - 0
app/admin/library/crud/stubs/mixins/model/mixins/beforeInsertWithSnowflake.stub

@@ -0,0 +1,2 @@
+        $pk         = $model->getPk();
+        $model->$pk = \app\common\library\SnowFlake::generateParticle();

+ 18 - 0
app/admin/library/crud/stubs/mixins/model/model.stub

@@ -0,0 +1,18 @@
+<?php
+
+namespace {%namespace%};
+
+use think\Model;
+
+/**
+ * {%className%}
+ */
+class {%className%} extends Model
+{{%pk%}
+    // 表名
+    protected $name = '{%name%}';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = {%autoWriteTimestamp%};{%createTime%}{%updateTime%}
+{%append%}{%fieldType%}{%beforeInsert%}{%afterInsert%}{%methods%}
+}

+ 5 - 0
app/admin/library/crud/stubs/mixins/model/setters/arrayToString.stub

@@ -0,0 +1,5 @@
+
+    public function set{%field%}Attr($value): string
+    {
+        return is_array($value) ? implode(',', $value) : $value;
+    }

+ 5 - 0
app/admin/library/crud/stubs/mixins/model/setters/time.stub

@@ -0,0 +1,5 @@
+
+    public function set{%field%}Attr($value): string
+    {
+        return $value ? date('H:i:s', strtotime($value)) : '';
+    }

+ 31 - 0
app/admin/library/crud/stubs/mixins/validate/validate.stub

@@ -0,0 +1,31 @@
+<?php
+
+namespace {%namespace%};
+
+use think\Validate;
+
+class {%className%} extends Validate
+{
+    protected $failException = true;
+
+    /**
+     * 验证规则
+     */
+    protected $rule = [
+    ];
+
+    /**
+     * 提示消息
+     */
+    protected $message = [
+    ];
+
+    /**
+     * 验证场景
+     */
+    protected $scene = [
+        'add'  => [],
+        'edit' => [],
+    ];
+
+}

+ 249 - 0
app/admin/library/traits/Backend.php

@@ -0,0 +1,249 @@
+<?php
+
+namespace app\admin\library\traits;
+
+use Exception;
+use think\facade\Config;
+use think\facade\Db;
+use think\db\exception\PDOException;
+use think\exception\ValidateException;
+
+/**
+ * 后台控制器trait类
+ * 已导入到 @var \app\common\controller\Backend 中
+ * 若需修改此类方法:请复制方法至对应控制器后进行重写
+ */
+trait Backend
+{
+    /**
+     * 排除入库字段
+     * @param $params
+     * @return mixed
+     */
+    protected function excludeFields($params)
+    {
+        if (!is_array($this->preExcludeFields)) {
+            $this->preExcludeFields = explode(',', (string)$this->preExcludeFields);
+        }
+
+        foreach ($this->preExcludeFields as $field) {
+            if (array_key_exists($field, $params)) {
+                unset($params[$field]);
+            }
+        }
+        return $params;
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        $this->request->filter(['strip_tags', 'trim']);
+        if ($this->request->param('select')) {
+            $this->select();
+        }
+
+        list($where, $alias, $limit, $order) = $this->queryBuilder();
+        $res = $this->model
+            ->field($this->indexField)
+            ->withJoin($this->withJoinTable, $this->withJoinType)
+            ->alias($alias)
+            ->where($where)
+            ->order($order)
+            ->paginate($limit);
+
+        $this->success('', [
+            'list'   => $res->items(),
+            'total'  => $res->total(),
+            'remark' => get_route_remark(),
+        ]);
+    }
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if ($this->request->isPost()) {
+            $data = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            $data = $this->excludeFields($data);
+            if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
+                $data[$this->dataLimitField] = $this->auth->id;
+            }
+
+            $result = false;
+            Db::startTrans();
+            try {
+                // 模型验证
+                if ($this->modelValidate) {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    if (class_exists($validate)) {
+                        $validate = new $validate;
+                        if ($this->modelSceneValidate) $validate->scene('add');
+                        $validate->check($data);
+                    }
+                }
+                $result = $this->model->save($data);
+                Db::commit();
+            } catch (ValidateException|PDOException|Exception $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('Added successfully'));
+            } else {
+                $this->error(__('No rows were added'));
+            }
+        }
+
+        $this->error(__('Parameter error'));
+    }
+
+    /**
+     * 编辑
+     */
+    public function edit()
+    {
+        $id  = $this->request->param($this->model->getPk());
+        $row = $this->model->find($id);
+        if (!$row) {
+            $this->error(__('Record not found'));
+        }
+
+        $dataLimitAdminIds = $this->getDataLimitAdminIds();
+        if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
+            $this->error(__('You have no permission'));
+        }
+
+        if ($this->request->isPost()) {
+            $data = $this->request->post();
+            if (!$data) {
+                $this->error(__('Parameter %s can not be empty', ['']));
+            }
+
+            $data   = $this->excludeFields($data);
+            $result = false;
+            Db::startTrans();
+            try {
+                // 模型验证
+                if ($this->modelValidate) {
+                    $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                    if (class_exists($validate)) {
+                        $validate = new $validate;
+                        if ($this->modelSceneValidate) $validate->scene('edit');
+                        $validate->check($data);
+                    }
+                }
+                $result = $row->save($data);
+                Db::commit();
+            } catch (ValidateException|PDOException|Exception $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($result !== false) {
+                $this->success(__('Update successful'));
+            } else {
+                $this->error(__('No rows updated'));
+            }
+
+        }
+
+        $this->success('', [
+            'row' => $row
+        ]);
+    }
+
+    /**
+     * 删除
+     * @param array $ids
+     */
+    public function del(array $ids = [])
+    {
+        if (!$this->request->isDelete() || !$ids) {
+            $this->error(__('Parameter error'));
+        }
+
+        $dataLimitAdminIds = $this->getDataLimitAdminIds();
+        if ($dataLimitAdminIds) {
+            $this->model->where($this->dataLimitField, 'in', $dataLimitAdminIds);
+        }
+
+        $pk    = $this->model->getPk();
+        $data  = $this->model->where($pk, 'in', $ids)->select();
+        $count = 0;
+        Db::startTrans();
+        try {
+            foreach ($data as $v) {
+                $count += $v->delete();
+            }
+            Db::commit();
+        } catch (PDOException|Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        if ($count) {
+            $this->success(__('Deleted successfully'));
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+    }
+
+    /**
+     * 排序
+     * @param int $id       排序主键值
+     * @param int $targetId 排序位置主键值
+     */
+    public function sortable(int $id, int $targetId)
+    {
+        $dataLimitAdminIds = $this->getDataLimitAdminIds();
+        if ($dataLimitAdminIds) {
+            $this->model->where($this->dataLimitField, 'in', $dataLimitAdminIds);
+        }
+
+        $row    = $this->model->find($id);
+        $target = $this->model->find($targetId);
+
+        if (!$row || !$target) {
+            $this->error(__('Record not found'));
+        }
+        if ($row[$this->weighField] == $target[$this->weighField]) {
+            $autoSortEqWeight = is_null($this->autoSortEqWeight) ? Config::get('buildadmin.auto_sort_eq_weight') : $this->autoSortEqWeight;
+            if (!$autoSortEqWeight) {
+                $this->error(__('Invalid collation because the weights of the two targets are equal'));
+            }
+
+            // 自动重新整理排序
+            $all = $this->model->select();
+            foreach ($all as $item) {
+                $item[$this->weighField] = $item[$this->model->getPk()];
+                $item->save();
+            }
+            unset($all);
+            // 重新获取
+            $row    = $this->model->find($id);
+            $target = $this->model->find($targetId);
+        }
+
+        $ebak                      = $target[$this->weighField];
+        $target[$this->weighField] = $row[$this->weighField];
+        $row[$this->weighField]    = $ebak;
+        $row->save();
+        $target->save();
+
+        $this->success();
+    }
+
+    /**
+     * 加载为select(远程下拉选择框)数据,默认还是走$this->index()方法
+     * 必要时请在对应控制器类中重写
+     */
+    public function select()
+    {
+
+    }
+}

Some files were not shown because too many files changed in this diff