afa 5 ヶ月 前
コミット
411dfb15bc
100 ファイル変更13350 行追加0 行削除
  1. 11 0
      .env
  2. 201 0
      LICENSE
  3. 45 0
      README.md
  4. 129 0
      app/admin/command/Queue.php
  5. 17 0
      app/admin/command/queueEvent/EventInterFace.php
  6. 26 0
      app/admin/command/queueEvent/SendMsg.php
  7. 14 0
      app/admin/command/queueEvent/Test.php
  8. 1 0
      app/admin/command/queueEvent/lock.txt
  9. 1 0
      app/admin/command/queueEvent/time.txt
  10. 43 0
      app/admin/common.php
  11. 19 0
      app/admin/config/session.php
  12. 25 0
      app/admin/config/view.php
  13. 59 0
      app/admin/config/yunqi.php
  14. 335 0
      app/admin/controller/Addons.php
  15. 298 0
      app/admin/controller/Ajax.php
  16. 119 0
      app/admin/controller/Dashboard.php
  17. 383 0
      app/admin/controller/Develop.php
  18. 145 0
      app/admin/controller/Index.php
  19. 255 0
      app/admin/controller/auth/Admin.php
  20. 94 0
      app/admin/controller/auth/Adminlog.php
  21. 47 0
      app/admin/controller/auth/Depart.php
  22. 203 0
      app/admin/controller/auth/Group.php
  23. 143 0
      app/admin/controller/auth/Rule.php
  24. 135 0
      app/admin/controller/general/Attachment.php
  25. 93 0
      app/admin/controller/general/Category.php
  26. 208 0
      app/admin/controller/general/Config.php
  27. 91 0
      app/admin/controller/general/Profile.php
  28. 89 0
      app/admin/controller/user/Index.php
  29. 39 0
      app/admin/lang/en-us.php
  30. 18 0
      app/admin/lang/zh-cn.php
  31. 39 0
      app/admin/lang/zh-tw.php
  32. 7 0
      app/admin/middleware.php
  33. 31 0
      app/admin/middleware/Redirect.php
  34. 24 0
      app/admin/route/route.php
  35. 523 0
      app/admin/service/AdminAuthService.php
  36. 759 0
      app/admin/service/addons/AddonsService.php
  37. 170 0
      app/admin/service/addons/eof.php
  38. 47 0
      app/admin/service/addons/install.txt
  39. 475 0
      app/admin/service/curd/CurdService.php
  40. 212 0
      app/admin/service/curd/controller-normal.txt
  41. 106 0
      app/admin/service/curd/controller-reduced.txt
  42. 281 0
      app/admin/service/curd/eof.php
  43. 62 0
      app/admin/service/curd/js-add.txt
  44. 109 0
      app/admin/service/curd/js-index.txt
  45. 29 0
      app/admin/service/curd/js.txt
  46. 24 0
      app/admin/service/curd/model-extend-base.txt
  47. 53 0
      app/admin/service/curd/model-normal.txt
  48. 29 0
      app/admin/service/curd/view-add.txt
  49. 15 0
      app/admin/service/curd/view-edit.txt
  50. 75 0
      app/admin/service/curd/view-index.txt
  51. 10 0
      app/admin/service/curd/view-method.txt
  52. 532 0
      app/admin/traits/Actions.php
  53. 99 0
      app/admin/view/addons/create.html
  54. 446 0
      app/admin/view/addons/index.html
  55. 61 0
      app/admin/view/addons/uninstall.html
  56. 101 0
      app/admin/view/auth/admin/add.html
  57. 1 0
      app/admin/view/auth/admin/edit.html
  58. 180 0
      app/admin/view/auth/admin/index.html
  59. 32 0
      app/admin/view/auth/adminlog/detail.html
  60. 67 0
      app/admin/view/auth/adminlog/index.html
  61. 47 0
      app/admin/view/auth/depart/add.html
  62. 1 0
      app/admin/view/auth/depart/edit.html
  63. 71 0
      app/admin/view/auth/depart/index.html
  64. 128 0
      app/admin/view/auth/group/add.html
  65. 1 0
      app/admin/view/auth/group/edit.html
  66. 91 0
      app/admin/view/auth/group/index.html
  67. 101 0
      app/admin/view/auth/rule/add.html
  68. 1 0
      app/admin/view/auth/rule/edit.html
  69. 122 0
      app/admin/view/auth/rule/index.html
  70. 10 0
      app/admin/view/common/meta.html
  71. 41 0
      app/admin/view/common/recyclebin.html
  72. 462 0
      app/admin/view/dashboard/index.html
  73. 18 0
      app/admin/view/dashboard/platform1.html
  74. 18 0
      app/admin/view/dashboard/platform2.html
  75. 45 0
      app/admin/view/develop/addQueue.html
  76. 1861 0
      app/admin/view/develop/crud.html
  77. 236 0
      app/admin/view/develop/queue.html
  78. 166 0
      app/admin/view/general/attachment/add.html
  79. 129 0
      app/admin/view/general/attachment/index.html
  80. 129 0
      app/admin/view/general/attachment/select.html
  81. 51 0
      app/admin/view/general/category/add.html
  82. 1 0
      app/admin/view/general/category/edit.html
  83. 64 0
      app/admin/view/general/category/index.html
  84. 327 0
      app/admin/view/general/config/index.html
  85. 94 0
      app/admin/view/general/profile/index.html
  86. 81 0
      app/admin/view/index/index.html
  87. 346 0
      app/admin/view/index/login.html
  88. 47 0
      app/admin/view/layout/index/classic/index.html
  89. 62 0
      app/admin/view/layout/index/columns/index.html
  90. 3 0
      app/admin/view/layout/index/footer.html
  91. 8 0
      app/admin/view/layout/index/rightbar.html
  92. 48 0
      app/admin/view/layout/index/transverse/index.html
  93. 45 0
      app/admin/view/layout/index/vertical/index.html
  94. 34 0
      app/admin/view/layout/vue.html
  95. 51 0
      app/admin/view/user/index/detail.html
  96. 32 0
      app/admin/view/user/index/edit.html
  97. 97 0
      app/admin/view/user/index/index.html
  98. 55 0
      app/admin/view/user/index/recharge.html
  99. 68 0
      app/admin/view/user/index/test.html
  100. 273 0
      app/common.php

+ 11 - 0
.env

@@ -0,0 +1,11 @@
+APP_DEBUG = true
+APP_BACKEND = iDGVSIg
+
+DB_TYPE = mysql
+DB_HOST = 127.0.0.1
+DB_NAME = test_jujin
+DB_USER = root
+DB_PASS = 123456
+DB_PORT = 3306
+DB_PREFIX = yun_
+DB_CHARSET = utf8mb4

+ 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 [yyyy] [name of copyright owner]
+
+   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.

+ 45 - 0
README.md

@@ -0,0 +1,45 @@
+<h3></h3>
+<h3 align="center">❤❤❤行到水穷处,坐看云起时❤❤❤</h3>
+<h3 align="center">云起开发平台,用一次就离不开的快速开发框架</h3>
+<h3 align="center">虽然咱们也用VUE,但开发无需打包</h3>
+<h3 align="center">虽然咱们前后端不分离,但照样用JSON声明式设计界面</h3>
+<h3 align="center">没有复杂繁琐的js,响应更加快速,编程更加简单</h3>
+<h3></h3>
+<p style="text-align:center;">
+<img src="https://www.56q7.com/upload/04/20231012164034.png" width="45%"/>
+<img src="https://www.56q7.com/upload/04/20231012164040.png" width="45%"/>
+</p>
+
+**官方网址:https://www.56q7.com/**
+
+**体验地址:https://demo.56q7.com/backend**
+
+**框架文档:https://48rmn452q3.k.topthink.com/@48rmwkl52q/xitongjieshao.html**
+
+**开源社区:https://bbs.56q7.com/**
+
+**QQ讨论群:237626046**
+
+## 环境或依赖规范
+* PHP >= 8.2
+* Thinkphp 8.0+
+* Mysql 5.7及以上
+* Vue3.2
+* ES6语法
+* ElementPlus2.3
+
+## 快速安装
+1. 两种下载方式:(1)登陆官网,下载稳定版本,已经打包好了composer依赖,解压到apache或nginx配置的网站目录。 (2)使用git clone命令在gitee或github中克隆到你的网站目录。
+1. 如果你使用宝塔,请配置好网站的运行目录。
+1. 配置好伪静态。
+1. 如果你使用git clone的项目,运行 composer install,下载依赖包,官网下载完整包的可以忽略。
+1. 在浏览器中运行:http://你的域名/install/install.php
+1. 根据提示完成安装。
+## 系统特色
+* 🚀一键CRUD,通过网页可视化编辑配置,生成菜单,表格,表单等功能以及所有文件和代码
+* 🍜对单表,内置了查看、添加、修改、删除、更新、导入、下载、回收站功能
+* 🚩注解方式路由控制
+* 🥰‍‍重写了thinkphp的模板引擎,使用后端渲染vue页面,无需编译打包,但仍能采用模块化编程,单页应用支持onLoad,onShow,onUnload,onHide,界面可以像uniapp一样写代码。
+* 🧊支持打包系统功能成为一个模块,换个项目一键安装,还可以作为扩展直接发布销售。
+* 📱手机端,PC端,平板响应式适配,一样的体验效果。
+* 🗒️全部使用ES6语法

+ 129 - 0
app/admin/command/Queue.php

@@ -0,0 +1,129 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types=1);
+
+namespace app\admin\command;
+
+use app\common\model\Queue as QueueModel;
+use think\console\Command;
+use think\console\Input;
+use think\console\Output;
+
+class Queue extends Command
+{
+    protected $output;
+
+    private static $EventTime=[];
+
+    public static $timetxt=__DIR__.DIRECTORY_SEPARATOR.'queueEvent'.DIRECTORY_SEPARATOR.'time.txt';
+    public static $locktxt=__DIR__.DIRECTORY_SEPARATOR.'queueEvent'.DIRECTORY_SEPARATOR.'lock.txt';
+
+    private $breath=0;
+
+    //定义更新时间
+    const  refreshTime=300;
+
+    protected function configure()
+    {
+        $this->setName('Event')->setDescription('队列任务');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        $this->output=$output;
+        $this->getQueueList();
+        $this->output('启动队列服务');
+        while(true){
+            sleep(1);
+            $r=intval(file_get_contents(self::$timetxt));
+            if($r==0){
+                $this->output('关闭轮询任务服务');
+                (new QueueModel())->saveAll(self::$EventTime);
+                break;
+            }
+            if(file_exists(self::$locktxt)){
+                $r=intval(file_get_contents(self::$locktxt));
+                $this->output('更新了轮询任务');
+                unlink(self::$locktxt);
+            }
+            foreach (self::$EventTime as &$value){
+                $function=$value['function'];
+                $delay=$value['delay'];
+                $filter=$value['filter'];
+                if($value['limit']!==0 && $value['times']>=$value['limit']){
+                    continue;
+                }
+                if($value['status']!='normal'){
+                    continue;
+                }
+                $lasttime='';
+                if($value['lasttime']){
+                    $lasttime=strtotime($value['lasttime']);
+                }
+                try {
+                    if($this->runEvent($function,$delay,$filter,$lasttime)){
+                        $value['times']++;
+                        $value['error']='';
+                        $value['lasttime']=date('Y-m-d H:i:s');
+                    }
+                }catch (\Exception $e) {
+                    $this->output('执行出错:'.$e->getMessage());
+                    $value['times']++;
+                    $value['lasttime']=date('Y-m-d H:i:s');
+                    $value['error']=$e->getMessage();
+                    $value['status']='hidden';
+                }
+                if($value['filter']){
+                    $value['filter']=json_encode($value['filter']);
+                }
+            }
+            //每5分钟更新一次数据库
+            if($r%self::refreshTime===0){
+                (new QueueModel())->saveAll(self::$EventTime);
+                $this->getQueueList();
+            }
+            $this->breath++;
+            file_put_contents(self::$timetxt,$this->breath);
+        }
+    }
+
+    private function getQueueList()
+    {
+        $list=QueueModel::alias('queue')->whereRaw("queue.limit=0 or queue.limit>queue.times")->select()->toArray();
+        self::$EventTime=$list;
+    }
+
+    private function runEvent($event,$time,$filter,$lasttime):bool
+    {
+        $now=time();
+        if($filter){
+            foreach ($filter as $key=>$fx){
+                if(date($key,$now)!=$fx){
+                    return false;
+                }
+            }
+        }
+        if($lasttime && $lasttime+$time>$now){
+            return false;
+        }
+        $class="\\app\\admin\\command\\queueEvent\\".$event;
+        if(!class_exists($class)){
+            throw new \Exception('处理类'.$event.'不存在');
+        }else{
+            $class::handle($this->output);
+            return true;
+        }
+    }
+
+    private function output($msg)
+    {
+        $this->output->info(date('Y-m-d H:i:s').'-'.$msg);
+    }
+}

+ 17 - 0
app/admin/command/queueEvent/EventInterFace.php

@@ -0,0 +1,17 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types=1);
+
+namespace app\admin\command\queueEvent;
+
+interface EventInterFace
+{
+    public static function handle($output);
+}

+ 26 - 0
app/admin/command/queueEvent/SendMsg.php

@@ -0,0 +1,26 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types=1);
+
+namespace app\admin\command\queueEvent;
+
+use app\common\model\Msg;
+
+class SendMsg implements EventInterFace
+{
+    public static function handle($output)
+    {
+        $list=Msg::where(['status'=>0])->select();
+        foreach ($list as $msg){
+            $handle=$msg->getHanlde();
+            $handle->send($msg);
+        }
+    }
+}

+ 14 - 0
app/admin/command/queueEvent/Test.php

@@ -0,0 +1,14 @@
+<?php
+declare(strict_types=1);
+
+namespace app\admin\command\queueEvent;
+
+use app\common\model\Msg;
+
+class Test implements EventInterFace
+{
+    public static function handle($output)
+    {
+        $output->info('测试dddddddddddddd');
+    }
+}

+ 1 - 0
app/admin/command/queueEvent/lock.txt

@@ -0,0 +1 @@
+774900

+ 1 - 0
app/admin/command/queueEvent/time.txt

@@ -0,0 +1 @@
+813499

+ 43 - 0
app/admin/common.php

@@ -0,0 +1,43 @@
+<?php
+declare (strict_types = 1);
+
+// 这是系统自动生成的公共文件
+
+if (!function_exists('build_var_json')) {
+
+    /**
+     * 将模板中通过assign的变量转换成json
+     *
+     * @return string
+     */
+    function build_var_json(array $arr):string
+    {
+        $keys=array_keys($arr['vars']);
+        $r=[];
+        foreach ($keys as $key){
+            if($key=='config' || $key=='auth' || $key=='upload'){
+                continue;
+            }
+            $r[$key]=$arr[$key];
+        }
+        return json_encode($r);
+    }
+}
+
+if (!function_exists('format_bytes')) {
+    /**
+     * 将字节转换为可读文本
+     * @param int    $size      大小
+     * @param string $delimiter 分隔符
+     * @param int    $precision 小数位数
+     * @return string
+     */
+    function format_bytes($size, $delimiter = '', $precision = 2)
+    {
+        $units = array('B', 'KB', 'MB', 'GB', 'TB', 'PB');
+        for ($i = 0; $size >= 1024 && $i < 6; $i++) {
+            $size /= 1024;
+        }
+        return round($size, $precision) . $delimiter . $units[$i];
+    }
+}

+ 19 - 0
app/admin/config/session.php

@@ -0,0 +1,19 @@
+<?php
+// +----------------------------------------------------------------------
+// | 会话设置
+// +----------------------------------------------------------------------
+
+return [
+    // session name
+    'name'           => 'YUNQI-SESSID',
+    // SESSION_ID的提交变量,解决flash上传跨域
+    'var_session_id' => '',
+    // 驱动方式 支持file cache
+    'type'           => 'file',
+    // 存储连接标识 当type使用cache的时候有效
+    'store'          => null,
+    // 过期时间
+    'expire'         => 3600 * 24,
+    // 前缀
+    'prefix'         => '',
+];

+ 25 - 0
app/admin/config/view.php

@@ -0,0 +1,25 @@
+<?php
+// +----------------------------------------------------------------------
+// | 模板设置
+// +----------------------------------------------------------------------
+
+return [
+    // 模板引擎类型使用Think
+    'type'          => '\app\common\library\Template',
+    // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法
+    'auto_rule'     => 3,
+    // 模板目录名
+    'view_dir_name' => 'view',
+    // 模板后缀
+    'view_suffix'   => 'html',
+    // 模板文件名分隔符
+    'view_depr'     => DIRECTORY_SEPARATOR,
+    // 模板引擎普通标签开始标记
+    'tpl_begin'     => '{',
+    // 模板引擎普通标签结束标记
+    'tpl_end'       => '}',
+    // 标签库标签开始标记
+    'taglib_begin'  => '{',
+    // 标签库标签结束标记
+    'taglib_end'    => '}',
+];

+ 59 - 0
app/admin/config/yunqi.php

@@ -0,0 +1,59 @@
+<?php
+// +----------------------------------------------------------------------
+// | 应用设置
+// +----------------------------------------------------------------------
+
+return [
+    //支持语言包
+    'language_list'    =>    [
+        'zh-cn'=>'中文简体',
+        'zh-tw'=>'中文繁體',
+        'en-us'=>'English'
+    ],
+    //默认语言包
+    'language'=>'zh-cn',
+    //登录验证码
+    'login_captcha' => true,
+    //登录失败超过10次则1天后重试
+    'login_failure_retry' => true,
+    //是否同一账号同一时间只能在一个地方登录
+    'login_unique' => false,
+    //是否开启IP变动检测
+    'loginip_check' => false,
+    //界面主题
+    'elementUi' => [
+        //布局
+        'layout' => 'vertical',
+        //主题颜色
+        'theme_color' => '#276EB8',
+        //暗黑模式
+        'dark' => false,
+        //面包屑
+        'breadcrumb' => true,
+        //折叠菜单
+        'is_menu_collapse' => false,
+        //选项卡
+        'tabs' => true,
+        //底部
+        'footer' => true,
+    ],
+    //上传文件
+    'upload'=>[
+        //上传地址
+        'uploadurl' => 'ajax/upload',
+        //上传适配器
+        'disks'   => 'local_public',
+        //最大可上传大小,单位mb
+        'maxsize'   => 20,
+        //可上传的文件类型
+        'mimetype'  => 'jpg,png,bmp,jpeg,gif,webp,zip,rar,wav,mp4,mp3,webm,doc,docx,xls,xlsx,pdf',
+        //生成缩略图
+        'thumb'=>true,
+        //压缩图片
+        'compress'=>true,
+        //图片加水印
+        'watermark'=>true
+    ],
+    //插件获取地址
+    'plugins_host'=>'https://www.56q7.com'
+];

+ 335 - 0
app/admin/controller/Addons.php

@@ -0,0 +1,335 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types=1);
+namespace app\admin\controller;
+
+use app\admin\service\addons\AddonsService;
+use app\common\controller\Backend;
+use app\common\library\QRcode;
+use app\common\model\Config;
+use think\annotation\route\Group;
+use think\annotation\route\Route;
+use app\common\model\Addons as AddonsModel;
+use think\facade\Db;
+use think\facade\Cache;
+
+function parseMenu(array $menu)
+{
+    $ids=[];
+    foreach ($menu as $value){
+        $ids[]=$value['id'];
+        if(isset($value['childlist'])){
+            $ids=array_merge($ids,parseMenu($value['childlist']));
+        }
+    }
+    return $ids;
+}
+
+#[Group("addons")]
+class Addons extends Backend
+{
+    protected $noNeedRight = ['*'];
+
+    /* @var AddonsService $service*/
+    private $service;
+
+    protected function _initialize()
+    {
+        parent::_initialize();
+        if(!$this->auth->isSuperAdmin()){
+            $this->error(__('超级管理员才能访问'));
+        }
+        $this->service=AddonsService::newInstance();
+    }
+
+    #[Route('GET,JSON','index')]
+    public function index()
+    {
+        if($this->request->isAjax()){
+            $type=$this->filter('type');
+            $plain=$this->filter('plain');
+            $page=$this->request->post('page/d',1);
+            $limit=$this->request->post('limit/d',10);
+            $keywords=$this->request->post('searchValue/s');
+            try {
+                if($plain=='local'){
+                    $where=[];
+                    if($type){
+                        $where[]=['type','=',$type];
+                    }
+                    if($plain=='free'){
+                        $where[]=['price','=',0];
+                    }
+                    if($plain=='not-free'){
+                        $where[]=['price','>',0];
+                    }
+                    if($keywords){
+                        $where[]=['name|key','like','%'.$keywords.'%'];
+                    }
+                    $fields='id,key,secret_key,pack,type,name,description,author,document,price,version,open,install';
+                    $result=AddonsModel::list($page,$limit,$fields,$where);
+                    foreach ($result['rows'] as $key=>$value){
+                        $result['rows'][$key]['download']=0;
+                        $result['rows'][$key]['packed']=0;
+                        $result['rows'][$key]['local']=1;
+                        //判断目录是否存在
+                        if(is_dir($this->service->getAddonsPath($value['type'],$value['pack']))){
+                            $result['rows'][$key]['download']=1;
+                        }
+                        if(file_exists($this->service->getAddonsPack($value['type'],$value['pack'],$value['version']))){
+                            $result['rows'][$key]['packed']=1;
+                        }
+                        //判断是否是作者
+                        if(AddonsModel::checkKey($value)){
+                            $result['rows'][$key]['is_author']=1;
+                        }
+                        unset($result['rows'][$key]['secret_key']);
+                    }
+                }else{
+                    //远程获取的扩展
+                    $result=$this->service->getAddons($page,$type,$plain,$limit,$keywords);
+                    $where=[];
+                    if($type){
+                        $where[]=['type','=',$type];
+                    }
+                    $local=AddonsModel::list(1,1000,'id,type,pack,author,version,key,secret_key,open,install',$where);
+                    //对比远程扩展和本地扩展
+                    foreach ($result['rows'] as $key=>$value){
+                        $result['rows'][$key]['open']=0;
+                        $result['rows'][$key]['install']=0;
+                        $result['rows'][$key]['download']=0;
+                        $result['rows'][$key]['packed']=0;
+                        $result['rows'][$key]['local']=0;
+                        foreach ($local['rows'] as $v){
+                            if($value['key']==$v['key']){
+                                $result['rows'][$key]['id']=$v['id'];
+                                $result['rows'][$key]['local']=1;
+                                $result['rows'][$key]['open']=$v['open'];
+                                $result['rows'][$key]['install']=$v['install'];
+                                //判断目录是否存在
+                                if(is_dir($this->service->getAddonsPath($value['type'],$v['pack']))){
+                                    $result['rows'][$key]['download']=1;
+                                }
+                                if(file_exists($this->service->getAddonsPack($value['type'],$v['pack'],$value['version']))){
+                                    $result['rows'][$key]['packed']=1;
+                                }
+                                //判断是否是作者
+                                if(AddonsModel::checkKey($v)){
+                                    $result['rows'][$key]['is_author']=1;
+                                }
+                            }
+                        }
+                    }
+                }
+                return json($result);
+            }catch (\Exception $e){
+                $this->error($e->getMessage());
+            }
+        }
+        $this->assign('type',AddonsModel::TYPE);
+        $this->assign('plugins_host',config('yunqi.plugins_host'));
+        return $this->fetch();
+    }
+
+    #[Route('POST','multi')]
+    public function multi()
+    {
+        $ids = $this->request->param('ids');
+        $field = $this->request->param('field');
+        $value = $this->request->param('value');
+        if($field=='open'){
+            $addon=AddonsModel::find($ids);
+            if(!AddonsModel::checkKey($addon)){
+                //禁止修改、删除,否则后果自负
+                $this->error('不是你的扩展,无法操作');
+            }
+            $packfile=$this->service->getAddonsPack($addon['type'],$addon['pack'],$addon['version']);
+            if($value && !is_file($packfile)){
+                $this->error('扩展未打包,请先打包');
+            }
+            $addon->open=$value;
+            $addon->save();
+        }
+        $this->success();
+    }
+
+    #[Route('GET,POST','create')]
+    public function create()
+    {
+        if($this->request->isPost()){
+            $param=$this->request->post('row/a');
+            try{
+                $param['version']=(string)$param['version'];
+                if(intval($param['version'])<1000){
+                    $param['version']=implode('.',str_split($param['version']));
+                }else{
+                    $param['version']=substr($param['version'],0,2).'.'.substr($param['version'],2,1).'.'.substr($param['version'],3,1);
+                }
+                $this->service->create($param);
+                Cache::set('download-addons','');
+            }catch (\Exception $e){
+                $this->error($e->getMessage());
+            }
+            $this->success('创建成功');
+        }
+        $id=$this->request->get('id');
+        if($id){
+            $rows=AddonsModel::find($id);
+            if(!AddonsModel::checkKey($rows)){
+                $this->error('不是你的扩展,无法操作');
+            }
+            $rows->version=intval(str_replace('.','',$rows['version']));
+            $info=$this->service->getAddonsInstallInfo($rows);
+            //将数组转换成换行隔开的字符串
+            $rows->files=implode("\n",$info['files']);
+            $rows->unpack=implode("\n",$info['unpack']);
+            $rows->require=implode("\n",array_map(function($item){return "\\".$item;},$info['require']));
+            $rows->addons=implode("\n",array_keys($info['addons']));
+            $rows->tables=$info['tables'];
+            $rows->config=array_map(function($item){return $item['id'];},$info['config']);
+            $rows->menu=parseMenu($info['menu']);
+            $this->assign('rows',$rows);
+        }else{
+            $this->assign('rows',['menu'=>[]]);
+        }
+        $config=Config::where('can_delete',1)->column('id,name,title','id');
+        $dbname=config('database.connections.mysql.database');
+        $tableList =Db::query("SELECT `TABLE_NAME` AS `name` FROM `information_schema`.`TABLES` where `TABLE_SCHEMA` = '{$dbname}';");
+        $table=array_map(function ($item){return $item['name'];},$tableList);
+        $this->assign('type',AddonsModel::TYPE);
+        $this->assign('table',$table);
+        $this->assign('sonfig',$config);
+        return $this->fetch();
+    }
+
+    #[Route('POST','pack')]
+    public function pack()
+    {
+        $key=$this->request->post('key');
+        try{
+            $this->service->package($key);
+        }catch (\Exception $e){
+            $this->error($e->getMessage());
+        }
+        $this->success('打包成功');
+    }
+
+    #[Route('GET','checkTransactionId')]
+    public function checkTransactionId()
+    {
+        $pack=$this->request->get('pack');
+        $transaction_id=$this->request->get('transaction_id');
+        try{
+            $result=$this->service->checkTransactionId($pack,$transaction_id);
+        }catch (\Exception $e){
+            $this->error($e->getMessage());
+        }
+        $this->success('',$result);
+    }
+
+    #[Route('POST','install')]
+    public function install()
+    {
+        $key=$this->request->post('key');
+        try{
+            $this->service->install($key);
+            Cache::set('download-addons','');
+            Cache::delete('admin_rule_list');
+            Cache::delete('admin_menu_list');
+        }catch (\Exception $e){
+            $this->error($e->getMessage());
+        }
+        $this->success('安装成功');
+    }
+
+    #[Route('GET','payCode')]
+    public function payCode()
+    {
+        $key=$this->request->get('key');
+        $out_trade_no=$this->request->get('out_trade_no');
+        try{
+            $code_url=$this->service->payCode($key,$out_trade_no);
+        }catch (\Exception $e){
+            $this->error($e->getMessage());
+        }
+        $errorCorrectionLevel = 'L';  //容错级别
+        $matrixPointSize = 10;//生成图片大小
+        QRcode::png($code_url,false, $errorCorrectionLevel, $matrixPointSize, 2);
+        exit;
+    }
+
+    #[Route('GET,POST','uninstall')]
+    public function uninstall()
+    {
+        if($this->request->isGet()){
+            $key=$this->request->get('key');
+            $addon=AddonsModel::where('key',$key)->find();
+            $result=$this->service->getAddonsInstallInfo($addon);
+            $this->assign('addon',$addon);
+            $this->assign('menu',$result['menu']);
+            $this->assign('conf',$result['config']);
+            $this->assign('tables',$result['tables']);
+            return $this->fetch();
+        }
+        if($this->request->isPost()){
+            $key=$this->request->post('key');
+            $actions=$this->request->post('actions',[]);
+            try{
+                $this->service->uninstall($key,$actions);
+                Cache::set('download-addons','');
+                Cache::delete('admin_rule_list');
+                Cache::delete('admin_menu_list');
+            }catch (\Exception $e){
+                $this->error($e->getMessage());
+            }
+            $this->success('卸载成功');
+        }
+    }
+
+    #[Route('POST','download')]
+    public function download()
+    {
+        $postdata=$this->request->post();
+        try{
+            $this->service->download($postdata);
+            Cache::set('download-addons','');
+        }catch (\Exception $e){
+            $this->error($e->getMessage());
+        }
+        $this->success('下载成功');
+    }
+
+    #[Route('POST','del')]
+    public function del()
+    {
+        $key=$this->request->post('key');
+        try{
+            $this->service->delAddons($key);
+            Cache::set('download-addons','');
+        }catch (\Exception $e){
+            $this->error($e->getMessage());
+        }
+        $this->success('删除成功');
+    }
+
+    #[Route('GET','checkPayStatus')]
+    public function checkPayStatus()
+    {
+        $out_trade_no=$this->request->get('out_trade_no');
+        $key=$this->request->get('key');
+        try{
+            $r=$this->service->checkPayStatus($key,$out_trade_no);
+        }catch (\Exception $e){
+            $this->error($e->getMessage());
+        }
+        $this->success('',$r);
+    }
+
+}

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

@@ -0,0 +1,298 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare (strict_types = 1);
+
+namespace app\admin\controller;
+
+use app\admin\service\AdminUploadService;
+use app\common\controller\Backend;
+use app\common\library\Tree;
+use app\common\model\Attachment;
+use app\common\model\QrcodeScan;
+use app\common\model\Third;
+use app\common\model\Qrcode;
+use app\common\model\Category;
+use app\common\service\msg\BackendMsg;
+use think\annotation\route\Group;
+use think\annotation\route\Route;
+use think\facade\Cache;
+use think\facade\Config;
+use think\Response;
+
+#[Group("ajax")]
+class Ajax extends Backend{
+
+    protected $noNeedRight = ['*'];
+    protected $noNeedLogin = ['js'];
+    /**
+     *上传文件
+     */
+    #[Route('POST','upload')]
+    public function upload()
+    {
+        $disks=$this->request->post('disks');
+        if(!$disks){
+            $disks=$this->config['upload']['disks'];
+        }
+        $category=$this->request->post('category');
+        $catelist=site_config('dictionary.filegroup');
+        if(!key_exists($category,$catelist)){
+            $category='';
+        }
+        $file = $this->request->file('file');
+        $classname=config('filesystem.disks')[$disks]['class'];
+        $name=config('filesystem.disks')[$disks]['name'];
+        if(!class_exists($classname)){
+            $this->error($name.'扩展未安装,请先下载');
+        }
+        try{
+            $savename=$classname::newInstance([
+                'config'=>config('yunqi.upload'),
+                'admin_id'=>$this->auth->id,
+                'category'=>$category,
+                'file'=>$file
+            ])->save();
+        }catch (\Exception $e){
+            $this->error(__('上传文件出错'),[
+                'file'=>$e->getFile(),
+                'line'=>$e->getLine(),
+                'msg'=>$e->getMessage()
+            ]);
+        }
+        $this->success('',$savename);
+    }
+
+    /**
+     *读取私有文件
+     */
+    #[Route('GET','readfile')]
+    public function readfile(string $sha1='')
+    {
+        $attachment=Attachment::where("sha1",$sha1)->find();
+        $filepath=root_path().$attachment['url'];
+        $myfile = fopen($filepath, "r");
+        $filecontent=fread($myfile,filesize($filepath));
+        fclose($myfile);
+        $ext = pathinfo($filepath, PATHINFO_EXTENSION);
+        if(
+            $ext=='jpg'  ||
+            $ext=='JPG'  ||
+            $ext=='png'  ||
+            $ext=='PNG'  ||
+            $ext=='jpeg' ||
+            $ext=='JPEG' ||
+            $ext=='gif'  ||
+            $ext=='GIF'  ||
+            $ext=='bmp'  ||
+            $ext=='BMP'
+        ){
+            header('Content-type:image/'.$ext);
+            echo $filecontent;
+            exit();
+        }
+        $filename = pathinfo($filepath, PATHINFO_FILENAME);
+        Header ( "Content-type: application/octet-stream");
+        Header ( "Accept-Ranges: bytes" );
+        Header ( "Accept-Length: ".filesize ($filepath));
+        Header ( "Content-Disposition: attachment; filename=".$filename.".".$ext);
+        echo $filecontent;
+        exit();
+    }
+    /**
+     * 模拟读取通知消息,需要开发者完善动态效果
+     */
+    #[Route('GET,POST','message')]
+    public function message()
+    {
+        $msgService=BackendMsg::newInstance(['msg_type'=>'backend']);
+        //阅读消息
+        if($this->request->isPost()){
+            $ids=$this->request->post('ids/a');
+            $msgService->read($ids);
+            $this->success();
+        }
+        //获取消息
+        if($this->request->isGet()){
+            $message=$msgService->list($this->auth->id,1,100,false);
+            if(empty($message)){
+                $this->success('',array([
+                    'title'=>__('通知消息'),
+                    'list'=>[]
+                ]));
+            }
+            $r=[];
+            foreach ($message as $value){
+                 $hastitle=false;
+                 foreach ($r as $xs){
+                     if($xs['title']==$value['title']){
+                         $hastitle=true;
+                     }
+                 }
+                 if(!$hastitle){
+                     $r[]=[
+                         'title'=>$value['title'],
+                         'list'=>[]
+                     ];
+                 }
+            }
+            foreach ($message as $value){
+                foreach ($r as $key=>$xs){
+                    if($xs['title']==$value['title']){
+                        $r[$key]['list'][]=$value;
+                    }
+                }
+            }
+            $this->success('',$r);
+        }
+    }
+    /**
+     * 清空系统缓存
+     */
+    #[Route('GET','wipecache')]
+    public function wipecache()
+    {
+        try {
+            $type = $this->request->request("type");
+            switch ($type) {
+                case 'all':
+                    // no break
+                case 'content':
+                    //内容缓存
+                    Cache::clear();
+                    if ($type == 'content') {
+                        break;
+                    }
+                case 'template':
+                    // 模板缓存
+                    $list=scandir(root_path().'app'.DS);
+                    foreach ($list as $file){
+                        if(!is_dir($file)){
+                            continue;
+                        }
+                        if($file=='common'){
+                            continue;
+                        }
+                        $temp=root_path().'runtime'.DS.$file.DS.'temp';
+                        if(is_dir($temp)){
+                            rmdirs($temp, false);
+                        }
+                    }
+                    if ($type == 'template') {
+                        break;
+                    }
+                case 'browser':
+                    // 浏览器缓存
+                    if ($type == 'browser') {
+                        break;
+                    }
+            }
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+        $this->success();
+    }
+    /**
+     * 获取分类
+     */
+    #[Route('GET','category')]
+    public function category()
+    {
+        $type = $this->request->get('type', '');
+        $pid = $this->request->get('pid', 0);
+        $where = ['status' => 'normal'];
+        if ($type) {
+            $where['type'] = $type;
+        }
+        $categorylist = Category::where($where)->field('id,pid,name')->order('weigh desc,id desc')->select()->toArray();
+        $r=Tree::instance()->init($categorylist)->getTreeArray($pid);
+        $this->success('',$r);
+    }
+
+    /**
+     * 获取地区
+     */
+    #[Route('GET','area')]
+    public function area()
+    {
+        if(!addons_installed('area')){
+            $this->error('请先安装插件area');
+        }
+        $pid = $this->request->get("pid");
+        $provincelist = \app\common\model\Area::where('pid',$pid)->field('id,name')->select();
+        $this->success('', $provincelist);
+    }
+
+    /**
+     * 获取js文件
+     */
+    #[Route('GET','js/:name')]
+    public function js($name)
+    {
+        $header = ['Content-Type' => 'application/javascript'];
+        if (!Config::get('app.app_debug')) {
+            $offset = 30 * 60 * 60 * 24; // 缓存一个月
+            $header['Cache-Control'] = 'public';
+            $header['Pragma'] = 'cache';
+            $header['Expires'] = gmdate("D, d M Y H:i:s", time() + $offset) . " GMT";
+        }
+        // 页面缓存
+        ob_start();
+        ob_implicit_flush(false);
+        require(root_path().'runtime'.DS.'admin'.DS.'temp'.DS.$name.'-js.php');
+        // 获取并清空缓存
+        $content = ob_get_clean();
+        $response = Response::create(trim($content))->header($header);
+        $response->send();
+        exit;
+    }
+
+    //绑定第三方账号
+    #[Route('GET,JSON','third/:action')]
+    public function third($action)
+    {
+        if($action=='qrcode'){
+            $platform=$this->request->get('platform');
+            $foreign_key=$this->request->get('foreign_key');
+            if($platform=='mpapp'){
+                $config=[
+                    'appid'=>site_config("uniapp.mpapp_id"),
+                    'appsecret'=>site_config("uniapp.mpapp_secret"),
+                ];
+                $qrcode=Qrcode::createQrcode(Qrcode::TYPE('绑定第三方账号'),$foreign_key,5*60);
+                $wechat=new \WeChat\Qrcode($config);
+                $ticket = $wechat->create($qrcode->id)['ticket'];
+                $url=$wechat->url($ticket);
+                $content=file_get_contents($url);
+                header('Content-Type: image/png');
+                echo $content;
+                exit;
+            }
+        }
+        if($action=='check'){
+            $platform=$this->request->get('platform');
+            $foreign_key=$this->request->get('foreign_key');
+            $scan=QrcodeScan::where('foreign_key',$foreign_key)->find();
+            if($scan){
+                $third=Third::where(['openid'=>$scan->openid,'platform'=>$platform])->find();
+                if($third){
+                    $this->success('',$third);
+                }
+            }
+            $this->error();
+        }
+        if($action=='selectpage'){
+            $platform=$this->request->get('platform');
+            $this->model=new Third();
+            $where=[];
+            $where[]=['platform','=',$platform];
+            return $this->selectpage($where);
+        }
+    }
+}

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

@@ -0,0 +1,119 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare (strict_types = 1);
+
+namespace app\admin\controller;
+
+use app\common\model\Admin;
+use app\common\model\delivery\Text;
+use app\common\model\User;
+use app\common\model\Category;
+use app\common\model\Attachment;
+use app\common\library\Date;
+use app\common\controller\Backend;
+use think\annotation\route\Route;
+use think\facade\Db;
+
+/**
+ * 控制台
+ */
+class Dashboard extends Backend
+{
+    public function _initialize()
+    {
+        parent::_initialize();
+    }
+    /**
+     * 查看
+     */
+    #[Route('GET','dashboard/index')]
+    public function index()
+    {
+        if($this->request->isAjax()){
+            //模拟数据面板
+            $panel=[
+                rand(100,1000),
+                rand(100,1000),
+                rand(100,1000),
+                rand(100,1000),
+            ];
+            //模拟折线图
+            $line=[
+                'date'=>[],
+                'data'=>[]
+            ];
+            $time=time();
+            for($i=0;$i<7;$i++){
+                $line['date'][]=date('Y-m-d',$time-(7-$i)*24*3600);
+                $line['data'][]=rand(100,1000);
+            }
+            //模拟表格
+            $names=['张三','李四','老王','老成','黑娃'];
+            $table=[];
+            $total=rand(100,999);
+            foreach ($names as $key=>$name){
+                $table[]=[
+                    'sort'=>$key+1,'name'=>$name,'total'=>$total,'money'=>$total*rand(10,20)
+                ];
+                $total-=rand(10,99);
+            }
+            //模拟柱状图
+            $bar=[
+                'date'=>['周一','周二','周三','周四','周五','周六','周日'],
+                'name'=>['张三','李四','老王'],
+                'data'=>[
+                    [],
+                    [],
+                    []
+                ]
+            ];
+            for($i=0;$i<3;$i++){
+                foreach ($bar['date'] as $name){
+                    $bar['data'][$i][]=rand(100,999);
+                }
+            }
+            //模拟饼状图
+            $pie=[
+                ['name'=>'张三','value'=>rand(100,999)],
+                ['name'=>'李四','value'=>rand(100,999)],
+                ['name'=>'老王','value'=>rand(100,999)],
+                ['name'=>'黑娃','value'=>rand(100,999)],
+            ];
+            //模拟订单
+            $count=rand(100,999);
+            $today=rand(1000,9999);
+            $yestoday=rand(1000,9999);
+            $order=[
+                'count'=>$count,
+                'total'=>1000,
+                'today'=>$today.'.'.rand(10,99),
+                'yestoday'=>$yestoday.'.'.rand(10,99),
+                'percentage'=>[
+                    round($count/10),
+                    $yestoday>$today?round($today/$yestoday*100):100,
+                ]
+            ];
+            $this->success('',compact('panel','line','table','bar','pie','order'));
+        }
+        return $this->fetch();
+    }
+
+    #[Route('GET','dashboard/platform1')]
+    public function platform1()
+    {
+        return $this->fetch();
+    }
+
+    #[Route('GET','dashboard/platform2')]
+    public function platform2()
+    {
+        return $this->fetch();
+    }
+}

+ 383 - 0
app/admin/controller/Develop.php

@@ -0,0 +1,383 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare (strict_types = 1);
+
+namespace app\admin\controller;
+
+use app\admin\service\curd\CurdService;
+use app\common\controller\Backend;
+use app\common\model\AuthRule;
+use app\common\model\Queue;
+use app\admin\command\Queue as QueueCommand;
+use think\annotation\route\Group;
+use think\annotation\route\Route;
+use think\facade\Db;
+
+#[Group("develop")]
+class Develop extends Backend
+{
+    protected $noNeedRight = ['*'];
+
+    protected function _initialize()
+    {
+        parent::_initialize();
+        if(!$this->auth->isSuperAdmin()){
+            $this->error(__('超级管理员才能访问'));
+        }
+    }
+
+    //一键crud
+    #[Route("GET,JSON","crud")]
+    public function crud()
+    {
+       if($this->request->isJson()){
+            if(!config('app.app_debug')) {
+                $this->error(__('只允许在开启调试模式下执行'));
+            }
+            $data=$this->request->post();
+            try{
+                $service=CurdService::newInstance($data);
+                $service->volidate();
+                $content=$service->build();
+                if($data['type']=='file'){
+                    $url=$service->getIndexUrl();
+                    $this->success(__('创建成功'),$url);
+                }
+                if($data['type']=='code'){
+                    $this->success('',$content);
+                }
+            }catch (\Exception $e){
+                $this->error($e->getMessage());
+            }
+       }else{
+           $tree=AuthRule::getMenuListTree('*');
+           $ruledata=[0 => __('无')];
+           foreach ($tree as $value){
+               $ruledata[$value['id']]=$value['title'];
+           }
+           $config = Db::getConfig();
+           $default=$config['default'];
+           $prefix=$config['connections'][$default]['prefix'];
+           $this->assign('ruledata',$ruledata);
+           $this->assign('app',['admin']);
+           $this->assign('tablePrefix',$prefix);
+           return $this->fetch();
+       }
+    }
+
+    //清除创建
+    #[Route("JSON","clear")]
+    public function clear()
+    {
+        if(!config('app.app_debug')) {
+            $this->error(__('只允许在开启调试模式下执行'));
+        }
+        $data=$this->request->post();
+        try{
+            CurdService::newInstance($data)->clear();
+        }catch (\Exception $e){
+            $this->error($e->getMessage());
+        }
+        $this->success(__('清除成功'));
+    }
+
+    //构造表格
+    #[Route("GET,POST","queue")]
+    public function queue()
+    {
+        if($this->request->isAjax()){
+            $list=Queue::alias('queue')->whereRaw("queue.limit=0 or queue.limit>queue.times")->select();
+            $this->success('',$list);
+        }
+        return $this->fetch();
+    }
+
+    #[Route("GET","queueLog")]
+    public function queueLog()
+    {
+        $type=$this->request->get('type');
+        //每次读取1kb
+        $readSize=1024*2;
+        $log=root_path().'queue.log';
+        if($type=='total'){
+            if (!file_exists($log)) {
+                $this->success('',1);
+            }
+            //获取文件大小
+            $size=filesize($log);
+            //获取总页数
+            $total=ceil($size/$readSize);
+            $this->success('',$total);
+        }
+        if($type=='content'){
+            $page=$this->request->get('page/d');
+            if (!file_exists($log)) {
+                $this->success('');
+            }
+            $fp=fopen($log,'r');
+            //定位到读取的页数的行首
+            $start=($page-1)*$readSize;
+            fseek($fp,$start);
+            while ($start>0){
+                $char=fgetc($fp);
+                if($char=="\n"){
+                    break;
+                }
+                $start--;
+                $readSize++;
+                fseek($fp,$start);
+            }
+            $content='';
+            while(!feof($fp)) {
+                $content.=fgets($fp);
+                if(strlen($content)>$readSize){
+                    break;
+                }
+            }
+            fclose($fp);
+            $this->success('',$content);
+        }
+    }
+
+    #[Route("POST","delQueue")]
+    public function delQueue()
+    {
+        $this->queueReload(function(){
+            $id=$this->request->post('id');
+            Queue::find($id)->delete();
+        });
+        $list=Queue::alias('queue')->whereRaw("queue.limit=0 or queue.limit>queue.times")->select();
+        $this->success(__('删除成功'),$list);
+    }
+
+    private function queueReload($callback)
+    {
+        try{
+            $callback();
+        }catch (\Exception $e){
+            $this->error($e->getMessage());
+        }
+        $refreshTime=QueueCommand::refreshTime;
+        $time=QueueCommand::$timetxt;
+        $locktime=intval(file_get_contents($time));
+        $locktime=$locktime+($refreshTime-$locktime%$refreshTime);
+        file_put_contents(QueueCommand::$locktxt,$locktime);
+    }
+
+    #[Route("GET,POST","addQueue")]
+    public function addQueue()
+    {
+        if($this->request->isPost()){
+            $data=$this->request->post('row/a');
+            if(!class_exists("\\app\\admin\\command\\queueEvent\\".$data['function'])){
+                $this->error(__('处理类不存在'));
+            }
+            $filter=[];
+            if($data['filter']){
+                foreach ($data['filter'] as $key=>$value){
+                    $value=trim($value);
+                    if($value==''){
+                        continue;
+                    }
+                    $year=intval(date('Y'));
+                    if($key=='年'){
+                        $value=intval($value);
+                        if($value<$year){
+                            $this->error(__('年份不能小于当前年份'));
+                        }
+                        $value=(string)$value;
+                    }
+                    if($key=='月'){
+                        $value=intval($value);
+                        if($value<1 || $value>12){
+                            $this->error(__('月份必须在1-12之间'));
+                        }
+                        if($value<10){
+                            $value='0'.$value;
+                        }
+                        $value=(string)$value;
+                    }
+                    if($key=='日'){
+                        $value=intval($value);
+                        if($value<1 || $value>31){
+                            $this->error(__('日必须在1-31之间'));
+                        }
+                        if($value<10){
+                            $value='0'.$value;
+                        }
+                        $value=(string)$value;
+                    }
+                    if($key=='时'){
+                        $value=intval($value);
+                        if($value<0 || $value>23){
+                            $this->error(__('时必须在0-23之间'));
+                        }
+                        if($value<10){
+                            $value='0'.$value;
+                        }
+                        $value=(string)$value;
+                    }
+                    if($key=='分'){
+                        $value=intval($value);
+                        if($value<0 || $value>59){
+                            $this->error(__('分必须在0-59之间'));
+                        }
+                        if($value<10){
+                            $value='0'.$value;
+                        }
+                        $value=(string)$value;
+                    }
+                    if($key=='秒'){
+                        $value=intval($value);
+                        if($value<0 || $value>59){
+                            $this->error(__('秒必须在0-59之间'));
+                        }
+                        if($value<10){
+                            $value='0'.$value;
+                        }
+                        $value=(string)$value;
+                    }
+                    switch ($key){
+                        case '年':
+                            $filter['Y']=$value;
+                            break;
+                        case '月':
+                            $filter['m']=$value;
+                            break;
+                        case '日':
+                            $filter['d']=$value;
+                            break;
+                        case '时':
+                            $filter['H']=$value;
+                            break;
+                        case '分':
+                            $filter['i']=$value;
+                            break;
+                        case '秒':
+                            $filter['s']=$value;
+                            break;
+                    }
+                }
+                $data['filter']=json_encode($filter,JSON_UNESCAPED_UNICODE);
+            }
+            $this->queueReload(function () use ($data){
+                (new Queue())->save($data);
+            });
+            $this->success(__('添加成功'));
+        }
+        return $this->fetch();
+    }
+
+    #[Route("GET,POST","queueStatus")]
+    public function queueStatus()
+    {
+        $timetxt=QueueCommand::$timetxt;
+        if($this->request->isPost()){
+            $status=$this->request->post('status');
+            $timetxt1=intval(file_get_contents($timetxt));
+            sleep(2);
+            $timetxt2=intval(file_get_contents($timetxt));
+            if($status){
+                if($timetxt1!=$timetxt2) {
+                    $this->error(__('队列正在执行中'));
+                }
+                $str=php_ini_loaded_file();
+                $str=substr($str,0,strrpos($str,'/'));
+                $phppath='';
+                $callback='';
+                if (substr(php_uname(), 0, 7) == "Windows"){
+                    if(!function_exists('popen') || !function_exists('pclose')){
+                        $this->error(__('popen或pclose函数被禁用了'));
+                    }
+                    $phppath=substr($str,0,strrpos($str,'/')).DS.'bin'.DS.'php.exe';
+                    $callback=function ($cmd){
+                        pclose(popen($cmd,'r'));
+                    };
+                }
+                if (substr(php_uname(), 0, 5) == "Linux"){
+                    if(!function_exists('exec')){
+                        $this->error(__('exec函数被禁用了'));
+                    }
+                    $phppath=substr($str,0,strrpos($str,'/')).DS.'bin'.DS.'php';
+                    $callback=function ($cmd){
+                         $logpath=root_path()."queue.log";
+                         if(file_exists($logpath)){
+                             unlink($logpath);
+                         }
+                         exec("{$cmd} > {$logpath} &");
+                    };
+                }
+                if($phppath==''){
+                    $this->error(__('当前操作仅支持windows或linux系统'));
+                }
+                $command=root_path().'think Queue';
+                $cmd=$phppath." ".$command;
+                file_put_contents($timetxt,1);
+                $callback($cmd);
+                $this->success();
+            }else{
+                if($timetxt1==$timetxt2) {
+                    $this->error(__('队列服务已经停止了'));
+                }
+                file_put_contents($timetxt,0);
+                $this->success();
+            }
+        }
+        $timetxt1=intval(file_get_contents($timetxt));
+        sleep(2);
+        $timetxt2=intval(file_get_contents($timetxt));
+        $status='normal';
+        $keeptime=$timetxt2;
+        $stoptime='';
+        if($timetxt1==$timetxt2) {
+            $status='hidden';
+            $stoptime=date('Y-m-d H:i:s',filemtime($timetxt));
+        }
+        $this->success('',compact('status','keeptime','stoptime'));
+    }
+
+    //获取表格
+    #[Route("JSON","getTable")]
+    public function getTable()
+    {
+        $limit=$this->request->post('limit/d',7);
+        $page=$this->request->post('page/d',7);
+        $labelValue=$this->request->post('labelValue');
+        $where="";
+        if($labelValue) {
+            $where="and `TABLE_NAME` like '%{$labelValue}%'";
+        }
+        $offect=($page-1)*$limit;
+        $config = Db::getConfig();
+        $default=$config['default'];
+        $dbname=$config['connections'][$default]['database'];
+        $count=Db::query("SELECT COUNT(*) AS `count` FROM `information_schema`.`TABLES` where `TABLE_SCHEMA` = '{$dbname}' {$where};");
+        $tableList = Db::query("SELECT `TABLE_NAME` AS `name`,`TABLE_COMMENT` AS `title` FROM `information_schema`.`TABLES` where `TABLE_SCHEMA` = '{$dbname}' {$where} limit {$offect},{$limit};");
+        foreach ($tableList as $key=>$value){
+            $tableList[$key]['title']=$tableList[$key]['name'].($tableList[$key]['title']?' - '.$tableList[$key]['title']:'');
+        }
+        return json(['total'=>$count[0]['count'],'rows'=>$tableList]);
+    }
+
+    //获取字段
+    #[Route("GET","getFields")]
+    public function getFields()
+    {
+        $table = $this->request->get('table');
+        $config = Db::getConfig();
+        $default=$config['default'];
+        $dbname=$config['connections'][$default]['database'];
+        //从数据库中获取表字段信息
+        $sql = "SELECT `COLUMN_NAME` AS `name`,`COLUMN_COMMENT` AS `title`,`DATA_TYPE` AS `type` FROM `information_schema`.`columns` WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION";
+        //加载主表的列
+        $fieldList = Db::query($sql, [$dbname, $table]);
+        $this->success("",$fieldList);
+    }
+}

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

@@ -0,0 +1,145 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare (strict_types = 1);
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use app\common\model\Admin;
+use app\common\model\Qrcode;
+use think\annotation\route\Route;
+use think\captcha\facade\Captcha;
+use think\facade\Session;
+
+class Index extends Backend
+{
+    protected $noNeedLogin = ['login','captcha','qrcodeLogin'];
+    protected $noNeedRight = ['index','logout','platform','changeTheme'];
+
+
+    #[Route('GET','index')]
+    public function index()
+    {
+        $referer=Session::pull('referer');
+        if($referer){
+            Session::save();
+        }
+        list($platform,$menulist, $selected, $referer) = $this->auth->getSidebar($referer);
+        $this->assign('site',site_config('basic'));
+        $this->assign('platform',$platform);
+        $this->assign('menulist',$menulist);
+        $this->assign('selected',$selected);
+        $this->assign('referer',$referer);
+        return $this->fetch('',[],false);
+    }
+
+    #[Route('POST','change-theme')]
+    public function changeTheme()
+    {
+        $key=$this->request->post('key');
+        $value=$this->request->post('value');
+        if($value==='true'){
+            $value=true;
+        }
+        if($value==='false'){
+            $value=false;
+        }
+        $element_ui=Admin::where('id',$this->auth->id)->value('element_ui');
+        if($element_ui){
+            $element_ui=json_decode($element_ui,true);
+        }else{
+            $element_ui=[];
+        }
+        $element_ui[$key]=$value;
+        $element_ui=json_encode($element_ui);
+        Admin::update(['element_ui'=>$element_ui],['id'=>$this->auth->id]);
+        Session::set('admin.element_ui',$element_ui);
+        Session::save();
+        $this->success();
+    }
+
+    #[Route('GET','captcha')]
+    public function captcha()
+    {
+        return Captcha::create();
+    }
+
+    #[Route('GET','platform')]
+    public function platform()
+    {
+        $id=$this->request->get('id');
+        Session::set('admin.platform_id',$id);
+        Session::save();
+        $this->success();
+    }
+
+    #[Route('GET','qrcodeLogin')]
+    public function qrcodeLogin()
+    {
+        $token=$this->request->get('token');
+        $admin_id=$this->request->get('admin_id');
+        $adminlist=[];
+        if($this->auth->loginByThird($token,$admin_id,$adminlist)){
+            $this->success(__('登陆成功'));
+        }
+        $this->error('',$adminlist);
+    }
+
+    #[Route('POST,GET','login')]
+    public function login()
+    {
+        if(!$this->request->isPost()){
+            if($this->auth->isLogin()){
+                $alis=get_module_alis();
+                return redirect(request()->domain().'/'.$alis.'/index');
+            }
+            $thirdLogin=addons_installed('uniapp') && site_config("uniapp.scan_login");
+            if($thirdLogin){
+                $config=[
+                    'appid'=>site_config("uniapp.mpapp_id"),
+                    'appsecret'=>site_config("uniapp.mpapp_secret"),
+                ];
+                $qrcode=Qrcode::createQrcode(Qrcode::TYPE('管理员扫码登录'),token(),5*60);
+                $wechat=new \WeChat\Qrcode($config);
+                $ticket = $wechat->create($qrcode->id)['ticket'];
+                $url=$wechat->url($ticket);
+                $this->assign('qrcode',$url);
+            }
+            $this->assign('thirdLogin',$thirdLogin);
+            $this->assign('logo',site_config("basic.logo"));
+            $this->assign('sitename',site_config("basic.sitename"));
+            $this->assign('login_captcha',config('yunqi.login_captcha'));
+            return $this->fetch();
+        }
+        $username = $this->request->post('username');
+        $password = $this->request->post('password');
+        $captcha = $this->request->post('captcha');
+        if(config('yunqi.login_captcha') && !captcha_check($captcha)){
+            $this->error(__('验证码错误'),0);
+        }
+        try{
+            $this->auth->login($username,$password);
+            Session::delete('captcha');
+            Session::save();
+        }catch (\Exception $e){
+            $this->error($e->getMessage(),1);
+        }
+        $this->success(__('登陆成功'));
+    }
+
+    #[Route('GET','logout')]
+    public function logout()
+    {
+        $alis=get_module_alis();
+        $this->auth->logout();
+        $url=request()->domain().'/'.$alis.'/login';
+        return redirect($url);
+    }
+}

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

@@ -0,0 +1,255 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types=1);
+
+namespace app\admin\controller\auth;
+
+use app\admin\traits\Actions;
+use app\common\controller\Backend;
+use app\common\model\AuthGroup;
+use app\common\model\Department;
+use think\annotation\route\Group;
+use think\annotation\route\Route;
+use think\facade\Validate;
+use app\common\model\Admin as AdminModel;
+
+/**
+ * 管理员管理
+ */
+#[Group("auth/admin")]
+class Admin extends Backend
+{
+    protected $groups;
+
+    private $thirdLogin=false;
+
+    private $departdata=[];
+
+    protected $noNeedRight='third';
+
+    use Actions{
+        add as private _add;
+        edit as private _edit;
+        del as private _del;
+    }
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model=new AdminModel();
+        $this->groups=AuthGroup::select();
+        $this->thirdLogin=addons_installed('uniapp') && site_config("uniapp.scan_login");
+        $this->departdata=Department::getDepartData();
+        $this->assign('thirdLogin',$this->thirdLogin);
+        $this->assign('departdata',$this->departdata);
+    }
+
+    #[Route("*","index")]
+    public function index()
+    {
+        if (false === $this->request->isAjax()) {
+            $this->assign('groupids',$this->auth->groupids);
+            $this->assign('isSuperAdmin',$this->auth->isSuperAdmin());
+            return $this->fetch();
+        }
+        if($this->request->post('selectpage')){
+            return $this->selectpage();
+        }
+        $where=[];
+        if(!$this->auth->isSuperAdmin()){
+            $groupids=$this->auth->getChildrenGroupIds();
+            $or=[];
+            foreach ($groupids as $v){
+                $or[]="FIND_IN_SET({$v},groupids)";
+            }
+            $where[]=[implode(' or ',$or)];
+        }
+        $depart=(int)$this->filter('depart');
+        if($depart){
+            $departids=$this->getChildrenDepartIds($depart);
+            $departids[]=$depart;
+            $where[]=['depart_id','in',$departids];
+        }
+        $this->relationField=['depart'];
+        [$where, $order, $limit, $with] = $this->buildparams($where);
+        $third_ids=[];
+        $list = $this->model
+            ->with($with)
+            ->where($where)
+            ->order($order)
+            ->paginate($limit)
+            ->each(function($res) use (&$third_ids){
+                $this->formartGroups($res);
+                if($this->thirdLogin){
+                    $third_ids[]=$res->third_id;
+                }
+            });
+        $rows=$list->items();
+        if($this->thirdLogin){
+            $thirds=\app\common\model\Third::where('id','in',$third_ids)->column('id,openname','id');
+            foreach ($rows as $k=>$v){
+                $rows[$k]['third']=$thirds[$v['third_id']]??'';
+            }
+        }
+        $result = ['total' => $list->total(), 'rows' => $rows];
+        return json($result);
+    }
+
+    private function getChildrenDepartIds(int $pid)
+    {
+        function getChildren(array $list){
+            $r=[];
+            foreach ($list as $v){
+                $r[]=$v['id'];
+                if(!empty($v['childlist'])){
+                    $r=array_merge($r,getChildren($v['childlist']));
+                    return $r;
+                }
+            }
+            return $r;
+        };
+        foreach ($this->departdata as $v){
+            if($v['id']==$pid){
+                return getChildren($v['childlist']);
+            }
+        }
+        return [];
+    }
+
+    #[Route('GET,POST','edit')]
+    public function edit()
+    {
+        $row=$this->model->find($this->request->get('ids'));
+        $row->groupids=explode(',',$row->groupids);
+        $groupids=$this->auth->getChildrenGroupIds();
+        if(!$this->auth->isSuperAdmin()){
+            foreach ($row->groupids as $v){
+                if(!in_array($v,$groupids)){
+                    $this->error(__('无权操作'));
+                }
+            }
+        }
+        if($this->request->isPost()){
+            $params = $this->request->post("row/a");
+            $postgroups=$params['groupids'];
+            if(!$this->auth->isSuperAdmin()){
+                foreach ($postgroups as $v){
+                    if(!in_array($v,$groupids)){
+                        $this->error(__('无权操作'));
+                    }
+                }
+            }
+            if ($params['password']) {
+                if (!Validate::is($params['password'], '\S{6,30}')) {
+                    $this->error(__('密码长度不对!'));
+                }
+                $params['salt'] = str_rand(4);
+                $params['password'] = md5(md5($params['password']) . $params['salt']);
+            } else {
+                unset($params['password'], $params['salt']);
+            }
+            $params['groupids']=implode(',',$postgroups);
+            if(isset($params['third_id']) && !$params['third_id']){
+                $params['third_id']=null;
+            }
+            $row->save($params);
+            $this->success();
+        }else{
+            $this->assign('row',$row);
+            $this->assign('groupdata',$this->getGroupData());
+            return $this->fetch();
+        }
+    }
+
+    #[Route('GET,POST','add')]
+    public function add()
+    {
+        if($this->request->isPost()){
+            $groupids=$this->auth->getChildrenGroupIds();
+            $params = $this->request->post("row/a");
+            $postgroups=$params['groupids'];
+            if(!$this->auth->isSuperAdmin()){
+                foreach ($postgroups as $v){
+                    if(!in_array($v,$groupids)){
+                        $this->error(__('无权操作'));
+                    }
+                }
+            }
+            if (!$params['password']) {
+                $this->error(__('请输入密码!'));
+            }
+            if (!Validate::is($params['password'], '\S{6,30}')) {
+                $this->error(__('密码长度不对!'));
+            }
+            $params['salt'] = str_rand(4);
+            $params['password'] = md5(md5($params['password']) . $params['salt']);
+            $params['groupids']=implode(',',$postgroups);
+            if(isset($params['third_id']) && !$params['third_id']){
+                $params['third_id']=null;
+            }
+            $this->model->save($params);
+            $this->success();
+        }else{
+            $this->assign('groupdata',$this->getGroupData());
+            return $this->fetch();
+        }
+    }
+
+    #[Route('GET,POST','del')]
+    public function del()
+    {
+        if(!$this->auth->isSuperAdmin()){
+            $groupids=$this->auth->getChildrenGroupIds();
+            $ids = $this->request->param("ids");
+            $list = $this->model->where('id', 'in', $ids)->select();
+            foreach ($list as $row){
+                $row->groupids=explode(',',$row->groupids);
+                foreach ($row->groupids as $v){
+                    if(!in_array($v,$groupids)){
+                        $this->error(__('无权操作'));
+                    }
+                }
+            }
+        }
+        return $this->_del();
+    }
+
+    private function getGroupData()
+    {
+        $groupids='*';
+        if(!$this->auth->isSuperAdmin()){
+            $groupids=$this->auth->getChildrenGroupIds();
+            foreach ($groupids as $k=>$v){
+                //去除已经拥有的权限
+                if(in_array($v,$this->auth->groupids)){
+                    unset($groupids[$k]);
+                }
+            }
+        }
+        $groupdata=AuthGroup::getGroupListTree($groupids);
+        return $groupdata;
+    }
+
+    private function formartGroups(&$admin)
+    {
+        $groups=$this->groups;
+        $names=array_column($groups->toArray(),'name','id');
+        $status=array_column($groups->toArray(),'status','id');
+        $groupids=$admin->groupids?explode(',',$admin->groupids):[];
+        foreach($groupids as $k=>$v){
+            $groupids[$k]=[
+                'id'=>$v,
+                'status'=>$status[$v],
+                'name'=>$names[$v]
+            ];
+        }
+        $admin->groupids=$groupids;
+    }
+}

+ 94 - 0
app/admin/controller/auth/Adminlog.php

@@ -0,0 +1,94 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types=1);
+
+namespace app\admin\controller\auth;
+
+use app\common\model\AdminLog as LogModel;
+use app\common\model\Admin;
+use app\common\controller\Backend;
+use think\annotation\route\Group;
+use think\annotation\route\Route;
+
+/**
+ * 管理员日志
+ */
+#[Group("auth/adminlog")]
+class Adminlog extends Backend
+{
+
+    protected $relationField=[];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new LogModel();
+    }
+
+    #[Route('GET,JSON','index')]
+    public function index()
+    {
+        if (false === $this->request->isAjax()) {
+            return $this->fetch();
+        }
+        [$where, $order, $limit, $with] = $this->buildparams();
+        $list = $this->model
+            ->where(function ($query){
+                if(!$this->auth->isSuperAdmin()){
+                    $query->whereIn('admin_id',$this->getChildrenAdminIds());
+                }
+            })
+            ->order($order)
+            ->paginate($limit);
+        $result = ['total' => $list->total(), 'rows' => $list->items()];
+        return json($result);
+    }
+
+    #[Route('POST','del')]
+    public function del()
+    {
+        $adminids=$this->getChildrenAdminIds();
+        $ids = $this->request->post('ids');
+        foreach ($ids as $id){
+            if(!in_array($id,$adminids)){
+                $this->error(__('没有权限'));
+            }
+        }
+        if ($ids) {
+            $this->model->whereIn('id', $ids)->delete();
+        }
+        $this->success();
+    }
+
+    #[Route('GET','detail')]
+    public function detail($ids)
+    {
+        $row = $this->model->where(['id' => $ids])->find();
+        $adminids=$this->getChildrenAdminIds();
+        if(!in_array($row->admin_id,$adminids)){
+            $this->error(__('没有权限'));
+        }
+        $row->content=htmlspecialchars_decode($row->content);
+        $this->assign("row", $row);
+        return $this->fetch();
+    }
+
+    private function getChildrenAdminIds()
+    {
+        $groupids=$this->auth->getChildrenGroupIds();
+        $or=[];
+        foreach ($groupids as $v){
+            $or[]="FIND_IN_SET({$v},groupids)";
+        }
+        $where=implode(' or ',$or);
+        $adminids=Admin::where($where)->column('id');
+        return $adminids;
+    }
+}

+ 47 - 0
app/admin/controller/auth/Depart.php

@@ -0,0 +1,47 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types=1);
+
+namespace app\admin\controller\auth;
+
+use app\admin\traits\Actions;
+use app\common\controller\Backend;
+use app\common\model\Department;
+use think\annotation\route\Group;
+use think\annotation\route\Route;
+
+/**
+ * 部门管理
+ */
+#[Group("auth/depart")]
+class Depart extends Backend
+{
+    private $departdata;
+
+    use Actions;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model=new Department();
+        $this->departdata=Department::getDepartData();
+        $this->assign('departdata',$this->departdata);
+    }
+
+    #[Route('GET,JSON','index')]
+    public function index()
+    {
+        if (false === $this->request->isAjax()) {
+            return $this->fetch();
+        }
+        $result = ['total' => 1000, 'rows' =>$this->departdata];
+        return json($result);
+    }
+}

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

@@ -0,0 +1,203 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types=1);
+
+namespace app\admin\controller\auth;
+
+use app\admin\traits\Actions;
+use app\common\model\AuthGroup;
+use app\common\model\Admin;
+use app\common\controller\Backend;
+use app\common\model\AuthRule;
+use think\annotation\route\Group as GroupAnnotation;
+use think\annotation\route\Route;
+
+/**
+ * 角色组
+ */
+#[GroupAnnotation("auth/group")]
+class Group extends Backend
+{
+    protected $noNeedRight=['roletree'];
+    private $groupdata=null;
+
+    use Actions{
+        index as private _index;
+        add as private _add;
+        edit as private _edit;
+        del as private _del;
+        multi as private _multi;
+    }
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model=new AuthGroup();
+    }
+
+    private function getGroupData()
+    {
+        $groupids='*';
+        if(!$this->auth->isSuperAdmin()){
+            $groupids=$this->auth->getChildrenGroupIds();
+        }
+        return AuthGroup::getGroupListTree($groupids);
+    }
+
+    #[Route('GET,JSON','index')]
+    public function index()
+    {
+        if (false === $this->request->isAjax()) {
+            $this->assign('groupids',$this->auth->groupids);
+            return $this->fetch();
+        }
+        $result = ['total' => 1000, 'rows' =>$this->getGroupData()];
+        return json($result);
+    }
+
+    #[Route('GET,POST','add')]
+    public function add()
+    {
+        if($this->request->isPost()){
+            $this->volidate();
+        }else{
+            $this->assign('groupdata',$this->getGroupData());
+        }
+        return $this->_add();
+    }
+
+    #[Route('GET,POST','edit')]
+    public function edit()
+    {
+        if($this->request->isPost()){
+            $this->volidate();
+            return $this->_edit();
+        }else{
+            $ids = $this->request->get('ids');
+            $row = $this->model->find($ids);
+            $count=0;
+            $row->rules=implode(',',$this->getDiffRules($row->rules,$count));
+            $this->assign('row', $row);
+            $this->assign('groupdata',$this->getGroupData());
+            return $this->fetch();
+        }
+    }
+
+    private function getDiffRules($rules,&$count)
+    {
+        if(is_string($rules)){
+            $rules=explode(',',$rules);
+        }
+        $ruleslist=AuthRule::field('id,pid')->select();
+        $pids=[];
+        foreach ($ruleslist as $rule){
+            if(!in_array($rule->id,$rules) && $rule->pid){
+                $pids[]=$rule->pid;
+            }
+        }
+        $pids=array_unique($pids);
+        //删除数组$rules中存在于$pids中的元素
+        $rules=array_diff($rules,$pids);
+        if(count($pids)!=$count){
+            $count=count($pids);
+            $rules=$this->getDiffRules($rules,$count);
+        }
+        return $rules;
+    }
+
+    #[Route('POST,GET','del')]
+    public function del()
+    {
+        $ids = $this->request->param("ids");
+        $ids=explode(',',$ids);
+        foreach ($ids as $id){
+            $count=Admin::where("FIND_IN_SET({$id},groupids)")->count();
+            if($count>0){
+                $this->error(__('请先删除该角色组下的管理员'));
+            }
+        }
+        if(!$this->auth->isSuperAdmin()){
+            $groupids=$this->auth->getChildrenGroupIds();
+            foreach ($ids as $id){
+                if(!in_array($id,$groupids)){
+                    $this->error(__('无权操作'));
+                }
+            }
+            foreach ($ids as $id){
+                if(in_array($id,$this->auth->groupids)){
+                    $this->error(__('无权操作'));
+                }
+            }
+        }
+        return $this->_del();
+    }
+
+    #[Route('POST,GET','multi')]
+    public function multi()
+    {
+        $ids = $this->request->param('ids');
+        $ids=is_string($ids)?explode(',',$ids):$ids;
+        if(!$this->auth->isSuperAdmin()){
+            $groupids=$this->auth->getChildrenGroupIds();
+            foreach ($ids as $id){
+                if(!in_array($id,$groupids)){
+                    $this->error(__('无权操作'));
+                }
+            }
+            foreach ($ids as $id){
+                if(in_array($id,$this->auth->groupids)){
+                    $this->error(__('无权操作'));
+                }
+            }
+        }
+        return $this->_multi();
+    }
+
+    #[Route('GET','roletree')]
+    public function roletree($groupid=0)
+    {
+        if($groupid==1){
+            $ruleids='*';
+        }else{
+            $ruleids=explode(',',AuthGroup::find($groupid)->auth_rules);
+        }
+        $list=AuthRule::getRuleList($ruleids);
+        return json($list);
+    }
+
+    //验证加菜单的权限
+    private function volidate()
+    {
+        $pid=$this->request->post('row.pid');
+        $rules=$this->request->post('row.rules');
+        $ids=$this->request->get('ids');
+        if($pid==$ids){
+            $this->error(__('上级不能是自己'));
+        }
+        if(empty($rules)){
+            $this->error(__('角色操作权限不能为空'));
+        }
+        if(!$this->auth->isSuperAdmin()){
+            $auth_rules=explode(',',$this->request->post('row.auth_rules'));
+            $userrule=$this->auth->getUserRuleList();
+            $usermenu=$this->auth->getUserMenuList();
+            $arr=array_column(array_merge($userrule,$usermenu),'id');
+            for($i=0;$i<count($auth_rules);$i++){
+                if(!in_array($auth_rules[$i],$arr)){
+                    $this->error(__('无权操作'));
+                }
+            }
+            $groupids=$this->auth->getChildrenGroupIds();
+            if(!in_array($pid,$groupids)){
+                $this->error(__('无权操作'));
+            }
+        }
+    }
+}

+ 143 - 0
app/admin/controller/auth/Rule.php

@@ -0,0 +1,143 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types=1);
+
+namespace app\admin\controller\auth;
+
+use app\common\controller\Backend;
+use app\admin\traits\Actions;
+use think\annotation\route\Route;
+use think\annotation\route\Group;
+use app\common\model\AuthRule;
+use think\facade\Cache;
+use think\facade\Db;
+
+/**
+ * 规则管理
+ */
+#[Group("auth/rule")]
+class Rule extends Backend
+{
+    use Actions{
+        add as private _add;
+        edit as private _edit;
+    }
+
+    protected function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new AuthRule();
+        Cache::delete('admin_rule_list');
+        Cache::delete('admin_menu_list');
+    }
+
+    #[Route('GET,JSON','index')]
+    public function index()
+    {
+        if (false === $this->request->isAjax()) {
+            return $this->fetch();
+        }
+        $tree=AuthRule::getRuleListTree('*');
+        $result = ['total' => 1000, 'rows' => $tree];
+        return json($result);
+    }
+
+    #[Route('GET,POST','add')]
+    public function add()
+    {
+        $this->beforeAction();
+        return $this->_add();
+    }
+
+    #[Route('GET,POST','edit')]
+    public function edit()
+    {
+        $this->beforeAction();
+        return $this->_edit();
+    }
+
+    #[Route('GET,POST','del')]
+    public function del()
+    {
+        $ids = $this->request->param("ids");
+        $list = $this->model->where('id', 'in', $ids)->select();
+        foreach ($list as $item) {
+            $ins=AuthRule::where(['pid'=>$item->id,'ismenu'=>1])->count();
+            if($ins>0){
+                $this->error(__('请先删除【%s】的子菜单',['s'=>$item->title]));
+            }
+        }
+        $count = 0;
+        Db::startTrans();
+        try {
+            foreach ($list as $item) {
+                AuthRule::where(['pid'=>$item->id])->delete();
+                $count += $item->delete();
+            }
+            Db::commit();
+        } catch (\Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        if ($count) {
+            $this->success();
+        }
+        $this->error(__('没有记录被删除'));
+    }
+
+    private function beforeAction()
+    {
+        if(!$this->request->isPost()){
+            $tree=AuthRule::getRuleListTree('*',true);
+            $ruledata=array_merge(array([
+                'id'=>'0',
+                'title'=>__('无'),
+                'childlist'=>[]
+            ]),$tree);
+            $this->assign('ruledata',$ruledata);
+            $this->assign('menutypeList',AuthRule::menutypeList);
+        }else{
+            $ismenu=$this->request->post('row.ismenu');
+            $controller=$this->request->post('row.controller');
+            if($controller && !class_exists($controller)){
+                $this->error(__('控制器不存在'));
+            }
+            if($ismenu){
+                $action=$this->request->post('row.action');
+                if($action){
+                    if(!method_exists($controller,$action)){
+                        $this->error(__('方法%s不存在',['s'=>$action]));
+                    }
+                }
+            }else{
+                $this->postParams['menutype']='';
+                $this->postParams['icon']='';
+                $this->postParams['extend']='';
+                $this->postParams['status']='';
+                $actions=$this->request->post('row.actions');
+                if(!$actions){
+                    $this->error(__('请填写方法列表'));
+                }
+                $actions=json_decode(htmlspecialchars_decode($actions),true);
+                $title=[];
+                $action=[];
+                foreach ($actions as $key=>$value){
+                    if(!method_exists($controller,$key)){
+                        $this->error(__('方法%s不存在',['s'=>$key]));
+                    }
+                    $action[]=$key;
+                    $title[]=$value;
+                }
+                $this->postParams['action']=json_encode($action,JSON_UNESCAPED_UNICODE);
+                $this->postParams['title']=json_encode($title,JSON_UNESCAPED_UNICODE);
+            }
+        }
+    }
+}

+ 135 - 0
app/admin/controller/general/Attachment.php

@@ -0,0 +1,135 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types=1);
+
+namespace app\admin\controller\general;
+
+use app\admin\traits\Actions;
+use app\common\controller\Backend;
+use app\common\model\Attachment as AttaModel;
+use think\annotation\route\Group;
+use think\annotation\route\Route;
+use think\facade\Cache;
+use think\facade\Db;
+use app\common\model\Config;
+
+/**
+ * 附件管理
+ */
+#[Group("general/attachment")]
+class Attachment extends Backend
+{
+    protected $noNeedRight=['select'];
+
+    use Actions{
+        add as private _add;
+        del as private _del;
+    }
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model=new AttaModel();
+        $this->assign('categoryList',AttaModel::getCategory());
+        $this->assign('disksList',AttaModel::getDisksType());
+    }
+
+    #[Route("GET","select")]
+    public function select()
+    {
+        if($this->request->isAjax()){
+            $limit=[
+                'page'  => $this->request->get('page/d',1),
+                'list_rows' => 17
+            ];
+            $list = $this->model->where(function($query){
+                $category=$this->request->param('category');
+                $keywords=$this->request->param('keywords');
+                $query->where('is_image',1);
+                if($category && $category=='unclassed'){
+                    $query->where('category="" or category is null');
+                }
+                if($category && $category!='all' && $category!='unclassed'){
+                    $query->where('category',$category);
+                }
+                if($keywords){
+                    $query->where('filename','like',"%{$keywords}%");
+                }
+            })->order('weigh desc,id desc')->paginate($limit);
+            $result = ['total' => $list->total(), 'rows' => $list->items()];
+            $this->success('',$result);
+        }
+        $this->assign('limit', $this->request->get('limit',5));
+        return $this->fetch();
+    }
+
+    #[Route("POST,GET","add")]
+    public function add()
+    {
+        if($this->request->isPost()){
+            $this->success();
+        }
+        return $this->_add();
+    }
+
+    #[Route("POST","setcate")]
+    public function setcate()
+    {
+        $type=$this->request->post('type');
+        $key=$this->request->post('key');
+        $value=$this->request->post('value');
+        $cateconfig=Config::where(['group'=>'dictionary','name'=>'filegroup'])->find();
+        $arr=$cateconfig->value;
+        if($type=='add' && isset($arr[$key])){
+            $this->error('分类已存在');
+        }
+        $message='';
+        if($type=='add') {
+            $arr[$key] = $value;
+            $message = '添加成功';
+        }
+        if($type=='edit') {
+            $arr[$key] = $value;
+            $message = '修改成功';
+        }
+        if($type=='del') {
+            unset($arr[$key]);
+            AttaModel::where('category',$key)->update(['category'=>'']);
+            $message = '删除成功';
+        }
+        $cateconfig->value=json_encode($arr,JSON_UNESCAPED_UNICODE);
+        $cateconfig->save();
+        Cache::delete('site_config_dictionary');
+        $this->success($message,$arr);
+    }
+
+    #[Route("POST,GET","del")]
+    public function del()
+    {
+        $ids = $this->request->param("ids");
+        $list = $this->model->where('id', 'in', $ids)->select();
+        $count = 0;
+        Db::startTrans();
+        try {
+            foreach ($list as $item) {
+                $classname=config('filesystem.disks')[$item->storage]['class'];
+                $classname::deleteFile($item);
+                $count += $item->delete();
+            }
+            Db::commit();
+        } catch (\Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        if ($count) {
+            $this->success();
+        }
+        $this->error(__('没有记录被删除'));
+    }
+}

+ 93 - 0
app/admin/controller/general/Category.php

@@ -0,0 +1,93 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types=1);
+
+namespace app\admin\controller\general;
+
+use app\admin\traits\Actions;
+use app\common\controller\Backend;
+use app\common\library\Tree;
+use think\annotation\route\Group;
+use app\common\model\Category as CategoryModel;
+use think\annotation\route\Route;
+
+/**
+ * 分类管理
+ */
+#[Group("general/category")]
+class Category extends Backend
+{
+    protected $noNeedRight = ['selectpage'];
+
+    private $categorylist = [];
+
+    use Actions{
+        index as private _index;
+        edit as private _edit;
+        add as private _add;
+    }
+
+    protected function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new CategoryModel();
+        $this->assign('typeList',site_config("dictionary.categorytype"));
+    }
+
+    #[Route("*","index")]
+    public function index()
+    {
+        if($this->request->isAjax()){
+            $result = ['total' => 1000, 'rows' => $this->getCatelist()];
+            return json($result);
+        }
+        return $this->_index();
+    }
+
+    #[Route("POST,GET","add")]
+    public function add()
+    {
+        if(!$this->request->isPost()){
+            $catelist=$this->getCatelist();
+            foreach ($catelist as $k => $v) {
+                $categorydata[$v['id']] = $v;
+            }
+            $this->assign('parentList',$categorydata);
+        }
+        return $this->_add();
+    }
+
+    #[Route("POST,GET","edit")]
+    public function edit()
+    {
+        if(!$this->request->isPost()){
+            $catelist=$this->getCatelist();
+            foreach ($catelist as $k => $v) {
+                $categorydata[$v['id']] = $v;
+            }
+            $this->assign('parentList',$categorydata);
+        }
+        return $this->_edit();
+    }
+
+    private function getCatelist()
+    {
+        $tree = Tree::instance();
+        $list=$this->model->order('weigh desc,id desc')->where(function ($query){
+            $type = $this->filter("type");
+            if($type){
+                $query->where('type',$type);
+            }
+        })->select()->toArray();
+        $tree->init($list, 'pid');
+        $categorylist = $tree->getTreeList($tree->getTreeArray(0), 'name');
+        return $categorylist;
+    }
+}

+ 208 - 0
app/admin/controller/general/Config.php

@@ -0,0 +1,208 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types = 1);
+
+namespace app\admin\controller\general;
+
+use app\common\controller\Backend;
+use app\common\model\Addons;
+use app\common\model\Config as ConfigModel;
+use think\annotation\route\Group;
+use think\annotation\route\Route;
+use think\facade\Cache;
+
+/**
+ * 系统配置
+ */
+#[Group("general/config")]
+class Config extends Backend
+{
+    public function _initialize()
+    {
+        parent::_initialize();
+    }
+
+    #[Route("GET","index")]
+    public function index()
+    {
+        if($this->request->isAjax()){
+            $group=$this->request->get('group');
+            $list=ConfigModel::where(['group'=>$group])->select();
+            if($group=='addons'){
+                $result=[];
+                foreach ($list as $item){
+                    $pack=$item->addons;
+                    if(!isset($result[$pack])){
+                        $name='';
+                        $type='';
+                        $addons=Addons::where(['pack'=>$pack])->find();
+                        if($addons){
+                            $name=$addons->name;
+                            $type=Addons::TYPE[$addons['type']];
+                        }
+                        $result[$pack]=[
+                            'key'=>$pack,
+                            'name'=>$name,
+                            'type'=>$type,
+                            'list'=>[]
+                        ];
+                    }
+                    $result[$pack]['list'][]=$item;
+                    //替换value中的{$host}为本地域名
+                    if($item['value']){
+                        $value=str_replace('{$host}',$this->request->host(),$item['value']);
+                        $item['value']=$value;
+                    }
+                }
+                $this->success('',array_values($result));
+            }
+            $this->success('',$list);
+        }
+        $groupList= ConfigModel::where(['group'=>'dictionary','name'=>'configgroup'])->find()->value;
+        $typeList=ConfigModel::getTypeList();
+        $this->assign('groupList',$groupList);
+        $this->assign('typeList',$typeList);
+        $this->assign('app_debug',config('app.app_debug'));
+        return $this->fetch();
+    }
+
+    #[Route("POST","del")]
+    public function del()
+    {
+        $app_debug=\think\facade\Config::get('app.app_debug');
+        if(!$app_debug){
+            $this->error(__('非调试模式下不能删除配置'));
+        }
+        $group=$this->request->post('group');
+        $name=$this->request->post('name');
+        $config=ConfigModel::where(['name'=>$name,'group'=>$group])->find();
+        if(!$config->can_delete){
+            $this->error(__('该配置不可删除'));
+        }
+        $config->delete();
+        Cache::delete('site_config_'.$group);
+        $this->success();
+    }
+
+    #[Route("POST","add")]
+    public function add()
+    {
+        $app_debug=\think\facade\Config::get('app.app_debug');
+        if(!$app_debug){
+            $this->error(__('非调试模式下不能增加配置'));
+        }
+        $data=$this->request->post('row/a');
+        $name=($data['group']=='addons')?$data['addons_pack'].'_'.$data['name']:$data['name'];
+        ConfigModel::where(['name'=>$name,'group'=>$data['group']])->find() && $this->error(__('配置已存在'));
+        if($data['group']=='addons' && $data['addons_pack']==''){
+            $this->error(__('扩展包名不能为空'));
+        }
+        $extend='';
+        switch ($data['type']){
+            case 'radio':
+            case 'select':
+                if(!isset($data['options'])){
+                    $this->error(__('选项不能为空'));
+                }
+                $extend=htmlspecialchars_decode($data['options']);
+                break;
+            case 'checkbox':
+            case 'selects':
+                if(!isset($data['options'])){
+                    $this->error(__('选项不能为空'));
+                }
+                if($data['value']){
+                    $isarr=str_starts_with($data['value'], '[') &&  str_ends_with($data['value'], ']');
+                    if(!$isarr){
+                        $this->error(__('默认值必须是数组'));
+                    }
+                }
+                $extend=htmlspecialchars_decode($data['options']);
+                break;
+            case 'selectpage':
+            case 'selectpages':
+                if($data['url']===''){
+                    $this->error(__('参数%s不能为空',['s'=>'url']));
+                }
+                if($data['keyField']===''){
+                    $this->error(__('参数%s不能为空',['s'=>'keyField']));
+                }
+                if($data['labelField']===''){
+                    $this->error(__('参数%s不能为空',['s'=>'labelField']));
+                }
+                $extend=json_encode([
+                    'url'=>$data['url'],
+                    'keyField'=>$data['keyField'],
+                    'labelField'=>$data['labelField'],
+                ],JSON_UNESCAPED_UNICODE);
+                break;
+            case 'json':
+                if(!$data['label']){
+                    $data['label']=['键名','键值'];
+                }else{
+                    $data['label']=explode(',',$data['label']);
+                }
+                if(!$data['keys']){
+                    $data['keys']=['0','1'];
+                }else{
+                    $data['keys']=explode(',',$data['keys']);
+                }
+                $extend=json_encode([$data['label'],$data['keys']],JSON_UNESCAPED_UNICODE);
+                break;
+        }
+        (new ConfigModel())->save([
+            'name'=>$name,
+            'title'=>$data['title'],
+            'type'=>$data['type'],
+            'addons'=>($data['group']=='addons')?$data['addons_pack']:'',
+            'group'=>$data['group'],
+            'value'=>$data['value'],
+            'rules'=>str_replace(',',';',$data['rules']),
+            'tips'=>$data['tips'],
+            'extend'=>$extend,
+            'can_delete'=>1
+        ]);
+        Cache::delete('site_config_'.$data['group']);
+        $this->success();
+    }
+
+    #[Route("POST","edit")]
+    public function edit()
+    {
+        $app_debug=\think\facade\Config::get('app.app_debug');
+        if(!$app_debug){
+            $this->error(__('非调试模式下不能修改配置'));
+        }
+        $data=$this->request->post('row/a');
+        $group=$data['group'];
+        if($group=='addons'){
+            unset($data['addons']);
+        }
+        foreach ($data as $key=>$value){
+            $value=htmlspecialchars_decode($value);
+            if($group=='dictionary' && $key=='configgroup'){
+                $editvalue=json_decode($value,true);
+                $arr=array_keys($editvalue);
+                if(!in_array('basic',$arr)){
+                    $this->error(__('基础配置项必须存在'));
+                }
+                if(!in_array('addons',$arr)){
+                    $this->error(__('插件配置项必须存在'));
+                }
+                if(!in_array('dictionary',$arr)){
+                    $this->error(__('配置分组项必须存在'));
+                }
+            }
+            ConfigModel::where(['group'=>$group,'name'=>$key])->update(['value'=>$value]);
+        }
+        Cache::delete('site_config_'.$data['group']);
+        $this->success();
+    }
+}

+ 91 - 0
app/admin/controller/general/Profile.php

@@ -0,0 +1,91 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types=1);
+
+namespace app\admin\controller\general;
+
+use app\common\model\Admin;
+use app\common\controller\Backend;
+use app\common\model\AdminLog;
+use think\annotation\route\Group;
+use think\annotation\route\Route;
+use think\facade\Session;
+use think\facade\Validate;
+
+/**
+ * 个人信息
+ */
+#[Group("general/profile")]
+class Profile extends Backend
+{
+    protected $noNeedRight='*';
+
+    protected $relationField=[];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model=new AdminLog();
+    }
+
+    /**
+     * 查看
+     */
+    #[Route("*",'index')]
+    public function index()
+    {
+        if (false === $this->request->isAjax()) {
+            $thirdLogin=addons_installed('uniapp') && site_config("uniapp.scan_login");
+            $field='id,username,nickname,mobile,avatar';
+            if($thirdLogin){
+                $field.=',third_id';
+            }
+            $this->assign('thirdLogin',$thirdLogin);
+            $this->assign('admininfo',Admin::field($field)->find($this->auth->id));
+            return $this->fetch();
+        }
+        $where=[];
+        if(!$this->auth->isSuperAdmin()){
+            $where[]=['admin_id','=',$this->auth->id];
+        }
+        [$where, $order, $limit, $with] = $this->buildparams($where);
+        $list = $this->model
+            ->where($where)
+            ->order($order)
+            ->paginate($limit);
+        $result = ['total' => $list->total(), 'rows' => $list->items()];
+        return json($result);
+    }
+
+    /**
+     * 更新个人信息
+     */
+    #[Route("POST",'update')]
+    public function update()
+    {
+        $params = $this->request->post("row/a");
+        if(!empty($params['password'])){
+            if (!Validate::is($params['password'], '\S{6,30}')) {
+                $this->error(__('密码长度不对!'));
+            }
+            $params['salt'] = str_rand(4);
+            $params['password'] = md5(md5($params['password']) . $params['salt']);
+        }else{
+            unset($params['password']);
+        }
+        $admin=Admin::find($this->auth->id);
+        $admin->save($params);
+        Session::set('admin.mobile',$params['mobile']);
+        Session::set('admin.nickname',$params['nickname']);
+        Session::set('admin.avatar',$params['avatar']);
+        Session::save();
+        $this->success();
+    }
+}

+ 89 - 0
app/admin/controller/user/Index.php

@@ -0,0 +1,89 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare (strict_types = 1);
+
+namespace app\admin\controller\user;
+
+use app\common\controller\Backend;
+use think\annotation\route\Group;
+use app\admin\traits\Actions;
+use app\common\model\User;
+use app\common\model\UserLog;
+use think\annotation\route\Route;
+
+#[Group("user/index")]
+class Index extends Backend
+{
+    protected $noNeedRight = ['index'];
+
+    use Actions;
+
+    protected function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new User();
+    }
+
+    #[Route('POST,GET','recharge')]
+    public function recharge($ids)
+    {
+        if($this->request->isPost()){
+            $module_type=$this->request->post('row.module_type');
+            $change_type=$this->request->post('row.recharge_type');
+            $change=$this->request->post('row.change/d');
+            $remark=$this->request->post('row.remark');
+            $order_no=time().rand(1000,9999);
+            switch ($module_type){
+                case 'score':
+                    UserLog::addScoreLog($ids,$change_type,$change,$order_no,$remark);
+                    break;
+                case 'balance':
+                    UserLog::addBalanceLog($ids,$change_type,$change,$order_no,$remark);
+                    break;
+            }
+            $this->success();
+        }else{
+            $user=User::find($ids);
+            $this->assign('moduletype',UserLog::TYPE);
+            $this->assign('user',$user);
+            return $this->fetch();
+        }
+    }
+
+    #[Route('GET','test')]
+    public function test()
+    {
+        return $this->fetch();
+    }
+
+    #[Route('GET,JSON','detail')]
+    public function detail($ids)
+    {
+        if($this->request->isAjax()){
+            $this->model=new UserLog();
+            $where=[];
+            $where[]=['type','=',$this->request->get('type')];
+            $where[]=['user_id','=',$ids];
+            [$where, $order, $limit, $with] = $this->buildparams($where);
+            $list = $this->model
+                ->where($where)
+                ->order($order)
+                ->paginate($limit);
+            $result = ['total' => $list->total(), 'rows' => $list->items()];
+            return json($result);
+        }else{
+            $user=User::find($ids);
+            $this->assign('moduletype',UserLog::TYPE);
+            $this->assign('user',$user);
+            return $this->fetch();
+        }
+    }
+}
+

+ 39 - 0
app/admin/lang/en-us.php

@@ -0,0 +1,39 @@
+<?php
+use app\admin\controller\Ajax;
+use app\admin\controller\Index;
+return [
+    //全局语言包
+    'default'=>[
+        '添加'=>'Add',
+        '编辑'=>'Edit',
+        '删除'=>'Delete',
+        '更多'=>'More',
+        '正常'=>'Normal',
+        '隐藏'=>'Hidden',
+        '是'=>'Yes',
+        '否'=>'No',
+    ],
+    //控制器语言包
+    'controller'=>[
+        Index::class=>[
+            '控制台'=>'Dashboard',
+            '常规管理'=>'General',
+            '系统配置'=>'System',
+            '分类管理'=>'Category',
+            '附件管理'=>'Attachment',
+            '个人资料'=>'Profile',
+            '菜单规则'=>'Menu',
+            '管理员管理'=>'Admin',
+            '角色组'=>'Role',
+            '管理员日志'=>'Admin Log',
+            '一键Crud'=>'Crud',
+            '任务队列'=>'Task Queue',
+            '权限管理'=>'Permission',
+            '开发管理'=>'Development',
+        ],
+        Ajax::class=>[
+
+        ]
+        //...英语不好,自己加
+    ]
+];

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

@@ -0,0 +1,18 @@
+<?php
+use app\admin\controller\Ajax;
+use app\admin\controller\Index;
+return [
+    //全局语言包
+    'default'=>[
+
+    ],
+    //控制器语言包
+    'controller'=>[
+        Index::class=>[
+
+        ],
+        Ajax::class=>[
+
+        ]
+    ]
+];

+ 39 - 0
app/admin/lang/zh-tw.php

@@ -0,0 +1,39 @@
+<?php
+use app\admin\controller\Ajax;
+use app\admin\controller\Index;
+return [
+    //全局语言包
+    'default'=>[
+        '添加'=>'添加',
+        '编辑'=>'編輯',
+        '删除'=>'刪除',
+        '更多'=>'更多',
+        '正常'=>'正常',
+        '隐藏'=>'隱藏',
+        '是'=>'是',
+        '否'=>'否',
+    ],
+    //控制器语言包
+    'controller'=>[
+        Index::class=>[
+            '控制台'=>'控制台',
+            '常规管理'=>'常規管理',
+            '系统配置'=>'系統配置',
+            '分类管理'=>'分類管理',
+            '附件管理'=>'附件管理',
+            '个人资料'=>'個人資料',
+            '菜单规则'=>'菜單規則',
+            '管理员管理'=>'管理員管理',
+            '角色组'=>'角色組',
+            '管理员日志'=>'管理員日志',
+            '一键Crud'=>'一鍵Crud',
+            '任务队列'=>'任務隊列',
+            '权限管理'=>'權限管理',
+            '开发管理'=>'開發管理'
+        ],
+        Ajax::class=>[
+
+        ]
+        //...自己加
+    ]
+];

+ 7 - 0
app/admin/middleware.php

@@ -0,0 +1,7 @@
+<?php
+return [
+    //启用session
+    think\middleware\SessionInit::class,
+    //非选项卡时重定向
+    app\admin\middleware\Redirect::class
+];

+ 31 - 0
app/admin/middleware/Redirect.php

@@ -0,0 +1,31 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types = 1);
+
+namespace app\admin\middleware;
+
+use think\facade\Session;
+
+class Redirect{
+    public function handle($request, \Closure $next)
+    {
+        $modulealis=get_module_alis();
+        // 非选项卡时重定向
+        if (!$request->isPost() && !$request->isAjax() && !$request->isJson() && input("ref") == 'addtabs') {
+            $url = preg_replace_callback("/([\?|&]+)ref=addtabs(&?)/i", function ($matches) {
+                return $matches[2] == '&' ? $matches[1] : '';
+            }, $request->url());
+            Session::set('referer',build_url($url));
+            Session::save();
+            redirect('/'.$modulealis.'/index')->send();
+        }
+        return $next($request);
+    }
+}

+ 24 - 0
app/admin/route/route.php

@@ -0,0 +1,24 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types=1);
+
+use think\facade\Route;
+
+//后台首页
+Route::get('/',function(){
+    $alis=get_module_alis('admin');
+    if(isset($_GET['del_install']) && $_GET['del_install']==1){
+        $install=root_path().'/public/install';
+        if(is_dir($install)){
+            rmdirs($install);
+        }
+    }
+    return redirect('/'.$alis.'/index');
+});

+ 523 - 0
app/admin/service/AdminAuthService.php

@@ -0,0 +1,523 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types=1);
+namespace app\admin\service;
+
+use app\common\library\Tree;
+use app\common\model\Admin;
+use app\common\model\AuthGroup;
+use app\common\model\AuthRule;
+use app\common\model\QrcodeScan;
+use app\common\model\Third;
+use app\common\service\AuthService;
+use think\facade\Config;
+use think\facade\Session;
+use think\facade\Cache;
+
+class AdminAuthService extends AuthService{
+    protected $allowFields = ['id', 'username', 'nickname', 'mobile', 'avatar', 'third_id', 'depart_id', 'groupids', 'status'];
+    protected $userRuleList = [];
+    protected $userMenuList = [];
+    private $platformList=[];
+    private $modulealis;
+    private $modulename;
+    private $controllername;
+    private $actionname;
+
+
+    protected function init()
+    {
+        parent::init();
+        $this->setUserRuleAndMenu();
+    }
+
+    public function getUserRuleList()
+    {
+        return $this->userRuleList;
+    }
+
+    public function getUserMenuList()
+    {
+        return $this->userMenuList;
+    }
+
+    public function userinfo(bool $allinfo=false)
+    {
+        $r=Session::get('admin');
+        if(!$r){
+            return null;
+        }
+        if (Config::get('yunqi.login_unique')) {
+            $my = Admin::get($this->id);
+            if (!$my || $my['token'] != $r['token']) {
+                Session::delete("admin");
+                Session::save();
+                return null;
+            }
+        }
+        if(Config::get('yunqi.loginip_check')){
+            if (request()->ip()!=$r['loginip']) {
+                Session::delete("admin");
+                Session::save();
+                return null;
+            }
+        }
+        $r['groupids']=array_map('intval',explode(',',$r['groupids']));
+        if($allinfo){
+            return $r;
+        }
+        return array_intersect_key($r,array_flip($this->allowFields));
+    }
+
+    public function isSuperAdmin():bool
+    {
+        return in_array(1,$this->groupids);
+    }
+
+    public function getElementUi($elementUi)
+    {
+        if($this->element_ui){
+            $data=json_decode($this->element_ui,true);
+            foreach ($data as $k=>$v){
+                $elementUi[$k]=$v;
+            }
+        }
+        return $elementUi;
+    }
+
+    public function getChildrenGroupIds(array $groupids=[]):array
+    {
+        if(count($groupids)==0){
+            $groupids=$this->groupids;
+        }
+        $list=AuthGroup::where('pid','in',$groupids)->column('id');
+        $groupids=array_merge($groupids,$list);
+        if(count($list)>0){
+            $r=$this->getChildrenGroupIds($list);
+            $groupids=array_merge($groupids,$r);
+        }
+        return array_unique($groupids);
+    }
+
+    public function getRoute(string $type):string
+    {
+        switch ($type){
+            case 'modulealis':
+                return $this->modulealis;
+            case 'modulename':
+                return $this->modulename;
+            case 'controllername':
+                return $this->controllername;
+            case 'actionname':
+                return $this->actionname;
+            case 'title':
+                $rulelist=$this->getRuleList();
+                $actiontitle=['未定义'];
+                $rulepid=0;
+                foreach ($rulelist as $rule){
+                    if($rule['controller']==$this->controllername && !$rule['ismenu']){
+                        $action=json_decode($rule['action'],true);
+                        $title=json_decode($rule['title'],true);
+                        foreach ($action as $key=>$item){
+                            if($item==$this->actionname){
+                                $actiontitle=[$title[$key]];
+                                $rulepid=$rule['pid'];
+                            }
+                        }
+                    }
+                }
+                if($rulepid){
+                    $this->getRuleTitles($rulelist,$rulepid,$actiontitle);
+                    return implode('/',array_reverse($actiontitle));
+                }
+                return '未定义';
+            default:
+                return '';
+        }
+    }
+
+    public function logout()
+    {
+        $admin = Admin::find(intval($this->id));
+        if ($admin) {
+            $admin->token = '';
+            $admin->save();
+        }
+        Session::delete("admin");
+        return true;
+    }
+
+    public function loginByThird(string $__token__,$admin_id,array &$adminlist):bool
+    {
+        $scan=QrcodeScan::where(['type'=>'backend-login','foreign_key'=>$__token__])->order('id desc')->find();
+        if($scan){
+            $third=Third::where(['platform'=>Third::PLATFORM('微信公众号'),'openid'=>$scan->openid])->find();
+            if($third){
+                $list=Admin::where(['third_id'=>$third->id])->select();
+                $adminlist=[];
+                foreach ($list as $item){
+                    unset($item['password']);
+                    unset($item['salt']);
+                    if($item['status']!='normal'){
+                        continue;
+                    }
+                    $adminlist[]=$item;
+                }
+                if(count($adminlist)==1){
+                    $admin=$adminlist[0];
+                    $admin->loginfailure = 0;
+                    $admin->logintime = time();
+                    $admin->loginip = request()->ip();
+                    $admin->token = uuid();
+                    $admin->save();
+                    Session::set('admin',$admin->toArray());
+                    Session::save();
+                    return true;
+                }
+                if(count($adminlist)>1 && $admin_id){
+                    foreach ($adminlist as $xitem){
+                        if($xitem['id']==$admin_id){
+                            $admin=$xitem;
+                            $admin->loginfailure = 0;
+                            $admin->logintime = time();
+                            $admin->loginip = request()->ip();
+                            $admin->token = uuid();
+                            $admin->save();
+                            Session::set('admin',$admin->toArray());
+                            Session::save();
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    public function login(string $username, string $password)
+    {
+        $admin = Admin::where(['username' => $username])->find();
+        if (!$admin) {
+            throw new \Exception('用户名或密码错误!');
+        }
+        if ($admin['status'] == 'hidden') {
+            throw new \Exception('用户已被禁止使用!');
+        }
+        if (Config::get('yunqi.login_failure_retry') && $admin->loginfailure >= 10 && time() - $admin->updatetime < 86400) {
+            throw new \Exception('登陆失败次数过多,请一天后重试!');
+        }
+        if ($admin->password != md5(md5($password) . $admin->salt)) {
+            $admin->loginfailure++;
+            $admin->save();
+            throw new \Exception('用户名或密码错误!');
+        }
+        $admin->loginfailure = 0;
+        $admin->logintime = time();
+        $admin->loginip = request()->ip();
+        $admin->token = uuid();
+        $admin->save();
+        Session::set('admin',$admin->toArray());
+        Session::save();
+        return $admin;
+    }
+
+    public function getRuleList()
+    {
+        $rule = AuthRule::field('id,pid,status,controller,action,title,icon,menutype,ismenu,isplatform,extend')
+            ->order('weigh', 'desc')
+            ->cache('admin_rule_list')
+            ->select()
+            ->toArray();
+        foreach ($rule as $k=>$v) {
+            if($v['ismenu'] && $v['status']=='hidden'){
+                unset($rule[$k]);
+                continue;
+            }
+            if($v['ismenu'] && $v['status']=='normal'){
+                $rule[$k]['url'] = $this->getPath($v['controller'], $v['action']);
+            }
+        }
+        return $rule;
+    }
+
+    /**
+     * 根据控制器注解获取到菜单栏的path
+     * @param string $controller
+     * @param string $action
+     * @return string
+     */
+    public function getPath(mixed $controller,mixed $action):string
+    {
+        $url='';
+        if(!$controller || !$action){
+            return build_url('404');
+        }
+        if(!class_exists($controller) || !method_exists($controller,$action)){
+            return build_url('404');
+        }
+        $class=new \ReflectionClass($controller);
+        $attributes=$class->getAttributes();
+        foreach ($attributes as $attribute)
+        {
+            $name=$attribute->getName();
+            if($name=='think\annotation\route\Group'){
+                $url=$attribute->getArguments()[0].'/';
+                break;
+            }
+        }
+        $method=new \ReflectionMethod($controller, $action);
+        $attributes=$method->getAttributes();;
+        foreach ($attributes as $attribute)
+        {
+            $name=$attribute->getName();
+            if($name=='think\annotation\route\Get' || $name=='think\annotation\route\Post'){
+                $url=$url.$attribute->getArguments()[0];
+                break;
+            }
+            if($name=='think\annotation\route\Route'){
+                $url=$url.$attribute->getArguments()[1];
+                break;
+            }
+        }
+        return build_url($url);
+    }
+
+    private function getRuleTitles(array $rulelist,int $ruleid,array &$actiontitle)
+    {
+        foreach ($rulelist as $rule){
+            if($rule['id']==$ruleid){
+                $actiontitle[]=$rule['title'];
+                if($rule['pid']){
+                    $this->getRuleTitles($rulelist,$rule['pid'],$actiontitle);
+                }
+            }
+        }
+    }
+    /**
+     * 为用户权限列表ruleList赋值
+     */
+    private function setUserRuleAndMenu()
+    {
+        if($this->id){
+            $adminRuleList= Cache::get('admin_rule_list_'.$this->id);
+            $adminMenuList= Cache::get('admin_menu_list_'.$this->id);
+            $platformList=Cache::get('admin_platform_list_'.$this->id);
+            $rulelist=$this->getRuleList();
+            if(!$adminRuleList || !$adminMenuList || $platformList || Config::get('app.app_debug')){
+                $rules=array_column($rulelist,null,'id');
+                $groups=AuthGroup::column('auth_rules','id');;
+                foreach ($groups as $key=>$value){
+                    $value=explode(',',$value);
+                    $groups[$key]=array_filter(array_map(function($v) use ($rules){
+                        if($v=='*'){
+                            return '*';
+                        }
+                        return isset($rules[$v])?$rules[$v]:'';
+                    },$value),function ($f){
+                        return $f!='';
+                    });
+                }
+                $rulesids=[];
+                $menuids=[];
+                $platformids=[];
+                if($this->isSuperAdmin()){
+                    $adminRuleList='*';
+                    $adminMenuList='*';
+                    $platformList=array(['id'=>0,'title'=>'管理平台']);
+                    foreach ($rulelist as $value){
+                        if($value['isplatform']){
+                            array_push($platformList,['id'=>$value['id'],'title'=>$value['title']]);
+                        }
+                    }
+                }else{
+                    $adminRuleList=[];
+                    $adminMenuList=[];
+                    $platformList=[];
+                    foreach ($this->groupids as $groupid){
+                        foreach ($groups[$groupid] as $value){
+                            if($value['ismenu']===1){
+                                continue;
+                            }
+                            if(in_array($value['id'],$rulesids)){
+                                continue;
+                            }
+                            $adminRuleList[]=$value;
+                            $rulesids[]=$value['id'];
+                        }
+                        foreach ($groups[$groupid] as $value){
+                            if($value['ismenu']===0){
+                                continue;
+                            }
+                            if(in_array($value['id'],$menuids)){
+                                continue;
+                            }
+                            $adminMenuList[]=$value;
+                            $menuids[]=$value['id'];
+                        }
+                        foreach ($groups[$groupid] as $value){
+                            if($value['pid']===0 && $value['isplatform']===0 && !in_array(0,$platformids)){
+                                $platformList[]=['id'=>0,'title'=>'管理平台'];
+                                $platformids[]=0;
+                            }
+                            if($value['isplatform']===0){
+                                continue;
+                            }
+                            if(in_array($value['id'],$platformids)){
+                                continue;
+                            }
+                            $platformList[]=['id'=>$value['id'],'title'=>$value['title']];
+                            $platformids[]=$value['id'];
+                        }
+                    }
+                }
+                Cache::set('admin_rule_list_'.$this->id,$adminRuleList);
+                Cache::set('admin_menu_list_'.$this->id,$adminMenuList);
+                Cache::set('admin_platform_list_'.$this->id,$platformList);
+            }
+            $this->userRuleList=$adminRuleList;
+            $this->userMenuList=$adminMenuList;
+            $this->platformList=$platformList;
+        }
+    }
+
+    /**
+     * 检测权限
+     * @param string $controller
+     * @param string $action
+     * @return mixed
+     */
+    public function check(string $controller,string $action):int
+    {
+        if ($this->userRuleList=='*') {
+            return 1;
+        }
+        foreach ($this->userRuleList as $value){
+            if($value['controller']==$controller){
+                $actions=json_decode($value['action'],true);
+                foreach ($actions as $v){
+                    if($v==$action){
+                        return 1;
+                    }
+                }
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * 获取左侧和顶部菜单栏
+     * @return array
+     */
+    public function getSidebar(mixed $refererUrl=''):array
+    {
+        $ruleList=$this->getRuleList();
+        foreach ($ruleList as $k => &$v) {
+            unset($v['controller']);
+            unset($v['action']);
+            if($this->userMenuList!='*' && !in_array($v['id'],array_column($this->userMenuList,'id'))){
+                unset($ruleList[$k]);
+                continue;
+            }
+            if (!$v['ismenu']) {
+                unset($ruleList[$k]);
+                continue;
+            }
+            if ($v['isplatform']) {
+                unset($ruleList[$k]);
+                continue;
+            }
+            $v['title'] = __($v['title']);
+            if($v['extend']){
+                $v['extend']=json_decode($v['extend'],true);
+            }
+        }
+        $ruleList=array_values($ruleList);
+        $platform_id=$this->getPlatformId();
+        $treeRuleList=Tree::instance()->init($ruleList)->getTreeArray($platform_id);
+        $selected=[];
+        $referer=[];
+        $this->getSelectAndReferer($treeRuleList,$refererUrl,$selected,$referer);
+        if($selected==$referer){
+            $referer=[];
+        }
+        return [$this->platformList,$treeRuleList,$selected,$referer];
+    }
+
+    private function getPlatformId()
+    {
+        if($this->platform_id){
+            foreach ($this->platformList as &$value){
+                if($value['id']==$this->platform_id){
+                    $value['active']=1;
+                    return $this->platform_id;
+                }
+            }
+        }
+        $this->platformList[0]['active']=1;
+        return $this->platformList[0]['id'];
+    }
+
+    private function getSelectAndReferer($treeRuleList,$refererUrl,&$selected,&$referer)
+    {
+        foreach ($treeRuleList as $value){
+            if(count($value['childlist'])===0 && !isset($selected['url'])){
+                $selected=$value;
+            }
+            if($refererUrl){
+                if(parse_url($refererUrl,PHP_URL_PATH)==parse_url($value['url'],PHP_URL_PATH)){
+                    $value['url']=$refererUrl;
+                    $referer=$value;
+                }
+            }
+            if(count($value['childlist'])>0){
+                $this->getSelectAndReferer($value['childlist'],$refererUrl,$selected,$referer);
+            }
+        }
+    }
+
+    public function getRuleId()
+    {
+        foreach ($this->getUserRuleList() as $rule){
+            if($rule['controller']==$this->controllername){
+                $action=json_decode($rule['action'],true);
+                if(in_array($this->actionname,$action)){
+                    return $rule['id'];
+                }
+            }
+        }
+        return null;
+    }
+
+    public function getBackendAuth()
+    {
+        $userlist=$this->userRuleList;
+        //如果$userlist是数组
+        if(is_array($userlist)){
+            foreach ($userlist as $key=>$value){
+                $userlist[$key]['action']=json_decode($value['action'],true);
+                $userlist[$key]['title']=json_decode($value['title'],true);
+            }
+        }
+        return [
+            'admin'=>$this->userinfo(),
+            'rules_list'=>$userlist
+        ];
+    }
+
+    public function loginByMobile(string $mobile, string $code)
+    {
+
+    }
+
+    public function loginByThirdPlatform(string $platform, Third $third)
+    {
+
+    }
+}

+ 759 - 0
app/admin/service/addons/AddonsService.php

@@ -0,0 +1,759 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types=1);
+
+namespace app\admin\service\addons;
+
+use app\common\model\AuthRule;
+use app\common\model\Config;
+use app\common\service\BaseService;
+use app\common\library\Http;
+use app\common\model\Addons;
+use think\facade\Db;
+
+class AddonsService extends BaseService {
+
+    private $pluginsPath;
+
+    private $pluginsHost;
+
+    private $menuid;
+
+    protected function init()
+    {
+        $this->pluginsPath=root_path().'addons'.DS;
+        $this->pluginsHost=config('yunqi.plugins_host');
+    }
+
+    public function getAddonsPath(string $type,string $pack)
+    {
+        $path=$this->pluginsPath.$type.DS.$pack.DS;
+        return $path;
+    }
+
+    public function getAddonsPack(string $type,string $pack,string $version)
+    {
+        $path=$this->pluginsPath.$type.DS.$pack.DS.$version.'.zip';
+        return $path;
+    }
+
+    public function delAddons(string $key)
+    {
+        /* @var Addons $addon*/
+        $addon=Addons::where('key',$key)->find();
+        $addonpath=$this->getAddonsPath($addon['type'],$addon['pack']);
+        rmdirs($addonpath);
+        $addon->delete();
+    }
+
+    public function download(array $addon)
+    {
+        $packdir=$this->pluginsPath.$addon['type'].DS.$addon['pack'];
+        if(is_dir($packdir)){
+            throw new \Exception('存在同名的包【'.$packdir.'】,请先卸载');
+        }
+        $savefile=$this->getAddonsPack($addon['type'],$addon['pack'],$addon['version']);
+        $transaction_id=isset($addon['transaction_id'])?$addon['transaction_id']:'';
+        $response=Http::download($this->pluginsHost.'/addons/download?key='.$addon['key'].'&transaction_id='.$transaction_id,$savefile);
+        if(!$response->isSuccess()){
+            rmdirs($packdir);
+            throw new \Exception($response->errorMsg);
+        }
+        //解压下载文件
+        $zip = new \ZipArchive();
+        if ($zip->open($savefile) === TRUE) {
+            $addonspath=$this->getAddonsPath($addon['type'],$addon['pack']);
+            $package=$addonspath.'package'.DS;
+            $zip->extractTo($package);
+            $zip->close();
+            $this->copy_file($package.'Install.php',$addonspath.'Install.php');
+            if(is_file($package.'install.sql')){
+                $this->copy_file($package.'install.sql',$addonspath.'install.sql');
+                unlink($package.'install.sql');
+            }
+            unlink($package.'Install.php');
+            (new Addons())->save($addon);
+        } else {
+            throw new \Exception('解压失败');
+        }
+    }
+
+    public function checkPayStatus(string $key,string $out_trade_no)
+    {
+        $response=Http::get($this->pluginsHost.'/addons/checkpay?key='.$key.'&out_trade_no='.$out_trade_no);
+        if(!$response->isSuccess()){
+            throw new \Exception($response->errorMsg);
+        }
+        return $response->content;
+    }
+
+    public function uninstall(string $key,array $actions)
+    {
+        $addon=Addons::where('key',$key)->find();
+        $this->includeInstall($addon);
+        $addonpath=$this->getAddonsPath($addon['type'],$addon['pack']);
+        if(!is_file($addonpath.'Install.php')){
+            throw new \Exception('安装文件不存在');
+        }
+        $pack=$this->getAddonsPack($addon['type'],$addon['pack'],$addon['version']);
+        if(!file_exists($pack)){
+            throw new \Exception('禁止卸载未打包的扩展');
+        }
+        $install='\\addons\\'.$addon['type'].'\\'.$addon['pack'].'\\Install';
+        try{
+            Db::startTrans();
+            //删除菜单
+            if(in_array('menu',$actions)){
+                AuthRule::where(['addons'=>$addon['pack']])->delete();
+            }
+            //删除配置
+            if(in_array('config',$actions)){
+                if($addon['type']=='app'){
+                    Config::where(['group'=>$addon['pack']])->delete();
+                }else{
+                    Config::where(['addons'=>$addon['pack']])->delete();
+                }
+            }
+            //删除数据表
+            if(in_array('tables',$actions)){
+                $tables=$this->parseTable($addonpath);
+                foreach ($tables as $table){
+                    $sql="drop table {$table};";
+                    Db::execute($sql);
+                }
+            }
+            //卸载文件
+            foreach ($install::$files as $file){
+                $path=root_path().$file;
+                if(is_file($path)){
+                    unlink($path);
+                }
+                if(is_dir($path)){
+                    rmdirs($path);
+                }
+            }
+            $install::uninstall();
+            $addon->install=0;
+            $addon->save();
+            Db::commit();
+        }catch (\Exception $e){
+            Db::rollback();
+            throw new \Exception($e->getMessage());
+        }
+    }
+
+    public function getAddonsInstallInfo(Addons $addons)
+    {
+        $this->includeInstall($addons);
+        $addonpath=$this->getAddonsPath($addons['type'],$addons['pack']);
+        if(!is_file($addonpath.'Install.php')){
+            throw new \Exception('安装文件不存在');
+        }
+        $install='\\addons\\'.$addons['type'].'\\'.$addons['pack'].'\\Install';
+        return [
+            'files'=>$install::$files,
+            'unpack'=>$install::$unpack,
+            'menu'=>$install::$menu,
+            'require'=>$install::$require,
+            'addons'=>isset($install::$addons)?$install::$addons:[],
+            'config'=>$install::$config,
+            'tables'=>isset($install::$tables)?$install::$tables:[]
+        ];
+    }
+
+    public function payCode(string $key,string $out_trade_no)
+    {
+        $response=Http::get($this->pluginsHost.'/addons/paycode?key='.$key.'&out_trade_no='.$out_trade_no);
+        if(!$response->isSuccess()){
+            throw new \Exception($response->errorMsg);
+        }
+        return $response->content;
+    }
+
+    public function install(string $key)
+    {
+        $addon=Addons::where('key',$key)->find();
+        $this->includeInstall($addon);
+        $addonpath=$this->getAddonsPath($addon['type'],$addon['pack']);
+        if(!is_file($addonpath.'Install.php')){
+            throw new \Exception('安装文件不存在');
+        }
+        if(!is_dir($addonpath.'package')){
+            throw new \Exception('安装包不存在');
+        }
+        $install='\\addons\\'.$addon['type'].'\\'.$addon['pack'].'\\Install';
+        if(!$this->checkInstallFiles($install::$files,$error)){
+            throw new \Exception($error);
+        }
+        //检测表是否存在
+        $tables=$this->parseTable($addonpath);
+        foreach ($tables as $table){
+            if(!empty(Db::query("SHOW TABLES LIKE '{$table}'"))){
+                throw new \Exception('表【'.$table.'】已存在');
+            }
+        }
+        //检测配置是否冲突
+        foreach ($install::$config as &$value){
+            if($addon['type']=='app'){
+                $value['addons']=null;
+                $value['group']=$addon['pack'];
+            }else{
+                $value['addons']=$addon['pack'];
+                $value['group']='addons';
+            }
+            $havaconfig=Config::where(['name'=>$value['name'],'group'=>$value['group']])->find();
+            if($havaconfig){
+                throw new \Exception('配置【'.$value['title'].'】已存在');
+            }
+            $value['can_delete']=1;
+            $value['value']=$value['value']??'';
+            unset($value['id']);
+        }
+        //检测依赖
+        foreach ($install::$require as $require){
+            if(!class_exists($require)){
+                throw new \Exception('缺少类:'.$require.',请先安装依赖包');
+            }
+        }
+        //检测依赖
+        if(isset($install::$addons)){
+            foreach ($install::$addons as $key=>$aons){
+                if(!addons_installed($key)){
+                    throw new \Exception('缺少依赖扩展:'.$aons.',请先安装依赖扩展');
+                }
+            }
+        }
+        //安装菜单
+        $this->menuid=(int)AuthRule::max('id')+1;
+        $menus=$this->parseMenu($install::$menu,$addon['pack']);
+        try{
+            Db::startTrans();
+            Db::name('auth_rule')->insertAll($menus);
+            //安装配置
+            (new Config())->saveAll($install::$config);
+            if($addon['type']=='app'){
+                $configgroup=Config::where(['name'=>'configgroup','group'=>'dictionary'])->value('value');
+                $configgroup=json_decode($configgroup,true);
+                unset($configgroup['dictionary']);
+                $configgroup[$addon['pack']]='应用配置';
+                $configgroup['dictionary']='配置分组';
+                $configgroup=json_encode($configgroup,JSON_UNESCAPED_UNICODE);
+                Config::where(['name'=>'configgroup','group'=>'dictionary'])->update(['value'=>$configgroup]);
+            }
+            //安装sql
+            if(is_file($addonpath.'install.sql')){
+                //直接导入数据库文件
+                $sql=file_get_contents($addonpath.'install.sql');
+                $this->installSql($sql);
+                //修改表名
+                foreach ($tables as $old=>$newtable){
+                    Db::execute("alter table {$old} rename to {$newtable};");
+                }
+            }
+            //安装文件
+            $package=$addonpath.'package'.DS;
+            foreach ($install::$files as $file){
+                $path=$package.$file;
+                if(is_dir($path)){
+                    $this->copy_dir($path,root_path().$file);
+                }
+                if(is_file($path)){
+                    $this->copy_file($path,root_path().$file);
+                }
+            }
+            $install::install();
+            $addon->install=1;
+            $addon->save();
+            Db::commit();
+        }catch(\Exception $e){
+            Db::rollback();
+            throw new \Exception($e->getMessage());
+        }
+    }
+
+    //安装sql文件
+    private function installSql(string $sql)
+    {
+        $host=config('database.connections.mysql.hostname');
+        $port=config('database.connections.mysql.hostport');
+        $dbname=config('database.connections.mysql.database');
+        $dbuser=config('database.connections.mysql.username');
+        $dbpass=config('database.connections.mysql.password');
+        $dsn="mysql:host={$host};port={$port};dbname={$dbname}";
+        $pdo = new \PDO($dsn, $dbuser, $dbpass);
+        $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+        $pdo->exec($sql);
+    }
+
+    public function checkTransactionId(string $pack,string $transaction_id)
+    {
+        $response=Http::get($this->pluginsHost.'/addons/checktransactionid?pack='.$pack.'&transaction_id='.$transaction_id);
+        if(!$response->isSuccess()){
+            throw new \Exception($response->errorMsg);
+        }
+        return $response->content;
+    }
+
+    public function create(array $param)
+    {
+        include __DIR__.DS.'eof.php';
+        $pack=$param['pack'];
+        $type=$param['type'];
+        $addonspath=$this->getAddonsPath($type,$pack);
+        if(is_dir($addonspath)){
+            rmdirs($addonspath);
+        }
+        $haddon=Addons::where(function ($query) use ($param,$pack){
+            $query->where('pack',$pack);
+            if($param['id']){
+                $query->where('id','<>',$param['id']);
+            }
+        })->find();
+        if($haddon){
+            throw new \Exception('包名已经存在,请更换包名');
+        }
+        if($param['id']){
+            $model=Addons::find($param['id']);
+            if(!Addons::checkKey($model)){
+                throw new \Exception('不是你的扩展,无法操作');
+            }
+        }else{
+            $model=new Addons();
+        }
+        $files=$param['files'];
+        //将换行符转换为数组
+        $files=array_map(function ($file){
+            return trim($file);
+        },explode("\n",$files));
+        if(!$this->checkCreateFiles($files,$error)){
+            throw new \Exception($error);
+        }
+        $unpack=$param['unpack'];
+        $unpack=array_map(function ($file){
+            return trim($file);
+        },explode("\n",$unpack));
+        //设置依赖类
+        $require=$param['require'];
+        $require=array_map(function ($class){
+            return trim($class);
+        },explode("\n",$require));
+        //设置依赖扩展
+        $addons=$param['addons'];
+        $addons=array_map(function ($class){
+            return trim($class);
+        },explode("\n",$addons));
+        $tables=$param['tables'];
+        $config=Config::whereIn('id',$param['config'])->select()->toArray();
+        $menu=AuthRule::getRuleList($param['menu']);
+        //加密密钥
+        $param['secret_key']=md5(str_rand(10).rand(1000,9999));
+        //生成key
+        $param['key']=md5($param['type'].$param['pack'].$param['author'].$param['version'].$param['secret_key']);
+        //创建Install文件
+        $files_txt=rtrim(getFilesTxt($files));
+        $unpack_txt=rtrim(getUnpackTxt($unpack));
+        $require_txt=rtrim(getRequireTxt($require));
+        $addons_txt=rtrim(getAddonsTxt($addons));
+        $config_txt=rtrim(getConfigTxt($config));
+        $menu_txt=rtrim(getMenuTxt($menu));
+        $tables_txt=rtrim(getTableTxt($tables));
+        $install=$this->getContent('install',[
+            'pack'=>$pack,
+            'type'=>$type,
+            'files'=>$files_txt,
+            'unpack'=>$unpack_txt,
+            'require'=>$require_txt,
+            'addons'=>$addons_txt,
+            'config'=>$config_txt,
+            'tables'=>$tables_txt,
+            'menu'=>$menu_txt,
+        ]);
+        mkdir($addonspath,0777,true);
+        file_put_contents($addonspath.'Install.php',$install);
+        //拷贝数据文件
+        $param['install']=1;
+        $model->save($param);
+    }
+
+    public function package(string $key)
+    {
+        $addon=Addons::where('key',$key)->find();
+        if(!Addons::checkKey($addon)){
+            //禁止修改、删除,否则后果自负
+            throw new \Exception('不是你的扩展,无法操作');
+        }
+        $this->includeInstall($addon);
+        $packfile=$this->getAddonsPack($addon['type'],$addon['pack'],$addon['version']);
+        if(is_file($packfile)){
+            throw new \Exception('已经打包过了');
+        }
+        $addonpath=$this->getAddonsPath($addon['type'],$addon['pack']);
+        if(!is_file($addonpath.'Install.php')){
+            throw new \Exception('安装文件不存在');
+        }
+        $install='\\addons\\'.$addon['type'].'\\'.$addon['pack'].'\\Install';
+        if(!$this->checkPackFiles($install::$files,$error)){
+            throw new \Exception($error);
+        }
+        //打包表格
+        if(!empty($install::$tables)){
+            $savesql=$addonpath.'install.sql';
+            $prefix=config('database.connections.mysql.prefix');
+            $sqltxt='';
+            foreach ($install::$tables as $table){
+                $nopretable=str_replace($prefix,'__PREFIX__',$table);
+                $createComment=PHP_EOL.'-- 创建表结构 `'.$nopretable.'`'.PHP_EOL;
+                $sql="SHOW CREATE TABLE `{$table}`";
+                $createResult=Db::query($sql)[0]['Create Table'].';';
+                $createResult=str_replace('CREATE TABLE','CREATE TABLE IF NOT EXISTS',$createResult);
+                $createResult=str_replace($prefix,'__PREFIX__',$createResult);
+                $sql="select * from `{$table}`";
+                $data=Db::query($sql);
+                $insertComment='';
+                $insertResult='';
+                if(count($data)>0){
+                    $insertComment=PHP_EOL.PHP_EOL.'-- 导入表数据 `'.$nopretable.'`'.PHP_EOL;
+                    $insertResult='';
+                    foreach ($data as $item){
+                        $insertResult.="INSERT INTO `{$nopretable}` VALUES (";
+                        foreach ($item as $value){
+                            if($value===null) {
+                                $insertResult .= 'null,';
+                            }else if(is_string($value)){
+                                $value=addslashes($value);
+                                $insertResult.="'{$value}',";
+                            }else{
+                                $insertResult.="'{$value}',";
+                            }
+                        }
+                        $insertResult=rtrim($insertResult,',');
+                        $insertResult.=");".PHP_EOL;
+                    }
+                }else{
+                    $createResult.=PHP_EOL;
+                }
+                $sqltxt.=$createComment.$createResult.$insertComment.$insertResult;
+            }
+            file_put_contents($savesql,$sqltxt);
+        }
+        //创建打包目录
+        $package=$addonpath.'package'.DS;
+        if(!is_dir($package)){
+            mkdir($package,0777,true);
+        }
+        //拷贝文件
+        foreach ($install::$files as $file){
+            $path=root_path().$file;
+            if(is_dir($path)){
+                $this->copy_dir($path,$package.$file,$install::$unpack);
+            }
+            if(is_file($path)){
+                $this->copy_file($path,$package.$file,$install::$unpack);
+            }
+        }
+        $zip=new \ZipArchive();
+        $zip->open($packfile,\ZipArchive::CREATE);
+        self::addFileToZip($package,$package,$zip);
+        //追加安装文件
+        $zip->addFile($addonpath.'Install.php','Install.php');
+        //追加数据库文件
+        if(is_file($addonpath.'install.sql')){
+            $zip->addFile($addonpath.'install.sql','install.sql');
+        }
+        $zip->close();
+        $addon->save();
+    }
+
+    public function getAddons(int $page,string $type,string $plain,int $limit,string $keywords='')
+    {
+        $data=[
+            'page'=>$page,
+            'type'=>$type,
+            'plain'=>$plain,
+            'limit'=>$limit
+        ];
+        if($keywords){
+            $data['keywords']=$keywords;
+        }
+        $response=Http::get($this->pluginsHost.'/addons/list',$data);
+        if($response->isSuccess()){
+            return $response->content;
+        }else{
+            return [
+                'total'=>0,
+                'rows'=>[]
+            ];
+        }
+    }
+
+    private function parseMenu(array $menu,string $pack,int $pid=0)
+    {
+        $time=time();
+        $rr=[];
+        foreach ($menu as $item){
+            $id=$this->menuid;
+            $arr=[];
+            $arr['id']=$id;
+            $arr['pid']=isset($item['pid'])?$item['pid']:$pid;
+            $arr['addons']=$pack;
+            $arr['controller']=$item['controller'];
+            $arr['action']=$item['action'];
+            $arr['title']=$item['title'];
+            $arr['icon']=$item['icon'];
+            $arr['weigh']=$item['weigh'];
+            $arr['ismenu']=$item['ismenu'];
+            $arr['extend']=$item['extend'];
+            if($arr['ismenu']){
+                $arr['status']='normal';
+                $arr['menutype']=$item['menutype'];
+            }else{
+                $arr['status']=null;
+                $arr['menutype']=null;
+            }
+            $arr['createtime']=$time;
+            $arr['updatetime']=$time;
+            $rr[]=$arr;
+            $this->menuid++;
+            if(isset($item['childlist'])){
+                $ir=$this->parseMenu($item['childlist'],$pack,$id);
+                $rr=array_merge($rr,$ir);
+            }
+        }
+       return $rr;
+    }
+
+    private function parseTable(string $addonspath)
+    {
+        $sqlpath=$addonspath.'install.sql';
+        if(!is_file($sqlpath)){
+            return [];
+        }
+        $prefix=config('database.connections.mysql.prefix');
+        $tables=[];
+        $fp=fopen($sqlpath,'r');
+        while(!feof($fp)) {
+            $line=fgets($fp);
+            if(!$line){
+                continue;
+            }
+            if(strpos($line,'CREATE TABLE')!==false){
+                $prefixtable=substr($line,strpos($line,'`')+1);
+                $prefixtable=substr($prefixtable,0,strpos($prefixtable,'`'));
+                $table=str_replace('__PREFIX__',$prefix,$prefixtable);
+                $tables[$prefixtable]=$table;
+            }
+        }
+        fclose($fp);
+        return $tables;
+    }
+
+    private function addFileToZip(string $root,string $folder,\ZipArchive $zip){
+        $handler=opendir($folder);
+        while (($filename=readdir($handler))!==false){
+            if($filename!='.' && $filename!='..'){
+                if(is_dir($folder.$filename)){
+                    $this->addFileToZip($root,$folder.$filename.DS,$zip);
+                }else{
+                    $writefile=substr($folder,strlen($root));
+                    $zip->addFile($folder.$filename,$writefile.$filename);
+                }
+            }
+        }
+        @closedir($handler);
+    }
+
+    private function includeInstall(Addons $addon)
+    {
+        $install=root_path().'addons'.DS.$addon['type'].DS.$addon['pack'].DS.'Install.php';
+        include $install;
+    }
+
+    private function checkInstallFiles(array $files,&$error)
+    {
+        usort($files,function ($x,$y){
+            return strlen($x)-strlen($y);
+        });
+        foreach ($files as $k1=>$file)
+        {
+            foreach ($files as $k2=>$check){
+                if($k1!=$k2 && $file==$check){
+                    $error='检测到安装目录或文件【'.$file.'】重复';
+                    return false;
+                }
+                if($k1!=$k2 && str_starts_with($file, $check)){
+                    $error='检测到安装目录或文件【'.$file.'】包含于【'.$check.'】中';
+                    return false;
+                }
+            }
+            $path=root_path().$file;
+            if(is_file($path) || is_dir($path)){
+                $error='检测到安装目录或文件【'.$file.'】已经存在';
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private function checkCreateFiles(array $files,&$error)
+    {
+        usort($files,function ($x,$y){
+            return strlen($x)-strlen($y);
+        });
+        foreach ($files as $k1=>$file)
+        {
+            foreach ($files as $k2=>$check){
+                if($k1!=$k2 && $file==$check){
+                    $error='检测到要创建目录或文件【'.$file.'】重复';
+                    return false;
+                }
+                if($k1!=$k2 && str_starts_with($file, $check)){
+                    $error='检测到要创建目录或文件【'.$file.'】包含于【'.$check.'】中';
+                    return false;
+                }
+            }
+            $path=root_path().$file;
+            if(!is_file($path) && !is_dir($path)){
+                $error='检测到要创建目录或文件【'.$file.'】不存在';
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    private function checkPackFiles(array $files,&$error)
+    {
+        usort($files,function ($x,$y){
+            return strlen($x)-strlen($y);
+        });
+        foreach ($files as $k1=>$file)
+        {
+            foreach ($files as $k2=>$check){
+                if($k1!=$k2 && $file==$check){
+                    $error='检测到打包目录或文件【'.$file.'】重复';
+                    return false;
+                }
+                if($k1!=$k2 && str_starts_with($file, $check)){
+                    $error='检测到打包目录或文件【'.$file.'】包含于【'.$check.'】中';
+                    return false;
+                }
+            }
+            $path=root_path().$file;
+            if(!is_file($path) && !is_dir($path)){
+                $error='检测到打包目录或文件【'.$file.'】不存在';
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private function copy_file(string $from,string $to,array $filters=[])
+    {
+        if(!is_file($from)){
+            return;
+        }
+        //判断文件扩展名是否在过滤列表中
+        $ext='*.'.strtolower(substr($from,strrpos($from,'.')+1));
+        if(in_array($ext,$filters)){
+            return;
+        }
+        //获取文件$to所在的目录
+        $folder=substr($to,0,strrpos($to,DS));
+        if(!is_dir($folder)){
+            mkdir($folder, 0777, true);
+        }
+        copy($from,$to);
+    }
+
+    private function copy_dir(string $from, string $to,array $filters=[])
+    {
+        if(!is_dir($from)){
+            return;
+        }
+        if(!is_dir($to)){
+            mkdir($to,0777,true);
+        }
+        $handle= dir($from);
+        while($entry = $handle->read()) {
+            if(($entry != ".") && ($entry != "..")){
+                if(in_array($entry,$filters)){
+                    continue;
+                }
+                if(is_dir($from."/".$entry)){
+                    $this->copy_dir($from."/".$entry,$to."/".$entry,$filters);
+                }
+                if(is_file($from."/".$entry)){
+                    //判断文件扩展名是否在过滤列表中
+                    $ext='*.'.strtolower(substr($entry,strrpos($entry,'.')+1));
+                    if(in_array($ext,$filters)){
+                        continue;
+                    }
+                    copy($from."/".$entry,$to."/".$entry);
+                }
+            }
+        }
+    }
+
+    private function getContent(string $file,array $replace=[]):string
+    {
+        $filepath=__DIR__.DS.$file.'.txt';
+        $myfile = fopen($filepath, "r");
+        $content='';
+        $if=[];
+        $lastline='';
+        while(!feof($myfile)) {
+            $line_str = fgets($myfile);
+            if($line_str){
+                $ix=false;
+                if($lastline==='' && trim($line_str)===''){
+                    $ix=true;
+                }
+                //条件判断
+                if(strpos($line_str,'<#if')!==false){
+                    $continue=false;
+                    $if_str=substr($line_str,strpos($line_str,'<#if')+4);
+                    $if_str=substr($if_str,0,strpos($if_str,'#>'));
+                    $keys=array_keys($replace);
+                    $values=array_map(function ($res){
+                        return '$replace[\''.$res.'\']';
+                    },$keys);
+                    $if_str=str_replace(array_keys($replace),$values,$if_str);
+                    $phpstr="if(!({$if_str})){\$continue=true;}";
+                    eval($phpstr);
+                    $if[]=$continue;
+                    $ix=true;
+                }
+                if(strpos($line_str,'<#endif#>')!==false){
+                    array_pop($if);
+                    $ix=true;
+                }
+                foreach ($if as $value) {
+                    if ($value) {
+                        $ix = true;
+                        break;
+                    }
+                }
+                if($ix){
+                    continue;
+                }
+                //替换内容
+                foreach ($replace as $key=>$value){
+                    if(is_string($value)){
+                        $line_str=str_replace('<#'.$key.'#>',$value,$line_str);
+                    }
+                }
+                $content.=$line_str;
+                $lastline=trim($line_str);
+            }
+        }
+        fclose($myfile);
+        return $content;
+    }
+}

+ 170 - 0
app/admin/service/addons/eof.php

@@ -0,0 +1,170 @@
+<?php
+
+function getFilesTxt($files){
+    $str = '';
+    foreach($files as $file){
+        if(!$file){
+            continue;
+        }
+        $str.=<<<EOF
+        "{$file}",
+
+EOF;
+    }
+    return $str;
+}
+
+function getUnpackTxt($unpack)
+{
+    $str = '';
+    foreach($unpack as $file){
+        if(!$file){
+            continue;
+        }
+        $str.=<<<EOF
+        "{$file}",
+
+EOF;
+    }
+    return $str;
+}
+
+function getRequireTxt($require)
+{
+    $str = '';
+    foreach($require as $re){
+        if(!$re){
+            continue;
+        }
+        if(!class_exists($re)){
+            throw new \Exception("{$re}类不存在");
+        }
+        $str.=<<<EOF
+        {$re}::class,
+
+EOF;
+    }
+    return $str;
+}
+
+function getAddonsTxt($addons)
+{
+    $str = '';
+    foreach($addons as $re){
+        if(!$re){
+            continue;
+        }
+        if(!addons_installed($re)){
+            $ad=get_addons($re);
+            throw new \Exception("扩展{$ad->name}不存在");
+        }
+        $ad=get_addons($re);
+        $str.=<<<EOF
+        "{$re}"=>"{$ad->name}",
+
+EOF;
+    }
+    return $str;
+}
+
+function getMenuTxt($menulist)
+{
+    if(count($menulist)==0){
+        return '';
+    }
+    $str='';
+    foreach ($menulist as $menu){
+        $arr=parseMenu($menu);
+        $txt=getArrayTxt($arr);
+        $str.=<<<EOF
+        {$txt}
+
+EOF;
+    }
+    return $str;
+}
+
+function getTableTxt($table)
+{
+    if(empty($table)){
+        return '';
+    }
+    $str='';
+    foreach ($table as $te){
+        $str.=<<<EOF
+        "{$te}",
+
+EOF;
+    }
+    return $str;
+}
+
+function parseMenu($menu){
+    $arr=[
+        'id'=>$menu['id'],
+        'controller'=>$menu['controller'],
+        'action'=>$menu['action'],
+        'title'=>$menu['title'],
+        'icon'=>$menu['icon'],
+        'ismenu'=>$menu['ismenu'],
+        'menutype'=>$menu['menutype'],
+        'extend'=>$menu['extend'],
+        'weigh'=>$menu['weigh'],
+    ];
+    if(count($menu['childlist'])>0){
+        foreach ($menu['childlist'] as $key=>$value){
+            $menu['childlist'][$key]=parseMenu($value);
+        }
+        $arr['childlist']=$menu['childlist'];
+    }
+    return $arr;
+}
+
+function getConfigTxt($config)
+{
+    $str = '';
+    foreach($config as $fig){
+        $arr=[
+            'id'=>$fig['id'],
+            'name'=>$fig['name'],
+            'title'=>$fig['title'],
+            'type'=>$fig['type'],
+            'tip'=>$fig['tip'],
+            'rules'=>$fig['rules'],
+            'extend'=>$fig['extend']
+        ];
+        $txt=getArrayTxt($arr);
+        //去掉末尾的逗号
+        $str.=<<<EOF
+        {$txt}
+
+EOF;
+    }
+    return $str;
+}
+
+function getArrayTxt($arr)
+{
+    $str = '[';
+    foreach($arr as $key=>$value){
+       if(is_array($value)){
+           if(is_numeric($key)){
+               $str.=getArrayTxt($value);
+           }else{
+               $str.='\''.$key.'\'=>'.getArrayTxt($value);
+           }
+       }else{
+           if(is_numeric($value)){
+               $str.=<<<EOF
+'{$key}'=>{$value},
+EOF;
+           }else{
+               $str.=<<<EOF
+'{$key}'=>'{$value}',
+EOF;
+           }
+       }
+    }
+    $str=substr($str,0,strlen($str)-1);
+    return $str.'],';
+}

+ 47 - 0
app/admin/service/addons/install.txt

@@ -0,0 +1,47 @@
+<?php
+declare(strict_types=1);
+namespace addons\<#type#>\<#pack#>;
+
+class Install{
+
+    public static $files=[
+<#files#>
+    ];
+
+    public static $unpack=[
+<#unpack#>
+    ];
+
+    public static $menu=[
+<#menu#>
+    ];
+
+    public static $require=[
+<#require#>
+    ];
+
+    public static $tables=[
+<#tables#>
+    ];
+
+    public static $addons=[
+<#addons#>
+    ];
+
+    public static $config=[
+<#config#>
+    ];
+
+    //安装扩展时的回调方法
+    public static function install()
+    {
+
+    }
+
+    //卸载扩展时的回调方法
+    public static function uninstall()
+    {
+
+    }
+
+}

+ 475 - 0
app/admin/service/curd/CurdService.php

@@ -0,0 +1,475 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types=1);
+
+namespace app\admin\service\curd;
+
+use app\common\service\BaseService;
+use app\common\model\AuthRule;
+use think\facade\Cache;
+use think\facade\Db;
+
+include __DIR__.DS.'eof.php';
+
+class CurdService extends BaseService
+{
+    public static $prefix;
+    private $table;
+    private $controller;
+    private $model;
+    private $reduced;
+    private $actions;
+    private $actionList;
+    private $fields;
+    private $summary;
+    private $expand;
+    private $isTree;
+    private $treeTitle;
+    private $pagination;
+    private $tabs;
+    private $recyclebinField=[];
+    private $relations;
+    private $type;
+
+    public function build()
+    {
+        $error='';
+        Db::startTrans();
+        try{
+            $model=$this->createModel();
+            $controller=$this->createController();
+            $view=$this->createView();
+            Db::commit();
+        }catch (\Exception $e){
+            $error=$e->getMessage();
+            $this->rollback();
+        }
+        if($error){
+            throw new \Exception($error);
+        }
+        $r=compact('model','controller','view');
+        return $r;
+    }
+    public function volidate()
+    {
+        if($this->type=='file'){
+            $filelist=$this->buildFile();
+            foreach ($filelist as $key=>$file){
+                if($key=='view'){
+                    foreach ($file as $f){
+                        if(file_exists($f)){
+                            throw new \Exception(__('%s文件已存在',['s'=>'view']));
+                        }
+                    }
+                }else if(file_exists($file)){
+                    throw new \Exception(__('%s文件已存在',['s'=>$key]));
+                }
+            }
+        }
+        if($this->isTree && !$this->treeTitle){
+            throw new \Exception(__('树形结构标题未定义'));
+        }
+        if(!key_exists('index',$this->actionList)){
+            throw new \Exception(__('%s方法未定义',['s'=>'index']));
+        }
+    }
+
+    public function clear()
+    {
+        $filelist=$this->buildFile();
+        foreach ($filelist as $key=>$file){
+            if($key=='view'){
+                foreach ($file as $f){
+                    if(file_exists($f)){
+                        $time=filectime($f);
+                        if(time()-$time>3600){
+                            throw new \Exception(__('%s已创建超过一个小时,禁止删除',['s'=>'view']));
+                        }
+                        unlink($f);
+                    }
+                }
+            }else if(file_exists($file)){
+                $time=filectime($file);
+                if(time()-$time>3600){
+                    throw new \Exception(__('%s已创建超过一个小时,禁止删除',['s'=>$key]));
+                }
+                unlink($file);
+            }
+        }
+    }
+
+    protected function init()
+    {
+        $config = Db::getConfig();
+        $default=$config['default'];
+        self::$prefix=$config['connections'][$default]['prefix'];
+        $recyclebinField=[];
+        $relations=[];
+        foreach ($this->fields as $key=>$value){
+            $title=trim($value['title']);
+            $this->fields[$key]['title']=$title?:$value['field'];
+            if($this->actions['table'] && $value['visible']==='relation'){
+                if(!$value['relation']){
+                    throw new \Exception(__('%s字段关联关系未定义',['s'=>$value['title']]));
+                }
+                $relation=json_decode($value['relation'],true);
+                if($value['field']==$relation['table']){
+                    throw new \Exception(__('字段名与关联表名重名,禁止关联,建议修改字段名为%s',['s'=>$relation['table'].'_id']));
+                }
+                $relations[$value['field']]=$relation;
+            }
+            if(!empty($value['recyclebin'])){
+                $type=$value['formatter'];
+                if(!in_array($type,['text','image','images','date','datetime','tag','tags'])){
+                    $type='text';
+                }
+                $recyclebinField[]=[
+                    'field'=>$value['field'],
+                    'type'=>$type,
+                    'title'=>$value['title']
+                ];
+            }
+        }
+        if(key_exists('recyclebin',$this->actionList) && empty($recyclebinField)){
+            throw new \Exception(__('回收站的显示字段未定义'));
+        }
+        $this->relations=$relations;
+        $this->recyclebinField=$recyclebinField;
+    }
+
+    public function getIndexUrl()
+    {
+        $pack=str_replace('\\','/',substr($this->controller,strpos($this->controller,'controller\\')+11));
+        $pack=strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $pack));
+        return request()->domain().build_url($pack.'/index','admin');
+    }
+
+    private function rollback()
+    {
+        Db::rollback();
+        if($this->type=='file'){
+            $filelist=$this->buildFile();
+            foreach ($filelist as $key=>$file){
+                if($key=='view'){
+                    foreach ($file as $f){
+                        if(file_exists($f)){
+                            unlink($f);
+                        }
+                    }
+                }else if(file_exists($file)){
+                    unlink($file);
+                }
+            }
+        }
+    }
+
+    private function buildFile(mixed $key=false)
+    {
+        $rootPath = root_path();
+        $controller=$rootPath.str_replace('\\',DS,$this->controller).'.php';
+        $model=$rootPath.str_replace('\\',DS,$this->model).'.php';
+        $temp=strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $this->controller));
+        $view=[
+            'index'=>$rootPath.str_replace(['\\','controller'],[DS,'view'],$temp).DS.'index.html',
+        ];
+        if(key_exists('add',$this->actionList)){
+            $view['add']=$rootPath.str_replace(['\\','controller'],[DS,'view'],$temp).DS.'add.html';
+        }
+        if(key_exists('edit',$this->actionList)){
+            $view['edit']=$rootPath.str_replace(['\\','controller'],[DS,'view'],$temp).DS.'edit.html';
+        }
+        $js=$rootPath.'public'.DS.'assets'.DS.'js'.DS.str_replace(['app\\','controller\\','\\'],['','',DS],$temp).'.js';
+        $arr=compact('controller','model','view','js');
+        if($key){
+            return $arr[$key];
+        }
+        return $arr;
+    }
+
+    private function createView()
+    {
+        $keys=array_keys($this->actionList);
+        $reduced=$this->reduced;
+        $table=$this->actions['table'];
+        $form=$this->actions['form'];
+        $isTree=$this->isTree;
+        $treeTitle=$this->treeTitle;
+        $viewContent=[];
+        foreach ($keys as $key){
+            if(in_array($key,['del','multi','import','download','recyclebin'])){
+               continue;
+            }
+            $action=$key;
+            $title=$this->actionList[$key];
+            $search=[];
+            $commonSearch=false;
+            $pagination=$this->pagination;
+            $tabs=$this->tabs;
+            $toolbar=['refresh'];
+            if($key=='index'){
+                foreach ($this->actionList as $xkey=>$value){
+                    if(in_array($xkey,['add','edit','del'])){
+                        $toolbar[]=$xkey;
+                    }
+                }
+                $slot='';
+                $weigh=0;
+                foreach ($this->fields as $value){
+                    if(!empty($value['search'])){
+                        $search[]=$value['field'];
+                    }
+                    if(trim($value['operate'])){
+                        $commonSearch=true;
+                    }
+                    if($value['field']=='weigh' && $value['type']=='int'){
+                        $weigh=1;
+                    }
+                    if($value['field']=='status' && $value['type']=='varchar'){
+                        $toolbar[]='more';
+                    }
+                    $slot.=getTableslot($value);
+                }
+                $slot=rtrim($slot);
+                foreach ($this->actionList as $fkey=>$value){
+                    if(in_array($fkey,['import','download','recyclebin'])){
+                        $toolbar[]=$fkey;
+                    }
+                }
+                $search=empty($search)?'':implode(',',$search);
+                $toolbarStr=implode(',',$toolbar);
+                $controller=str_replace('\\','\\\\',$this->controller);
+                $summary=$this->summary;
+                $expand=$this->expand;
+                $js=$this->getJsContent('js-index');
+                $replace=compact('title','reduced','table','slot','expand','weigh','search','summary','controller','toolbar','toolbarStr','commonSearch','pagination','tabs','isTree','js');
+                $content=$this->getContent('view-index',$replace);
+            }else if($key=='add'){
+                $slot='';
+                foreach ($this->fields as $value){
+                    $slot.=getFormslot($value,$isTree,$treeTitle);
+                }
+                $slot=rtrim($slot);
+                $js=$this->getJsContent('js-add');
+                $replace=compact('title','reduced','slot','form','js');
+                $content=$this->getContent('view-add',$replace);
+            }else if($key=='edit'){
+                $temp='';
+                if($key=='edit'){
+                    $temp=str_replace('\\','/',substr($this->controller,strpos($this->controller,'controller\\')+11));
+                    $temp=strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $temp)).'/add';
+                }
+                $js=$this->getJsContent('js');
+                $replace=compact('title','reduced','form','temp','js');
+                $content=$this->getContent('view-edit',$replace);
+            }else{
+                $js=$this->getJsContent('js');
+                $replace=compact('title','js');
+                $content=$this->getContent('view-method',$replace);
+            }
+            if($this->type=='file'){
+                $file=$this->buildFile('view')[$key];
+                create_file($file,$content);
+            }
+            $viewContent[$key]=$content;
+        }
+        return $viewContent;
+    }
+
+    private function getJsContent(string $jsfile)
+    {
+        $reduced=$this->reduced;
+        $table=$this->actions['table'];
+        $form=$this->actions['form'];
+        $expand=$this->expand;
+        $actions=array_keys($this->actionList);
+        $pack=str_replace('\\','/',substr($this->controller,strpos($this->controller,'controller\\')+11));
+        $pack=strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $pack));
+        $sort='';
+        $fields='';
+        $isTree=$this->isTree;
+        $treeTitle=$this->treeTitle;
+        foreach ($this->fields as $value){
+            $istable=($jsfile=='js-index');
+            $isform=($jsfile=='js-add');
+            $fields.=getFields($value,$istable,$isform,$isTree,$treeTitle);
+            if($value['field']=='weigh' && $value['type']=='int'){
+                $sort=true;
+            }
+        }
+        $fields=rtrim($fields);
+        $isTree=$this->isTree;
+        $replace=compact('pack','reduced','table','form','actions','expand','sort','isTree','fields');
+        $content=$this->getContent($jsfile,$replace);
+        return $content;
+    }
+
+    private function createController()
+    {
+        $reduced=$this->reduced;
+        $controllerName=substr($this->controller,strrpos($this->controller,'\\')+1);
+        $namespace=substr($this->controller,0,strrpos($this->controller,'\\'));
+        $model=$this->model;
+        $modelName=substr($this->model,strrpos($this->model,'\\')+1);
+        $recyclebinField=rtrim(getRecyclebinField($this->recyclebinField));
+        $recyclebinType=rtrim(getRecyclebinType($this->recyclebinField));
+        $table=$this->actions['table'];
+        $form=$this->actions['form'];
+        $group=str_replace('\\','/',substr($this->controller,strpos($this->controller,'controller\\')+11));
+        $group=strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $group));
+        $methods='';
+        foreach ($this->actionList as $key=>$value){
+            if($table && in_array($key,['index','del','multi','import','download','recyclebin'])){
+                continue;
+            }
+            if($form && in_array($key,['add','edit'])){
+                continue;
+            }
+            $methods.=getAction($key,$value);
+        }
+        $methods=rtrim($methods);
+        $relation=$this->getRelation();
+        $actionList=$this->actionList;
+        $isTree=$this->isTree;
+        $summary=$this->summary;
+        $treeTitle=$this->treeTitle;
+        $replace=compact(
+            'namespace','controllerName','model','modelName',
+            'group','table','summary','form', 'relation',
+            'methods','actionList','isTree','treeTitle',
+            'recyclebinField','recyclebinType'
+        );
+        if($reduced){
+            $content=$this->getContent('controller-reduced',$replace);
+        }else{
+            $content=$this->getContent('controller-normal',$replace);
+        }
+        if($this->type=='file'){
+            $file=$this->buildFile('controller');
+            create_file($file,$content);
+        }
+        return $content;
+    }
+
+    private function getRelation()
+    {
+        if(!$this->actions['table']){
+            return '';
+        }
+        $relation=[];
+        foreach ($this->relations as $value){
+            $relation[]=str_replace(self::$prefix,'',$value['table']);
+        }
+        $relation=count($relation)>0?json_encode($relation):'';
+        return $relation;
+    }
+
+    private function createModel()
+    {
+        //用户自定义模型名
+        $modelName=substr($this->model,strrpos($this->model,'\\')+1);
+        $namespace=substr($this->model,0,strrpos($this->model,'\\'));
+        //根据表名生成模型名
+        $table=str_replace(self::$prefix,'',$this->table);
+        $tableModelName=str_replace(' ','',ucwords(str_replace('_',' ',$table)));
+        $name='';
+        if($tableModelName!=$modelName) {
+            $name = $table;
+        }
+        $createtime=false;
+        $updatetime=false;
+        $deletetime=false;
+        $weigh=false;
+        foreach ($this->fields as $value){
+            if($value['field']=='createtime'){
+                $createtime=true;
+            }
+            if($value['field']=='updatetime'){
+                $updatetime=true;
+            }
+            if($value['field']=='deletetime'){
+                $deletetime=true;
+            }
+            if($value['field']=='weigh'){
+                $weigh=true;
+            }
+        }
+        $methods='';
+        foreach($this->relations as $field=>$value){
+            $methods.=getRelationMethods($field,$value);
+        }
+        $methods=rtrim($methods);
+        if($createtime && $updatetime && $deletetime) {
+            $content=$this->getContent('model-extend-base',compact('namespace','modelName','name','weigh','methods'));
+        }else{
+            $content=$this->getContent('model-normal',compact('namespace','modelName','name','createtime','updatetime','deletetime','weigh','methods'));
+        }
+        if($this->type=='file'){
+            $file=$this->buildFile('model');
+            create_file($file,$content);
+        }
+        return $content;
+    }
+
+    private function getContent(string $file,array $replace=[]):string
+    {
+        $filepath=__DIR__.DS.$file.'.txt';
+        $myfile = fopen($filepath, "r");
+        $content='';
+        $if=[];
+        $lastline='';
+        while(!feof($myfile)) {
+            $line_str = fgets($myfile);
+            if($line_str){
+                $ix=false;
+                if($lastline==='' && trim($line_str)===''){
+                    $ix=true;
+                }
+                //条件判断
+                if(strpos($line_str,'<#if')!==false){
+                    $continue=false;
+                    $if_str=substr($line_str,strpos($line_str,'<#if')+4);
+                    $if_str=substr($if_str,0,strpos($if_str,'#>'));
+                    $keys=array_keys($replace);
+                    $values=array_map(function ($res){
+                        return '$replace[\''.$res.'\']';
+                    },$keys);
+                    $if_str=str_replace(array_keys($replace),$values,$if_str);
+                    $phpstr="if(!({$if_str})){\$continue=true;}";
+                    eval($phpstr);
+                    $if[]=$continue;
+                    $ix=true;
+                }
+                if(strpos($line_str,'<#endif#>')!==false){
+                    array_pop($if);
+                    $ix=true;
+                }
+                foreach ($if as $value) {
+                    if ($value) {
+                        $ix = true;
+                        break;
+                    }
+                }
+                if($ix){
+                    continue;
+                }
+                //替换内容
+                foreach ($replace as $key=>$value){
+                    if(is_string($value)){
+                        $line_str=str_replace('<#'.$key.'#>',$value,$line_str);
+                    }
+                }
+                $content.=$line_str;
+                $lastline=trim($line_str);
+            }
+        }
+        fclose($myfile);
+        return $content;
+    }
+}

+ 212 - 0
app/admin/service/curd/controller-normal.txt

@@ -0,0 +1,212 @@
+<?php
+declare (strict_types = 1);
+
+namespace <#namespace#>;
+
+use app\common\controller\Backend;
+<#if table || form#>
+use app\admin\traits\Actions;
+<#endif#>
+use think\annotation\route\Group;
+use think\annotation\route\Route;
+use <#model#> as <#modelName#>Model;
+<#if isTree#>
+use app\common\library\Tree;
+<#endif#>
+
+#[Group("<#group#>")]
+class <#controllerName#> extends Backend
+{
+    <#if table || form#>
+    use Actions{
+        <#if table && isset(actionList['index'])#>
+        index as private _index;
+        <#endif#>
+        <#if form && isset(actionList['add'])#>
+        add as private _add;
+        <#endif#>
+        <#if form && isset(actionList['edit'])#>
+        edit as private _edit;
+        <#endif#>
+        <#if isset(actionList['del'])#>
+        del as private _del;
+        <#endif#>
+        <#if isset(actionList['multi'])#>
+        multi as private _multi;
+        <#endif#>
+        <#if isset(actionList['import'])#>
+        import as private _import;
+        <#endif#>
+        <#if isset(actionList['download'])#>
+        download as private _download;
+        <#endif#>
+        <#if isset(actionList['recyclebin'])#>
+        recyclebin as private _recyclebin;
+        <#endif#>
+    }
+    <#endif#>
+
+    protected function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new <#modelName#>Model();
+    }
+
+    <#if table && isset(actionList['index'])#>
+    //查看
+    #[Route("GET,JSON","index")]
+    public function index()
+    {
+        <#if relation#>
+        $this->relationField=<#relation#>;
+        <#endif#>
+        <#if isTree || summary#>
+        if (false === $this->request->isAjax()) {
+            return $this->fetch();
+        }
+        if($this->request->post('selectpage')){
+            return $this->selectpage();
+        }
+        [$where, $order, $limit, $with] = $this->buildparams();
+        $list = $this->model
+            ->withJoin($with,'left')
+            //如果没有使用operate filter过滤的情况下,推荐使用with关联,可以提高查询效率
+            //->with($with)
+            ->where($where)
+            ->order($order)
+            ->paginate($limit);
+        $result = [
+            <#if summary#>
+            'summary' => '自定义统计信息',
+            <#endif#>
+            'total' => $list->total(),
+            <#if isTree#>
+            'rows' => $this->toTree($list->items())
+            <#endif#>
+            <#if !isTree#>
+            'rows' => $list->items()
+            <#endif#>
+        ];
+        return json($result);
+        <#endif#>
+        <#if !isTree && !summary#>
+        return $this->_index();
+        <#endif#>
+    }
+    <#endif#>
+
+    <#if isTree#>
+    private function toTree($list)
+    {
+        $tree = Tree::instance();
+        $tree->init($list, 'pid');
+        return $tree->getTreeList($tree->getTreeArray(0), '<#treeTitle#>');
+    }
+    <#endif#>
+
+    <#if form && isset(actionList['add'])#>
+    //添加
+    #[Route("GET,POST","add")]
+    public function add()
+    {
+        //通过定义postParams来增加或覆盖post提交的表单
+        $this->postParams=[];
+        //通过定义callback回调函数来执行添加后的操作
+        $this->callback=function ($model){};
+        <#if isTree#>
+        if(!$this->request->isPost()){
+            $list=$this->model->select()->toArray();
+            $parentList=$this->toTree($list);
+            $this->assign('parentList',$parentList);
+        }
+        <#endif#>
+        return $this->_add();
+    }
+    <#endif#>
+
+    <#if form && isset(actionList['edit'])#>
+    //修改
+    #[Route("GET,POST","edit")]
+    public function edit()
+    {
+        //通过定义postParams来增加或覆盖post提交的表单
+        $this->postParams=[];
+        //通过定义callback回调函数来执行修改后的操作
+        $this->callback=function ($model){};
+        <#if isTree#>
+        if(!$this->request->isPost()){
+            $list=$this->model->select()->toArray();
+            $parentList=$this->toTree($list);
+            $this->assign('parentList',$parentList);
+        }
+        <#endif#>
+        return $this->_edit();
+    }
+    <#endif#>
+
+    <#if table && isset(actionList['del'])#>
+    //删除
+    #[Route("GET,POST","del")]
+    public function del()
+    {
+        //通过定义callback回调函数来执行删除后的操作
+        $this->callback=function ($ids){};
+        return $this->_del();
+    }
+    <#endif#>
+
+    <#if table && isset(actionList['multi'])#>
+    //更新
+    #[Route("GET,POST","multi")]
+    public function multi()
+    {
+        //通过定义callback回调函数来执行更新后的操作
+        $this->callback=function ($ids,$field,$value){};
+        return $this->_multi();
+    }
+    <#endif#>
+
+    <#if table && isset(actionList['import'])#>
+    //导入
+    #[Route("GET,POST","import")]
+    public function import()
+    {
+        //通过定义callback回调函数来处理导入的数据
+        $this->callback=function ($inserData){
+            return $inserData;
+        };
+        return $this->_import();
+    }
+    <#endif#>
+
+    <#if table && isset(actionList['recyclebin'])#>
+    //回收站
+    #[Route("GET,POST,JSON","recyclebin")]
+    public function recyclebin($action)
+    {
+        $this->recyclebinColumns=[
+        <#recyclebinField#>
+        ];
+        $this->recyclebinColumnsType=[
+        <#recyclebinType#>
+        ];
+        return $this->_recyclebin($action);
+    }
+    <#endif#>
+
+    <#if table && isset(actionList['download'])#>
+    //下载
+    #[Route("GET,POST","download")]
+    public function download()
+    {
+        //通过定义callback回调函数来处理下载的数据
+        $this->callback=function ($downloadData){
+            return $downloadData;
+        };
+        return $this->_download();
+    }
+    <#endif#>
+    <#if methods#>
+<#methods#>
+    <#endif#>
+}

+ 106 - 0
app/admin/service/curd/controller-reduced.txt

@@ -0,0 +1,106 @@
+<?php
+declare (strict_types = 1);
+
+namespace <#namespace#>;
+
+use app\common\controller\Backend;
+use think\annotation\route\Group;
+<#if (!table && !form) || methods || relation || isTree || summary#>
+use think\annotation\route\Route;
+<#endif#>
+<#if table || form#>
+use app\admin\traits\Actions;
+<#endif#>
+use <#model#> as <#modelName#>Model;
+<#if isTree#>
+use app\common\library\Tree;
+<#endif#>
+
+#[Group("<#group#>")]
+class <#controllerName#> extends Backend
+{
+    <#if table || form#>
+    <#if relation && !isTree && !summary#>
+    use Actions{
+        index as private _index;
+    }
+    <#endif#>
+    <#if !(relation && !isTree && !summary)#>
+    use Actions;
+    <#endif#>
+    <#endif#>
+
+    protected function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new <#modelName#>Model();
+        <#if isTree#>
+        if(!$this->request->isPost()){
+            $list=$this->model->select()->toArray();
+            $parentList=$this->toTree($list);
+            $this->assign('parentList',$parentList);
+        }
+        <#endif#>
+        <#if table && isset(actionList['recyclebin'])#>
+        $this->recyclebinColumns=[
+        <#recyclebinField#>
+        ];
+        $this->recyclebinColumnsType=[
+        <#recyclebinType#>
+        ];
+        <#endif#>
+    }
+
+    <#if relation || isTree || summary#>
+    #[Route("GET,JSON","index")]
+    public function index()
+    {
+        <#if relation#>
+        $this->relationField=<#relation#>;
+        <#endif#>
+        <#if !isTree && !summary#>
+        return $this->_index();
+        <#endif#>
+        <#if isTree || summary#>
+        if (false === $this->request->isAjax()) {
+            return $this->fetch();
+        }
+        if($this->request->post('selectpage')){
+            return $this->selectpage();
+        }
+        [$where, $order, $limit, $with] = $this->buildparams();
+        $list = $this->model
+            ->withJoin($with,'left')
+            ->where($where)
+            ->order($order)
+            ->paginate($limit);
+        $result = [
+            <#if summary#>
+            'summary' => '自定义统计信息',
+            <#endif#>
+            'total' => $list->total(),
+            <#if isTree#>
+            'rows' => $this->toTree($list->items())
+            <#endif#>
+            <#if !isTree#>
+            'rows' => $list->items()
+            <#endif#>
+        ];
+        return json($result);
+        <#endif#>
+    }
+    <#endif#>
+
+    <#if isTree#>
+    private function toTree($list)
+    {
+        $tree = Tree::instance();
+        $tree->init($list, 'pid');
+        return $tree->getTreeList($tree->getTreeArray(0), '<#treeTitle#>');
+    }
+    <#endif#>
+
+    <#if methods#>
+<#methods#>
+    <#endif#>
+}

+ 281 - 0
app/admin/service/curd/eof.php

@@ -0,0 +1,281 @@
+<?php
+
+use think\facade\Db;
+use app\admin\service\curd\CurdService;
+
+function getAction($key, $value)
+{
+    if($key=='del' || $key=='multi' || $key=='import' || $key=='download' || $key=='recyclebin'){
+        $return='return "'.$value.'";';
+    }else{
+        $return='return $this->fetch();';
+    }
+    $action=<<<EOF
+
+    //{$value}
+    #[Route("GET","{$key}")]
+    public function {$key}()
+    {
+        {$return}
+    }
+    
+EOF;
+    return $action;
+}
+
+function getFields($rows,$table,$form,$isTree,$treeTitle)
+{
+    foreach ($rows as &$value){
+        $value=is_string($value)?trim($value):$value;
+    }
+    $arr=[
+        'field'=>$rows['field'],
+        'title'=>$rows['title']
+    ];
+    if($table){
+        if($rows['visible']==='none'){
+            return '';
+        }
+        if($rows['visible']===false){
+            $arr['visible']=false;
+        }
+        if(!empty($rows['sortable'])){
+            $arr['sortable']=true;
+        }
+        if($rows['operate']){
+            $operate=parseJson($rows['operate']);
+            if($operate!='='){
+                $arr['operate']=$operate;
+            }
+            if(is_array($operate) && isset($operate['value'])){
+                if(str_starts_with($operate['value'],'[') && str_ends_with($operate['value'],']')){
+                    $arr['operate']['value']=json_decode($operate['value'],true);
+                }
+                //通过正则表达式判断$operate['value']是否为整数
+                if(preg_match('/^\d+$/',$operate['value'])){
+                    $arr['operate']['value']=intval($operate['value']);
+                }
+            }
+        }else{
+            $arr['operate']=false;
+        }
+    }
+    if($form){
+        if($rows['visible']==='none'){
+            return '';
+        }
+        if($rows['edit']){
+            $arr['edit']=parseJson($rows['edit']);
+            if(is_array($arr['edit']) && isset($arr['edit']['value'])){
+                if(str_starts_with($arr['edit']['value'],'[') && str_ends_with($arr['edit']['value'],']')){
+                    $arr['edit']['value']=json_decode($arr['edit']['value'],true);
+                }
+                //通过正则表达式判断$operate['value']是否为整数
+                if(is_string($arr['edit']['value']) && preg_match('/^\d+$/',$arr['edit']['value'])){
+                    $arr['edit']['value']=intval($arr['edit']['value']);
+                }
+            }
+        }
+        if($rows['rules']){
+            $arr['rules']=parseJson($rows['rules']);
+        }
+    }
+    if($rows['searchList']){
+        $arr['searchList']=parseJson($rows['searchList']);
+    }
+    $json=json_encode($arr,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
+    if($table){
+        $json=substr($json,0,-1).getFormatter($rows,$isTree,$treeTitle).'},';
+    }
+    if($form){
+        $json.=',';
+    }
+    $str=<<<EOF
+            {$json}
+
+EOF;
+    return removeQuotationMarks($str);
+}
+
+//删除key的双引号
+function removeQuotationMarks($str)
+{
+    $str=preg_replace('/"(\w+)":/','$1:',$str);
+    return $str;
+}
+
+function getTableslot($rows)
+{
+    if($rows['formatter']!='slot'){
+        return '';
+    }
+    $field=$rows['field'];
+    $title=$rows['title'];
+    $str=<<<EOF
+                <template #formatter="{field,rows}">
+                    <div v-if="field=='{$field}'">
+                        <span>{$title}插槽内容</span>
+                    </div>
+                </template>
+
+EOF;
+    return $str;
+}
+
+function getFormslot($rows,$istree=false,$treetitle='')
+{
+    $edit=parseJson($rows['edit']);
+    $isslot=false;
+    if(is_string($edit) && $edit=='slot'){
+        $isslot=true;
+    }
+    if(is_array($edit) && $edit['form']=='slot'){
+        $isslot=true;
+    }
+    if(!$isslot){
+        return '';
+    }
+    $field=$rows['field'];
+    $title=$rows['title'];
+    if($field=='pid' && $istree){
+        $str=<<<EOF
+        <template #{$field}="{rows}">
+            <el-form-item label="{:__('{$title}')}:" prop="{$field}">
+                <el-select placeholder="{:__('请选择父级')}" v-model="rows.pid" :clearable="true" style="width: 100%">
+                    <el-option key="all" label="无" value="0"></el-option>
+                    {foreach name="parentList" item="vo"}
+                    <el-option key="{\$vo.id}" label="{:str_replace('&amp;','&',\$vo.{$treetitle})}" value="{\$vo.id}"></el-option>
+                    {/foreach}
+                </el-select>
+            </el-form-item>
+        </template>
+
+EOF;
+    }else{
+        $str=<<<EOF
+        <template #{$field}="{rows}">
+            <el-form-item label="{:__('{$title}')}:" prop="{$field}">
+                <span>插槽内容</span>
+            </el-form-item>
+        </template>
+
+EOF;
+    }
+    return $str;
+}
+
+function parseJson($str)
+{
+    $str=trim($str);
+    if(str_starts_with($str,'{') && str_ends_with($str,'}')){
+        $jsonarr=json_decode($str,true);
+        //如果是非关联数组
+        if(array_keys($jsonarr) === range(0, count($jsonarr) - 1)){
+            $r=[];
+            for($i=count($jsonarr);$i>0;$i--){
+                $r[$i-1]=$jsonarr[$i-1];
+            }
+            return $r;
+        }
+        return $jsonarr;
+    }
+    return $str;
+}
+
+function getRelationMethods($field,$relation)
+{
+    $table=$relation['table'];
+    $config = Db::getConfig();
+    $default=$config['default'];
+    $prefix=$config['connections'][$default]['prefix'];
+    $table=str_replace($prefix,'',$table);
+    $funcname=parse_name($table,1);
+    $tableModelName=str_replace(' ','',ucwords(str_replace('_',' ',$table)));
+    $selectfields=implode(',',array_unique([$relation['relationField'],$relation['showField'],$relation['filterField']]));
+    if($relation['ralationType']=='one'){
+        $action=<<<EOF
+
+    public function {$funcname}()
+    {
+        return \$this->hasOne({$tableModelName}::class,'{$relation['relationField']}','{$field}')->field('{$selectfields}');
+    }
+     
+EOF;
+        return $action;
+    }
+    if($relation['ralationType']=='many'){
+        $action=<<<EOF
+
+    public function {$table}()
+    {
+        return \$this->hasMany({$tableModelName}::class,'{$relation['relationField']}','{$field}')->field('{$selectfields}');
+    }
+     
+EOF;
+        return $action;
+    }
+}
+
+function getRecyclebinField($field){
+    if(empty($field)){
+        return '';
+    }
+    $str='';
+    foreach ($field as $value){
+        $str.=<<<EOF
+    "{$value['field']}"=>"{$value['title']}",
+        
+EOF;
+    }
+    return $str;
+}
+
+function getRecyclebinType($field){
+    if(empty($field)){
+        return '';
+    }
+    $str='';
+    foreach ($field as $value){
+        $str.=<<<EOF
+    "{$value['field']}"=>"{$value['type']}",
+        
+EOF;
+    }
+    return $str;
+}
+
+function getFormatter($rows,$isTree,$treeTitle)
+{
+    if($rows['visible']==='relation'){
+        $relation=json_decode($rows['relation'],true);
+        $table=str_replace(CurdService::$prefix,'',$relation['table']);
+        $arrfieldtype="''";
+        if($rows['formatter']=='tags' || $rows['formatter']=='images'){
+            $arrfieldtype="[]";
+        }
+        $str=<<<EOF
+,"formatter":function (data,row){
+                let {$rows['formatter']}=Yunqi.formatter.{$rows['formatter']};
+                {$rows['formatter']}.value=row.{$table}?row.{$table}.{$relation['showField']}:{$arrfieldtype};
+                return {$rows['formatter']};
+            }
+EOF;
+        return $str;
+    }else{
+        if($isTree && $rows['field']==$treeTitle){
+            $str=<<<EOF
+,"formatter":function(data){
+                let html=Yunqi.formatter.html;
+                html.value=data.replace(/&nbsp;/g,'&nbsp;&nbsp;');
+                return html;
+            }
+EOF;
+            return $str;
+        }
+        if($rows['formatter']=='text'){
+            return '';
+        }
+        $formatter=',"formatter":Yunqi.formatter.'.$rows['formatter'];
+        return $formatter;
+    }
+}

+ 62 - 0
app/admin/service/curd/js-add.txt

@@ -0,0 +1,62 @@
+<#if form#>
+import form from "@components/Form.js";
+<#endif#>
+export default{
+    components:{
+        <#if form#>
+        'YunForm':form
+        <#endif#>
+    },
+    data:{
+        <#if form#>
+        columns:[
+<#fields#>
+        ],
+        row:Yunqi.data.row || {}
+        <#endif#>
+    },
+    <#if !reduced#>
+    //页面加载完成时执行
+    onLoad:function(query){
+        console.log(query);
+    },
+    //页面初始显示或在框架内显示时执行
+    onShow:function(){
+
+    },
+    //页面在框架内隐藏时执行
+    onHide:function(){
+
+    },
+    //页面在框架内关闭时执行
+    onUnload:function(){
+
+    },
+    <#endif#>
+    methods: {
+        <#if !reduced && form#>
+        onFormRender:function(rows){
+            //表单渲染完成后执行
+        },
+        onSubmit:function(rows){
+            //表单提交前执行,返回false可以阻止表单提交
+            /**
+             * form常用方法
+             * this.$refs.yunform.setError(field,message);//聚焦表单项并显示错误信息
+             * this.$refs.yunform.hideField(field);//隐藏表单项
+             * this.$refs.yunform.showField(field);//显示表单项
+             * this.$refs.yunform.setValue(field,value);//为表单项设置值
+             * this.$refs.yunform.getValue(field);//为获取表单项的值
+             * this.$refs.yunform.setField(field,key,value);//修改表单json的其他属性值,比如rules,title,searchList等
+             */
+            return true;
+        },
+        onSuccess:function(response){
+            //表单提交成功后执行
+        },
+        onFail:function(err){
+            //表单提交失败后执行
+        }
+        <#endif#>
+    }
+}

+ 109 - 0
app/admin/service/curd/js-index.txt

@@ -0,0 +1,109 @@
+<#if table#>
+import table from "@components/Table.js";
+<#endif#>
+export default{
+    components:{
+        <#if table#>
+        'YunTable':table
+        <#endif#>
+    },
+    data:{
+        <#if table#>
+        extend:{
+            index_url: '<#pack#>/index',
+            <#if form#>
+            add_url: '<#pack#>/add',
+            edit_url: '<#pack#>/edit',
+            <#endif#>
+            <#if in_array('del',actions)#>
+            del_url: '<#pack#>/del',
+            <#endif#>
+            multi_url: '<#pack#>/multi',
+            <#if in_array('download',actions)#>
+            download_url: '<#pack#>/download',
+            <#endif#>
+            <#if in_array('import',actions)#>
+            import_url: '<#pack#>/import',
+            <#endif#>
+            <#if in_array('recyclebin',actions)#>
+            recyclebin_url:'<#pack#>/recyclebin'
+            <#endif#>
+        },
+        columns:[
+            <#if reduced#>
+            {checkbox: true},
+            <#endif#>
+            <#if !reduced#>
+            {checkbox: true,selectable:function (row,index){
+                //可以根据业务需求返回false让某些行不可选中
+                return true;
+            }},
+            <#endif#>
+<#fields#>
+            <#if isTree#>
+            {treeExpand: true},
+            <#endif#>
+            {
+                field: 'operate',
+                title: __('操作'),
+                width:130,
+                action:{
+                    <#if in_array('edit',actions)#>
+                    <#if reduced#>
+                    edit:true,
+                    <#endif#>
+                    <#if !reduced#>
+                    edit:function(row){
+                        //可以根据业务需求返回false让按钮不显示
+                        return true
+                    },
+                    <#endif#>
+                    <#endif#>
+                    <#if in_array('del',actions)#>
+                    del:true,
+                    <#endif#>
+                    <#if sort#>
+                    sort:true,
+                    <#endif#>
+                    <#if expand#>
+                    expand:true,
+                    <#endif#>
+                }
+            }
+        ]
+        <#endif#>
+    },
+    <#if !reduced#>
+    //页面加载完成时执行
+    onLoad:function(query){
+        console.log(query);
+    },
+    //页面初始显示或在框架内显示时执行
+    onShow:function(){
+
+    },
+    //页面在框架内隐藏时执行
+    onHide:function(){
+
+    },
+    //页面在框架内关闭时执行
+    onUnload:function(){
+
+    },
+    <#endif#>
+    methods: {
+        <#if !reduced && table#>
+        onTableRender:function(list){
+            //表格渲染完成后执行
+            /**
+             * table常用方法
+             * this.$refs.yuntable.reset();//重新渲染整个组件,当columns修改时,需要重新渲染表格才能生效,可以执行该方法。
+             * this.$refs.yuntable.reload();//保持当前的page,重新获取数据
+             * this.$refs.yuntable.submit();//返回第一页,重新获取数据
+             * this.$refs.yuntable.expandAllTree();//树形表格展开所有节点
+             * this.$refs.yuntable.expandTree(topid);//树形表格展开指定节点
+             */
+        }
+        <#endif#>
+    }
+}

+ 29 - 0
app/admin/service/curd/js.txt

@@ -0,0 +1,29 @@
+export default{
+    components:{
+
+    },
+    data:{
+
+    },
+    <#if !reduced#>
+    //页面加载完成时执行
+    onLoad:function(query){
+        console.log(query);
+    },
+    //页面初始显示或在框架内显示时执行
+    onShow:function(){
+
+    },
+    //页面在框架内隐藏时执行
+    onHide:function(){
+
+    },
+    //页面在框架内关闭时执行
+    onUnload:function(){
+
+    },
+    <#endif#>
+    methods: {
+
+    }
+}

+ 24 - 0
app/admin/service/curd/model-extend-base.txt

@@ -0,0 +1,24 @@
+<?php
+declare(strict_types=1);
+
+namespace <#namespace#>;
+
+use app\common\model\base\BaseModel;
+
+class <#modelName#> extends BaseModel
+{
+    <#if name#>
+    protected $name = '<#name#>';
+    <#endif#>
+
+    <#if weigh#>
+    public static function onAfterInsert($data)
+    {
+        $data->weigh=1000-$data->id;
+        $data->save();
+    }
+    <#endif#>
+    <#if methods#>
+<#methods#>
+    <#endif#>
+}

+ 53 - 0
app/admin/service/curd/model-normal.txt

@@ -0,0 +1,53 @@
+<?php
+declare(strict_types=1);
+
+namespace <#namespace#>;
+
+use think\Model;
+<#if deletetime#>
+use think\model\concern\SoftDelete;
+<#endif#>
+
+class <#modelName#> Extends Model
+{
+    <#if name#>
+    protected $name = '<#name#>';
+    <#endif#>
+    <#if createtime || updatetime#>
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = true;
+    <#endif#>
+    <#if createtime#>
+    protected $createTime = 'createtime';
+    <#endif#>
+    <#if updatetime#>
+    protected $updateTime = 'updatetime';
+    <#endif#>
+
+    <#if deletetime#>
+    use SoftDelete;
+    protected $deleteTime = 'deletetime';
+    <#endif#>
+
+    <#if updatetime || createtime#>
+    protected $type = [
+         <#if createtime#>
+        'createtime'     =>  'timestamp:Y-m-d H:i',
+         <#endif#>
+         <#if updatetime#>
+        'updatetime'     =>  'timestamp:Y-m-d H:i',
+         <#endif#>
+    ];
+    <#endif#>
+
+    <#if weigh#>
+    public static function onAfterInsert($data)
+    {
+        $data->weigh=1000-$data->id;
+        $data->save();
+    }
+    <#endif#>
+    <#if methods#>
+<#methods#>
+    <#endif#>
+}

+ 29 - 0
app/admin/service/curd/view-add.txt

@@ -0,0 +1,29 @@
+<template>
+    <el-card shadow="never" style="border: 0;">
+        <#if form#>
+        <yun-form
+            <#if !reduced#>
+            ref="yunform"
+            @render="onFormRender"
+            @submit="onSubmit"
+            @success="onSuccess"
+            @fail="onFail"
+            <#endif#>
+            :data="row"
+            :columns="columns">
+            <template #default>
+                {:token_field()}
+            </template>
+    <#slot#>
+        </yun-form>
+        <#endif#>
+        <#if !form#>
+        <#title#>
+        <#endif#>
+    </el-card>
+</template>
+<script>
+<#js#>
+</script>
+<style>
+</style>

+ 15 - 0
app/admin/service/curd/view-edit.txt

@@ -0,0 +1,15 @@
+<#if form#>
+{include vue="<#temp#>" /}
+<#endif#>
+<#if !form#>
+<template>
+    <el-card shadow="never" style="border: 0;">
+       <#title#>
+    </el-card>
+</template>
+<script>
+<#js#>
+</script>
+<style>
+</style>
+<#endif#>

+ 75 - 0
app/admin/service/curd/view-index.txt

@@ -0,0 +1,75 @@
+<template>
+    <el-card shadow="never">
+        <#if table#>
+        <yun-table
+                :columns="columns"
+                <#if !reduced#>
+                ref="yuntable"
+                @render="onTableRender"
+                <#endif#>
+                <#if search#>
+                search="nickname,mobile"
+                <#endif#>
+                <#if !commonSearch#>
+                :common-search="false"
+                <#endif#>
+                <#if !pagination#>
+                :pagination="false"
+                <#endif#>
+                <#if tabs#>
+                tabs="<#tabs#>"
+                <#endif#>
+                <#if isTree#>
+                :is-tree="true"
+                :tree-expand-all="true"
+                <#endif#>
+                <#if summary#>
+                :show-summary="true"
+                <#endif#>
+                <#if weigh#>
+                sort-name="weigh,id"
+                order="desc,desc"
+                <#endif#>
+                toolbar="<#toolbarStr#>"
+                :auth="{
+                    <#if in_array('add',toolbar)#>
+                    add:{:$auth->check('<#controller#>','add')},
+                    <#endif#>
+                    <#if in_array('edit',toolbar)#>
+                    edit:{:$auth->check('<#controller#>','edit')},
+                    <#endif#>
+                    <#if in_array('del',toolbar)#>
+                    del:{:$auth->check('<#controller#>','del')},
+                    <#endif#>
+                    multi:{:$auth->check('<#controller#>','multi')},
+                    <#if in_array('import',toolbar)#>
+                    import:{:$auth->check('<#controller#>','import')},
+                    <#endif#>
+                    <#if in_array('download',toolbar)#>
+                    download:{:$auth->check('<#controller#>','download')},
+                    <#endif#>
+                    <#if in_array('recyclebin',toolbar)#>
+                    recyclebin:{:$auth->check('<#controller#>','recyclebin')},
+                    <#endif#>
+                }"
+                :extend="extend">
+                <#if expand#>
+                <template #expand="{rows}">
+                    <span>扩展内容</span>
+                </template>
+                <#endif#>
+                <#if slot#>
+<#slot#>
+                <#endif#>
+        </yun-table>
+        <#endif#>
+        <#if !table#>
+        <#title#>
+        <#endif#>
+    </el-card>
+</template>
+<script>
+<#js#>
+</script>
+<style>
+</style>

+ 10 - 0
app/admin/service/curd/view-method.txt

@@ -0,0 +1,10 @@
+<template>
+    <el-card shadow="never">
+        <#title#>
+    </el-card>
+</template>
+<script>
+<#js#>
+</script>
+<style>
+</style>

+ 532 - 0
app/admin/traits/Actions.php

@@ -0,0 +1,532 @@
+<?php
+/**
+ * ----------------------------------------------------------------------------
+ * 行到水穷处,坐看云起时
+ * 开发软件,找贵阳云起信息科技,官网地址:https://www.56q7.com/
+ * ----------------------------------------------------------------------------
+ * Author: 老成
+ * email:85556713@qq.com
+ */
+declare(strict_types=1);
+
+namespace app\admin\traits;
+
+use app\common\library\Tree;
+use PhpOffice\PhpSpreadsheet\Cell\DataType;
+use PhpOffice\PhpSpreadsheet\Shared\Date;
+use think\annotation\route\Route;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
+use PhpOffice\PhpSpreadsheet\Reader\Xls;
+use PhpOffice\PhpSpreadsheet\Reader\Csv;
+use think\facade\Db;
+use app\common\library\Export;
+
+trait Actions
+{
+    /**
+     * 增删改查等操作成功后的回调方法
+     * 请勿在回调函数内使用$this->error()方法,使用ThrowException抛出异常
+     */
+    protected $callback;
+
+    /**
+     * 自定义导出数据的控制器及方法
+     */
+    protected $downloadController;
+    protected $downloadAction;
+    /**
+     * 添加插入时额外增加的字段
+     * 支持add,edit
+     */
+    protected $postParams = [];
+
+    /**
+     * 回收站显示的字段
+     */
+    protected $recyclebinColumns=[
+        'id'=>'ID'
+    ];
+    /**
+     * 回收站显示的字段类型,支持text,image,images,date,datetime,tag,tags,默认为text
+     */
+    protected $recyclebinColumnsType=[];
+    /**
+     * 导入字段
+     */
+    protected $importFields=[];
+
+    /**
+     * 关联查询的字段
+     * @var array
+     */
+    protected $relationField=[];
+
+
+    /**
+     * 修改、删除、更新时验证属性权限
+     * @var array
+     */
+    protected $volidateFields=[];
+
+    /**
+     * 查看
+     */
+    #[Route('GET,JSON','index')]
+    public function index()
+    {
+        if (false === $this->request->isAjax()) {
+            return $this->fetch();
+        }
+        if($this->request->post('selectpage')){
+            return $this->selectpage();
+        }
+        [$where, $order, $limit, $with] = $this->buildparams();
+        $list = $this->model
+            ->withJoin($with,'left')
+            //如果没有使用operate filter过滤的情况下,推荐使用with关联,可以提高查询效率
+            //->with($with)
+            ->where($where)
+            ->order($order)
+            ->paginate($limit);
+        $result = ['total' => $list->total(), 'rows' => $list->items()];
+        return json($result);
+    }
+    /**
+     * 添加
+     */
+    #[Route('GET,POST','add')]
+    public function add()
+    {
+        if (false === $this->request->isPost()) {
+            return $this->fetch();
+        }
+        $params = array_merge($this->request->post("row/a"),$this->postParams);
+        if (empty($params)) {
+            $this->error(__('提交的参数不能为空'));
+        }
+        if(!$this->request->checkToken('__token__',['__token__'=>$this->request->post('__token__')])){
+            $this->error(__('token错误,请刷新页面重试'));
+        }
+        foreach ($params as &$value){
+            if(is_array($value)){
+                $value=implode(',',$value);
+            }
+            if($value===''){
+                $value=null;
+            }
+        }
+        $result = false;
+        Db::startTrans();
+        try {
+            $result = $this->model->save($params);
+            if($this->callback){
+                $callback=$this->callback;
+                $callback($this->model);
+            }
+            Db::commit();
+        } catch (\Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        if ($result === false) {
+            $this->error(__('没有新增任何数据'));
+        }
+        $this->success();
+    }
+    /**
+     * 编辑
+     */
+    #[Route('GET,POST','edit')]
+    public function edit(mixed $row=null)
+    {
+        $ids = $this->request->get('ids');
+        if(!$row || is_array($row)){
+            $row = $this->model->find($ids);
+        }
+        if (!$row) {
+            $this->error(__('没有找到记录'));
+        }
+        if(count($this->volidateFields)>0){
+            foreach ($this->volidateFields as $field=>$value){
+                if($row[$field]!=$value){
+                    $this->error(__('没有操作权限'));
+                }
+            }
+        }
+        if (false === $this->request->isPost()) {
+            $this->assign('row', $row);
+            return $this->fetch();
+        }
+        $params = array_merge($this->request->post("row/a"),$this->postParams);
+        if (empty($params)) {
+            $this->error(__('提交的参数不能为空'));
+        }
+        if(!$this->request->checkToken('__token__',['__token__'=>$this->request->post('__token__')])){
+            $this->error(__('token错误,请刷新页面重试'));
+        }
+        foreach ($params as &$value){
+            if(is_array($value)){
+                $value=implode(',',$value);
+            }
+            if($value===''){
+                $value=null;
+            }
+        }
+        $result = false;
+        Db::startTrans();
+        try {
+            $result = $row->save($params);
+            if($this->callback){
+                $callback=$this->callback;
+                $callback($row);
+            }
+            Db::commit();
+        } catch (\Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        if (false === $result) {
+            $this->error(__('没有数据被更新'));
+        }
+        $this->success();
+    }
+    /**
+     * 删除
+     */
+    #[Route('GET,POST','del')]
+    public function del()
+    {
+        $ids = $this->request->param("ids");
+        if (empty($ids)) {
+            $this->error(__('参数%s不能为空', ['s'=>'ids']));
+        }
+        $pk = $this->model->getPk();
+        $list = $this->model->where($pk, 'in', $ids)->select();
+        $count = 0;
+        Db::startTrans();
+        try {
+            foreach ($list as $item) {
+                if(count($this->volidateFields)>0){
+                    foreach ($this->volidateFields as $field=>$value){
+                        if($item[$field]!=$value){
+                            $this->error(__('没有操作权限'));
+                        }
+                    }
+                }
+                $count += $item->delete();
+            }
+            if($this->callback){
+                $callback=$this->callback;
+                $callback($ids);
+            }
+            Db::commit();
+        } catch (\Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        if ($count) {
+            $this->success();
+        }
+        $this->error(__('没有记录被删除'));
+    }
+    /**
+     * 批量更新一个字段
+     */
+    #[Route('POST,GET','multi')]
+    public function multi()
+    {
+        $ids = $this->request->param('ids');
+        $field = $this->request->param('field');
+        $value = $this->request->param('value');
+        if(!$ids){
+            $this->error(__('没有需要更新的行'));
+        }
+        if(!$field){
+            $this->error(__('没有需要更新的列'));
+        }
+        $ids=is_string($ids)?explode(',',$ids):$ids;
+        $pk=$this->model->getPk();
+        $count = 0;
+        Db::startTrans();
+        try {
+            foreach ($ids as $id) {
+                $id=intval($id);
+                $where=[
+                    $pk=>$id
+                ];
+                if(count($this->volidateFields)>0){
+                    foreach ($this->volidateFields as $sk=>$sv){
+                        $where[$sk]=$sv;
+                    }
+                }
+                $r = $this->model->where($where)->update([$field=>$value]);
+                if($r){
+                    $count++;
+                }
+            }
+            if($this->callback){
+                $callback=$this->callback;
+                $callback($ids,$field,$value);
+            }
+            Db::commit();
+        } catch (\Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        if ($count) {
+            $this->success();
+        }
+        $this->error(__('没有数据被更新'));
+    }
+    /**
+     * 导入
+     */
+    #[Route('GET,POST','import')]
+    protected function import()
+    {
+        $file = $this->request->request('file');
+        if (!$file) {
+            $this->error(__('参数%s不能为空', ['s'=>'file']));
+        }
+        $filePath = root_path() . DS . $file;
+        if (!is_file($filePath)) {
+            $this->error(__('上传文件不存在'));
+        }
+        //实例化reader
+        $ext = pathinfo($filePath, PATHINFO_EXTENSION);
+        if (!in_array($ext, ['csv', 'xls', 'xlsx'])) {
+            $this->error(__('文件格式不正确'));
+        }
+        if ($ext === 'csv') {
+            $file = fopen($filePath, 'r');
+            $filePath = tempnam(sys_get_temp_dir(), 'import_csv');
+            $fp = fopen($filePath, 'w');
+            $n = 0;
+            while ($line = fgets($file)) {
+                $line = rtrim($line, "\n\r\0");
+                $encoding = mb_detect_encoding($line, ['utf-8', 'gbk', 'latin1', 'big5']);
+                if ($encoding !== 'utf-8') {
+                    $line = mb_convert_encoding($line, 'utf-8', $encoding);
+                }
+                if ($n == 0 || preg_match('/^".*"$/', $line)) {
+                    fwrite($fp, $line . "\n");
+                } else {
+                    fwrite($fp, '"' . str_replace(['"', ','], ['""', '","'], $line) . "\"\n");
+                }
+                $n++;
+            }
+            fclose($file) || fclose($fp);
+            $reader = new Csv();
+        } elseif ($ext === 'xls') {
+            $reader = new Xls();
+        } else {
+            $reader = new Xlsx();
+        }
+        $fieldArr = array_flip($this->importFields);
+        if(empty($fieldArr)){
+            $this->error(__('导入字段不能为空'));
+        }
+        //加载文件
+        $insert = [];
+        try {
+            if (!$PHPExcel = $reader->load($filePath)) {
+                $this->error(__('未知文件格式'));
+            }
+            $currentSheet = $PHPExcel->getSheet(0);  //读取文件中的第一个工作表
+            $allColumn = $currentSheet->getHighestDataColumn(); //取得最大的列号
+            $allRow = $currentSheet->getHighestRow(); //取得一共有多少行
+            $maxColumnNumber = Coordinate::columnIndexFromString($allColumn);
+            $fields = [];
+            for ($currentRow = 1; $currentRow <= 1; $currentRow++) {
+                for ($currentColumn = 1; $currentColumn <= $maxColumnNumber; $currentColumn++) {
+                    $columnName = Coordinate::stringFromColumnIndex($currentColumn);
+                    $val = $currentSheet->getCell($columnName.$currentRow)->getCalculatedValue();
+                    $fields[] = is_null($val) ? '' : $val;
+                }
+            }
+            for ($currentRow = 2; $currentRow <= $allRow; $currentRow++) {
+                $values = [];
+                for ($currentColumn = 1; $currentColumn <= $maxColumnNumber; $currentColumn++) {
+                    $columnName = Coordinate::stringFromColumnIndex($currentColumn);
+                    $cell = $currentSheet->getCell($columnName.$currentRow);
+                    $dataType=$cell->getDataType();
+                    $val = $cell->getCalculatedValue();
+                    if($dataType==DataType::TYPE_ISO_DATE){
+
+                    }
+                    $values[] = is_null($val) ? '' : $val;
+                }
+                $row = [];
+                $temp = array_combine($fields, $values);
+                foreach ($temp as $k => $v) {
+                    if(isset($fieldArr[$k])){
+                        $row[$fieldArr[$k]]=$v;
+                    }
+                }
+                if ($row) {
+                    $insert[] = $row;
+                }
+            }
+        } catch (Exception $exception) {
+            $this->error($exception->getMessage());
+        }
+        if (!$insert) {
+            $this->error(__('一行都未导入'));
+        }
+        $success=0;
+        $fail=[];
+        try {
+            if($this->callback){
+                $rinsert=[];
+                $callback = $this->callback;
+                foreach ($insert as $vf){
+                    $ss=$callback($vf,$success,$fail);
+                    if($ss){
+                        $rinsert[]=$ss;
+                        $success++;
+                    }
+                }
+                if(!empty($rinsert)){
+                    $this->model->saveAll($rinsert);
+                }
+            }else{
+                $this->model->saveAll($insert);
+            }
+        } catch (PDOException $exception) {
+            $msg = $exception->getMessage();
+            $this->error($msg);
+        } catch (Exception $e) {
+            $this->error($e->getMessage());
+        }
+        $this->success('',compact('success','fail'));
+    }
+    /**
+     * 回收站
+     */
+    #[Route('GET,POST,JSON','recyclebin')]
+    public function recyclebin($action='list')
+    {
+        switch ($action){
+            case 'list':
+                if (false === $this->request->isAjax()) {
+                    $search=[];
+                    $this->assign('search', implode(',',$search));
+                    $this->assign('columns', $this->recyclebinColumns);
+                    $this->assign('columnsType', $this->recyclebinColumnsType);
+                    return $this->fetch('common/recyclebin');
+                }
+                [$where, $order, $limit, $with] = $this->buildparams();
+                $list = $this->model
+                    ->withJoin($with,'left')
+                    ->onlyTrashed()
+                    ->where($where)
+                    ->order($order)
+                    ->paginate($limit);
+                $result = ['total' => $list->total(), 'rows' => $list->items()];
+                return json($result);
+            case 'restore':
+                $ids=$this->request->param('ids');
+                foreach ($ids as $id){
+                    $row=$this->model->onlyTrashed()->find($id);
+                    if($row){
+                        $row->restore();
+                    }
+                }
+                $this->success();
+            case 'destroy':
+                $ids=$this->request->param('ids');
+                foreach ($ids as $id){
+                    $row=$this->model->onlyTrashed()->find($id);
+                    if($row){
+                        $row->force()->delete();
+                    }
+                }
+                $this->success();
+            case 'restoreall':
+                $this->model->onlyTrashed()->where('deletetime','<>',null)->update(['deletetime'=>null]);
+                $this->success();
+            case 'clear':
+                Db::execute('delete from '.$this->model->getTable().' where deletetime is not null');
+                $this->success();
+        }
+    }
+    /**
+     * 下载
+     */
+    #[Route('GET,JSON','download')]
+    public function download()
+    {
+        if($this->request->isAjax()){
+            $postdata=$this->request->post();
+            if (!$this->downloadController){
+                //获取table的列
+                $listAction=explode('/',str_replace('.','/',$postdata['listAction']));
+                $controller='\\app\\admin\\controller';
+                for($i=0;$i<count($listAction)-1;$i++){
+                    if($i==count($listAction)-2){
+                        $listAction[$i]=ucfirst($listAction[$i]);
+                    }
+                    $controller.='\\'.$listAction[$i];
+                }
+                $this->downloadController=$controller;
+            }
+            if (!$this->downloadAction){
+                $listAction=explode('/',str_replace('.','/',$postdata['listAction']));
+                $action=$listAction[count($listAction)-1];
+                if(strpos($action,'?')){
+                    $action=substr($action,0,strpos($action,'?'));
+                }
+                if(strpos($action,'-')!==false){
+                    $arrs=explode('-',$action);
+                    $action='';
+                    foreach ($arrs as $k=>$v){
+                        if($k>0){
+                            $action.=ucfirst($v);
+                        }else{
+                            $action.=$v;
+                        }
+                    }
+                }
+                $this->downloadAction=$action;
+            }
+            $obj=new ($this->downloadController)($this->request);
+            $result=call_user_func_array([$obj,$this->downloadAction],[]);
+            $list=$result->getData()['rows'];
+            if($postdata['isTree']){
+                $list=Tree::instance()->getTreeList($list);
+            }
+            if($this->callback){
+                $callback=$this->callback;
+                foreach ($list as $k=>$v){
+                    $list[$k]=$callback($v);
+                }
+            }
+            //格式化
+            $fields=[];
+            foreach ($postdata['field'] as $v){
+                $fields[$v['field']]=$v['title'];
+            }
+            //导出到excel
+            $export=new Export();
+            $export->setColumn($fields);
+            $export->setData($list,$postdata['searchList']);
+            $export->write();
+            $file=date('YmdHis',time()).'.xlsx';
+            $export->save(root_path().'runtime'.DS,$file);
+            $this->success('',$file);
+        }else{
+            $file=$this->request->get('file');
+            $filepath=root_path().'runtime'.DS.$file;
+            if(!file_exists($filepath)){
+                $this->error('没有找到文件');
+            }
+            header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
+            header('Content-Disposition: attachment;filename="'.$file);
+            header('Cache-Control: max-age=1');
+            echo file_get_contents($filepath);
+            unlink($filepath);
+            exit;
+        }
+    }
+}

+ 99 - 0
app/admin/view/addons/create.html

@@ -0,0 +1,99 @@
+<template>
+    <el-card shadow="never" style="border: 0;">
+        <yun-form
+                :steps="['设置基础资料','设置扩展文件','添加菜单与数据']"
+                @submit="onSubmit"
+                :data="rows"
+                :columns="columns">
+            <template #default>
+                {:token_field()}
+            </template>
+            <template #tables="{step,rows}">
+                <el-form-item label="{:__('选择表')}:" v-if="step==2">
+                    <el-select v-model="rows.tables" placeholder="请选择" multiple filterable style="width: 100%">
+                        {foreach $table as $value}
+                        <el-option value="{$value}">{$value}</el-option>
+                        {/foreach}
+                    </el-select>
+                </el-form-item>
+            </template>
+            <template #config="{step,rows}">
+                <el-form-item label="{:__('选择配置')}:" v-if="step==2">
+                    <el-select v-model="rows.config" placeholder="请选择" multiple filterable style="width: 100%">
+                        {foreach $sonfig as $key=>$value}
+                        <el-option :value="{$key}" label="{$value.name}-{$value.title}"></el-option>
+                        {/foreach}
+                    </el-select>
+                </el-form-item>
+            </template>
+            <template #menu="{step,rows}">
+                <el-form-item label="{:__('选择菜单')}:" v-if="step==2">
+                    <el-tree ref="tree" :props="{children:'childlist',label:'title'}" node-key="id" show-checkbox :default-checked-keys="checkedKey" :data="treedata" style="width: 100%;"></el-tree>
+                </el-form-item>
+            </template>
+        </yun-form>
+    </el-card>
+</template>
+<script>
+    import form from "@components/Form.js";
+    import {copyObj} from "@util.js";
+    export default {
+        components:{'YunForm':form},
+        data:{
+            checkedKey:[],
+            columns:[
+                {"field":"id","title":"ID",width:80,"edit":"hidden"},
+                {
+                    field:"pack",
+                    title:"包名",
+                    edit:{form: 'input',type:'text',placeholder: '请输入包名,包名只能由小写字母、数字、下划线组成'},
+                    step:0,
+                    rules: {
+                        required:true,
+                        validator:function(rule, value, callback){
+                            if(!/^[a-z0-9_]+$/.test(value)){
+                                callback(new Error(__('包名必须为小写字母、数字、下划线组成')));
+                            }
+                            callback();
+                        },
+                        trigger: 'blur'
+                    },
+                },
+                {field:"name",title:"扩展名称",edit:'text',step:0,rules:'required;length(2~30)'},
+                {field:"type",title:"扩展类型",searchList:Yunqi.data.type,edit:{form:'select',value:'plugin'},step:0,rules:'required'},
+                {field:"author",title:"作者",edit:'text',step:0},
+                {field:"price",title:"价格",edit:{form:'input',type:'number',append:'元'},step:0,rules:'required;range(0~)'},
+                {field:"version",title:"版本号",edit:{form:'input',type:'number',placeholder: '请输入100到1999之间的数字,对应版本号为1.0.0或19.9.9'},rules:'required;range(100~1999)',step:0},
+                {field:"document",title:"说明文档",edit:{form:'input',type:'text',placeholder:'请输入文档链接'},step:0},
+                {field:"description",title:"简介",edit:'textarea',step:0},
+                {field: "files",title:'扩展文件',edit:{form:'input',type:'textarea',rows:3,placeholder:'请输入扩展文件目录或扩展文件,相对系统根目录路径,每一行一个目录或文件'},step:1,rules:'required'},
+                {field: "unpack",title:'过滤文件',edit:{form:'input',type:'textarea',rows:3,placeholder:'请输入打包时需要过滤掉的目录或文件,每一行一个目录或文件'},step:1},
+                {field: "require",title:'依赖文件',edit:{form:'input',type:'textarea',rows:3,placeholder:'请输入依赖的composer类,每一行一个完整类名,如\\Yansongda\\Pay\\Pay'},step:1},
+                {field: "addons",title:'依赖扩展',edit:{form:'input',type:'textarea',rows:3,placeholder:'请输入依赖的扩展,每一行一个扩展的包名,如qqmap'},step:1},
+                {field: "tables",title:'选择表',edit:'slot',step:2},
+                {field: "config",title:'选择配置',edit:'slot',step:2},
+                {field: "menu",title:'选择菜单',edit:'slot',step:2},
+            ],
+            rows:Yunqi.data.rows,
+            treedata:[],
+        },
+        onShow:function (){
+            this.checkedKey = copyObj(Yunqi.data.rows.menu);
+            Yunqi.ajax.get('auth/group/roletree',{groupid:1},false,false,true).then(res=>{
+                this.treedata=res;
+            });
+        },
+        methods: {
+            onSubmit:function (row){
+                let s1=this.$refs.tree.getCheckedKeys();
+                let s2=this.$refs.tree.getHalfCheckedKeys();
+                row.menu=s2.concat(s1).join(',');
+                return true;
+            }
+        }
+    }
+</script>
+<style>
+
+</style>
+

+ 446 - 0
app/admin/view/addons/index.html

@@ -0,0 +1,446 @@
+<template>
+    <el-card shadow="never">
+        <template #header>
+            {if $plugins_host!='www.56q7.com'}
+            <el-alert effect="dark" :closable="false">扩展可以为系统提供丰富的功能,支持通过更换扩展服务地址来安装其他开发者的扩展程序,同时你也可以打包好的扩展程序开放给别人使用,如需要开放本地扩展,请下载插件【开放扩展支持】</el-alert>
+            {else}
+            <el-alert effect="dark" :closable="false" type="error" title="温馨提醒">您当前未使用官方服务端,为了避免不必要的损失,请勿在正式环境上安装扩展,如有人违法销售涉及“赌博、色情、盗版”等违法扩展程序,<a style="color: #fff;text-decoration: underline;" target="_blank" href="https://bbs.56q7.com/">点击这里举报</a>。</el-alert>
+            {/if}
+        </template>
+        <yun-table
+                :columns="columns"
+                :common-search="false"
+                search="title"
+                tabs="type"
+                ref="yuntable"
+                toolbar="refresh,install,create"
+                :extend="extend">
+                <template #toolbar="{tool}">
+                    <template v-if="tool=='install'">
+                        <el-button type="primary" plain class="install hide-600">
+                            <el-radio-group v-model="plain" @change="changePlain">
+                                <el-radio label="all">全部</el-radio>
+                                <el-radio label="free" class="hide-1000">免费</el-radio>
+                                <el-radio label="not-free" class="hide-1000">付费</el-radio>
+                                <el-radio label="local">本地</el-radio>
+                            </el-radio-group>
+                        </el-button>
+                    </template>
+                    <template v-if="tool=='create'">
+                        <el-button type="primary" @click="createAddon">
+                            创建扩展
+                        </el-button>
+                    </template>
+                </template>
+                <template #header="{field}">
+                    <div v-if="field=='packed'">
+                        打包
+                        <el-tooltip
+                                effect="dark"
+                                content="仅支持打包安装好的扩展"
+                                placement="top-start">
+                            <i class="fa fa-info-circle"></i>
+                        </el-tooltip>
+                    </div>
+                    <div v-if="field=='open'">
+                        开放
+                        <el-tooltip
+                                effect="dark"
+                                content="打包好的扩展才能开放给其他开发者使用,禁止销售涉及“赌博、色情、盗版”等违法扩展程序,禁止销售未取得版权,其他开发者的付费程序"
+                                placement="top-start">
+                            <i class="fa fa-info-circle"></i>
+                        </el-tooltip>
+                    </div>
+                </template>
+        </yun-table>
+    </el-card>
+    <el-dialog :width="500" v-model="buyDialog.show" @close="closeBuyDialog" :title="'购买'+type[buyDialog.row.type]+'【'+buyDialog.row.name+'】'">
+        <div class="paycode">
+            <el-alert type="warning">购买后,可以通过微信的支付凭证交易单号重复下载,有效期为30天</el-alert>
+            <img v-if="buyDialog.code_url" :src="buyDialog.code_url" style="width: 150px;height: 150px;"/>
+            <div style="margin-bottom: 10px;">¥{{buyDialog.row.price}}</div>
+        </div>
+        <div class="transaction_id">
+            <el-input v-model="buyDialog.transaction_id" placeholder="请输入交易单号" @change="checkTransactionId"></el-input>
+        </div>
+        <div class="message" v-if="buyDialog.message">
+            <el-tag type="success" v-if="buyDialog.expire_time>0">{{buyDialog.message}}</el-tag>
+            <el-tag type="info" v-else>{{buyDialog.message}}</el-tag>
+        </div>
+        <div class="footer" v-if="buyDialog.status && buyDialog.expire_time>0">
+            <el-button size="large" type="primary" style="width: 100%" @click="payDownload">下载</el-button>
+        </div>
+    </el-dialog>
+</template>
+<script>
+    import table from "@components/Table.js";
+    import {rand} from "@util.js";
+    //生成订单号
+    function create_out_trade_no()
+    {
+        let date=new Date();
+        let year=date.getFullYear();
+        let month=date.getMonth()+1;
+        let day=date.getDate();
+        if(day<10){
+            day='0'+day;
+        }
+        let hour=date.getHours();
+        if(hour<10){
+            hour='0'+hour;
+        }
+        let minute=date.getMinutes();
+        if(minute<10){
+            minute='0'+minute;
+        }
+        let seconds=date.getSeconds();
+        if(seconds<10){
+            seconds='0'+seconds;
+        }
+        let r=''+year+month+day+hour+minute+seconds+rand(10000,99999);
+        return r;
+    }
+    function formatTime(seconds)
+    {
+        let interval = Math.floor(seconds / 31536000);
+        if (interval >= 1) {
+            return `${interval}年后`;
+        }
+        interval = Math.floor(seconds / 2592000);
+        if (interval >= 1) {
+            return `${interval}月后`;
+        }
+        interval = Math.floor(seconds / 86400);
+        if (interval >= 1) {
+            return `${interval}天后`;
+        }
+        interval = Math.floor(seconds / 3600);
+        if (interval >= 1) {
+            return `${interval}小时后`;
+        }
+        interval = Math.floor(seconds / 60);
+        if (interval >= 1) {
+            return `${interval}分钟后`;
+        }
+        return `${Math.floor(seconds)}秒后`;
+    }
+    export default{
+        components:{'YunTable':table},
+        data:{
+            extend:{
+                index_url: 'addons/index',
+                multi_url: 'addons/multi'
+            },
+            type:Yunqi.data.type,
+            tabsValue:'plugin',
+            plain:'all',
+            columns:[
+                {field:"plain",visible:'none',operate:{form:'hidden',value:function (){return Yunqi.app.plain},filter:false}},
+                {field:"name",title:"扩展名称"},
+                {field:"type",title:"扩展类型",searchList:Yunqi.data.type,operate:false},
+                {field:"author",title:"作者"},
+                {field:"price",title:"价格",width: 80,formatter:function (data) {
+                        let tag=Yunqi.formatter.tag;
+                        if (data) {
+                            tag.type='primary';
+                            tag.value='¥'+data;
+                        }else{
+                            tag.type='success';
+                            tag.value='免费';
+                        }
+                        return tag;
+                    }},
+                {field:"version",title:"版本号",width: 80},
+                {field:"document",title:"说明文档",formatter: function (data){
+                        if(data) {
+                            let link=Yunqi.formatter.link;
+                            link.value = data;
+                            return link;
+                        }else{
+                            return '';
+                        }
+                    }},
+                {field:"description",title:"简介",width: 300},
+                {
+                    field:"local",
+                    title:"本地",
+                    width: 80,
+                    formatter: function (data){
+                        let tag=Yunqi.formatter.tag;
+                        if(data){
+                            tag.value='是';
+                            tag.type='success';
+                        }else{
+                            tag.value='否';
+                            tag.type='danger';
+                        }
+                        return tag;
+                    }
+                },
+                {
+                    field:"packed",
+                    title:"打包",
+                    width: 80,
+                    formatter:function (data,row){
+                        if(!row.local){
+                            return '';
+                        }
+                        let tag=Yunqi.formatter.tag;
+                        if(data){
+                            tag.value='是';
+                            tag.type='primary';
+                        }else{
+                            tag.value='否';
+                            tag.type='danger';
+                        }
+                        return tag;
+                    }
+                },
+                {
+                    field:"open",
+                    title:"开放",
+                    width: 80,
+                    formatter:function (data,row){
+                        if(!row.local){
+                            return '';
+                        }
+                        let sw=Yunqi.formatter.switch;
+                        sw.activeValue=1;
+                        sw.inactiveValue=0;
+                        sw.value=row.packed?data:0;
+                        sw.disabled=true;
+                        if(row.packed){
+                            sw.disabled=false;
+                        }
+                        return sw;
+                    }
+                },
+                {
+                    field: 'operate',
+                    title: __('操作'),
+                    direction:'column',
+                    width:100,
+                    action:{
+                        download:{
+                            text:__('下载'),
+                            type:'primary',
+                            tooltip:false,
+                            icon:'fa fa-download',
+                            method:'downloadAddon',
+                            visible:function (row) {
+                                return !row.download;
+                            }
+                        },
+                        install:{
+                            text:__('安装'),
+                            type:'primary',
+                            tooltip:false,
+                            icon:'fa fa-wrench',
+                            method:'installAddon',
+                            visible:function (row) {
+                                return row.download && !row.install;
+                            }
+                        },
+                        pack:{
+                            text:__('打包'),
+                            type:'primary',
+                            tooltip:false,
+                            icon:'fa fa-briefcase',
+                            method:'packAddon',
+                            visible:function (row) {
+                                return row.download && row.install && !row.packed;
+                            }
+                        },
+                        edit:{
+                            text:__('编辑'),
+                            type:'warning',
+                            tooltip:false,
+                            icon:'fa fa-edit',
+                            method:'editAddon',
+                            visible:function (row) {
+                                return row.local && row.is_author && !row.packed;
+                            }
+                        },
+                        uninstall:{
+                            text:__('卸载'),
+                            type:'danger',
+                            tooltip:false,
+                            icon:'fa fa-remove',
+                            method:'uninstallAddon',
+                            visible:function (row) {
+                                return row.install && row.packed;
+                            }
+                        },
+                        remove:{
+                            text:__('删除'),
+                            type:'danger',
+                            tooltip:false,
+                            icon:'fa fa-remove',
+                            method:'delAddon',
+                            visible:function (row) {
+                                return row.download && !row.install;
+                            }
+                        },
+                    },
+                }
+            ],
+            buyDialog:{
+                show:false,
+                out_trade_no:'',
+                transaction_id:'',
+                expire_time:0,
+                status:0,
+                message:'',
+                code_url:'',
+                row:''
+            }
+        },
+        onLoad:function (){
+            setInterval(()=>{
+                this.checkPayStatus();
+            },2000)
+        },
+        methods:{
+            changePlain:function (plain) {
+                this.plain = plain;
+                this.$refs.yuntable.submit();
+            },
+            packAddon:function(row) {
+                Yunqi.ajax.post('addons/pack',row).then(res=>{
+                    this.$refs.yuntable.reload();
+                });
+            },
+            editAddon:function (row){
+                let that=this;
+                Yunqi.api.open({
+                    title: __('修改本地扩展'),
+                    url: 'addons/create?id='+row.id,
+                    icon:'fa fa-edit',
+                    close:function (r) {
+                        if(r){
+                            that.$refs.yuntable.reload();
+                        }
+                    }
+                });
+            },
+            downloadAddon:function(row){
+                if(row.price>0){
+                    this.buyDialog.out_trade_no=create_out_trade_no();
+                    this.buyDialog.code_url=Yunqi.config.baseUrl+'addons/payCode?key='+row.key+'&out_trade_no='+this.buyDialog.out_trade_no;
+                    this.buyDialog.show=true;
+                    this.buyDialog.row=row;
+                }else{
+                    Yunqi.ajax.post('addons/download', row).then(res=>{
+                        this.$refs.yuntable.reload();
+                    });
+                }
+            },
+            payDownload:function (){
+                let postdata={...this.buyDialog.row,transaction_id:this.buyDialog.transaction_id}
+                Yunqi.ajax.post('addons/download',postdata).then(res=>{
+                    this.closeBuyDialog();
+                    this.$refs.yuntable.reload();
+                });
+            },
+            checkPayStatus:function (){
+                if(this.buyDialog.show){
+                    Yunqi.ajax.get('addons/checkPayStatus',{out_trade_no:this.buyDialog.out_trade_no,key:this.buyDialog.row.key}).then(res=>{
+                        if(res){
+                            this.buyDialog.transaction_id=res.transaction_id;
+                            this.buyDialog.expire_time=res.expire_time;
+                            this.buyDialog.message='支付成功';
+                            this.buyDialog.status=1;
+                        }
+                    });
+                }
+            },
+            checkTransactionId:function (){
+                let postdata={
+                    pack:this.buyDialog.row.pack,
+                    transaction_id:this.buyDialog.transaction_id
+                };
+                Yunqi.ajax.get('addons/checkTransactionId',postdata).then(res=>{
+                    this.buyDialog.status=res.status;
+                    this.buyDialog.expire_time=res.expire_time;
+                    if(res.status==0){
+                        this.buyDialog.message='交易单号不存在';
+                    }else{
+                        if(res.expire_time>0){
+                            this.buyDialog.message=formatTime(res.expire_time)+'过期';
+                        }else{
+                            this.buyDialog.message='交易单号已经过期';
+                        }
+                    }
+                });
+            },
+            closeBuyDialog:function () {
+                this.buyDialog={
+                    show:false,
+                    out_trade_no:'',
+                    transaction_id:'',
+                    expire_time:0,
+                    status:0,
+                    message:'',
+                    code_url:'',
+                    row:''
+                }
+            },
+            installAddon:function(row)
+            {
+                Yunqi.ajax.post('addons/install',{key:row.key}).then(res=>{
+                    this.$refs.yuntable.reload();
+                });
+            },
+            uninstallAddon:function(row)
+            {
+                let that=this;
+                Yunqi.api.open({
+                    title: __('卸载扩展'),
+                    url: 'addons/uninstall?key='+row.key,
+                    icon:'fa fa-remove',
+                    close:function (r) {
+                        if(r){
+                            that.$refs.yuntable.reload();
+                        }
+                    }
+                });
+            },
+            delAddon:function (row){
+                let that=this;
+                Yunqi.confirm('删除扩展将会清空掉所有文件,重新下载扩展可能会再次收取费用,你确定要删除吗?','提醒').then(res=>{
+                    Yunqi.ajax.post('addons/del',{key:row.key}).then(res=>{
+                        that.$refs.yuntable.reload();
+                    });
+                });
+            },
+            createAddon:function(){
+                let that=this;
+                Yunqi.api.open({
+                    title: __('创建本地扩展'),
+                    url: 'addons/create',
+                    icon:'fa fa-plus',
+                    close:function (r) {
+                        if(r){
+                            that.$refs.yuntable.reload();
+                        }
+                    }
+                });
+            }
+        }
+    }
+</script>
+<style>
+    .install:hover{
+        background:var(--el-color-primary-light-9);
+        border:1px solid var(--el-color-primary-light-5);
+    }
+    .paycode{
+        text-align:center;
+    }
+    .message{
+        margin-top:5px;
+    }
+    .footer{
+        margin-top:15px;
+    }
+</style>

+ 61 - 0
app/admin/view/addons/uninstall.html

@@ -0,0 +1,61 @@
+<template>
+    <el-card shadow="never" style="border: 0;">
+        <yun-form
+                ref="yunform"
+                :columns="unistallField">
+            <template #content>
+                <el-form-item label="插件配置:">
+                    <template v-if="conf.length>0">
+                        <el-tag style="margin-right: 10px;" v-for="item in conf">{{item.title}}</el-tag>
+                    </template>
+                    <span v-else>无</span>
+                </el-form-item>
+                <el-form-item label="插件数据表:">
+                    <template v-if="tables.length>0">
+                        <div>
+                            <el-tag style="margin-right: 10px;" v-for="item in tables">{{item}}</el-tag><br>
+                        </div>
+                    </template>
+                    <span v-else>无</span>
+                </el-form-item>
+                <el-form-item label="插件菜单:">
+                    <el-tree v-if="menu.length>0" :default-expand-all="true" :props="{children:'childlist',label:'title'}" node-key="id" :data="menu" style="width: 100%;"></el-tree>
+                    <span v-else>无</span>
+                </el-form-item>
+            </template>
+            <template #footer="{step}">
+                <el-button size="large" style="width:100%" type="danger" @click="uninstall">确认卸载</el-button>
+            </template>
+        </yun-form>
+    </el-card>
+</template>
+<script>
+import form from "@components/Form.js";
+export default {
+    components:{'YunForm':form},
+    data:{
+        unistallField:[
+            {field:"name",title:"扩展名称",edit: {form:'input',type:'text',readonly:true,value:Yunqi.data.addon.name}},
+            {field:"key",title:"关键字",edit: {form:'input',type:'hidden',value:Yunqi.data.addon.key}},
+            {field:"actions",title:"卸载内容",edit:{form:'checkbox',value:['menu','config','tables']},searchList:{'menu':'菜单','config':'配置','tables':'数据表'}},
+            {field:"content",edit:'slot'},
+        ],
+        menu:Yunqi.data.menu,
+        conf:Yunqi.data.conf,
+        tables:Yunqi.data.tables
+    },
+    methods: {
+        uninstall:function (){
+            let key=this.$refs.yunform.getValue('key');
+            let actions=this.$refs.yunform.getValue('actions');
+            Yunqi.ajax.post('addons/uninstall',{key:key,actions:actions}).then(res=>{
+                Yunqi.api.closelayer(Yunqi.config.window.id,true);
+            });
+        },
+    }
+}
+</script>
+<style>
+
+</style>
+

+ 101 - 0
app/admin/view/auth/admin/add.html

@@ -0,0 +1,101 @@
+<template>
+    <el-card shadow="never" style="border: 0;">
+        <template #header v-if="action=='edit'">
+            <el-alert
+                    type="warning"
+                    show-icon>
+                非调试模式下,提交后请清空缓存才能生效!
+            </el-alert>
+        </template>
+        <yun-form
+                v-if="columns"
+                :data="data"
+                ref="yunform"
+                :columns="columns">
+            <template #default>
+                {:token_field()}
+            </template>
+            <template #groupids="{rows}" required>
+                <el-form-item label="{:__('所属组别')}:" prop="groupids">
+                    <el-tree-select
+                            v-model="rows.groupids"
+                            :data="groupdata"
+                            multiple
+                            check-strictly
+                            :default-expand-all="true"
+                            :props='{label:"name",children:"childlist",value:"id"}'
+                    >
+                    </el-tree-select>
+                </el-form-item>
+            </template>
+            <template #third_id="{rows}">
+                <el-form-item label="绑定微信:">
+                    <third :value="rows.third_id" :selectable="true" @change="changValue"></third>
+                </el-form-item>
+            </template>
+            <template #depart_id="{rows}">
+                <el-form-item label="{:__('所属部门')}:" required>
+                    <el-tree-select
+                            v-model="rows.depart_id"
+                            :data="departdata"
+                            check-strictly
+                            :default-expand-all="true"
+                            :props='{label:"name",children:"childlist",value:"id"}'
+                    >
+                    </el-tree-select>
+                </el-form-item>
+            </template>
+        </yun-form>
+    </el-card>
+</template>
+<script>
+    import form from "@components/Form.js";
+    import third from "@components/Third.js";
+    import {TreeIdtoString} from "@util.js";
+    export default{
+        components:{'YunForm':form,'Third':third},
+        data:{
+            action:Yunqi.config.action,
+            data:Yunqi.data.row || {},
+            groupdata:TreeIdtoString(Yunqi.data.groupdata),
+            departdata:TreeIdtoString(Yunqi.data.departdata),
+            disabled:false,
+            columns:''
+        },
+        onLoad:function (query){
+            let ids=query.ids;
+            this.columns=[
+                {field: 'id',title: __('ID'),edit:'hidden'},
+                {field: 'username', title: __('用户名'),edit:(ids==1)?'readonly':'text',rules:'required'},
+                {field: 'password', title: __('密码'),edit:'password',rules:(Yunqi.config.action=='add')?'required':''},
+                {field: 'nickname', title: __('昵称'),edit:'text',rules:'required'},
+                {field: 'mobile', title: __('手机号'),edit:'text',rules:'required;mobile'},
+                {
+                    field: 'groupids',
+                    title: __('所属组别'),
+                    edit:{form:'slot',value:[]},
+                    rules:'required'
+                },
+                {
+                    field: 'depart_id',
+                    title: __('所属部门'),
+                    edit:{form:'slot',value:[]},
+                    rules:'required'
+                },
+                {field: 'third_id', title: __('绑定微信'),edit:Yunqi.data.thirdLogin?'slot':false},
+                {field: 'status', title: __('状态'),searchList: {'normal': __('正常'),'hidden': __('隐藏')},edit:{form:'radio',value:'normal'}}
+            ];
+            if(ids==1){
+                this.disabled=true;
+            }
+        },
+        methods: {
+            changValue:function (e){
+                this.$refs.yunform.setValue('third_id',e);
+            }
+        }
+    }
+</script>
+<style>
+
+</style>

+ 1 - 0
app/admin/view/auth/admin/edit.html

@@ -0,0 +1 @@
+{include vue="auth/admin/add" /}

+ 180 - 0
app/admin/view/auth/admin/index.html

@@ -0,0 +1,180 @@
+<template>
+    <el-row>
+        <el-col :span="4" v-if="departdata[0].childlist.length>1">
+            <el-card shadow="never" style="width: 95%;">
+                <el-input placeholder="请输入部门名称" v-model="search" @input="handleSearch">
+                    <template #prepend>
+                        <i class="fa fa-search"></i>
+                    </template>
+                </el-input>
+                <el-scrollbar height="calc(100vh)">
+                <el-tree
+                    style="margin-top: 15px"
+                    ref="elTree"
+                    :data='departdata'
+                    :expand-on-click-node="false"
+                    :default-expand-all="true"
+                    :filter-node-method="filterNode"
+                    :props='{label:"name",children:"childlist",value:"id"}'
+                    @node-click="handleNodeClick"
+                >
+                </el-tree>
+                </el-scrollbar>
+            </el-card>
+        </el-col>
+        <el-col :span="departdata[0].childlist.length>1?20:24">
+            <el-card shadow="never">
+                <yun-table
+                        :columns="columns"
+                        toolbar="refresh,add,del"
+                        ref="yuntable"
+                        order="asc"
+                        :auth="{
+                            add:{:$auth->check('app\\admin\\controller\\auth\\Admin','add')},
+                            edit:{:$auth->check('app\\admin\\controller\\auth\\Admin','edit')},
+                            del:{:$auth->check('app\\admin\\controller\\auth\\Admin','del')},
+                            multi:{:$auth->check('app\\admin\\controller\\auth\\Admin','multi')},
+                        }"
+                        :extend="extend">
+                    <template #formatter="{field,rows}">
+                        <div v-if="field=='groupids'">
+                            <template v-for="item in rows.groupids">
+                                <el-tag :type="item.status=='normal'?'primary':'info'" effect="dark" style="margin-right: 5px;">{{item.name}}</el-tag>
+                            </template>
+                        </div>
+                        {if $thirdLogin}
+                        <div v-if="field=='third'">
+                            <el-tag effect="dark" v-if="rows.third" style="margin-right: 5px;">{{rows.third.openname}}</el-tag>
+                        </div>
+                        {/if}
+                    </template>
+                </yun-table>
+            </el-card>
+        </el-col>
+    </el-row>
+</template>
+<script>
+    import table from "@components/Table.js";
+    import {inArray} from "@util.js";
+    export default{
+        components:{'YunTable':table},
+        data:{
+            search:'',
+            departdata:Yunqi.data.departdata,
+            extend:{
+                index_url: 'auth/admin/index',
+                add_url: 'auth/admin/add',
+                edit_url: 'auth/admin/edit',
+                del_url: 'auth/admin/del',
+                multi_url: 'auth/admin/multi'
+            },
+            columns:[
+                {checkbox: true,selectable:function (row,index){
+                    let r=true;
+                    for(let i in row.groupids){
+                        if(inArray(Yunqi.data.groupids,row.groupids[i].id)){
+                            r=false;
+                        }
+                    }
+                    if(Yunqi.data.isSuperAdmin){
+                        r=true;
+                    }
+                    if(row.id==1){
+                        r=false;
+                    }
+                    return r;
+                }},
+                {field: 'id',title: __('ID'),width:80,operate:false},
+                {field: 'username', title: __('用户名'),operate:'like'},
+                {field: 'nickname', title: __('昵称'),operate:'like'},
+                {field: 'third', title: __('绑定微信'),operate:false,visible:Yunqi.data.thirdLogin?true:'none',formatter: Yunqi.formatter.slot},
+                {field: 'mobile', title: __('手机号')},
+                {
+                    field: 'groupids',
+                    title: __('所属组别'),
+                    formatter:Yunqi.formatter.slot,
+                    operate:false
+                },
+                {
+                    field: 'depart',
+                    formatter: function (data){
+                        return data?data.name:'';
+                    },
+                    title: __('所属部门'),
+                    operate: {form:'input',type:'hidden',filter:false}
+                },
+                {field: 'status', title: __('状态'),operate:false, searchList: {'normal': __('正常'),'hidden': __('隐藏')},formatter:function(data,row){
+                        let sw=Yunqi.formatter.switch;
+                        sw.activeValue='normal';
+                        sw.inactiveValue='hidden';
+                        sw.value=row.status;
+                        sw.disabled=false;
+                        for(let i in row.groupids){
+                            if(inArray(Yunqi.data.groupids,row.groupids[i].id)){
+                                sw.disabled=true;
+                            }
+                        }
+                        if(Yunqi.data.isSuperAdmin){
+                            sw.disabled=false;
+                        }
+                        if(row.id==1){
+                            sw.disabled=true;
+                        }
+                        return sw;
+                    }},
+                {
+                    field: 'operate',
+                    title: __('操作'),
+                    width:100,
+                    action:{
+                        edit:function(row){
+                            if(Yunqi.data.isSuperAdmin){
+                                return true;
+                            }
+                            for(let i in row.groupids){
+                                if(inArray(Yunqi.data.groupids,row.groupids[i].id)){
+                                    return false;
+                                }
+                            }
+                            return true;
+                        },
+                        del:function(row){
+                            if(row.id==1){
+                                return false;
+                            }
+                            if(Yunqi.data.isSuperAdmin){
+                                return true;
+                            }
+                            for(let i in row.groupids){
+                                if(inArray(Yunqi.data.groupids,row.groupids[i].id)){
+                                    return false;
+                                }
+                            }
+                            return true;
+                        }
+                    }
+                }
+            ]
+        },
+        methods: {
+            handleSearch:function (e){
+                this.$refs.elTree.filter();
+            },
+            filterNode:function (value,data){
+                return data.name.indexOf(this.search) !== -1;
+            },
+            handleNodeClick:function (e){
+                let columns=this.columns;
+                for(let i in columns){
+                    if(columns[i].field=='depart'){
+                        columns[i].operate.value=e.id;
+                    }
+                }
+                this.$refs.yuntable.reset();
+            }
+        }
+    }
+</script>
+<style>
+
+</style>

+ 32 - 0
app/admin/view/auth/adminlog/detail.html

@@ -0,0 +1,32 @@
+<template>
+    <el-table :data="detail" stripe border style="width: 100%">
+        <el-table-column prop="title" label="{:__('标题')}" width="180"></el-table-column>
+        <el-table-column prop="content" label="{:__('内容')}"></el-table-column>
+    </el-table>
+</template>
+<script>
+    export default {
+        data() {
+            return {
+                detail:[]
+            }
+        },
+        onShow:function () {
+            let detail=[];
+            let row=Yunqi.data.row;
+            for(let k in row){
+                detail.push({
+                    title:k,
+                    content:row[k]
+                });
+            }
+            this.detail=detail;
+        },
+        methods: {
+
+        }
+    }
+</script>
+<style>
+
+</style>

+ 67 - 0
app/admin/view/auth/adminlog/index.html

@@ -0,0 +1,67 @@
+<template>
+    <el-card shadow="never">
+        <template #header>
+            <el-alert effect="dark" :closable="false">管理员可以查看自己所拥有的权限的所有管理员的日志</el-alert>
+        </template>
+        <yun-table
+                :columns="columns"
+                toolbar="refresh,del"
+                :extend="extend">
+        </yun-table>
+    </el-card>
+</template>
+<script>
+    import table from "@components/Table.js";
+    export default {
+        components: {'YunTable':table},
+        data:{
+            extend:{
+                index_url: 'auth/adminlog/index',
+                del_url: 'auth/adminlog/del',
+                detail_url: 'auth/adminlog/detail',
+            },
+            columns:[
+                {checkbox: true},
+                {field: 'id',title:'ID',width:80,operate:false},
+                {field: 'username', title: __('用户名'),operate:'='},
+                {field: 'title', title: __('标题'), operate:'='},
+                {field: 'controller', title: __('控制器'), operate:'='},
+                {field: 'action', title: __('方法'), operate:'like'},
+                {field: 'url', title: __('访问链接'),operate:'like'},
+                {field: 'ip', title: __('IP'),width:140},
+                {field: 'createtime', title: __('创建时间'),sortable: true,width:150,operate:false},
+                {
+                    field: 'operate',
+                    fixed: 'right',
+                    title: __('操作'),
+                    width:140,
+                    action:{
+                        detail:{
+                            tooltip:false,
+                            type:'primary',
+                            icon:'fa fa-list',
+                            text:'详情',
+                            method:'showDetail'
+                        },
+                        del:true
+                    }
+                }
+            ],
+            detail:[]
+        },
+        methods: {
+            showDetail:function (rows){
+                Yunqi.api.open({
+                    title:'详情',
+                    height:450,
+                    width:900,
+                    icon:'fa fa-list',
+                    url:this.extend.detail_url+'?ids='+rows.id
+                });
+            }
+        }
+    }
+</script>
+<style>
+
+</style>

+ 47 - 0
app/admin/view/auth/depart/add.html

@@ -0,0 +1,47 @@
+<template>
+    <el-card shadow="never" style="border: 0;">
+        <yun-form
+            ref="yunform"
+            :data="data"
+            :columns="columns">
+            <template #default>
+                {:token_field()}
+            </template>
+            <template #pid="{rows}">
+                <el-form-item label="{:__('父级')}:" required>
+                    <el-tree-select
+                            v-model="rows.pid"
+                            :data="departdata"
+                            check-strictly
+                            :default-expand-all="true"
+                            :props='{label:"name",children:"childlist",value:"id"}'
+                    >
+                    </el-tree-select>
+                </el-form-item>
+            </template>
+        </yun-form>
+    </el-card>
+</template>
+<script>
+import form from "@components/Form.js";
+import {TreeIdtoString} from "@util.js";
+export default{
+    components:{'YunForm':form},
+    data:{
+        data:Yunqi.data.row || {},
+        departdata:TreeIdtoString(Yunqi.data.departdata),
+        columns:[
+            {field: 'id',title: __('ID'),edit:'hidden'},
+            {field: 'pid', title: __('父级'),edit:'slot'},
+            {field: 'name', title: __('名称'),edit:'text',rules:'required'},
+            {field: 'description', title: __('描述'),edit:'textarea'},
+            {field: 'status', title: __('状态'), edit:{form:'radio',value:'normal'},searchList: {'normal': __('正常'),'hidden': __('隐藏')}}
+        ]
+    },
+    methods: {
+
+    }
+}
+</script>
+<style>
+</style>

+ 1 - 0
app/admin/view/auth/depart/edit.html

@@ -0,0 +1 @@
+{include vue="auth/depart/add" /}

+ 71 - 0
app/admin/view/auth/depart/index.html

@@ -0,0 +1,71 @@
+<template>
+    <el-card shadow="never">
+        <yun-table
+            :columns="columns"
+            :common-search="false"
+            :pagination="false"
+            order="asc"
+            ref="yuntable"
+            :is-tree="true"
+            :tree-expand-all="true"
+            toolbar="refresh,add,del"
+            :auth="auth"
+            :extend="extend">
+        </yun-table>
+    </el-card>
+</template>
+<script>
+import table from "@components/Table.js";
+import auth from "@components/Auth.js";
+const doCheck=function (tree,checkKey){
+    tree.forEach(res=>{
+        checkKey.push(res.id);
+        if(res.children && res.children.length>0){
+            doCheck(res.children,checkKey);
+        }
+    });
+}
+export default{
+    components:{'YunTable':table,'Auth':auth},
+    data:{
+        auth:{
+            add:Yunqi.auth.check('app\\admin\\controller\\auth\\Depart','add'),
+            edit:Yunqi.auth.check('app\\admin\\controller\\auth\\Depart','edit'),
+            del:Yunqi.auth.check('app\\admin\\controller\\auth\\Depart','del'),
+            multi:Yunqi.auth.check('app\\admin\\controller\\auth\\Depart','multi'),
+            download:Yunqi.auth.check('app\\admin\\controller\\auth\\Depart','download'),
+        },
+        extend:{
+            index_url: 'auth/depart/index',
+            add_url: 'auth/depart/add',
+            edit_url: 'auth/depart/edit',
+            del_url: 'auth/depart/del',
+            multi_url: 'auth/depart/multi',
+            download_url: 'auth/depart/download',
+        },
+        columns:[
+            {checkbox: true},
+            {field: 'id',title: __('ID'),width:80},
+            {field: 'name', title: __('名称'),align:'left'},
+            {field: 'description', title: __('描述')},
+            {field: 'status', title: __('状态'),searchList: {'normal': __('正常'),'hidden': __('隐藏')},formatter:Yunqi.formatter.switch},
+            {treeExpand: true},
+            {
+                field: 'operate',
+                title: __('操作'),
+                width:150,
+                action:{
+                    edit:true,
+                    del:true
+                }
+            }
+        ]
+    },
+    methods: {
+
+    }
+}
+</script>
+<style>
+
+</style>

+ 128 - 0
app/admin/view/auth/group/add.html

@@ -0,0 +1,128 @@
+<template>
+    <el-card shadow="never" style="border: 0;">
+        <template #header v-if="action=='edit'">
+            <el-alert
+                    type="warning"
+                    show-icon>
+                非调试模式下,提交后请清空缓存才能生效!
+            </el-alert>
+        </template>
+        <yun-form
+            ref="yunform"
+            :data="data"
+            @submit="onSubmit"
+            :columns="columns">
+            <template #default>
+                {:token_field()}
+            </template>
+            <template #pid="{rows}">
+                <el-form-item label="{:__('父级')}:" required>
+                    <el-tree-select
+                            v-model="rows.pid"
+                            :data="groupdata"
+                            check-strictly
+                            :default-expand-all="true"
+                            @change="changePid"
+                            :props='{label:"name",children:"childlist",value:"id"}'
+                    >
+                    </el-tree-select>
+                </el-form-item>
+            </template>
+            <template #rules="item">
+                <el-form-item label="{:__('权限')}:" required>
+                    <div style="position: relative;left: 5px;">
+                        <el-checkbox v-model="checkAll">{:__('全部选中')}</el-checkbox>
+                        <el-checkbox v-model="expandAll">{:__('全部展开')}</el-checkbox>
+                    </div>
+                    <el-tree ref="tree" :props="{children:'childlist',label:'title'}" node-key="id" show-checkbox :default-checked-keys="checkedKey" :data="treedata" style="width: 100%; left: -18px;"></el-tree>
+                </el-form-item>
+            </template>
+        </yun-form>
+    </el-card>
+</template>
+<script>
+import form from "@components/Form.js";
+import {TreeIdtoString} from "@util.js";
+const doCheck=function (tree,checkKey){
+    tree.forEach(res=>{
+        checkKey.push(res.id);
+        if(res.children && res.children.length>0){
+            doCheck(res.children,checkKey);
+        }
+    });
+}
+export default{
+    components:{'YunForm':form},
+    data:{
+        action:Yunqi.config.action,
+        data:Yunqi.data.row || {pid:Yunqi.data.groupdata[0].id},
+        groupdata:TreeIdtoString(Yunqi.data.groupdata),
+        columns:[
+            {field: 'id',title: __('ID'),edit:'hidden'},
+            {field: 'pid', title: __('父级'),edit:'slot',rules:'required'},
+            {field: 'rules',title: __('权限'),edit:'slot'},
+            {field: 'auth_rules',edit:'hidden'},
+            {field: 'name', title: __('名称'),edit:'text',rules:'required'},
+            {field: 'status', title: __('状态'), edit:{form:'radio',value:'normal'},searchList: {'normal': __('正常'),'hidden': __('隐藏')}}
+        ],
+        checkAll:false,
+        expandAll:false,
+        checkedKey:[],
+        treedata:[]
+    },
+    onShow:function (){
+        if(Yunqi.config.action=='add'){
+            this.roletree(Yunqi.data.groupdata[0].id);
+        }
+        if(Yunqi.config.action=='edit'){
+            this.roletree(Yunqi.data.row.pid);
+            this.checkedKey = Yunqi.data.row.rules.split(',');
+        }
+    },
+    watch:{
+        checkAll:function (data){
+            if (data) {
+                let checkedKey = [];
+                doCheck(this.treedata, checkedKey);
+                this.checkedKey = checkedKey;
+            } else {
+                for(let i=0;i<this.$refs.tree.store._getAllNodes().length;i++){
+                    this.$refs.tree.store._getAllNodes()[i].checked = false;
+                }
+            }
+        },
+        expandAll:function (data) {
+            if (data) {
+                for(let i=0;i<this.$refs.tree.store._getAllNodes().length;i++){
+                    this.$refs.tree.store._getAllNodes()[i].expanded = true;
+                }
+            } else {
+                for(let i=0;i<this.$refs.tree.store._getAllNodes().length;i++){
+                    this.$refs.tree.store._getAllNodes()[i].expanded = false;
+                }
+            }
+        }
+    },
+    methods: {
+        changePid:function (pid){
+            this.roletree(pid);
+        },
+        roletree:function (pid){
+            Yunqi.ajax.get('auth/group/roletree',{groupid:pid},true,false,true).then(res=>{
+                this.treedata=res;
+            });
+        },
+        onSubmit:function (data){
+            let row=this.$refs.yunform.form_.data;
+            let s1=this.$refs.tree.getCheckedKeys();
+            let s2=this.$refs.tree.getHalfCheckedKeys();
+            row.rules=s1.join(',');
+            row.auth_rules=s2.concat(s1).join(',');
+            return true;
+        }
+    }
+}
+</script>
+<style>
+
+</style>

+ 1 - 0
app/admin/view/auth/group/edit.html

@@ -0,0 +1 @@
+{include vue="auth/group/add" /}

+ 91 - 0
app/admin/view/auth/group/index.html

@@ -0,0 +1,91 @@
+<template>
+    <el-card shadow="never">
+        <template #header>
+            <el-alert effect="dark" :closable="false" title="使用说明">角色组可以有多个,角色有上下级层级关系,如果子角色有角色组和管理员的权限则可以派生属于自己组别的下级角色组或管理员</el-alert>
+        </template>
+        <yun-table
+            :columns="columns"
+            :common-search="false"
+            :pagination="false"
+            order="asc"
+            ref="yuntable"
+            :is-tree="true"
+            :tree-expand-all="true"
+            toolbar="refresh,add,del"
+            :auth="auth"
+            :extend="extend">
+        </yun-table>
+    </el-card>
+</template>
+<script>
+import table from "@components/Table.js";
+import {inArray} from "@util.js";
+const doCheck=function (tree,checkKey){
+    tree.forEach(res=>{
+        checkKey.push(res.id);
+        if(res.children && res.children.length>0){
+            doCheck(res.children,checkKey);
+        }
+    });
+}
+export default{
+    components:{'YunTable':table},
+    data:{
+        auth:{
+            add:Yunqi.auth.check('app\\admin\\controller\\auth\\Group','add'),
+            edit:Yunqi.auth.check('app\\admin\\controller\\auth\\Group','edit'),
+            del:Yunqi.auth.check('app\\admin\\controller\\auth\\Group','del'),
+            multi:Yunqi.auth.check('app\\admin\\controller\\auth\\Group','multi'),
+        },
+        extend:{
+            index_url: 'auth/group/index',
+            add_url: 'auth/group/add',
+            edit_url: 'auth/group/edit',
+            del_url: 'auth/group/del',
+            multi_url: 'auth/group/multi'
+        },
+        columns:[
+            {checkbox: true,selectable:function (row,index){
+                if(inArray(Yunqi.data.groupids,row.id)){
+                    return false;
+                }
+                return true;
+            }},
+            {field: 'id',title: __('ID'),width:80},
+            {field: 'name', title: __('名称'),align:'left'},
+            {field: 'status', title: __('状态'),searchList: {'normal': __('正常'),'hidden': __('隐藏')},formatter:function(data,row){
+                let sw=Yunqi.formatter.switch;
+                sw.activeValue='normal';
+                sw.inactiveValue='hidden';
+                sw.value=row.status;
+                if(inArray(Yunqi.data.groupids,row.id)){
+                    sw.disabled=true;
+                }else{
+                    sw.disabled=false;
+                }
+                return sw;
+            }},
+            {treeExpand: true},
+            {
+                field: 'operate',
+                title: __('操作'),
+                width:150,
+                action:{
+                    edit:function(row){
+                        return !inArray(Yunqi.data.groupids,row.id);
+                    },
+                    del:function(row){
+                        return !inArray(Yunqi.data.groupids,row.id);
+                    }
+                }
+            }
+        ]
+    },
+    methods: {
+
+    }
+}
+</script>
+<style>
+
+</style>

+ 101 - 0
app/admin/view/auth/rule/add.html

@@ -0,0 +1,101 @@
+<template>
+    <el-card shadow="never" style="border: 0;">
+        <yun-form
+                :token="true"
+                :data="data"
+                ref="yunform"
+                :columns="eidtColumns">
+                <template #default>
+                    {:token_field()}
+                </template>
+            <template #pid="{rows}">
+                <el-form-item label="{:__('父级')}:" required>
+                    <el-tree-select
+                            placeholder="{:__('请选择父级')}"
+                            v-model="rows.pid"
+                            :data="ruledata"
+                            check-strictly
+                            :default-expand-all="true"
+                            :props='{label:"title",children:"childlist",value:"id"}'
+                    >
+                    </el-tree-select>
+                </el-form-item>
+            </template>
+            <template #icon="{rows}">
+                <el-form-item label="{:__('菜单图标')}:" prop="icon" v-if="rows.ismenu=='1'">
+                    <el-input v-model="rows.icon">
+                        <template #prepend><i :class="rows.icon"></i></template>
+                        <template #append>
+                            <el-button size="small" @click="openIconPanel">{:__('选择图标')}</el-button>
+                        </template>
+                    </el-input>
+                </el-form-item>
+            </template>
+        </yun-form>
+    </el-card>
+    <check-icon ref="checkicon" @selected="selectIcon"></check-icon>
+</template>
+<script>
+    import form from "@components/Form.js";
+    import checkicon from "@components/CheckIcon.js";
+    import {TreeIdtoString} from "@util.js";
+    function parseActions(row){
+        if(parseInt(row.ismenu)===1){
+            return row;
+        }
+        let action=JSON.parse(row.action);
+        let title=JSON.parse(row.title);
+        let actions={};
+        for(let key in action){
+            actions[action[key]]=title[key];
+        }
+        row.actions=actions;
+        return row;
+    }
+    export default{
+        components:{'YunForm':form,'CheckIcon':checkicon},
+        data:{
+            ruledata:TreeIdtoString(Yunqi.data.ruledata),
+            data:Yunqi.data.row?parseActions(Yunqi.data.row):{},
+            eidtColumns:[
+                {field: 'id',edit:'hidden'},
+                {field: 'ismenu',title: __('菜单'),edit: {form:'radio',value:'1'},searchList: {'1':__('是'),'0':__('否')}},
+                {field: 'pid',title: __('父级'),edit:'slot',rules:'required'},
+                {field: 'title', title: __('名称'),visible:function(row){return row.ismenu=='1'},edit:'text'},
+                {field: 'controller', title: __('控制器'),edit:'text'},
+                {field: 'action', title: __('方法'),edit:'text',visible:function(row){
+                    return row.ismenu=='1';
+                }},
+                {
+                    field: 'actions',
+                    title: __('方法'),
+                    edit: {
+                        form:'fieldlist',
+                        label:[__('方法名'),__('功能描述')],
+                        value:{index:__('查看'),add:__('添加'),edit:__('编辑'),multi:__('更新'),del:__('删除'),import:__('导入'),download:__('下载'),recyclebin:__('回收站')}
+                    },
+                    visible:function(row){
+                        return row.ismenu=='0';
+                    },
+                },
+                {field: 'menutype', title: __('类型'),visible:function(row){return row.ismenu=='1'},edit:{form:'radio',value:'tab'},searchList: Yunqi.data.menutypeList},
+                {field: 'icon', title: __('图标'),edit: {form:'slot',value: 'fa fa-th-large'}},
+                {field: 'extend', title: __('扩展属性'),visible:function(row){return row.ismenu=='1'},edit: {form:'input',type:'textarea',placeholder:'请输入菜单的扩展属性,格式为json'}},
+                {field: 'weigh', title: __('权重'),visible:function(row){return row.ismenu=='1'},edit:(Yunqi.config.action=='edit')?'number':false},
+                {field: 'status', title: __('状态'),visible:function(row){return row.ismenu=='1'},edit: {form:'radio',value:'normal'},searchList: {'normal': __('正常'),'hidden': __('隐藏')}}
+            ],
+            pageinit:false
+        },
+        methods: {
+            openIconPanel:function (){
+                this.$refs.checkicon.open();
+            },
+            selectIcon:function (i){
+                this.$refs.yunform.form_.data.icon=i;
+            }
+        }
+    }
+</script>
+<style>
+
+</style>

+ 1 - 0
app/admin/view/auth/rule/edit.html

@@ -0,0 +1 @@
+{include vue="auth/rule/add" /}

+ 122 - 0
app/admin/view/auth/rule/index.html

@@ -0,0 +1,122 @@
+<template>
+    <el-card shadow="never">
+        <template #header>
+            <el-alert effect="dark" :closable="false" title="使用说明">菜单规则包含两部分,(1)菜单(2)规则,菜单如果不存在子菜单,需要设置控制器与方法,表示点击菜单时访问位置。菜单仅提供显示功能,如果要设置访问位置的权限,需要继续配置规则。</el-alert>
+        </template>
+        <yun-table
+                :columns="indexColumns"
+                toolbar="refresh,add,edit,del,more"
+                @render="onRender"
+                ref="yuntable"
+                :is-tree="true"
+                :common-search="false"
+                :pagination="false"
+                :auth="auth"
+                :extend="extend">
+                <template #formatter="item">
+                    <div v-if="item.field=='icon'">
+                        <i :class="item.rows.icon"></i>
+                    </div>
+                </template>
+        </yun-table>
+    </el-card>
+</template>
+<script>
+    import table from "@components/Table.js";
+    export default{
+        components:{'YunTable':table},
+        data:{
+            auth:{
+                add:Yunqi.auth.check('app\\admin\\controller\\auth\\Rule','add'),
+                edit:Yunqi.auth.check('app\\admin\\controller\\auth\\Rule','edit'),
+                del:Yunqi.auth.check('app\\admin\\controller\\auth\\Rule','del'),
+                multi:Yunqi.auth.check('app\\admin\\controller\\auth\\Rule','multi'),
+                download:Yunqi.auth.check('app\\admin\\controller\\auth\\Rule','download'),
+            },
+            extend:{
+                index_url: 'auth/rule/index',
+                add_url: 'auth/rule/add',
+                edit_url: 'auth/rule/edit',
+                del_url: 'auth/rule/del',
+                multi_url: 'auth/rule/multi',
+                download_url: 'auth/rule/download',
+            },
+            indexColumns:[
+                {checkbox: true,selectable:function (row,index){
+                    if(!row.ismenu){
+                        return false;
+                    }
+                    return true;
+                }},
+                {field: 'id',title: __('ID'),width:80},
+                {field: 'title',expand:true,title: __('标题'),align:'left',formatter:function (data,row){
+                    if(row.ismenu){
+                        return data;
+                    }else{
+                        data=JSON.parse(data);
+                        return data.join(',');
+                    }
+                }},
+                {field: 'controller', title: __('控制器'),align:'left',formatter:function (data,row){
+                    if(!data){
+                        return '';
+                    }
+                    return data;
+                }},
+                {field: 'action', title: __('方法'),align:'left',formatter:function (data,row){
+                    if(!data){
+                        return '';
+                    }
+                    if(row.ismenu){
+                        return data;
+                    }else{
+                        data=JSON.parse(data);
+                        return data.join(',');
+                    }
+                }},
+                {field: 'icon',width:80, title: __('图标'),formatter:Yunqi.formatter.slot},
+                {field: 'ismenu',width:80, title: __('菜单'),formatter:function(data){
+                    if(data==1){
+                        return __('是');
+                    }
+                    return __('否');
+                }},
+                {field: 'isplatform', title: __('平台'),width:80,formatter: function(data,row){
+                    if(row.pid===0 && row.ismenu){
+                        let t=Yunqi.formatter.switch;
+                        t.value=data;
+                        return t;
+                    }
+                }},
+                {field: 'weigh', title: __('权重'),width:80},
+                {field: 'status', title: __('状态'),width:80,searchList: {'normal': __('正常'),'hidden': __('隐藏')},formatter: Yunqi.formatter.switch},
+                {treeExpand: true},
+                {
+                    field: 'operate',
+                    title: __('操作'),
+                    width:150,
+                    action:{sort:true,edit:true, del:true}
+                }
+            ]
+        },
+        methods: {
+            onRender:function (data){
+                if(Yunqi.config.action=='edit'){
+                    if(parseInt(data.ismenu)===0){
+                        let title=JSON.parse(data.title);
+                        let action=JSON.parse(data.action);
+                        let actions={};
+                        for(let i=0;i<title.length;i++){
+                            actions[action[i]]=title[i];
+                        }
+                        data.actions=actions;
+                        this.changeMenu(0);
+                    }
+                }
+            }
+        }
+    }
+</script>
+<style>
+
+</style>

+ 10 - 0
app/admin/view/common/meta.html

@@ -0,0 +1,10 @@
+<meta charset="utf-8">
+<title>{:site_config('basic.sitename')}</title>
+<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+<meta name="renderer" content="webkit">
+<meta name="referrer" content="never">
+<meta name="robots" content="noindex, nofollow">
+<link rel="shortcut icon" href="{:request()->domain()}/favicon.ico" />
+<link rel="stylesheet" href="{:request()->domain()}/assets/css/element-plus.css" />
+<link rel="stylesheet" href="{:request()->domain()}/assets/css/theme/dark.css" />
+<link rel="stylesheet" href="{:request()->domain()}/assets/libs/font-awesome/css/font-awesome.min.css" />

+ 41 - 0
app/admin/view/common/recyclebin.html

@@ -0,0 +1,41 @@
+<template>
+    <el-card shadow="never" style="border:0;">
+        <yun-table
+                :columns='recyclebin_("init")'
+                search="{$search}"
+                ref="yuntable"
+                :common-search="false"
+                :extend="extend"
+                toolbar="refresh,restore,destroy,restoreall,clear">
+            <template #toolbar="{tool,selections}">
+                <el-button v-if="tool=='restore'" type="success" @click.stop="recyclebin_('restore',selections)" :disabled="selections.length == 0"><i class="fa fa-rotate-left"></i>&nbsp;还原</el-button>
+                <el-button v-if="tool=='destroy'" type="danger" @click.stop="recyclebin_('destroy',selections)" :disabled="selections.length == 0"><i class="fa fa-remove"></i>&nbsp;彻底删除</el-button>
+                <el-button v-if="tool=='restoreall'" type="warning" @click.stop="recyclebin_('restoreall')"><i class="fa fa-rotate-left"></i>&nbsp;全部还原</el-button>
+                <el-button v-if="tool=='clear'" type="danger" @click.stop="recyclebin_('clear')"><i class="fa fa-remove"></i>&nbsp;全部清空</el-button>
+            </template>
+        </yun-table>
+    </el-card>
+</template>
+<script>
+    import table from "@components/Table.js";
+    export default{
+        components:{
+            'YunTable':table
+        },
+        data:{
+            extend:{
+                index_url:Yunqi.config.url,
+                recyclebin_url:'',
+            }
+        },
+        onLoad:function (){
+            this.extend.recyclebin_url = Yunqi.config.url.slice(0,Yunqi.config.url.indexOf('?'));
+        },
+        methods: {
+
+        }
+    }
+</script>
+<style>
+
+</style>

+ 462 - 0
app/admin/view/dashboard/index.html

@@ -0,0 +1,462 @@
+<template>
+    <div style="overflow: hidden;max-width: 100%;">
+        <el-row :gutter="10">
+            <el-col :md="18" :xs="24" :sm="24">
+                <div class="card-container">
+                    <el-card shadow="always" body-style="padding-bottom:10px;">
+                        <el-row>
+                            <el-col :md="6" :sm="12" :xs="12">
+                                <div class="style-1 bkcolor1">
+                                    <div class="box">
+                                        <div class="box-title">用户总数</div>
+                                        <div class="box-number">{{panel[0]}}</div>
+                                    </div>
+                                    <i class="fa fa-user-circle-o"></i>
+                                </div>
+                            </el-col>
+                            <el-col :md="6" :sm="12" :xs="12">
+                                <div class="style-1 bkcolor2">
+                                    <div class="box">
+                                        <div class="box-title">在线人数</div>
+                                        <div class="box-number">{{panel[1]}}</div>
+                                    </div>
+                                    <i class="fa fa-tasks"></i>
+                                </div>
+                            </el-col>
+                            <el-col :md="6" :sm="12" :xs="12">
+                                <div class="style-1 bkcolor3">
+                                    <div class="box">
+                                        <div class="box-title">月租用户</div>
+                                        <div class="box-number">{{panel[2]}}</div>
+                                    </div>
+                                    <i class="fa fa-dashcube"></i>
+                                </div>
+                            </el-col>
+                            <el-col :md="6" :sm="12" :xs="12">
+                                <div class="style-1 bkcolor4">
+                                    <div class="box">
+                                        <div class="box-title">数据表数量</div>
+                                        <div class="box-number">{{panel[3]}}</div>
+                                    </div>
+                                    <i class="fa fa-database"></i>
+                                </div>
+                            </el-col>
+                        </el-row>
+                    </el-card>
+                </div>
+                <div class="card-container">
+                    <el-card shadow="always">
+                        <template #header>
+                            <div class="header">
+                                <div class="title"><i class="fa fa-caret-right"></i>折线图</div>
+                            </div>
+                        </template>
+                        <div class="chart1" id="chart1"></div>
+                    </el-card>
+                </div>
+                <div class="card-container">
+                    <el-card shadow="always">
+                        <template #header>
+                            <div class="header">
+                                <div class="title"><i class="fa fa-caret-right"></i>表格</div>
+                                <div class="right-filter">
+                                    <el-button-group>
+                                        <el-button @click="changeForm('all')" size="small" :type="(filterForm.table=='all')?'primary':''">全部</el-button>
+                                        <el-button @click="changeForm('today')" size="small" :type="(filterForm.table=='today')?'primary':''">今日</el-button>
+                                        <el-button @click="changeForm('week')" size="small" :type="(filterForm.table=='week')?'primary':''">本周</el-button>
+                                        <el-button @click="changeForm('month')" size="small" :type="(filterForm.table=='month')?'primary':''">当月</el-button>
+                                    </el-button-group>
+                                </div>
+                            </div>
+                        </template>
+                        <el-table :data="table">
+                            <el-table-column label="排名" prop="sort"></el-table-column>
+                            <el-table-column label="会员" prop="name"></el-table-column>
+                            <el-table-column label="下单">
+                                <template #default="{row}">{{row.total}}笔</template>
+                            </el-table-column>
+                            <el-table-column label="金额">
+                                <template #default="{row}">¥{{row.money}}</template>
+                            </el-table-column>
+                        </el-table>
+                    </el-card>
+                </div>
+                <div class="card-container">
+                    <el-card shadow="always">
+                        <template #header>
+                            <div class="header">
+                                <div class="title"><i class="fa fa-caret-right"></i>柱状图</div>
+                                <div class="right-filter">
+                                    <el-form :model="filterForm">
+                                        <el-form-item label="统计时间" style="margin-bottom: 0;">
+                                            <el-select v-model="filterForm.select" style="margin-right: 10px;width: 150px;" @change="changeForm(0)">
+                                                <el-option label="第一项" value="one"></el-option>
+                                                <el-option label="第二项" value="two"></el-option>
+                                                <el-option label="第三项" value="three"></el-option>
+                                            </el-select>
+                                            <el-date-picker @change="changeForm(0)" v-model="filterForm.datepicker" style="width: 250px;" type="daterange" range-separator="到"></el-date-picker>
+                                        </el-form-item>
+                                    </el-form>
+                                </div>
+                            </div>
+                        </template>
+                        <div class="chart3" id="chart3"></div>
+                    </el-card>
+                </div>
+            </el-col>
+            <el-col :md="6" :xs="24" :sm="24">
+                <div class="card-container left">
+                    <el-card shadow="always">
+                        <div style="font-weight: bold;margin-bottom: 10px;">😀欢迎您,{:$auth->nickname}!</div>
+                        <el-alert type="success" :closable="false">行到水穷处,坐看云起时。在线乞讨公司,贵阳云起信息科技,跪求打赏😭</el-alert>
+                        <div class="pay">
+                            <img src="{:request()->domain()}/assets/img/pay.png">
+                        </div>
+                    </el-card>
+                </div>
+                <div class="card-container left">
+                    <el-card shadow="always">
+                        <template #header>
+                            <div class="header">
+                                <div class="title"><i class="fa fa-caret-right"></i>进度框样式</div>
+                            </div>
+                        </template>
+                        <el-row>
+                            <el-col :span="24">
+                                <div class="style-2">
+                                    <div class="box">
+                                        <el-progress type="circle" :percentage="order.percentage[0]"></el-progress>
+                                        <div class="box-title">销售目标</div>
+                                        <div class="box-number">{{order.count}}单/{{order.total}}单</div>
+                                    </div>
+                                </div>
+                            </el-col>
+                            <el-col :span="24">
+                                <div class="style-3">
+                                    <div class="box">
+                                        <div class="box-title">今日销售额</div>
+                                        <div class="box-content">
+                                            <div class="box-content-left">
+                                                <div class="box-number-top">¥{{order.today}}</div>
+                                                <div class="box-number-bottom">昨日销售额:¥{{order.yestoday}}</div>
+                                            </div>
+                                            <div class="icon bkcolor3" v-if="order.percentage[1]<50">
+                                                <i class="fa fa-arrow-down"></i>
+                                            </div>
+                                            <div class="icon bkcolor2" v-if="order.percentage[1]>=50 && order.percentage[1]<100">
+                                                <i class="fa fa-arrow-down"></i>
+                                            </div>
+                                            <div class="icon bkcolor4" v-if="order.percentage[1]>=100">
+                                                <i class="fa fa-arrow-up"></i>
+                                            </div>
+                                        </div>
+                                        <el-progress v-if="order.percentage[1]<50" :percentage="order.percentage[1]" color="#F56C6C"></el-progress>
+                                        <el-progress v-if="order.percentage[1]>=50 && order.percentage[1]<100" :percentage="order.percentage[1]" color="#E6A23C"></el-progress>
+                                        <el-progress v-if="order.percentage[1]>=100" :percentage="order.percentage[1]" color="#45991b"></el-progress>
+                                    </div>
+                                </div>
+                            </el-col>
+                        </el-row>
+                    </el-card>
+                </div>
+                <div class="card-container left">
+                    <el-card shadow="always">
+                        <template #header>
+                            <div class="header">
+                                <div class="title"><i class="fa fa-caret-right"></i>饼状图</div>
+                            </div>
+                        </template>
+                        <div class="chart2" id="chart2"></div>
+                    </el-card>
+                </div>
+            </el-col>
+        </el-row>
+    </div>
+</template>
+<script>
+    export default{
+        data:{
+            echarts:'',
+            panel:[],
+            line:{
+                date:[],
+                data:[]
+            },
+            table:[],
+            bar:{
+                date:[],
+                name:[],
+                data:[]
+            },
+            pie:[],
+            order:{
+                percentage:[0,0]
+            },
+            filterForm:{
+                table:'all',
+                select:'one',
+                datepicker:['2023-01-01','2023-02-01'],
+            }
+        },
+        onLoad:function (){
+            Yunqi.use('/assets/js/echarts.min.js').then(res=>{
+                this.echarts=res;
+                this.parseData();
+            });
+        },
+        methods:{
+            parseData:function (){
+                Yunqi.ajax.get('dashboard/index',{}).then(res=>{
+                    this.panel=res.panel;
+                    this.line=res.line;
+                    this.table=res.table;
+                    this.bar=res.bar;
+                    this.pie=res.pie;
+                    this.order=res.order;
+                    this.chart1();
+                    this.chart2();
+                    this.chart3();
+                });
+            },
+            chart1:function () {
+                let mychart = this.echarts.init(document.getElementById('chart1'), 'walden');
+                mychart.setOption({
+                    title: {text: '每日新增用户数',left: 'center'},
+                    tooltip: {
+                        trigger: 'axis'
+                    },
+                    toolbox: {
+                        show: false,
+                        feature: {
+                            magicType: {show: true, type: ['stack', 'tiled']},
+                            saveAsImage: {show: true}
+                        }
+                    },
+                    xAxis: {
+                        type: 'category',
+                        boundaryGap: false,
+                        data: this.line.date
+                    },
+                    yAxis: {},
+                    grid: [{
+                        left: 40,
+                        top: 40,
+                        right: 0,
+                        bottom:30
+                    }],
+                    series: [{
+                        name: '注册用户',
+                        type: 'line',
+                        smooth: true,
+                        areaStyle: {
+                            normal: {}
+                        },
+                        lineStyle: {
+                            normal: {
+                                width: 1.5
+                            }
+                        },
+                        data: this.line.data
+                    }]
+                });
+                window.addEventListener('resize',()=>{
+                    mychart.resize();
+                });
+            },
+            chart2:function (){
+                let mychart = this.echarts.init(document.getElementById('chart2'))
+                mychart.setOption({
+                    title: {text: '消费比例图',left: 'center'},
+                    legend: {
+                        orient: 'horizontal',
+                        bottom: 0,
+                    },
+                    series: [{
+                        type: 'pie',
+                        data:this.pie,
+                        label: {
+                            normal: {
+                                show: true,
+                                formatter: "¥{c}",
+                            }
+                        }
+                    }]
+                });
+                window.addEventListener('resize',()=>{
+                    mychart.resize();
+                });
+            },
+            chart3:function (){
+                let mychart = this.echarts.init(document.getElementById('chart3'))
+                mychart.setOption({
+                    title: {text: '消费日历图',left: 'center'},
+                    legend: {
+                        orient: 'vertical',
+                        left: 'left',
+                    },
+                    yAxis: {},
+                    xAxis: {
+                        data: this.bar.date
+                    },
+                    grid: [{
+                        left: 100,
+                        top: 40,
+                        right: 40,
+                        bottom:20
+                    }],
+                    series: [
+                        {
+                            type: 'bar',
+                            name:this.bar.name[0],
+                            data: this.bar.data[0]
+                        },
+                        {
+                            type: 'bar',
+                            name:this.bar.name[1],
+                            data: this.bar.data[1]
+                        },
+                        {
+                            type: 'bar',
+                            name:this.bar.name[2],
+                            data: this.bar.data[2]
+                        }
+                    ]
+                });
+                window.addEventListener('resize',()=>{
+                    mychart.resize();
+                });
+            },
+            changeForm:function (type){
+                if(type){
+                    this.filterForm.table=type;
+                }
+                this.parseData();
+            }
+        }
+    }
+</script>
+<style>
+.pay{
+    text-align: center;
+    padding: 10px;
+}
+.pay img{
+    width: 180px;
+    height: 180px;
+}
+.card-container{
+    margin-bottom: 10px;
+}
+.card-container.left{
+    padding-right: 10px;
+}
+@media screen and (max-width: 992px) {
+    .card-container.left{
+        padding-right: 0;
+    }
+}
+.card-container .el-card__header{
+    padding: 8px 20px;
+}
+.card-container .header{
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+.card-container .el-card__header .title{
+    font-weight: bold;
+    font-size: 14px;
+    display: flex;
+    align-items: center;
+}
+.card-container .el-card__header .title i{
+    font-size: 22px;
+    color: var(--el-color-primary);
+    margin-right: 8px;
+}
+.style-1{
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 10px;
+    border-radius: 6px;
+    width: 87%;
+    margin:0 auto 10px;
+    color: #fff;
+}
+.style-1 i{
+    font-size: 42px;
+    color: #fff;
+}
+.style-2{
+    padding: 10px;
+    width: 87%;
+    margin:0 auto;
+    text-align: center;
+}
+.style-3{
+    padding: 10px;
+    width: 87%;
+    margin:0 auto;
+    text-align: center;
+}
+.style-3 .box-title{
+    text-align: left;
+}
+.style-3 .box{
+    height: 190px;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-around;
+}
+.style-3 .box-content-left{
+    text-align: left;
+}
+.style-3 .box-number-top{
+    font-size: 26px;
+}
+.style-3 .box-number-bottom{
+    color: darkgrey;
+}
+.style-3 .box-content{
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+.style-3 .icon{
+    color: #fff;
+    width: 60px;
+    height: 60px;
+    line-height: 60px;
+    text-align: center;
+    font-size: 32px;
+}
+.box .box-title{
+    font-size: 18px;
+}
+.bkcolor1{
+    background: linear-gradient(to right,var(--el-color-primary-light-3),var(--el-color-primary));
+}
+.bkcolor2{
+    background: linear-gradient(to right,var(--el-color-warning-light-3),var(--el-color-warning));
+}
+.bkcolor3{
+    background: linear-gradient(to right,var(--el-color-danger-light-3),var(--el-color-danger));
+}
+.bkcolor4{
+    background: linear-gradient(to right,var(--el-color-success-light-3),var(--el-color-success));
+}
+.chart1{
+    width: 100%;
+    height: 305px;
+}
+.chart2{
+    width: 100%;
+    height: 310px;
+    margin: 0 auto;
+}
+.chart3{
+    width: 100%;
+    height: 300px;
+}
+</style>

+ 18 - 0
app/admin/view/dashboard/platform1.html

@@ -0,0 +1,18 @@
+<template>
+    <el-card shadow="never">
+        平台一首页
+    </el-card>
+</template>
+<script>
+    export default{
+        data:{
+
+        },
+        methods: {
+
+        }
+    }
+</script>
+<style>
+
+</style>

+ 18 - 0
app/admin/view/dashboard/platform2.html

@@ -0,0 +1,18 @@
+<template>
+    <el-card shadow="never">
+        平台二首页
+    </el-card>
+</template>
+<script>
+    export default{
+        data:{
+
+        },
+        methods: {
+
+        }
+    }
+</script>
+<style>
+
+</style>

+ 45 - 0
app/admin/view/develop/addQueue.html

@@ -0,0 +1,45 @@
+<template>
+    <el-card shadow="never" style="border: 0;">
+        <yun-form
+            ref="yunform"
+            :columns="columns">
+            <template #default>
+                {:token_field()}
+            </template>
+            <template #filter="{rows}">
+                <el-form-item label="{:__('规则限制')}:">
+                    <el-alert type="warning">
+                        规则限制表示在执行任务时,只有满足规则限制的条件才会执行,否则跳过执行,如【日为10,时为12】表示该任务只有在日期为10号,时间在12:00:00~12:59:59才会执行,规则的优先级高于间隔时间。
+                    </el-alert>
+                    <Fieldlist :value="{年:'',月:'',日:'',时:'',分:'',秒:''}" @change="changeFilter"></Fieldlist>
+                </el-form-item>
+            </template>
+        </yun-form>
+    </el-card>
+</template>
+<script>
+    import fieldlist from '@components/Fieldlist.js';
+    import form from "@components/Form.js";
+    let inter;
+    export default{
+        components:{'Fieldlist':fieldlist,'YunForm':form},
+        data:{
+            columns:[
+                {"field":"title","title":__("任务名称"),"edit":"text","rules":"required"},
+                {"field":"function","title":__("处理类"),"edit":"text","rules":"required"},
+                {"field":"limit","title":__("限制次数"),"edit":{form:'input',type:'number',placeholder:'任务执行的次数,0为循环无限执行'},"rules":"required;range(0~)"},
+                {"field":"filter","title":__("规则限制"),"edit":"slot"},
+                {"field":"delay","title":__("间隔时间"),"edit":{form:'input',type:'number',placeholder:'两次执行间隔时间,0为立即执行',append:'秒'},"rules":"required;range(0~)"},
+                {"field":"status","title":__("状态"),"edit":"switch","searchList":{"normal":"正常","hidden":"隐藏"},"formatter":Yunqi.formatter.switch},
+            ]
+        },
+        methods: {
+            changeFilter:function (e){
+                this.$refs.yunform.setValue('filter',e);
+            }
+        }
+    }
+</script>
+<style>
+
+</style>

+ 1861 - 0
app/admin/view/develop/crud.html

@@ -0,0 +1,1861 @@
+<template>
+    <el-card shadow="never">
+        <el-form :model="crudForm" label-width="120px">
+            <el-row :gutter="20">
+                <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                    <el-form-item label="{:__('数据表')}:">
+                        <select-page url="develop/getTable" key-field="name" label-field="title" @change="changeTable"></select-page>
+                    </el-form-item>
+                </el-col>
+                <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                    <el-form-item label="{:__('控制器')}:">
+                        <el-input :disabled="!crudForm.table" placeholder="{:__('请输入控制器')}" v-model="crudForm.controller"></el-input>
+                    </el-form-item>
+                </el-col>
+                <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                    <el-form-item label="{:__('数据模型')}:">
+                        <el-input :disabled="!crudForm.table" placeholder="{:__('请输入数据模型')}" v-model="crudForm.model"></el-input>
+                    </el-form-item>
+                </el-col>
+                <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                    <el-form-item label="{:__('代码风格')}:">
+                        <el-radio-group v-model="crudForm.reduced">
+                            <el-radio :label="true">干净简洁版</el-radio>
+                            <el-radio :label="false">带功能描述且注释版</el-radio>
+                        </el-radio-group>
+                    </el-form-item>
+                </el-col>
+                <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12" v-if="crudForm.actionList">
+                    <el-form-item label="{:__('操作功能')}:">
+                        <field-list @change="changeAction" :label="[__('方法名'),__('功能描述')]" :value="crudForm.actionList"></field-list>
+                    </el-form-item>
+                </el-col>
+            </el-row>
+            <el-form-item>
+                <el-divider>
+                    <el-checkbox-group v-model="actions">
+                    <el-checkbox label="table"><span style="font-weight: bolder;">{:__('配置表格')}</span></el-checkbox>
+                    </el-checkbox-group>
+                </el-divider>
+            </el-form-item>
+            <template v-if="crudForm.table && inArray(actions,'table')">
+                <el-form-item label="{:__('表格列表')}:">
+                    <el-table v-if="tableData" :data="tableData" border style="width: 100%">
+                        <el-table-column prop="field" label="{:__('字段')}" width="150" fixed="left"></el-table-column>
+                        <el-table-column label="{:__('标题')}" width="150">
+                            <template #default="{row}">
+                                <el-input v-model="row.title"></el-input>
+                            </template>
+                        </el-table-column>
+                        <el-table-column label="{:__('展示状态')}" width="150">
+                            <template #default="{row}">
+                                <el-select v-model="row.visible" @change="parseSearchList(row)">
+                                    <el-option label="展示" :value="true"></el-option>
+                                    <el-option label="不展示" value="none"></el-option>
+                                    <el-option label="展示关联表" value="relation"></el-option>
+                                    <el-option label="默认隐藏" :value="false"></el-option>
+                                </el-select>
+                            </template>
+                        </el-table-column>
+                        <el-table-column label="{:__('展示样式')}" width="150">
+                            <template #default="{row}">
+                                <el-select v-model="row.formatter" @change="parseFields(row)" v-if="row.visible!='none'">
+                                    <el-option v-for="(label,key) in formatter" :label="label" :key="key" :value="key"></el-option>
+                                </el-select>
+                            </template>
+                        </el-table-column>
+                        <el-table-column label="{:__('过滤方式')}" width="300">
+                            <template #default="{row}">
+                                <el-input v-model="row.operate" v-if="row.visible!='none'">
+                                    <template #append>
+                                        <el-button size="small" type="primary" @click="showOperate(row)">编辑</el-button>
+                                    </template>
+                                </el-input>
+                            </template>
+                        </el-table-column>
+                        <el-table-column label="{:__('选择项')}" width="300">
+                            <template #default="{row}">
+                                <el-input v-model="row.searchList" v-if="row.visible!='none'">
+                                    <template #append>
+                                        <el-button size="small" type="primary" @click="showSearchList(row)">编辑</el-button>
+                                    </template>
+                                </el-input>
+                            </template>
+                        </el-table-column>
+                        <el-table-column label="{:__('关联表')}" width="300">
+                            <template #default="{row}">
+                                <el-input v-model="row.relation" v-if="row.visible=='relation'">
+                                    <template #append>
+                                        <el-button size="small" type="primary" @click="showRelation(row)">编辑</el-button>
+                                    </template>
+                                </el-input>
+                            </template>
+                        </el-table-column>
+                        <el-table-column label="{:__('允许排序')}" width="100">
+                            <template #default="{row}">
+                                <el-checkbox-group v-model="row.sortable" v-if="row.visible!='none'">
+                                    <el-checkbox label="是"></el-checkbox>
+                                </el-checkbox-group>
+                            </template>
+                        </el-table-column>
+                        <el-table-column label="{:__('快速搜索')}" width="100">
+                            <template #default="{row}">
+                                <el-checkbox-group v-model="row.search" v-if="row.visible!='none'">
+                                    <el-checkbox label="是"></el-checkbox>
+                                </el-checkbox-group>
+                            </template>
+                        </el-table-column>
+                        <el-table-column label="{:__('回收站')}" width="100" v-if="crudForm.recyclebin">
+                            <template #default="{row}">
+                                <el-checkbox-group v-model="row.recyclebin" v-if="row.visible!='none'">
+                                    <el-checkbox label="是"></el-checkbox>
+                                </el-checkbox-group>
+                            </template>
+                        </el-table-column>
+                    </el-table>
+                </el-form-item>
+                <el-row :gutter="20">
+                    <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
+                        <el-form-item label="{:__('顶部分栏')}:">
+                            <el-select v-model="crudForm.tabs" style="width: 100%">
+                                <el-option label="不分栏显示" value=""></el-option>
+                                <template v-for="xtable in tableData" :key="xtable.field">
+                                    <el-option v-if="xtable.searchList" :label="xtable.title" :value="xtable.field"></el-option>
+                                </template>
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
+                        <el-form-item label="{:__('不要分页')}:">
+                            <label class="checklabel">
+                                <input type="checkbox" v-model="crudForm.pagination"/>
+                                <span>{:__('是')}</span>
+                            </label>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
+                        <el-form-item label="{:__('展示统计')}:">
+                            <label class="checklabel">
+                                <input type="checkbox" v-model="crudForm.summary"/>
+                                <span>{:__('是')}</span>
+                            </label>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
+                        <el-form-item label="{:__('展示扩展')}:">
+                            <label class="checklabel">
+                                <input type="checkbox" v-model="crudForm.expand"/>
+                                <span>{:__('是')}</span>
+                            </label>
+                        </el-form-item>
+                    </el-col>
+                    <template v-if="havaPid()">
+                        <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
+                            <el-form-item label="{:__('树形表格')}:">
+                                <label class="checklabel">
+                                    <input type="checkbox" v-model="crudForm.isTree"/>
+                                    <span>{:__('是')}</span>
+                                </label>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6" v-if="crudForm.isTree">
+                            <el-form-item label="{:__('树形表格标题')}:">
+                                <el-select v-model="crudForm.treeTitle" style="width: 100%">
+                                    <template v-for="xtable in tableData" :key="xtable.field">
+                                        <el-option :label="xtable.title" :value="xtable.field"></el-option>
+                                    </template>
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                    </template>
+                </el-row>
+            </template>
+            <el-form-item>
+                <el-divider>
+                    <el-checkbox-group v-model="actions">
+                        <el-checkbox label="form"><span style="font-weight: bolder;">{:__('配置表单')}</span></el-checkbox>
+                    </el-checkbox-group>
+                </el-divider>
+            </el-form-item>
+            <template v-if="crudForm.table && inArray(actions,'form')">
+                <el-form-item label="{:__('表单列表')}:">
+                    <el-table v-if="tableData" :data="tableData" border style="width: 100%">
+                        <el-table-column prop="field" label="{:__('字段')}" width="150" fixed="left"></el-table-column>
+                        <el-table-column label="{:__('标题')}" width="150">
+                            <template #default="{row}">
+                                <el-input v-model="row.title"></el-input>
+                            </template>
+                        </el-table-column>
+                        <el-table-column label="{:__('表单')}" width="300">
+                            <template #default="{row}">
+                                <el-input v-model="row.edit">
+                                    <template #append>
+                                        <el-button size="small" type="primary" @click="showFormDialog(row)">编辑</el-button>
+                                    </template>
+                                </el-input>
+                            </template>
+                        </el-table-column>
+                        <el-table-column label="{:__('验证')}" width="300">
+                            <template #default="{row}">
+                                <el-input v-model="row.rules" placeholder="多项验证用“;”隔开" v-if="isShowEdit(row)"></el-input>
+                            </template>
+                        </el-table-column>
+                        <el-table-column label="{:__('选择项')}" width="300">
+                            <template #default="{row}">
+                                <el-input v-model="row.searchList" v-if="isShowEdit(row)">
+                                    <template #append>
+                                        <el-button size="small" type="primary" @click="showSearchList(row)">编辑</el-button>
+                                    </template>
+                                </el-input>
+                            </template>
+                        </el-table-column>
+                    </el-table>
+                </el-form-item>
+                <template v-if="havaPid()">
+                    <el-row>
+                        <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
+                            <el-form-item label="{:__('树形结构')}:">
+                                <label class="checklabel">
+                                    <input type="checkbox" v-model="crudForm.isTree"/>
+                                    <span>{:__('是')}</span>
+                                </label>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6" v-if="crudForm.isTree">
+                            <el-form-item label="{:__('树形结构标题')}:">
+                                <el-select v-model="crudForm.treeTitle" style="width: 100%">
+                                    <template v-for="xtable in tableData" :key="xtable.field">
+                                        <el-option :label="xtable.title" :value="xtable.field"></el-option>
+                                    </template>
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </template>
+            </template>
+            <el-form-item>
+                <el-button :disabled="!crudForm.table || !crudForm.controller || !crudForm.model" type="primary" @click="submit('code')"><i class="fa fa-code"></i>&nbsp;生成代码</el-button>
+                <el-button :disabled="!crudForm.table || !crudForm.controller || !crudForm.model" type="primary" @click="submit('file')"><i class="fa fa-file"></i>&nbsp;生成文件</el-button>
+                <el-button :disabled="!crudForm.table || !crudForm.controller || !crudForm.model" type="danger" @click="clear"><i class="fa fa-remove"></i>&nbsp;清除文件</el-button>
+            </el-form-item>
+        </el-form>
+    </el-card>
+    <el-dialog
+        v-model="operateDialog.show"
+        title="{:__('编辑过滤方式')}"
+        width="800">
+        <el-scrollbar height="400px">
+            <el-form label-width="100px">
+                <el-form-item label="{:__('过滤简写')}:">
+                    <el-select v-model="operateDialog.data.short" @change="changeShort('table')" style="width:100%">
+                        <el-option v-for="item in short.table" :key="item.key" :value="item.key">
+                            <span>{{item.key}}</span>
+                            <span style="float: right;font-size: 13px;color: #a2a2a2">{{item.label}}</span>
+                        </el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="{:__('表单类型')}:">
+                    <el-select v-model="operateDialog.data.form" @change="changeForm('table')" style="width: 100%">
+                        <el-option v-for="item in formtype.form" :key="item.key" :value="item.key">
+                            <span>{{item.key}}</span>
+                            <span style="float: right;font-size: 13px;color: #a2a2a2">{{item.label}}</span>
+                        </el-option>
+                    </el-select>
+                </el-form-item>
+                <template v-if="operateDialog.data.form=='input'">
+                    <el-form-item label="{:__('文本类型')}:">
+                        <el-select v-model="operateDialog.data.type" style="width: 100%">
+                            <el-option key="text" value="text">
+                                <span>text</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('输入文本')}</span>
+                            </el-option>
+                            <el-option key="number" value="number">
+                                <span>number</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('输入数字')}</span>
+                            </el-option>
+                            <el-option key="password" value="password">
+                                <span>password</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('输入密码')}</span>
+                            </el-option>
+                            <el-option key="color" value="color">
+                                <span>color</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('输入颜色')}</span>
+                            </el-option>
+                        </el-select>
+                    </el-form-item>
+                </template>
+                <template v-if="operateDialog.data.form=='date-picker'">
+                    <el-form-item label="{:__('日期类型')}:">
+                        <el-select v-model="operateDialog.data.type" style="width: 100%" @change="changeForm('table')">
+                            <el-option key="date" value="date">
+                                <span>date</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('选择日期')}</span>
+                            </el-option>
+                            <el-option key="datetime" value="datetime">
+                                <span>datetime</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('选择日期+时间')}</span>
+                            </el-option>
+                            <el-option key="daterange" value="daterange">
+                                <span>daterange</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('选择日期区间')}</span>
+                            </el-option>
+                        </el-select>
+                    </el-form-item>
+                </template>
+                <template v-if="operateDialog.data.form=='time-picker'">
+                    <el-form-item label="{:__('时间类型')}:">
+                        <el-select v-model="operateDialog.data.type" style="width: 100%" @change="changeForm('table')">
+                            <el-option key="time" value="time">
+                                <span>time</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('选择时间')}</span>
+                            </el-option>
+                            <el-option key="timerange" value="timerange">
+                                <span>timerange</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('选择时间区间')}</span>
+                            </el-option>
+                        </el-select>
+                    </el-form-item>
+                </template>
+                <template v-if="operateDialog.data.form=='input'">
+                    <el-form-item label="{:__('前置内容')}:">
+                        <el-input v-model="operateDialog.data.prepend"></el-input>
+                    </el-form-item>
+                    <el-form-item label="{:__('后置内容')}:">
+                        <el-input v-model="operateDialog.data.append"></el-input>
+                    </el-form-item>
+                </template>
+                <template v-if="operateDialog.searchList && (operateDialog.data.form=='checkbox' || operateDialog.data.form=='radio' || operateDialog.data.form=='select')">
+                    <el-form-item label="{:__('选择项')}:">
+                        <field-list field="operate" @change="changeSearchList" :value="operateDialog.searchList"></field-list>
+                    </el-form-item>
+                </template>
+                <template v-if="inArray(['input','select','date-picker','time-picker','cascader','selectpage'],operateDialog.data.form)">
+                    <el-form-item label="{:__('提示文字')}:">
+                        <el-input v-model="operateDialog.data.placeholder" placeholder="{:__('不填默认显示为字段标题')}"></el-input>
+                    </el-form-item>
+                </template>
+                <template v-if="operateDialog.data.form!='hidden'">
+                    <el-form-item label="{:__('表单尺寸')}:">
+                        <el-select v-model="operateDialog.data.size" style="width: 100%">
+                            <el-option label="{:__('大')}" key="large" value="large"></el-option>
+                            <el-option label="{:__('中')}" key="default" value="default"></el-option>
+                            <el-option label="{:__('小')}" key="small" value="small"></el-option>
+                        </el-select>
+                    </el-form-item>
+                </template>
+                <template v-if="operateDialog.data.filter!='IS NULL' && operateDialog.data.filter!='IS NOT NULL'">
+                    <el-form-item label="{:__('默认值')}:">
+                        <el-input v-model="operateDialog.data.value"></el-input>
+                    </el-form-item>
+                </template>
+                <template v-if="operateDialog.data.form=='cascader'">
+                    <el-form-item label="{:__('数据获取方式')}:">
+                        <el-select v-model="operateDialog.data.cascaderType" style="width: 100%">
+                            <el-option label="{:__('JSON数据')}" key="options" value="options"></el-option>
+                            <el-option label="{:__('网络获取')}" key="url" value="url"></el-option>
+                        </el-select>
+                    </el-form-item>
+                    <template v-if="operateDialog.data.cascaderType=='url'">
+                        <el-form-item label="{:__('请求地址')}:">
+                            <el-input v-model="operateDialog.data.url"></el-input>
+                        </el-form-item>
+                        <el-form-item label="{:__('获取级数')}:">
+                            <el-input v-model="operateDialog.data.level"></el-input>
+                        </el-form-item>
+                    </template>
+                    <template v-if="operateDialog.data.cascaderType=='options'">
+                        <el-form-item label="{:__('JSON树')}:">
+                            <el-input type="textarea" rows="4" v-model="operateDialog.data.options"></el-input>
+                        </el-form-item>
+                    </template>
+                </template>
+                <template v-if="operateDialog.data.form=='selectpage'">
+                    <el-form-item label="{:__('请求地址')}:">
+                        <el-input v-model="operateDialog.data.url"></el-input>
+                    </el-form-item>
+                    <el-form-item label="{:__('存储字段')}:">
+                        <el-input v-model="operateDialog.data.keyField"></el-input>
+                    </el-form-item>
+                    <el-form-item label="{:__('显示字段')}:">
+                        <el-input v-model="operateDialog.data.labelField"></el-input>
+                    </el-form-item>
+                </template>
+                <template v-if="operateDialog.data.form=='select' || operateDialog.data.form=='cascader' || operateDialog.data.form=='selectpage'">
+                    <el-form-item label="{:__('是否多选')}:">
+                        <el-select v-model="operateDialog.data.multiple" style="width: 100%" @change="changeForm('table')">
+                            <el-option label="{:__('是')}" :key="1" :value="1"></el-option>
+                            <el-option label="{:__('否')}" :key="0" :value="0"></el-option>
+                        </el-select>
+                    </el-form-item>
+                </template>
+                <el-form-item label="{:__('过滤方式')}:">
+                    <el-select v-model="operateDialog.data.filter" style="width: 100%">
+                        <el-option v-for="item in operateDialog.filter" :key="item.key" :value="item.key">
+                            <span>{{item.key}}</span>
+                            <span style="float: right;font-size: 13px;color: #a2a2a2">{{item.label}}</span>
+                        </el-option>
+                    </el-select>
+                </el-form-item>
+            </el-form>
+        </el-scrollbar>
+        <template #footer>
+          <span class="dialog-footer">
+            <el-button type="info" @click="operateDialog.show = false">{:__('取消')}</el-button>
+            <el-button type="primary" @click="confirmFilter">{:__('确定')}</el-button>
+          </span>
+        </template>
+    </el-dialog>
+    <el-dialog
+            v-model="searchListDialog.show"
+            title="{:__('编辑选择项')}"
+            width="800">
+        <el-scrollbar height="400px">
+            <el-form label-width="100px">
+                <template v-if="searchListDialog.searchList">
+                    <el-form-item label="{:__('选择项')}:">
+                        <field-list field="searchlist" @change="changeSearchList" :value="searchListDialog.searchList"></field-list>
+                    </el-form-item>
+                </template>
+            </el-form>
+        </el-scrollbar>
+        <template #footer>
+          <span class="dialog-footer">
+            <el-button type="info" @click="searchListDialog.show = false">{:__('取消')}</el-button>
+            <el-button type="primary" @click="confirmSearchList">{:__('确定')}</el-button>
+          </span>
+        </template>
+    </el-dialog>
+    <el-dialog
+            v-model="relationDialog.show"
+            title="{:__('编辑关联表')}"
+            width="800">
+        <el-scrollbar height="400px">
+            <el-form label-width="100px">
+                <el-form-item label="{:__('关联表')}:">
+                    <select-page url="develop/getTable" key-field="name" label-field="title" @change="changeRelationTable"></select-page>
+                </el-form-item>
+                <el-form-item label="{:__('关联方式')}:">
+                    <el-select v-model="relationDialog.data.ralationType" style="width: 100%">
+                        <el-option label="{:__('一对一关联')}" key="one" value="one"></el-option>
+                        <el-option label="{:__('一对多关联')}" key="many" value="many"></el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="{:__('关联字段')}:">
+                    <el-select v-model="relationDialog.data.relationField" style="width: 100%">
+                        <el-option :label="field.name" :key="field.name" :value="field.name" v-for="field in relationDialog.fields"></el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="{:__('展示字段')}:">
+                    <el-select v-model="relationDialog.data.showField" style="width: 100%">
+                        <el-option :label="field.name" :key="field.name" :value="field.name" v-for="field in relationDialog.fields"></el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="{:__('过滤字段')}:">
+                    <el-select v-model="relationDialog.data.filterField" style="width: 100%">
+                        <el-option :label="field.name" :key="field.name" :value="field.name" v-for="field in relationDialog.fields"></el-option>
+                    </el-select>
+                </el-form-item>
+            </el-form>
+        </el-scrollbar>
+        <template #footer>
+          <span class="dialog-footer">
+            <el-button type="info" @click="relationDialog.show = false">{:__('取消')}</el-button>
+            <el-button type="primary" @click="confirmRelation">{:__('确定')}</el-button>
+          </span>
+        </template>
+    </el-dialog>
+    <el-dialog
+            v-model="formDialog.show"
+            title="{:__('编辑输入类型')}"
+            width="800">
+        <el-scrollbar height="400px">
+            <el-form label-width="100px">
+                <el-form-item label="{:__('表单简写')}:">
+                    <el-select v-model="formDialog.data.short" @change="changeShort('form')" style="width:100%">
+                        <el-option v-for="item in short.form" :key="item.key" :value="item.key">
+                            <span>{{item.key}}</span>
+                            <span style="float: right;font-size: 13px;color: #a2a2a2">{{item.label}}</span>
+                        </el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="{:__('表单类型')}:">
+                    <el-select v-model="formDialog.data.form" @change="changeForm('form')" style="width: 100%">
+                        <el-option v-for="item in formtype.form" :key="item.key" :value="item.key">
+                            <span>{{item.key}}</span>
+                            <span style="float: right;font-size: 13px;color: #a2a2a2">{{item.label}}</span>
+                        </el-option>
+                    </el-select>
+                </el-form-item>
+                <template v-if="formDialog.data.form=='input'">
+                    <el-form-item label="{:__('文本类型')}:">
+                        <el-select v-model="formDialog.data.type" style="width: 100%">
+                            <el-option key="text" value="text">
+                                <span>text</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('单行文本')}</span>
+                            </el-option>
+                            <el-option key="textarea" value="textarea">
+                                <span>textarea</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('多行文本')}</span>
+                            </el-option>
+                            <el-option key="hidden" value="hidden">
+                                <span>hidden</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('隐藏输入')}</span>
+                            </el-option>
+                            <el-option key="number" value="number">
+                                <span>number</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('输入数字')}</span>
+                            </el-option>
+                            <el-option key="password" value="password">
+                                <span>password</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('输入密码')}</span>
+                            </el-option>
+                            <el-option key="color" value="color">
+                                <span>color</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('输入颜色')}</span>
+                            </el-option>
+                        </el-select>
+                    </el-form-item>
+                </template>
+                <template v-if="formDialog.data.form=='date-picker'">
+                    <el-form-item label="{:__('日期类型')}:">
+                        <el-select v-model="formDialog.data.type" style="width: 100%" @change="changeForm('form')">
+                            <el-option key="date" value="date">
+                                <span>date</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('选择日期')}</span>
+                            </el-option>
+                            <el-option key="datetime" value="datetime">
+                                <span>datetime</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('选择日期+时间')}</span>
+                            </el-option>
+                            <el-option key="daterange" value="daterange">
+                                <span>daterange</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('选择日期区间')}</span>
+                            </el-option>
+                        </el-select>
+                    </el-form-item>
+                </template>
+                <template v-if="formDialog.data.form=='time-picker'">
+                    <el-form-item label="{:__('时间类型')}:">
+                        <el-select v-model="formDialog.data.type" style="width: 100%" @change="changeForm('form')">
+                            <el-option key="time" value="time">
+                                <span>time</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('选择时间')}</span>
+                            </el-option>
+                            <el-option key="timerange" value="timerange">
+                                <span>timerange</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('选择时间区间')}</span>
+                            </el-option>
+                        </el-select>
+                    </el-form-item>
+                </template>
+                <template v-if="formDialog.data.form=='input' && formDialog.data.type!='hidden' && formDialog.data.type!='textarea'">
+                    <el-form-item label="{:__('前置内容')}:">
+                        <el-input v-model="formDialog.data.prepend"></el-input>
+                    </el-form-item>
+                    <el-form-item label="{:__('后置内容')}:">
+                        <el-input v-model="formDialog.data.append"></el-input>
+                    </el-form-item>
+                    <el-form-item label="{:__('只读')}:">
+                        <el-switch v-model="formDialog.data.readonly" :active-value="1" :inactive-value="0"></el-switch>
+                    </el-form-item>
+                </template>
+                <template v-if="formDialog.data.form=='input' && formDialog.data.type=='textarea'">
+                    <el-form-item label="{:__('显示行数')}:">
+                        <el-input v-model="formDialog.data.rows" type="number"></el-input>
+                    </el-form-item>
+                </template>
+                <template v-if="formDialog.data.form=='attachment' || formDialog.data.form=='files'">
+                    <el-form-item label="{:__('允许上传数量')}:">
+                        <el-input v-model="formDialog.data.limit" type="number"></el-input>
+                    </el-form-item>
+                </template>
+                <template v-if="formDialog.data.form=='files'">
+                    <el-form-item label="{:__('支持文件类型')}:">
+                        <el-input v-model="formDialog.data.accept"></el-input>
+                    </el-form-item>
+                    <el-form-item label="{:__('存储方式')}:">
+                        <el-select v-model="formDialog.data.disks" style="width: 100%">
+                            <el-option key="local_public" value="local_public">
+                                <span>local_public</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('本地开放文件')}</span>
+                            </el-option>
+                            <el-option key="local_private" value="local_private">
+                                <span>local_private</span>
+                                <span style="float: right;font-size: 13px;color: #a2a2a2">{:__('本地私有文件')}</span>
+                            </el-option>
+                        </el-select>
+                    </el-form-item>
+                </template>
+                <template v-if="formDialog.data.form=='fieldlist'">
+                    <el-form-item label="{:__('项目标题')}:">
+                        <el-input v-model="formDialog.data.label"></el-input>
+                    </el-form-item>
+                </template>
+                <template v-if="formDialog.searchList && (formDialog.data.form=='switch' || formDialog.data.form=='checkbox' || formDialog.data.form=='radio' || formDialog.data.form=='select')">
+                    <el-form-item label="{:__('选择项')}:">
+                        <field-list field="form" @change="changeSearchList" :value="formDialog.searchList"></field-list>
+                    </el-form-item>
+                </template>
+                <template v-if="inArray(['input','select','date-picker','time-picker','cascader','selectpage'],formDialog.data.form)">
+                    <el-form-item label="{:__('提示文字')}:">
+                        <el-input v-model="formDialog.data.placeholder" placeholder="{:__('不填默认显示为字段标题')}"></el-input>
+                    </el-form-item>
+                </template>
+                <template v-if="formDialog.data.form=='editor'">
+                    <el-form-item label="{:__('宽度')}:">
+                        <el-input v-model="formDialog.data.width" placeholder="{:__('支持数字,像素,百分比如:300,300px,100%')}"></el-input>
+                    </el-form-item>
+                    <el-form-item label="{:__('高度')}:">
+                        <el-input v-model="formDialog.data.height" placeholder="{:__('支持数字,像素:300,300px')}"></el-input>
+                    </el-form-item>
+                </template>
+                <template v-if="formDialog.data.form!='slot'">
+                    <el-form-item label="{:__('默认值')}:">
+                        <el-input v-model="formDialog.data.value"></el-input>
+                    </el-form-item>
+                </template>
+                <template v-if="formDialog.data.form=='cascader'">
+                    <el-form-item label="{:__('数据获取方式')}:">
+                        <el-select v-model="formDialog.data.cascaderType" style="width: 100%">
+                            <el-option label="{:__('JSON数据')}" key="options" value="options"></el-option>
+                            <el-option label="{:__('网络获取')}" key="url" value="url"></el-option>
+                        </el-select>
+                    </el-form-item>
+                    <template v-if="formDialog.data.cascaderType=='url'">
+                        <el-form-item label="{:__('请求地址')}:">
+                            <el-input v-model="formDialog.data.url"></el-input>
+                        </el-form-item>
+                        <el-form-item label="{:__('获取级数')}:">
+                            <el-input v-model="formDialog.data.level"></el-input>
+                        </el-form-item>
+                    </template>
+                    <template v-if="formDialog.data.cascaderType=='options'">
+                        <el-form-item label="{:__('JSON树')}:">
+                            <el-input type="textarea" rows="4" v-model="formDialog.data.options"></el-input>
+                        </el-form-item>
+                    </template>
+                </template>
+                <template v-if="formDialog.data.form=='selectpage'">
+                    <el-form-item label="{:__('请求地址')}:">
+                        <el-input v-model="formDialog.data.url"></el-input>
+                    </el-form-item>
+                    <el-form-item label="{:__('存储字段')}:">
+                        <el-input v-model="formDialog.data.keyField"></el-input>
+                    </el-form-item>
+                    <el-form-item label="{:__('显示字段')}:">
+                        <el-input v-model="formDialog.data.labelField"></el-input>
+                    </el-form-item>
+                </template>
+                <template v-if="formDialog.data.form=='select' || formDialog.data.form=='cascader' || formDialog.data.form=='selectpage'">
+                    <el-form-item label="{:__('是否多选')}:">
+                        <el-select v-model="formDialog.data.multiple" style="width: 100%" @change="changeForm('form')">
+                            <el-option label="{:__('是')}" :key="1" :value="1"></el-option>
+                            <el-option label="{:__('否')}" :key="0" :value="0"></el-option>
+                        </el-select>
+                    </el-form-item>
+                </template>
+            </el-form>
+        </el-scrollbar>
+        <template #footer>
+          <span class="dialog-footer">
+            <el-button type="info" @click="formDialog.show = false">{:__('取消')}</el-button>
+            <el-button type="primary" @click="confirmForm">{:__('确定')}</el-button>
+          </span>
+        </template>
+    </el-dialog>
+    <el-dialog
+            v-model="codeDialog.show"
+            title="{:__('生成代码')}"
+            :destroy-on-close="true"
+            height="500"
+            width="80%">
+            <el-tabs type="border-card">
+                <el-tab-pane :label="key+'文件'" v-for="(code,key) in codeDialog.row">
+                    <el-scrollbar style="height: 400px">
+                        <div v-if="key=='view'">
+                            <template v-for="(view,name) in code">
+                                <el-divider>{{name}}</el-divider>
+                                <el-input style="border: 0" autosize type="textarea" :value="view"></el-input>
+                            </template>
+                        </div>
+                        <div v-else>
+                            <el-input style="border: 0" autosize type="textarea" :value="code"></el-input>
+                        </div>
+                    </el-scrollbar>
+                </el-tab-pane>
+            </el-tabs>
+            <template #footer>
+                <el-button type="primary">
+                    复制
+                </el-button>
+            </template>
+    </el-dialog>
+</template>
+<script>
+    import selectpage from "@components/SelectPage.js";
+    import fieldlist from '@components/Fieldlist.js';
+    import {inArray} from "@util.js";
+    function isImage(name)
+    {
+        if(
+            name.indexOf('imgs')!=-1 ||
+            name.indexOf('images')!=-1 ||
+            name.indexOf('logos')!=-1 ||
+            name.indexOf('photos')!=-1 ||
+            name.indexOf('pictures')!=-1 ||
+            name.indexOf('icons')!=-1
+        ){
+            return 'images';
+        }
+        if(
+            name.indexOf('img')!=-1 ||
+            name.indexOf('image')!=-1 ||
+            name.indexOf('logo')!=-1 ||
+            name.indexOf('photo')!=-1 ||
+            name.indexOf('picture')!=-1 ||
+            name.indexOf('icon')!=-1
+        ){
+            return 'image';
+        }
+        return false;
+    }
+    function getAccept()
+    {
+        let mimetype=Yunqi.config.upload.mimetype.split(',');
+        let accept=[];
+        mimetype.forEach(res=>{
+            accept.push('.'+res);
+        });
+        accept=accept.join(',');
+        return accept;
+    }
+    function parseShort(data){
+        let short=data.short;
+        switch (short){
+            case '=':
+                data.form='input';
+                data.type='text';
+                data.filter='=';
+                break;
+            case '<>':
+                data.form='input';
+                data.type='text';
+                data.filter='<>';
+                break;
+            case 'like':
+                data.form='input';
+                data.type='text';
+                data.filter='LIKE';
+                break;
+            case 'not like':
+                data.form='input';
+                data.type='text';
+                data.filter='NOT LIKE';
+                break;
+            case 'null':
+                data.form='hidden';
+                data.filter='IS NULL';
+                break;
+            case 'not null':
+                data.form='hidden';
+                data.filter='IS NOT NULL';
+                break;
+            case 'select':
+                data.form='select';
+                data.filter='=';
+                break;
+            case 'selects':
+                data.form='select';
+                data.filter='IN';
+                data.multiple=1;
+                data.value='[]';
+                break;
+            case 'checkbox':
+                data.form='checkbox';
+                data.filter='IN';
+                data.value='[]';
+                break;
+            case 'radio':
+                data.form='radio';
+                data.filter='=';
+                break;
+            case 'find_in_set':
+                data.form='select';
+                data.filter='FIND_IN_SET';
+                break;
+            case 'between':
+                data.form='between';
+                data.filter='BETWEEN';
+                data.value='[]';
+                break;
+            case 'not between':
+                data.form='between';
+                data.filter='NOT BETWEEN';
+                data.value='[]';
+                break;
+            case 'date':
+                data.form='date-picker';
+                data.type='date';
+                data.filter='=';
+                break;
+            case 'datetime':
+                data.form='date-picker';
+                data.type='datetime';
+                data.filter='=';
+                break;
+            case 'daterange':
+                data.form='date-picker';
+                data.type='daterange';
+                data.filter='BETWEEN TIME';
+                break;
+            case 'time':
+                data.form='time-picker';
+                data.type='time';
+                data.filter='=';
+                break;
+            case 'timerange':
+                data.form='time-picker';
+                data.type='timerange';
+                data.filter='BETWEEN';
+                break;
+            case 'selectpage':
+                data.form='selectpage';
+                data.filter='=';
+                data.keyField='id';
+                data.labelField='name';
+                data.url='';
+                break;
+            case 'cascader':
+                data.form='cascader';
+                data.filter='=';
+                data.cascaderType='url';
+                data.level=2;
+                data.url='';
+                break;
+            case 'area':
+                data.form='cascader';
+                data.filter='=';
+                data.cascaderType='url';
+                data.level=3;
+                data.url='ajax/area';
+                break;
+            case 'category':
+                data.form='cascader';
+                data.filter='=';
+                data.cascaderType='url';
+                data.level=2;
+                data.url='ajax/category';
+                break;
+            case 'hidden':
+                data.form='input';
+                data.type='hidden';
+                break;
+            case 'text':
+                data.form='input';
+                data.type='text';
+                break;
+            case 'password':
+                data.form='input';
+                data.type='password';
+                break;
+            case 'readonly':
+                data.form='input';
+                data.type='text';
+                data.readonly=1;
+                break;
+            case 'number':
+                data.form='input';
+                data.type='number';
+                break;
+            case 'textarea':
+                data.form='input';
+                data.type='textarea';
+                data.rows=4;
+                break;
+            case 'editor':
+                data.form='editor';
+                data.width='100%';
+                data.height='350px';
+                break;
+            case 'switch':
+                data.form='switch';
+                break;
+            case 'image':
+                data.form='attachment';
+                data.limit=1;
+                break;
+            case 'images':
+                data.form='attachment';
+                data.limit=10;
+                break;
+            case 'file':
+                data.form='files';
+                data.limit=1;
+                break;
+            case 'files':
+                data.form='files';
+                data.limit=10;
+                break;
+            case 'fieldlist':
+                data.form='fieldlist';
+                data.label=['键名','键值'];
+                data.value='{}';
+                break;
+            case 'slot':
+                data.form='slot';
+                break;
+        }
+    }
+    export default{
+        components:{'SelectPage':selectpage,'FieldList':fieldlist},
+        data:{
+            fields:[],
+            actions:[],
+            operateDialog:{
+                show:false,
+                row:'',
+                filter:[],
+                searchList: '',
+                data:''
+            },
+            searchListDialog:{
+                show: false,
+                row: '',
+                searchList: ''
+            },
+            relationDialog:{
+                show: false,
+                row: '',
+                fields:[],
+                data:{
+                    table:'',
+                    relationField:'',
+                    filterField:'',
+                    showField:'',
+                    ralationType:'one'
+                }
+            },
+            formDialog:{
+                show:false,
+                row:'',
+                searchList: '',
+                data:''
+            },
+            codeDialog:{
+                show:false,
+                row:[],
+            },
+            formatter:{
+                text:__('文本'),
+                image:__('图片'),
+                images:__('多图'),
+                date:__('日期'),
+                datetime:__('日期时间'),
+                tag:__('标签'),
+                tags:__('多标签'),
+                switch:__('开关'),
+                select:__('下拉框'),
+                link:__('链接'),
+                html:__('HTML'),
+                slot:__('自定义插槽'),
+            },
+            short:{
+                table:[
+                    {key:'=',label:__('单行文本框,field等于输入值')},
+                    {key:'<>',label:__('单行文本框,field不等于输入值')},
+                    {key:'like',label:__('单行文本框,field文本包含输入值')},
+                    {key:'not like',label:__('单行文本框,field文本不包含输入值')},
+                    {key:'null',label:__('隐藏过滤器,field字段为空值')},
+                    {key:'not null',label:__('隐藏过滤器,field字段为非空值')},
+                    {key:'select',label:__('下拉框(单选),field等于选项')},
+                    {key:'selects',label:__('下拉框(多选),field包含于选项,如2包含于[1,2,3]')},
+                    {key:'checkbox',label:__('多选输入框,field包含于选项,,如2包含于[1,2,3]')},
+                    {key:'radio',label:__('单选输入框,field等于选项')},
+                    {key:'find_in_set',label:__('下拉框(单选),field文本包含选项,如“1,2,3”包含2')},
+                    {key:'between',label:__('并排输入框,field介于两个数字之间')},
+                    {key:'not between',label:__('并排输入框,field介于两个数字之外')},
+                    {key:'date',label:__('日期选择框,field等于选项')},
+                    {key:'datetime',label:__('日期+时间选择框,field等于选项')},
+                    {key:'daterange',label:__('日期区间选择框,field介于两个日期之间')},
+                    {key:'time',label:__('时间选择框,field等于选项')},
+                    {key:'timerange',label:__('时间区间选择框,field介于两个时间之间')},
+                    {key:'selectpage',label:__('关联表分页选择框,field等于表的keyField')},
+                    {key:'cascader',label:__('多级树形选择框,field等于最后一级的id')},
+                    {key:'area',label:__('省/市/区县选择框,field等于最后一级的id')},
+                    {key:'category',label:__('分类表category选择框,field等于最后一级的id')}
+                ],
+                form:[
+                    {key:'hidden',label:__('隐藏表单')},
+                    {key:'text',label:__('单行文本输入框')},
+                    {key:'number',label:__('数字输入框')},
+                    {key:'readonly',label:__('单行只读文本输入框')},
+                    {key:'password',label:__('密码输入框')},
+                    {key:'textarea',label:__('多行文本输入框')},
+                    {key:'editor',label:__('富文本输入框')},
+                    {key:'select',label:__('下拉框(单选)')},
+                    {key:'selects',label:__('下拉框(多选)')},
+                    {key:'radio',label:__('单选框')},
+                    {key:'checkbox',label:__('复选框')},
+                    {key:'switch',label:__('开关')},
+                    {key:'date',label:__('选择日期')},
+                    {key:'datetime',label:__('选择日期+时间')},
+                    {key:'daterange',label:__('选择日期区间')},
+                    {key:'time',label:__('选择时间')},
+                    {key:'timerange',label:__('选择时间区间')},
+                    {key:'selectpage',label:__('关联表分页选择框')},
+                    {key:'cascader',label:__('多级树形选择框')},
+                    {key:'image',label:__('选择单张图片')},
+                    {key:'images',label:__('选择多张图片')},
+                    {key:'file',label:__('选择单个文件')},
+                    {key:'files',label:__('选择多个文件')},
+                    {key:'fieldlist',label:__('JSON输入框')},
+                    {key:'area',label:__('选择省/市/区县')},
+                    {key:'category',label:__('分类表category选择框')},
+                    {key:'slot',label:__('自定义插槽')},
+                ]
+            },
+            formtype:{
+                table:[
+                    {key:'hidden',label:__('隐藏表单')},
+                    {key:'input',label:__('文本输入框')},
+                    {key:'select',label:__('下拉框')},
+                    {key:'radio',label:__('单选框')},
+                    {key:'checkbox',label:__('复选框')},
+                    {key:'between',label:__('并排输入框')},
+                    {key:'date-picker',label:__('日期选择框')},
+                    {key:'time-picker',label:__('时间选择框')},
+                    {key:'cascader',label:__('多级树形选择框')},
+                    {key:'selectpage',label:__('关联表分页选择框')},
+                ],
+                form:[
+                    {key:'input',label:__('文本输入框')},
+                    {key:'select',label:__('下拉框')},
+                    {key:'radio',label:__('单选框')},
+                    {key:'checkbox',label:__('复选框')},
+                    {key:'editor',label:__('富文本输入框')},
+                    {key:'switch',label:__('开关')},
+                    {key:'date-picker',label:__('日期选择框')},
+                    {key:'time-picker',label:__('时间选择框')},
+                    {key:'cascader',label:__('多级树形选择框')},
+                    {key:'selectpage',label:__('关联表分页选择框')},
+                    {key:'attachment',label:__('相册')},
+                    {key:'files',label:__('上传文件')},
+                    {key:'fieldlist',label:__('输入JSON')},
+                    {key:'slot',label:__('自定义插槽')},
+                ]
+            },
+            filter:[
+                {key:'=',label:__('等于')},
+                {key:'<>',label:__('不等于')},
+                {key:'>',label:__('大于')},
+                {key:'>=',label:__('大于等于')},
+                {key:'<',label:__('小于')},
+                {key:'<=',label:__('小于等于')},
+                {key:'< TIME',label:__('早于')},
+                {key:'<= TIME',label:__('早于等于')},
+                {key:'> TIME',label:__('晚于')},
+                {key:'>= TIME',label:__('晚于等于')},
+                {key:'BETWEEN TIME',label:__('时间介于')},
+                {key:'NOT BETWEEN TIME',label:__('时间不介于')},
+                {key:'LIKE',label:__('包含字符')},
+                {key:'NOT LIKE',label:__('不包含字符')},
+                {key:'FIND_IN_SET',label:__('序列包含')},
+                {key:'NOT FIND_IN_SET',label:__('序列不包含')},
+                {key:'IN',label:__('包含于数组')},
+                {key:'NOT IN',label:__('不包含于数组')},
+                {key:'BETWEEN',label:__('介于')},
+                {key:'NOT BETWEEN',label:__('不介于')},
+                {key:'IS NULL',label:__('为空')},
+                {key:'IS NOT NULL',label:__('不为空')},
+            ],
+            tableData:'',
+            crudForm:{
+                table:'',
+                controller:'',
+                model:'',
+                reduced:false,
+                isTree:false,
+                treeTitle:'',
+                pagination:false,
+                summary:false,
+                expand:false,
+                tabs:'',
+                actionList:'',
+                recyclebin:false
+            }
+        },
+        methods:{
+            inArray:inArray,
+            havaPid:function (){
+                if(!this.tableData){
+                    return false;
+                }
+                for(let k in this.tableData){
+                    if(this.tableData[k].field=='pid'){
+                        return true;
+                    }
+                }
+                return false;
+            },
+            changeTable:function (table){
+                this.crudForm.table=table;
+                Yunqi.ajax.get('develop/getFields',{table:table}).then(res=>{
+                    this.fields=res;
+                    this.parseAction();
+                    this.parseController();
+                    this.parseModel();
+                    this.parseTable();
+                });
+            },
+            parseTable:function (){
+                this.tableData='';
+                let list=[];
+                for(let k in this.fields){
+                    let item=this.fields[k];
+                    let obj={
+                        field:item.name,
+                        title:this.parseTitle(item),
+                        type:item.type,
+                        visible:this.parseVisible(item),
+                        formatter:this.parseFormatter(item),
+                        operate:'',
+                        searchList:'',
+                        relation:'',
+                        sortable:[],
+                        search:[],
+                        edit:'',
+                        rules:'',
+                        recyclebin:[]
+                    };
+                    this.parseFields(obj);
+                    list.push(obj);
+                }
+                Vue.nextTick(()=>{
+                    this.tableData=list;
+                });
+            },
+            parseTitle:function (row){
+                if(row.title){
+                    return row.title;
+                }
+                if(row.name=='deletetime'){
+                    return __('删除时间');
+                }
+                if(row.name=='createtime'){
+                    return __('创建时间');
+                }
+                if(row.name=='updatetime'){
+                    return __('修改时间');
+                }
+                if(row.name=='pid'){
+                    return __('父级');
+                }
+                if(row.name=='status'){
+                    return __('状态');
+                }
+                if(row.name=='id'){
+                    return __('ID');
+                }
+                if(row.name=='weigh'){
+                    return __('权重');
+                }
+                return row.name;
+            },
+            parseController:function (){
+                let table=this.crudForm.table.replace(Yunqi.data.tablePrefix,'');
+                table=table.replace(table[0],table[0].toUpperCase());
+                table=table.replace(/_([a-z])/g,function (all,letter){
+                    return letter.toUpperCase();
+                });
+                this.crudForm.controller='app\\admin\\controller\\'+table;
+            },
+            parseModel:function (){
+                let table=this.crudForm.table.replace(Yunqi.data.tablePrefix,'');
+                table=table.replace(table[0],table[0].toUpperCase());
+                table=table.replace(/_([a-z])/g,function (all,letter){
+                    return letter.toUpperCase();
+                });
+                this.crudForm.model='app\\common\\model\\'+table;
+            },
+            parseAction:function (){
+                this.crudForm.actionList='';
+                let list={index:__('查看'),add:__('添加'),edit:__('编辑'),multi:__('更新'),del:__('删除'),import:__('导入'),download:__('下载')};
+                for(let k in this.fields){
+                    let item=this.fields[k];
+                    if(item.name=='deletetime'){
+                        list.recyclebin=__('回收站');
+                        this.crudForm.recyclebin=true;
+                    }
+                }
+                Vue.nextTick(()=>{
+                    this.crudForm.actionList=list;
+                });
+            },
+            parseFormatter:function (row){
+                if(row.name.endsWith('time')){
+                    return 'datetime';
+                }
+                let image=isImage(row.name);
+                if(image){
+                    return image;
+                }
+                if(row.name=='status'){
+                    return 'switch';
+                }
+                if(row.type=='tinyint'){
+                    return 'select';
+                }
+                return 'text';
+            },
+            parseVisible:function (row){
+                if(row.name=='deletetime'){
+                    return 'none';
+                }
+                if(row.name=='updatetime'){
+                    return false;
+                }
+                return true;
+            },
+            parseFields:function (obj){
+                obj.operate=this.parseOperate(obj);
+                obj.searchList=this.parseSearchList(obj);
+                obj.edit=this.parseEdit(obj);
+                obj.rules=this.parseRules(obj);
+            },
+            parseEdit:function (row){
+                if(row.field=='id'){
+                    return 'hidden';
+                }
+                if(row.field=='pid'){
+                    return 'slot';
+                }
+                if(row.field=='createtime' || row.field=='updatetime' || row.field=='deletetime'){
+                    return '';
+                }
+                let image=isImage(row.field);
+                if(image){
+                    return image;
+                }
+                if(row.field=='status'){
+                    return 'switch';
+                }
+                if(row.searchList){
+                    return 'select';
+                }
+                if(row.type=='tinyint' || row.type=='int'){
+                    return 'number';
+                }
+                return 'text';
+            },
+            parseRules:function (row){
+                if(row.edit=='hidden' || !row.edit){
+                    return '';
+                }
+                if(row.field=='pid'){
+                    return 'required';
+                }
+                if(row.field=='status' || row.type=='int'){
+                    return '';
+                }
+                return 'required';
+            },
+            parseOperate:function (row){
+                if(row.field=='pid' || row.field=='id'){
+                    return '';
+                }
+                if(row.formatter=='text'){
+                    return '=';
+                }
+                if(row.formatter=='date'){
+                    return 'date';
+                }
+                if(row.formatter=='datetime'){
+                    return 'daterange';
+                }
+                if(row.formatter=='tag'){
+                    return 'like';
+                }
+                if(row.formatter=='tags'){
+                    return 'find_in_set';
+                }
+                if(row.formatter=='switch' || row.formatter=='select'){
+                    return 'select';
+                }
+                return '';
+            },
+            parseSearchList:function (row){
+                let r='';
+                if(row.visible=='none'){
+                    return r;
+                }
+                if(row.formatter=='switch' ||  row.formatter=='select'){
+                    if(row.field=='status'){
+                        r={'normal':__('正常'),'hidden':__('隐藏')};
+                    }else if(row.type=='tinyint' || row.type=='int'){
+                        r={'1':__('是'),'0':__('否')};
+                    }else{
+                        r={'key1':'选项1','key2':'选项2'};
+                    }
+                }
+                if(!r && (
+                    row.operate=='SELECT' ||
+                    row.operate=='SELECTS' ||
+                    row.operate=='RADIO' ||
+                    row.operate=='FIND_IN_SET' ||
+                    row.operate=='CHECKBOX')
+                ){
+                    r={'key1':'选项1','key2':'选项2'};
+                }
+                if(r){
+                    r=JSON.stringify(r);
+                }
+                return r;
+            },
+            parseOperateForm:function (){
+                let data=this.operateDialog.data;
+                let showFilter=[];
+                let form=data.form;
+                switch (form){
+                    case 'hidden':
+                        showFilter='all';
+                        break;
+                    case 'input':
+                        if(!inArray(['text','number','password','color'],data.type)){
+                            data.type='text';
+                        }
+                        showFilter=['=','<>','>','>=','<','<=','LIKE','NOT LIKE','FIND_IN_SET','NOT FIND_IN_SET'];
+                        break;
+                    case 'select':
+                    case 'cascader':
+                    case 'selectpage':
+                        if(data.multiple){
+                            showFilter=['IN','NOT IN'];
+                        }else{
+                            showFilter=['=','<>','>','>=','<','<=','LIKE','NOT LIKE','FIND_IN_SET','NOT FIND_IN_SET'];
+                        }
+                        break;
+                    case 'radio':
+                        showFilter=['=','<>','>','>=','<','<=','LIKE','NOT LIKE','FIND_IN_SET','NOT FIND_IN_SET'];
+                        break;
+                    case 'checkbox':
+                        showFilter=['IN','NOT IN'];
+                        break;
+                    case 'between':
+                        showFilter=['BETWEEN','NOT BETWEEN'];
+                        break;
+                    case 'date-picker':
+                        if(!inArray(['date','datetime','daterange'],data.type)){
+                            data.type='date';
+                        }
+                        if(data.type=='date' || data.type=='datetime'){
+                            showFilter=['=','< TIME','<= TIME','> TIME','>= TIME'];
+                        }
+                        if(data.type=='daterange'){
+                            showFilter=['BETWEEN TIME','NOT BETWEEN TIME'];
+                        }
+                        break;
+                    case 'time-picker':
+                        if(!inArray(['time','timerange'],data.type)){
+                            data.type='time';
+                        }
+                        if(data.type=='time'){
+                            showFilter=['=','< TIME','<= TIME','> TIME','>= TIME'];
+                        }
+                        if(data.type=='timerange'){
+                            showFilter=['BETWEEN TIME','NOT BETWEEN TIME'];
+                        }
+                        break;
+                }
+                this.operateDialog.searchList='';
+                if(form=='checkbox' || form=='select' || form=='radio'){
+                    Vue.nextTick(()=>{
+                        let searchList=this.operateDialog.row.searchList?JSON.parse(this.operateDialog.row.searchList): {};
+                        this.operateDialog.searchList=searchList;
+                    });
+                }
+                this.operateDialog.filter=this.parseFilter(showFilter);
+            },
+            parseFilter:function (arr){
+                if(arr=='all'){
+                    return this.filter;
+                }else{
+                    let filter=[];
+                    for(let i=0;i<this.filter.length;i++){
+                        if(inArray(arr,this.filter[i].key)){
+                            filter.push(this.filter[i]);
+                        }
+                    }
+                    return filter;
+                }
+            },
+            showOperate:function (row){
+                this.operateDialog.row=row;
+                let obj={
+                    short:row.operate,
+                    form:'',
+                    type:'',
+                    filter:'',
+                    placeholder:'',
+                    size:'default',
+                    append:'',
+                    prepend:'',
+                    value:'',
+                    url:'',
+                    labelField:'name',
+                    keyField:'id',
+                    cascaderType:'url',
+                    options:'',
+                    level:2,
+                    multiple:0
+                };
+                if(row.operate.startsWith("{") && row.operate.endsWith("}")){
+                    let operate=JSON.parse(row.operate);
+                    if(operate.multiple){
+                        operate.multiple=1;
+                    }
+                    this.operateDialog.data=Object.assign(obj,operate);
+                }else{
+                    this.operateDialog.data=obj;
+                    parseShort(this.operateDialog.data);
+                    this.parseOperateForm();
+                }
+                this.operateDialog.show=true;
+            },
+            changeShort:function (type){
+                if(type=='table'){
+                    this.operateDialog.data.value='';
+                    parseShort(this.operateDialog.data);
+                    this.parseOperateForm();
+                }
+                if(type=='form'){
+                    this.formDialog.data.value='';
+                    this.formDialog.data.readonly=0;
+                    parseShort(this.formDialog.data);
+                    this.parseForm();
+                }
+            },
+            changeForm:function (type){
+                if(type=='table'){
+                    this.parseOperateForm();
+                    this.operateDialog.data.value='';
+                    this.operateDialog.data.filter=this.operateDialog.filter[0].key;
+                    if(this.operateDialog.data.form=='select' || this.operateDialog.data.form=='selectpage' || this.operateDialog.data.form=='cascader'){
+                        if(this.operateDialog.data.multiple){
+                            this.operateDialog.data.value='[]';
+                        }
+                    }
+                    if(this.operateDialog.data.form=='date-picker' || this.operateDialog.data.form=='time-picker'){
+                        if(this.operateDialog.data.type=='daterange' || this.operateDialog.data.type=='timerange'){
+                            this.operateDialog.data.value='[]';
+                        }
+                    }
+                    if(this.operateDialog.data.form=='checkbox' || this.operateDialog.data.form=='between'){
+                        this.operateDialog.data.value='[]';
+                    }
+                }
+                if(type=='form'){
+                    this.parseForm();
+                    this.formDialog.data.value='';
+                    if(this.formDialog.data.form=='select' || this.formDialog.data.form=='selectpage' || this.formDialog.data.form=='cascader'){
+                        if(this.formDialog.data.multiple){
+                            this.formDialog.data.value='[]';
+                        }
+                    }
+                    if(this.formDialog.data.form=='date-picker' || this.formDialog.data.form=='time-picker'){
+                        if(this.formDialog.data.type=='daterange' || this.formDialog.data.type=='timerange'){
+                            this.formDialog.data.value='[]';
+                        }
+                    }
+                    if(this.formDialog.data.form=='checkbox'){
+                        this.formDialog.data.value='[]';
+                    }
+                    if(this.formDialog.data.form=='fieldlist'){
+                        this.formDialog.data.value='{}';
+                    }
+                }
+            },
+            parseForm:function (){
+                let data=this.formDialog.data;
+                let form=data.form;
+                this.formDialog.searchList='';
+                if(form=='input' && !inArray(['text','number','hidden','textarea','password','color'],data.type)){
+                    data.type='text';
+                }
+                if(form=='date-picker' && !inArray(['date','datetime','daterange'],data.type)){
+                    data.type='date';
+                }
+                if(form=='time-picker' && !inArray(['time','timerange'],data.type)){
+                    data.type='time';
+                }
+                if(form=='checkbox' || form=='select' || form=='radio'){
+                    Vue.nextTick(()=>{
+                        let searchList=this.formDialog.row.searchList?JSON.parse(this.formDialog.row.searchList): {};
+                        this.formDialog.searchList=searchList;
+                    });
+
+                }else if(form=='switch'){
+                    Vue.nextTick(()=>{
+                        let json={'normal':'正常','hidden':'隐藏'};
+                        data.value='normal';
+                        let type=this.formDialog.row.type;
+                        if(type=='int'){
+                            json={'0':'否','1':'是'};
+                            data.value='1';
+                        }
+                        let searchList=this.formDialog.row.searchList?JSON.parse(this.formDialog.row.searchList):json;
+                        this.formDialog.searchList=searchList;
+                    });
+                }
+            },
+            confirmFilter:function (){
+                let data=this.operateDialog.data;
+                let row=this.operateDialog.row;
+                let field=[];
+                switch (data.form){
+                    case 'hidden':
+                        field=['form','filter','value'];
+                        break;
+                    case 'input':
+                        field=['form','type','filter','placeholder','size','append','prepend','value'];
+                        break;
+                    case 'select':
+                        field=['form','filter','placeholder','size','value','multiple'];
+                        break;
+                    case 'cascader':
+                        if(data.cascaderType=='url'){
+                            field=['form','filter','placeholder','size','value','url','level','multiple'];
+                        }
+                        if(data.cascaderType=='options'){
+                            field=['form','filter','placeholder','size','value','options','multiple'];
+                        }
+                        break;
+                    case 'selectpage':
+                        field=['form','filter','placeholder','size','value','url','labelField','keyField','multiple'];
+                        break;
+                    case 'radio':
+                        field=['form','filter','size','value'];
+                        break;
+                    case 'checkbox':
+                        field=['form','filter','size','value'];
+                        break;
+                    case 'between':
+                        field=['form','filter','size','value'];
+                        break;
+                    case 'date-picker':
+                        field=['form','type','placeholder','filter','size','value'];
+                        break;
+                    case 'time-picker':
+                        field=['form','type','placeholder','filter','size','value'];
+                        break;
+                }
+                let r={};
+                for(let key in data){
+                    if(inArray(field,key)){
+                        if(!data[key]){
+                            continue;
+                        }
+                        if(key=='multiple'){
+                            if(!data[key]){
+                                continue;
+                            }else{
+                                data[key]=true;
+                            }
+                        }
+                        if(key=='size' && data[key]=='default'){
+                            continue;
+                        }
+                        r[key]=data[key];
+                    }
+                }
+                row.operate=JSON.stringify(r);
+                if(this.operateDialog.searchList && Object.keys(this.operateDialog.searchList).length>0){
+                    row.searchList=JSON.stringify(this.operateDialog.searchList);
+                }else{
+                    row.searchList='';
+                }
+                this.operateDialog.show=false;
+            },
+            changeSearchList:function (row){
+                if(row.field=='searchlist'){
+                    this.searchListDialog.searchList=row.value;
+                }
+                if(row.field=='operate'){
+                    this.operateDialog.searchList=row.value;
+                }
+                if(row.field=='form'){
+                    this.formDialog.searchList=row.value;
+                }
+            },
+            showSearchList:function (row){
+                this.searchListDialog.searchList='';
+                Vue.nextTick(()=>{
+                    this.searchListDialog.searchList=row.searchList?JSON.parse(row.searchList): {};
+                });
+                this.searchListDialog.row=row;
+                this.searchListDialog.show=true;
+            },
+            confirmSearchList:function (){
+                let row= this.searchListDialog.row;
+                if(this.searchListDialog.searchList && Object.keys(this.searchListDialog.searchList).length>0){
+                    row.searchList=JSON.stringify(this.searchListDialog.searchList);
+                }else{
+                    row.searchList='';
+                }
+                this.searchListDialog.show=false;
+            },
+            showRelation:function (row){
+                this.relationDialog.row=row;
+                this.relationDialog.show=true;
+            },
+            changeRelationTable:function (table){
+                this.relationDialog.data.table=table;
+                Yunqi.ajax.get('develop/getFields',{table:table}).then(res=>{
+                    this.relationDialog.fields=res;
+                    this.relationDialog.data.relationField=res[0].name;
+                });
+            },
+            confirmRelation:function (){
+                let row=this.relationDialog.row;
+                let data=this.relationDialog.data;
+                for(let k in data){
+                    if(!data[k]){
+                        Yunqi.message.error(__('每一项都必须填写完整'));
+                        return;
+                    }
+                }
+                row.relation=JSON.stringify(data);
+                if(row.operate.startsWith('{') && row.operate.endsWith('}')){
+                    let operate=JSON.parse(row.operate);
+                    let table=data.table.replace(Yunqi.data.tablePrefix,'');
+                    operate.name=table+'.'+data.filterField;
+                    row.operate=JSON.stringify(operate);
+                }else{
+                    let obj={
+                        short:row.operate
+                    };
+                    parseShort(obj);
+                    let table=data.table.replace(Yunqi.data.tablePrefix,'');
+                    obj.name=table+'.'+data.filterField;
+                    delete obj.short;
+                    row.operate=JSON.stringify(obj);
+                }
+                this.relationDialog.show=false;
+                this.relationDialog.data={
+                    table:'',
+                    relationField:'',
+                    filterField:'',
+                    showField:'',
+                    ralationType:'one'
+                };
+            },
+            showFormDialog:function (row){
+                this.formDialog.row=row;
+                let obj={
+                    short:row.edit,
+                    form:'',
+                    type:'',
+                    placeholder:'',
+                    append:'',
+                    prepend:'',
+                    readonly:0,
+                    value:'',
+                    url:'',
+                    labelField:'name',
+                    keyField:'id',
+                    cascaderType:'url',
+                    options:'',
+                    level:2,
+                    width:'100%',
+                    height:'400px',
+                    limit:1,
+                    disks:'local_public',
+                    accept:getAccept(),
+                    multiple:0,
+                    label:['键名','键值']
+                };
+                if(row.edit.startsWith("{") && row.edit.endsWith("}")){
+                    let edit=JSON.parse(row.edit);
+                    if(edit.multiple){
+                        edit.multiple=1;
+                    }
+                    if(edit.readonly){
+                        edit.readonly=1;
+                    }
+                    this.formDialog.data=Object.assign(obj,edit);
+                }else{
+                    this.formDialog.data=obj;
+                    parseShort(this.formDialog.data);
+                    this.parseForm();
+                }
+                this.formDialog.show=true;
+            },
+            confirmForm:function (){
+                let data=this.formDialog.data;
+                let row=this.formDialog.row;
+                let field=[];
+                switch (data.form){
+                    case 'input':
+                        field=['form','type','placeholder','append','prepend','readonly','value'];
+                        break;
+                    case 'select':
+                        field=['form','placeholder','placeholder','value','multiple'];
+                        break;
+                    case 'radio':
+                    case 'checkbox':
+                    case 'switch':
+                        field=['form','value'];
+                        break;
+                    case 'editor':
+                        field=['form','width','height','value'];
+                        break;
+                    case 'date-picker':
+                    case 'time-picker':
+                        field=['form','type','placeholder','value'];
+                        break;
+                    case 'cascader':
+                        if(data.cascaderType=='url'){
+                            field=['form','placeholder','url','level','multiple','value'];
+                        }
+                        if(data.cascaderType=='options'){
+                            field=['form','placeholder','options','multiple','value'];
+                        }
+                        break;
+                    case 'selectpage':
+                        field=['form','placeholder','url','labelField','keyField','multiple','value'];
+                        break;
+                    case 'attachment':
+                        field=['form','limit','value'];
+                        break;
+                    case 'files':
+                        if(data.limit>1){
+                            data.multiple=1;
+                        }
+                        field=['form','limit','accept','disks','value','multiple'];
+                        break;
+                    case 'fieldlist':
+                        field=['form','label','value'];
+                        break;
+                    case 'slot':
+                        field=['form'];
+                        break;
+                }
+                let r={};
+                for(let key in data){
+                    if(inArray(field,key)){
+                        if(!data[key]){
+                            continue;
+                        }
+                        if(key=='multiple' || key=='readonly'){
+                            if(!data[key]){
+                                continue;
+                            }else{
+                                data[key]=true;
+                            }
+                        }
+                        r[key]=data[key];
+                    }
+                }
+                row.edit=JSON.stringify(r);
+                if(this.formDialog.searchList && Object.keys(this.formDialog.searchList).length>0){
+                    row.searchList=JSON.stringify(this.formDialog.searchList);
+                }else{
+                    row.searchList='';
+                }
+                this.formDialog.show=false;
+            },
+            changeAction:function (action){
+                let recyclebin=false;
+                for(let k in action){
+                    if(k=='recyclebin'){
+                        recyclebin=true;
+                        break;
+                    }
+                }
+                this.crudForm.recyclebin=recyclebin;
+                this.crudForm.actionList=action;
+            },
+            isShowEdit:function (row){
+                if(!row.edit){
+                    return false;
+                }
+                if(row.edit=='hidden'){
+                    return false
+                }
+                if(row.edit.startsWith("{") && row.edit.endsWith("}")){
+                    let edit=JSON.parse(row.edit);
+                    if(edit.form=='input' && edit.type=='hidden'){
+                        return false;
+                    }
+                }
+                return true;
+            },
+            openIconPanel:function (){
+                this.$refs.checkicon.open();
+            },
+            clear:function (){
+                Yunqi.confirm(__('支持清除一个小时内的操作,你确定要清除吗')).then(res=>{
+                    let postdata={
+                        table:this.crudForm.table,
+                        controller:this.crudForm.controller,
+                        model:this.crudForm.model,
+                        fields:this.tableData,
+                        actionList:this.crudForm.actionList,
+                        actions:{table:0,form:0}
+                    };
+                    Yunqi.ajax.json('develop/clear',postdata,true,true);
+                });
+            },
+            submit:function (type){
+                let postdata={
+                    table:this.crudForm.table,
+                    controller:this.crudForm.controller,
+                    model:this.crudForm.model,
+                    reduced:this.crudForm.reduced,
+                    actionList:this.crudForm.actionList,
+                    fields:this.tableData,
+                    isTree:this.crudForm.isTree,
+                    treeTitle:this.crudForm.treeTitle,
+                    pagination:!this.crudForm.pagination,
+                    tabs:this.crudForm.tabs,
+                    summary:this.crudForm.summary,
+                    expand:this.crudForm.expand,
+                    type:type,
+                    actions:{
+                        table:0,
+                        form:0
+                    }
+                };
+                if(inArray(this.actions,'table')){
+                    postdata.actions.table=1;
+                }
+                if(inArray(this.actions,'form')){
+                    postdata.actions.form=1;
+                }
+                Yunqi.ajax.json('develop/crud',postdata,true,true).then(res=>{
+                    if(type=='file'){
+                        Yunqi.api.addtabs({
+                            url:res,
+                            title:__('查看'),
+                            icon:'fa fa-th-large',
+                        });
+                    }
+                    if(type=='code'){
+                        this.codeDialog.row=res;
+                        this.codeDialog.show=true;
+                    }
+                });
+            }
+        }
+    }
+</script>
+<style>
+    .checklabel{
+        position: relative;top: 2px;cursor: pointer;
+    }
+    .checklabel input{
+        border: var(--el-checkbox-input-border);
+    }
+    .checklabel span{
+        position: relative;top:-2px;left: 5px;
+    }
+</style>

+ 236 - 0
app/admin/view/develop/queue.html

@@ -0,0 +1,236 @@
+<template>
+    <el-card shadow="never">
+        <template #header>
+            <el-alert effect="dark" :closable="false" title="使用说明">任务队列为异步执行,通常用于定时任务,循环任务,发送消息等,让队列任务与主业务进行解耦,使其不阻塞主业务的操作</el-alert>
+        </template>
+        <el-form label-width="120px" label-position="top">
+            <el-form-item label="{:__('服务状态')}:">
+                <el-alert style="margin-bottom:10px;" type="warning">
+                    如果服务启动不成功,请在根目录下手动执行命令:<el-tag style="margin:0 10px;">php think Queue</el-tag>linux环境下使用nohup让服务在后台运行<el-tag style="margin:0 10px;">nohup php think Queue > queue.log &</el-tag>windows环境下请勿关闭进程窗口。
+                </el-alert>
+                <div class="status" v-if="!status">
+                    <i class="fa fa-spinner fa-spin"></i>
+                    <span style="margin-left: 5px;">服务检测中..</span>
+                </div>
+                <div class="status" v-if="status == 'normal'">
+                    <el-tag  type="success">{:__('正常运行')}</el-tag>
+                    <span style="margin-left: 30px;margin-right: 30px;">运行时长:{{parseTime(keeptime)}}</span>
+                    <el-button size="small" type="danger" @click="changeStatus(0)">停止运行</el-button>
+                </div>
+                <div class="status" v-if="status == 'hidden'">
+                    <el-tag type="info">{:__('已经停止')}</el-tag>
+                    <span style="margin-left: 30px;margin-right: 30px;">停止时间:{{stoptime}}</span>
+                    <el-button size="small" type="success" @click="changeStatus(1)">启动服务</el-button>
+                </div>
+            </el-form-item>
+            <el-divider></el-divider>
+            <el-form-item label="{:__('添加任务')}:">
+                <el-alert style="margin-bottom:10px;" type="warning">添加新任务需要完成一个处理类,该处理类实现了接口<el-tag>app\admin\command\queueEvent\EventInterFace</el-tag>接口。</el-alert>
+                <el-button type="success" size="small" @click="addEvent">{:__('添加')}</el-button>
+            </el-form-item>
+            <el-divider></el-divider>
+            <el-form-item label="{:__('当前任务')}:">
+                <el-alert style="margin-bottom:10px;" type="warning">运行中的任务,每5分钟刷新一次。</el-alert>
+                <el-table :data="eventList" style="width: 100%">
+                    <el-table-column prop="title" label="{:__('任务名称')}"></el-table-column>
+                    <el-table-column prop="function" label="{:__('处理类')}"></el-table-column>
+                    <el-table-column label="{:__('任务类型')}">
+                        <template #default="{row}">
+                            <el-tag v-if="row.limit == 0">{:__('循环任务')}</el-tag>
+                            <el-tag v-else type="warning">{:__('定时任务')}-{{row.limit}}{:__('次')}</el-tag>
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="times" label="{:__('已执行次数')}"></el-table-column>
+                    <el-table-column label="{:__('执行间隔')}">
+                        <template #default="{row}">
+                            {{parseTime(row.delay) || '立即执行'}}
+                        </template>
+                    </el-table-column>
+                    <el-table-column label="{:__('规则限制')}" width="200">
+                        <template #default="{row}">
+                            {{parseFilter(row.filter)}}
+                        </template>
+                    </el-table-column>
+                    <el-table-column label="{:__('上次执行时间')}" width="200">
+                        <template #default="{row}">
+                            {{parseLasttime(row.lasttime)}}
+                        </template>
+                    </el-table-column>
+                    <el-table-column label="{:__('错误')}" width="200">
+                        <template #default="{row}">
+                            <el-tag type="danger" v-if="row.error">{{row.error}}</el-tag>
+                        </template>
+                    </el-table-column>
+                    <el-table-column label="{:__('状态')}" width="120">
+                        <template #default="{row}">
+                            <el-switch active-value="normal" inactive-value="hidden" v-model="row.status"></el-switch>
+                        </template>
+                    </el-table-column>
+                    <el-table-column label="{:__('操作')}" width="120">
+                        <template #default="{row}">
+                            <el-button type="danger" size="small" @click="delEvent(row.id)">{:__('删除')}</el-button>
+                        </template>
+                    </el-table-column>
+                </el-table>
+            </el-form-item>
+            <el-form-item label="{:__('运行日志')}:">
+                <el-input type="textarea" autosize v-model="log.content" style="margin-bottom: 10px;"></el-input>
+                <el-pagination :current-page="log.currentPage" :small="true" background @current-change="getLog" layout="prev, pager, next" :page-count="log.total" />
+            </el-form-item>
+        </el-form>
+    </el-card>
+</template>
+<script>
+    let inter;
+    export default{
+        data:{
+            eventList:[],
+            log:{
+                currentPage:1,
+                content:'',
+                total:1
+            },
+            status:'',
+            keeptime:0,
+            stoptime:''
+        },
+        onLoad:function(){
+            Yunqi.ajax.get('develop/queueLog',{type:'total'}).then(res=>{
+                this.log.total=res;
+                this.log.currentPage=res;
+            });
+            Yunqi.ajax.get('develop/queue').then(res=>{
+                this.eventList=res;
+            });
+            this.getStatus();
+        },
+        methods: {
+            changeStatus:function (status){
+                Yunqi.ajax.post('develop/queueStatus',{status:status}).then(res=>{
+                    this.getStatus();
+                    if(status){
+                        this.log.total=1;
+                        this.log.currentPage=1;
+                    }
+                }).catch(err=>{
+                    this.getStatus();
+                });
+            },
+            getLog:function (e){
+                this.log.currentPage=e;
+                Yunqi.ajax.get('develop/queueLog',{type:'content',page:e}).then(res=>{
+                    this.log.content=res;
+                });
+            },
+            getStatus:function (){
+                if(inter){
+                    clearInterval(inter);
+                }
+                Yunqi.ajax.get('develop/queueStatus').then(res=>{
+                    this.status=res.status;
+                    this.keeptime=res.keeptime;
+                    this.stoptime=res.stoptime;
+                    inter=setInterval(()=>{
+                        if(this.keeptime%5===0 && this.log.currentPage==this.log.total){
+                            this.getLog(this.log.total);
+                        }
+                        this.keeptime++;
+                    },1000)
+                });
+            },
+            delEvent:function (id){
+                Yunqi.ajax.post('develop/delQueue',{id:id}).then(res=>{
+                    this.eventList=res;
+                });
+            },
+            addEvent:function (){
+                let that=this;
+                Yunqi.api.open({
+                    url:'develop/addQueue',
+                    title:'添加任务',
+                    close:function (){
+                        Yunqi.ajax.get('develop/queue').then(res=>{
+                            that.eventList=res;
+                        });
+                    }
+                });
+            },
+            parseTime:function (second){
+                if(second===0){
+                    return 0;
+                }
+                if(second>0 && second<60){
+                    return second+'秒';
+                }
+                if(second>=60 && second<3600){
+                    let r=Math.floor(second/60)+'分钟';
+                    if(second%60>0){
+                        r+=second%60+'秒';
+                    }
+                    return r;
+                }
+                if(second>=3600 && second<86400){
+                    let r=Math.floor(second/3600)+'小时';
+                    if(second%3600>60){
+                        r+=Math.floor(second%3600/60)+'分钟';
+                    }
+                    if(second%3600%60>0){
+                        r+=second%3600%60+'秒';
+                    }
+                    return r;
+                }
+                if(second>=86400){
+                    let r=Math.floor(second/86400)+'天';
+                    if(second%86400>3600){
+                        r+=Math.floor(second%86400/3600)+'小时';
+                    }
+                    if(second%86400%3600>60){
+                        r+=Math.floor(second%86400%3600/60)+'分钟';
+                    }
+                    if(second%86400%3600%60>0){
+                        r+=second%86400%3600%60+'秒';
+                    }
+                    return r;
+                }
+            },
+            parseFilter:function (obj){
+                if(!obj){
+                    return '-';
+                }
+                let r='';
+                if(obj.Y!==undefined){
+                    r+='year:'+obj.Y+' ';
+                }
+                if(obj.m!==undefined){
+                    r+='month:'+obj.m+' ';
+                }
+                if(obj.d!==undefined){
+                    r+='day:'+obj.d+' ';
+                }
+                if(obj.H!==undefined){
+                    r+='hours:'+obj.H+' ';
+                }
+                if(obj.i!==undefined){
+                    r+='minute:'+obj.i+' ';
+                }
+                if(obj.s!==undefined){
+                    r+='second:'+obj.s+' ';
+                }
+                //去掉r最后的空格
+                return r.replace(/\s*$/,'');
+            },
+            parseLasttime:function (data){
+                if(!data){
+                    return '-';
+                }
+                return data;
+            }
+        }
+    }
+</script>
+<style>
+    .status{
+        display: flex;
+        align-items: center;
+    }
+</style>

+ 166 - 0
app/admin/view/general/attachment/add.html

@@ -0,0 +1,166 @@
+<template>
+    <el-card shadow="never" style="border: 0;">
+        <div class="form-body">
+            <el-form ref="formRef" :model="addform" :rules="addformRules" label-width="120px">
+                <el-form-item label="{:__('所属类别')}:">
+                       <el-select v-model="addform.category" placeholder="请选择所属类别" :clearable="true" style="width: 100%">
+                            {foreach name="categoryList" id="item"}
+                            <el-option key="{$key}" label="{$item}" value="{$key}"></el-option>
+                            {/foreach}
+                       </el-select>
+                </el-form-item>
+                <el-form-item label="{:__('存储方式')}:" prop="disks">
+                        <el-select v-model="addform.disks" placeholder="请选择存储方式" :clearable="true" style="width: 100%">
+                              {foreach name="disksList" id="item"}
+                              <el-option key="{$key}" label="{$item}" value="{$key}"></el-option>
+                              {/foreach}
+                        </el-select>
+                </el-form-item>
+                <el-form-item label="{:__('上传文件')}:" prop="files">
+                    <el-upload
+                            :multiple="true"
+                            ref="uploadRef"
+                            :accept="accept"
+                            :limit="10"
+                            :on-change="fileUploadChange"
+                            action="{$config.baseUrl}{$config.upload.uploadurl}"
+                            :headers="{'x-requested-with': 'XMLHttpRequest'}"
+                            :data="{disks:addform.disks,category:addform.category}"
+                            :auto-upload="false"
+                            list-type="picture-card">
+                            <i class="fa fa-plus"></i>
+                            <template #file="{file}">
+                                <div class="fileupload-thumb">
+                                    <i class="fa fa-times" @click.stop="removeFile(file)"></i>
+                                    <img :src="showFileThumb(file)" class="thumb" style="width: 100%;height: 100%"/>
+                                    <el-progress status="success" :text-inside="true" :stroke-width="20" :percentage="file.percentage"></el-progress>
+                                </div>
+                            </template>
+                    </el-upload>
+                </el-form-item>
+                <el-form-item class="form-footer">
+                    <el-button type="primary" @click="submit"><i class="fa fa-check"></i>&nbsp;提交</el-button>
+                    <el-button type="info"  @click="reset"><i class="fa fa-reply"></i>&nbsp;重置</el-button>
+                </el-form-item>
+            </el-form>
+        </div>
+    </el-card>
+</template>
+<script>
+    import form from "@components/Form.js";
+    import {inArray} from "@util.js";
+    export default{
+        components:{'YunForm':form},
+        data:{
+            addform:{
+                category:'',
+                disks:'',
+                ready:0,
+                success:0
+            },
+            addformRules:{
+                disks:[{required:true,message:'请选择存储方式',trigger:'change'}],
+                ready:[{required:true,min:1,message:'请上传文件',trigger:'change'}]
+            },
+            accept:''
+        },
+        onLoad:function (){
+            let mimetype=Yunqi.config.upload.mimetype.split(',');
+            let accept=[];
+            mimetype.forEach(res=>{
+                accept.push('.'+res);
+            });
+            this.accept=accept.join(',');
+        },
+        methods: {
+            //添加页面
+            showFileThumb:function (file) {
+                let filename=file.name.toLowerCase();
+                let icon=filename.slice(filename.lastIndexOf('.')+1);
+                if(inArray(['jpg','jpeg','png','gif','bmp'],icon)){
+                    return file.url;
+                }else if(inArray(['doc','docx'],icon)){
+                    let iconpath=location.origin+'/assets/img/fileicon/doc.png';
+                    return iconpath;
+                }else if(inArray(['ppt','pptx'],icon)){
+                    let iconpath=location.origin+'/assets/img/fileicon/ppt.png';
+                    return iconpath;
+                }else if(inArray(['xls','xlsx'],icon)){
+                    let iconpath=location.origin+'/assets/img/fileicon/xls.png';
+                    return iconpath;
+                }else if(inArray(['mp3','wav','wma','ogg'],icon)){
+                    let iconpath=location.origin+'/assets/img/fileicon/audio.png';
+                    return iconpath;
+                }else if(inArray(['mp4', 'avi', 'rmvb','swf', 'flv','rm', 'ram', 'mpeg', 'mpg', 'wmv', 'mov'],icon)){
+                    let iconpath=location.origin+'/assets/img/fileicon/video.png';
+                    return iconpath;
+                }else if(inArray(['zip', 'rar', '7z', 'gz', 'tar'],icon)){
+                    let iconpath=location.origin+'/assets/img/fileicon/zip.png';
+                    return iconpath;
+                }else if(inArray(['apk','tiff','exe','html','pdf','psd','visio','svg','txt','xml'],icon)){
+                    let iconpath=location.origin+'/assets/img/fileicon/'+icon+'.png';
+                    return iconpath;
+                }else{
+                    let iconpath=location.origin+'/assets/img/fileicon/wz.png';
+                    return iconpath;
+                }
+            },
+            submit:function (){
+                let elloading=Yunqi.loading({background:'rgba(255,255,255,0.3)',text:'请求中..'});
+                this.$refs.formRef.validate((valid, fields)=>{
+                    if(valid){
+                        this.$refs.uploadRef.submit();
+                        setInterval(()=>{
+                            if(this.addform.success==this.addform.ready){
+                                Yunqi.api.closelayer(Yunqi.config.window.id,true);
+                                elloading.close();
+                            }
+                        },100);
+                    }else{
+                        elloading.close();
+                    }
+                });
+            },
+            reset:function () {
+                this.addform={
+                    category:'',
+                    disks:'',
+                    files:[]
+                };
+            },
+            removeFile:function (file) {
+                this.$refs.uploadRef.handleRemove(file);
+                this.addform.ready--;
+            },
+            fileUploadChange:function (file) {
+                if(file.status=='ready'){
+                    let maxsize=Number(Yunqi.config.upload.maxsize);
+                    if(file.size>1024*1024*maxsize){
+                        Yunqi.message.error(__('文件大小不能超过'+maxsize+'mb'));
+                        return false;
+                    }
+                    this.addform.ready++;
+                }
+                if(file.status=='success'){
+                    this.addform.success++;
+                }
+            }
+        }
+    }
+</script>
+<style>
+    .fileupload-thumb{
+        overflow: hidden;
+        position: relative;
+    }
+    .fa-times{
+        position: absolute;
+        right: 0px;
+        top: 0px;
+        background: var(--el-color-danger);
+        color: #fff;
+        padding:2px 4px;
+        border-radius: 50%;
+        cursor: pointer;
+    }
+</style>

+ 129 - 0
app/admin/view/general/attachment/index.html

@@ -0,0 +1,129 @@
+<template>
+    <el-card shadow="never">
+        <yun-table
+                :columns="columns"
+                tabs="category"
+                search="filename"
+                toolbar="refresh,add,del,guilei"
+                :auth="auth"
+                :extend="extend">
+                <template #toolbar="{tool,selections}">
+                    <el-dropdown trigger="click" v-if="tool=='guilei'">
+                        <el-button type="warning" :disabled="selections.length?false:true"><i class="fa fa-arrow-right"></i>&nbsp;归类</el-button>
+                        <template #dropdown>
+                            <el-dropdown-menu>
+                                {foreach name="categoryList" id="item"}
+                                <el-dropdown-item @click.stop="changeCategory(selections,'{$key}')"><i class="fa fa-eye"></i> {$item}</el-dropdown-item>
+                                {/foreach}
+                            </el-dropdown-menu>
+                        </template>
+                    </el-dropdown>
+                </template>
+                <template #formatter="{field,rows}">
+                    <div v-if="field=='filename'">
+                        <el-tooltip
+                                effect="dark"
+                                :content="rows.filename"
+                                placement="top">
+                                <el-tag style="cursor: pointer;">{{formateName(rows.filename)}}</el-tag>
+                        </el-tooltip>
+                    </div>
+                </template>
+        </yun-table>
+    </el-card>
+</template>
+<script>
+    import table from "@components/Table.js";
+    export default{
+        components:{'YunTable':table},
+        data:{
+            auth:{
+                del:Yunqi.auth.check('app\\admin\\controller\\general\\Attachment','del'),
+                add:Yunqi.auth.check('app\\admin\\controller\\general\\Attachment','add'),
+                multi:Yunqi.auth.check('app\\admin\\controller\\general\\Attachment','multi'),
+            },
+            //列表页面
+            extend:{
+                index_url: 'general/attachment/index',
+                add_url: 'general/attachment/add',
+                del_url: 'general/attachment/del',
+                multi_url: 'general/attachment/multi'
+            },
+            columns:[
+                {checkbox: true},
+                {field: 'id',title:'ID',operate:false,edit:'hidden'},
+                {field: 'category', title: __('类别'),visible:false,operate: false, formatter: Yunqi.formatter.tag, searchList:Yunqi.data.categoryList},
+                {field: 'admin_id', title: __('管理员ID'), visible: false,operate:false},
+                {field: 'user_id', title: __('会员ID'), visible: false,operate:false},
+                {field: 'thumburl', title: __('缩略图'), operate: false,formatter: Yunqi.formatter.image},
+                {field: 'filename', title: __('文件名'),align:'left',operate: 'like',formatter: Yunqi.formatter.slot},
+                {field: 'fullurl', title: __('源文件'),align:'left',operate: false,formatter: Yunqi.formatter.link},
+                {
+                    field: 'filesize', title: __('文件大小'),operate: false, sortable: true, formatter: function (value, row) {
+                        var size = parseFloat(value);
+                        var i = Math.floor(Math.log(size) / Math.log(1024));
+                        return (size / Math.pow(1024, i)).toFixed(i < 2 ? 0 : 2) * 1 + ' ' + ['B', 'KB', 'MB', 'GB', 'TB'][i];
+                    }
+                },
+                {field: 'is_image', title: __('图片'), operate: false,searchList: {1: __('是'), 0: __('否')},formatter: function(data,row){
+                        let tag=Yunqi.formatter.tag;
+                        if(row.is_image){
+                            tag.value='是';
+                            tag.type='success';
+                        }else{
+                            tag.value='否';
+                            tag.type='danger';
+                        }
+                        return tag;
+                    }},
+                {field: 'imagetype', title: __('图片类型'), operate: false},
+                {field: 'imagewidth', title: __('宽度'), operate: false},
+                {field: 'imageheight', title: __('高度'), operate: false},
+                {field: 'storage', title: __('存储方式'), operate: false,searchList: Yunqi.data.disksList,formatter: Yunqi.formatter.tag},
+                {
+                    field: 'createtime',
+                    title: __('创建时间'),
+                    formatter: Yunqi.formatter.datetime,
+                    operate: {form:'date-picker',type:'daterange'},
+                    sortable: true
+                },
+                {
+                    field: 'operate',
+                    fixed: 'right',
+                    title: __('操作'),
+                    width:50,
+                    action:{
+                        del:true
+                    }
+                }
+            ]
+        },
+        methods: {
+            changeCategory:function (selections,key){
+                if(selections.length==0){
+                    return;
+                }
+                let ids=[];
+                selections.forEach(res=>{
+                    ids.push(res.id);
+                });
+                Yunqi.api.multi(this.extend.multi_url,{ids:ids,field:'category',value:key},function(){
+                    location.reload();
+                });
+            },
+            formateName:function (data){
+                //获取data的后缀名
+                let ext=data.substring(data.lastIndexOf('.')+1);
+                let name=data.substring(0,data.lastIndexOf('.'));
+                if(name.length>8){
+                    //取末尾5个字符
+                    return name.substring(0,3)+'...'+name.substring(name.length-5)+'.'+ext;
+                }
+                return data;
+            }
+        }
+    }
+</script>
+<style>
+
+</style>

+ 129 - 0
app/admin/view/general/attachment/select.html

@@ -0,0 +1,129 @@
+<template>
+    <div class="filebox">
+        <el-card shadow="never">
+            <div class="left">
+                <el-button type="primary" @click="addCate">添加分类</el-button>
+                <el-input placeholder="请输入分类搜索" style="margin: 15px 0;"></el-input>
+                <el-scrollbar height="320px" style="margin:0 -10px;">
+                    <div :class="['li',category=='all'?'active':'']" @click="checkCategory('all');">
+                        <i :class="['fa',category=='all'?'fa-folder-open-o':'fa-folder-o']"></i>&nbsp;&nbsp;全部
+                    </div>
+                    <div :class="['li',category=='unclassed'?'active':'']" @click="checkCategory('unclassed');">
+                        <i :class="['fa',category==''?'fa-folder-open-o':'fa-folder-o']"></i>&nbsp;&nbsp;未归类
+                    </div>
+                    <template  v-for="(item,key) in catelist">
+                        <div :class="['li',category==key?'active':'']" @click="checkCategory(key);">
+                            <i :class="['fa',category==key?'fa-folder-open-o':'fa-folder-o']"></i>&nbsp;&nbsp;{{item}}
+                            <div class="fr"><span @click.stop="editCate(key,item)">编辑</span>|<span @click.stop="delCate(key,item)">删除</span></div>
+                        </div>
+                    </template>
+                </el-scrollbar>
+            </div>
+            <div class="right">
+                <el-input placeholder="请输入名称搜索" style="width: 725px;margin-left: 7px;" v-model="keywords">
+                    <template #append>
+                        <el-button type="primary" size="small" @click="refresh">搜索</el-button>
+                    </template>
+                </el-input>
+                <el-upload
+                        :multiple="true"
+                        accept="image/*"
+                        action="{$config.baseUrl}{$config.upload.uploadurl}"
+                        :data="{category:category,disks:'{$config.upload.disks}'}"
+                        :headers="{'x-requested-with': 'XMLHttpRequest'}"
+                        list-type="picture-card"
+                        :on-success="beforeUploadSuccess"
+                        :file-list="list">
+                    <i class="fa fa-cloud-upload"></i>
+                    <template #file="{file}">
+                        <div :class="inArray(file.fullurl)!==false?'checked':''" @click="checkImg(file)">
+                            <div class="img">
+                                <img @load="parseImg" class="upload-file-img" :data-url="file.thumburl" style="width: 100%"/>
+                            </div>
+                            <span>{{file.filename}}</span>
+                            <i class="fa fa-check"></i>
+                        </div>
+                    </template>
+                </el-upload>
+                <el-pagination
+                        style="position: absolute;bottom: 15px;right: 15px"
+                        @current-change="pageChange"
+                        small
+                        :page-size="17"
+                        :current-page="page"
+                        background
+                        layout="prev, pager, next"
+                        :total="total"
+                ></el-pagination>
+            </div>
+        </el-card>
+        <div class="footer">
+            <el-button type="danger" @click="delPic">删除</el-button>
+            <el-button type="warning" @click="setCategory">归类</el-button>
+            <el-button type="primary" @click="confirmImg">选定</el-button>
+            <el-button type="success" @click="refresh">刷新</el-button>
+        </div>
+    </div>
+    <el-dialog
+            v-model="editCateForm.show"
+            destroy-on-close
+            :lock-scroll="false"
+            :title="editCateForm.title"
+            width="50%"
+            :modal="false"
+            align-center>
+        <el-select style="width: 100%" v-model="editCateForm.key" v-if="editCateForm.type=='set'">
+            <el-option
+                    v-for="(item,key) in catelist"
+                    :key="key"
+                    :label="item"
+                    :value="key">
+            </el-option>
+        </el-select>
+        <el-input placeholder="请输入分类名称" v-model="editCateForm.value" v-else></el-input>
+        <template #footer>
+              <span class="dialog-footer">
+                <el-button type="danger" @click="editCateForm.show=false">取消</el-button>
+                <el-button type="primary" @click="confirm">确认</el-button>
+              </span>
+        </template>
+    </el-dialog>
+</template>
+<script>
+    export default{
+        data:{
+            list:[],
+            page:1,
+            category:'all',
+            catelist:Yunqi.data.categoryList,
+            checked:[],
+            keywords:'',
+            total:0,
+            editCateForm:{
+                show:false,
+                type:'add',
+                key:'',
+                value:''
+            },
+            loading:false,
+            limit:Yunqi.data.limit,
+        },
+        onLoad:function (){
+            this.getImglist();
+        },
+        methods: {
+            inArray:function (url) {
+                let r=false;
+                for(let i=0;i<this.checked.length;i++){
+                    if(this.checked[i].fullurl==url){
+                        r=i;
+                        break;
+                    }
+                }
+                return r;
+            },
+            beforeUploadSuccess:function (res,file) {
+                if(res.code===0){
+                    Yunqi.message.error(__(res.msg));
+                }
+                th

+ 51 - 0
app/admin/view/general/category/add.html

@@ -0,0 +1,51 @@
+<template>
+    <el-card shadow="never" style="border: 0;">
+        <template #header>
+            <el-alert type="warning" :closable="false">{:__('【所属分组】请前往常规管理->系统配置->配置分组中进行管理')}</el-alert>
+        </template>
+        <yun-form
+            :data="data"
+            :columns="columns">
+            <template #default>
+                {:token_field()}
+            </template>
+            <template v-slot:pid="{rows}">
+                <el-form-item label="{:__('父级')}:" prop="pid">
+                    <el-select placeholder="{:__('请选择父级')}" v-model="rows.pid" :clearable="true" style="width: 100%">
+                        <el-option key="all" label="无" value="0"></el-option>
+                        {foreach name="parentList" item="vo"}
+                        <el-option v-if="rows.type=='{$vo.type}'" key="{$key}" label="{:str_replace('&amp;','&',$vo.name)}" value="{$key}"></el-option>
+                        {/foreach}
+                    </el-select>
+                </el-form-item>
+            </template>
+        </yun-form>
+    </el-card>
+</template>
+<script>
+    import form from "@components/Form.js";
+    export default{
+        components:{'YunForm':form},
+        data:{
+            data:Yunqi.data.row || {},
+            columns:[
+                {field: 'id',title:'ID',edit:'hidden'},
+                {field: 'type', title: __('所属分组'),searchList:Yunqi.data.typeList,edit: {form:'select',change:'changeType'},rules:'required'},
+                {field: 'pid',title:__('父级'),edit: {form:'slot',value:'0'}},
+                {field: 'name',title:__('名称'),edit:'text',rules:'required'},
+                {field: 'nickname',title:__('昵称'),edit:'text'},
+                {field: 'image', title: __('图片'),edit:'image'},
+                {field: 'weigh', title: __('权重'),edit:(Yunqi.config.action=='edit')?'number':false},
+                {field: 'status', title: __('状态'),edit:'switch',searchList: {'normal': __('正常'),'hidden': __('隐藏')}},
+            ]
+        },
+        methods: {
+            changeType:function (data,row){
+                row.pid='0';
+            }
+        }
+    }
+</script>
+<style>
+
+</style>

+ 1 - 0
app/admin/view/general/category/edit.html

@@ -0,0 +1 @@
+{include vue="general/category/add" /}

+ 64 - 0
app/admin/view/general/category/index.html

@@ -0,0 +1,64 @@
+<template>
+    <el-card shadow="never">
+        <template #header>
+            <el-alert effect="dark" :closable="false">在表单中组件中,可以通过{form:"cascader",options:"ajax/category"}来读取多级分类</el-alert>
+        </template>
+        <yun-table
+                :columns="columns"
+                :common-search="false"
+                :pagination="false"
+                tabs="type"
+                toolbar="refresh,add,edit,del,more"
+                :extend="extend">
+        </yun-table>
+    </el-card>
+</template>
+<script>
+    import table from "@components/Table.js";
+    export default{
+        components:{'YunTable':table},
+        data:{
+            extend:{
+                index_url: 'general/category/index',
+                add_url: 'general/category/add',
+                edit_url: 'general/category/edit',
+                del_url: 'general/category/del',
+                multi_url: 'general/category/multi'
+            },
+            columns:[
+                {checkbox: true},
+                {field: 'id',title:'ID',width:80},
+                {field: 'type', title: __('所属分组'),width:120,searchList:Yunqi.data.typeList,formatter:Yunqi.formatter.tag},
+                {field: 'name',title:__('名称'),formatter:function(data){
+                    let html=Yunqi.formatter.html;
+                    html.value=data.replace(/&nbsp;/g,'&nbsp;&nbsp;');
+                    return html;
+                }},
+                {field: 'nickname',title:__('昵称')},
+                {field: 'image', title: __('图片'),width:90,formatter: function (data){
+                    let image=Yunqi.formatter.image;
+                    image.value=data;
+                    image.width=30;
+                    image.height=30;
+                    return image;
+                }},
+                {field: 'weigh', title: __('权重'),width:80},
+                {field: 'status', title: __('状态'),width:120,searchList: {'normal': __('正常'),'hidden': __('隐藏')},formatter:Yunqi.formatter.switch},
+                {treeExpand:true},
+                {
+                    field: 'operate',
+                    title: __('操作'),
+                    width:150,
+                    action:{sort:true,edit:true,del:true}
+                }
+            ]
+        },
+        methods: {
+
+        }
+    }
+</script>
+<style>
+
+</style>
+

+ 327 - 0
app/admin/view/general/config/index.html

@@ -0,0 +1,327 @@
+<template>
+    <el-card shadow="never">
+        <template #header>
+            <el-alert effect="dark" :closable="false" title="使用说明">在此处定义的变量可以在全局通过site_config("组名.变量名")使用</el-alert>
+        </template>
+        <el-tabs type="card" v-model="tabValue" @tab-change="tabChange">
+            <el-tab-pane :name="key" v-for="(label,key) in groupList" :label="label"></el-tab-pane>
+            {if $app_debug}
+            <el-tab-pane name="addconfig">
+                <template #label>
+                    <i class="fa fa-plus"></i>&nbsp;添加配置
+                </template>
+            </el-tab-pane>
+            {/if}
+        </el-tabs>
+        <div class="form-container">
+            <yun-form
+                    label-position="left"
+                    require-asterisk-position="right"
+                    :label-width="200"
+                    @submit="onSubmit"
+                    :append-width="8"
+                    :action="extend.edit_url"
+                    @success="onSuccess"
+                    v-if="columns && tabValue!='addconfig'"
+                    :columns="columns">
+                <template #default>
+                    <el-form-item>
+                        <template #label><span class="bolderText">变量标题</span></template>
+                        <el-row style="width:100%">
+                            <el-col :span="16">
+                                <span class="bolderText">变量值</span>
+                            </el-col>
+                            {if $app_debug}
+                            <el-col :span="8">
+                                <div class="bolderText" style="padding-left: 50px;">读取方式</div>
+                            </el-col>
+                            {/if}
+                        </el-row>
+                    </el-form-item>
+                </template>
+                <template #addons="{value}">
+                    <el-divider>
+                        <el-tag v-if="value.name">{{value.type}}-{{value.name}}</el-tag>
+                        <el-tag v-else type="warning">未安装扩展-{{value.key}}</el-tag>
+                    </el-divider>
+                </template>
+                {if $app_debug}
+                <template #append="item">
+                    <div style="padding-left:30px;">
+                        <span>{{formatVar(item.column.field,item.column.addons)}}</span>
+                        <el-button style="position: absolute;right: 0" @click="delVar(item.column.field)" size="small" type="danger" v-if="item.column.can_delete">{:__('删除')}</el-button>
+                    </div>
+                </template>
+                {/if}
+            </yun-form>
+            <yun-form
+                    v-if="tabValue=='addconfig'"
+                    :action="extend.add_url"
+                    @success="onSuccess"
+                    ref="yunform"
+                    :columns="addconfig">
+            </yun-form>
+        </div>
+    </el-card>
+</template>
+<script>
+    import form from "@components/Form.js";
+    import {inArray} from "@util.js";
+    export default{
+        components:{'YunForm':form},
+        data:{
+            extend:{
+                index_url: 'general/config/index',
+                add_url: 'general/config/add',
+                edit_url: 'general/config/edit',
+                del_url: 'general/config/del'
+            },
+            addconfig:[
+                {field:'group',title:__('分组'),searchList:Yunqi.data.groupList,edit: {form:'select',value:'basic',change:'changeGroup'},rules:'required'},
+                {field:'addons_pack',title:__('扩展包名'),edit:{form:'input',type:'text'},visible:false},
+                {field:'type',title:__('类型'),searchList:Yunqi.data.typeList,edit: {form:'select',change:'changeType',value:'text'},rules:'required'},
+                {field:'title',title:__('变量标题'),edit:'text',rules:'required'},
+                {field:'name',title:__('变量名'),edit:'text',rules:'required'},
+                {field:'url',title:__('分页列表Url'),edit: {form:'input',type:'text'},visible:false},
+                {field:'labelField',title:__('显示字段'),edit: {form:'input',type:'text',placeholder:'请输入显示字段labelField'},visible:false},
+                {field:'keyField',title:__('存储字段'),edit: {form:'input',type:'text',placeholder:'请输入显示字段keyField'},visible:false},
+                {field:'options',title:__('选项'),edit: {form:'fieldlist',label:['键名','键值']},visible:false},
+                {field:'value',title:__('默认值'),edit:'text'},
+                {field:'label',title:__('JSON标题'),edit: {form:'input',type:'text',placeholder:'请输入Fieldlist的标题label,用“,”隔开'},visible:false},
+                {field:'keys',title:__('JSON Keys'),edit: {form:'input',type:'text',placeholder:'请输入Fieldlist的标题keys,用“,”隔开'},visible:false},
+                {field:'tips',title:__('提示信息'),edit:'text'},
+                {field:'rules',title:__('验证规则'),edit:{form:'input',type:'text',placeholder:'请输入验证规则,多个规则用“,”隔开'}},
+            ],
+            groupList:Yunqi.data.groupList,
+            typeList:Yunqi.data.typeList,
+            tableList:[],
+            fieldList:[],
+            tabValue:'basic',
+            columns:''
+        },
+        onLoad:function (){
+            this.getSiteList();
+        },
+        methods: {
+            getSiteList:function (){
+                Yunqi.ajax.get(this.extend.index_url,{group:this.tabValue}).then(res=>{
+                    if(this.tabValue=='addons'){
+                        let columns=[];
+                        for(let i=0;i<res.length;i++){
+                            let value={key:res[i].key,type:res[i].type,name:res[i].name};
+                            columns.push({field:'addons', edit:{form:'slot',value:value}});
+                            let row=this.formatColumns(res[i].list);
+                            columns=columns.concat(row);
+                        }
+                        this.columns=columns;
+                    }else{
+                        this.columns=this.formatColumns(res);
+                    }
+                });
+            },
+            tabChange:function (tab){
+                this.columns='';
+                this.tabValue=tab;
+                this.getSiteList();
+            },
+            delVar:function (name){
+                Yunqi.ajax.post(this.extend.del_url,{group:this.tabValue,name:name}).then(res=>{
+                    location.reload();
+                });
+            },
+            formatVar:function (field,addons){
+                if(addons){
+                    return 'site_config('+this.tabValue+'.'+addons+'.'+field+'")';
+                }else{
+                    return 'site_config("'+this.tabValue+'.'+field+'")';
+                }
+            },
+            formatColumns:function (list){
+                let one=[{field:'group',edit:'hidden'}];
+                for(let i=0;i<list.length;i++){
+                    let obj={
+                        id:list[i].id,
+                        field:list[i].name,
+                        title:list[i].title,
+                        can_delete:list[i].can_delete,
+                        edit: {}
+                    };
+                    if(list[i].rules){
+                        obj.rules=list[i].rules;
+                    }
+                    if(list[i].type=='text'){
+                        obj.edit.form='input';
+                        obj.edit.type='text';
+                        obj.edit.value=list[i].value;
+                        if(list[i].extend=='readonly'){
+                            obj.edit.readonly=true;
+                        }
+                    }
+                    if(list[i].type=='textarea'){
+                        obj.edit.form='input';
+                        obj.edit.type='textarea';
+                        obj.edit.value=list[i].value;
+                        obj.edit.rows=4;
+                    }
+                    if(list[i].type=='password'){
+                        obj.edit.form='input';
+                        obj.edit.type='password';
+                        obj.edit.value=list[i].value;
+                    }
+                    if(list[i].type=='number'){
+                        obj.edit.form='input';
+                        obj.edit.type='number';
+                        obj.edit.value=list[i].value;
+                    }
+                    if(list[i].type=='date'){
+                        obj.edit.form='date-picker';
+                        obj.edit.type='date';
+                        obj.edit.value=list[i].value;
+                    }
+                    if(list[i].type=='time'){
+                        obj.edit.form='time-picker';
+                        obj.edit.value=list[i].value;
+                    }
+                    if(list[i].type=='datetime'){
+                        obj.edit.form='date-picker';
+                        obj.edit.type='datetime';
+                        obj.edit.value=list[i].value;
+                    }
+                    if(list[i].type=='daterange'){
+                        obj.edit.form='date-picker';
+                        obj.edit.type='daterange';
+                        obj.edit.value=list[i].value;
+                    }
+                    if(list[i].type=='timerange'){
+                        obj.edit.form='time-picker';
+                        obj.edit.isRange=true;
+                        obj.edit.value=list[i].value;
+                    }
+                    if(list[i].type=='select'){
+                        obj.searchList=list[i].extend;
+                        obj.edit.form='select';
+                        obj.edit.value=list[i].value.toString();
+                    }
+                    if(list[i].type=='selects'){
+                        obj.edit.form='select';
+                        obj.searchList=list[i].extend;
+                        obj.edit.multiple=true;
+                        obj.edit.value=list[i].value || [];
+                    }
+                    if(list[i].type=='selectpage'){
+                        obj.edit.form='selectpage';
+                        obj.edit.url='general/config/selectpage?id='+list[i].id;
+                        obj.edit.keyField=list[i].setting.primarykey;
+                        obj.edit.labelField=list[i].setting.field;
+                        obj.edit.value=list[i].value;
+                    }
+                    if(list[i].type=='selectpages'){
+                        obj.edit.form='selectpage';
+                        obj.edit.url='general/config/selectpage?id='+list[i].id;
+                        obj.edit.keyField=list[i].setting.primarykey;
+                        obj.edit.labelField=list[i].setting.field;
+                        obj.edit.multiple=true;
+                        obj.edit.value=list[i].value;
+                    }
+                    if(list[i].type=='radio'){
+                        obj.edit.form='radio';
+                        obj.searchList=list[i].extend;
+                        obj.edit.value=list[i].value.toString();
+                    }
+                    if(list[i].type=='checkbox'){
+                        obj.edit.form='checkbox';
+                        obj.searchList=list[i].extend;
+                        obj.edit.value=list[i].value || [];
+                    }
+                    if(list[i].type=='image'){
+                        obj.edit.form='attachment';
+                        obj.edit.limit=1;
+                        obj.edit.value=list[i].value;
+                    }
+                    if(list[i].type=='images'){
+                        obj.edit.form='attachment';
+                        obj.edit.limit=10;
+                        obj.edit.value=list[i].value;
+                    }
+                    if(list[i].type=='file'){
+                        obj.edit.form='files';
+                        obj.edit.limit=1;
+                        let mimetype=Yunqi.config.upload.mimetype.split(',');
+                        let accept=[];
+                        mimetype.forEach(res=>{
+                            accept.push('.'+res);
+                        });
+                        obj.edit.accept=accept;
+                        obj.edit.multiple=false;
+                        obj.edit.value=list[i].value;
+                    }
+                    if(list[i].type=='files'){
+                        obj.edit.form='files';
+                        let mimetype=Yunqi.config.upload.mimetype.split(',');
+                        let accept=[];
+                        mimetype.forEach(res=>{
+                            accept.push('.'+res);
+                        });
+                        obj.edit.accept=accept;
+                        obj.edit.multiple=true;
+                        obj.edit.value=list[i].value;
+                    }
+                    if(list[i].type=='json'){
+                        obj.edit.form='fieldlist';
+                        obj.edit.value=list[i].value || null;
+                        obj.edit.label=list[i].extend[0];
+                        obj.edit.keys=list[i].extend[1];
+                    }
+                    if(list[i].type=='switch'){
+                        obj.edit.form='switch';
+                        obj.edit.inactiveValue='0';
+                        obj.edit.activeValue='1'
+                        obj.edit.value=list[i].value;
+                    }
+                    if(list[i].tip){
+                        obj.edit.placeholder=list[i].tip;
+                    }
+                    one.push(obj);
+                }
+                return one;
+            },
+            changeType:function (data,row){
+                this.$refs.yunform.hideField(['label','url','labelField','keyField','options']);
+                if(data=='selectpage' || data=='selectpages'){
+                    this.$refs.yunform.showField(['url','labelField','keyField']);
+                }else if(data=='json'){
+                    this.$refs.yunform.showField(['label','keys']);
+                }else if(inArray(['select','selects','radio','checkbox'],data)){
+                    this.$refs.yunform.showField('options');
+                }
+            },
+            changeGroup:function (data){
+                if(data=='addons'){
+                    this.$refs.yunform.showField('addons_pack');
+                }else{
+                    this.$refs.yunform.hideField('addons_pack');
+                }
+            },
+            onSubmit:function (row){
+                row.group=this.tabValue;
+                return true;
+            },
+            onSuccess:function (){
+                if(this.tabValue=='dictionary'){
+                    location.reload();
+                }
+            }
+        }
+    }
+</script>
+<style>
+    .bolderText{
+        font-weight:bolder;
+    }
+    .form-container{
+        padding: 30px;
+        border:1px solid var(--el-border-color-light);
+        border-top: 0;
+        margin-top: -16px;
+    }
+</style>

+ 94 - 0
app/admin/view/general/profile/index.html

@@ -0,0 +1,94 @@
+<template>
+    <el-row>
+        <el-col :md="6" style="padding: 5px;">
+            <el-card header="个人资料" style="height: 100%;" shadow="never">
+                <yun-form
+                        ref="yunform"
+                        @submit="onSubmit"
+                        :columns="columns"
+                        :action="extend.edit_url"
+                        :data='admininfo'
+                        label-position="top">
+                        <template #default>
+                            {:token_field()}
+                            <div style="display: flex;justify-content: center;padding: 15px 0;">
+                                <upload-img :image-url="avatar" :is-circle="true" @change="changeImg">
+                                    <template #title>
+                                        <i class="fa fa-user-circle"></i>
+                                        <span>请上传头像</span>
+                                    </template>
+                                </upload-img>
+                            </div>
+                        </template>
+                        <template #third_id="{rows}">
+                            <el-form-item label="绑定微信:">
+                                <third :value="rows.third_id" :selectable="true" @change="changValue"></third>
+                            </el-form-item>
+                        </template>
+                </yun-form>
+            </el-card>
+        </el-col>
+        <el-col :md="18" style="padding: 5px;">
+            <el-card header="操作日志" style="height: 100%;" shadow="never">
+                <yun-table
+                        :columns="log"
+                        :common-search="false"
+                        toolbar="refresh"
+                        :extend="extend">
+                </yun-table>
+            </el-card>
+        </el-col>
+    </el-row>
+</template>
+<script>
+    import table from "@components/Table.js";
+    import form from "@components/Form.js";
+    import uploadimg from "@components/UploadImg.js";
+    import third from "@components/Third.js";
+    export default{
+        components:{'YunTable':table,'YunForm':form,'UploadImg':uploadimg,'Third':third},
+        data:{
+            admininfo:Yunqi.data.admininfo,
+            extend:{
+                index_url: 'general/profile/index',
+                edit_url: 'general/profile/update'
+            },
+            columns:[
+                {field: 'avatar',edit:'hidden'},
+                {field: 'username', title: __('用户名'),edit:'readonly'},
+                {field: 'mobile', title: __('手机号'),edit:'text',rules:'required;mobile'},
+                {field: 'nickname', title: __('昵称'),edit:'text',rules:'required'},
+                {field: 'third_id', title: __('微信'),edit:Yunqi.data.thirdLogin?'slot':false,rules:'required'},
+                {field: 'password', title: __('密码'),edit: {form:'input',type:'password',placeholder:'不修改密码请留空'}},
+            ],
+            log:[
+                {field: 'id',title: __('ID'),width:70},
+                {field: 'title',title: __('标题')},
+                {field: 'url',title: __('链接'),formatter: Yunqi.formatter.link,width:240},
+                {field: 'ip',title: __('IP'),width:140},
+                {field: 'createtime',title: __('访问时间'),sortable: true,width:150,formatter:Yunqi.formatter.datetime},
+            ],
+            avatar:'',
+        },
+        onShow:function (){
+            this.avatar=this.$refs.yunform.data.avatar;
+        },
+        methods: {
+            changeImg:function (r){
+                this.avatar=r;
+            },
+            onSubmit:function (data){
+                data.avatar=this.avatar;
+                return true;
+            },
+            changValue:function (e){
+                this.$refs.yunform.setValue('third_id',e);
+            }
+        }
+    }
+</script>
+<style>
+
+</style>
+
+

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

@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<html {if $config.elementUi.dark}class="dark"{/if}>
+<head>
+{include file="common/meta" /}
+<link rel="stylesheet" href="{:request()->domain()}/assets/css/index/common.css?v={$config.version}" />
+<link rel="stylesheet" href="{:request()->domain()}/assets/css/index/{$config.elementUi.layout}.css?v={$config.version}" />
+</head>
+<body>
+<div id="app">
+    {if $config.elementUi.layout=='classic'}
+    {include file="layout/index/classic/index" /}
+    {/if}
+    {if $config.elementUi.layout=='vertical'}
+    {include file="layout/index/vertical/index" /}
+    {/if}
+    {if $config.elementUi.layout=='transverse'}
+    {include file="layout/index/transverse/index" /}
+    {/if}
+    {if $config.elementUi.layout=='columns'}
+    {include file="layout/index/columns/index" /}
+    {/if}
+    <template v-for="(layer,index) in layerList">
+        <el-dialog
+            v-model="layer.show"
+            :draggable="true"
+            :close-on-click-modal="false"
+            :close-on-press-escape="false"
+            class="layer-dialog"
+            :modal="calculateLayerIndex(index)"
+            :show-close="false"
+            :width="layer.width"
+            :fullscreen="layer.expand">
+            <template #header>
+                <div class="custom-dialog-header">
+                    <span class="custom-dialog-title"><i :class="layer.icon"></i>&nbsp;{{ layer.title }}</span>
+                    <div class="custom-dialog-buttons">
+                        <i class="fa fa-minus custom-dialog-minimize"  @click="hideLayer(layer);"></i>
+                        <i class="fa fa-expand custom-dialog-maximize"
+                           v-if="!layer.expand"
+                           @click="layerExpand(layer)">
+                        </i>
+                        <i class="fa fa-compress custom-dialog-maximize"
+                           v-else
+                           @click="layerExpand(layer)">
+                        </i>
+                        <i class="fa fa-close custom-dialog-close"
+                           @click="closeLayer(layer.id)">
+                        </i>
+                    </div>
+                </div>
+            </template>
+            <template #default>
+                <iframe :src="layer.url" :id="'layer-'+layer.id" class="layer-iframe" width="100%" :height="layer.expand?layerExpandHeight:layer.height" frameborder="no" border="0" marginwidth="0" marginheight="0" scrolling-x="no" scrolling-y="auto" allowtransparency="yes"></iframe>
+            </template>
+        </el-dialog>
+    </template>
+    <el-image-viewer
+            v-if="imageList.length>0"
+            :hide-on-click-modal="true"
+            :url-list="imageList"
+            @close="imageList=[]"
+            :initial-index="0">
+    </el-image-viewer>
+</div>
+</body>
+<script type="text/javascript" src="{:request()->domain()}/assets/js/yunqi.js?v={$config.version}"></script>
+<script type="text/javascript">
+    Yunqi.setConfig({:json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)});
+    Yunqi.setData({:build_var_json(get_defined_vars())});
+    Yunqi.setAuth({:json_encode($auth->getBackendAuth(),JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)});
+</script>
+<script type="module">
+    import pageinfo from '{:request()->domain()}/assets/js/index.js?v={$config.version}';
+    import zhcn from '{:request()->domain()}/assets/js/zh-cn.js';
+    try{
+        Yunqi.setUp(pageinfo,zhcn);
+    }catch (e){
+        console.error(e);
+    }
+</script>
+</html>

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

@@ -0,0 +1,346 @@
+<template>
+    <div class="login-container">
+        <div class="login-box">
+            <div class="login-left">
+                <img class="login-left-img" src="/assets/img/banner.png" alt="login" />
+            </div>
+            <div class="login-form">
+                <div class="login-logo">
+                    <img class="login-icon" src="{$logo}" alt="" />
+                    <h2 class="logo-text">{$sitename}</h2>
+                </div>
+                <el-form ref="loginForm" :model="loginForm" label-width="0px" :rules="rules">
+                    {:token_field()}
+                    <el-row>
+                        <el-col :span="thirdLogin?15:24">
+                            <el-form-item label="" prop="username">
+                                <el-input size="large" v-model="loginForm.username" placeholder="用户名">
+                                    <template #prepend>
+                                        <i class="fa fa-user"></i>
+                                    </template>
+                                </el-input>
+                            </el-form-item>
+                            <el-form-item label="" prop="password">
+                                <el-input size="large" type="password" v-model="loginForm.password" placeholder="密码">
+                                    <template #prepend>
+                                        <i class="fa fa-lock"></i>
+                                    </template>
+                                </el-input>
+                            </el-form-item>
+                            {if $login_captcha}
+                            <el-form-item label="" prop="captcha" style="margin-bottom:10px;">
+                                <el-row>
+                                    <el-col :span="12" :xs="14">
+                                        <el-input size="large" v-model="loginForm.captcha" placeholder="验证码">
+                                            <template #prepend>
+                                                <i class="fa fa-ellipsis-h"></i>
+                                            </template>
+                                        </el-input>
+                                    </el-col>
+                                    <el-col :span="12" :xs="10">
+                                        <div class="captcha-img">
+                                            <img :src="captchaUrl" @click="refreshCaptcha"/>
+                                        </div>
+                                    </el-col>
+                                </el-row>
+                            </el-form-item>
+                            {/if}
+                        </el-col>
+                        <el-col :span="9" v-if="thirdLogin && qrcode" class="hide-800">
+                            <div class="login-right">
+                                <img class="login-right-img" :src="qrcode" alt="login" />
+                                <span>微信扫码</span>
+                            </div>
+                        </el-col>
+                    </el-row>
+                    <el-form-item label="" prop="savepassword" style="margin-bottom:10px;">
+                        <el-checkbox-group v-model="loginForm.savepassword">
+                            <el-checkbox :label="1">{:__('记住密码')}</el-checkbox>
+                        </el-checkbox-group>
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button type="primary" size="large" style="width: 100%" @click="login">登陆</el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </div>
+    </div>
+    <el-dialog
+        v-model="dialogVisible"
+        title="选择登录账号"
+        width="600"
+    >
+        <div class="login-admin">
+            <el-radio-group v-model="checked">
+                <el-radio v-for="item in adminlist" :label="item.id" size="large" border>{{item.nickname}}</el-radio>
+            </el-radio-group>
+            <span style="margin-top: 20px">您当前微信绑定了多个账户,请任选择一个登录</span>
+        </div>
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="cancelDialog">取消</el-button>
+                <el-button type="primary" @click="confirmDialog">
+                    确认
+                </el-button>
+            </div>
+        </template>
+    </el-dialog>
+</template>
+<script>
+    export default{
+        data(){
+            return {
+                thirdLogin:false,
+                qrcode:'',
+                captchaUrl:'',
+                loginForm:{
+                    __token__:'',
+                    username: '',
+                    password: '',
+                    captcha: '',
+                    savepassword:[1],
+                },
+                rules:{
+                    username:[{required:true,message:'用户名不能为空!',}],
+                    password:[{required:true,message:'密码不能为空!',}],
+                },
+                dialogVisible:false,
+                adminlist:[],
+                checked:''
+            }
+        },
+        onLoad:function (){
+            this.refreshCaptcha();
+            let width=document.body.clientWidth;
+            this.thirdLogin=Yunqi.data.thirdLogin && width>800;
+            window.addEventListener('resize',()=>{
+                let width=document.body.clientWidth;
+                this.thirdLogin=Yunqi.data.thirdLogin && width>800;
+            });
+            this.qrcode=Yunqi.data.qrcode;
+        },
+        onShow:function (){
+            this.loginForm.__token__=document.getElementsByTagName('input')[0].value;
+            this.loginForm.username= localStorage.getItem('username') || '';
+            this.loginForm.password= localStorage.getItem('password') || '';
+            this.loginForm.savepassword= localStorage.getItem('savepassword')? [1] : [];
+            this.checklogin();
+        },
+        methods:{
+            refreshCaptcha:function (){
+                this.captchaUrl=Yunqi.config.baseUrl+"captcha?"+Math.random();
+            },
+            checklogin:function (){
+                if(!this.thirdLogin){
+                    return;
+                }
+                let token=document.querySelector('input[name="__token__"]').value;
+                Yunqi.ajax.get('qrcodeLogin', {token:token},false,false).then(res=>{
+                    Yunqi.message.success('登录成功');
+                    setTimeout(()=>{
+                        this.redirect();
+                    },1000);
+                }).catch(err=>{
+                    if(err.data.length>0){
+                        this.adminlist=err.data;
+                        this.dialogVisible=true;
+                        return;
+                    }
+                    setTimeout(()=>{
+                        this.checklogin();
+                    },2000);
+                });
+            },
+            cancelDialog:function (){
+                location.reload();
+            },
+            confirmDialog:function (){
+                let token=document.querySelector('input[name="__token__"]').value;
+                Yunqi.ajax.get('qrcodeLogin',{token:token,admin_id:this.checked},true,false).then(res=>{
+                    this.dialogVisible=false;
+                    Yunqi.message.success('登录成功');
+                    setTimeout(()=>{
+                        this.redirect();
+                    },1000);
+                });
+            },
+            redirect:function (){
+                location.href=Yunqi.config.baseUrl+'index';
+            },
+            login:function (){
+                this.$refs.loginForm.validate((valid)=>{
+                    if(valid){
+                        Yunqi.ajax.post('login',this.loginForm,true).then(res=>{
+                            let savepassword=this.loginForm.savepassword.length>0?1:0;
+                            if(savepassword){
+                                localStorage.setItem('username',this.loginForm.username);
+                                localStorage.setItem('password',this.loginForm.password);
+                                localStorage.setItem('savepassword',savepassword);
+                            }else{
+                                localStorage.removeItem('username');
+                                localStorage.removeItem('password');
+                                localStorage.removeItem('savepassword');
+                            }
+                            this.redirect();
+                        }).catch(err=>{
+                            if(err.data){
+                                this.refreshCaptcha();
+                            }
+                        });
+                    }
+                });
+            }
+        }
+    }
+</script>
+<style>
+    body {
+        color: #999;
+    }
+    .login-container {
+        height: 100%;
+        min-height: 550px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        position: fixed;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        top: 0;
+        background-color: #eeeeee;
+        background-image: url("/assets/img/bg.svg");
+        background-size: 100% 100%;
+        background-size: cover;
+    }
+
+    .login-container .login-box {
+        position: relative;
+        box-sizing: border-box;
+        display: flex;
+        align-items: center;
+        justify-content: space-around;
+        width: 96.5%;
+        height: 94%;
+        padding: 0 50px;
+        background-color: rgba(255, 255, 255, 0.8);
+        border-radius: 10px;
+    }
+
+    .login-container .login-box .under {
+        position: absolute;
+        top: 13px;
+        left: 18px;
+    }
+
+    .login-container .login-box .under a{
+        text-decoration:none;
+        font-size: 16px;
+        color: #4c4c4c;
+    }
+
+    .login-container .login-box .under span{
+        font-size: 16px;
+        color: #4c4c4c;
+        margin-left: 20px;
+        cursor: pointer;
+    }
+
+    .login-container .login-box .login-left {
+        width: 800px;
+        margin-right: 10px;
+        text-align: center;
+    }
+
+    .login-container .login-box .login-left .login-left-img {
+        width: 80%;
+    }
+
+    .login-container .login-box .login-form {
+        width: 420px;
+        padding: 50px 40px 0px;
+        background-color: var(--el-bg-color);
+        border-radius: 10px;
+        box-shadow: rgba(0, 0, 0, 0.1) 0 2px 10px 2px;
+    }
+
+    .login-container .login-box .login-form .login-logo {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin-bottom: 45px;
+    }
+
+    .login-container .login-box .login-form .login-logo .login-icon {
+        width: 80px;
+    }
+
+    .login-container .login-box .login-form .login-logo .logo-text {
+        padding: 0 0 0 25px;
+        margin: 0;
+        font-size: 42px;
+        font-weight: bold;
+        color: #34495e;
+        white-space: nowrap;
+    }
+    .login-container .login-box .login-form .el-form-item {
+        margin-bottom: 10px;
+    }
+
+    .login-container .login-box .login-form .login-btn {
+        display: flex;
+        justify-content: space-between;
+        width: 100%;
+        margin-top: 40px;
+        white-space: nowrap;
+    }
+
+    .login-container .login-box .login-form .login-btn .el-button {
+        width: 185px;
+    }
+    .captcha-img img{
+        width: 125px;
+    }
+    .login-right{
+        display: flex;
+        flex-direction: column;
+        margin-left: 10px;
+        align-items: center;
+        justify-content: center;
+    }
+    .login-right-img{
+        width: 125px;
+    }
+
+    @media screen and (max-width: 1250px) {
+        .login-left {
+            display: none;
+        }
+    }
+
+    @media screen and (max-width: 600px) {
+        .login-box{
+            padding: 0 20px!important;
+        }
+        .login-form {
+            width: 100% !important;
+            padding: 10px !important;
+        }
+        .captcha-img img{
+            width: 120px;
+        }
+        .login-icon {
+            width: 20%!important;
+        }
+        .login-logo{
+            margin: 20px 0!important;
+        }
+        .logo-text {
+            font-size: 32px!important;
+        }
+    }
+    .login-admin{
+        display: flex;
+        flex-direction: column;
+    }
+</style>

+ 47 - 0
app/admin/view/layout/index/classic/index.html

@@ -0,0 +1,47 @@
+<el-container class="layout" id="container">
+    <el-header v-show="!mainFrameExpand">
+        <div class="header-lf">
+            <div class="logo flx-center hide-600">
+                <img class="logo-img" :src="logo_img" alt="logo" />
+                <span class="logo-text">{$site.sitename}</span>
+            </div>
+            <div class="tool-bar-lf">
+                <i @click="elementUi.is_menu_collapse=!elementUi.is_menu_collapse;setMainContentFrame();" :class="['collapse-icon',elementUi.is_menu_collapse?'fa fa-indent':'fa fa-outdent']"></i>
+                <Breadcrumb class="hide-800" v-if="elementUi.breadcrumb" :list="breadcrumb"></Breadcrumb>
+            </div>
+        </div>
+        <div class="header-ri">
+            <div class="tool-bar-ri">
+                {include file="layout/index/rightbar" /}
+            </div>
+        </div>
+    </el-header>
+    <el-container class="classic-content">
+        <el-aside v-show="documentWidth>600 || !elementUi.is_menu_collapse">
+            <div class="aside-box" :style="{ width: elementUi.is_menu_collapse ? '65px' : documentWidth>600 ? '210px':(documentWidth-2)+'px'}">
+                <el-scrollbar :height="menuHeight+'px'">
+                    <el-menu
+                            :default-active="activeTab?activeTab.id.toString():''"
+                            :router="false"
+                            :collapse="elementUi.is_menu_collapse"
+                            :collapse-transition="false"
+                            :unique-opened="true">
+                           <Menulist :list="menuList" @onclickmenu="clickMenu"></Menulist>
+                    </el-menu>
+                </el-scrollbar>
+            </div>
+        </el-aside>
+        <el-container class="classic-main" v-show="documentWidth>600 || elementUi.is_menu_collapse">
+            <Tabs v-show="!mainFrameExpand && elementUi.tabs" ref="tabs"></Tabs>
+            <el-main :class="mainFrameExpand?'expand':''">
+                <div id="main-content" :style="mainFrameExpand?'width:100%;height:100%;':`width:${contentWidth}px;height:${contentHeight}px;`"></div>
+                <div class="close-main-expand" v-if="mainFrameExpand" @click="minimize">
+                    <i class="fa fa-close"></i>
+                </div>
+            </el-main>
+            <el-footer v-if="elementUi.footer">
+                {include file="layout/index/footer" /}
+            </el-footer>
+        </el-container>
+    </el-container>
+</el-container>

+ 62 - 0
app/admin/view/layout/index/columns/index.html

@@ -0,0 +1,62 @@
+<el-container class="layout" id="container">
+    <div class="aside-split" v-show="documentWidth>600 || !elementUi.is_menu_collapse">
+        <div class="logo flx-center">
+            <img class="logo-img" :src="logo_img" alt="logo" />
+        </div>
+        <el-scrollbar :height="menuHeight+'px'">
+            <div class="split-list">
+                <div
+                    v-for="(menu,index) in menuList"
+                    :key="menu.url"
+                    :class="['split-item',isChildMenu(menu.id)?'split-active':'']"
+                    @click="changeSubMenu(menu)">
+                    <i :class="menu.icon"></i>
+                    <span class="title">{{ menu.title }}</span>
+                </div>
+            </div>
+        </el-scrollbar>
+    </div>
+    <el-aside v-show="documentWidth>600 || !elementUi.is_menu_collapse" :class="{ 'not-aside': !childMenuList.length }" :style="{ width: elementUi.is_menu_collapse ? '65px' : documentWidth>600 ? '210px':(documentWidth-2)+'px'}">
+        <div class="logo flx-center">
+            <template v-if="elementUi.is_menu_collapse">
+                <span class="logo-text">{:mb_substr($site.sitename,0,1)}</span>
+            </template>
+            <template v-else>
+                <span class="logo-text">{$site.sitename}</span>
+            </template>
+        </div>
+        <el-scrollbar :height="menuHeight+'px'">
+            <el-menu
+                    :default-active="activeTab?activeTab.id.toString():''"
+                    :router="false"
+                    :collapse="elementUi.is_menu_collapse"
+                    :collapse-transition="false"
+                    :unique-opened="true">
+                    <Menulist :list="childMenuList" @onclickmenu="clickMenu"></Menulist>
+            </el-menu>
+        </el-scrollbar>
+    </el-aside>
+    <el-container v-show="documentWidth>600 || elementUi.is_menu_collapse">
+        <el-header v-show="!mainFrameExpand">
+            <div class="tool-bar-lf">
+                <i @click="elementUi.is_menu_collapse=!elementUi.is_menu_collapse;setMainContentFrame();" :class="['collapse-icon',elementUi.is_menu_collapse?'fa fa-indent':'fa fa-outdent']"></i>
+                <Breadcrumb class="hide-800" v-if="elementUi.breadcrumb" :list="breadcrumb"></Breadcrumb>
+            </div>
+            <div class="tool-bar-ri">
+                {include file="layout/index/rightbar" /}
+            </div>
+        </el-header>
+        <el-container class="classic-main is-vertical">
+            <Tabs v-show="!mainFrameExpand && elementUi.tabs" ref="tabs"></Tabs>
+            <el-main :class="mainFrameExpand?'expand':''">
+                <div id="main-content" :style="mainFrameExpand?'width:100%;height:100%;':`width:${contentWidth}px;height:${contentHeight}px;`"></div>
+                <div class="close-main-expand" v-if="mainFrameExpand" @click="minimize">
+                    <i class="fa fa-close"></i>
+                </div>
+            </el-main>
+            <el-footer v-if="elementUi.footer">
+                {include file="layout/index/footer" /}
+            </el-footer>
+        </el-container>
+    </el-container>
+</el-container>

+ 3 - 0
app/admin/view/layout/index/footer.html

@@ -0,0 +1,3 @@
+<div class="footer flx-center">
+    <a href="{:request()->domain()}" target="_blank"> {:date('Y')} © <span class="hide-600">{$site.sitename} By </span>{$site.copyright}. </a>
+</div>

+ 8 - 0
app/admin/view/layout/index/rightbar.html

@@ -0,0 +1,8 @@
+<Platform></Platform>
+{if $auth->isSuperAdmin()}
+<Trash></Trash>
+{/if}
+<Message></Message>
+<Fullscreen></Fullscreen>
+<Theme-setting></Theme-setting>
+<Userinfo :admin='{:json_encode($auth->userinfo())}'></Userinfo>

+ 48 - 0
app/admin/view/layout/index/transverse/index.html

@@ -0,0 +1,48 @@
+<el-container class="layout" id="container">
+    <el-header v-show="!mainFrameExpand">
+        <div class="logo flx-center hide-600">
+            <img class="logo-img" :src="logo_img" alt="logo" />
+            <span class="logo-text">{$site.sitename}</span>
+        </div>
+        <el-menu
+            mode="horizontal"
+            :default-active="activeTab?activeTab.id.toString():''"
+            :router="false"
+            :unique-opened="true">
+            <template v-for="menu in menuList">
+                <template v-if="menu.childlist && menu.childlist.length>0">
+                    <el-sub-menu :index="menu.id.toString()" :key="menu.id.toString()">
+                        <template #title>
+                            <i :class="menu.icon"></i>
+                            <span class="sle">{{menu.title}}</span>
+                        </template>
+                        <Menulist :list="menu.childlist" @onclickmenu="clickMenu"></Menulist>
+                    </el-sub-menu>
+                </template>
+                <template v-else>
+                    <el-menu-item :index="menu.id.toString()" @click="clickMenu(menu)" :key="menu.id.toString()">
+                        <i :class="menu.icon"></i>
+                        <template #title>
+                            <span class="sle">{{ menu.title }}</span>
+                        </template>
+                    </el-menu-item>
+                </template>
+            </template>
+        </el-menu>
+        <div class="tool-bar-ri">
+            {include file="layout/index/rightbar" /}
+        </div>
+    </el-header>
+    <el-container class="classic-main is-vertical">
+        <Tabs v-show="!mainFrameExpand && elementUi.tabs" ref="tabs"></Tabs>
+        <el-main :class="mainFrameExpand?'expand':''">
+            <div id="main-content" :style="mainFrameExpand?'width:100%;height:100%;':`width:${contentWidth}px;height:${contentHeight}px;`"></div>
+            <div class="close-main-expand" v-if="mainFrameExpand" @click="minimize">
+                <i class="fa fa-close"></i>
+            </div>
+        </el-main>
+        <el-footer v-if="elementUi.footer">
+            {include file="layout/index/footer" /}
+        </el-footer>
+    </el-container>
+</el-container>

+ 45 - 0
app/admin/view/layout/index/vertical/index.html

@@ -0,0 +1,45 @@
+<el-container class="layout" id="container">
+    <el-aside v-show="documentWidth>600 || !elementUi.is_menu_collapse">
+        <div class="aside-box" :style="{ width: elementUi.is_menu_collapse ? '65px' : documentWidth>600 ? '210px':(documentWidth-2)+'px'}">
+            <div class="logo flx-center">
+                <img class="logo-img" :src="logo_img" alt="logo" />
+                <span v-if="!elementUi.is_menu_collapse" class="logo-text">{$site.sitename}</span>
+            </div>
+            <el-scrollbar :height="menuHeight+'px'">
+                <el-menu
+                        :default-active="activeTab?activeTab.id.toString():''"
+                        :router="false"
+                        :collapse="elementUi.is_menu_collapse"
+                        :collapse-transition="false"
+                        :unique-opened="true">
+                    <Menulist :list="menuList" @onclickmenu="clickMenu"></Menulist>
+                </el-menu>
+            </el-scrollbar>
+        </div>
+    </el-aside>
+    <el-container v-show="documentWidth>600 || elementUi.is_menu_collapse">
+        <el-header v-show="!mainFrameExpand">
+            <div class="header-lf">
+                <div class="tool-bar-lf">
+                    <i @click="elementUi.is_menu_collapse=!elementUi.is_menu_collapse;setMainContentFrame();" :class="['collapse-icon',elementUi.is_menu_collapse?'fa fa-indent':'fa fa-outdent']"></i>
+                    <Breadcrumb class="hide-800" v-if="elementUi.breadcrumb" :list="breadcrumb"></Breadcrumb>
+                </div>
+            </div>
+            <div class="header-ri">
+                <div class="tool-bar-ri">
+                    {include file="layout/index/rightbar" /}
+                </div>
+            </div>
+        </el-header>
+        <Tabs v-show="!mainFrameExpand && elementUi.tabs" ref="tabs"></Tabs>
+        <el-main :class="mainFrameExpand?'expand':''">
+            <div id="main-content" :style="mainFrameExpand?'width:100%;height:100%;':`width:${contentWidth}px;height:${contentHeight}px;`"></div>
+            <div class="close-main-expand" v-if="mainFrameExpand" @click="minimize">
+                <i class="fa fa-close"></i>
+            </div>
+        </el-main>
+        <el-footer v-if="elementUi.footer">
+            {include file="layout/index/footer" /}
+        </el-footer>
+    </el-container>
+</el-container>

+ 34 - 0
app/admin/view/layout/vue.html

@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html {if $config.elementUi.dark}class="dark"{/if}>
+<head>
+{include file="common/meta" /}
+<link rel="stylesheet" href="{:request()->domain()}/assets/css/yunqi.css" />
+{__CSS__}
+</head>
+<body>
+    <div id="app">
+        <el-container id="container" style="display: none;">
+            <el-main style="padding: 0px;border-radius:4px;" id="mainScrollbar">
+                <el-scrollbar>
+                    {__CONTENT__}
+                </el-scrollbar>
+            </el-main>
+        </el-container>
+    </div>
+</body>
+<script type="text/javascript" src="{:request()->domain()}/assets/js/yunqi.js?v={$config.version}"></script>
+<script type="text/javascript">
+    Yunqi.setConfig({:json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)});
+    Yunqi.setData({:build_var_json(get_defined_vars())});
+    Yunqi.setAuth({:json_encode($auth->getBackendAuth(),JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)});
+</script>
+<script type="module">
+    import pageinfo from '{$config.baseUrl}ajax/js/{__JS__}';
+    import zhcn from '{:request()->domain()}/assets/js/zh-cn.js';
+    try{
+        Yunqi.setUp(pageinfo,zhcn);
+    }catch (e){
+        console.error(e);
+    }
+</script>
+</html>

+ 51 - 0
app/admin/view/user/index/detail.html

@@ -0,0 +1,51 @@
+<template>
+    <el-card shadow="never">
+        <el-tabs v-model="activeType" type="card" @tab-change="refreshLog">
+            {foreach $moduletype as $key=>$type}
+            <el-tab-pane label="{$type}" name="{$key}"></el-tab-pane>
+            {/foreach}
+        </el-tabs>
+        <yun-table
+                v-if="recharge_url"
+                :columns="rechargeDetail"
+                toolbar="refresh"
+                :extend="recharge_url">
+        </yun-table>
+    </el-card>
+</template>
+<script>
+    import table from "@components/Table.js";
+    export default{
+        components:{'YunTable':table},
+        data:{
+            rechargeDetail: [
+                {field: 'createtime', title: __('时间'),operate: {form:'date-picker',type:'daterange',size:'large'}},
+                {field: 'before', title: __('交易前'),operate:false},
+                {field: 'change', title: __('变化数目'),operate:false},
+                {field: 'after', title: __('交易后'),operate:false},
+                {field: 'order_no', title: __('订单编号')},
+                {field: 'remark', title: __('备注'),operate:false}
+            ],
+            activeType:'',
+            recharge_url:''
+        },
+        onLoad:function (){
+            this.activeType=Object.keys(Yunqi.data.moduletype)[0];
+            this.refreshLog();
+        },
+        methods: {
+            refreshLog:function (){
+                this.recharge_url='';
+                Vue.nextTick(()=>{
+                    this.recharge_url={
+                        index_url:'user/index/detail?type='+this.activeType+'&ids='+Yunqi.data.user.id
+                    };
+                });
+            }
+        }
+    }
+</script>
+<style>
+
+</style>
+

+ 32 - 0
app/admin/view/user/index/edit.html

@@ -0,0 +1,32 @@
+<template>
+    <el-card shadow="never" style="border: 0;">
+        <yun-form :data="row" :columns="columns">
+            {:token_field()}
+        </yun-form>
+    </el-card>
+</template>
+<script>
+    import form from "@components/Form.js";
+    export default{
+        components:{'YunForm':form},
+        data:{
+            row:Yunqi.data.row,
+            columns:[
+                {field: 'id',title: __('ID'),edit:'hidden'},
+                {field: 'username',title: __('用户名'),edit:'text',rules:'required'},
+                {field: 'nickname',title: __('昵称'),edit:'readonly',rules:'required'},
+                {field: 'sex', title: __('性别'), edit: 'radio',rules:"required",searchList: {1: __('男'), 2: __('女')}},
+                {field: 'email',title: __('邮箱'),rules:'email',edit:'text'},
+                {field: 'mobile',title: __('手机'),edit:'text',rules:'mobile'},
+                {field: 'level', title: __('等级'),edit:{form:'select', value:1},searchList:{0:'普通',1:'1级',2:'2级',3:'3级',4:'4级'}},
+                {field: 'status', title: __('状态'), edit:'switch',searchList: {'normal': __('正常'),'hidden': __('隐藏')}},
+            ]
+        },
+        methods: {
+
+        }
+    }
+</script>
+<style>
+
+</style>

+ 97 - 0
app/admin/view/user/index/index.html

@@ -0,0 +1,97 @@
+<template>
+    <el-card shadow="never">
+        <yun-table
+                :columns="columns"
+                search="nickname,mobile"
+                toolbar="refresh,edit,del,recyclebin,more"
+                ref="yuntable"
+                :auth="auth"
+                :extend="extend">
+        </yun-table>
+    </el-card>
+</template>
+<script>
+    import table from "@components/Table.js";
+    export default{
+        components:{'YunTable':table},
+        data:{
+            auth:{
+                add:Yunqi.auth.check('app\\admin\\controller\\user\\Index','add'),
+                edit:Yunqi.auth.check('app\\admin\\controller\\user\\Index','edit'),
+                del:Yunqi.auth.check('app\\admin\\controller\\user\\Index','del'),
+                multi:Yunqi.auth.check('app\\admin\\controller\\user\\Index','multi'),
+                recyclebin:Yunqi.auth.check('app\\admin\\controller\\user\\Index','recyclebin'),
+            },
+            extend:{
+                index_url: 'user/index/index',
+                edit_url: 'user/index/edit',
+                del_url: 'user/index/del',
+                multi_url: 'user/index/multi',
+                download_url: 'user/index/download',
+                recyclebin_url: 'user/index/recyclebin',
+            },
+            columns:[
+                {checkbox: true},
+                {field: 'id',title: __('ID'),width:80,sortable: true},
+                {field: 'username',title: __('用户名'),operate:'='},
+                {field: 'avatar', title: __('头像'), formatter: Yunqi.formatter.image, operate: false},
+                {field: 'nickname',title: __('昵称'),operate: 'LIKE',formatter: Yunqi.formatter.tag},
+                {field: 'sex', title: __('性别'),width:100,searchList: {1: __('男'), 2: __('女')},operate:'select',formatter:Yunqi.formatter.select},
+                {field: 'email',title: __('邮箱'),operate:'LIKE'},
+                {field: 'mobile',title: __('手机'),operate: '='},
+                {field: 'level', title: __('等级'),sortable: true,operate:'selects',searchList:{0:'普通',1:'1级',2:'2级',3:'3级',4:'4级'}},
+                {field: 'score', title: __('积分'),sortable: true,operate:'between'},
+                {field: 'balance', title: __('余额'),sortable: true,operate:'between'},
+                {field: 'status', title: __('状态'),searchList: {'normal': __('正常'),'hidden': __('隐藏')},formatter:Yunqi.formatter.switch,operate:'select'},
+                {field: 'createtime', title: __('创建时间'), width:160,formatter: Yunqi.formatter.datetime,operate:false,sortable: true},
+                {
+                    field: 'operate',
+                    title: __('操作'),
+                    width:180,
+                    fixed:'right',
+                    action:{
+                        recharge:{
+                            tooltip:true,
+                            icon:'fa fa-plug',
+                            type:'warning',
+                            text:__('会员充值'),
+                            method:'recharge'
+                        },
+                        detail:{
+                            tooltip:true,
+                            icon:'fa fa-list',
+                            type:'info',
+                            text:__('会员明细'),
+                            method:'detail'
+                        },
+                        edit:true,
+                        del:true
+                    }
+                }
+            ]
+        },
+        methods: {
+            detail:function (row){
+                Yunqi.api.open({
+                    url:'user/index/detail?ids='+row.id,
+                    width:1000,
+                    title:__('会员明细'),
+                    icon:'fa fa-list'
+                });
+            },
+            recharge:function (row){
+                Yunqi.api.open({
+                    url:'user/index/recharge?ids='+row.id,
+                    title:__('会员充值'),
+                    icon:'fa fa-plug',
+                    close:()=>{
+                        this.$refs.yuntable.reload();
+                    }
+                });
+            }
+        }
+    }
+</script>
+<style>
+
+</style>

+ 55 - 0
app/admin/view/user/index/recharge.html

@@ -0,0 +1,55 @@
+<template>
+    <el-card shadow="never" style="border: 0;">
+        <yun-form v-if="rechargeData" :data="rechargeData" :columns="rechargeColumn">
+            <template #default>
+                {:token_field()}
+            </template>
+            <template #now="{rows}">
+                {foreach $moduletype as $key=>$type}
+                <el-form-item label="当前{$type}:" v-if="rows.module_type=='{$key}'">
+                    <el-input value="{$user[$key]}" :disabled="true"></el-input>
+                </el-form-item>
+                {/foreach}
+            </template>
+            <template #change="{rows}">
+                <el-form-item :label="'变化'+getModuleTypeName(rows.module_type)+':'" prop="change">
+                    <el-input v-model="rows.change" type="number"></el-input>
+                </el-form-item>
+            </template>
+        </yun-form>
+    </el-card>
+</template>
+<script>
+    import form from "@components/Form.js";
+    export default{
+        components:{'YunForm':form},
+        data:{
+            rechargeColumn:[
+                {field: 'user_id',title: __('会员ID'),edit:'hidden'},
+                {field: 'nickname',title: __('充值会员'),edit:'readonly'},
+                {field: 'module_type',title: __('充值类型'),edit:'radio',searchList:Yunqi.data.moduletype},
+                {field: 'now',title: __('当前'),edit:'slot'},
+                {field: 'recharge_type',title: __('充值方式'),edit:'radio',searchList: {add:'增加',minus:'减少',last:'最终'}},
+                {field: 'change',title: __('变化'),edit:'slot',rules:'required;range(0~)'},
+                {field: 'remark',title: __('备注'),edit:'textarea'}
+            ],
+            rechargeData:''
+        },
+        onLoad:function (){
+            this.rechargeData={
+                user_id:Yunqi.data.user.id,
+                nickname:Yunqi.data.user.nickname,
+                module_type:Object.keys(Yunqi.data.moduletype)[0],
+                recharge_type:'add'
+            };
+        },
+        methods: {
+            getModuleTypeName:function (type){
+                return  Yunqi.data.moduletype[type];
+            }
+        }
+    }
+</script>
+<style>
+
+</style>

+ 68 - 0
app/admin/view/user/index/test.html

@@ -0,0 +1,68 @@
+<template>
+    <el-table
+        :data="tableData"
+        style="width: 100%; margin-bottom: 20px"
+        row-key="id"
+        border
+        default-expand-all>
+        <el-table-column prop="date" label="Date"></el-table-column>
+        <el-table-column prop="name" label="Name">
+            <template #default="scope">
+                打豆豆{{scope.row}}
+            </template>
+        </el-table-column>
+        <el-table-column prop="address" label="Address"></el-table-column>
+    </el-table>
+</template>
+<script>
+    export default{
+        data:{
+            tableData: [
+                {
+                    id: 1,
+                    date: '2016-05-02',
+                    name: 'wangxiaohu',
+                    address: 'No. 189, Grove St, Los Angeles',
+                },
+                {
+                    id: 2,
+                    date: '2016-05-04',
+                    name: 'wangxiaohu',
+                    address: 'No. 189, Grove St, Los Angeles',
+                },
+                {
+                    id: 3,
+                    date: '2016-05-01',
+                    name: 'wangxiaohu',
+                    address: 'No. 189, Grove St, Los Angeles',
+                    children: [
+                        {
+                            id: 31,
+                            date: '2016-05-01',
+                            name: 'wangxiaohu',
+                            address: 'No. 189, Grove St, Los Angeles',
+                        },
+                        {
+                            id: 32,
+                            date: '2016-05-01',
+                            name: 'wangxiaohu',
+                            address: 'No. 189, Grove St, Los Angeles',
+                        },
+                    ],
+                },
+                {
+                    id: 4,
+                    date: '2016-05-03',
+                    name: 'wangxiaohu',
+                    address: 'No. 189, Grove St, Los Angeles',
+                },
+            ]
+        },
+        methods: {
+
+        }
+    }
+</script>
+<style>
+
+</style>

+ 273 - 0
app/common.php

@@ -0,0 +1,273 @@
+<?php
+declare (strict_types = 1);
+
+use app\common\model\Addons;
+use think\facade\Cache;
+use app\common\model\Config;
+use app\common\service\LangService;
+
+if (!function_exists('site_config')) {
+
+    /**
+     * 获取/设置系统配置
+     * @param string $name 属性名
+     * @param mixed  $vars 属性值
+     * @return mixed
+     */
+    function site_config(string $name,mixed $vars='')
+    {
+        if(strpos($name,'.')!==false){
+            $name=explode('.',$name);
+            $group=$name[0];
+            $name=$name[1];
+        }else{
+            $group=$name;
+            $name='';
+        }
+        if(!$vars){
+            $groupval=Cache::get('site_config_'.$group);
+            if(!$groupval){
+                $groupval=Config::where('group',$group)->column('value','name');
+                foreach ($groupval as $key=>$val){
+                    if(is_string($val)){
+                        if (str_starts_with($val, '{') &&  str_ends_with($val, '}')) {
+                            $groupval[$key]=json_decode($val,true);
+                            continue;
+                        }
+                        if(str_starts_with($val, '[') &&  str_ends_with($val, ']')){
+                            $groupval[$key]=json_decode($val,true);
+                            continue;
+                        }
+                    }
+                    $groupval[$key]=$val;
+                }
+                Cache::set('site_config_'.$group,$groupval);
+            }
+            if($name) {
+                return $groupval[$name];
+            }else{
+                return $groupval;
+            }
+        }else{
+            if($name) {
+                if(is_array($vars)){
+                    $vars=json_encode($vars,JSON_UNESCAPED_UNICODE);
+                }
+                Config::where(['group'=>$group,'name'=>$name])->update(['value'=>$vars]);
+            }else{
+                foreach ($vars as $key=>$val){
+                    if(is_array($val)){
+                        $val=json_encode($val,JSON_UNESCAPED_UNICODE);
+                    }
+                    Config::where(['group'=>$group,'name'=>$key])->update(['value'=>$val]);
+                }
+            }
+            Cache::delete('site_config_'.$group);
+        }
+    }
+}
+
+if (!function_exists('__')) {
+
+    /**
+     * 获取语言变量值
+     * @param string $name 语言变量名
+     * @param array  $vars 动态变量值
+     * @param string $lang 语言
+     * @return mixed
+     */
+    function __(string $name,array $vars = [])
+    {
+        return LangService::newInstance()->get($name,$vars);
+    }
+}
+if (!function_exists('str_rand')) {
+    /**
+     * 获取随机字符串
+     * @return string
+     */
+    function str_rand(int $num,string $str=''):string
+    {
+        if(!$str){
+            $str='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+        }
+        $len=strlen($str)-1;
+        $rand='';
+        for($i=0;$i<$num;$i++){
+            $rand.=$str[mt_rand(0,$len)];
+        }
+        return $rand;
+    }
+}
+
+if (!function_exists('uuid')) {
+    /**
+     * 获取全球唯一标识
+     * @return string
+     */
+    function uuid()
+    {
+        return sprintf(
+            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
+            mt_rand(0, 0xffff),
+            mt_rand(0, 0xffff),
+            mt_rand(0, 0xffff),
+            mt_rand(0, 0x0fff) | 0x4000,
+            mt_rand(0, 0x3fff) | 0x8000,
+            mt_rand(0, 0xffff),
+            mt_rand(0, 0xffff),
+            mt_rand(0, 0xffff)
+        );
+    }
+}
+
+if (!function_exists('get_module_alis')) {
+    /**
+     * 获取模块别名
+     * @return string
+     */
+    function get_module_alis(string $module='')
+    {
+        if(!$module){
+            $module=app('http')->getName();
+        }
+        $arr=config('app.app_map');
+        foreach ($arr as $key=>$vars){
+            if($vars==$module){
+                return $key;
+            }
+        }
+        return $module;
+    }
+}
+
+if (!function_exists('build_url')) {
+    /**
+     * 生成url地址
+     * @return string
+     */
+    function build_url(string $url,string $module=''):string
+    {
+        $arr=parse_url($url);
+        $modulename=get_module_alis($module);
+        $url_html_suffix='.'.config('route.url_html_suffix');
+        if(strpos($arr['path'],$url_html_suffix)===false){
+            $arr['path'].=$url_html_suffix;
+        }
+        $r='';
+        if(isset($arr['scheme'])){
+            $r.=$arr['scheme'].'://';
+        }
+        if(isset($arr['host'])){
+            $r.=$arr['host'];
+        }
+        if(isset($arr['path'])){
+            if(!str_starts_with($arr['path'],'/')) {
+                $r .= '/'.$modulename.'/';
+            }
+            $r.=$arr['path'];
+        }
+        if(isset($arr['query'])){
+            $r.='?'.$arr['query'];
+        }
+        return $r;
+    }
+}
+
+if (!function_exists('rmdirs')) {
+
+    /**
+     * 删除文件夹
+     * @param string $dirname  目录
+     * @param bool   $withself 是否删除自身
+     * @return boolean
+     */
+    function rmdirs(string $dirname, bool $withself = true)
+    {
+        if (!is_dir($dirname)) {
+            return false;
+        }
+        $files = new RecursiveIteratorIterator(
+            new RecursiveDirectoryIterator($dirname, RecursiveDirectoryIterator::SKIP_DOTS),
+            RecursiveIteratorIterator::CHILD_FIRST
+        );
+        foreach ($files as $fileinfo) {
+            $todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
+            $todo($fileinfo->getRealPath());
+        }
+        if ($withself) {
+            @rmdir($dirname);
+        }
+        return true;
+    }
+}
+
+if (!function_exists('create_file')) {
+    /**
+     * 创建文件并写入内容,如果所在文件夹不存在,则创建
+     */
+    function create_file(string $filepath, string $content = ''){
+        $dir = dirname($filepath);
+        if (!is_dir($dir)) {
+            mkdir($dir, 0755, true);
+        }
+        file_put_contents($filepath, $content);
+    }
+}
+
+if (!function_exists('get_addons')) {
+    /**
+     * 获取插件信息
+     * @param string $pack 插件标识
+     * @return array|bool
+     */
+    function get_addons(string $pack='')
+    {
+        $addons=Cache::get('download-addons');
+        if(!$addons){
+            $addons=Addons::field('id,key,type,name,install,open')->select();
+            Cache::set('download-addons',$addons);
+        }
+        if(!$pack){
+            return $addons;
+        }
+        foreach ($addons as $addon){
+            if($addon['pack']==$pack){
+                return $addon;
+            }
+        }
+        return false;
+    }
+}
+
+if (!function_exists('addons_installed')) {
+    /**
+     * 判断是否安装插件
+     * @param string $pack 插件标识
+     * @return array|bool
+     */
+    function addons_installed(string $pack)
+    {
+        $addons=Cache::get('download-addons');
+        if(!$addons){
+            $addons=Addons::field('id,key,pack,type,name,install,open')->select();
+            Cache::set('download-addons',$addons);
+        }
+        foreach ($addons as $addon){
+            if($addon['pack']==$pack && $addon['install']==1){
+                return true;
+            }
+        }
+        return false;
+    }
+}
+
+if(!function_exists('getDbPrefix')){
+    function getDbPrefix()
+    {
+        $config = \think\facade\Db::getConfig();
+        $default=$config['default'];
+        $prefix=$config['connections'][$default]['prefix'];
+        return $prefix;
+    }
+}

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません