作者 root

更新框架

正在显示 47 个修改的文件 包含 4803 行增加0 行删除

要显示太多修改。

为保证性能只显示 47 of 47+ 个文件。

  1 +{
  2 + "directory": "public/assets/libs",
  3 + "ignoredDependencies": [
  4 + "es6-promise",
  5 + "file-saver",
  6 + "html2canvas",
  7 + "jspdf",
  8 + "jspdf-autotable"
  9 + ]
  10 +}
  1 +[app]
  2 +debug = false
  3 +trace = false
  4 +
  5 +[database]
  6 +hostname = 127.0.0.1
  7 +database = fastadmin
  8 +username = root
  9 +password = root
  10 +hostport = 3306
  11 +prefix = fa_
  1 +/nbproject/
  2 +/thinkphp/
  3 +/vendor/
  4 +/runtime/*
  5 +/addons/*
  6 +/application/admin/command/Install/*.lock
  7 +/public/assets/libs/
  8 +/public/assets/addons/*
  9 +/public/uploads/*
  10 +.idea
  11 +composer.lock
  12 +*.log
  13 +*.css.map
  14 +!.gitkeep
  15 +.env
  16 +.svn
  17 +.vscode
  18 +node_modules
  1 +Apache License
  2 +Version 2.0, January 2004
  3 +http://www.apache.org/licenses/
  4 +
  5 +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
  6 +
  7 +1. Definitions.
  8 +
  9 +"License" shall mean the terms and conditions for use, reproduction, and
  10 +distribution as defined by Sections 1 through 9 of this document.
  11 +
  12 +"Licensor" shall mean the copyright owner or entity authorized by the copyright
  13 +owner that is granting the License.
  14 +
  15 +"Legal Entity" shall mean the union of the acting entity and all other entities
  16 +that control, are controlled by, or are under common control with that entity.
  17 +For the purposes of this definition, "control" means (i) the power, direct or
  18 +indirect, to cause the direction or management of such entity, whether by
  19 +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
  20 +outstanding shares, or (iii) beneficial ownership of such entity.
  21 +
  22 +"You" (or "Your") shall mean an individual or Legal Entity exercising
  23 +permissions granted by this License.
  24 +
  25 +"Source" form shall mean the preferred form for making modifications, including
  26 +but not limited to software source code, documentation source, and configuration
  27 +files.
  28 +
  29 +"Object" form shall mean any form resulting from mechanical transformation or
  30 +translation of a Source form, including but not limited to compiled object code,
  31 +generated documentation, and conversions to other media types.
  32 +
  33 +"Work" shall mean the work of authorship, whether in Source or Object form, made
  34 +available under the License, as indicated by a copyright notice that is included
  35 +in or attached to the work (an example is provided in the Appendix below).
  36 +
  37 +"Derivative Works" shall mean any work, whether in Source or Object form, that
  38 +is based on (or derived from) the Work and for which the editorial revisions,
  39 +annotations, elaborations, or other modifications represent, as a whole, an
  40 +original work of authorship. For the purposes of this License, Derivative Works
  41 +shall not include works that remain separable from, or merely link (or bind by
  42 +name) to the interfaces of, the Work and Derivative Works thereof.
  43 +
  44 +"Contribution" shall mean any work of authorship, including the original version
  45 +of the Work and any modifications or additions to that Work or Derivative Works
  46 +thereof, that is intentionally submitted to Licensor for inclusion in the Work
  47 +by the copyright owner or by an individual or Legal Entity authorized to submit
  48 +on behalf of the copyright owner. For the purposes of this definition,
  49 +"submitted" means any form of electronic, verbal, or written communication sent
  50 +to the Licensor or its representatives, including but not limited to
  51 +communication on electronic mailing lists, source code control systems, and
  52 +issue tracking systems that are managed by, or on behalf of, the Licensor for
  53 +the purpose of discussing and improving the Work, but excluding communication
  54 +that is conspicuously marked or otherwise designated in writing by the copyright
  55 +owner as "Not a Contribution."
  56 +
  57 +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
  58 +of whom a Contribution has been received by Licensor and subsequently
  59 +incorporated within the Work.
  60 +
  61 +2. Grant of Copyright License.
  62 +
  63 +Subject to the terms and conditions of this License, each Contributor hereby
  64 +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
  65 +irrevocable copyright license to reproduce, prepare Derivative Works of,
  66 +publicly display, publicly perform, sublicense, and distribute the Work and such
  67 +Derivative Works in Source or Object form.
  68 +
  69 +3. Grant of Patent License.
  70 +
  71 +Subject to the terms and conditions of this License, each Contributor hereby
  72 +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
  73 +irrevocable (except as stated in this section) patent license to make, have
  74 +made, use, offer to sell, sell, import, and otherwise transfer the Work, where
  75 +such license applies only to those patent claims licensable by such Contributor
  76 +that are necessarily infringed by their Contribution(s) alone or by combination
  77 +of their Contribution(s) with the Work to which such Contribution(s) was
  78 +submitted. If You institute patent litigation against any entity (including a
  79 +cross-claim or counterclaim in a lawsuit) alleging that the Work or a
  80 +Contribution incorporated within the Work constitutes direct or contributory
  81 +patent infringement, then any patent licenses granted to You under this License
  82 +for that Work shall terminate as of the date such litigation is filed.
  83 +
  84 +4. Redistribution.
  85 +
  86 +You may reproduce and distribute copies of the Work or Derivative Works thereof
  87 +in any medium, with or without modifications, and in Source or Object form,
  88 +provided that You meet the following conditions:
  89 +
  90 +You must give any other recipients of the Work or Derivative Works a copy of
  91 +this License; and
  92 +You must cause any modified files to carry prominent notices stating that You
  93 +changed the files; and
  94 +You must retain, in the Source form of any Derivative Works that You distribute,
  95 +all copyright, patent, trademark, and attribution notices from the Source form
  96 +of the Work, excluding those notices that do not pertain to any part of the
  97 +Derivative Works; and
  98 +If the Work includes a "NOTICE" text file as part of its distribution, then any
  99 +Derivative Works that You distribute must include a readable copy of the
  100 +attribution notices contained within such NOTICE file, excluding those notices
  101 +that do not pertain to any part of the Derivative Works, in at least one of the
  102 +following places: within a NOTICE text file distributed as part of the
  103 +Derivative Works; within the Source form or documentation, if provided along
  104 +with the Derivative Works; or, within a display generated by the Derivative
  105 +Works, if and wherever such third-party notices normally appear. The contents of
  106 +the NOTICE file are for informational purposes only and do not modify the
  107 +License. You may add Your own attribution notices within Derivative Works that
  108 +You distribute, alongside or as an addendum to the NOTICE text from the Work,
  109 +provided that such additional attribution notices cannot be construed as
  110 +modifying the License.
  111 +You may add Your own copyright statement to Your modifications and may provide
  112 +additional or different license terms and conditions for use, reproduction, or
  113 +distribution of Your modifications, or for any such Derivative Works as a whole,
  114 +provided Your use, reproduction, and distribution of the Work otherwise complies
  115 +with the conditions stated in this License.
  116 +
  117 +5. Submission of Contributions.
  118 +
  119 +Unless You explicitly state otherwise, any Contribution intentionally submitted
  120 +for inclusion in the Work by You to the Licensor shall be under the terms and
  121 +conditions of this License, without any additional terms or conditions.
  122 +Notwithstanding the above, nothing herein shall supersede or modify the terms of
  123 +any separate license agreement you may have executed with Licensor regarding
  124 +such Contributions.
  125 +
  126 +6. Trademarks.
  127 +
  128 +This License does not grant permission to use the trade names, trademarks,
  129 +service marks, or product names of the Licensor, except as required for
  130 +reasonable and customary use in describing the origin of the Work and
  131 +reproducing the content of the NOTICE file.
  132 +
  133 +7. Disclaimer of Warranty.
  134 +
  135 +Unless required by applicable law or agreed to in writing, Licensor provides the
  136 +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
  137 +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
  138 +including, without limitation, any warranties or conditions of TITLE,
  139 +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
  140 +solely responsible for determining the appropriateness of using or
  141 +redistributing the Work and assume any risks associated with Your exercise of
  142 +permissions under this License.
  143 +
  144 +8. Limitation of Liability.
  145 +
  146 +In no event and under no legal theory, whether in tort (including negligence),
  147 +contract, or otherwise, unless required by applicable law (such as deliberate
  148 +and grossly negligent acts) or agreed to in writing, shall any Contributor be
  149 +liable to You for damages, including any direct, indirect, special, incidental,
  150 +or consequential damages of any character arising as a result of this License or
  151 +out of the use or inability to use the Work (including but not limited to
  152 +damages for loss of goodwill, work stoppage, computer failure or malfunction, or
  153 +any and all other commercial damages or losses), even if such Contributor has
  154 +been advised of the possibility of such damages.
  155 +
  156 +9. Accepting Warranty or Additional Liability.
  157 +
  158 +While redistributing the Work or Derivative Works thereof, You may choose to
  159 +offer, and charge a fee for, acceptance of support, warranty, indemnity, or
  160 +other liability obligations and/or rights consistent with this License. However,
  161 +in accepting such obligations, You may act only on Your own behalf and on Your
  162 +sole responsibility, not on behalf of any other Contributor, and only if You
  163 +agree to indemnify, defend, and hold each Contributor harmless for any liability
  164 +incurred by, or claims asserted against, such Contributor by reason of your
  165 +accepting any such warranty or additional liability.
  166 +
  167 +END OF TERMS AND CONDITIONS
  168 +
  169 +APPENDIX: How to apply the Apache License to your work
  170 +
  171 +To apply the Apache License to your work, attach the following boilerplate
  172 +notice, with the fields enclosed by brackets "{}" replaced with your own
  173 +identifying information. (Don't include the brackets!) The text should be
  174 +enclosed in the appropriate comment syntax for the file format. We also
  175 +recommend that a file or class name and description of purpose be included on
  176 +the same "printed page" as the copyright notice for easier identification within
  177 +third-party archives.
  178 +
  179 + Copyright 2017 Karson
  180 +
  181 + Licensed under the Apache License, Version 2.0 (the "License");
  182 + you may not use this file except in compliance with the License.
  183 + You may obtain a copy of the License at
  184 +
  185 + http://www.apache.org/licenses/LICENSE-2.0
  186 +
  187 + Unless required by applicable law or agreed to in writing, software
  188 + distributed under the License is distributed on an "AS IS" BASIS,
  189 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  190 + See the License for the specific language governing permissions and
  191 + limitations under the License.
  1 +FastAdmin是一款基于ThinkPHP+Bootstrap的极速后台开发框架。
  2 +
  3 +
  4 +## 主要特性
  5 +
  6 +* 基于`Auth`验证的权限管理系统
  7 + * 支持无限级父子级权限继承,父级的管理员可任意增删改子级管理员及权限设置
  8 + * 支持单管理员多角色
  9 + * 支持管理子级数据或个人数据
  10 +* 强大的一键生成功能
  11 + * 一键生成CRUD,包括控制器、模型、视图、JS、语言包、菜单、回收站等
  12 + * 一键压缩打包JS和CSS文件,一键CDN静态资源部署
  13 + * 一键生成控制器菜单和规则
  14 + * 一键生成API接口文档
  15 +* 完善的前端功能组件开发
  16 + * 基于`AdminLTE`二次开发
  17 + * 基于`Bootstrap`开发,自适应手机、平板、PC
  18 + * 基于`RequireJS`进行JS模块管理,按需加载
  19 + * 基于`Less`进行样式开发
  20 +* 强大的插件扩展功能,在线安装卸载升级插件
  21 +* 通用的会员模块和API模块
  22 +* 共用同一账号体系的Web端会员中心权限验证和API接口会员权限验证
  23 +* 二级域名部署支持,同时域名支持绑定到应用插件
  24 +* 多语言支持,服务端及客户端支持
  25 +* 支持大文件分片上传、剪切板粘贴上传、拖拽上传,进度条显示,图片上传前压缩
  26 +* 支持表格固定列、固定表头、跨页选择、Excel导出、模板渲染等功能
  27 +* 强大的第三方应用模块支持([CMS](https://www.fastadmin.net/store/cms.html)[博客](https://www.fastadmin.net/store/blog.html)[知识付费问答](https://www.fastadmin.net/store/ask.html)[在线投票系统](https://www.fastadmin.net/store/vote.html)[B2C商城](https://www.fastadmin.net/store/shopro.html)[B2B2C商城](https://www.fastadmin.net/store/wanlshop.html))
  28 +* 支持CMS、博客、知识付费问答无缝整合[Xunsearch全文搜索](https://www.fastadmin.net/store/xunsearch.html)
  29 +* 第三方小程序支持([CMS小程序](https://www.fastadmin.net/store/cms.html)[预订小程序](https://www.fastadmin.net/store/ball.html)[问答小程序](https://www.fastadmin.net/store/ask.html)[点餐小程序](https://www.fastadmin.net/store/unidrink.html)[B2C小程序](https://www.fastadmin.net/store/shopro.html)[B2B2C小程序](https://www.fastadmin.net/store/wanlshop.html)[博客小程序](https://www.fastadmin.net/store/blog.html))
  30 +* 整合第三方短信接口(阿里云、腾讯云短信)
  31 +* 无缝整合第三方云存储(七牛云、阿里云OSS、又拍云)功能,支持云储存分片上传
  32 +* 第三方富文本编辑器支持(Summernote、Kindeditor、百度编辑器)
  33 +* 第三方登录(QQ、微信、微博)整合
  34 +* 第三方支付(微信、支付宝)无缝整合,微信支持PC端扫码支付
  35 +* 丰富的插件应用市场
  36 +
  37 +## 安装使用
  38 +
  39 +https://doc.fastadmin.net
  40 +
  41 +## 在线演示
  42 +
  43 +https://demo.fastadmin.net
  44 +
  45 +用户名:admin
  46 +
  47 +密 码:123456
  48 +
  49 +提 示:演示站数据无法进行修改,请下载源码安装体验全部功能
  50 +
  51 +## 界面截图
  52 +![控制台](https://images.gitee.com/uploads/images/2020/0929/202947_8db2d281_10933.gif "控制台")
  53 +
  54 +## 问题反馈
  55 +
  56 +在使用中有任何问题,请使用以下联系方式联系我们
  57 +
  58 +交流社区: https://ask.fastadmin.net
  59 +
  60 +QQ群: [636393962](https://jq.qq.com/?_wv=1027&k=487PNBb)() [708784003](https://jq.qq.com/?_wv=1027&k=5ObjtwM)(满) [964776039](https://jq.qq.com/?_wv=1027&k=59qjU2P)(3群) [749803490](https://jq.qq.com/?_wv=1027&k=5tczi88)(满) [767103006](https://jq.qq.com/?_wv=1027&k=5Z1U751)() [675115483](https://jq.qq.com/?_wv=1027&k=54I6mts)(6群)
  61 +
  62 +Github: https://github.com/karsonzhang/fastadmin
  63 +
  64 +Gitee: https://gitee.com/karson/fastadmin
  65 +
  66 +## 特别鸣谢
  67 +
  68 +感谢以下的项目,排名不分先后
  69 +
  70 +ThinkPHP:http://www.thinkphp.cn
  71 +
  72 +AdminLTE:https://adminlte.io
  73 +
  74 +Bootstrap:http://getbootstrap.com
  75 +
  76 +jQuery:http://jquery.com
  77 +
  78 +Bootstrap-table:https://github.com/wenzhixin/bootstrap-table
  79 +
  80 +Nice-validator: https://validator.niceue.com
  81 +
  82 +SelectPage: https://github.com/TerryZ/SelectPage
  83 +
  84 +Layer: https://layer.layui.com
  85 +
  86 +DropzoneJS: https://www.dropzonejs.com
  87 +
  88 +
  89 +## 版权信息
  90 +
  91 +FastAdmin遵循Apache2开源协议发布,并提供免费使用。
  92 +
  93 +本项目包含的第三方源码和二进制文件之版权信息另行标注。
  94 +
  95 +版权所有Copyright © 2017-2020 by FastAdmin (https://www.fastadmin.net)
  96 +
  97 +All rights reserved。
  1 +deny from all
  1 +<?php
  2 +
  3 +namespace app\admin\behavior;
  4 +
  5 +class AdminLog
  6 +{
  7 + public function run(&$params)
  8 + {
  9 + //只记录POST请求的日志
  10 + if (request()->isPost() && config('fastadmin.auto_record_log')) {
  11 + \app\admin\model\AdminLog::record();
  12 + }
  13 + }
  14 +}
  1 +<?php
  2 +
  3 +namespace app\admin\command;
  4 +
  5 +use think\addons\AddonException;
  6 +use think\addons\Service;
  7 +use think\Config;
  8 +use think\console\Command;
  9 +use think\console\Input;
  10 +use think\console\input\Option;
  11 +use think\console\Output;
  12 +use think\Db;
  13 +use think\Exception;
  14 +use think\exception\PDOException;
  15 +
  16 +class Addon extends Command
  17 +{
  18 +
  19 + protected function configure()
  20 + {
  21 + $this
  22 + ->setName('addon')
  23 + ->addOption('name', 'a', Option::VALUE_REQUIRED, 'addon name', null)
  24 + ->addOption('action', 'c', Option::VALUE_REQUIRED, 'action(create/enable/disable/install/uninstall/refresh/upgrade/package/move)', 'create')
  25 + ->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override', null)
  26 + ->addOption('release', 'r', Option::VALUE_OPTIONAL, 'addon release version', null)
  27 + ->addOption('uid', 'u', Option::VALUE_OPTIONAL, 'fastadmin uid', null)
  28 + ->addOption('token', 't', Option::VALUE_OPTIONAL, 'fastadmin token', null)
  29 + ->addOption('local', 'l', Option::VALUE_OPTIONAL, 'local package', null)
  30 + ->setDescription('Addon manager');
  31 + }
  32 +
  33 + protected function execute(Input $input, Output $output)
  34 + {
  35 + $name = $input->getOption('name') ?: '';
  36 + $action = $input->getOption('action') ?: '';
  37 + if (stripos($name, 'addons' . DS) !== false) {
  38 + $name = explode(DS, $name)[1];
  39 + }
  40 + //强制覆盖
  41 + $force = $input->getOption('force');
  42 + //版本
  43 + $release = $input->getOption('release') ?: '';
  44 + //uid
  45 + $uid = $input->getOption('uid') ?: '';
  46 + //token
  47 + $token = $input->getOption('token') ?: '';
  48 +
  49 + include dirname(__DIR__) . DS . 'common.php';
  50 +
  51 + if (!$name) {
  52 + throw new Exception('Addon name could not be empty');
  53 + }
  54 + if (!$action || !in_array($action, ['create', 'disable', 'enable', 'install', 'uninstall', 'refresh', 'upgrade', 'package', 'move'])) {
  55 + throw new Exception('Please input correct action name');
  56 + }
  57 +
  58 + // 查询一次SQL,判断连接是否正常
  59 + Db::execute("SELECT 1");
  60 +
  61 + $addonDir = ADDON_PATH . $name . DS;
  62 + switch ($action) {
  63 + case 'create':
  64 + //非覆盖模式时如果存在则报错
  65 + if (is_dir($addonDir) && !$force) {
  66 + throw new Exception("addon already exists!\nIf you need to create again, use the parameter --force=true ");
  67 + }
  68 + //如果存在先移除
  69 + if (is_dir($addonDir)) {
  70 + rmdirs($addonDir);
  71 + }
  72 + mkdir($addonDir, 0755, true);
  73 + mkdir($addonDir . DS . 'controller', 0755, true);
  74 + $menuList = \app\common\library\Menu::export($name);
  75 + $createMenu = $this->getCreateMenu($menuList);
  76 + $prefix = Config::get('database.prefix');
  77 + $createTableSql = '';
  78 + try {
  79 + $result = Db::query("SHOW CREATE TABLE `" . $prefix . $name . "`;");
  80 + if (isset($result[0]) && isset($result[0]['Create Table'])) {
  81 + $createTableSql = $result[0]['Create Table'];
  82 + }
  83 + } catch (PDOException $e) {
  84 +
  85 + }
  86 +
  87 + $data = [
  88 + 'name' => $name,
  89 + 'addon' => $name,
  90 + 'addonClassName' => ucfirst($name),
  91 + 'addonInstallMenu' => $createMenu ? "\$menu = " . var_export_short($createMenu) . ";\n\tMenu::create(\$menu);" : '',
  92 + 'addonUninstallMenu' => $menuList ? 'Menu::delete("' . $name . '");' : '',
  93 + 'addonEnableMenu' => $menuList ? 'Menu::enable("' . $name . '");' : '',
  94 + 'addonDisableMenu' => $menuList ? 'Menu::disable("' . $name . '");' : '',
  95 + ];
  96 + $this->writeToFile("addon", $data, $addonDir . ucfirst($name) . '.php');
  97 + $this->writeToFile("config", $data, $addonDir . 'config.php');
  98 + $this->writeToFile("info", $data, $addonDir . 'info.ini');
  99 + $this->writeToFile("controller", $data, $addonDir . 'controller' . DS . 'Index.php');
  100 + if ($createTableSql) {
  101 + $createTableSql = str_replace("`" . $prefix, '`__PREFIX__', $createTableSql);
  102 + file_put_contents($addonDir . 'install.sql', $createTableSql);
  103 + }
  104 +
  105 + $output->info("Create Successed!");
  106 + break;
  107 + case 'disable':
  108 + case 'enable':
  109 + try {
  110 + //调用启用、禁用的方法
  111 + Service::$action($name, 0);
  112 + } catch (AddonException $e) {
  113 + if ($e->getCode() != -3) {
  114 + throw new Exception($e->getMessage());
  115 + }
  116 + if (!$force) {
  117 + //如果有冲突文件则提醒
  118 + $data = $e->getData();
  119 + foreach ($data['conflictlist'] as $k => $v) {
  120 + $output->warning($v);
  121 + }
  122 + $output->info("Are you sure you want to " . ($action == 'enable' ? 'override' : 'delete') . " all those files? Type 'yes' to continue: ");
  123 + $line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'));
  124 + if (trim($line) != 'yes') {
  125 + throw new Exception("Operation is aborted!");
  126 + }
  127 + }
  128 + //调用启用、禁用的方法
  129 + Service::$action($name, 1);
  130 + } catch (Exception $e) {
  131 + throw new Exception($e->getMessage());
  132 + }
  133 + $output->info(ucfirst($action) . " Successed!");
  134 + break;
  135 + case 'install':
  136 + //非覆盖模式时如果存在则报错
  137 + if (is_dir($addonDir) && !$force) {
  138 + throw new Exception("addon already exists!\nIf you need to install again, use the parameter --force=true ");
  139 + }
  140 + //如果存在先移除
  141 + if (is_dir($addonDir)) {
  142 + rmdirs($addonDir);
  143 + }
  144 + // 获取本地路径
  145 + $local = $input->getOption('local');
  146 + try {
  147 + Service::install($name, 0, ['version' => $release], $local);
  148 + } catch (AddonException $e) {
  149 + if ($e->getCode() != -3) {
  150 + throw new Exception($e->getMessage());
  151 + }
  152 + if (!$force) {
  153 + //如果有冲突文件则提醒
  154 + $data = $e->getData();
  155 + foreach ($data['conflictlist'] as $k => $v) {
  156 + $output->warning($v);
  157 + }
  158 + $output->info("Are you sure you want to override all those files? Type 'yes' to continue: ");
  159 + $line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'));
  160 + if (trim($line) != 'yes') {
  161 + throw new Exception("Operation is aborted!");
  162 + }
  163 + }
  164 + Service::install($name, 1, ['version' => $release, 'uid' => $uid, 'token' => $token], $local);
  165 + } catch (Exception $e) {
  166 + throw new Exception($e->getMessage());
  167 + }
  168 +
  169 + $output->info("Install Successed!");
  170 + break;
  171 + case 'uninstall':
  172 + //非覆盖模式时如果存在则报错
  173 + if (!$force) {
  174 + throw new Exception("If you need to uninstall addon, use the parameter --force=true ");
  175 + }
  176 + try {
  177 + Service::uninstall($name, 0);
  178 + } catch (AddonException $e) {
  179 + if ($e->getCode() != -3) {
  180 + throw new Exception($e->getMessage());
  181 + }
  182 + if (!$force) {
  183 + //如果有冲突文件则提醒
  184 + $data = $e->getData();
  185 + foreach ($data['conflictlist'] as $k => $v) {
  186 + $output->warning($v);
  187 + }
  188 + $output->info("Are you sure you want to delete all those files? Type 'yes' to continue: ");
  189 + $line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'));
  190 + if (trim($line) != 'yes') {
  191 + throw new Exception("Operation is aborted!");
  192 + }
  193 + }
  194 + Service::uninstall($name, 1);
  195 + } catch (Exception $e) {
  196 + throw new Exception($e->getMessage());
  197 + }
  198 +
  199 + $output->info("Uninstall Successed!");
  200 + break;
  201 + case 'refresh':
  202 + Service::refresh();
  203 + $output->info("Refresh Successed!");
  204 + break;
  205 + case 'upgrade':
  206 + Service::upgrade($name, ['version' => $release, 'uid' => $uid, 'token' => $token]);
  207 + $output->info("Upgrade Successed!");
  208 + break;
  209 + case 'package':
  210 + $infoFile = $addonDir . 'info.ini';
  211 + if (!is_file($infoFile)) {
  212 + throw new Exception(__('Addon info file was not found'));
  213 + }
  214 +
  215 + $info = get_addon_info($name);
  216 + if (!$info) {
  217 + throw new Exception(__('Addon info file data incorrect'));
  218 + }
  219 + $infoname = isset($info['name']) ? $info['name'] : '';
  220 + if (!$infoname || !preg_match("/^[a-z]+$/i", $infoname) || $infoname != $name) {
  221 + throw new Exception(__('Addon info name incorrect'));
  222 + }
  223 +
  224 + $infoversion = isset($info['version']) ? $info['version'] : '';
  225 + if (!$infoversion || !preg_match("/^\d+\.\d+\.\d+$/i", $infoversion)) {
  226 + throw new Exception(__('Addon info version incorrect'));
  227 + }
  228 +
  229 + $addonTmpDir = RUNTIME_PATH . 'addons' . DS;
  230 + if (!is_dir($addonTmpDir)) {
  231 + @mkdir($addonTmpDir, 0755, true);
  232 + }
  233 + $addonFile = $addonTmpDir . $infoname . '-' . $infoversion . '.zip';
  234 + if (!class_exists('ZipArchive')) {
  235 + throw new Exception(__('ZinArchive not install'));
  236 + }
  237 + $zip = new \ZipArchive;
  238 + $zip->open($addonFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
  239 +
  240 + $files = new \RecursiveIteratorIterator(
  241 + new \RecursiveDirectoryIterator($addonDir), \RecursiveIteratorIterator::LEAVES_ONLY
  242 + );
  243 +
  244 + foreach ($files as $name => $file) {
  245 + if (!$file->isDir()) {
  246 + $filePath = $file->getRealPath();
  247 + $relativePath = str_replace(DS, '/', substr($filePath, strlen($addonDir)));
  248 + if (!in_array($file->getFilename(), ['.git', '.DS_Store', 'Thumbs.db'])) {
  249 + $zip->addFile($filePath, $relativePath);
  250 + }
  251 + }
  252 + }
  253 + $zip->close();
  254 + $output->info("Package Successed!");
  255 + break;
  256 + case 'move':
  257 + $movePath = [
  258 + 'adminOnlySelfDir' => ['admin/behavior', 'admin/controller', 'admin/library', 'admin/model', 'admin/validate', 'admin/view'],
  259 + 'adminAllSubDir' => ['admin/lang'],
  260 + 'publicDir' => ['public/assets/addons', 'public/assets/js/backend']
  261 + ];
  262 + $paths = [];
  263 + $appPath = str_replace('/', DS, APP_PATH);
  264 + $rootPath = str_replace('/', DS, ROOT_PATH);
  265 + foreach ($movePath as $k => $items) {
  266 + switch ($k) {
  267 + case 'adminOnlySelfDir':
  268 + foreach ($items as $v) {
  269 + $v = str_replace('/', DS, $v);
  270 + $oldPath = $appPath . $v . DS . $name;
  271 + $newPath = $rootPath . "addons" . DS . $name . DS . "application" . DS . $v . DS . $name;
  272 + $paths[$oldPath] = $newPath;
  273 + }
  274 + break;
  275 + case 'adminAllSubDir':
  276 + foreach ($items as $v) {
  277 + $v = str_replace('/', DS, $v);
  278 + $vPath = $appPath . $v;
  279 + $list = scandir($vPath);
  280 + foreach ($list as $_v) {
  281 + if (!in_array($_v, ['.', '..']) && is_dir($vPath . DS . $_v)) {
  282 + $oldPath = $appPath . $v . DS . $_v . DS . $name;
  283 + $newPath = $rootPath . "addons" . DS . $name . DS . "application" . DS . $v . DS . $_v . DS . $name;
  284 + $paths[$oldPath] = $newPath;
  285 + }
  286 + }
  287 + }
  288 + break;
  289 + case 'publicDir':
  290 + foreach ($items as $v) {
  291 + $v = str_replace('/', DS, $v);
  292 + $oldPath = $rootPath . $v . DS . $name;
  293 + $newPath = $rootPath . 'addons' . DS . $name . DS . $v . DS . $name;
  294 + $paths[$oldPath] = $newPath;
  295 + }
  296 + break;
  297 + }
  298 + }
  299 + foreach ($paths as $oldPath => $newPath) {
  300 + if (is_dir($oldPath)) {
  301 + if ($force) {
  302 + if (is_dir($newPath)) {
  303 + $list = scandir($newPath);
  304 + foreach ($list as $_v) {
  305 + if (!in_array($_v, ['.', '..'])) {
  306 + $file = $newPath . DS . $_v;
  307 + @chmod($file, 0777);
  308 + @unlink($file);
  309 + }
  310 + }
  311 + @rmdir($newPath);
  312 + }
  313 + }
  314 + copydirs($oldPath, $newPath);
  315 + }
  316 + }
  317 + break;
  318 + default:
  319 + break;
  320 + }
  321 + }
  322 +
  323 + /**
  324 + * 获取创建菜单的数组
  325 + * @param array $menu
  326 + * @return array
  327 + */
  328 + protected function getCreateMenu($menu)
  329 + {
  330 + $result = [];
  331 + foreach ($menu as $k => & $v) {
  332 + $arr = [
  333 + 'name' => $v['name'],
  334 + 'title' => $v['title'],
  335 + ];
  336 + if ($v['icon'] != 'fa fa-circle-o') {
  337 + $arr['icon'] = $v['icon'];
  338 + }
  339 + if ($v['ismenu']) {
  340 + $arr['ismenu'] = $v['ismenu'];
  341 + }
  342 + if (isset($v['childlist']) && $v['childlist']) {
  343 + $arr['sublist'] = $this->getCreateMenu($v['childlist']);
  344 + }
  345 + $result[] = $arr;
  346 + }
  347 + return $result;
  348 + }
  349 +
  350 + /**
  351 + * 写入到文件
  352 + * @param string $name
  353 + * @param array $data
  354 + * @param string $pathname
  355 + * @return mixed
  356 + */
  357 + protected function writeToFile($name, $data, $pathname)
  358 + {
  359 + $search = $replace = [];
  360 + foreach ($data as $k => $v) {
  361 + $search[] = "{%{$k}%}";
  362 + $replace[] = $v;
  363 + }
  364 + $stub = file_get_contents($this->getStub($name));
  365 + $content = str_replace($search, $replace, $stub);
  366 +
  367 + if (!is_dir(dirname($pathname))) {
  368 + mkdir(strtolower(dirname($pathname)), 0755, true);
  369 + }
  370 + return file_put_contents($pathname, $content);
  371 + }
  372 +
  373 + /**
  374 + * 获取基础模板
  375 + * @param string $name
  376 + * @return string
  377 + */
  378 + protected function getStub($name)
  379 + {
  380 + return __DIR__ . '/Addon/stubs/' . $name . '.stub';
  381 + }
  382 +
  383 +}
  1 +<?php
  2 +
  3 +namespace addons\{%name%};
  4 +
  5 +use app\common\library\Menu;
  6 +use think\Addons;
  7 +
  8 +/**
  9 + * 插件
  10 + */
  11 +class {%addonClassName%} extends Addons
  12 +{
  13 +
  14 + /**
  15 + * 插件安装方法
  16 + * @return bool
  17 + */
  18 + public function install()
  19 + {
  20 + {%addonInstallMenu%}
  21 + return true;
  22 + }
  23 +
  24 + /**
  25 + * 插件卸载方法
  26 + * @return bool
  27 + */
  28 + public function uninstall()
  29 + {
  30 + {%addonUninstallMenu%}
  31 + return true;
  32 + }
  33 +
  34 + /**
  35 + * 插件启用方法
  36 + * @return bool
  37 + */
  38 + public function enable()
  39 + {
  40 + {%addonEnableMenu%}
  41 + return true;
  42 + }
  43 +
  44 + /**
  45 + * 插件禁用方法
  46 + * @return bool
  47 + */
  48 + public function disable()
  49 + {
  50 + {%addonDisableMenu%}
  51 + return true;
  52 + }
  53 +
  54 + /**
  55 + * 实现钩子方法
  56 + * @return mixed
  57 + */
  58 + public function testhook($param)
  59 + {
  60 + // 调用钩子时候的参数信息
  61 + print_r($param);
  62 + // 当前插件的配置信息,配置信息存在当前目录的config.php文件中,见下方
  63 + print_r($this->getConfig());
  64 + // 可以返回模板,模板文件默认读取的为插件目录中的文件。模板名不能为空!
  65 + //return $this->fetch('view/info');
  66 + }
  67 +
  68 +}
  1 +<?php
  2 +
  3 +return [
  4 + [
  5 + //配置唯一标识
  6 + 'name' => 'usernmae',
  7 + //显示的标题
  8 + 'title' => '用户名',
  9 + //类型
  10 + 'type' => 'string',
  11 + //数据字典
  12 + 'content' => [
  13 + ],
  14 + //值
  15 + 'value' => '',
  16 + //验证规则
  17 + 'rule' => 'required',
  18 + //错误消息
  19 + 'msg' => '',
  20 + //提示消息
  21 + 'tip' => '',
  22 + //成功消息
  23 + 'ok' => '',
  24 + //扩展信息
  25 + 'extend' => ''
  26 + ],
  27 + [
  28 + 'name' => 'password',
  29 + 'title' => '密码',
  30 + 'type' => 'string',
  31 + 'content' => [
  32 + ],
  33 + 'value' => '',
  34 + 'rule' => 'required',
  35 + 'msg' => '',
  36 + 'tip' => '',
  37 + 'ok' => '',
  38 + 'extend' => ''
  39 + ],
  40 +];
  1 +<?php
  2 +
  3 +namespace addons\{%addon%}\controller;
  4 +
  5 +use think\addons\Controller;
  6 +
  7 +class Index extends Controller
  8 +{
  9 +
  10 + public function index()
  11 + {
  12 + $this->error("当前插件暂无前台页面");
  13 + }
  14 +
  15 +}
  1 +name = {%name%}
  2 +title = 插件名称{%name%}
  3 +intro = FastAdmin插件
  4 +author = yourname
  5 +website = https://www.fastadmin.net
  6 +version = 1.0.0
  7 +state = 1
  1 +<?php
  2 +
  3 +namespace app\admin\command;
  4 +
  5 +use app\admin\command\Api\library\Builder;
  6 +use think\Config;
  7 +use think\console\Command;
  8 +use think\console\Input;
  9 +use think\console\input\Option;
  10 +use think\console\Output;
  11 +use think\Exception;
  12 +
  13 +class Api extends Command
  14 +{
  15 + protected function configure()
  16 + {
  17 + $site = Config::get('site');
  18 + $this
  19 + ->setName('api')
  20 + ->addOption('url', 'u', Option::VALUE_OPTIONAL, 'default api url', '')
  21 + ->addOption('module', 'm', Option::VALUE_OPTIONAL, 'module name(admin/index/api)', 'api')
  22 + ->addOption('output', 'o', Option::VALUE_OPTIONAL, 'output index file name', 'api.html')
  23 + ->addOption('template', 'e', Option::VALUE_OPTIONAL, '', 'index.html')
  24 + ->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override general file', false)
  25 + ->addOption('title', 't', Option::VALUE_OPTIONAL, 'document title', $site['name'] ?? '')
  26 + ->addOption('class', 'c', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'extend class', null)
  27 + ->addOption('language', 'l', Option::VALUE_OPTIONAL, 'language', 'zh-cn')
  28 + ->addOption('addon', 'a', Option::VALUE_OPTIONAL, 'addon name', null)
  29 + ->addOption('controller', 'r', Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, 'controller name', null)
  30 + ->setDescription('Build Api document from controller');
  31 + }
  32 +
  33 + protected function execute(Input $input, Output $output)
  34 + {
  35 + $apiDir = __DIR__ . DS . 'Api' . DS;
  36 +
  37 + $force = $input->getOption('force');
  38 + $url = $input->getOption('url');
  39 + $language = $input->getOption('language');
  40 + $template = $input->getOption('template');
  41 + if (!preg_match("/^([a-z0-9]+)\.html\$/i", $template)) {
  42 + throw new Exception('template file not correct');
  43 + }
  44 + $language = $language ? $language : 'zh-cn';
  45 + $langFile = $apiDir . 'lang' . DS . $language . '.php';
  46 + if (!is_file($langFile)) {
  47 + throw new Exception('language file not found');
  48 + }
  49 + $lang = include_once $langFile;
  50 + // 目标目录
  51 + $output_dir = ROOT_PATH . 'public' . DS;
  52 + $output_file = $output_dir . $input->getOption('output');
  53 + if (is_file($output_file) && !$force) {
  54 + throw new Exception("api index file already exists!\nIf you need to rebuild again, use the parameter --force=true ");
  55 + }
  56 + // 模板文件
  57 + $template_dir = $apiDir . 'template' . DS;
  58 + $template_file = $template_dir . $template;
  59 + if (!is_file($template_file)) {
  60 + throw new Exception('template file not found');
  61 + }
  62 + // 额外的类
  63 + $classes = $input->getOption('class');
  64 + // 标题
  65 + $title = $input->getOption('title');
  66 + // 模块
  67 + $module = $input->getOption('module');
  68 + // 插件
  69 + $addon = $input->getOption('addon');
  70 +
  71 + $moduleDir = $addonDir = '';
  72 + if ($addon) {
  73 + $addonInfo = get_addon_info($addon);
  74 + if (!$addonInfo) {
  75 + throw new Exception('addon not found');
  76 + }
  77 + $moduleDir = ADDON_PATH . $addon . DS;
  78 + } else {
  79 + $moduleDir = APP_PATH . $module . DS;
  80 + }
  81 + if (!is_dir($moduleDir)) {
  82 + throw new Exception('module not found');
  83 + }
  84 +
  85 + if (version_compare(PHP_VERSION, '7.0.0', '<')) {
  86 + throw new Exception("Requires PHP version 7.0 or newer");
  87 + }
  88 +
  89 + //控制器名
  90 + $controller = $input->getOption('controller') ?: [];
  91 + if (!$controller) {
  92 + $controllerDir = $moduleDir . Config::get('url_controller_layer') . DS;
  93 + $files = new \RecursiveIteratorIterator(
  94 + new \RecursiveDirectoryIterator($controllerDir),
  95 + \RecursiveIteratorIterator::LEAVES_ONLY
  96 + );
  97 +
  98 + foreach ($files as $name => $file) {
  99 + if (!$file->isDir() && $file->getExtension() == 'php') {
  100 + $filePath = $file->getRealPath();
  101 + $classes[] = $this->get_class_from_file($filePath);
  102 + }
  103 + }
  104 + } else {
  105 + foreach ($controller as $index => $item) {
  106 + $filePath = $moduleDir . Config::get('url_controller_layer') . DS . $item . '.php';
  107 + $classes[] = $this->get_class_from_file($filePath);
  108 + }
  109 + }
  110 +
  111 + $classes = array_unique(array_filter($classes));
  112 +
  113 + $config = [
  114 + 'sitename' => config('site.name'),
  115 + 'title' => $title,
  116 + 'author' => config('site.name'),
  117 + 'description' => '',
  118 + 'apiurl' => $url,
  119 + 'language' => $language,
  120 + ];
  121 +
  122 + $builder = new Builder($classes);
  123 + $content = $builder->render($template_file, ['config' => $config, 'lang' => $lang]);
  124 +
  125 + if (!file_put_contents($output_file, $content)) {
  126 + throw new Exception('Cannot save the content to ' . $output_file);
  127 + }
  128 + $output->info("Build Successed!");
  129 + }
  130 +
  131 + /**
  132 + * get full qualified class name
  133 + *
  134 + * @param string $path_to_file
  135 + * @return string
  136 + * @author JBYRNE http://jarretbyrne.com/2015/06/197/
  137 + */
  138 + protected function get_class_from_file($path_to_file)
  139 + {
  140 + //Grab the contents of the file
  141 + $contents = file_get_contents($path_to_file);
  142 +
  143 + //Start with a blank namespace and class
  144 + $namespace = $class = "";
  145 +
  146 + //Set helper values to know that we have found the namespace/class token and need to collect the string values after them
  147 + $getting_namespace = $getting_class = false;
  148 +
  149 + //Go through each token and evaluate it as necessary
  150 + foreach (token_get_all($contents) as $token) {
  151 +
  152 + //If this token is the namespace declaring, then flag that the next tokens will be the namespace name
  153 + if (is_array($token) && $token[0] == T_NAMESPACE) {
  154 + $getting_namespace = true;
  155 + }
  156 +
  157 + //If this token is the class declaring, then flag that the next tokens will be the class name
  158 + if (is_array($token) && $token[0] == T_CLASS) {
  159 + $getting_class = true;
  160 + }
  161 +
  162 + //While we're grabbing the namespace name...
  163 + if ($getting_namespace === true) {
  164 +
  165 + //If the token is a string or the namespace separator...
  166 + if (is_array($token) && in_array($token[0], [T_STRING, T_NS_SEPARATOR])) {
  167 +
  168 + //Append the token's value to the name of the namespace
  169 + $namespace .= $token[1];
  170 + } elseif ($token === ';') {
  171 +
  172 + //If the token is the semicolon, then we're done with the namespace declaration
  173 + $getting_namespace = false;
  174 + }
  175 + }
  176 +
  177 + //While we're grabbing the class name...
  178 + if ($getting_class === true) {
  179 +
  180 + //If the token is a string, it's the name of the class
  181 + if (is_array($token) && $token[0] == T_STRING) {
  182 +
  183 + //Store the token's value as the class name
  184 + $class = $token[1];
  185 +
  186 + //Got what we need, stope here
  187 + break;
  188 + }
  189 + }
  190 + }
  191 +
  192 + //Build the fully-qualified class name and return it
  193 + return $namespace ? $namespace . '\\' . $class : $class;
  194 + }
  195 +}
  1 +<?php
  2 +
  3 +return [
  4 + 'Info' => '基础信息',
  5 + 'Sandbox' => '在线测试',
  6 + 'Sampleoutput' => '返回示例',
  7 + 'Headers' => 'Headers',
  8 + 'Parameters' => '参数',
  9 + 'Body' => '正文',
  10 + 'Name' => '名称',
  11 + 'Type' => '类型',
  12 + 'Required' => '必选',
  13 + 'Description' => '描述',
  14 + 'Send' => '提交',
  15 + 'Reset' => '重置',
  16 + 'Tokentips' => 'Token在会员注册或登录后都会返回,WEB端同时存在于Cookie中',
  17 + 'Apiurltips' => 'API接口URL',
  18 + 'Savetips' => '点击保存后Token和Api url都将保存在本地Localstorage中',
  19 + 'Authorization' => '权限',
  20 + 'NeedLogin' => '登录',
  21 + 'NeedRight' => '鉴权',
  22 + 'ReturnHeaders' => '响应头',
  23 + 'ReturnParameters' => '返回参数',
  24 + 'Response' => '响应输出',
  25 +];
  1 +<?php
  2 +
  3 +namespace app\admin\command\Api\library;
  4 +
  5 +use think\Config;
  6 +
  7 +/**
  8 + * @website https://github.com/calinrada/php-apidoc
  9 + * @author Calin Rada <rada.calin@gmail.com>
  10 + * @author Karson <karsonzhang@163.com>
  11 + */
  12 +class Builder
  13 +{
  14 +
  15 + /**
  16 + *
  17 + * @var \think\View
  18 + */
  19 + public $view = null;
  20 +
  21 + /**
  22 + * parse classes
  23 + * @var array
  24 + */
  25 + protected $classes = [];
  26 +
  27 + /**
  28 + *
  29 + * @param array $classes
  30 + */
  31 + public function __construct($classes = [])
  32 + {
  33 + $this->classes = array_merge($this->classes, $classes);
  34 + $this->view = new \think\View(Config::get('template'), Config::get('view_replace_str'));
  35 + }
  36 +
  37 + protected function extractAnnotations()
  38 + {
  39 + foreach ($this->classes as $class) {
  40 + $classAnnotation = Extractor::getClassAnnotations($class);
  41 + // 如果忽略
  42 + if (isset($classAnnotation['ApiInternal'])) {
  43 + continue;
  44 + }
  45 + Extractor::getClassMethodAnnotations($class);
  46 + //Extractor::getClassPropertyValues($class);
  47 + }
  48 + $allClassAnnotation = Extractor::getAllClassAnnotations();
  49 + $allClassMethodAnnotation = Extractor::getAllClassMethodAnnotations();
  50 + //$allClassPropertyValue = Extractor::getAllClassPropertyValues();
  51 +
  52 +// foreach ($allClassMethodAnnotation as $className => &$methods) {
  53 +// foreach ($methods as &$method) {
  54 +// //权重判断
  55 +// if ($method && !isset($method['ApiWeigh']) && isset($allClassAnnotation[$className]['ApiWeigh'])) {
  56 +// $method['ApiWeigh'] = $allClassAnnotation[$className]['ApiWeigh'];
  57 +// }
  58 +// }
  59 +// }
  60 +// unset($methods);
  61 + return [$allClassAnnotation, $allClassMethodAnnotation];
  62 + }
  63 +
  64 + protected function generateHeadersTemplate($docs)
  65 + {
  66 + if (!isset($docs['ApiHeaders'])) {
  67 + return [];
  68 + }
  69 +
  70 + $headerslist = array();
  71 + foreach ($docs['ApiHeaders'] as $params) {
  72 + $tr = array(
  73 + 'name' => $params['name'] ?? '',
  74 + 'type' => $params['type'] ?? 'string',
  75 + 'sample' => $params['sample'] ?? '',
  76 + 'required' => $params['required'] ?? false,
  77 + 'description' => $params['description'] ?? '',
  78 + );
  79 + $headerslist[] = $tr;
  80 + }
  81 +
  82 + return $headerslist;
  83 + }
  84 +
  85 + protected function generateParamsTemplate($docs)
  86 + {
  87 + if (!isset($docs['ApiParams'])) {
  88 + return [];
  89 + }
  90 +
  91 + $paramslist = array();
  92 + foreach ($docs['ApiParams'] as $params) {
  93 + $tr = array(
  94 + 'name' => $params['name'],
  95 + 'type' => $params['type'] ?? 'string',
  96 + 'sample' => $params['sample'] ?? '',
  97 + 'required' => $params['required'] ?? true,
  98 + 'description' => $params['description'] ?? '',
  99 + );
  100 + $paramslist[] = $tr;
  101 + }
  102 +
  103 + return $paramslist;
  104 + }
  105 +
  106 + protected function generateReturnHeadersTemplate($docs)
  107 + {
  108 + if (!isset($docs['ApiReturnHeaders'])) {
  109 + return [];
  110 + }
  111 +
  112 + $headerslist = array();
  113 + foreach ($docs['ApiReturnHeaders'] as $params) {
  114 + $tr = array(
  115 + 'name' => $params['name'] ?? '',
  116 + 'type' => 'string',
  117 + 'sample' => $params['sample'] ?? '',
  118 + 'required' => isset($params['required']) && $params['required'] ? 'Yes' : 'No',
  119 + 'description' => $params['description'] ?? '',
  120 + );
  121 + $headerslist[] = $tr;
  122 + }
  123 +
  124 + return $headerslist;
  125 + }
  126 +
  127 + protected function generateReturnParamsTemplate($st_params)
  128 + {
  129 + if (!isset($st_params['ApiReturnParams'])) {
  130 + return [];
  131 + }
  132 +
  133 + $paramslist = array();
  134 + foreach ($st_params['ApiReturnParams'] as $params) {
  135 + $tr = array(
  136 + 'name' => $params['name'] ?? '',
  137 + 'type' => $params['type'] ?? 'string',
  138 + 'sample' => $params['sample'] ?? '',
  139 + 'description' => $params['description'] ?? '',
  140 + );
  141 + $paramslist[] = $tr;
  142 + }
  143 +
  144 + return $paramslist;
  145 + }
  146 +
  147 + protected function generateBadgeForMethod($data)
  148 + {
  149 + $method = strtoupper(is_array($data['ApiMethod'][0]) ? $data['ApiMethod'][0]['data'] : $data['ApiMethod'][0]);
  150 + $labes = array(
  151 + 'POST' => 'label-primary',
  152 + 'GET' => 'label-success',
  153 + 'PUT' => 'label-warning',
  154 + 'DELETE' => 'label-danger',
  155 + 'PATCH' => 'label-default',
  156 + 'OPTIONS' => 'label-info'
  157 + );
  158 +
  159 + return isset($labes[$method]) ? $labes[$method] : $labes['GET'];
  160 + }
  161 +
  162 + public function parse()
  163 + {
  164 + list($allClassAnnotations, $allClassMethodAnnotations) = $this->extractAnnotations();
  165 +
  166 + $sectorArr = [];
  167 + foreach ($allClassAnnotations as $index => &$allClassAnnotation) {
  168 + // 如果设置隐藏,则不显示在文档
  169 + if (isset($allClassAnnotation['ApiInternal'])) {
  170 + continue;
  171 + }
  172 + $sector = isset($allClassAnnotation['ApiSector']) ? $allClassAnnotation['ApiSector'][0] : $allClassAnnotation['ApiTitle'][0];
  173 + $sectorArr[$sector] = isset($allClassAnnotation['ApiWeigh']) ? $allClassAnnotation['ApiWeigh'][0] : 0;
  174 + }
  175 + unset($allClassAnnotation);
  176 +
  177 + arsort($sectorArr);
  178 + $routes = include_once CONF_PATH . 'route.php';
  179 + $subdomain = false;
  180 + if (config('url_domain_deploy') && isset($routes['__domain__']) && isset($routes['__domain__']['api']) && $routes['__domain__']['api']) {
  181 + $subdomain = true;
  182 + }
  183 + $counter = 0;
  184 + $section = null;
  185 + $weigh = 0;
  186 + $docsList = [];
  187 + foreach ($allClassMethodAnnotations as $class => $methods) {
  188 + foreach ($methods as $name => $docs) {
  189 + if (isset($docs['ApiSector'][0])) {
  190 + $section = is_array($docs['ApiSector'][0]) ? $docs['ApiSector'][0]['data'] : $docs['ApiSector'][0];
  191 + } else {
  192 + $section = $class;
  193 + }
  194 + if (0 === count($docs)) {
  195 + continue;
  196 + }
  197 + $route = is_array($docs['ApiRoute'][0]) ? $docs['ApiRoute'][0]['data'] : $docs['ApiRoute'][0];
  198 + if ($subdomain) {
  199 + $route = substr($route, 4);
  200 + }
  201 + $docsList[$section][$name] = [
  202 + 'id' => $counter,
  203 + 'method' => is_array($docs['ApiMethod'][0]) ? $docs['ApiMethod'][0]['data'] : $docs['ApiMethod'][0],
  204 + 'methodLabel' => $this->generateBadgeForMethod($docs),
  205 + 'section' => $section,
  206 + 'route' => $route,
  207 + 'title' => is_array($docs['ApiTitle'][0]) ? $docs['ApiTitle'][0]['data'] : $docs['ApiTitle'][0],
  208 + 'summary' => is_array($docs['ApiSummary'][0]) ? $docs['ApiSummary'][0]['data'] : $docs['ApiSummary'][0],
  209 + 'body' => isset($docs['ApiBody'][0]) ? (is_array($docs['ApiBody'][0]) ? $docs['ApiBody'][0]['data'] : $docs['ApiBody'][0]) : '',
  210 + 'headersList' => $this->generateHeadersTemplate($docs),
  211 + 'paramsList' => $this->generateParamsTemplate($docs),
  212 + 'returnHeadersList' => $this->generateReturnHeadersTemplate($docs),
  213 + 'returnParamsList' => $this->generateReturnParamsTemplate($docs),
  214 + 'weigh' => is_array($docs['ApiWeigh'][0]) ? $docs['ApiWeigh'][0]['data'] : $docs['ApiWeigh'][0],
  215 + 'return' => isset($docs['ApiReturn']) ? (is_array($docs['ApiReturn'][0]) ? $docs['ApiReturn'][0]['data'] : $docs['ApiReturn'][0]) : '',
  216 + 'needLogin' => $docs['ApiPermissionLogin'][0],
  217 + 'needRight' => $docs['ApiPermissionRight'][0],
  218 + ];
  219 + $counter++;
  220 + }
  221 + }
  222 +
  223 + //重建排序
  224 + foreach ($docsList as $index => &$methods) {
  225 + $methodSectorArr = [];
  226 + foreach ($methods as $name => $method) {
  227 + $methodSectorArr[$name] = isset($method['weigh']) ? $method['weigh'] : 0;
  228 + }
  229 + arsort($methodSectorArr);
  230 + $methods = array_merge(array_flip(array_keys($methodSectorArr)), $methods);
  231 + }
  232 + $docsList = array_merge(array_flip(array_keys($sectorArr)), $docsList);
  233 + return $docsList;
  234 + }
  235 +
  236 + public function getView()
  237 + {
  238 + return $this->view;
  239 + }
  240 +
  241 + /**
  242 + * 渲染
  243 + * @param string $template
  244 + * @param array $vars
  245 + * @return string
  246 + */
  247 + public function render($template, $vars = [])
  248 + {
  249 + $docsList = $this->parse();
  250 +
  251 + return $this->view->display(file_get_contents($template), array_merge($vars, ['docsList' => $docsList]));
  252 + }
  253 +}
  1 +<?php
  2 +
  3 +namespace app\admin\command\Api\library;
  4 +
  5 +use Exception;
  6 +
  7 +/**
  8 + * Class imported from https://github.com/eriknyk/Annotations
  9 + * @author Erik Amaru Ortiz https://github.com/eriknyk‎
  10 + *
  11 + * @license http://opensource.org/licenses/bsd-license.php The BSD License
  12 + * @author Calin Rada <rada.calin@gmail.com>
  13 + */
  14 +class Extractor
  15 +{
  16 +
  17 + /**
  18 + * Static array to store already parsed annotations
  19 + * @var array
  20 + */
  21 + private static $annotationCache;
  22 +
  23 + private static $classAnnotationCache;
  24 +
  25 + private static $classMethodAnnotationCache;
  26 +
  27 + private static $classPropertyValueCache;
  28 +
  29 + /**
  30 + * Indicates that annotations should has strict behavior, 'false' by default
  31 + * @var boolean
  32 + */
  33 + private $strict = false;
  34 +
  35 + /**
  36 + * Stores the default namespace for Objects instance, usually used on methods like getMethodAnnotationsObjects()
  37 + * @var string
  38 + */
  39 + public $defaultNamespace = '';
  40 +
  41 + /**
  42 + * Sets strict variable to true/false
  43 + * @param bool $value boolean value to indicate that annotations to has strict behavior
  44 + */
  45 + public function setStrict($value)
  46 + {
  47 + $this->strict = (bool)$value;
  48 + }
  49 +
  50 + /**
  51 + * Sets default namespace to use in object instantiation
  52 + * @param string $namespace default namespace
  53 + */
  54 + public function setDefaultNamespace($namespace)
  55 + {
  56 + $this->defaultNamespace = $namespace;
  57 + }
  58 +
  59 + /**
  60 + * Gets default namespace used in object instantiation
  61 + * @return string $namespace default namespace
  62 + */
  63 + public function getDefaultAnnotationNamespace()
  64 + {
  65 + return $this->defaultNamespace;
  66 + }
  67 +
  68 + /**
  69 + * Gets all anotations with pattern @SomeAnnotation() from a given class
  70 + *
  71 + * @param string $className class name to get annotations
  72 + * @return array self::$classAnnotationCache all annotated elements
  73 + */
  74 + public static function getClassAnnotations($className)
  75 + {
  76 + if (!isset(self::$classAnnotationCache[$className])) {
  77 + $class = new \ReflectionClass($className);
  78 + $annotationArr = self::parseAnnotations($class->getDocComment());
  79 + $annotationArr['ApiTitle'] = !isset($annotationArr['ApiTitle'][0]) || !trim($annotationArr['ApiTitle'][0]) ? [$class->getShortName()] : $annotationArr['ApiTitle'];
  80 + self::$classAnnotationCache[$className] = $annotationArr;
  81 + }
  82 +
  83 + return self::$classAnnotationCache[$className];
  84 + }
  85 +
  86 + /**
  87 + * 获取类所有方法的属性配置
  88 + * @param $className
  89 + * @return mixed
  90 + * @throws \ReflectionException
  91 + */
  92 + public static function getClassMethodAnnotations($className)
  93 + {
  94 + $class = new \ReflectionClass($className);
  95 +
  96 + foreach ($class->getMethods() as $object) {
  97 + self::$classMethodAnnotationCache[$className][$object->name] = self::getMethodAnnotations($className, $object->name);
  98 + }
  99 +
  100 + return self::$classMethodAnnotationCache[$className];
  101 + }
  102 +
  103 + public static function getClassPropertyValues($className)
  104 + {
  105 + $class = new \ReflectionClass($className);
  106 +
  107 + foreach ($class->getProperties() as $object) {
  108 + self::$classPropertyValueCache[$className][$object->name] = self::getClassPropertyValue($className, $object->name);
  109 + }
  110 +
  111 + return self::$classMethodAnnotationCache[$className];
  112 + }
  113 +
  114 + public static function getAllClassAnnotations()
  115 + {
  116 + return self::$classAnnotationCache;
  117 + }
  118 +
  119 + public static function getAllClassMethodAnnotations()
  120 + {
  121 + return self::$classMethodAnnotationCache;
  122 + }
  123 +
  124 + public static function getAllClassPropertyValues()
  125 + {
  126 + return self::$classPropertyValueCache;
  127 + }
  128 +
  129 + public static function getClassPropertyValue($className, $property)
  130 + {
  131 + $_SERVER['REQUEST_METHOD'] = 'GET';
  132 + $reflectionClass = new \ReflectionClass($className);
  133 + $reflectionProperty = $reflectionClass->getProperty($property);
  134 + $reflectionProperty->setAccessible(true);
  135 + return $reflectionProperty->getValue($reflectionClass->newInstanceWithoutConstructor());
  136 + }
  137 +
  138 + /**
  139 + * Gets all anotations with pattern @SomeAnnotation() from a determinated method of a given class
  140 + *
  141 + * @param string $className class name
  142 + * @param string $methodName method name to get annotations
  143 + * @return array self::$annotationCache all annotated elements of a method given
  144 + */
  145 + public static function getMethodAnnotations($className, $methodName)
  146 + {
  147 + if (!isset(self::$annotationCache[$className . '::' . $methodName])) {
  148 + try {
  149 + $method = new \ReflectionMethod($className, $methodName);
  150 + $class = new \ReflectionClass($className);
  151 + if (!$method->isPublic() || $method->isConstructor()) {
  152 + $annotations = array();
  153 + } else {
  154 + $annotations = self::consolidateAnnotations($method, $class);
  155 + }
  156 + } catch (\ReflectionException $e) {
  157 + $annotations = array();
  158 + }
  159 +
  160 + self::$annotationCache[$className . '::' . $methodName] = $annotations;
  161 + }
  162 +
  163 + return self::$annotationCache[$className . '::' . $methodName];
  164 + }
  165 +
  166 + /**
  167 + * Gets all anotations with pattern @SomeAnnotation() from a determinated method of a given class
  168 + * and instance its abcAnnotation class
  169 + *
  170 + * @param string $className class name
  171 + * @param string $methodName method name to get annotations
  172 + * @return array self::$annotationCache all annotated objects of a method given
  173 + */
  174 + public function getMethodAnnotationsObjects($className, $methodName)
  175 + {
  176 + $annotations = $this->getMethodAnnotations($className, $methodName);
  177 + $objects = array();
  178 +
  179 + $i = 0;
  180 +
  181 + foreach ($annotations as $annotationClass => $listParams) {
  182 + $annotationClass = ucfirst($annotationClass);
  183 + $class = $this->defaultNamespace . $annotationClass . 'Annotation';
  184 +
  185 + // verify is the annotation class exists, depending if Annotations::strict is true
  186 + // if not, just skip the annotation instance creation.
  187 + if (!class_exists($class)) {
  188 + if ($this->strict) {
  189 + throw new Exception(sprintf('Runtime Error: Annotation Class Not Found: %s', $class));
  190 + } else {
  191 + // silent skip & continue
  192 + continue;
  193 + }
  194 + }
  195 +
  196 + if (empty($objects[$annotationClass])) {
  197 + $objects[$annotationClass] = new $class();
  198 + }
  199 +
  200 + foreach ($listParams as $params) {
  201 + if (is_array($params)) {
  202 + foreach ($params as $key => $value) {
  203 + $objects[$annotationClass]->set($key, $value);
  204 + }
  205 + } else {
  206 + $objects[$annotationClass]->set($i++, $params);
  207 + }
  208 + }
  209 + }
  210 +
  211 + return $objects;
  212 + }
  213 +
  214 + private static function consolidateAnnotations($method, $class)
  215 + {
  216 + $dockblockClass = $class->getDocComment();
  217 + $docblockMethod = $method->getDocComment();
  218 + $methodName = $method->getName();
  219 +
  220 + $methodAnnotations = self::parseAnnotations($docblockMethod);
  221 + $methodAnnotations['ApiTitle'] = !isset($methodAnnotations['ApiTitle'][0]) || !trim($methodAnnotations['ApiTitle'][0]) ? [$method->getName()] : $methodAnnotations['ApiTitle'];
  222 +
  223 + $classAnnotations = self::parseAnnotations($dockblockClass);
  224 + $classAnnotations['ApiTitle'] = !isset($classAnnotations['ApiTitle'][0]) || !trim($classAnnotations['ApiTitle'][0]) ? [$class->getShortName()] : $classAnnotations['ApiTitle'];
  225 +
  226 + if (isset($methodAnnotations['ApiInternal']) || $methodName == '_initialize' || $methodName == '_empty') {
  227 + return [];
  228 + }
  229 +
  230 + $properties = $class->getDefaultProperties();
  231 + $noNeedLogin = isset($properties['noNeedLogin']) ? (is_array($properties['noNeedLogin']) ? $properties['noNeedLogin'] : [$properties['noNeedLogin']]) : [];
  232 + $noNeedRight = isset($properties['noNeedRight']) ? (is_array($properties['noNeedRight']) ? $properties['noNeedRight'] : [$properties['noNeedRight']]) : [];
  233 +
  234 + preg_match_all("/\*[\s]+(.*)(\\r\\n|\\r|\\n)/U", str_replace('/**', '', $docblockMethod), $methodArr);
  235 + preg_match_all("/\*[\s]+(.*)(\\r\\n|\\r|\\n)/U", str_replace('/**', '', $dockblockClass), $classArr);
  236 +
  237 + if (!isset($methodAnnotations['ApiMethod'])) {
  238 + $methodAnnotations['ApiMethod'] = ['get'];
  239 + }
  240 + if (!isset($methodAnnotations['ApiWeigh'])) {
  241 + $methodAnnotations['ApiWeigh'] = [0];
  242 + }
  243 + if (!isset($methodAnnotations['ApiSummary'])) {
  244 + $methodAnnotations['ApiSummary'] = $methodAnnotations['ApiTitle'];
  245 + }
  246 +
  247 + if ($methodAnnotations) {
  248 + foreach ($classAnnotations as $name => $valueClass) {
  249 + if (count($valueClass) !== 1) {
  250 + continue;
  251 + }
  252 +
  253 + if ($name === 'ApiRoute') {
  254 + if (isset($methodAnnotations[$name])) {
  255 + $methodAnnotations[$name] = [rtrim($valueClass[0], '/') . $methodAnnotations[$name][0]];
  256 + } else {
  257 + $methodAnnotations[$name] = [rtrim($valueClass[0], '/') . '/' . $method->getName()];
  258 + }
  259 + }
  260 +
  261 + if ($name === 'ApiSector') {
  262 + $methodAnnotations[$name] = $valueClass;
  263 + }
  264 + }
  265 + }
  266 + if (!isset($methodAnnotations['ApiRoute'])) {
  267 + $urlArr = [];
  268 + $className = $class->getName();
  269 +
  270 + list($prefix, $suffix) = explode('\\' . \think\Config::get('url_controller_layer') . '\\', $className);
  271 + $prefixArr = explode('\\', $prefix);
  272 + $suffixArr = explode('\\', $suffix);
  273 + if ($prefixArr[0] == \think\Config::get('app_namespace')) {
  274 + $prefixArr[0] = '';
  275 + }
  276 + $urlArr = array_merge($urlArr, $prefixArr);
  277 + $urlArr[] = implode('.', array_map(function ($item) {
  278 + return \think\Loader::parseName($item);
  279 + }, $suffixArr));
  280 + $urlArr[] = $method->getName();
  281 +
  282 + $methodAnnotations['ApiRoute'] = [implode('/', $urlArr)];
  283 + }
  284 + if (!isset($methodAnnotations['ApiSector'])) {
  285 + $methodAnnotations['ApiSector'] = isset($classAnnotations['ApiSector']) ? $classAnnotations['ApiSector'] : $classAnnotations['ApiTitle'];
  286 + }
  287 + if (!isset($methodAnnotations['ApiParams'])) {
  288 + $params = self::parseCustomAnnotations($docblockMethod, 'param');
  289 + foreach ($params as $k => $v) {
  290 + $arr = explode(' ', preg_replace("/[\s]+/", " ", $v));
  291 + $methodAnnotations['ApiParams'][] = [
  292 + 'name' => isset($arr[1]) ? str_replace('$', '', $arr[1]) : '',
  293 + 'nullable' => false,
  294 + 'type' => isset($arr[0]) ? $arr[0] : 'string',
  295 + 'description' => isset($arr[2]) ? $arr[2] : ''
  296 + ];
  297 + }
  298 + }
  299 + $methodAnnotations['ApiPermissionLogin'] = [!in_array('*', $noNeedLogin) && !in_array($methodName, $noNeedLogin)];
  300 + $methodAnnotations['ApiPermissionRight'] = !$methodAnnotations['ApiPermissionLogin'][0] ? [false] : [!in_array('*', $noNeedRight) && !in_array($methodName, $noNeedRight)];
  301 + return $methodAnnotations;
  302 + }
  303 +
  304 + /**
  305 + * Parse annotations
  306 + *
  307 + * @param string $docblock
  308 + * @param string $name
  309 + * @return array parsed annotations params
  310 + */
  311 + private static function parseCustomAnnotations($docblock, $name = 'param')
  312 + {
  313 + $annotations = array();
  314 +
  315 + $docblock = substr($docblock, 3, -2);
  316 + if (preg_match_all('/@' . $name . '(?:\s*(?:\(\s*)?(.*?)(?:\s*\))?)??\s*(?:\n|\*\/)/', $docblock, $matches)) {
  317 + foreach ($matches[1] as $k => $v) {
  318 + $annotations[] = $v;
  319 + }
  320 + }
  321 + return $annotations;
  322 + }
  323 +
  324 + /**
  325 + * Parse annotations
  326 + *
  327 + * @param string $docblock
  328 + * @return array parsed annotations params
  329 + */
  330 + private static function parseAnnotations($docblock)
  331 + {
  332 + $annotations = array();
  333 +
  334 + // Strip away the docblock header and footer to ease parsing of one line annotations
  335 + $docblock = substr($docblock, 3, -2);
  336 + if (preg_match_all('/@(?<name>[A-Za-z_-]+)[\s\t]*\((?<args>(?:(?!\)).)*)\)\r?/s', $docblock, $matches)) {
  337 + $numMatches = count($matches[0]);
  338 + for ($i = 0; $i < $numMatches; ++$i) {
  339 + $name = $matches['name'][$i];
  340 + $value = '';
  341 + // annotations has arguments
  342 + if (isset($matches['args'][$i])) {
  343 + $argsParts = trim($matches['args'][$i]);
  344 + if ($name == 'ApiReturn') {
  345 + $value = $argsParts;
  346 + } elseif ($matches['args'][$i] != '') {
  347 + $argsParts = preg_replace("/\{(\w+)\}/", '#$1#', $argsParts);
  348 + $value = self::parseArgs($argsParts);
  349 + if (is_string($value)) {
  350 + $value = preg_replace("/\#(\w+)\#/", '{$1}', $argsParts);
  351 + }
  352 + }
  353 + }
  354 +
  355 + $annotations[$name][] = $value;
  356 + }
  357 + }
  358 + if (stripos($docblock, '@ApiInternal') !== false) {
  359 + $annotations['ApiInternal'] = [true];
  360 + }
  361 + if (!isset($annotations['ApiTitle'])) {
  362 + preg_match_all("/\*[\s]+(.*)(\\r\\n|\\r|\\n)/U", str_replace('/**', '', $docblock), $matchArr);
  363 + $title = isset($matchArr[1]) && isset($matchArr[1][0]) ? $matchArr[1][0] : '';
  364 + $annotations['ApiTitle'] = [$title];
  365 + }
  366 +
  367 + return $annotations;
  368 + }
  369 +
  370 + /**
  371 + * Parse individual annotation arguments
  372 + *
  373 + * @param string $content arguments string
  374 + * @return array annotated arguments
  375 + */
  376 + private static function parseArgs($content)
  377 + {
  378 + // Replace initial stars
  379 + $content = preg_replace('/^\s*\*/m', '', $content);
  380 +
  381 + $data = array();
  382 + $len = strlen($content);
  383 + $i = 0;
  384 + $var = '';
  385 + $val = '';
  386 + $level = 1;
  387 +
  388 + $prevDelimiter = '';
  389 + $nextDelimiter = '';
  390 + $nextToken = '';
  391 + $composing = false;
  392 + $type = 'plain';
  393 + $delimiter = null;
  394 + $quoted = false;
  395 + $tokens = array('"', '"', '{', '}', ',', '=');
  396 +
  397 + while ($i <= $len) {
  398 + $prev_c = substr($content, $i - 1, 1);
  399 + $c = substr($content, $i++, 1);
  400 +
  401 + if ($c === '"' && $prev_c !== "\\") {
  402 + $delimiter = $c;
  403 + //open delimiter
  404 + if (!$composing && empty($prevDelimiter) && empty($nextDelimiter)) {
  405 + $prevDelimiter = $nextDelimiter = $delimiter;
  406 + $val = '';
  407 + $composing = true;
  408 + $quoted = true;
  409 + } else {
  410 + // close delimiter
  411 + if ($c !== $nextDelimiter) {
  412 + throw new Exception(sprintf(
  413 + "Parse Error: enclosing error -> expected: [%s], given: [%s]",
  414 + $nextDelimiter,
  415 + $c
  416 + ));
  417 + }
  418 +
  419 + // validating syntax
  420 + if ($i < $len) {
  421 + if (',' !== substr($content, $i, 1) && '\\' !== $prev_c) {
  422 + throw new Exception(sprintf(
  423 + "Parse Error: missing comma separator near: ...%s<--",
  424 + substr($content, ($i - 10), $i)
  425 + ));
  426 + }
  427 + }
  428 +
  429 + $prevDelimiter = $nextDelimiter = '';
  430 + $composing = false;
  431 + $delimiter = null;
  432 + }
  433 + } elseif (!$composing && in_array($c, $tokens)) {
  434 + switch ($c) {
  435 + case '=':
  436 + $prevDelimiter = $nextDelimiter = '';
  437 + $level = 2;
  438 + $composing = false;
  439 + $type = 'assoc';
  440 + $quoted = false;
  441 + break;
  442 + case ',':
  443 + $level = 3;
  444 +
  445 + // If composing flag is true yet,
  446 + // it means that the string was not enclosed, so it is parsing error.
  447 + if ($composing === true && !empty($prevDelimiter) && !empty($nextDelimiter)) {
  448 + throw new Exception(sprintf(
  449 + "Parse Error: enclosing error -> expected: [%s], given: [%s]",
  450 + $nextDelimiter,
  451 + $c
  452 + ));
  453 + }
  454 +
  455 + $prevDelimiter = $nextDelimiter = '';
  456 + break;
  457 + case '{':
  458 + $subc = '';
  459 + $subComposing = true;
  460 +
  461 + while ($i <= $len) {
  462 + $c = substr($content, $i++, 1);
  463 +
  464 + if (isset($delimiter) && $c === $delimiter) {
  465 + throw new Exception(sprintf(
  466 + "Parse Error: Composite variable is not enclosed correctly."
  467 + ));
  468 + }
  469 +
  470 + if ($c === '}') {
  471 + $subComposing = false;
  472 + break;
  473 + }
  474 + $subc .= $c;
  475 + }
  476 +
  477 + // if the string is composing yet means that the structure of var. never was enclosed with '}'
  478 + if ($subComposing) {
  479 + throw new Exception(sprintf(
  480 + "Parse Error: Composite variable is not enclosed correctly. near: ...%s'",
  481 + $subc
  482 + ));
  483 + }
  484 +
  485 + $val = self::parseArgs($subc);
  486 + break;
  487 + }
  488 + } else {
  489 + if ($level == 1) {
  490 + $var .= $c;
  491 + } elseif ($level == 2) {
  492 + $val .= $c;
  493 + }
  494 + }
  495 +
  496 + if ($level === 3 || $i === $len) {
  497 + if ($type == 'plain' && $i === $len) {
  498 + $data = self::castValue($var);
  499 + } else {
  500 + $data[trim($var)] = self::castValue($val, !$quoted);
  501 + }
  502 +
  503 + $level = 1;
  504 + $var = $val = '';
  505 + $composing = false;
  506 + $quoted = false;
  507 + }
  508 + }
  509 +
  510 + return $data;
  511 + }
  512 +
  513 + /**
  514 + * Try determinate the original type variable of a string
  515 + *
  516 + * @param string $val string containing possibles variables that can be cast to bool or int
  517 + * @param boolean $trim indicate if the value passed should be trimmed after to try cast
  518 + * @return mixed returns the value converted to original type if was possible
  519 + */
  520 + private static function castValue($val, $trim = false)
  521 + {
  522 + if (is_array($val)) {
  523 + foreach ($val as $key => $value) {
  524 + $val[$key] = self::castValue($value);
  525 + }
  526 + } elseif (is_string($val)) {
  527 + if ($trim) {
  528 + $val = trim($val);
  529 + }
  530 + $val = stripslashes($val);
  531 + $tmp = strtolower($val);
  532 +
  533 + if ($tmp === 'false' || $tmp === 'true') {
  534 + $val = $tmp === 'true';
  535 + } elseif (is_numeric($val)) {
  536 + return $val + 0;
  537 + }
  538 +
  539 + unset($tmp);
  540 + }
  541 +
  542 + return $val;
  543 + }
  544 +}
  1 +<!DOCTYPE html>
  2 +<html lang="{$config.language}">
  3 + <head>
  4 + <meta charset="utf-8">
  5 + <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6 + <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7 + <meta name="description" content="">
  8 + <title>{$config.title}</title>
  9 +
  10 + <!-- Bootstrap Core CSS -->
  11 + <link href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
  12 +
  13 + <!-- Plugin CSS -->
  14 + <link href="https://cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
  15 +
  16 + <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
  17 + <!--[if lt IE 9]>
  18 + <script src="https://cdn.staticfile.org/html5shiv/3.7.3/html5shiv.min.js"></script>
  19 + <script src="https://cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script>
  20 + <![endif]-->
  21 +
  22 + <style type="text/css">
  23 + body {
  24 + padding-top: 70px; margin-bottom: 15px;
  25 + -webkit-font-smoothing: antialiased;
  26 + -moz-osx-font-smoothing: grayscale;
  27 + font-family: "Roboto", "SF Pro SC", "SF Pro Display", "SF Pro Icons", "PingFang SC", BlinkMacSystemFont, -apple-system, "Segoe UI", "Microsoft Yahei", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
  28 + font-weight: 400;
  29 + }
  30 + h2 { font-size: 1.2em; }
  31 + hr { margin-top: 10px; }
  32 + .tab-pane { padding-top: 10px; }
  33 + .mt0 { margin-top: 0px; }
  34 + .footer { font-size: 12px; color: #666; }
  35 + .docs-list .label { display: inline-block; min-width: 65px; padding: 0.3em 0.6em 0.3em; }
  36 + .string { color: green; }
  37 + .number { color: darkorange; }
  38 + .boolean { color: blue; }
  39 + .null { color: magenta; }
  40 + .key { color: red; }
  41 + .popover { max-width: 400px; max-height: 400px; overflow-y: auto;}
  42 + .list-group.panel > .list-group-item {
  43 + }
  44 + .list-group-item:last-child {
  45 + border-radius:0;
  46 + }
  47 + h4.panel-title a {
  48 + font-weight:normal;
  49 + font-size:14px;
  50 + }
  51 + h4.panel-title a .text-muted {
  52 + font-size:12px;
  53 + font-weight:normal;
  54 + font-family: 'Verdana';
  55 + }
  56 + #sidebar {
  57 + width: 220px;
  58 + position: fixed;
  59 + margin-left: -240px;
  60 + overflow-y:auto;
  61 + }
  62 + #sidebar > .list-group {
  63 + margin-bottom:0;
  64 + }
  65 + #sidebar > .list-group > a{
  66 + text-indent:0;
  67 + }
  68 + #sidebar .child > a .tag{
  69 + position: absolute;
  70 + right: 10px;
  71 + top: 11px;
  72 + }
  73 + #sidebar .child > a .pull-right{
  74 + margin-left:3px;
  75 + }
  76 + #sidebar .child {
  77 + border:1px solid #ddd;
  78 + border-bottom:none;
  79 + }
  80 + #sidebar .child:last-child {
  81 + border-bottom:1px solid #ddd;
  82 + }
  83 + #sidebar .child > a {
  84 + border:0;
  85 + min-height: 40px;
  86 + }
  87 + #sidebar .list-group a.current {
  88 + background:#f5f5f5;
  89 + }
  90 + @media (max-width: 1620px){
  91 + #sidebar {
  92 + margin:0;
  93 + }
  94 + #accordion {
  95 + padding-left:235px;
  96 + }
  97 + }
  98 + @media (max-width: 768px){
  99 + #sidebar {
  100 + display: none;
  101 + }
  102 + #accordion {
  103 + padding-left:0px;
  104 + }
  105 + }
  106 + .label-primary {
  107 + background-color: #248aff;
  108 + }
  109 + .docs-list .panel .panel-body .table {
  110 + margin-bottom: 0;
  111 + }
  112 +
  113 + </style>
  114 + </head>
  115 + <body>
  116 + <!-- Fixed navbar -->
  117 + <div class="navbar navbar-default navbar-fixed-top" role="navigation">
  118 + <div class="container">
  119 + <div class="navbar-header">
  120 + <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
  121 + <span class="sr-only">Toggle navigation</span>
  122 + <span class="icon-bar"></span>
  123 + <span class="icon-bar"></span>
  124 + <span class="icon-bar"></span>
  125 + </button>
  126 + <a class="navbar-brand" href="./" target="_blank">{$config.title}</a>
  127 + </div>
  128 + <div class="navbar-collapse collapse">
  129 + <form class="navbar-form navbar-right">
  130 + <div class="form-group">
  131 + Token:
  132 + </div>
  133 + <div class="form-group">
  134 + <input type="text" class="form-control input-sm" data-toggle="tooltip" title="{$lang.Tokentips}" placeholder="token" id="token" />
  135 + </div>
  136 + <div class="form-group">
  137 + Apiurl:
  138 + </div>
  139 + <div class="form-group">
  140 + <input id="apiUrl" type="text" class="form-control input-sm" data-toggle="tooltip" title="{$lang.Apiurltips}" placeholder="https://api.mydomain.com" value="{$config.apiurl}" />
  141 + </div>
  142 + <div class="form-group">
  143 + <button type="button" class="btn btn-success btn-sm" data-toggle="tooltip" title="{$lang.Savetips}" id="save_data">
  144 + <span class="glyphicon glyphicon-floppy-disk" aria-hidden="true"></span>
  145 + </button>
  146 + </div>
  147 + </form>
  148 + </div><!--/.nav-collapse -->
  149 + </div>
  150 + </div>
  151 +
  152 + <div class="container">
  153 + <!-- menu -->
  154 + <div id="sidebar">
  155 + <div class="list-group panel">
  156 + {foreach name="docsList" id="docs"}
  157 + <a href="#{$key}" class="list-group-item" data-toggle="collapse" data-parent="#sidebar">{$key} <i class="fa fa-caret-down"></i></a>
  158 + <div class="child collapse" id="{$key}">
  159 + {foreach name="docs" id="api" }
  160 + <a href="javascript:;" data-id="{$api.id}" class="list-group-item">{$api.title}
  161 + <span class="tag">
  162 + {if $api.needRight}
  163 + <span class="label label-danger pull-right"></span>
  164 + {/if}
  165 + {if $api.needLogin}
  166 + <span class="label label-success pull-right noneedlogin"></span>
  167 + {/if}
  168 + </span>
  169 + </a>
  170 + {/foreach}
  171 + </div>
  172 + {/foreach}
  173 + </div>
  174 + </div>
  175 + <div class="panel-group docs-list" id="accordion">
  176 + {foreach name="docsList" id="docs"}
  177 + <h2>{$key}</h2>
  178 + <hr>
  179 + {foreach name="docs" id="api" }
  180 + <div class="panel panel-default">
  181 + <div class="panel-heading" id="heading-{$api.id}">
  182 + <h4 class="panel-title">
  183 + <span class="label {$api.methodLabel}">{$api.method|strtoupper}</span>
  184 + <a data-toggle="collapse" data-parent="#accordion{$api.id}" href="#collapseOne{$api.id}"> {$api.title} <span class="text-muted">{$api.route}</span></a>
  185 + </h4>
  186 + </div>
  187 + <div id="collapseOne{$api.id}" class="panel-collapse collapse">
  188 + <div class="panel-body">
  189 +
  190 + <!-- Nav tabs -->
  191 + <ul class="nav nav-tabs" id="doctab{$api.id}">
  192 + <li class="active"><a href="#info{$api.id}" data-toggle="tab">{$lang.Info}</a></li>
  193 + <li><a href="#sandbox{$api.id}" data-toggle="tab">{$lang.Sandbox}</a></li>
  194 + <li><a href="#sample{$api.id}" data-toggle="tab">{$lang.Sampleoutput}</a></li>
  195 + </ul>
  196 +
  197 + <!-- Tab panes -->
  198 + <div class="tab-content">
  199 +
  200 + <div class="tab-pane active" id="info{$api.id}">
  201 + <div class="well">
  202 + {$api.summary}
  203 + </div>
  204 + <div class="panel panel-default">
  205 + <div class="panel-heading"><strong>{$lang.Authorization}</strong></div>
  206 + <div class="panel-body">
  207 + <table class="table table-hover">
  208 + <tbody>
  209 + <tr>
  210 + <td>{$lang.NeedLogin}</td>
  211 + <td>{$api.needLogin?'是':'否'}</td>
  212 + </tr>
  213 + <tr>
  214 + <td>{$lang.NeedRight}</td>
  215 + <td>{$api.needRight?'是':'否'}</td>
  216 + </tr>
  217 + </tbody>
  218 + </table>
  219 + </div>
  220 + </div>
  221 + <div class="panel panel-default">
  222 + <div class="panel-heading"><strong>{$lang.Headers}</strong></div>
  223 + <div class="panel-body">
  224 + {if $api.headersList}
  225 + <table class="table table-hover">
  226 + <thead>
  227 + <tr>
  228 + <th>{$lang.Name}</th>
  229 + <th>{$lang.Type}</th>
  230 + <th>{$lang.Required}</th>
  231 + <th>{$lang.Description}</th>
  232 + </tr>
  233 + </thead>
  234 + <tbody>
  235 + {foreach name="api['headersList']" id="header"}
  236 + <tr>
  237 + <td>{$header.name}</td>
  238 + <td>{$header.type}</td>
  239 + <td>{$header.required?'是':'否'}</td>
  240 + <td>{$header.description}</td>
  241 + </tr>
  242 + {/foreach}
  243 + </tbody>
  244 + </table>
  245 + {else /}
  246 +
  247 + {/if}
  248 + </div>
  249 + </div>
  250 + <div class="panel panel-default">
  251 + <div class="panel-heading"><strong>{$lang.Parameters}</strong></div>
  252 + <div class="panel-body">
  253 + {if $api.paramsList}
  254 + <table class="table table-hover">
  255 + <thead>
  256 + <tr>
  257 + <th>{$lang.Name}</th>
  258 + <th>{$lang.Type}</th>
  259 + <th>{$lang.Required}</th>
  260 + <th>{$lang.Description}</th>
  261 + </tr>
  262 + </thead>
  263 + <tbody>
  264 + {foreach name="api['paramsList']" id="param"}
  265 + <tr>
  266 + <td>{$param.name}</td>
  267 + <td>{$param.type}</td>
  268 + <td>{:$param.required?'是':'否'}</td>
  269 + <td>{$param.description}</td>
  270 + </tr>
  271 + {/foreach}
  272 + </tbody>
  273 + </table>
  274 + {else /}
  275 +
  276 + {/if}
  277 + </div>
  278 + </div>
  279 + <div class="panel panel-default">
  280 + <div class="panel-heading"><strong>{$lang.Body}</strong></div>
  281 + <div class="panel-body">
  282 + {$api.body|default='无'}
  283 + </div>
  284 + </div>
  285 + </div><!-- #info -->
  286 +
  287 + <div class="tab-pane" id="sandbox{$api.id}">
  288 + <div class="row">
  289 + <div class="col-md-12">
  290 + {if $api.headersList}
  291 + <div class="panel panel-default">
  292 + <div class="panel-heading"><strong>{$lang.Headers}</strong></div>
  293 + <div class="panel-body">
  294 + <div class="headers">
  295 + {foreach name="api['headersList']" id="param"}
  296 + <div class="form-group">
  297 + <label class="control-label" for="{$param.name}">{$param.name}</label>
  298 + <input type="{$param.type}" class="form-control input-sm" id="{$param.name}" {if $param.required}required{/if} placeholder="{$param.description} - Ex: {$param.sample}" name="{$param.name}">
  299 + </div>
  300 + {/foreach}
  301 + </div>
  302 + </div>
  303 + </div>
  304 + {/if}
  305 + <div class="panel panel-default">
  306 + <div class="panel-heading"><strong>{$lang.Parameters}</strong>
  307 + <div class="pull-right">
  308 + <a href="javascript:" class="btn btn-xs btn-info btn-append">追加</a>
  309 + </div>
  310 + </div>
  311 + <div class="panel-body">
  312 + <form enctype="application/x-www-form-urlencoded" role="form" action="{$api.route}" method="{$api.method}" name="form{$api.id}" id="form{$api.id}">
  313 + {if $api.paramsList}
  314 + {foreach name="api['paramsList']" id="param"}
  315 + <div class="form-group">
  316 + <label class="control-label" for="{$param.name}">{$param.name}</label>
  317 + <input type="{$param.type}" class="form-control input-sm" id="{$param.name}" {if $param.required}required{/if} placeholder="{$param.description}{if $param.sample} - 例: {$param.sample}{/if}" name="{$param.name}">
  318 + </div>
  319 + {/foreach}
  320 + {else /}
  321 + <div class="form-group">
  322 +
  323 + </div>
  324 + {/if}
  325 + <div class="form-group form-group-submit">
  326 + <button type="submit" class="btn btn-success send" rel="{$api.id}">{$lang.Send}</button>
  327 + <button type="reset" class="btn btn-info" rel="{$api.id}">{$lang.Reset}</button>
  328 + </div>
  329 + </form>
  330 + </div>
  331 + </div>
  332 + <div class="panel panel-default">
  333 + <div class="panel-heading"><strong>{$lang.Response}</strong></div>
  334 + <div class="panel-body">
  335 + <div class="row">
  336 + <div class="col-md-12" style="overflow-x:auto">
  337 + <pre id="response_headers{$api.id}"></pre>
  338 + <pre id="response{$api.id}"></pre>
  339 + </div>
  340 + </div>
  341 + </div>
  342 + </div>
  343 + <div class="panel panel-default">
  344 + <div class="panel-heading"><strong>{$lang.ReturnParameters}</strong></div>
  345 + <div class="panel-body">
  346 + {if $api.returnParamsList}
  347 + <table class="table table-hover">
  348 + <thead>
  349 + <tr>
  350 + <th>{$lang.Name}</th>
  351 + <th>{$lang.Type}</th>
  352 + <th>{$lang.Description}</th>
  353 + </tr>
  354 + </thead>
  355 + <tbody>
  356 + {foreach name="api['returnParamsList']" id="param"}
  357 + <tr>
  358 + <td>{$param.name}</td>
  359 + <td>{$param.type}</td>
  360 + <td>{$param.description}</td>
  361 + </tr>
  362 + {/foreach}
  363 + </tbody>
  364 + </table>
  365 + {else /}
  366 +
  367 + {/if}
  368 + </div>
  369 + </div>
  370 + </div>
  371 + </div>
  372 + </div><!-- #sandbox -->
  373 +
  374 + <div class="tab-pane" id="sample{$api.id}">
  375 + <div class="row">
  376 + <div class="col-md-12">
  377 + <pre id="sample_response{$api.id}">{$api.return|default='无'}</pre>
  378 + </div>
  379 + </div>
  380 + </div><!-- #sample -->
  381 +
  382 + </div><!-- .tab-content -->
  383 + </div>
  384 + </div>
  385 + </div>
  386 + {/foreach}
  387 + {/foreach}
  388 + </div>
  389 +
  390 + <hr>
  391 +
  392 + <div class="row mt0 footer">
  393 + <div class="col-md-6" align="left">
  394 +
  395 + </div>
  396 + <div class="col-md-6" align="right">
  397 + Generated on {:date('Y-m-d H:i:s')} <a href="./" target="_blank">{$config.sitename}</a>
  398 + </div>
  399 + </div>
  400 +
  401 + </div> <!-- /container -->
  402 +
  403 + <!-- jQuery -->
  404 + <script src="https://cdn.staticfile.org/jquery/2.1.4/jquery.min.js"></script>
  405 +
  406 + <!-- Bootstrap Core JavaScript -->
  407 + <script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
  408 +
  409 + <script type="text/javascript">
  410 + function syntaxHighlight(json) {
  411 + if (typeof json != 'string') {
  412 + json = JSON.stringify(json, undefined, 2);
  413 + }
  414 + json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  415 + return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
  416 + var cls = 'number';
  417 + if (/^"/.test(match)) {
  418 + if (/:$/.test(match)) {
  419 + cls = 'key';
  420 + } else {
  421 + cls = 'string';
  422 + }
  423 + } else if (/true|false/.test(match)) {
  424 + cls = 'boolean';
  425 + } else if (/null/.test(match)) {
  426 + cls = 'null';
  427 + }
  428 + return '<span class="' + cls + '">' + match + '</span>';
  429 + });
  430 + }
  431 +
  432 + function prepareStr(str) {
  433 + try {
  434 + return syntaxHighlight(JSON.stringify(JSON.parse(str.replace(/'/g, '"')), null, 2));
  435 + } catch (e) {
  436 + return str;
  437 + }
  438 + }
  439 + var storage = (function () {
  440 + var uid = new Date;
  441 + var storage;
  442 + var result;
  443 + try {
  444 + (storage = window.localStorage).setItem(uid, uid);
  445 + result = storage.getItem(uid) == uid;
  446 + storage.removeItem(uid);
  447 + return result && storage;
  448 + } catch (exception) {
  449 + }
  450 + }());
  451 +
  452 + $.fn.serializeObject = function ()
  453 + {
  454 + var o = {};
  455 + var a = this.serializeArray();
  456 + $.each(a, function () {
  457 + if (!this.value) {
  458 + return;
  459 + }
  460 + if (o[this.name] !== undefined) {
  461 + if (!o[this.name].push) {
  462 + o[this.name] = [o[this.name]];
  463 + }
  464 + o[this.name].push(this.value || '');
  465 + } else {
  466 + o[this.name] = this.value || '';
  467 + }
  468 + });
  469 + return o;
  470 + };
  471 +
  472 + $(document).ready(function () {
  473 +
  474 + if (storage) {
  475 + storage.getItem('token') && $('#token').val(storage.getItem('token'));
  476 + storage.getItem('apiUrl') && $('#apiUrl').val(storage.getItem('apiUrl'));
  477 + }
  478 +
  479 + $('[data-toggle="tooltip"]').tooltip({
  480 + placement: 'bottom'
  481 + });
  482 +
  483 + $(window).on("resize", function(){
  484 + $("#sidebar").css("max-height", $(window).height()-80);
  485 + });
  486 +
  487 + $(window).trigger("resize");
  488 +
  489 + $(document).on("click", "#sidebar .list-group > .list-group-item", function(){
  490 + $("#sidebar .list-group > .list-group-item").removeClass("current");
  491 + $(this).addClass("current");
  492 + });
  493 + $(document).on("click", "#sidebar .child a", function(){
  494 + var heading = $("#heading-"+$(this).data("id"));
  495 + if(!heading.next().hasClass("in")){
  496 + $("a", heading).trigger("click");
  497 + }
  498 + $("html,body").animate({scrollTop:heading.offset().top-70});
  499 + });
  500 +
  501 + $('code[id^=response]').hide();
  502 +
  503 + $.each($('pre[id^=sample_response],pre[id^=sample_post_body]'), function () {
  504 + if ($(this).html() == 'NA') {
  505 + return;
  506 + }
  507 + var str = prepareStr($(this).html());
  508 + $(this).html(str);
  509 + });
  510 +
  511 + $("[data-toggle=popover]").popover({placement: 'right'});
  512 +
  513 + $('[data-toggle=popover]').on('shown.bs.popover', function () {
  514 + var $sample = $(this).parent().find(".popover-content"),
  515 + str = $(this).data('content');
  516 + if (typeof str == "undefined" || str === "") {
  517 + return;
  518 + }
  519 + var str = prepareStr(str);
  520 + $sample.html('<pre>' + str + '</pre>');
  521 + });
  522 +
  523 + $(document).on('click', '#save_data', function (e) {
  524 + if (storage) {
  525 + storage.setItem('token', $('#token').val());
  526 + storage.setItem('apiUrl', $('#apiUrl').val());
  527 + } else {
  528 + alert('Your browser does not support local storage');
  529 + }
  530 + });
  531 + $(document).on('click', '.btn-append', function (e) {
  532 + $($("#appendtpl").html()).insertBefore($(this).closest(".panel").find(".form-group-submit"));
  533 + return false;
  534 + });
  535 + $(document).on('click', '.btn-remove', function (e) {
  536 + $(this).closest(".form-group").remove();
  537 + return false;
  538 + });
  539 + $(document).on('keyup', '.input-custom-name', function (e) {
  540 + $(this).closest(".row").find(".input-custom-value").attr("name", $(this).val());
  541 + return false;
  542 + });
  543 +
  544 + $(document).on('click', '.send', function (e) {
  545 + e.preventDefault();
  546 + var form = $(this).closest('form');
  547 + //added /g to get all the matched params instead of only first
  548 + var matchedParamsInRoute = $(form).attr('action').match(/[^{]+(?=\})/g);
  549 + var theId = $(this).attr('rel');
  550 + //keep a copy of action attribute in order to modify the copy
  551 + //instead of the initial attribute
  552 + var url = $(form).attr('action');
  553 + var method = $(form).prop('method').toLowerCase() || 'get';
  554 +
  555 + var formData = new FormData();
  556 +
  557 + $(form).find('input').each(function (i, input) {
  558 + if ($(input).attr('type').toLowerCase() == 'file') {
  559 + formData.append($(input).attr('name'), $(input)[0].files[0]);
  560 + method = 'post';
  561 + } else {
  562 + formData.append($(input).attr('name'), $(input).val())
  563 + }
  564 + });
  565 +
  566 + var index, key, value;
  567 +
  568 + if (matchedParamsInRoute) {
  569 + var params = {};
  570 + formData.forEach(function(value, key){
  571 + params[key] = value;
  572 + });
  573 + for (index = 0; index < matchedParamsInRoute.length; ++index) {
  574 + try {
  575 + key = matchedParamsInRoute[index];
  576 + value = params[key];
  577 + if (typeof value == "undefined")
  578 + value = "";
  579 + url = url.replace("\{" + key + "\}", value);
  580 + formData.delete(key);
  581 + } catch (err) {
  582 + console.log(err);
  583 + }
  584 + }
  585 + }
  586 +
  587 + var headers = {};
  588 +
  589 + var token = $('#token').val();
  590 + if (token.length > 0) {
  591 + headers['token'] = token;
  592 + }
  593 +
  594 + $("#sandbox" + theId + " .headers input[type=text]").each(function () {
  595 + val = $(this).val();
  596 + if (val.length > 0) {
  597 + headers[$(this).prop('name')] = val;
  598 + }
  599 + });
  600 +
  601 + $.ajax({
  602 + url: $('#apiUrl').val() + url,
  603 + data: method == 'get' ? $(form).serialize() : formData,
  604 + type: method,
  605 + dataType: 'json',
  606 + contentType: false,
  607 + processData: false,
  608 + headers: headers,
  609 + xhrFields: {
  610 + withCredentials: true
  611 + },
  612 + success: function (data, textStatus, xhr) {
  613 + if (typeof data === 'object') {
  614 + var str = JSON.stringify(data, null, 2);
  615 + $('#response' + theId).html(syntaxHighlight(str));
  616 + } else {
  617 + $('#response' + theId).html(data || '');
  618 + }
  619 + $('#response_headers' + theId).html('HTTP ' + xhr.status + ' ' + xhr.statusText + '<br/><br/>' + xhr.getAllResponseHeaders());
  620 + $('#response' + theId).show();
  621 + },
  622 + error: function (xhr, textStatus, error) {
  623 + try {
  624 + var str = JSON.stringify($.parseJSON(xhr.responseText), null, 2);
  625 + } catch (e) {
  626 + var str = xhr.responseText;
  627 + }
  628 + $('#response_headers' + theId).html('HTTP ' + xhr.status + ' ' + xhr.statusText + '<br/><br/>' + xhr.getAllResponseHeaders());
  629 + $('#response' + theId).html(syntaxHighlight(str));
  630 + $('#response' + theId).show();
  631 + }
  632 + });
  633 + return false;
  634 + });
  635 + });
  636 + </script>
  637 + <script type="text/html" id="appendtpl">
  638 + <div class="form-group">
  639 + <label class="control-label">自定义</label>
  640 + <div class="row">
  641 + <div class="col-xs-4">
  642 + <input type="text" class="form-control input-sm input-custom-name" placeholder="名称">
  643 + </div>
  644 + <div class="col-xs-6">
  645 + <input type="text" class="form-control input-sm input-custom-value" placeholder="值">
  646 + </div>
  647 + <div class="col-xs-2 text-center">
  648 + <a href="javascript:" class="btn btn-sm btn-danger btn-remove">删除</a>
  649 + </div>
  650 + </div>
  651 + </div>
  652 + </script>
  653 + </body>
  654 +</html>
  1 +<?php
  2 +
  3 +namespace app\admin\command;
  4 +
  5 +use fast\Form;
  6 +use think\Config;
  7 +use think\console\Command;
  8 +use think\console\Input;
  9 +use think\console\input\Option;
  10 +use think\console\Output;
  11 +use think\Db;
  12 +use think\Exception;
  13 +use think\exception\ErrorException;
  14 +use think\Lang;
  15 +use think\Loader;
  16 +
  17 +class Crud extends Command
  18 +{
  19 + protected $stubList = [];
  20 +
  21 + protected $internalKeywords = [
  22 + 'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'final', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'isset', 'list', 'namespace', 'new', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use', 'var', 'while', 'xor'
  23 + ];
  24 +
  25 + /**
  26 + * 受保护的系统表, crud不会生效
  27 + */
  28 + protected $systemTables = [
  29 + 'admin', 'admin_log', 'auth_group', 'auth_group_access', 'auth_rule',
  30 + 'attachment', 'config', 'category', 'ems', 'sms',
  31 + 'user', 'user_group', 'user_rule', 'user_score_log', 'user_token',
  32 + ];
  33 +
  34 + /**
  35 + * Selectpage搜索字段关联
  36 + */
  37 + protected $fieldSelectpageMap = [
  38 + 'nickname' => ['user_id', 'user_ids', 'admin_id', 'admin_ids']
  39 + ];
  40 +
  41 + /**
  42 + * Enum类型识别为单选框的结尾字符,默认会识别为单选下拉列表
  43 + */
  44 + protected $enumRadioSuffix = ['data', 'state', 'status'];
  45 +
  46 + /**
  47 + * Set类型识别为复选框的结尾字符,默认会识别为多选下拉列表
  48 + */
  49 + protected $setCheckboxSuffix = ['data', 'state', 'status'];
  50 +
  51 + /**
  52 + * Int类型识别为日期时间的结尾字符,默认会识别为日期文本框
  53 + */
  54 + protected $intDateSuffix = ['time'];
  55 +
  56 + /**
  57 + * 开关后缀
  58 + */
  59 + protected $switchSuffix = ['switch'];
  60 +
  61 + /**
  62 + * 富文本后缀
  63 + */
  64 + protected $editorSuffix = ['content'];
  65 +
  66 + /**
  67 + * 城市后缀
  68 + */
  69 + protected $citySuffix = ['city'];
  70 +
  71 + /**
  72 + * JSON后缀
  73 + */
  74 + protected $jsonSuffix = ['json'];
  75 +
  76 + /**
  77 + * Selectpage对应的后缀
  78 + */
  79 + protected $selectpageSuffix = ['_id', '_ids'];
  80 +
  81 + /**
  82 + * Selectpage多选对应的后缀
  83 + */
  84 + protected $selectpagesSuffix = ['_ids'];
  85 +
  86 + /**
  87 + * 以指定字符结尾的字段格式化函数
  88 + */
  89 + protected $fieldFormatterSuffix = [
  90 + 'status' => ['type' => ['varchar', 'enum'], 'name' => 'status'],
  91 + 'icon' => 'icon',
  92 + 'flag' => 'flag',
  93 + 'url' => 'url',
  94 + 'image' => 'image',
  95 + 'images' => 'images',
  96 + 'avatar' => 'image',
  97 + 'switch' => 'toggle',
  98 + 'time' => ['type' => ['int', 'timestamp'], 'name' => 'datetime']
  99 + ];
  100 +
  101 + /**
  102 + * 识别为图片字段
  103 + */
  104 + protected $imageField = ['image', 'images', 'avatar', 'avatars'];
  105 +
  106 + /**
  107 + * 识别为文件字段
  108 + */
  109 + protected $fileField = ['file', 'files'];
  110 +
  111 + /**
  112 + * 保留字段
  113 + */
  114 + protected $reservedField = ['admin_id'];
  115 +
  116 + /**
  117 + * 排除字段
  118 + */
  119 + protected $ignoreFields = [];
  120 +
  121 + /**
  122 + * 排序字段
  123 + */
  124 + protected $sortField = 'weigh';
  125 +
  126 + /**
  127 + * 筛选字段
  128 + * @var string
  129 + */
  130 + protected $headingFilterField = 'status';
  131 +
  132 + /**
  133 + * 添加时间字段
  134 + * @var string
  135 + */
  136 + protected $createTimeField = 'createtime';
  137 +
  138 + /**
  139 + * 更新时间字段
  140 + * @var string
  141 + */
  142 + protected $updateTimeField = 'updatetime';
  143 +
  144 + /**
  145 + * 软删除时间字段
  146 + * @var string
  147 + */
  148 + protected $deleteTimeField = 'deletetime';
  149 +
  150 + /**
  151 + * 编辑器的Class
  152 + */
  153 + protected $editorClass = 'editor';
  154 +
  155 + /**
  156 + * langList的key最长字节数
  157 + */
  158 + protected $fieldMaxLen = 0;
  159 +
  160 + protected function configure()
  161 + {
  162 + $this
  163 + ->setName('crud')
  164 + ->addOption('table', 't', Option::VALUE_REQUIRED, 'table name without prefix', null)
  165 + ->addOption('controller', 'c', Option::VALUE_OPTIONAL, 'controller name', null)
  166 + ->addOption('model', 'm', Option::VALUE_OPTIONAL, 'model name', null)
  167 + ->addOption('fields', 'i', Option::VALUE_OPTIONAL, 'model visible fields', null)
  168 + ->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override or force delete,without tips', null)
  169 + ->addOption('local', 'l', Option::VALUE_OPTIONAL, 'local model', 1)
  170 + ->addOption('relation', 'r', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation table name without prefix', null)
  171 + ->addOption('relationmodel', 'e', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation model name', null)
  172 + ->addOption('relationforeignkey', 'k', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation foreign key', null)
  173 + ->addOption('relationprimarykey', 'p', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation primary key', null)
  174 + ->addOption('relationfields', 's', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation table fields', null)
  175 + ->addOption('relationmode', 'o', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation table mode,hasone or belongsto', null)
  176 + ->addOption('delete', 'd', Option::VALUE_OPTIONAL, 'delete all files generated by CRUD', null)
  177 + ->addOption('menu', 'u', Option::VALUE_OPTIONAL, 'create menu when CRUD completed', null)
  178 + ->addOption('setcheckboxsuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate checkbox component with suffix', null)
  179 + ->addOption('enumradiosuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate radio component with suffix', null)
  180 + ->addOption('imagefield', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate image component with suffix', null)
  181 + ->addOption('filefield', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate file component with suffix', null)
  182 + ->addOption('intdatesuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate date component with suffix', null)
  183 + ->addOption('switchsuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate switch component with suffix', null)
  184 + ->addOption('citysuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate citypicker component with suffix', null)
  185 + ->addOption('jsonsuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate fieldlist component with suffix', null)
  186 + ->addOption('editorsuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate editor component with suffix', null)
  187 + ->addOption('selectpagesuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate selectpage component with suffix', null)
  188 + ->addOption('selectpagessuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate multiple selectpage component with suffix', null)
  189 + ->addOption('ignorefields', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'ignore fields', null)
  190 + ->addOption('sortfield', null, Option::VALUE_OPTIONAL, 'sort field', null)
  191 + ->addOption('headingfilterfield', null, Option::VALUE_OPTIONAL, 'heading filter field', null)
  192 + ->addOption('editorclass', null, Option::VALUE_OPTIONAL, 'automatically generate editor class', null)
  193 + ->addOption('db', null, Option::VALUE_OPTIONAL, 'database config name', 'database')
  194 + ->setDescription('Build CRUD controller and model from table');
  195 + }
  196 +
  197 + protected function execute(Input $input, Output $output)
  198 + {
  199 + $adminPath = dirname(__DIR__) . DS;
  200 + //数据库
  201 + $db = $input->getOption('db');
  202 + //表名
  203 + $table = $input->getOption('table') ?: '';
  204 + //自定义控制器
  205 + $controller = $input->getOption('controller');
  206 + //自定义模型
  207 + $model = $input->getOption('model');
  208 + $model = $model ? $model : $controller;
  209 + //验证器类
  210 + $validate = $model;
  211 + //自定义显示字段
  212 + $fields = $input->getOption('fields');
  213 + //强制覆盖
  214 + $force = $input->getOption('force');
  215 + //是否为本地model,为0时表示为全局model将会把model放在app/common/model中
  216 + $local = $input->getOption('local');
  217 +
  218 + if (!$table) {
  219 + throw new Exception('table name can\'t empty');
  220 + }
  221 +
  222 +
  223 + //是否生成菜单
  224 + $menu = $input->getOption("menu");
  225 + //关联表
  226 + $relation = $input->getOption('relation');
  227 + //自定义关联表模型
  228 + $relationModels = $input->getOption('relationmodel');
  229 + //模式
  230 + $relationMode = $mode = $input->getOption('relationmode');
  231 + //外键
  232 + $relationForeignKey = $input->getOption('relationforeignkey');
  233 + //主键
  234 + $relationPrimaryKey = $input->getOption('relationprimarykey');
  235 + //关联表显示字段
  236 + $relationFields = $input->getOption('relationfields');
  237 + //复选框后缀
  238 + $setcheckboxsuffix = $input->getOption('setcheckboxsuffix');
  239 + //单选框后缀
  240 + $enumradiosuffix = $input->getOption('enumradiosuffix');
  241 + //图片后缀
  242 + $imagefield = $input->getOption('imagefield');
  243 + //文件后缀
  244 + $filefield = $input->getOption('filefield');
  245 + //日期后缀
  246 + $intdatesuffix = $input->getOption('intdatesuffix');
  247 + //开关后缀
  248 + $switchsuffix = $input->getOption('switchsuffix');
  249 + //城市后缀
  250 + $citysuffix = $input->getOption('citysuffix');
  251 + //JSON配置后缀
  252 + $jsonsuffix = $input->getOption('jsonsuffix');
  253 + //selectpage后缀
  254 + $selectpagesuffix = $input->getOption('selectpagesuffix');
  255 + //selectpage多选后缀
  256 + $selectpagessuffix = $input->getOption('selectpagessuffix');
  257 + //排除字段
  258 + $ignoreFields = $input->getOption('ignorefields');
  259 + //排序字段
  260 + $sortfield = $input->getOption('sortfield');
  261 + //顶部筛选过滤字段
  262 + $headingfilterfield = $input->getOption('headingfilterfield');
  263 + //编辑器Class
  264 + $editorclass = $input->getOption('editorclass');
  265 + if ($setcheckboxsuffix) {
  266 + $this->setCheckboxSuffix = $setcheckboxsuffix;
  267 + }
  268 + if ($enumradiosuffix) {
  269 + $this->enumRadioSuffix = $enumradiosuffix;
  270 + }
  271 + if ($imagefield) {
  272 + $this->imageField = $imagefield;
  273 + }
  274 + if ($filefield) {
  275 + $this->fileField = $filefield;
  276 + }
  277 + if ($intdatesuffix) {
  278 + $this->intDateSuffix = $intdatesuffix;
  279 + }
  280 + if ($switchsuffix) {
  281 + $this->switchSuffix = $switchsuffix;
  282 + }
  283 + if ($citysuffix) {
  284 + $this->citySuffix = $citysuffix;
  285 + }
  286 + if ($jsonsuffix) {
  287 + $this->jsonSuffix = $jsonsuffix;
  288 + }
  289 + if ($selectpagesuffix) {
  290 + $this->selectpageSuffix = $selectpagesuffix;
  291 + }
  292 + if ($selectpagessuffix) {
  293 + $this->selectpagesSuffix = $selectpagessuffix;
  294 + }
  295 + if ($ignoreFields) {
  296 + $this->ignoreFields = $ignoreFields;
  297 + }
  298 + if ($editorclass) {
  299 + $this->editorClass = $editorclass;
  300 + }
  301 + if ($sortfield) {
  302 + $this->sortField = $sortfield;
  303 + }
  304 + if ($headingfilterfield) {
  305 + $this->headingFilterField = $headingfilterfield;
  306 + }
  307 +
  308 + $this->reservedField = array_merge($this->reservedField, [$this->createTimeField, $this->updateTimeField, $this->deleteTimeField]);
  309 +
  310 + $dbconnect = Db::connect($db);
  311 + $dbname = Config::get($db . '.database');
  312 + $prefix = Config::get($db . '.prefix');
  313 +
  314 + //系统表无法生成,防止后台错乱
  315 + if(in_array(str_replace($prefix,"",$table),$this->systemTables)){
  316 + throw new Exception('system table can\'t be crud');
  317 + }
  318 +
  319 + //模块
  320 + $moduleName = 'admin';
  321 + $modelModuleName = $local ? $moduleName : 'common';
  322 + $validateModuleName = $local ? $moduleName : 'common';
  323 +
  324 + //检查主表
  325 + $modelName = $table = stripos($table, $prefix) === 0 ? substr($table, strlen($prefix)) : $table;
  326 + $modelTableType = 'table';
  327 + $modelTableTypeName = $modelTableName = $modelName;
  328 + $modelTableInfo = $dbconnect->query("SHOW TABLE STATUS LIKE '{$modelTableName}'", [], true);
  329 + if (!$modelTableInfo) {
  330 + $modelTableType = 'name';
  331 + $modelTableName = $prefix . $modelName;
  332 + $modelTableInfo = $dbconnect->query("SHOW TABLE STATUS LIKE '{$modelTableName}'", [], true);
  333 + if (!$modelTableInfo) {
  334 + throw new Exception("table not found");
  335 + }
  336 + }
  337 + $modelTableInfo = $modelTableInfo[0];
  338 +
  339 + $relations = [];
  340 + //检查关联表
  341 + if ($relation) {
  342 + $relationArr = $relation;
  343 + $relations = [];
  344 +
  345 + foreach ($relationArr as $index => $relationTable) {
  346 + $relationName = stripos($relationTable, $prefix) === 0 ? substr($relationTable, strlen($prefix)) : $relationTable;
  347 + $relationTableType = 'table';
  348 + $relationTableTypeName = $relationTableName = $relationName;
  349 + $relationTableInfo = $dbconnect->query("SHOW TABLE STATUS LIKE '{$relationTableName}'", [], true);
  350 + if (!$relationTableInfo) {
  351 + $relationTableType = 'name';
  352 + $relationTableName = $prefix . $relationName;
  353 + $relationTableInfo = $dbconnect->query("SHOW TABLE STATUS LIKE '{$relationTableName}'", [], true);
  354 + if (!$relationTableInfo) {
  355 + throw new Exception("relation table not found");
  356 + }
  357 + }
  358 + $relationTableInfo = $relationTableInfo[0];
  359 + $relationModel = isset($relationModels[$index]) ? $relationModels[$index] : '';
  360 +
  361 + list($relationNamespace, $relationName, $relationFile) = $this->getModelData($modelModuleName, $relationModel, $relationName);
  362 +
  363 + $relations[] = [
  364 + //关联表基础名
  365 + 'relationName' => $relationName,
  366 + //关联表类命名空间
  367 + 'relationNamespace' => $relationNamespace,
  368 + //关联模型名
  369 + 'relationModel' => $relationModel,
  370 + //关联文件
  371 + 'relationFile' => $relationFile,
  372 + //关联表名称
  373 + 'relationTableName' => $relationTableName,
  374 + //关联表信息
  375 + 'relationTableInfo' => $relationTableInfo,
  376 + //关联模型表类型(name或table)
  377 + 'relationTableType' => $relationTableType,
  378 + //关联模型表类型名称
  379 + 'relationTableTypeName' => $relationTableTypeName,
  380 + //关联模式
  381 + 'relationFields' => isset($relationFields[$index]) ? explode(',', $relationFields[$index]) : [],
  382 + //关联模式
  383 + 'relationMode' => isset($relationMode[$index]) ? $relationMode[$index] : 'belongsto',
  384 + //关联表外键
  385 + 'relationForeignKey' => isset($relationForeignKey[$index]) ? $relationForeignKey[$index] : Loader::parseName($relationName) . '_id',
  386 + //关联表主键
  387 + 'relationPrimaryKey' => isset($relationPrimaryKey[$index]) ? $relationPrimaryKey[$index] : '',
  388 + ];
  389 + }
  390 + }
  391 +
  392 + //根据表名匹配对应的Fontawesome图标
  393 + $iconPath = ROOT_PATH . str_replace('/', DS, '/public/assets/libs/font-awesome/less/variables.less');
  394 + $iconName = is_file($iconPath) && stripos(file_get_contents($iconPath), '@fa-var-' . $table . ':') ? 'fa fa-' . $table : 'fa fa-circle-o';
  395 +
  396 + //控制器
  397 + list($controllerNamespace, $controllerName, $controllerFile, $controllerArr) = $this->getControllerData($moduleName, $controller, $table);
  398 + //模型
  399 + list($modelNamespace, $modelName, $modelFile, $modelArr) = $this->getModelData($modelModuleName, $model, $table);
  400 + //验证器
  401 + list($validateNamespace, $validateName, $validateFile, $validateArr) = $this->getValidateData($validateModuleName, $validate, $table);
  402 +
  403 + //处理基础文件名,取消所有下划线并转换为小写
  404 + $baseNameArr = $controllerArr;
  405 + $baseFileName = Loader::parseName(array_pop($baseNameArr), 0);
  406 + array_push($baseNameArr, $baseFileName);
  407 + $controllerBaseName = strtolower(implode(DS, $baseNameArr));
  408 + $controllerUrl = strtolower(implode('/', $baseNameArr));
  409 +
  410 + //视图文件
  411 + $viewArr = $controllerArr;
  412 + $lastValue = array_pop($viewArr);
  413 + $viewArr[] = Loader::parseName($lastValue, 0);
  414 + array_unshift($viewArr, 'view');
  415 + $viewDir = $adminPath . strtolower(implode(DS, $viewArr)) . DS;
  416 +
  417 + //最终将生成的文件路径
  418 + $javascriptFile = ROOT_PATH . 'public' . DS . 'assets' . DS . 'js' . DS . 'backend' . DS . $controllerBaseName . '.js';
  419 + $addFile = $viewDir . 'add.html';
  420 + $editFile = $viewDir . 'edit.html';
  421 + $indexFile = $viewDir . 'index.html';
  422 + $recyclebinFile = $viewDir . 'recyclebin.html';
  423 + $langFile = $adminPath . 'lang' . DS . Lang::detect() . DS . $controllerBaseName . '.php';
  424 +
  425 + //是否为删除模式
  426 + $delete = $input->getOption('delete');
  427 + if ($delete) {
  428 + $readyFiles = [$controllerFile, $modelFile, $validateFile, $addFile, $editFile, $indexFile, $recyclebinFile, $langFile, $javascriptFile];
  429 + foreach ($readyFiles as $k => $v) {
  430 + $output->warning($v);
  431 + }
  432 + if (!$force) {
  433 + $output->info("Are you sure you want to delete all those files? Type 'yes' to continue: ");
  434 + $line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'));
  435 + if (trim($line) != 'yes') {
  436 + throw new Exception("Operation is aborted!");
  437 + }
  438 + }
  439 + foreach ($readyFiles as $k => $v) {
  440 + if (file_exists($v)) {
  441 + unlink($v);
  442 + }
  443 + //删除空文件夹
  444 + switch ($v) {
  445 + case $modelFile:
  446 + $this->removeEmptyBaseDir($v, $modelArr);
  447 + break;
  448 + case $validateFile:
  449 + $this->removeEmptyBaseDir($v, $validateArr);
  450 + break;
  451 + case $addFile:
  452 + case $editFile:
  453 + case $indexFile:
  454 + case $recyclebinFile:
  455 + $this->removeEmptyBaseDir($v, $viewArr);
  456 + break;
  457 + default:
  458 + $this->removeEmptyBaseDir($v, $controllerArr);
  459 + }
  460 + }
  461 +
  462 + //继续删除菜单
  463 + if ($menu) {
  464 + exec("php think menu -c {$controllerUrl} -d 1 -f 1");
  465 + }
  466 +
  467 + $output->info("Delete Successed");
  468 + return;
  469 + }
  470 +
  471 + //非覆盖模式时如果存在控制器文件则报错
  472 + if (is_file($controllerFile) && !$force) {
  473 + throw new Exception("controller already exists!\nIf you need to rebuild again, use the parameter --force=true ");
  474 + }
  475 +
  476 + //非覆盖模式时如果存在模型文件则报错
  477 + if (is_file($modelFile) && !$force) {
  478 + throw new Exception("model already exists!\nIf you need to rebuild again, use the parameter --force=true ");
  479 + }
  480 +
  481 + //非覆盖模式时如果存在验证文件则报错
  482 + if (is_file($validateFile) && !$force) {
  483 + throw new Exception("validate already exists!\nIf you need to rebuild again, use the parameter --force=true ");
  484 + }
  485 +
  486 + require $adminPath . 'common.php';
  487 +
  488 + //从数据库中获取表字段信息
  489 + $sql = "SELECT * FROM `information_schema`.`columns` "
  490 + . "WHERE TABLE_SCHEMA = ? AND table_name = ? "
  491 + . "ORDER BY ORDINAL_POSITION";
  492 + //加载主表的列
  493 + $columnList = $dbconnect->query($sql, [$dbname, $modelTableName]);
  494 + $fieldArr = [];
  495 + foreach ($columnList as $k => $v) {
  496 + $fieldArr[] = $v['COLUMN_NAME'];
  497 + }
  498 +
  499 + // 加载关联表的列
  500 + foreach ($relations as $index => &$relation) {
  501 + $relationColumnList = $dbconnect->query($sql, [$dbname, $relation['relationTableName']]);
  502 +
  503 + $relationFieldList = [];
  504 + foreach ($relationColumnList as $k => $v) {
  505 + $relationFieldList[] = $v['COLUMN_NAME'];
  506 + }
  507 + if (!$relation['relationPrimaryKey']) {
  508 + foreach ($relationColumnList as $k => $v) {
  509 + if ($v['COLUMN_KEY'] == 'PRI') {
  510 + $relation['relationPrimaryKey'] = $v['COLUMN_NAME'];
  511 + break;
  512 + }
  513 + }
  514 + }
  515 + // 如果主键为空
  516 + if (!$relation['relationPrimaryKey']) {
  517 + throw new Exception('Relation Primary key not found!');
  518 + }
  519 + // 如果主键不在表字段中
  520 + if (!in_array($relation['relationPrimaryKey'], $relationFieldList)) {
  521 + throw new Exception('Relation Primary key not found in table!');
  522 + }
  523 + $relation['relationColumnList'] = $relationColumnList;
  524 + $relation['relationFieldList'] = $relationFieldList;
  525 + }
  526 + unset($relation);
  527 +
  528 + $addList = [];
  529 + $editList = [];
  530 + $javascriptList = [];
  531 + $langList = [];
  532 + $field = 'id';
  533 + $order = 'id';
  534 + $priDefined = false;
  535 + $priKey = '';
  536 + $relationPrimaryKey = '';
  537 + foreach ($columnList as $k => $v) {
  538 + if ($v['COLUMN_KEY'] == 'PRI') {
  539 + $priKey = $v['COLUMN_NAME'];
  540 + break;
  541 + }
  542 + }
  543 + if (!$priKey) {
  544 + throw new Exception('Primary key not found!');
  545 + }
  546 +
  547 + $order = $priKey;
  548 +
  549 + //如果是关联模型
  550 + foreach ($relations as $index => &$relation) {
  551 + if ($relation['relationMode'] == 'hasone') {
  552 + $relationForeignKey = $relation['relationForeignKey'] ? $relation['relationForeignKey'] : $table . "_id";
  553 + $relationPrimaryKey = $relation['relationPrimaryKey'] ? $relation['relationPrimaryKey'] : $priKey;
  554 +
  555 + if (!in_array($relationForeignKey, $relation['relationFieldList'])) {
  556 + throw new Exception('relation table [' . $relation['relationTableName'] . '] must be contain field [' . $relationForeignKey . ']');
  557 + }
  558 + if (!in_array($relationPrimaryKey, $fieldArr)) {
  559 + throw new Exception('table [' . $modelTableName . '] must be contain field [' . $relationPrimaryKey . ']');
  560 + }
  561 + } else {
  562 + $relationForeignKey = $relation['relationForeignKey'] ? $relation['relationForeignKey'] : Loader::parseName($relation['relationName']) . "_id";
  563 + $relationPrimaryKey = $relation['relationPrimaryKey'] ? $relation['relationPrimaryKey'] : $relation['relationPriKey'];
  564 + if (!in_array($relationForeignKey, $fieldArr)) {
  565 + throw new Exception('table [' . $modelTableName . '] must be contain field [' . $relationForeignKey . ']');
  566 + }
  567 + if (!in_array($relationPrimaryKey, $relation['relationFieldList'])) {
  568 + throw new Exception('relation table [' . $relation['relationTableName'] . '] must be contain field [' . $relationPrimaryKey . ']');
  569 + }
  570 + }
  571 + $relation['relationForeignKey'] = $relationForeignKey;
  572 + $relation['relationPrimaryKey'] = $relationPrimaryKey;
  573 + $relation['relationClassName'] = $modelNamespace != $relation['relationNamespace'] ? $relation['relationNamespace'] . '\\' . $relation['relationName'] : $relation['relationName'];
  574 + }
  575 + unset($relation);
  576 +
  577 + try {
  578 + Form::setEscapeHtml(false);
  579 + $setAttrArr = [];
  580 + $getAttrArr = [];
  581 + $getEnumArr = [];
  582 + $appendAttrList = [];
  583 + $controllerAssignList = [];
  584 + $headingHtml = '{:build_heading()}';
  585 + $recyclebinHtml = '';
  586 +
  587 + //循环所有字段,开始构造视图的HTML和JS信息
  588 + foreach ($columnList as $k => $v) {
  589 + $field = $v['COLUMN_NAME'];
  590 + $itemArr = [];
  591 + // 这里构建Enum和Set类型的列表数据
  592 + if (in_array($v['DATA_TYPE'], ['enum', 'set', 'tinyint'])) {
  593 + if ($v['DATA_TYPE'] !== 'tinyint') {
  594 + $itemArr = substr($v['COLUMN_TYPE'], strlen($v['DATA_TYPE']) + 1, -1);
  595 + $itemArr = explode(',', str_replace("'", '', $itemArr));
  596 + }
  597 + $itemArr = $this->getItemArray($itemArr, $field, $v['COLUMN_COMMENT']);
  598 + //如果类型为tinyint且有使用备注数据
  599 + if ($itemArr && $v['DATA_TYPE'] == 'tinyint') {
  600 + $v['DATA_TYPE'] = 'enum';
  601 + }
  602 + }
  603 + // 语言列表
  604 + if ($v['COLUMN_COMMENT'] != '') {
  605 + $langList[] = $this->getLangItem($field, $v['COLUMN_COMMENT']);
  606 + }
  607 + $inputType = '';
  608 + //保留字段不能修改和添加
  609 + if ($v['COLUMN_KEY'] != 'PRI' && !in_array($field, $this->reservedField) && !in_array($field, $this->ignoreFields)) {
  610 + $inputType = $this->getFieldType($v);
  611 +
  612 + // 如果是number类型时增加一个步长
  613 + $step = $inputType == 'number' && $v['NUMERIC_SCALE'] > 0 ? "0." . str_repeat(0, $v['NUMERIC_SCALE'] - 1) . "1" : 0;
  614 +
  615 + $attrArr = ['id' => "c-{$field}"];
  616 + $cssClassArr = ['form-control'];
  617 + $fieldName = "row[{$field}]";
  618 + $defaultValue = $v['COLUMN_DEFAULT'];
  619 + $editValue = "{\$row.{$field}|htmlentities}";
  620 + // 如果默认值非null,则是一个必选项
  621 + if ($v['IS_NULLABLE'] == 'NO') {
  622 + $attrArr['data-rule'] = 'required';
  623 + }
  624 +
  625 + //如果字段类型为无符号型,则设置<input min=0>
  626 + if (stripos($v['COLUMN_TYPE'], 'unsigned') !== false) {
  627 + $attrArr['min'] = 0;
  628 + }
  629 +
  630 + if ($inputType == 'select') {
  631 + $cssClassArr[] = 'selectpicker';
  632 + $attrArr['class'] = implode(' ', $cssClassArr);
  633 + if ($v['DATA_TYPE'] == 'set') {
  634 + $attrArr['multiple'] = '';
  635 + $fieldName .= "[]";
  636 + }
  637 + $attrArr['name'] = $fieldName;
  638 +
  639 + $this->getEnum($getEnumArr, $controllerAssignList, $field, $itemArr, $v['DATA_TYPE'] == 'set' ? 'multiple' : 'select');
  640 +
  641 + $itemArr = $this->getLangArray($itemArr, false);
  642 + //添加一个获取器
  643 + $this->getAttr($getAttrArr, $field, $v['DATA_TYPE'] == 'set' ? 'multiple' : 'select');
  644 + if ($v['DATA_TYPE'] == 'set') {
  645 + $this->setAttr($setAttrArr, $field, $inputType);
  646 + }
  647 + $this->appendAttr($appendAttrList, $field);
  648 + $formAddElement = $this->getReplacedStub('html/select', ['field' => $field, 'fieldName' => $fieldName, 'fieldList' => $this->getFieldListName($field), 'attrStr' => Form::attributes($attrArr), 'selectedValue' => $defaultValue]);
  649 + $formEditElement = $this->getReplacedStub('html/select', ['field' => $field, 'fieldName' => $fieldName, 'fieldList' => $this->getFieldListName($field), 'attrStr' => Form::attributes($attrArr), 'selectedValue' => "\$row.{$field}"]);
  650 + } elseif ($inputType == 'datetime') {
  651 + $cssClassArr[] = 'datetimepicker';
  652 + $attrArr['class'] = implode(' ', $cssClassArr);
  653 + $format = "YYYY-MM-DD HH:mm:ss";
  654 + $phpFormat = "Y-m-d H:i:s";
  655 + $fieldFunc = '';
  656 + switch ($v['DATA_TYPE']) {
  657 + case 'year':
  658 + $format = "YYYY";
  659 + $phpFormat = 'Y';
  660 + break;
  661 + case 'date':
  662 + $format = "YYYY-MM-DD";
  663 + $phpFormat = 'Y-m-d';
  664 + break;
  665 + case 'time':
  666 + $format = "HH:mm:ss";
  667 + $phpFormat = 'H:i:s';
  668 + break;
  669 + case 'timestamp':
  670 + $fieldFunc = 'datetime';
  671 + // no break
  672 + case 'datetime':
  673 + $format = "YYYY-MM-DD HH:mm:ss";
  674 + $phpFormat = 'Y-m-d H:i:s';
  675 + break;
  676 + default:
  677 + $fieldFunc = 'datetime';
  678 + $this->getAttr($getAttrArr, $field, $inputType);
  679 + $this->setAttr($setAttrArr, $field, $inputType);
  680 + $this->appendAttr($appendAttrList, $field);
  681 + break;
  682 + }
  683 + $defaultDateTime = "{:date('{$phpFormat}')}";
  684 + $attrArr['data-date-format'] = $format;
  685 + $attrArr['data-use-current'] = "true";
  686 + $formAddElement = Form::text($fieldName, $defaultDateTime, $attrArr);
  687 + $formEditElement = Form::text($fieldName, ($fieldFunc ? "{:\$row.{$field}?{$fieldFunc}(\$row.{$field}):''}" : "{\$row.{$field}{$fieldFunc}}"), $attrArr);
  688 + } elseif ($inputType == 'checkbox' || $inputType == 'radio') {
  689 + unset($attrArr['data-rule']);
  690 + $fieldName = $inputType == 'checkbox' ? $fieldName .= "[]" : $fieldName;
  691 + $attrArr['name'] = "row[{$fieldName}]";
  692 +
  693 + $this->getEnum($getEnumArr, $controllerAssignList, $field, $itemArr, $inputType);
  694 + $itemArr = $this->getLangArray($itemArr, false);
  695 + //添加一个获取器
  696 + $this->getAttr($getAttrArr, $field, $inputType);
  697 + if ($inputType == 'checkbox') {
  698 + $this->setAttr($setAttrArr, $field, $inputType);
  699 + }
  700 + $this->appendAttr($appendAttrList, $field);
  701 + $defaultValue = $inputType == 'radio' && !$defaultValue ? key($itemArr) : $defaultValue;
  702 +
  703 + $formAddElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'fieldList' => $this->getFieldListName($field), 'attrStr' => Form::attributes($attrArr), 'selectedValue' => $defaultValue]);
  704 + $formEditElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'fieldList' => $this->getFieldListName($field), 'attrStr' => Form::attributes($attrArr), 'selectedValue' => "\$row.{$field}"]);
  705 + } elseif ($inputType == 'textarea' && !$this->isMatchSuffix($field, $this->selectpagesSuffix) && !$this->isMatchSuffix($field, $this->imageField)) {
  706 + $cssClassArr[] = $this->isMatchSuffix($field, $this->editorSuffix) ? $this->editorClass : '';
  707 + $attrArr['class'] = implode(' ', $cssClassArr);
  708 + $attrArr['rows'] = 5;
  709 + $formAddElement = Form::textarea($fieldName, $defaultValue, $attrArr);
  710 + $formEditElement = Form::textarea($fieldName, $editValue, $attrArr);
  711 + } elseif ($inputType == 'switch') {
  712 + unset($attrArr['data-rule']);
  713 + if ($defaultValue === '1' || $defaultValue === 'Y') {
  714 + $yes = $defaultValue;
  715 + $no = $defaultValue === '1' ? '0' : 'N';
  716 + } else {
  717 + $no = $defaultValue;
  718 + $yes = $defaultValue === '0' ? '1' : 'Y';
  719 + }
  720 + if (!$itemArr) {
  721 + $itemArr = [$yes => 'Yes', $no => 'No'];
  722 + }
  723 + $stateNoClass = 'fa-flip-horizontal text-gray';
  724 + $formAddElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'fieldYes' => $yes, 'fieldNo' => $no, 'attrStr' => Form::attributes($attrArr), 'fieldValue' => $defaultValue, 'fieldSwitchClass' => $defaultValue == $no ? $stateNoClass : '']);
  725 + $formEditElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'fieldYes' => $yes, 'fieldNo' => $no, 'attrStr' => Form::attributes($attrArr), 'fieldValue' => "{\$row.{$field}}", 'fieldSwitchClass' => "{eq name=\"\$row.{$field}\" value=\"{$no}\"}fa-flip-horizontal text-gray{/eq}"]);
  726 + } elseif ($inputType == 'citypicker') {
  727 + $attrArr['class'] = implode(' ', $cssClassArr);
  728 + $attrArr['data-toggle'] = "city-picker";
  729 + $formAddElement = sprintf("<div class='control-relative'>%s</div>", Form::input('text', $fieldName, $defaultValue, $attrArr));
  730 + $formEditElement = sprintf("<div class='control-relative'>%s</div>", Form::input('text', $fieldName, $editValue, $attrArr));
  731 + } elseif ($inputType == 'fieldlist') {
  732 + $itemArr = $this->getItemArray($itemArr, $field, $v['COLUMN_COMMENT']);
  733 + $itemKey = isset($itemArr['key']) ? ucfirst($itemArr['key']) : 'Key';
  734 + $itemValue = isset($itemArr['value']) ? ucfirst($itemArr['value']) : 'Value';
  735 + $formAddElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'itemKey' => $itemKey, 'itemValue' => $itemValue, 'fieldValue' => $defaultValue]);
  736 + $formEditElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'itemKey' => $itemKey, 'itemValue' => $itemValue, 'fieldValue' => $editValue]);
  737 + } else {
  738 + $search = $replace = '';
  739 + //特殊字段为关联搜索
  740 + if ($this->isMatchSuffix($field, $this->selectpageSuffix)) {
  741 + $inputType = 'text';
  742 + $defaultValue = '';
  743 + $attrArr['data-rule'] = 'required';
  744 + $cssClassArr[] = 'selectpage';
  745 + $selectpageController = str_replace('_', '/', substr($field, 0, strripos($field, '_')));
  746 + $attrArr['data-source'] = $selectpageController . "/index";
  747 + //如果是类型表需要特殊处理下
  748 + if ($selectpageController == 'category') {
  749 + $attrArr['data-source'] = 'category/selectpage';
  750 + $attrArr['data-params'] = '##replacetext##';
  751 + $search = '"##replacetext##"';
  752 + $replace = '\'{"custom[type]":"' . $table . '"}\'';
  753 + } elseif ($selectpageController == 'admin') {
  754 + $attrArr['data-source'] = 'auth/admin/selectpage';
  755 + } elseif ($selectpageController == 'user') {
  756 + $attrArr['data-source'] = 'user/user/index';
  757 + }
  758 + if ($this->isMatchSuffix($field, $this->selectpagesSuffix)) {
  759 + $attrArr['data-multiple'] = 'true';
  760 + }
  761 + foreach ($this->fieldSelectpageMap as $m => $n) {
  762 + if (in_array($field, $n)) {
  763 + $attrArr['data-field'] = $m;
  764 + break;
  765 + }
  766 + }
  767 + }
  768 + //因为有自动完成可输入其它内容
  769 + $step = array_intersect($cssClassArr, ['selectpage']) ? 0 : $step;
  770 + $attrArr['class'] = implode(' ', $cssClassArr);
  771 + $isUpload = false;
  772 + if ($this->isMatchSuffix($field, array_merge($this->imageField, $this->fileField))) {
  773 + $isUpload = true;
  774 + }
  775 + //如果是步长则加上步长
  776 + if ($step) {
  777 + $attrArr['step'] = $step;
  778 + }
  779 + //如果是图片加上个size
  780 + if ($isUpload) {
  781 + $attrArr['size'] = 50;
  782 + }
  783 +
  784 + $formAddElement = Form::input($inputType, $fieldName, $defaultValue, $attrArr);
  785 + $formEditElement = Form::input($inputType, $fieldName, $editValue, $attrArr);
  786 + if ($search && $replace) {
  787 + $formAddElement = str_replace($search, $replace, $formAddElement);
  788 + $formEditElement = str_replace($search, $replace, $formEditElement);
  789 + }
  790 + //如果是图片或文件
  791 + if ($isUpload) {
  792 + $formAddElement = $this->getImageUpload($field, $formAddElement);
  793 + $formEditElement = $this->getImageUpload($field, $formEditElement);
  794 + }
  795 + }
  796 + //构造添加和编辑HTML信息
  797 + $addList[] = $this->getFormGroup($field, $formAddElement);
  798 + $editList[] = $this->getFormGroup($field, $formEditElement);
  799 + }
  800 +
  801 + //过滤text类型字段
  802 + if ($v['DATA_TYPE'] != 'text' && $inputType != 'fieldlist') {
  803 + //主键
  804 + if ($v['COLUMN_KEY'] == 'PRI' && !$priDefined) {
  805 + $priDefined = true;
  806 + $javascriptList[] = "{checkbox: true}";
  807 + }
  808 + if ($this->deleteTimeField == $field) {
  809 + $recyclebinHtml = $this->getReplacedStub('html/recyclebin-html', ['controllerUrl' => $controllerUrl]);
  810 + continue;
  811 + }
  812 + if (!$fields || in_array($field, explode(',', $fields))) {
  813 + //构造JS列信息
  814 + $javascriptList[] = $this->getJsColumn($field, $v['DATA_TYPE'], $inputType && in_array($inputType, ['select', 'checkbox', 'radio']) ? '_text' : '', $itemArr);
  815 + }
  816 + if ($this->headingFilterField && $this->headingFilterField == $field && $itemArr) {
  817 + $headingHtml = $this->getReplacedStub('html/heading-html', ['field' => $field, 'fieldName' => Loader::parseName($field, 1, false)]);
  818 + }
  819 + //排序方式,如果有指定排序字段,否则按主键排序
  820 + $order = $field == $this->sortField ? $this->sortField : $order;
  821 + }
  822 + }
  823 +
  824 + //循环关联表,追加语言包和JS列
  825 + foreach ($relations as $index => $relation) {
  826 + foreach ($relation['relationColumnList'] as $k => $v) {
  827 + // 不显示的字段直接过滤掉
  828 + if ($relation['relationFields'] && !in_array($v['COLUMN_NAME'], $relation['relationFields'])) {
  829 + continue;
  830 + }
  831 +
  832 + $relationField = strtolower($relation['relationName']) . "." . $v['COLUMN_NAME'];
  833 + // 语言列表
  834 + if ($v['COLUMN_COMMENT'] != '') {
  835 + $langList[] = $this->getLangItem($relationField, $v['COLUMN_COMMENT']);
  836 + }
  837 +
  838 + //过滤text类型字段
  839 + if ($v['DATA_TYPE'] != 'text') {
  840 + //构造JS列信息
  841 + $javascriptList[] = $this->getJsColumn($relationField, $v['DATA_TYPE']);
  842 + }
  843 + }
  844 + }
  845 +
  846 + //JS最后一列加上操作列
  847 + $javascriptList[] = str_repeat(" ", 24) . "{field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}";
  848 + $addList = implode("\n", array_filter($addList));
  849 + $editList = implode("\n", array_filter($editList));
  850 + $javascriptList = implode(",\n", array_filter($javascriptList));
  851 + $langList = implode(",\n", array_filter($langList));
  852 + //数组等号对齐
  853 + $langList = array_filter(explode(",\n", $langList . ",\n"));
  854 + foreach ($langList as &$line) {
  855 + if (preg_match("/^\s+'([^']+)'\s*=>\s*'([^']+)'\s*/is", $line, $matches)) {
  856 + $line = " '{$matches[1]}'" . str_pad('=>', ($this->fieldMaxLen - strlen($matches[1]) + 3), ' ', STR_PAD_LEFT) . " '{$matches[2]}'";
  857 + }
  858 + }
  859 + unset($line);
  860 + $langList = implode(",\n", array_filter($langList));
  861 +
  862 + //表注释
  863 + $tableComment = $modelTableInfo['Comment'];
  864 + $tableComment = mb_substr($tableComment, -1) == '' ? mb_substr($tableComment, 0, -1) . '管理' : $tableComment;
  865 +
  866 + $modelInit = '';
  867 + if ($priKey != $order) {
  868 + $modelInit = $this->getReplacedStub('mixins' . DS . 'modelinit', ['order' => $order]);
  869 + }
  870 +
  871 + $data = [
  872 + 'modelConnection' => $db == 'database' ? '' : "protected \$connection = '{$db}';",
  873 + 'controllerNamespace' => $controllerNamespace,
  874 + 'modelNamespace' => $modelNamespace,
  875 + 'validateNamespace' => $validateNamespace,
  876 + 'controllerUrl' => $controllerUrl,
  877 + 'controllerName' => $controllerName,
  878 + 'controllerAssignList' => implode("\n", $controllerAssignList),
  879 + 'modelName' => $modelName,
  880 + 'modelTableName' => $modelTableName,
  881 + 'modelTableType' => $modelTableType,
  882 + 'modelTableTypeName' => $modelTableTypeName,
  883 + 'validateName' => $validateName,
  884 + 'tableComment' => $tableComment,
  885 + 'iconName' => $iconName,
  886 + 'pk' => $priKey,
  887 + 'order' => $order,
  888 + 'table' => $table,
  889 + 'tableName' => $modelTableName,
  890 + 'addList' => $addList,
  891 + 'editList' => $editList,
  892 + 'javascriptList' => $javascriptList,
  893 + 'langList' => $langList,
  894 + 'softDeleteClassPath' => in_array($this->deleteTimeField, $fieldArr) ? "use traits\model\SoftDelete;" : '',
  895 + 'softDelete' => in_array($this->deleteTimeField, $fieldArr) ? "use SoftDelete;" : '',
  896 + 'modelAutoWriteTimestamp' => in_array($this->createTimeField, $fieldArr) || in_array($this->updateTimeField, $fieldArr) ? "'int'" : 'false',
  897 + 'createTime' => in_array($this->createTimeField, $fieldArr) ? "'{$this->createTimeField}'" : 'false',
  898 + 'updateTime' => in_array($this->updateTimeField, $fieldArr) ? "'{$this->updateTimeField}'" : 'false',
  899 + 'deleteTime' => in_array($this->deleteTimeField, $fieldArr) ? "'{$this->deleteTimeField}'" : 'false',
  900 + 'relationSearch' => $relations ? 'true' : 'false',
  901 + 'relationWithList' => '',
  902 + 'relationMethodList' => '',
  903 + 'controllerIndex' => '',
  904 + 'recyclebinJs' => '',
  905 + 'headingHtml' => $headingHtml,
  906 + 'recyclebinHtml' => $recyclebinHtml,
  907 + 'visibleFieldList' => $fields ? "\$row->visible(['" . implode("','", array_filter(in_array($priKey,explode(',', $fields))?explode(',', $fields):explode(',',$priKey.','.$fields))) . "']);" : '',
  908 + 'appendAttrList' => implode(",\n", $appendAttrList),
  909 + 'getEnumList' => implode("\n\n", $getEnumArr),
  910 + 'getAttrList' => implode("\n\n", $getAttrArr),
  911 + 'setAttrList' => implode("\n\n", $setAttrArr),
  912 + 'modelInit' => $modelInit,
  913 + ];
  914 +
  915 + //如果使用关联模型
  916 + if ($relations) {
  917 + $relationWithList = $relationMethodList = $relationVisibleFieldList = [];
  918 + foreach ($relations as $index => $relation) {
  919 + //需要构造关联的方法
  920 + $relation['relationMethod'] = strtolower($relation['relationName']);
  921 +
  922 + //关联的模式
  923 + $relation['relationMode'] = $relation['relationMode'] == 'hasone' ? 'hasOne' : 'belongsTo';
  924 +
  925 + //关联字段
  926 + $relation['relationPrimaryKey'] = $relation['relationPrimaryKey'] ? $relation['relationPrimaryKey'] : $priKey;
  927 +
  928 + //预载入的方法
  929 + $relationWithList[] = $relation['relationMethod'];
  930 +
  931 + unset($relation['relationColumnList'], $relation['relationFieldList'], $relation['relationTableInfo']);
  932 +
  933 + //构造关联模型的方法
  934 + $relationMethodList[] = $this->getReplacedStub('mixins' . DS . 'modelrelationmethod', $relation);
  935 +
  936 + //如果设置了显示主表字段,则必须显式将关联表字段显示
  937 + if ($fields) {
  938 + $relationVisibleFieldList[] = "\$row->visible(['{$relation['relationMethod']}']);";
  939 + }
  940 +
  941 + //显示的字段
  942 + if ($relation['relationFields']) {
  943 + $relationVisibleFieldList[] = "\$row->getRelation('" . $relation['relationMethod'] . "')->visible(['" . implode("','", $relation['relationFields']) . "']);";
  944 + }
  945 + }
  946 +
  947 + $data['relationWithList'] = "->with(['" . implode("','", $relationWithList) . "'])";
  948 + $data['relationMethodList'] = implode("\n\n", $relationMethodList);
  949 + $data['relationVisibleFieldList'] = implode("\n\t\t\t\t", $relationVisibleFieldList);
  950 +
  951 + //需要重写index方法
  952 + $data['controllerIndex'] = $this->getReplacedStub('controllerindex', $data);
  953 + } elseif ($fields) {
  954 + $data = array_merge($data, ['relationWithList' => '', 'relationMethodList' => '', 'relationVisibleFieldList' => '']);
  955 + //需要重写index方法
  956 + $data['controllerIndex'] = $this->getReplacedStub('controllerindex', $data);
  957 + }
  958 +
  959 + // 生成控制器文件
  960 + $this->writeToFile('controller', $data, $controllerFile);
  961 + // 生成模型文件
  962 + $this->writeToFile('model', $data, $modelFile);
  963 +
  964 + if ($relations) {
  965 + foreach ($relations as $i => $relation) {
  966 + $relation['modelNamespace'] = $data['modelNamespace'];
  967 + if (!is_file($relation['relationFile'])) {
  968 + // 生成关联模型文件
  969 + $this->writeToFile('relationmodel', $relation, $relation['relationFile']);
  970 + }
  971 + }
  972 + }
  973 + // 生成验证文件
  974 + $this->writeToFile('validate', $data, $validateFile);
  975 + // 生成视图文件
  976 + $this->writeToFile('add', $data, $addFile);
  977 + $this->writeToFile('edit', $data, $editFile);
  978 + $this->writeToFile('index', $data, $indexFile);
  979 + if ($recyclebinHtml) {
  980 + $this->writeToFile('recyclebin', $data, $recyclebinFile);
  981 + $recyclebinTitle = in_array('title', $fieldArr) ? 'title' : (in_array('name', $fieldArr) ? 'name' : '');
  982 + $recyclebinTitleJs = $recyclebinTitle ? "\n {field: '{$recyclebinTitle}', title: __('" . (ucfirst($recyclebinTitle)) . "'), align: 'left'}," : '';
  983 + $data['recyclebinJs'] = $this->getReplacedStub('mixins/recyclebinjs', ['deleteTimeField' => $this->deleteTimeField, 'recyclebinTitleJs' => $recyclebinTitleJs, 'controllerUrl' => $controllerUrl]);
  984 + }
  985 + // 生成JS文件
  986 + $this->writeToFile('javascript', $data, $javascriptFile);
  987 + // 生成语言文件
  988 + $this->writeToFile('lang', $data, $langFile);
  989 + } catch (ErrorException $e) {
  990 + throw new Exception("Code: " . $e->getCode() . "\nLine: " . $e->getLine() . "\nMessage: " . $e->getMessage() . "\nFile: " . $e->getFile());
  991 + }
  992 +
  993 + //继续生成菜单
  994 + if ($menu) {
  995 + exec("php think menu -c {$controllerUrl}");
  996 + }
  997 +
  998 + $output->info("Build Successed");
  999 + }
  1000 +
  1001 + protected function getEnum(&$getEnum, &$controllerAssignList, $field, $itemArr = '', $inputType = '')
  1002 + {
  1003 + if (!in_array($inputType, ['datetime', 'select', 'multiple', 'checkbox', 'radio'])) {
  1004 + return;
  1005 + }
  1006 + $fieldList = $this->getFieldListName($field);
  1007 + $methodName = 'get' . ucfirst($fieldList);
  1008 + foreach ($itemArr as $k => &$v) {
  1009 + $v = "__('" . mb_ucfirst($v) . "')";
  1010 + }
  1011 + unset($v);
  1012 + $itemString = $this->getArrayString($itemArr);
  1013 + $getEnum[] = <<<EOD
  1014 + public function {$methodName}()
  1015 + {
  1016 + return [{$itemString}];
  1017 + }
  1018 +EOD;
  1019 + $controllerAssignList[] = <<<EOD
  1020 + \$this->view->assign("{$fieldList}", \$this->model->{$methodName}());
  1021 +EOD;
  1022 + }
  1023 +
  1024 + protected function getAttr(&$getAttr, $field, $inputType = '')
  1025 + {
  1026 + if (!in_array($inputType, ['datetime', 'select', 'multiple', 'checkbox', 'radio'])) {
  1027 + return;
  1028 + }
  1029 + $attrField = ucfirst($this->getCamelizeName($field));
  1030 + $getAttr[] = $this->getReplacedStub("mixins" . DS . $inputType, ['field' => $field, 'methodName' => "get{$attrField}TextAttr", 'listMethodName' => "get{$attrField}List"]);
  1031 + }
  1032 +
  1033 + protected function setAttr(&$setAttr, $field, $inputType = '')
  1034 + {
  1035 + if (!in_array($inputType, ['datetime', 'checkbox', 'select'])) {
  1036 + return;
  1037 + }
  1038 + $attrField = ucfirst($this->getCamelizeName($field));
  1039 + if ($inputType == 'datetime') {
  1040 + $return = <<<EOD
  1041 +return \$value === '' ? null : (\$value && !is_numeric(\$value) ? strtotime(\$value) : \$value);
  1042 +EOD;
  1043 + } elseif (in_array($inputType, ['checkbox', 'select'])) {
  1044 + $return = <<<EOD
  1045 +return is_array(\$value) ? implode(',', \$value) : \$value;
  1046 +EOD;
  1047 + }
  1048 + $setAttr[] = <<<EOD
  1049 + protected function set{$attrField}Attr(\$value)
  1050 + {
  1051 + $return
  1052 + }
  1053 +EOD;
  1054 + }
  1055 +
  1056 + protected function appendAttr(&$appendAttrList, $field)
  1057 + {
  1058 + $appendAttrList[] = <<<EOD
  1059 + '{$field}_text'
  1060 +EOD;
  1061 + }
  1062 +
  1063 + /**
  1064 + * 移除相对的空目录
  1065 + * @param $parseFile
  1066 + * @param $parseArr
  1067 + * @return bool
  1068 + */
  1069 + protected function removeEmptyBaseDir($parseFile, $parseArr)
  1070 + {
  1071 + if (count($parseArr) > 1) {
  1072 + $parentDir = dirname($parseFile);
  1073 + for ($i = 0; $i < count($parseArr); $i++) {
  1074 + try {
  1075 + $iterator = new \FilesystemIterator($parentDir);
  1076 + $isDirEmpty = !$iterator->valid();
  1077 + if ($isDirEmpty) {
  1078 + rmdir($parentDir);
  1079 + $parentDir = dirname($parentDir);
  1080 + } else {
  1081 + return true;
  1082 + }
  1083 + } catch (\UnexpectedValueException $e) {
  1084 + return false;
  1085 + }
  1086 + }
  1087 + }
  1088 + return true;
  1089 + }
  1090 +
  1091 + /**
  1092 + * 获取控制器相关信息
  1093 + * @param $module
  1094 + * @param $controller
  1095 + * @param $table
  1096 + * @return array
  1097 + */
  1098 + protected function getControllerData($module, $controller, $table)
  1099 + {
  1100 + return $this->getParseNameData($module, $controller, $table, 'controller');
  1101 + }
  1102 +
  1103 + /**
  1104 + * 获取模型相关信息
  1105 + * @param $module
  1106 + * @param $model
  1107 + * @param $table
  1108 + * @return array
  1109 + */
  1110 + protected function getModelData($module, $model, $table)
  1111 + {
  1112 + return $this->getParseNameData($module, $model, $table, 'model');
  1113 + }
  1114 +
  1115 + /**
  1116 + * 获取验证器相关信息
  1117 + * @param $module
  1118 + * @param $validate
  1119 + * @param $table
  1120 + * @return array
  1121 + */
  1122 + protected function getValidateData($module, $validate, $table)
  1123 + {
  1124 + return $this->getParseNameData($module, $validate, $table, 'validate');
  1125 + }
  1126 +
  1127 + /**
  1128 + * 获取已解析相关信息
  1129 + * @param string $module 模块名称
  1130 + * @param string $name 自定义名称
  1131 + * @param string $table 数据表名
  1132 + * @param string $type 解析类型,本例中为controller、model、validate
  1133 + * @return array
  1134 + */
  1135 + protected function getParseNameData($module, $name, $table, $type)
  1136 + {
  1137 + $arr = [];
  1138 + if (!$name) {
  1139 + $parseName = Loader::parseName($table, 1);
  1140 + $parseArr = [$table];
  1141 + } else {
  1142 + $name = str_replace(['.', '/', '\\'], '/', $name);
  1143 + $arr = explode('/', $name);
  1144 + $parseName = ucfirst(array_pop($arr));
  1145 + $parseArr = $arr;
  1146 + array_push($parseArr, $parseName);
  1147 + }
  1148 + //类名不能为内部关键字
  1149 + if (in_array(strtolower($parseName), $this->internalKeywords)) {
  1150 + throw new Exception('Unable to use internal variable:' . $parseName);
  1151 + }
  1152 + $appNamespace = Config::get('app_namespace');
  1153 + $parseNamespace = "{$appNamespace}\\{$module}\\{$type}" . ($arr ? "\\" . implode("\\", $arr) : "");
  1154 + $moduleDir = APP_PATH . $module . DS;
  1155 + $parseFile = $moduleDir . $type . DS . ($arr ? implode(DS, $arr) . DS : '') . $parseName . '.php';
  1156 + return [$parseNamespace, $parseName, $parseFile, $parseArr];
  1157 + }
  1158 +
  1159 + /**
  1160 + * 写入到文件
  1161 + * @param string $name
  1162 + * @param array $data
  1163 + * @param string $pathname
  1164 + * @return mixed
  1165 + */
  1166 + protected function writeToFile($name, $data, $pathname)
  1167 + {
  1168 + foreach ($data as $index => &$datum) {
  1169 + $datum = is_array($datum) ? '' : $datum;
  1170 + }
  1171 + unset($datum);
  1172 + $content = $this->getReplacedStub($name, $data);
  1173 +
  1174 + if (!is_dir(dirname($pathname))) {
  1175 + mkdir(dirname($pathname), 0755, true);
  1176 + }
  1177 + return file_put_contents($pathname, $content);
  1178 + }
  1179 +
  1180 + /**
  1181 + * 获取替换后的数据
  1182 + * @param string $name
  1183 + * @param array $data
  1184 + * @return string
  1185 + */
  1186 + protected function getReplacedStub($name, $data)
  1187 + {
  1188 + foreach ($data as $index => &$datum) {
  1189 + $datum = is_array($datum) ? '' : $datum;
  1190 + }
  1191 + unset($datum);
  1192 + $search = $replace = [];
  1193 + foreach ($data as $k => $v) {
  1194 + $search[] = "{%{$k}%}";
  1195 + $replace[] = $v;
  1196 + }
  1197 + $stubname = $this->getStub($name);
  1198 + if (isset($this->stubList[$stubname])) {
  1199 + $stub = $this->stubList[$stubname];
  1200 + } else {
  1201 + $this->stubList[$stubname] = $stub = file_get_contents($stubname);
  1202 + }
  1203 + $content = str_replace($search, $replace, $stub);
  1204 + return $content;
  1205 + }
  1206 +
  1207 + /**
  1208 + * 获取基础模板
  1209 + * @param string $name
  1210 + * @return string
  1211 + */
  1212 + protected function getStub($name)
  1213 + {
  1214 + return __DIR__ . DS . 'Crud' . DS . 'stubs' . DS . $name . '.stub';
  1215 + }
  1216 +
  1217 + protected function getLangItem($field, $content)
  1218 + {
  1219 + if ($content || !Lang::has($field)) {
  1220 + $this->fieldMaxLen = strlen($field) > $this->fieldMaxLen ? strlen($field) : $this->fieldMaxLen;
  1221 + $content = str_replace(',', ',', $content);
  1222 + if (stripos($content, ':') !== false && stripos($content, ',') && stripos($content, '=') !== false) {
  1223 + list($fieldLang, $item) = explode(':', $content);
  1224 + $itemArr = [$field => $fieldLang];
  1225 + foreach (explode(',', $item) as $k => $v) {
  1226 + $valArr = explode('=', $v);
  1227 + if (count($valArr) == 2) {
  1228 + list($key, $value) = $valArr;
  1229 + $itemArr[$field . ' ' . $key] = $value;
  1230 + $this->fieldMaxLen = strlen($field . ' ' . $key) > $this->fieldMaxLen ? strlen($field . ' ' . $key) : $this->fieldMaxLen;
  1231 + }
  1232 + }
  1233 + } else {
  1234 + $itemArr = [$field => $content];
  1235 + }
  1236 + $resultArr = [];
  1237 + foreach ($itemArr as $k => $v) {
  1238 + $resultArr[] = " '" . mb_ucfirst($k) . "' => '{$v}'";
  1239 + }
  1240 + return implode(",\n", $resultArr);
  1241 + } else {
  1242 + return '';
  1243 + }
  1244 + }
  1245 +
  1246 + /**
  1247 + * 读取数据和语言数组列表
  1248 + * @param array $arr
  1249 + * @param boolean $withTpl
  1250 + * @return array
  1251 + */
  1252 + protected function getLangArray($arr, $withTpl = true)
  1253 + {
  1254 + $langArr = [];
  1255 + foreach ($arr as $k => $v) {
  1256 + $langArr[$k] = is_numeric($k) ? ($withTpl ? "{:" : "") . "__('" . mb_ucfirst($v) . "')" . ($withTpl ? "}" : "") : $v;
  1257 + }
  1258 + return $langArr;
  1259 + }
  1260 +
  1261 + /**
  1262 + * 将数据转换成带字符串
  1263 + * @param array $arr
  1264 + * @return string
  1265 + */
  1266 + protected function getArrayString($arr)
  1267 + {
  1268 + if (!is_array($arr)) {
  1269 + return $arr;
  1270 + }
  1271 + $stringArr = [];
  1272 + foreach ($arr as $k => $v) {
  1273 + $is_var = in_array(substr($v, 0, 1), ['$', '_']);
  1274 + if (!$is_var) {
  1275 + $v = str_replace("'", "\'", $v);
  1276 + $k = str_replace("'", "\'", $k);
  1277 + }
  1278 + $stringArr[] = "'" . $k . "' => " . ($is_var ? $v : "'{$v}'");
  1279 + }
  1280 + return implode(", ", $stringArr);
  1281 + }
  1282 +
  1283 + protected function getItemArray($item, $field, $comment)
  1284 + {
  1285 + $itemArr = [];
  1286 + $comment = str_replace(',', ',', $comment);
  1287 + if (stripos($comment, ':') !== false && stripos($comment, ',') && stripos($comment, '=') !== false) {
  1288 + list($fieldLang, $item) = explode(':', $comment);
  1289 + $itemArr = [];
  1290 + foreach (explode(',', $item) as $k => $v) {
  1291 + $valArr = explode('=', $v);
  1292 + if (count($valArr) == 2) {
  1293 + list($key, $value) = $valArr;
  1294 + $itemArr[$key] = $field . ' ' . $key;
  1295 + }
  1296 + }
  1297 + } else {
  1298 + foreach ($item as $k => $v) {
  1299 + $itemArr[$v] = is_numeric($v) ? $field . ' ' . $v : $v;
  1300 + }
  1301 + }
  1302 + return $itemArr;
  1303 + }
  1304 +
  1305 + protected function getFieldType(& $v)
  1306 + {
  1307 + $inputType = 'text';
  1308 + switch ($v['DATA_TYPE']) {
  1309 + case 'bigint':
  1310 + case 'int':
  1311 + case 'mediumint':
  1312 + case 'smallint':
  1313 + case 'tinyint':
  1314 + $inputType = 'number';
  1315 + break;
  1316 + case 'enum':
  1317 + case 'set':
  1318 + $inputType = 'select';
  1319 + break;
  1320 + case 'decimal':
  1321 + case 'double':
  1322 + case 'float':
  1323 + $inputType = 'number';
  1324 + break;
  1325 + case 'longtext':
  1326 + case 'text':
  1327 + case 'mediumtext':
  1328 + case 'smalltext':
  1329 + case 'tinytext':
  1330 + $inputType = 'textarea';
  1331 + break;
  1332 + case 'year':
  1333 + case 'date':
  1334 + case 'time':
  1335 + case 'datetime':
  1336 + case 'timestamp':
  1337 + $inputType = 'datetime';
  1338 + break;
  1339 + default:
  1340 + break;
  1341 + }
  1342 + $fieldsName = $v['COLUMN_NAME'];
  1343 + // 指定后缀说明也是个时间字段
  1344 + if ($this->isMatchSuffix($fieldsName, $this->intDateSuffix)) {
  1345 + $inputType = 'datetime';
  1346 + }
  1347 + // 指定后缀结尾且类型为enum,说明是个单选框
  1348 + if ($this->isMatchSuffix($fieldsName, $this->enumRadioSuffix) && $v['DATA_TYPE'] == 'enum') {
  1349 + $inputType = "radio";
  1350 + }
  1351 + // 指定后缀结尾且类型为set,说明是个复选框
  1352 + if ($this->isMatchSuffix($fieldsName, $this->setCheckboxSuffix) && $v['DATA_TYPE'] == 'set') {
  1353 + $inputType = "checkbox";
  1354 + }
  1355 + // 指定后缀结尾且类型为char或tinyint且长度为1,说明是个Switch复选框
  1356 + if ($this->isMatchSuffix($fieldsName, $this->switchSuffix) && ($v['COLUMN_TYPE'] == 'tinyint(1)' || $v['COLUMN_TYPE'] == 'char(1)') && $v['COLUMN_DEFAULT'] !== '' && $v['COLUMN_DEFAULT'] !== null) {
  1357 + $inputType = "switch";
  1358 + }
  1359 + // 指定后缀结尾城市选择框
  1360 + if ($this->isMatchSuffix($fieldsName, $this->citySuffix) && ($v['DATA_TYPE'] == 'varchar' || $v['DATA_TYPE'] == 'char')) {
  1361 + $inputType = "citypicker";
  1362 + }
  1363 + // 指定后缀结尾JSON配置
  1364 + if ($this->isMatchSuffix($fieldsName, $this->jsonSuffix) && ($v['DATA_TYPE'] == 'varchar' || $v['DATA_TYPE'] == 'text')) {
  1365 + $inputType = "fieldlist";
  1366 + }
  1367 + return $inputType;
  1368 + }
  1369 +
  1370 + /**
  1371 + * 判断是否符合指定后缀
  1372 + * @param string $field 字段名称
  1373 + * @param mixed $suffixArr 后缀
  1374 + * @return boolean
  1375 + */
  1376 + protected function isMatchSuffix($field, $suffixArr)
  1377 + {
  1378 + $suffixArr = is_array($suffixArr) ? $suffixArr : explode(',', $suffixArr);
  1379 + foreach ($suffixArr as $k => $v) {
  1380 + if (preg_match("/{$v}$/i", $field)) {
  1381 + return true;
  1382 + }
  1383 + }
  1384 + return false;
  1385 + }
  1386 +
  1387 + /**
  1388 + * 获取表单分组数据
  1389 + * @param string $field
  1390 + * @param string $content
  1391 + * @return string
  1392 + */
  1393 + protected function getFormGroup($field, $content)
  1394 + {
  1395 + $langField = mb_ucfirst($field);
  1396 + return <<<EOD
  1397 + <div class="form-group">
  1398 + <label class="control-label col-xs-12 col-sm-2">{:__('{$langField}')}:</label>
  1399 + <div class="col-xs-12 col-sm-8">
  1400 + {$content}
  1401 + </div>
  1402 + </div>
  1403 +EOD;
  1404 + }
  1405 +
  1406 + /**
  1407 + * 获取图片模板数据
  1408 + * @param string $field
  1409 + * @param string $content
  1410 + * @return string
  1411 + */
  1412 + protected function getImageUpload($field, $content)
  1413 + {
  1414 + $uploadfilter = $selectfilter = '';
  1415 + if ($this->isMatchSuffix($field, $this->imageField)) {
  1416 + $uploadfilter = ' data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp"';
  1417 + $selectfilter = ' data-mimetype="image/*"';
  1418 + }
  1419 + $multiple = substr($field, -1) == 's' ? ' data-multiple="true"' : ' data-multiple="false"';
  1420 + $preview = ' data-preview-id="p-' . $field . '"';
  1421 + $previewcontainer = $preview ? '<ul class="row list-inline faupload-preview" id="p-' . $field . '"></ul>' : '';
  1422 + return <<<EOD
  1423 +<div class="input-group">
  1424 + {$content}
  1425 + <div class="input-group-addon no-border no-padding">
  1426 + <span><button type="button" id="faupload-{$field}" class="btn btn-danger faupload" data-input-id="c-{$field}"{$uploadfilter}{$multiple}{$preview}><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
  1427 + <span><button type="button" id="fachoose-{$field}" class="btn btn-primary fachoose" data-input-id="c-{$field}"{$selectfilter}{$multiple}><i class="fa fa-list"></i> {:__('Choose')}</button></span>
  1428 + </div>
  1429 + <span class="msg-box n-right" for="c-{$field}"></span>
  1430 + </div>
  1431 + {$previewcontainer}
  1432 +EOD;
  1433 + }
  1434 +
  1435 + /**
  1436 + * 获取JS列数据
  1437 + * @param string $field
  1438 + * @param string $datatype
  1439 + * @param string $extend
  1440 + * @param array $itemArr
  1441 + * @return string
  1442 + */
  1443 + protected function getJsColumn($field, $datatype = '', $extend = '', $itemArr = [])
  1444 + {
  1445 + $lang = mb_ucfirst($field);
  1446 + $formatter = '';
  1447 + foreach ($this->fieldFormatterSuffix as $k => $v) {
  1448 + if (preg_match("/{$k}$/i", $field)) {
  1449 + if (is_array($v)) {
  1450 + if (in_array($datatype, $v['type'])) {
  1451 + $formatter = $v['name'];
  1452 + break;
  1453 + }
  1454 + } else {
  1455 + $formatter = $v;
  1456 + break;
  1457 + }
  1458 + }
  1459 + }
  1460 + $html = str_repeat(" ", 24) . "{field: '{$field}', title: __('{$lang}')";
  1461 +
  1462 + if ($datatype == 'set') {
  1463 + $formatter = 'label';
  1464 + }
  1465 + foreach ($itemArr as $k => &$v) {
  1466 + if (substr($v, 0, 3) !== '__(') {
  1467 + $v = "__('" . mb_ucfirst($v) . "')";
  1468 + }
  1469 + }
  1470 + unset($v);
  1471 + $searchList = json_encode($itemArr, JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE);
  1472 + $searchList = str_replace(['":"', '"}', ')","'], ['":', '}', '),"'], $searchList);
  1473 + if ($itemArr) {
  1474 + $html .= ", searchList: " . $searchList;
  1475 + }
  1476 +
  1477 + // 文件、图片、权重等字段默认不加入搜索栏,字符串类型默认LIKE
  1478 + $noSearchFiles = ['file$', 'files$', 'image$', 'images$', '^weigh$'];
  1479 + if (preg_match("/" . implode('|', $noSearchFiles) . "/i", $field)) {
  1480 + $html .= ", operate: false";
  1481 + } else if (in_array($datatype, ['varchar'])) {
  1482 + $html .= ", operate: 'LIKE'";
  1483 + }
  1484 +
  1485 + if (in_array($datatype, ['date', 'datetime']) || $formatter === 'datetime') {
  1486 + $html .= ", operate:'RANGE', addclass:'datetimerange', autocomplete:false";
  1487 + } elseif (in_array($datatype, ['float', 'double', 'decimal'])) {
  1488 + $html .= ", operate:'BETWEEN'";
  1489 + }
  1490 + if (in_array($datatype, ['set'])) {
  1491 + $html .= ", operate:'FIND_IN_SET'";
  1492 + }
  1493 + if (in_array($formatter, ['image', 'images'])) {
  1494 + $html .= ", events: Table.api.events.image";
  1495 + }
  1496 + if (in_array($formatter, ['toggle'])) {
  1497 + $html .= ", table: table";
  1498 + }
  1499 + if ($itemArr && !$formatter) {
  1500 + $formatter = 'normal';
  1501 + }
  1502 + if ($formatter) {
  1503 + $html .= ", formatter: Table.api.formatter." . $formatter . "}";
  1504 + } else {
  1505 + $html .= "}";
  1506 + }
  1507 + return $html;
  1508 + }
  1509 +
  1510 + protected function getCamelizeName($uncamelized_words, $separator = '_')
  1511 + {
  1512 + $uncamelized_words = $separator . str_replace($separator, " ", strtolower($uncamelized_words));
  1513 + return ltrim(str_replace(" ", "", ucwords($uncamelized_words)), $separator);
  1514 + }
  1515 +
  1516 + protected function getFieldListName($field)
  1517 + {
  1518 + return $this->getCamelizeName($field) . 'List';
  1519 + }
  1520 +}
  1 +<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
  2 +
  3 +{%addList%}
  4 + <div class="form-group layer-footer">
  5 + <label class="control-label col-xs-12 col-sm-2"></label>
  6 + <div class="col-xs-12 col-sm-8">
  7 + <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
  8 + <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
  9 + </div>
  10 + </div>
  11 +</form>
  1 +<?php
  2 +
  3 +namespace {%controllerNamespace%};
  4 +
  5 +use app\common\controller\Backend;
  6 +
  7 +/**
  8 + * {%tableComment%}
  9 + *
  10 + * @icon {%iconName%}
  11 + */
  12 +class {%controllerName%} extends Backend
  13 +{
  14 +
  15 + /**
  16 + * {%modelName%}模型对象
  17 + * @var \{%modelNamespace%}\{%modelName%}
  18 + */
  19 + protected $model = null;
  20 +
  21 + public function _initialize()
  22 + {
  23 + parent::_initialize();
  24 + $this->model = new \{%modelNamespace%}\{%modelName%};
  25 +{%controllerAssignList%}
  26 + }
  27 +
  28 + public function import()
  29 + {
  30 + parent::import();
  31 + }
  32 +
  33 + /**
  34 + * 默认生成的控制器所继承的父类中有index/add/edit/del/multi五个基础方法、destroy/restore/recyclebin三个回收站方法
  35 + * 因此在当前控制器中可不用编写增删改查的代码,除非需要自己控制这部分逻辑
  36 + * 需要将application/admin/library/traits/Backend.php中对应的方法复制到当前控制器,然后进行修改
  37 + */
  38 +
  39 +{%controllerIndex%}
  40 +}
  1 +
  2 + /**
  3 + * 查看
  4 + */
  5 + public function index()
  6 + {
  7 + //当前是否为关联查询
  8 + $this->relationSearch = {%relationSearch%};
  9 + //设置过滤方法
  10 + $this->request->filter(['strip_tags', 'trim']);
  11 + if ($this->request->isAjax()) {
  12 + //如果发送的来源是Selectpage,则转发到Selectpage
  13 + if ($this->request->request('keyField')) {
  14 + return $this->selectpage();
  15 + }
  16 + list($where, $sort, $order, $offset, $limit) = $this->buildparams();
  17 +
  18 + $list = $this->model
  19 + {%relationWithList%}
  20 + ->where($where)
  21 + ->order($sort, $order)
  22 + ->paginate($limit);
  23 +
  24 + foreach ($list as $row) {
  25 + {%visibleFieldList%}
  26 + {%relationVisibleFieldList%}
  27 + }
  28 +
  29 + $result = array("total" => $list->total(), "rows" => $list->items());
  30 +
  31 + return json($result);
  32 + }
  33 + return $this->view->fetch();
  34 + }
  1 +<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
  2 +
  3 +{%editList%}
  4 + <div class="form-group layer-footer">
  5 + <label class="control-label col-xs-12 col-sm-2"></label>
  6 + <div class="col-xs-12 col-sm-8">
  7 + <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
  8 + <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
  9 + </div>
  10 + </div>
  11 +</form>
  1 +
  2 + <div class="checkbox">
  3 + {foreach name="{%fieldList%}" item="vo"}
  4 + <label for="{%fieldName%}-{$key}"><input id="{%fieldName%}-{$key}" name="{%fieldName%}" type="checkbox" value="{$key}" {in name="key" value="{%selectedValue%}"}checked{/in} /> {$vo}</label>
  5 + {/foreach}
  6 + </div>
  1 +
  2 + <dl class="fieldlist" data-name="{%fieldName%}">
  3 + <dd>
  4 + <ins>{:__('{%itemKey%}')}</ins>
  5 + <ins>{:__('{%itemValue%}')}</ins>
  6 + </dd>
  7 + <dd><a href="javascript:;" class="btn btn-sm btn-success btn-append"><i class="fa fa-plus"></i> {:__('Append')}</a></dd>
  8 + <textarea name="{%fieldName%}" class="form-control hide" cols="30" rows="5">{%fieldValue%}</textarea>
  9 + </dl>
  10 +
  1 +
  2 + <div class="panel-heading">
  3 + {:build_heading(null,FALSE)}
  4 + <ul class="nav nav-tabs" data-field="{%field%}">
  5 + <li class="{:$Think.get.{%field%} === null ? 'active' : ''}"><a href="#t-all" data-value="" data-toggle="tab">{:__('All')}</a></li>
  6 + {foreach name="{%fieldName%}List" item="vo"}
  7 + <li class="{:$Think.get.{%field%} === (string)$key ? 'active' : ''}"><a href="#t-{$key}" data-value="{$key}" data-toggle="tab">{$vo}</a></li>
  8 + {/foreach}
  9 + </ul>
  10 + </div>
  1 +
  2 + <div class="radio">
  3 + {foreach name="{%fieldList%}" item="vo"}
  4 + <label for="{%fieldName%}-{$key}"><input id="{%fieldName%}-{$key}" name="{%fieldName%}" type="radio" value="{$key}" {in name="key" value="{%selectedValue%}"}checked{/in} /> {$vo}</label>
  5 + {/foreach}
  6 + </div>
  1 +<a class="btn btn-success btn-recyclebin btn-dialog {:$auth->check('{%controllerUrl%}/recyclebin')?'':'hide'}" href="{%controllerUrl%}/recyclebin" title="{:__('Recycle bin')}"><i class="fa fa-recycle"></i> {:__('Recycle bin')}</a>
  1 +
  2 + <select {%attrStr%}>
  3 + {foreach name="{%fieldList%}" item="vo"}
  4 + <option value="{$key}" {in name="key" value="{%selectedValue%}"}selected{/in}>{$vo}</option>
  5 + {/foreach}
  6 + </select>
  1 +
  2 + <input {%attrStr%} name="{%fieldName%}" type="hidden" value="{%fieldValue%}">
  3 + <a href="javascript:;" data-toggle="switcher" class="btn-switcher" data-input-id="c-{%field%}" data-yes="{%fieldYes%}" data-no="{%fieldNo%}" >
  4 + <i class="fa fa-toggle-on text-success {%fieldSwitchClass%} fa-2x"></i>
  5 + </a>
  1 +<div class="panel panel-default panel-intro">
  2 + {%headingHtml%}
  3 +
  4 + <div class="panel-body">
  5 + <div id="myTabContent" class="tab-content">
  6 + <div class="tab-pane fade active in" id="one">
  7 + <div class="widget-body no-padding">
  8 + <div id="toolbar" class="toolbar">
  9 + <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
  10 + <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('{%controllerUrl%}/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
  11 + <a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('{%controllerUrl%}/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
  12 + <a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('{%controllerUrl%}/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
  13 + <a href="javascript:;" class="btn btn-danger btn-import {:$auth->check('{%controllerUrl%}/import')?'':'hide'}" title="{:__('Import')}" id="btn-import-file" data-url="ajax/upload" data-mimetype="csv,xls,xlsx" data-multiple="false"><i class="fa fa-upload"></i> {:__('Import')}</a>
  14 +
  15 + <div class="dropdown btn-group {:$auth->check('{%controllerUrl%}/multi')?'':'hide'}">
  16 + <a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> {:__('More')}</a>
  17 + <ul class="dropdown-menu text-left" role="menu">
  18 + <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=normal"><i class="fa fa-eye"></i> {:__('Set to normal')}</a></li>
  19 + <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=hidden"><i class="fa fa-eye-slash"></i> {:__('Set to hidden')}</a></li>
  20 + </ul>
  21 + </div>
  22 +
  23 + {%recyclebinHtml%}
  24 + </div>
  25 + <table id="table" class="table table-striped table-bordered table-hover table-nowrap"
  26 + data-operate-edit="{:$auth->check('{%controllerUrl%}/edit')}"
  27 + data-operate-del="{:$auth->check('{%controllerUrl%}/del')}"
  28 + width="100%">
  29 + </table>
  30 + </div>
  31 + </div>
  32 +
  33 + </div>
  34 + </div>
  35 +</div>
  1 +define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
  2 +
  3 + var Controller = {
  4 + index: function () {
  5 + // 初始化表格参数配置
  6 + Table.api.init({
  7 + extend: {
  8 + index_url: '{%controllerUrl%}/index' + location.search,
  9 + add_url: '{%controllerUrl%}/add',
  10 + edit_url: '{%controllerUrl%}/edit',
  11 + del_url: '{%controllerUrl%}/del',
  12 + multi_url: '{%controllerUrl%}/multi',
  13 + import_url: '{%controllerUrl%}/import',
  14 + table: '{%table%}',
  15 + }
  16 + });
  17 +
  18 + var table = $("#table");
  19 +
  20 + // 初始化表格
  21 + table.bootstrapTable({
  22 + url: $.fn.bootstrapTable.defaults.extend.index_url,
  23 + pk: '{%pk%}',
  24 + sortName: '{%order%}',
  25 + columns: [
  26 + [
  27 + {%javascriptList%}
  28 + ]
  29 + ]
  30 + });
  31 +
  32 + // 为表格绑定事件
  33 + Table.api.bindevent(table);
  34 + },{%recyclebinJs%}
  35 + add: function () {
  36 + Controller.api.bindevent();
  37 + },
  38 + edit: function () {
  39 + Controller.api.bindevent();
  40 + },
  41 + api: {
  42 + bindevent: function () {
  43 + Form.api.bindevent($("form[role=form]"));
  44 + }
  45 + }
  46 + };
  47 + return Controller;
  48 +});
  1 +<?php
  2 +
  3 +return [
  4 +{%langList%}
  5 +];
  1 +
  2 + public function {%methodName%}($value, $data)
  3 + {
  4 + $value = $value ? $value : (isset($data['{%field%}']) ? $data['{%field%}'] : '');
  5 + $valueArr = explode(',', $value);
  6 + $list = $this->{%listMethodName%}();
  7 + return implode(',', array_intersect_key($list, array_flip($valueArr)));
  8 + }
  1 +
  2 + public function {%methodName%}($value, $data)
  3 + {
  4 + $value = $value ? $value : (isset($data['{%field%}']) ? $data['{%field%}'] : '');
  5 + return is_numeric($value) ? date("Y-m-d H:i:s", $value) : $value;
  6 + }
  1 +
  2 + protected static function init()
  3 + {
  4 + self::afterInsert(function ($row) {
  5 + $pk = $row->getPk();
  6 + $row->getQuery()->where($pk, $row[$pk])->update(['{%order%}' => $row[$pk]]);
  7 + });
  8 + }
  1 +
  2 + public function {%relationMethod%}()
  3 + {
  4 + return $this->{%relationMode%}('{%relationClassName%}', '{%relationForeignKey%}', '{%relationPrimaryKey%}', [], 'LEFT')->setEagerlyType(0);
  5 + }
  1 +
  2 + public function {%methodName%}($value, $data)
  3 + {
  4 + $value = $value ? $value : (isset($data['{%field%}']) ? $data['{%field%}'] : '');
  5 + $valueArr = explode(',', $value);
  6 + $list = $this->{%listMethodName%}();
  7 + return implode(',', array_intersect_key($list, array_flip($valueArr)));
  8 + }
  1 +
  2 + public function {%methodName%}($value, $data)
  3 + {
  4 + $value = $value ? $value : (isset($data['{%field%}']) ? $data['{%field%}'] : '');
  5 + $list = $this->{%listMethodName%}();
  6 + return isset($list[$value]) ? $list[$value] : '';
  7 + }
  1 +
  2 + recyclebin: function () {
  3 + // 初始化表格参数配置
  4 + Table.api.init({
  5 + extend: {
  6 + 'dragsort_url': ''
  7 + }
  8 + });
  9 +
  10 + var table = $("#table");
  11 +
  12 + // 初始化表格
  13 + table.bootstrapTable({
  14 + url: '{%controllerUrl%}/recyclebin' + location.search,
  15 + pk: 'id',
  16 + sortName: 'id',
  17 + columns: [
  18 + [
  19 + {checkbox: true},
  20 + {field: 'id', title: __('Id')},{%recyclebinTitleJs%}
  21 + {
  22 + field: '{%deleteTimeField%}',
  23 + title: __('Deletetime'),
  24 + operate: 'RANGE',
  25 + addclass: 'datetimerange',
  26 + formatter: Table.api.formatter.datetime
  27 + },
  28 + {
  29 + field: 'operate',
  30 + width: '130px',
  31 + title: __('Operate'),
  32 + table: table,
  33 + events: Table.api.events.operate,
  34 + buttons: [
  35 + {
  36 + name: 'Restore',
  37 + text: __('Restore'),
  38 + classname: 'btn btn-xs btn-info btn-ajax btn-restoreit',
  39 + icon: 'fa fa-rotate-left',
  40 + url: '{%controllerUrl%}/restore',
  41 + refresh: true
  42 + },
  43 + {
  44 + name: 'Destroy',
  45 + text: __('Destroy'),
  46 + classname: 'btn btn-xs btn-danger btn-ajax btn-destroyit',
  47 + icon: 'fa fa-times',
  48 + url: '{%controllerUrl%}/destroy',
  49 + refresh: true
  50 + }
  51 + ],
  52 + formatter: Table.api.formatter.operate
  53 + }
  54 + ]
  55 + ]
  56 + });
  57 +
  58 + // 为表格绑定事件
  59 + Table.api.bindevent(table);
  60 + },
  1 +
  2 + public function {%methodName%}($value, $data)
  3 + {
  4 + $value = $value ? $value : (isset($data['{%field%}']) ? $data['{%field%}'] : '');
  5 + $list = $this->{%listMethodName%}();
  6 + return isset($list[$value]) ? $list[$value] : '';
  7 + }
  1 +<?php
  2 +
  3 +namespace {%modelNamespace%};
  4 +
  5 +use think\Model;
  6 +{%softDeleteClassPath%}
  7 +
  8 +class {%modelName%} extends Model
  9 +{
  10 +
  11 + {%softDelete%}
  12 +
  13 + {%modelConnection%}
  14 +
  15 + // 表名
  16 + protected ${%modelTableType%} = '{%modelTableTypeName%}';
  17 +
  18 + // 自动写入时间戳字段
  19 + protected $autoWriteTimestamp = {%modelAutoWriteTimestamp%};
  20 +
  21 + // 定义时间戳字段名
  22 + protected $createTime = {%createTime%};
  23 + protected $updateTime = {%updateTime%};
  24 + protected $deleteTime = {%deleteTime%};
  25 +
  26 + // 追加属性
  27 + protected $append = [
  28 +{%appendAttrList%}
  29 + ];
  30 +
  31 +{%modelInit%}
  32 +
  33 +{%getEnumList%}
  34 +
  35 +{%getAttrList%}
  36 +
  37 +{%setAttrList%}
  38 +
  39 +{%relationMethodList%}
  40 +}
  1 +<div class="panel panel-default panel-intro">
  2 + {:build_heading()}
  3 +
  4 + <div class="panel-body">
  5 + <div id="myTabContent" class="tab-content">
  6 + <div class="tab-pane fade active in" id="one">
  7 + <div class="widget-body no-padding">
  8 + <div id="toolbar" class="toolbar">
  9 + {:build_toolbar('refresh')}
  10 + <a class="btn btn-info btn-multi btn-disabled disabled {:$auth->check('{%controllerUrl%}/restore')?'':'hide'}" href="javascript:;" data-url="{%controllerUrl%}/restore" data-action="restore"><i class="fa fa-rotate-left"></i> {:__('Restore')}</a>
  11 + <a class="btn btn-danger btn-multi btn-disabled disabled {:$auth->check('{%controllerUrl%}/destroy')?'':'hide'}" href="javascript:;" data-url="{%controllerUrl%}/destroy" data-action="destroy"><i class="fa fa-times"></i> {:__('Destroy')}</a>
  12 + <a class="btn btn-success btn-restoreall {:$auth->check('{%controllerUrl%}/restore')?'':'hide'}" href="javascript:;" data-url="{%controllerUrl%}/restore" title="{:__('Restore all')}"><i class="fa fa-rotate-left"></i> {:__('Restore all')}</a>
  13 + <a class="btn btn-danger btn-destroyall {:$auth->check('{%controllerUrl%}/destroy')?'':'hide'}" href="javascript:;" data-url="{%controllerUrl%}/destroy" title="{:__('Destroy all')}"><i class="fa fa-times"></i> {:__('Destroy all')}</a>
  14 + </div>
  15 + <table id="table" class="table table-striped table-bordered table-hover"
  16 + data-operate-restore="{:$auth->check('{%controllerUrl%}/restore')}"
  17 + data-operate-destroy="{:$auth->check('{%controllerUrl%}/destroy')}"
  18 + width="100%">
  19 + </table>
  20 + </div>
  21 + </div>
  22 +
  23 + </div>
  24 + </div>
  25 +</div>
  1 +<?php
  2 +
  3 +namespace {%modelNamespace%};
  4 +
  5 +use think\Model;
  6 +
  7 +class {%relationName%} extends Model
  8 +{
  9 + // 表名
  10 + protected ${%relationTableType%} = '{%relationTableTypeName%}';
  11 +
  12 +}
  1 +<?php
  2 +
  3 +namespace {%validateNamespace%};
  4 +
  5 +use think\Validate;
  6 +
  7 +class {%validateName%} extends Validate
  8 +{
  9 + /**
  10 + * 验证规则
  11 + */
  12 + protected $rule = [
  13 + ];
  14 + /**
  15 + * 提示消息
  16 + */
  17 + protected $message = [
  18 + ];
  19 + /**
  20 + * 验证场景
  21 + */
  22 + protected $scene = [
  23 + 'add' => [],
  24 + 'edit' => [],
  25 + ];
  26 +
  27 +}
  1 +<?php
  2 +
  3 +namespace app\admin\command;
  4 +
  5 +use fast\Random;
  6 +use PDO;
  7 +use think\Config;
  8 +use think\console\Command;
  9 +use think\console\Input;
  10 +use think\console\input\Option;
  11 +use think\console\Output;
  12 +use think\Db;
  13 +use think\Exception;
  14 +use think\Lang;
  15 +use think\Request;
  16 +use think\View;
  17 +
  18 +class Install extends Command
  19 +{
  20 + protected $model = null;
  21 + /**
  22 + * @var \think\View 视图类实例
  23 + */
  24 + protected $view;
  25 +
  26 + /**
  27 + * @var \think\Request Request 实例
  28 + */
  29 + protected $request;
  30 +
  31 + protected function configure()
  32 + {
  33 + $config = Config::get('database');
  34 + $this
  35 + ->setName('install')
  36 + ->addOption('hostname', 'a', Option::VALUE_OPTIONAL, 'mysql hostname', $config['hostname'])
  37 + ->addOption('hostport', 'o', Option::VALUE_OPTIONAL, 'mysql hostport', $config['hostport'])
  38 + ->addOption('database', 'd', Option::VALUE_OPTIONAL, 'mysql database', $config['database'])
  39 + ->addOption('prefix', 'r', Option::VALUE_OPTIONAL, 'table prefix', $config['prefix'])
  40 + ->addOption('username', 'u', Option::VALUE_OPTIONAL, 'mysql username', $config['username'])
  41 + ->addOption('password', 'p', Option::VALUE_OPTIONAL, 'mysql password', $config['password'])
  42 + ->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override', false)
  43 + ->setDescription('New installation of FastAdmin');
  44 + }
  45 +
  46 + /**
  47 + * 命令行安装
  48 + */
  49 + protected function execute(Input $input, Output $output)
  50 + {
  51 + define('INSTALL_PATH', APP_PATH . 'admin' . DS . 'command' . DS . 'Install' . DS);
  52 + // 覆盖安装
  53 + $force = $input->getOption('force');
  54 + $hostname = $input->getOption('hostname');
  55 + $hostport = $input->getOption('hostport');
  56 + $database = $input->getOption('database');
  57 + $prefix = $input->getOption('prefix');
  58 + $username = $input->getOption('username');
  59 + $password = $input->getOption('password');
  60 +
  61 + $installLockFile = INSTALL_PATH . "install.lock";
  62 + if (is_file($installLockFile) && !$force) {
  63 + throw new Exception("\nFastAdmin already installed!\nIf you need to reinstall again, use the parameter --force=true ");
  64 + }
  65 +
  66 + $adminUsername = 'admin';
  67 + $adminPassword = Random::alnum(10);
  68 + $adminEmail = 'admin@admin.com';
  69 + $siteName = __('My Website');
  70 +
  71 + $adminName = $this->installation($hostname, $hostport, $database, $username, $password, $prefix, $adminUsername, $adminPassword, $adminEmail, $siteName);
  72 + if ($adminName) {
  73 + $output->highlight("Admin url:http://www.yoursite.com/{$adminName}");
  74 + }
  75 +
  76 + $output->highlight("Admin username:{$adminUsername}");
  77 + $output->highlight("Admin password:{$adminPassword}");
  78 +
  79 + \think\Cache::rm('__menu__');
  80 +
  81 + $output->info("Install Successed!");
  82 + }
  83 +
  84 + /**
  85 + * PC端安装
  86 + */
  87 + public function index()
  88 + {
  89 + $this->view = View::instance(Config::get('template'), Config::get('view_replace_str'));
  90 + $this->request = Request::instance();
  91 +
  92 + define('INSTALL_PATH', APP_PATH . 'admin' . DS . 'command' . DS . 'Install' . DS);
  93 + $langSet = strtolower($this->request->langset());
  94 + if (!$langSet || in_array($langSet, ['zh-cn', 'zh-hans-cn'])) {
  95 + Lang::load(INSTALL_PATH . 'zh-cn.php');
  96 + }
  97 +
  98 + $installLockFile = INSTALL_PATH . "install.lock";
  99 +
  100 + if (is_file($installLockFile)) {
  101 + echo __('The system has been installed. If you need to reinstall, please remove %s first', 'install.lock');
  102 + exit;
  103 + }
  104 + $output = function ($code, $msg, $url = null, $data = null) {
  105 + return json(['code' => $code, 'msg' => $msg, 'url' => $url, 'data' => $data]);
  106 + };
  107 +
  108 + if ($this->request->isPost()) {
  109 + $mysqlHostname = $this->request->post('mysqlHostname', '127.0.0.1');
  110 + $mysqlHostport = $this->request->post('mysqlHostport', '3306');
  111 + $hostArr = explode(':', $mysqlHostname);
  112 + if (count($hostArr) > 1) {
  113 + $mysqlHostname = $hostArr[0];
  114 + $mysqlHostport = $hostArr[1];
  115 + }
  116 + $mysqlUsername = $this->request->post('mysqlUsername', 'root');
  117 + $mysqlPassword = $this->request->post('mysqlPassword', '');
  118 + $mysqlDatabase = $this->request->post('mysqlDatabase', '');
  119 + $mysqlPrefix = $this->request->post('mysqlPrefix', 'fa_');
  120 + $adminUsername = $this->request->post('adminUsername', 'admin');
  121 + $adminPassword = $this->request->post('adminPassword', '');
  122 + $adminPasswordConfirmation = $this->request->post('adminPasswordConfirmation', '');
  123 + $adminEmail = $this->request->post('adminEmail', 'admin@admin.com');
  124 + $siteName = $this->request->post('siteName', __('My Website'));
  125 +
  126 + if ($adminPassword !== $adminPasswordConfirmation) {
  127 + return $output(0, __('The two passwords you entered did not match'));
  128 + }
  129 +
  130 + $adminName = '';
  131 + try {
  132 + $adminName = $this->installation($mysqlHostname, $mysqlHostport, $mysqlDatabase, $mysqlUsername, $mysqlPassword, $mysqlPrefix, $adminUsername, $adminPassword, $adminEmail, $siteName);
  133 + } catch (\PDOException $e) {
  134 + throw new Exception($e->getMessage());
  135 + } catch (\Exception $e) {
  136 + return $output(0, $e->getMessage());
  137 + }
  138 + return $output(1, __('Install Successed'), null, ['adminName' => $adminName]);
  139 + }
  140 + $errInfo = '';
  141 + try {
  142 + $this->checkenv();
  143 + } catch (\Exception $e) {
  144 + $errInfo = $e->getMessage();
  145 + }
  146 + return $this->view->fetch(INSTALL_PATH . "install.html", ['errInfo' => $errInfo]);
  147 + }
  148 +
  149 + /**
  150 + * 执行安装
  151 + */
  152 + protected function installation($mysqlHostname, $mysqlHostport, $mysqlDatabase, $mysqlUsername, $mysqlPassword, $mysqlPrefix, $adminUsername, $adminPassword, $adminEmail = null, $siteName = null)
  153 + {
  154 + $this->checkenv();
  155 +
  156 + if ($mysqlDatabase == '') {
  157 + throw new Exception(__('Please input correct database'));
  158 + }
  159 + if (!preg_match("/^\w{3,12}$/", $adminUsername)) {
  160 + throw new Exception(__('Please input correct username'));
  161 + }
  162 + if (!preg_match("/^[\S]{6,16}$/", $adminPassword)) {
  163 + throw new Exception(__('Please input correct password'));
  164 + }
  165 + if ($siteName == '' || preg_match("/fast" . "admin/i", $siteName)) {
  166 + throw new Exception(__('Please input correct website'));
  167 + }
  168 +
  169 + $sql = file_get_contents(INSTALL_PATH . 'fastadmin.sql');
  170 +
  171 + $sql = str_replace("`fa_", "`{$mysqlPrefix}", $sql);
  172 +
  173 + // 先尝试能否自动创建数据库
  174 + $config = Config::get('database');
  175 + try {
  176 + $pdo = new PDO("{$config['type']}:host={$mysqlHostname}" . ($mysqlHostport ? ";port={$mysqlHostport}" : ''), $mysqlUsername, $mysqlPassword);
  177 + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  178 + $pdo->query("CREATE DATABASE IF NOT EXISTS `{$mysqlDatabase}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;");
  179 +
  180 + // 连接install命令中指定的数据库
  181 + $instance = Db::connect([
  182 + 'type' => "{$config['type']}",
  183 + 'hostname' => "{$mysqlHostname}",
  184 + 'hostport' => "{$mysqlHostport}",
  185 + 'database' => "{$mysqlDatabase}",
  186 + 'username' => "{$mysqlUsername}",
  187 + 'password' => "{$mysqlPassword}",
  188 + 'prefix' => "{$mysqlPrefix}",
  189 + ]);
  190 +
  191 + // 查询一次SQL,判断连接是否正常
  192 + $instance->execute("SELECT 1");
  193 +
  194 + // 调用原生PDO对象进行批量查询
  195 + $instance->getPdo()->exec($sql);
  196 + } catch (\PDOException $e) {
  197 + throw new Exception($e->getMessage());
  198 + }
  199 + // 后台入口文件
  200 + $adminFile = ROOT_PATH . 'public' . DS . 'admin.php';
  201 +
  202 + // 数据库配置文件
  203 + $dbConfigFile = APP_PATH . 'database.php';
  204 + $dbConfigText = @file_get_contents($dbConfigFile);
  205 + $callback = function ($matches) use ($mysqlHostname, $mysqlHostport, $mysqlUsername, $mysqlPassword, $mysqlDatabase, $mysqlPrefix) {
  206 + $field = "mysql" . ucfirst($matches[1]);
  207 + $replace = $$field;
  208 + if ($matches[1] == 'hostport' && $mysqlHostport == 3306) {
  209 + $replace = '';
  210 + }
  211 + return "'{$matches[1]}'{$matches[2]}=>{$matches[3]}Env::get('database.{$matches[1]}', '{$replace}'),";
  212 + };
  213 + $dbConfigText = preg_replace_callback("/'(hostname|database|username|password|hostport|prefix)'(\s+)=>(\s+)Env::get\((.*)\)\,/", $callback, $dbConfigText);
  214 +
  215 + // 检测能否成功写入数据库配置
  216 + $result = @file_put_contents($dbConfigFile, $dbConfigText);
  217 + if (!$result) {
  218 + throw new Exception(__('The current permissions are insufficient to write the file %s', 'application/database.php'));
  219 + }
  220 +
  221 + // 设置新的Token随机密钥key
  222 + $oldTokenKey = config('token.key');
  223 + $newTokenKey = \fast\Random::alnum(32);
  224 + $coreConfigFile = CONF_PATH . 'config.php';
  225 + $coreConfigText = @file_get_contents($coreConfigFile);
  226 + $coreConfigText = preg_replace("/'key'(\s+)=>(\s+)'{$oldTokenKey}'/", "'key'\$1=>\$2'{$newTokenKey}'", $coreConfigText);
  227 +
  228 + $result = @file_put_contents($coreConfigFile, $coreConfigText);
  229 + if (!$result) {
  230 + throw new Exception(__('The current permissions are insufficient to write the file %s', 'application/config.php'));
  231 + }
  232 +
  233 + // 变更默认管理员密码
  234 + $adminPassword = $adminPassword ? $adminPassword : Random::alnum(8);
  235 + $adminEmail = $adminEmail ? $adminEmail : "admin@admin.com";
  236 + $newSalt = substr(md5(uniqid(true)), 0, 6);
  237 + $newPassword = md5(md5($adminPassword) . $newSalt);
  238 + $data = ['username' => $adminUsername, 'email' => $adminEmail, 'password' => $newPassword, 'salt' => $newSalt];
  239 + $instance->name('admin')->where('username', 'admin')->update($data);
  240 +
  241 + // 变更前台默认用户的密码,随机生成
  242 + $newSalt = substr(md5(uniqid(true)), 0, 6);
  243 + $newPassword = md5(md5(Random::alnum(8)) . $newSalt);
  244 + $instance->name('user')->where('username', 'admin')->update(['password' => $newPassword, 'salt' => $newSalt]);
  245 +
  246 + // 修改后台入口
  247 + $adminName = '';
  248 + if (is_file($adminFile)) {
  249 + $adminName = Random::alpha(10) . '.php';
  250 + rename($adminFile, ROOT_PATH . 'public' . DS . $adminName);
  251 + }
  252 +
  253 + //修改站点名称
  254 + if ($siteName != config('site.name')) {
  255 + $instance->name('config')->where('name', 'name')->update(['value' => $siteName]);
  256 + $siteConfigFile = CONF_PATH . 'extra' . DS . 'site.php';
  257 + $siteConfig = include $siteConfigFile;
  258 + $configList = $instance->name("config")->select();
  259 + foreach ($configList as $k => $value) {
  260 + if (in_array($value['type'], ['selects', 'checkbox', 'images', 'files'])) {
  261 + $value['value'] = explode(',', $value['value']);
  262 + }
  263 + if ($value['type'] == 'array') {
  264 + $value['value'] = (array)json_decode($value['value'], true);
  265 + }
  266 + $siteConfig[$value['name']] = $value['value'];
  267 + }
  268 + $siteConfig['name'] = $siteName;
  269 + file_put_contents($siteConfigFile, '<?php' . "\n\nreturn " . var_export_short($siteConfig) . ";\n");
  270 + }
  271 +
  272 + $installLockFile = INSTALL_PATH . "install.lock";
  273 + //检测能否成功写入lock文件
  274 + $result = @file_put_contents($installLockFile, 1);
  275 + if (!$result) {
  276 + throw new Exception(__('The current permissions are insufficient to write the file %s', 'application/admin/command/Install/install.lock'));
  277 + }
  278 +
  279 + return $adminName;
  280 + }
  281 +
  282 + /**
  283 + * 检测环境
  284 + */
  285 + protected function checkenv()
  286 + {
  287 + // 检测目录是否存在
  288 + $checkDirs = [
  289 + 'thinkphp',
  290 + 'vendor',
  291 + 'public' . DS . 'assets' . DS . 'libs'
  292 + ];
  293 +
  294 + //数据库配置文件
  295 + $dbConfigFile = APP_PATH . 'database.php';
  296 +
  297 + if (version_compare(PHP_VERSION, '7.1.0', '<')) {
  298 + throw new Exception(__("The current version %s is too low, please use PHP 7.1 or higher", PHP_VERSION));
  299 + }
  300 + if (!extension_loaded("PDO")) {
  301 + throw new Exception(__("PDO is not currently installed and cannot be installed"));
  302 + }
  303 + if (!is_really_writable($dbConfigFile)) {
  304 + throw new Exception(__('The current permissions are insufficient to write the configuration file application/database.php'));
  305 + }
  306 + foreach ($checkDirs as $k => $v) {
  307 + if (!is_dir(ROOT_PATH . $v)) {
  308 + throw new Exception(__('Please go to the official website to download the full package or resource package and try to install'));
  309 + break;
  310 + }
  311 + }
  312 + return true;
  313 + }
  314 +}