正在显示
50 个修改的文件
包含
4732 行增加
和
0 行删除
.bowerrc
0 → 100644
.env.sample
0 → 100644
.gitignore
0 → 100644
LICENSE
0 → 100644
| 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. |
README.md
0 → 100644
| 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 | + | ||
| 53 | + | ||
| 54 | +## 问题反馈 | ||
| 55 | + | ||
| 56 | +在使用中有任何问题,请使用以下联系方式联系我们 | ||
| 57 | + | ||
| 58 | +交流社区: https://ask.fastadmin.net | ||
| 59 | + | ||
| 60 | +QQ 1 群(满)、QQ 2 群(满)、QQ 3 群(满)、QQ 4 群(满)、QQ 5 群(满)、QQ 6 群(满)、[QQ 7 群](https://www.fastadmin.net/goto/qun)。 | ||
| 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://layuion.com/layer/ | ||
| 85 | + | ||
| 86 | +DropzoneJS: https://www.dropzonejs.com | ||
| 87 | + | ||
| 88 | + | ||
| 89 | +## 版权信息 | ||
| 90 | + | ||
| 91 | +FastAdmin遵循Apache2开源协议发布,并提供免费使用。 | ||
| 92 | + | ||
| 93 | +本项目包含的第三方源码和二进制文件之版权信息另行标注。 | ||
| 94 | + | ||
| 95 | +版权所有Copyright © 2017-2022 by FastAdmin (https://www.fastadmin.net) | ||
| 96 | + | ||
| 97 | +All rights reserved。 |
addons/.gitkeep
0 → 100644
| 1 | + |
application/.htaccess
0 → 100644
| 1 | +deny from all |
application/admin/behavior/AdminLog.php
0 → 100644
application/admin/command/Addon.php
0 → 100644
| 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/uninstall/refresh/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('domain', 'd', Option::VALUE_OPTIONAL, 'domain', null) | ||
| 30 | + ->addOption('local', 'l', Option::VALUE_OPTIONAL, 'local package', null) | ||
| 31 | + ->setDescription('Addon manager'); | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + protected function execute(Input $input, Output $output) | ||
| 35 | + { | ||
| 36 | + $name = $input->getOption('name') ?: ''; | ||
| 37 | + $action = $input->getOption('action') ?: ''; | ||
| 38 | + if (stripos($name, 'addons' . DS) !== false) { | ||
| 39 | + $name = explode(DS, $name)[1]; | ||
| 40 | + } | ||
| 41 | + //强制覆盖 | ||
| 42 | + $force = $input->getOption('force'); | ||
| 43 | + //版本 | ||
| 44 | + $release = $input->getOption('release') ?: ''; | ||
| 45 | + //uid | ||
| 46 | + $uid = $input->getOption('uid') ?: ''; | ||
| 47 | + //token | ||
| 48 | + $token = $input->getOption('token') ?: ''; | ||
| 49 | + | ||
| 50 | + include dirname(__DIR__) . DS . 'common.php'; | ||
| 51 | + | ||
| 52 | + if (!$name && !in_array($action, ['refresh'])) { | ||
| 53 | + throw new Exception('Addon name could not be empty'); | ||
| 54 | + } | ||
| 55 | + if (!$action || !in_array($action, ['create', 'disable', 'enable', 'install', 'uninstall', 'refresh', 'upgrade', 'package', 'move'])) { | ||
| 56 | + throw new Exception('Please input correct action name'); | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + // 查询一次SQL,判断连接是否正常 | ||
| 60 | + Db::execute("SELECT 1"); | ||
| 61 | + | ||
| 62 | + $addonDir = ADDON_PATH . $name . DS; | ||
| 63 | + switch ($action) { | ||
| 64 | + case 'create': | ||
| 65 | + //非覆盖模式时如果存在则报错 | ||
| 66 | + if (is_dir($addonDir) && !$force) { | ||
| 67 | + throw new Exception("addon already exists!\nIf you need to create again, use the parameter --force=true "); | ||
| 68 | + } | ||
| 69 | + //如果存在先移除 | ||
| 70 | + if (is_dir($addonDir)) { | ||
| 71 | + rmdirs($addonDir); | ||
| 72 | + } | ||
| 73 | + mkdir($addonDir, 0755, true); | ||
| 74 | + mkdir($addonDir . DS . 'controller', 0755, true); | ||
| 75 | + $menuList = \app\common\library\Menu::export($name); | ||
| 76 | + $createMenu = $this->getCreateMenu($menuList); | ||
| 77 | + $prefix = Config::get('database.prefix'); | ||
| 78 | + $createTableSql = ''; | ||
| 79 | + try { | ||
| 80 | + $result = Db::query("SHOW CREATE TABLE `" . $prefix . $name . "`;"); | ||
| 81 | + if (isset($result[0]) && isset($result[0]['Create Table'])) { | ||
| 82 | + $createTableSql = $result[0]['Create Table']; | ||
| 83 | + } | ||
| 84 | + } catch (PDOException $e) { | ||
| 85 | + | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + $data = [ | ||
| 89 | + 'name' => $name, | ||
| 90 | + 'addon' => $name, | ||
| 91 | + 'addonClassName' => ucfirst($name), | ||
| 92 | + 'addonInstallMenu' => $createMenu ? "\$menu = " . var_export_short($createMenu) . ";\n\tMenu::create(\$menu);" : '', | ||
| 93 | + 'addonUninstallMenu' => $menuList ? 'Menu::delete("' . $name . '");' : '', | ||
| 94 | + 'addonEnableMenu' => $menuList ? 'Menu::enable("' . $name . '");' : '', | ||
| 95 | + 'addonDisableMenu' => $menuList ? 'Menu::disable("' . $name . '");' : '', | ||
| 96 | + ]; | ||
| 97 | + $this->writeToFile("addon", $data, $addonDir . ucfirst($name) . '.php'); | ||
| 98 | + $this->writeToFile("config", $data, $addonDir . 'config.php'); | ||
| 99 | + $this->writeToFile("info", $data, $addonDir . 'info.ini'); | ||
| 100 | + $this->writeToFile("controller", $data, $addonDir . 'controller' . DS . 'Index.php'); | ||
| 101 | + if ($createTableSql) { | ||
| 102 | + $createTableSql = str_replace("`" . $prefix, '`__PREFIX__', $createTableSql); | ||
| 103 | + file_put_contents($addonDir . 'install.sql', $createTableSql); | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + $output->info("Create Successed!"); | ||
| 107 | + break; | ||
| 108 | + case 'disable': | ||
| 109 | + case 'enable': | ||
| 110 | + try { | ||
| 111 | + //调用启用、禁用的方法 | ||
| 112 | + Service::$action($name, 0); | ||
| 113 | + } catch (AddonException $e) { | ||
| 114 | + if ($e->getCode() != -3) { | ||
| 115 | + throw new Exception($e->getMessage()); | ||
| 116 | + } | ||
| 117 | + if (!$force) { | ||
| 118 | + //如果有冲突文件则提醒 | ||
| 119 | + $data = $e->getData(); | ||
| 120 | + foreach ($data['conflictlist'] as $k => $v) { | ||
| 121 | + $output->warning($v); | ||
| 122 | + } | ||
| 123 | + $output->info("Are you sure you want to " . ($action == 'enable' ? 'override' : 'delete') . " all those files? Type 'yes' to continue: "); | ||
| 124 | + $line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r')); | ||
| 125 | + if (trim($line) != 'yes') { | ||
| 126 | + throw new Exception("Operation is aborted!"); | ||
| 127 | + } | ||
| 128 | + } | ||
| 129 | + //调用启用、禁用的方法 | ||
| 130 | + Service::$action($name, 1); | ||
| 131 | + } catch (Exception $e) { | ||
| 132 | + throw new Exception($e->getMessage()); | ||
| 133 | + } | ||
| 134 | + $output->info(ucfirst($action) . " Successed!"); | ||
| 135 | + break; | ||
| 136 | + case 'uninstall': | ||
| 137 | + //非覆盖模式时如果存在则报错 | ||
| 138 | + if (!$force) { | ||
| 139 | + throw new Exception("If you need to uninstall addon, use the parameter --force=true "); | ||
| 140 | + } | ||
| 141 | + try { | ||
| 142 | + Service::uninstall($name, 0); | ||
| 143 | + } catch (AddonException $e) { | ||
| 144 | + if ($e->getCode() != -3) { | ||
| 145 | + throw new Exception($e->getMessage()); | ||
| 146 | + } | ||
| 147 | + if (!$force) { | ||
| 148 | + //如果有冲突文件则提醒 | ||
| 149 | + $data = $e->getData(); | ||
| 150 | + foreach ($data['conflictlist'] as $k => $v) { | ||
| 151 | + $output->warning($v); | ||
| 152 | + } | ||
| 153 | + $output->info("Are you sure you want to delete all those files? Type 'yes' to continue: "); | ||
| 154 | + $line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r')); | ||
| 155 | + if (trim($line) != 'yes') { | ||
| 156 | + throw new Exception("Operation is aborted!"); | ||
| 157 | + } | ||
| 158 | + } | ||
| 159 | + Service::uninstall($name, 1); | ||
| 160 | + } catch (Exception $e) { | ||
| 161 | + throw new Exception($e->getMessage()); | ||
| 162 | + } | ||
| 163 | + | ||
| 164 | + $output->info("Uninstall Successed!"); | ||
| 165 | + break; | ||
| 166 | + case 'refresh': | ||
| 167 | + Service::refresh(); | ||
| 168 | + $output->info("Refresh Successed!"); | ||
| 169 | + break; | ||
| 170 | + case 'package': | ||
| 171 | + $infoFile = $addonDir . 'info.ini'; | ||
| 172 | + if (!is_file($infoFile)) { | ||
| 173 | + throw new Exception(__('Addon info file was not found')); | ||
| 174 | + } | ||
| 175 | + | ||
| 176 | + $info = get_addon_info($name); | ||
| 177 | + if (!$info) { | ||
| 178 | + throw new Exception(__('Addon info file data incorrect')); | ||
| 179 | + } | ||
| 180 | + $infoname = isset($info['name']) ? $info['name'] : ''; | ||
| 181 | + if (!$infoname || !preg_match("/^[a-z]+$/i", $infoname) || $infoname != $name) { | ||
| 182 | + throw new Exception(__('Addon info name incorrect')); | ||
| 183 | + } | ||
| 184 | + | ||
| 185 | + $infoversion = isset($info['version']) ? $info['version'] : ''; | ||
| 186 | + if (!$infoversion || !preg_match("/^\d+\.\d+\.\d+$/i", $infoversion)) { | ||
| 187 | + throw new Exception(__('Addon info version incorrect')); | ||
| 188 | + } | ||
| 189 | + | ||
| 190 | + $addonTmpDir = RUNTIME_PATH . 'addons' . DS; | ||
| 191 | + if (!is_dir($addonTmpDir)) { | ||
| 192 | + @mkdir($addonTmpDir, 0755, true); | ||
| 193 | + } | ||
| 194 | + $addonFile = $addonTmpDir . $infoname . '-' . $infoversion . '.zip'; | ||
| 195 | + if (!class_exists('ZipArchive')) { | ||
| 196 | + throw new Exception(__('ZinArchive not install')); | ||
| 197 | + } | ||
| 198 | + $zip = new \ZipArchive; | ||
| 199 | + $zip->open($addonFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE); | ||
| 200 | + | ||
| 201 | + $files = new \RecursiveIteratorIterator( | ||
| 202 | + new \RecursiveDirectoryIterator($addonDir), \RecursiveIteratorIterator::LEAVES_ONLY | ||
| 203 | + ); | ||
| 204 | + | ||
| 205 | + foreach ($files as $name => $file) { | ||
| 206 | + if (!$file->isDir()) { | ||
| 207 | + $filePath = $file->getRealPath(); | ||
| 208 | + $relativePath = str_replace(DS, '/', substr($filePath, strlen($addonDir))); | ||
| 209 | + if (!in_array($file->getFilename(), ['.git', '.DS_Store', 'Thumbs.db'])) { | ||
| 210 | + $zip->addFile($filePath, $relativePath); | ||
| 211 | + } | ||
| 212 | + } | ||
| 213 | + } | ||
| 214 | + $zip->close(); | ||
| 215 | + $output->info("Package Successed!"); | ||
| 216 | + break; | ||
| 217 | + case 'move': | ||
| 218 | + $movePath = [ | ||
| 219 | + 'adminOnlySelfDir' => ['admin/behavior', 'admin/controller', 'admin/library', 'admin/model', 'admin/validate', 'admin/view'], | ||
| 220 | + 'adminAllSubDir' => ['admin/lang'], | ||
| 221 | + 'publicDir' => ['public/assets/addons', 'public/assets/js/backend'] | ||
| 222 | + ]; | ||
| 223 | + $paths = []; | ||
| 224 | + $appPath = str_replace('/', DS, APP_PATH); | ||
| 225 | + $rootPath = str_replace('/', DS, ROOT_PATH); | ||
| 226 | + foreach ($movePath as $k => $items) { | ||
| 227 | + switch ($k) { | ||
| 228 | + case 'adminOnlySelfDir': | ||
| 229 | + foreach ($items as $v) { | ||
| 230 | + $v = str_replace('/', DS, $v); | ||
| 231 | + $oldPath = $appPath . $v . DS . $name; | ||
| 232 | + $newPath = $rootPath . "addons" . DS . $name . DS . "application" . DS . $v . DS . $name; | ||
| 233 | + $paths[$oldPath] = $newPath; | ||
| 234 | + } | ||
| 235 | + break; | ||
| 236 | + case 'adminAllSubDir': | ||
| 237 | + foreach ($items as $v) { | ||
| 238 | + $v = str_replace('/', DS, $v); | ||
| 239 | + $vPath = $appPath . $v; | ||
| 240 | + $list = scandir($vPath); | ||
| 241 | + foreach ($list as $_v) { | ||
| 242 | + if (!in_array($_v, ['.', '..']) && is_dir($vPath . DS . $_v)) { | ||
| 243 | + $oldPath = $appPath . $v . DS . $_v . DS . $name; | ||
| 244 | + $newPath = $rootPath . "addons" . DS . $name . DS . "application" . DS . $v . DS . $_v . DS . $name; | ||
| 245 | + $paths[$oldPath] = $newPath; | ||
| 246 | + } | ||
| 247 | + } | ||
| 248 | + } | ||
| 249 | + break; | ||
| 250 | + case 'publicDir': | ||
| 251 | + foreach ($items as $v) { | ||
| 252 | + $v = str_replace('/', DS, $v); | ||
| 253 | + $oldPath = $rootPath . $v . DS . $name; | ||
| 254 | + $newPath = $rootPath . 'addons' . DS . $name . DS . $v . DS . $name; | ||
| 255 | + $paths[$oldPath] = $newPath; | ||
| 256 | + } | ||
| 257 | + break; | ||
| 258 | + } | ||
| 259 | + } | ||
| 260 | + foreach ($paths as $oldPath => $newPath) { | ||
| 261 | + if (is_dir($oldPath)) { | ||
| 262 | + if ($force) { | ||
| 263 | + if (is_dir($newPath)) { | ||
| 264 | + $list = scandir($newPath); | ||
| 265 | + foreach ($list as $_v) { | ||
| 266 | + if (!in_array($_v, ['.', '..'])) { | ||
| 267 | + $file = $newPath . DS . $_v; | ||
| 268 | + @chmod($file, 0777); | ||
| 269 | + @unlink($file); | ||
| 270 | + } | ||
| 271 | + } | ||
| 272 | + @rmdir($newPath); | ||
| 273 | + } | ||
| 274 | + } | ||
| 275 | + copydirs($oldPath, $newPath); | ||
| 276 | + } | ||
| 277 | + } | ||
| 278 | + break; | ||
| 279 | + default: | ||
| 280 | + break; | ||
| 281 | + } | ||
| 282 | + } | ||
| 283 | + | ||
| 284 | + /** | ||
| 285 | + * 获取创建菜单的数组 | ||
| 286 | + * @param array $menu | ||
| 287 | + * @return array | ||
| 288 | + */ | ||
| 289 | + protected function getCreateMenu($menu) | ||
| 290 | + { | ||
| 291 | + $result = []; | ||
| 292 | + foreach ($menu as $k => & $v) { | ||
| 293 | + $arr = [ | ||
| 294 | + 'name' => $v['name'], | ||
| 295 | + 'title' => $v['title'], | ||
| 296 | + ]; | ||
| 297 | + if ($v['icon'] != 'fa fa-circle-o') { | ||
| 298 | + $arr['icon'] = $v['icon']; | ||
| 299 | + } | ||
| 300 | + if ($v['ismenu']) { | ||
| 301 | + $arr['ismenu'] = $v['ismenu']; | ||
| 302 | + } | ||
| 303 | + if (isset($v['childlist']) && $v['childlist']) { | ||
| 304 | + $arr['sublist'] = $this->getCreateMenu($v['childlist']); | ||
| 305 | + } | ||
| 306 | + $result[] = $arr; | ||
| 307 | + } | ||
| 308 | + return $result; | ||
| 309 | + } | ||
| 310 | + | ||
| 311 | + /** | ||
| 312 | + * 写入到文件 | ||
| 313 | + * @param string $name | ||
| 314 | + * @param array $data | ||
| 315 | + * @param string $pathname | ||
| 316 | + * @return mixed | ||
| 317 | + */ | ||
| 318 | + protected function writeToFile($name, $data, $pathname) | ||
| 319 | + { | ||
| 320 | + $search = $replace = []; | ||
| 321 | + foreach ($data as $k => $v) { | ||
| 322 | + $search[] = "{%{$k}%}"; | ||
| 323 | + $replace[] = $v; | ||
| 324 | + } | ||
| 325 | + $stub = file_get_contents($this->getStub($name)); | ||
| 326 | + $content = str_replace($search, $replace, $stub); | ||
| 327 | + | ||
| 328 | + if (!is_dir(dirname($pathname))) { | ||
| 329 | + mkdir(strtolower(dirname($pathname)), 0755, true); | ||
| 330 | + } | ||
| 331 | + return file_put_contents($pathname, $content); | ||
| 332 | + } | ||
| 333 | + | ||
| 334 | + /** | ||
| 335 | + * 获取基础模板 | ||
| 336 | + * @param string $name | ||
| 337 | + * @return string | ||
| 338 | + */ | ||
| 339 | + protected function getStub($name) | ||
| 340 | + { | ||
| 341 | + return __DIR__ . '/Addon/stubs/' . $name . '.stub'; | ||
| 342 | + } | ||
| 343 | + | ||
| 344 | +} |
| 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 | +} |
| 1 | +<?php | ||
| 2 | + | ||
| 3 | +return [ | ||
| 4 | + [ | ||
| 5 | + //配置唯一标识 | ||
| 6 | + 'name' => 'username', | ||
| 7 | + //显示的标题 | ||
| 8 | + 'title' => '用户名', | ||
| 9 | + //类型 | ||
| 10 | + 'type' => 'string', | ||
| 11 | + //分组 | ||
| 12 | + 'group' => '', | ||
| 13 | + //动态显示 | ||
| 14 | + 'visible' => '', | ||
| 15 | + //数据字典 | ||
| 16 | + 'content' => [ | ||
| 17 | + ], | ||
| 18 | + //值 | ||
| 19 | + 'value' => '', | ||
| 20 | + //验证规则 | ||
| 21 | + 'rule' => 'required', | ||
| 22 | + //错误消息 | ||
| 23 | + 'msg' => '', | ||
| 24 | + //提示消息 | ||
| 25 | + 'tip' => '', | ||
| 26 | + //成功消息 | ||
| 27 | + 'ok' => '', | ||
| 28 | + //扩展信息 | ||
| 29 | + 'extend' => '' | ||
| 30 | + ], | ||
| 31 | + [ | ||
| 32 | + 'name' => 'password', | ||
| 33 | + 'title' => '密码', | ||
| 34 | + 'type' => 'string', | ||
| 35 | + 'content' => [ | ||
| 36 | + ], | ||
| 37 | + 'value' => '', | ||
| 38 | + 'rule' => 'required', | ||
| 39 | + 'msg' => '', | ||
| 40 | + 'tip' => '', | ||
| 41 | + 'ok' => '', | ||
| 42 | + 'extend' => '' | ||
| 43 | + ], | ||
| 44 | +]; |
application/admin/command/Api.php
0 → 100644
| 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->getClassFromFile($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->getClassFromFile($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 | + * 从文件获取命名空间和类名 | ||
| 133 | + * | ||
| 134 | + * @param string $filename | ||
| 135 | + * @return string | ||
| 136 | + */ | ||
| 137 | + protected function getClassFromFile($filename) | ||
| 138 | + { | ||
| 139 | + $getNext = null; | ||
| 140 | + $isNamespace = false; | ||
| 141 | + $skipNext = false; | ||
| 142 | + $namespace = ''; | ||
| 143 | + $class = ''; | ||
| 144 | + foreach (\PhpToken::tokenize(file_get_contents($filename)) as $token) { | ||
| 145 | + if (!$token->isIgnorable()) { | ||
| 146 | + $name = $token->getTokenName(); | ||
| 147 | + switch ($name) { | ||
| 148 | + case 'T_NAMESPACE': | ||
| 149 | + $isNamespace = true; | ||
| 150 | + break; | ||
| 151 | + case 'T_EXTENDS': | ||
| 152 | + case 'T_USE': | ||
| 153 | + case 'T_IMPLEMENTS': | ||
| 154 | + $skipNext = true; | ||
| 155 | + break; | ||
| 156 | + case 'T_CLASS': | ||
| 157 | + if ($skipNext) { | ||
| 158 | + $skipNext = false; | ||
| 159 | + } else { | ||
| 160 | + $getNext = strtolower(substr($name, 2)); | ||
| 161 | + } | ||
| 162 | + break; | ||
| 163 | + case 'T_NAME_QUALIFIED': | ||
| 164 | + case 'T_NS_SEPARATOR': | ||
| 165 | + case 'T_STRING': | ||
| 166 | + case ';': | ||
| 167 | + if ($isNamespace) { | ||
| 168 | + if ($name == ';') { | ||
| 169 | + $isNamespace = false; | ||
| 170 | + } else { | ||
| 171 | + $namespace .= $token->text; | ||
| 172 | + } | ||
| 173 | + } elseif ($skipNext) { | ||
| 174 | + $skipNext = false; | ||
| 175 | + } elseif ($getNext == 'class') { | ||
| 176 | + $class = $token->text; | ||
| 177 | + $getNext = null; | ||
| 178 | + break 2; | ||
| 179 | + } | ||
| 180 | + break; | ||
| 181 | + default: | ||
| 182 | + $getNext = null; | ||
| 183 | + } | ||
| 184 | + } | ||
| 185 | + } | ||
| 186 | + | ||
| 187 | + return $namespace . '\\' . $class; | ||
| 188 | + } | ||
| 189 | +} |
application/admin/command/Api/lang/zh-cn.php
0 → 100644
| 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 <karson@fastadmin.net> | ||
| 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 | + $typeArr = [ | ||
| 92 | + 'integer' => 'number', | ||
| 93 | + 'file' => 'file', | ||
| 94 | + ]; | ||
| 95 | + $paramslist = array(); | ||
| 96 | + foreach ($docs['ApiParams'] as $params) { | ||
| 97 | + $inputtype = $params['type'] && isset($typeArr[$params['type']]) ? $typeArr[$params['type']] : ($params['name'] == 'password' ? 'password' : 'text'); | ||
| 98 | + $tr = array( | ||
| 99 | + 'name' => $params['name'], | ||
| 100 | + 'type' => $params['type'] ?? 'string', | ||
| 101 | + 'inputtype' => $inputtype, | ||
| 102 | + 'sample' => $params['sample'] ?? '', | ||
| 103 | + 'required' => $params['required'] ?? true, | ||
| 104 | + 'description' => $params['description'] ?? '', | ||
| 105 | + ); | ||
| 106 | + $paramslist[] = $tr; | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + return $paramslist; | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + protected function generateReturnHeadersTemplate($docs) | ||
| 113 | + { | ||
| 114 | + if (!isset($docs['ApiReturnHeaders'])) { | ||
| 115 | + return []; | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + $headerslist = array(); | ||
| 119 | + foreach ($docs['ApiReturnHeaders'] as $params) { | ||
| 120 | + $tr = array( | ||
| 121 | + 'name' => $params['name'] ?? '', | ||
| 122 | + 'type' => 'string', | ||
| 123 | + 'sample' => $params['sample'] ?? '', | ||
| 124 | + 'required' => isset($params['required']) && $params['required'] ? 'Yes' : 'No', | ||
| 125 | + 'description' => $params['description'] ?? '', | ||
| 126 | + ); | ||
| 127 | + $headerslist[] = $tr; | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + return $headerslist; | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + protected function generateReturnParamsTemplate($st_params) | ||
| 134 | + { | ||
| 135 | + if (!isset($st_params['ApiReturnParams'])) { | ||
| 136 | + return []; | ||
| 137 | + } | ||
| 138 | + | ||
| 139 | + $paramslist = array(); | ||
| 140 | + foreach ($st_params['ApiReturnParams'] as $params) { | ||
| 141 | + $tr = array( | ||
| 142 | + 'name' => $params['name'] ?? '', | ||
| 143 | + 'type' => $params['type'] ?? 'string', | ||
| 144 | + 'sample' => $params['sample'] ?? '', | ||
| 145 | + 'description' => $params['description'] ?? '', | ||
| 146 | + ); | ||
| 147 | + $paramslist[] = $tr; | ||
| 148 | + } | ||
| 149 | + | ||
| 150 | + return $paramslist; | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | + protected function generateBadgeForMethod($data) | ||
| 154 | + { | ||
| 155 | + $method = strtoupper(is_array($data['ApiMethod'][0]) ? $data['ApiMethod'][0]['data'] : $data['ApiMethod'][0]); | ||
| 156 | + $labes = array( | ||
| 157 | + 'POST' => 'label-primary', | ||
| 158 | + 'GET' => 'label-success', | ||
| 159 | + 'PUT' => 'label-warning', | ||
| 160 | + 'DELETE' => 'label-danger', | ||
| 161 | + 'PATCH' => 'label-default', | ||
| 162 | + 'OPTIONS' => 'label-info' | ||
| 163 | + ); | ||
| 164 | + | ||
| 165 | + return isset($labes[$method]) ? $labes[$method] : $labes['GET']; | ||
| 166 | + } | ||
| 167 | + | ||
| 168 | + public function parse() | ||
| 169 | + { | ||
| 170 | + list($allClassAnnotations, $allClassMethodAnnotations) = $this->extractAnnotations(); | ||
| 171 | + | ||
| 172 | + $sectorArr = []; | ||
| 173 | + foreach ($allClassAnnotations as $index => &$allClassAnnotation) { | ||
| 174 | + // 如果设置隐藏,则不显示在文档 | ||
| 175 | + if (isset($allClassAnnotation['ApiInternal'])) { | ||
| 176 | + continue; | ||
| 177 | + } | ||
| 178 | + $sector = isset($allClassAnnotation['ApiSector']) ? $allClassAnnotation['ApiSector'][0] : $allClassAnnotation['ApiTitle'][0]; | ||
| 179 | + $sectorArr[$sector] = isset($allClassAnnotation['ApiWeigh']) ? $allClassAnnotation['ApiWeigh'][0] : 0; | ||
| 180 | + } | ||
| 181 | + unset($allClassAnnotation); | ||
| 182 | + | ||
| 183 | + arsort($sectorArr); | ||
| 184 | + $routes = include_once CONF_PATH . 'route.php'; | ||
| 185 | + $subdomain = false; | ||
| 186 | + if (config('url_domain_deploy') && isset($routes['__domain__']) && isset($routes['__domain__']['api']) && $routes['__domain__']['api']) { | ||
| 187 | + $subdomain = true; | ||
| 188 | + } | ||
| 189 | + $counter = 0; | ||
| 190 | + $section = null; | ||
| 191 | + $weigh = 0; | ||
| 192 | + $docsList = []; | ||
| 193 | + foreach ($allClassMethodAnnotations as $class => $methods) { | ||
| 194 | + foreach ($methods as $name => $docs) { | ||
| 195 | + if (isset($docs['ApiSector'][0])) { | ||
| 196 | + $section = is_array($docs['ApiSector'][0]) ? $docs['ApiSector'][0]['data'] : $docs['ApiSector'][0]; | ||
| 197 | + } else { | ||
| 198 | + $section = $class; | ||
| 199 | + } | ||
| 200 | + if (0 === count($docs)) { | ||
| 201 | + continue; | ||
| 202 | + } | ||
| 203 | + $route = is_array($docs['ApiRoute'][0]) ? $docs['ApiRoute'][0]['data'] : $docs['ApiRoute'][0]; | ||
| 204 | + if ($subdomain) { | ||
| 205 | + $route = substr($route, 4); | ||
| 206 | + } | ||
| 207 | + $docsList[$section][$name] = [ | ||
| 208 | + 'id' => $counter, | ||
| 209 | + 'method' => is_array($docs['ApiMethod'][0]) ? $docs['ApiMethod'][0]['data'] : $docs['ApiMethod'][0], | ||
| 210 | + 'methodLabel' => $this->generateBadgeForMethod($docs), | ||
| 211 | + 'section' => $section, | ||
| 212 | + 'route' => $route, | ||
| 213 | + 'title' => is_array($docs['ApiTitle'][0]) ? $docs['ApiTitle'][0]['data'] : $docs['ApiTitle'][0], | ||
| 214 | + 'summary' => is_array($docs['ApiSummary'][0]) ? $docs['ApiSummary'][0]['data'] : $docs['ApiSummary'][0], | ||
| 215 | + 'body' => isset($docs['ApiBody'][0]) ? (is_array($docs['ApiBody'][0]) ? $docs['ApiBody'][0]['data'] : $docs['ApiBody'][0]) : '', | ||
| 216 | + 'headersList' => $this->generateHeadersTemplate($docs), | ||
| 217 | + 'paramsList' => $this->generateParamsTemplate($docs), | ||
| 218 | + 'returnHeadersList' => $this->generateReturnHeadersTemplate($docs), | ||
| 219 | + 'returnParamsList' => $this->generateReturnParamsTemplate($docs), | ||
| 220 | + 'weigh' => is_array($docs['ApiWeigh'][0]) ? $docs['ApiWeigh'][0]['data'] : $docs['ApiWeigh'][0], | ||
| 221 | + 'return' => isset($docs['ApiReturn']) ? (is_array($docs['ApiReturn'][0]) ? $docs['ApiReturn'][0]['data'] : $docs['ApiReturn'][0]) : '', | ||
| 222 | + 'needLogin' => $docs['ApiPermissionLogin'][0], | ||
| 223 | + 'needRight' => $docs['ApiPermissionRight'][0], | ||
| 224 | + ]; | ||
| 225 | + $counter++; | ||
| 226 | + } | ||
| 227 | + } | ||
| 228 | + | ||
| 229 | + //重建排序 | ||
| 230 | + foreach ($docsList as $index => &$methods) { | ||
| 231 | + $methodSectorArr = []; | ||
| 232 | + foreach ($methods as $name => $method) { | ||
| 233 | + $methodSectorArr[$name] = isset($method['weigh']) ? $method['weigh'] : 0; | ||
| 234 | + } | ||
| 235 | + arsort($methodSectorArr); | ||
| 236 | + $methods = array_merge(array_flip(array_keys($methodSectorArr)), $methods); | ||
| 237 | + } | ||
| 238 | + $docsList = array_merge(array_flip(array_keys($sectorArr)), $docsList); | ||
| 239 | + return $docsList; | ||
| 240 | + } | ||
| 241 | + | ||
| 242 | + public function getView() | ||
| 243 | + { | ||
| 244 | + return $this->view; | ||
| 245 | + } | ||
| 246 | + | ||
| 247 | + /** | ||
| 248 | + * 渲染 | ||
| 249 | + * @param string $template | ||
| 250 | + * @param array $vars | ||
| 251 | + * @return string | ||
| 252 | + */ | ||
| 253 | + public function render($template, $vars = []) | ||
| 254 | + { | ||
| 255 | + $docsList = $this->parse(); | ||
| 256 | + | ||
| 257 | + return $this->view->display(file_get_contents($template), array_merge($vars, ['docsList' => $docsList])); | ||
| 258 | + } | ||
| 259 | +} |
| 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> | ||
| 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.example.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.inputtype|default='text'}" 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.inputtype|default='text'}" 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, '&').replace(/</g, '<').replace(/>/g, '>'); | ||
| 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> |
application/admin/command/Crud.php
0 → 100644
| 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\exception\PDOException; | ||
| 15 | +use think\Lang; | ||
| 16 | +use think\Loader; | ||
| 17 | + | ||
| 18 | +class Crud extends Command | ||
| 19 | +{ | ||
| 20 | + protected $stubList = []; | ||
| 21 | + | ||
| 22 | + protected $internalKeywords = [ | ||
| 23 | + 'abstract', | ||
| 24 | + 'and', | ||
| 25 | + 'array', | ||
| 26 | + 'as', | ||
| 27 | + 'break', | ||
| 28 | + 'callable', | ||
| 29 | + 'case', | ||
| 30 | + 'catch', | ||
| 31 | + 'class', | ||
| 32 | + 'clone', | ||
| 33 | + 'const', | ||
| 34 | + 'continue', | ||
| 35 | + 'declare', | ||
| 36 | + 'default', | ||
| 37 | + 'die', | ||
| 38 | + 'do', | ||
| 39 | + 'echo', | ||
| 40 | + 'else', | ||
| 41 | + 'elseif', | ||
| 42 | + 'empty', | ||
| 43 | + 'enddeclare', | ||
| 44 | + 'endfor', | ||
| 45 | + 'endforeach', | ||
| 46 | + 'endif', | ||
| 47 | + 'endswitch', | ||
| 48 | + 'endwhile', | ||
| 49 | + 'eval', | ||
| 50 | + 'exit', | ||
| 51 | + 'extends', | ||
| 52 | + 'final', | ||
| 53 | + 'for', | ||
| 54 | + 'foreach', | ||
| 55 | + 'function', | ||
| 56 | + 'global', | ||
| 57 | + 'goto', | ||
| 58 | + 'if', | ||
| 59 | + 'implements', | ||
| 60 | + 'include', | ||
| 61 | + 'include_once', | ||
| 62 | + 'instanceof', | ||
| 63 | + 'insteadof', | ||
| 64 | + 'interface', | ||
| 65 | + 'isset', | ||
| 66 | + 'list', | ||
| 67 | + 'namespace', | ||
| 68 | + 'new', | ||
| 69 | + 'or', | ||
| 70 | + 'print', | ||
| 71 | + 'private', | ||
| 72 | + 'protected', | ||
| 73 | + 'public', | ||
| 74 | + 'require', | ||
| 75 | + 'require_once', | ||
| 76 | + 'return', | ||
| 77 | + 'static', | ||
| 78 | + 'switch', | ||
| 79 | + 'throw', | ||
| 80 | + 'trait', | ||
| 81 | + 'try', | ||
| 82 | + 'unset', | ||
| 83 | + 'use', | ||
| 84 | + 'var', | ||
| 85 | + 'while', | ||
| 86 | + 'xor' | ||
| 87 | + ]; | ||
| 88 | + | ||
| 89 | + /** | ||
| 90 | + * 受保护的系统表, crud不会生效 | ||
| 91 | + */ | ||
| 92 | + protected $systemTables = [ | ||
| 93 | + 'admin', | ||
| 94 | + 'admin_log', | ||
| 95 | + 'auth_group', | ||
| 96 | + 'auth_group_access', | ||
| 97 | + 'auth_rule', | ||
| 98 | + 'attachment', | ||
| 99 | + 'config', | ||
| 100 | + 'category', | ||
| 101 | + 'ems', | ||
| 102 | + 'sms', | ||
| 103 | + 'user', | ||
| 104 | + 'user_group', | ||
| 105 | + 'user_rule', | ||
| 106 | + 'user_score_log', | ||
| 107 | + 'user_token', | ||
| 108 | + ]; | ||
| 109 | + | ||
| 110 | + /** | ||
| 111 | + * Selectpage搜索字段关联 | ||
| 112 | + */ | ||
| 113 | + protected $fieldSelectpageMap = [ | ||
| 114 | + 'nickname' => ['user_id', 'user_ids', 'admin_id', 'admin_ids'] | ||
| 115 | + ]; | ||
| 116 | + | ||
| 117 | + /** | ||
| 118 | + * Enum类型识别为单选框的结尾字符,默认会识别为单选下拉列表 | ||
| 119 | + */ | ||
| 120 | + protected $enumRadioSuffix = ['data', 'state', 'status']; | ||
| 121 | + | ||
| 122 | + /** | ||
| 123 | + * Set类型识别为复选框的结尾字符,默认会识别为多选下拉列表 | ||
| 124 | + */ | ||
| 125 | + protected $setCheckboxSuffix = ['data', 'state', 'status']; | ||
| 126 | + | ||
| 127 | + /** | ||
| 128 | + * Int类型识别为日期时间的结尾字符,默认会识别为日期文本框 | ||
| 129 | + */ | ||
| 130 | + protected $intDateSuffix = ['time']; | ||
| 131 | + | ||
| 132 | + /** | ||
| 133 | + * 开关后缀 | ||
| 134 | + */ | ||
| 135 | + protected $switchSuffix = ['switch']; | ||
| 136 | + | ||
| 137 | + /** | ||
| 138 | + * 富文本后缀 | ||
| 139 | + */ | ||
| 140 | + protected $editorSuffix = ['content']; | ||
| 141 | + | ||
| 142 | + /** | ||
| 143 | + * 城市后缀 | ||
| 144 | + */ | ||
| 145 | + protected $citySuffix = ['city']; | ||
| 146 | + | ||
| 147 | + /** | ||
| 148 | + * 时间区间后缀 | ||
| 149 | + */ | ||
| 150 | + protected $rangeSuffix = ['range']; | ||
| 151 | + | ||
| 152 | + /** | ||
| 153 | + * JSON后缀 | ||
| 154 | + */ | ||
| 155 | + protected $jsonSuffix = ['json']; | ||
| 156 | + | ||
| 157 | + /** | ||
| 158 | + * 标签后缀 | ||
| 159 | + */ | ||
| 160 | + protected $tagSuffix = ['tag', 'tags']; | ||
| 161 | + | ||
| 162 | + /** | ||
| 163 | + * Selectpage对应的后缀 | ||
| 164 | + */ | ||
| 165 | + protected $selectpageSuffix = ['_id', '_ids']; | ||
| 166 | + | ||
| 167 | + /** | ||
| 168 | + * Selectpage多选对应的后缀 | ||
| 169 | + */ | ||
| 170 | + protected $selectpagesSuffix = ['_ids']; | ||
| 171 | + | ||
| 172 | + /** | ||
| 173 | + * 以指定字符结尾的字段格式化函数 | ||
| 174 | + */ | ||
| 175 | + protected $fieldFormatterSuffix = [ | ||
| 176 | + 'status' => ['type' => ['varchar', 'enum'], 'name' => 'status'], | ||
| 177 | + 'icon' => 'icon', | ||
| 178 | + 'flag' => 'flag', | ||
| 179 | + 'url' => 'url', | ||
| 180 | + 'image' => 'image', | ||
| 181 | + 'images' => 'images', | ||
| 182 | + 'file' => 'file', | ||
| 183 | + 'files' => 'files', | ||
| 184 | + 'avatar' => 'image', | ||
| 185 | + 'switch' => 'toggle', | ||
| 186 | + 'tag' => 'flag', | ||
| 187 | + 'tags' => 'flag', | ||
| 188 | + 'time' => ['type' => ['int', 'bigint', 'timestamp'], 'name' => 'datetime'], | ||
| 189 | + ]; | ||
| 190 | + | ||
| 191 | + /** | ||
| 192 | + * 识别为图片字段 | ||
| 193 | + */ | ||
| 194 | + protected $imageField = ['image', 'images', 'avatar', 'avatars']; | ||
| 195 | + | ||
| 196 | + /** | ||
| 197 | + * 识别为文件字段 | ||
| 198 | + */ | ||
| 199 | + protected $fileField = ['file', 'files']; | ||
| 200 | + | ||
| 201 | + /** | ||
| 202 | + * 保留字段 | ||
| 203 | + */ | ||
| 204 | + protected $reservedField = ['admin_id']; | ||
| 205 | + | ||
| 206 | + /** | ||
| 207 | + * 排除字段 | ||
| 208 | + */ | ||
| 209 | + protected $ignoreFields = []; | ||
| 210 | + | ||
| 211 | + /** | ||
| 212 | + * 排序字段 | ||
| 213 | + */ | ||
| 214 | + protected $sortField = 'weigh'; | ||
| 215 | + | ||
| 216 | + /** | ||
| 217 | + * 筛选字段 | ||
| 218 | + * @var string | ||
| 219 | + */ | ||
| 220 | + protected $headingFilterField = 'status'; | ||
| 221 | + | ||
| 222 | + /** | ||
| 223 | + * 添加时间字段 | ||
| 224 | + * @var string | ||
| 225 | + */ | ||
| 226 | + protected $createTimeField = 'createtime'; | ||
| 227 | + | ||
| 228 | + /** | ||
| 229 | + * 更新时间字段 | ||
| 230 | + * @var string | ||
| 231 | + */ | ||
| 232 | + protected $updateTimeField = 'updatetime'; | ||
| 233 | + | ||
| 234 | + /** | ||
| 235 | + * 软删除时间字段 | ||
| 236 | + * @var string | ||
| 237 | + */ | ||
| 238 | + protected $deleteTimeField = 'deletetime'; | ||
| 239 | + | ||
| 240 | + /** | ||
| 241 | + * 编辑器的Class | ||
| 242 | + */ | ||
| 243 | + protected $editorClass = 'editor'; | ||
| 244 | + | ||
| 245 | + /** | ||
| 246 | + * langList的key最长字节数 | ||
| 247 | + */ | ||
| 248 | + protected $fieldMaxLen = 0; | ||
| 249 | + | ||
| 250 | + protected function configure() | ||
| 251 | + { | ||
| 252 | + $this | ||
| 253 | + ->setName('crud') | ||
| 254 | + ->addOption('table', 't', Option::VALUE_REQUIRED, 'table name without prefix', null) | ||
| 255 | + ->addOption('controller', 'c', Option::VALUE_OPTIONAL, 'controller name', null) | ||
| 256 | + ->addOption('model', 'm', Option::VALUE_OPTIONAL, 'model name', null) | ||
| 257 | + ->addOption('fields', 'i', Option::VALUE_OPTIONAL, 'model visible fields', null) | ||
| 258 | + ->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override or force delete,without tips', null) | ||
| 259 | + ->addOption('local', 'l', Option::VALUE_OPTIONAL, 'local model', 1) | ||
| 260 | + ->addOption('import', 'a', Option::VALUE_OPTIONAL, 'enable import function', 0) | ||
| 261 | + ->addOption('relation', 'r', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation table name without prefix', null) | ||
| 262 | + ->addOption('relationmodel', 'e', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation model name', null) | ||
| 263 | + ->addOption('relationforeignkey', 'k', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation foreign key', null) | ||
| 264 | + ->addOption('relationprimarykey', 'p', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation primary key', null) | ||
| 265 | + ->addOption('relationfields', 's', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation table fields', null) | ||
| 266 | + ->addOption('relationmode', 'o', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation table mode,hasone/belongsto/hasmany', null) | ||
| 267 | + ->addOption('relationcontroller', 'w', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation table controller,only work at hasmany mode', null) | ||
| 268 | + ->addOption('delete', 'd', Option::VALUE_OPTIONAL, 'delete all files generated by CRUD', null) | ||
| 269 | + ->addOption('menu', 'u', Option::VALUE_OPTIONAL, 'create menu when CRUD completed', null) | ||
| 270 | + ->addOption('setcheckboxsuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate checkbox component with suffix', null) | ||
| 271 | + ->addOption('enumradiosuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate radio component with suffix', null) | ||
| 272 | + ->addOption('imagefield', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate image component with suffix', null) | ||
| 273 | + ->addOption('filefield', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate file component with suffix', null) | ||
| 274 | + ->addOption('intdatesuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate date component with suffix', null) | ||
| 275 | + ->addOption('switchsuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate switch component with suffix', null) | ||
| 276 | + ->addOption('citysuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate citypicker component with suffix', null) | ||
| 277 | + ->addOption('jsonsuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate fieldlist component with suffix', null) | ||
| 278 | + ->addOption('tagsuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate tag component with suffix', null) | ||
| 279 | + ->addOption('editorsuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate editor component with suffix', null) | ||
| 280 | + ->addOption('selectpagesuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate selectpage component with suffix', null) | ||
| 281 | + ->addOption('selectpagessuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate multiple selectpage component with suffix', null) | ||
| 282 | + ->addOption('ignorefields', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'ignore fields', null) | ||
| 283 | + ->addOption('sortfield', null, Option::VALUE_OPTIONAL, 'sort field', null) | ||
| 284 | + ->addOption('headingfilterfield', null, Option::VALUE_OPTIONAL, 'heading filter field', null) | ||
| 285 | + ->addOption('fixedcolumns', null, Option::VALUE_OPTIONAL, 'fixed columns', null) | ||
| 286 | + ->addOption('editorclass', null, Option::VALUE_OPTIONAL, 'automatically generate editor class', null) | ||
| 287 | + ->addOption('db', null, Option::VALUE_OPTIONAL, 'database config name', 'database') | ||
| 288 | + ->setDescription('Build CRUD controller and model from table'); | ||
| 289 | + } | ||
| 290 | + | ||
| 291 | + protected function execute(Input $input, Output $output) | ||
| 292 | + { | ||
| 293 | + $adminPath = dirname(__DIR__) . DS; | ||
| 294 | + //数据库 | ||
| 295 | + $db = $input->getOption('db'); | ||
| 296 | + //表名 | ||
| 297 | + $table = $input->getOption('table') ?: ''; | ||
| 298 | + //自定义控制器 | ||
| 299 | + $controller = $input->getOption('controller'); | ||
| 300 | + //自定义模型 | ||
| 301 | + $model = $input->getOption('model'); | ||
| 302 | + $model = $model ? $model : $controller; | ||
| 303 | + //验证器类 | ||
| 304 | + $validate = $model; | ||
| 305 | + //自定义显示字段 | ||
| 306 | + $fields = $input->getOption('fields'); | ||
| 307 | + //强制覆盖 | ||
| 308 | + $force = $input->getOption('force'); | ||
| 309 | + //是否为本地model,为0时表示为全局model将会把model放在app/common/model中 | ||
| 310 | + $local = $input->getOption('local'); | ||
| 311 | + //是否启用导入功能 | ||
| 312 | + $import = $input->getOption('import'); | ||
| 313 | + | ||
| 314 | + if (!$table) { | ||
| 315 | + throw new Exception('table name can\'t empty'); | ||
| 316 | + } | ||
| 317 | + | ||
| 318 | + | ||
| 319 | + //是否生成菜单 | ||
| 320 | + $menu = $input->getOption("menu"); | ||
| 321 | + //关联表 | ||
| 322 | + $relation = $input->getOption('relation'); | ||
| 323 | + //自定义关联表模型 | ||
| 324 | + $relationModels = $input->getOption('relationmodel'); | ||
| 325 | + //模式 | ||
| 326 | + $relationMode = $mode = $input->getOption('relationmode'); | ||
| 327 | + //外键 | ||
| 328 | + $relationForeignKey = $input->getOption('relationforeignkey'); | ||
| 329 | + //主键 | ||
| 330 | + $relationPrimaryKey = $input->getOption('relationprimarykey'); | ||
| 331 | + //关联表显示字段 | ||
| 332 | + $relationFields = $input->getOption('relationfields'); | ||
| 333 | + //关联表显示字段 | ||
| 334 | + $relationController = $input->getOption('relationcontroller'); | ||
| 335 | + //复选框后缀 | ||
| 336 | + $setcheckboxsuffix = $input->getOption('setcheckboxsuffix'); | ||
| 337 | + //单选框后缀 | ||
| 338 | + $enumradiosuffix = $input->getOption('enumradiosuffix'); | ||
| 339 | + //图片后缀 | ||
| 340 | + $imagefield = $input->getOption('imagefield'); | ||
| 341 | + //文件后缀 | ||
| 342 | + $filefield = $input->getOption('filefield'); | ||
| 343 | + //标签后缀 | ||
| 344 | + $tagsuffix = $input->getOption('tagsuffix'); | ||
| 345 | + //日期后缀 | ||
| 346 | + $intdatesuffix = $input->getOption('intdatesuffix'); | ||
| 347 | + //开关后缀 | ||
| 348 | + $switchsuffix = $input->getOption('switchsuffix'); | ||
| 349 | + //富文本编辑器 | ||
| 350 | + $editorsuffix = $input->getOption('editorsuffix'); | ||
| 351 | + //城市后缀 | ||
| 352 | + $citysuffix = $input->getOption('citysuffix'); | ||
| 353 | + //JSON配置后缀 | ||
| 354 | + $jsonsuffix = $input->getOption('jsonsuffix'); | ||
| 355 | + //selectpage后缀 | ||
| 356 | + $selectpagesuffix = $input->getOption('selectpagesuffix'); | ||
| 357 | + //selectpage多选后缀 | ||
| 358 | + $selectpagessuffix = $input->getOption('selectpagessuffix'); | ||
| 359 | + //排除字段 | ||
| 360 | + $ignoreFields = $input->getOption('ignorefields'); | ||
| 361 | + //排序字段 | ||
| 362 | + $sortfield = $input->getOption('sortfield'); | ||
| 363 | + //顶部筛选过滤字段 | ||
| 364 | + $headingfilterfield = $input->getOption('headingfilterfield'); | ||
| 365 | + //固定列数量 | ||
| 366 | + $fixedcolumns = $input->getOption('fixedcolumns'); | ||
| 367 | + //编辑器Class | ||
| 368 | + $editorclass = $input->getOption('editorclass'); | ||
| 369 | + if ($setcheckboxsuffix) { | ||
| 370 | + $this->setCheckboxSuffix = $setcheckboxsuffix; | ||
| 371 | + } | ||
| 372 | + if ($enumradiosuffix) { | ||
| 373 | + $this->enumRadioSuffix = $enumradiosuffix; | ||
| 374 | + } | ||
| 375 | + if ($imagefield) { | ||
| 376 | + $this->imageField = $imagefield; | ||
| 377 | + } | ||
| 378 | + if ($filefield) { | ||
| 379 | + $this->fileField = $filefield; | ||
| 380 | + } | ||
| 381 | + if ($tagsuffix) { | ||
| 382 | + $this->tagSuffix = $tagsuffix; | ||
| 383 | + } | ||
| 384 | + if ($intdatesuffix) { | ||
| 385 | + $this->intDateSuffix = $intdatesuffix; | ||
| 386 | + } | ||
| 387 | + if ($switchsuffix) { | ||
| 388 | + $this->switchSuffix = $switchsuffix; | ||
| 389 | + } | ||
| 390 | + if ($editorsuffix) { | ||
| 391 | + $this->editorSuffix = $editorsuffix; | ||
| 392 | + } | ||
| 393 | + if ($citysuffix) { | ||
| 394 | + $this->citySuffix = $citysuffix; | ||
| 395 | + } | ||
| 396 | + if ($jsonsuffix) { | ||
| 397 | + $this->jsonSuffix = $jsonsuffix; | ||
| 398 | + } | ||
| 399 | + if ($selectpagesuffix) { | ||
| 400 | + $this->selectpageSuffix = $selectpagesuffix; | ||
| 401 | + } | ||
| 402 | + if ($selectpagessuffix) { | ||
| 403 | + $this->selectpagesSuffix = $selectpagessuffix; | ||
| 404 | + } | ||
| 405 | + if ($ignoreFields) { | ||
| 406 | + $this->ignoreFields = $ignoreFields; | ||
| 407 | + } | ||
| 408 | + if ($editorclass) { | ||
| 409 | + $this->editorClass = $editorclass; | ||
| 410 | + } | ||
| 411 | + if ($sortfield) { | ||
| 412 | + $this->sortField = $sortfield; | ||
| 413 | + } | ||
| 414 | + if ($headingfilterfield) { | ||
| 415 | + $this->headingFilterField = $headingfilterfield; | ||
| 416 | + } | ||
| 417 | + | ||
| 418 | + $this->reservedField = array_merge($this->reservedField, [$this->createTimeField, $this->updateTimeField, $this->deleteTimeField]); | ||
| 419 | + | ||
| 420 | + $dbconnect = Db::connect($db); | ||
| 421 | + $dbname = Config::get($db . '.database'); | ||
| 422 | + $prefix = Config::get($db . '.prefix'); | ||
| 423 | + | ||
| 424 | + //系统表无法生成,防止后台错乱 | ||
| 425 | + if (in_array(str_replace($prefix, "", $table), $this->systemTables)) { | ||
| 426 | + throw new Exception('system table can\'t be crud'); | ||
| 427 | + } | ||
| 428 | + | ||
| 429 | + //模块 | ||
| 430 | + $moduleName = 'admin'; | ||
| 431 | + $modelModuleName = $local ? $moduleName : 'common'; | ||
| 432 | + $validateModuleName = $local ? $moduleName : 'common'; | ||
| 433 | + | ||
| 434 | + //检查主表 | ||
| 435 | + $modelName = $table = stripos($table, $prefix) === 0 ? substr($table, strlen($prefix)) : $table; | ||
| 436 | + $modelTableType = 'table'; | ||
| 437 | + $modelTableTypeName = $modelTableName = $modelName; | ||
| 438 | + $modelTableInfo = $dbconnect->query("SHOW TABLE STATUS LIKE '{$modelTableName}'", [], true); | ||
| 439 | + if (!$modelTableInfo) { | ||
| 440 | + $modelTableType = 'name'; | ||
| 441 | + $modelTableName = $prefix . $modelName; | ||
| 442 | + $modelTableInfo = $dbconnect->query("SHOW TABLE STATUS LIKE '{$modelTableName}'", [], true); | ||
| 443 | + if (!$modelTableInfo) { | ||
| 444 | + throw new Exception("table not found"); | ||
| 445 | + } | ||
| 446 | + } | ||
| 447 | + $modelTableInfo = $modelTableInfo[0]; | ||
| 448 | + | ||
| 449 | + $relations = []; | ||
| 450 | + //检查关联表 | ||
| 451 | + if ($relation) { | ||
| 452 | + $relationArr = $relation; | ||
| 453 | + $relations = []; | ||
| 454 | + | ||
| 455 | + foreach ($relationArr as $index => $relationTable) { | ||
| 456 | + $relationName = stripos($relationTable, $prefix) === 0 ? substr($relationTable, strlen($prefix)) : $relationTable; | ||
| 457 | + $relationTableType = 'table'; | ||
| 458 | + $relationTableTypeName = $relationTableName = $relationName; | ||
| 459 | + $relationTableInfo = $dbconnect->query("SHOW TABLE STATUS LIKE '{$relationTableName}'", [], true); | ||
| 460 | + if (!$relationTableInfo) { | ||
| 461 | + $relationTableType = 'name'; | ||
| 462 | + $relationTableName = $prefix . $relationName; | ||
| 463 | + $relationTableInfo = $dbconnect->query("SHOW TABLE STATUS LIKE '{$relationTableName}'", [], true); | ||
| 464 | + if (!$relationTableInfo) { | ||
| 465 | + throw new Exception("relation table not found"); | ||
| 466 | + } | ||
| 467 | + } | ||
| 468 | + $relationTableInfo = $relationTableInfo[0]; | ||
| 469 | + $relationModel = isset($relationModels[$index]) ? $relationModels[$index] : ''; | ||
| 470 | + | ||
| 471 | + list($relationNamespace, $relationName, $relationFile) = $this->getModelData($modelModuleName, $relationModel, $relationName); | ||
| 472 | + | ||
| 473 | + $relations[] = [ | ||
| 474 | + //关联表基础名 | ||
| 475 | + 'relationName' => $relationName, | ||
| 476 | + //关联表类命名空间 | ||
| 477 | + 'relationNamespace' => $relationNamespace, | ||
| 478 | + //关联模型名 | ||
| 479 | + 'relationModel' => $relationModel, | ||
| 480 | + //关联文件 | ||
| 481 | + 'relationFile' => $relationFile, | ||
| 482 | + //关联表名称 | ||
| 483 | + 'relationTableName' => $relationTableName, | ||
| 484 | + //关联表信息 | ||
| 485 | + 'relationTableInfo' => $relationTableInfo, | ||
| 486 | + //关联模型表类型(name或table) | ||
| 487 | + 'relationTableType' => $relationTableType, | ||
| 488 | + //关联模型表类型名称 | ||
| 489 | + 'relationTableTypeName' => $relationTableTypeName, | ||
| 490 | + //关联模式 | ||
| 491 | + 'relationFields' => isset($relationFields[$index]) ? explode(',', $relationFields[$index]) : [], | ||
| 492 | + //关联模式 | ||
| 493 | + 'relationMode' => isset($relationMode[$index]) ? $relationMode[$index] : 'belongsto', | ||
| 494 | + //关联模型控制器 | ||
| 495 | + 'relationController' => isset($relationController[$index]) ? $relationController[$index] : '', | ||
| 496 | + //关联表外键 | ||
| 497 | + 'relationForeignKey' => isset($relationForeignKey[$index]) ? $relationForeignKey[$index] : '', | ||
| 498 | + //关联表主键 | ||
| 499 | + 'relationPrimaryKey' => isset($relationPrimaryKey[$index]) ? $relationPrimaryKey[$index] : '', | ||
| 500 | + ]; | ||
| 501 | + } | ||
| 502 | + } | ||
| 503 | + | ||
| 504 | + //根据表名匹配对应的Fontawesome图标 | ||
| 505 | + $iconPath = ROOT_PATH . str_replace('/', DS, '/public/assets/libs/font-awesome/less/variables.less'); | ||
| 506 | + $iconName = is_file($iconPath) && stripos(file_get_contents($iconPath), '@fa-var-' . $table . ':') ? 'fa fa-' . $table : 'fa fa-circle-o'; | ||
| 507 | + | ||
| 508 | + //控制器 | ||
| 509 | + list($controllerNamespace, $controllerName, $controllerFile, $controllerArr) = $this->getControllerData($moduleName, $controller, $table); | ||
| 510 | + //模型 | ||
| 511 | + list($modelNamespace, $modelName, $modelFile, $modelArr) = $this->getModelData($modelModuleName, $model, $table); | ||
| 512 | + //验证器 | ||
| 513 | + list($validateNamespace, $validateName, $validateFile, $validateArr) = $this->getValidateData($validateModuleName, $validate, $table); | ||
| 514 | + | ||
| 515 | + //处理基础文件名,取消所有下划线并转换为小写 | ||
| 516 | + $baseNameArr = $controllerArr; | ||
| 517 | + $baseFileName = Loader::parseName(array_pop($baseNameArr), 0); | ||
| 518 | + array_push($baseNameArr, $baseFileName); | ||
| 519 | + $controllerBaseName = strtolower(implode(DS, $baseNameArr)); | ||
| 520 | + //$controllerUrl = strtolower(implode('/', $baseNameArr)); | ||
| 521 | + $controllerUrl = $this->getControllerUrl($moduleName, $baseNameArr); | ||
| 522 | + | ||
| 523 | + //视图文件 | ||
| 524 | + $viewArr = $controllerArr; | ||
| 525 | + $lastValue = array_pop($viewArr); | ||
| 526 | + $viewArr[] = Loader::parseName($lastValue, 0); | ||
| 527 | + array_unshift($viewArr, 'view'); | ||
| 528 | + $viewDir = $adminPath . strtolower(implode(DS, $viewArr)) . DS; | ||
| 529 | + | ||
| 530 | + //最终将生成的文件路径 | ||
| 531 | + $javascriptFile = ROOT_PATH . 'public' . DS . 'assets' . DS . 'js' . DS . 'backend' . DS . $controllerBaseName . '.js'; | ||
| 532 | + $addFile = $viewDir . 'add.html'; | ||
| 533 | + $editFile = $viewDir . 'edit.html'; | ||
| 534 | + $indexFile = $viewDir . 'index.html'; | ||
| 535 | + $recyclebinFile = $viewDir . 'recyclebin.html'; | ||
| 536 | + $langFile = $adminPath . 'lang' . DS . Lang::detect() . DS . $controllerBaseName . '.php'; | ||
| 537 | + | ||
| 538 | + //是否为删除模式 | ||
| 539 | + $delete = $input->getOption('delete'); | ||
| 540 | + if ($delete) { | ||
| 541 | + $readyFiles = [$controllerFile, $modelFile, $validateFile, $addFile, $editFile, $indexFile, $recyclebinFile, $langFile, $javascriptFile]; | ||
| 542 | + foreach ($readyFiles as $k => $v) { | ||
| 543 | + $output->warning($v); | ||
| 544 | + } | ||
| 545 | + if (!$force) { | ||
| 546 | + $output->info("Are you sure you want to delete all those files? Type 'yes' to continue: "); | ||
| 547 | + $line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r')); | ||
| 548 | + if (trim($line) != 'yes') { | ||
| 549 | + throw new Exception("Operation is aborted!"); | ||
| 550 | + } | ||
| 551 | + } | ||
| 552 | + foreach ($readyFiles as $k => $v) { | ||
| 553 | + if (file_exists($v)) { | ||
| 554 | + unlink($v); | ||
| 555 | + } | ||
| 556 | + //删除空文件夹 | ||
| 557 | + switch ($v) { | ||
| 558 | + case $modelFile: | ||
| 559 | + $this->removeEmptyBaseDir($v, $modelArr); | ||
| 560 | + break; | ||
| 561 | + case $validateFile: | ||
| 562 | + $this->removeEmptyBaseDir($v, $validateArr); | ||
| 563 | + break; | ||
| 564 | + case $addFile: | ||
| 565 | + case $editFile: | ||
| 566 | + case $indexFile: | ||
| 567 | + case $recyclebinFile: | ||
| 568 | + $this->removeEmptyBaseDir($v, $viewArr); | ||
| 569 | + break; | ||
| 570 | + default: | ||
| 571 | + $this->removeEmptyBaseDir($v, $controllerArr); | ||
| 572 | + } | ||
| 573 | + } | ||
| 574 | + | ||
| 575 | + //继续删除菜单 | ||
| 576 | + if ($menu) { | ||
| 577 | + exec("php think menu -c {$controllerUrl} -d 1 -f 1"); | ||
| 578 | + } | ||
| 579 | + | ||
| 580 | + $output->info("Delete Successed"); | ||
| 581 | + return; | ||
| 582 | + } | ||
| 583 | + | ||
| 584 | + //非覆盖模式时如果存在控制器文件则报错 | ||
| 585 | + if (is_file($controllerFile) && !$force) { | ||
| 586 | + throw new Exception("controller already exists!\nIf you need to rebuild again, use the parameter --force=true "); | ||
| 587 | + } | ||
| 588 | + | ||
| 589 | + //非覆盖模式时如果存在模型文件则报错 | ||
| 590 | + if (is_file($modelFile) && !$force) { | ||
| 591 | + throw new Exception("model already exists!\nIf you need to rebuild again, use the parameter --force=true "); | ||
| 592 | + } | ||
| 593 | + | ||
| 594 | + //非覆盖模式时如果存在验证文件则报错 | ||
| 595 | + if (is_file($validateFile) && !$force) { | ||
| 596 | + throw new Exception("validate already exists!\nIf you need to rebuild again, use the parameter --force=true "); | ||
| 597 | + } | ||
| 598 | + | ||
| 599 | + require $adminPath . 'common.php'; | ||
| 600 | + | ||
| 601 | + //从数据库中获取表字段信息 | ||
| 602 | + $sql = "SELECT * FROM `information_schema`.`columns` " | ||
| 603 | + . "WHERE TABLE_SCHEMA = ? AND table_name = ? " | ||
| 604 | + . "ORDER BY ORDINAL_POSITION"; | ||
| 605 | + //加载主表的列 | ||
| 606 | + $columnList = $dbconnect->query($sql, [$dbname, $modelTableName]); | ||
| 607 | + $fieldArr = []; | ||
| 608 | + foreach ($columnList as $k => $v) { | ||
| 609 | + $fieldArr[] = $v['COLUMN_NAME']; | ||
| 610 | + } | ||
| 611 | + | ||
| 612 | + // 加载关联表的列 | ||
| 613 | + foreach ($relations as $index => &$relation) { | ||
| 614 | + $relationColumnList = $dbconnect->query($sql, [$dbname, $relation['relationTableName']]); | ||
| 615 | + | ||
| 616 | + $relationFieldList = []; | ||
| 617 | + foreach ($relationColumnList as $k => $v) { | ||
| 618 | + $relationFieldList[] = $v['COLUMN_NAME']; | ||
| 619 | + } | ||
| 620 | + if (!$relation['relationPrimaryKey']) { | ||
| 621 | + foreach ($relationColumnList as $k => $v) { | ||
| 622 | + if ($v['COLUMN_KEY'] == 'PRI') { | ||
| 623 | + $relation['relationPrimaryKey'] = $v['COLUMN_NAME']; | ||
| 624 | + break; | ||
| 625 | + } | ||
| 626 | + } | ||
| 627 | + } | ||
| 628 | + // 如果主键为空 | ||
| 629 | + if (!$relation['relationPrimaryKey']) { | ||
| 630 | + throw new Exception('Relation Primary key not found!'); | ||
| 631 | + } | ||
| 632 | + // 如果主键不在表字段中 | ||
| 633 | + if (!in_array($relation['relationPrimaryKey'], $relationFieldList)) { | ||
| 634 | + throw new Exception('Relation Primary key not found in table!'); | ||
| 635 | + } | ||
| 636 | + $relation['relationColumnList'] = $relationColumnList; | ||
| 637 | + $relation['relationFieldList'] = $relationFieldList; | ||
| 638 | + } | ||
| 639 | + unset($relation); | ||
| 640 | + | ||
| 641 | + $addList = []; | ||
| 642 | + $editList = []; | ||
| 643 | + $javascriptList = []; | ||
| 644 | + $langList = []; | ||
| 645 | + $operateButtonList = []; | ||
| 646 | + $field = 'id'; | ||
| 647 | + $order = 'id'; | ||
| 648 | + $priDefined = false; | ||
| 649 | + $priKeyArr = []; | ||
| 650 | + $relationPrimaryKey = ''; | ||
| 651 | + foreach ($columnList as $k => $v) { | ||
| 652 | + if ($v['COLUMN_KEY'] == 'PRI') { | ||
| 653 | + $priKeyArr[] = $v['COLUMN_NAME']; | ||
| 654 | + } | ||
| 655 | + } | ||
| 656 | + if (!$priKeyArr) { | ||
| 657 | + throw new Exception('Primary key not found!'); | ||
| 658 | + } | ||
| 659 | + if (count($priKeyArr) > 1) { | ||
| 660 | + throw new Exception('Multiple primary key not support!'); | ||
| 661 | + } | ||
| 662 | + $priKey = reset($priKeyArr); | ||
| 663 | + | ||
| 664 | + $order = $priKey; | ||
| 665 | + | ||
| 666 | + //如果是关联模型 | ||
| 667 | + foreach ($relations as $index => &$relation) { | ||
| 668 | + if ($relation['relationMode'] == 'hasone') { | ||
| 669 | + $relationForeignKey = $relation['relationForeignKey'] ? $relation['relationForeignKey'] : $table . "_id"; | ||
| 670 | + $relationPrimaryKey = $relation['relationPrimaryKey'] ? $relation['relationPrimaryKey'] : $priKey; | ||
| 671 | + | ||
| 672 | + if (!in_array($relationForeignKey, $relation['relationFieldList'])) { | ||
| 673 | + throw new Exception('relation table [' . $relation['relationTableName'] . '] must be contain field [' . $relationForeignKey . ']'); | ||
| 674 | + } | ||
| 675 | + if (!in_array($relationPrimaryKey, $fieldArr)) { | ||
| 676 | + throw new Exception('table [' . $modelTableName . '] must be contain field [' . $relationPrimaryKey . ']'); | ||
| 677 | + } | ||
| 678 | + } elseif ($relation['relationMode'] == 'belongsto') { | ||
| 679 | + $relationForeignKey = $relation['relationForeignKey'] ? $relation['relationForeignKey'] : Loader::parseName($relation['relationName']) . "_id"; | ||
| 680 | + $relationPrimaryKey = $relation['relationPrimaryKey'] ? $relation['relationPrimaryKey'] : $relation['relationPriKey']; | ||
| 681 | + if (!in_array($relationForeignKey, $fieldArr)) { | ||
| 682 | + throw new Exception('table [' . $modelTableName . '] must be contain field [' . $relationForeignKey . ']'); | ||
| 683 | + } | ||
| 684 | + if (!in_array($relationPrimaryKey, $relation['relationFieldList'])) { | ||
| 685 | + throw new Exception('relation table [' . $relation['relationTableName'] . '] must be contain field [' . $relationPrimaryKey . ']'); | ||
| 686 | + } | ||
| 687 | + } elseif ($relation['relationMode'] == 'hasmany') { | ||
| 688 | + $relationForeignKey = $relation['relationForeignKey'] ? $relation['relationForeignKey'] : $table . "_id"; | ||
| 689 | + $relationPrimaryKey = $relation['relationPrimaryKey'] ? $relation['relationPrimaryKey'] : $priKey; | ||
| 690 | + if (!in_array($relationForeignKey, $relation['relationFieldList'])) { | ||
| 691 | + throw new Exception('relation table [' . $relation['relationTableName'] . '] must be contain field [' . $relationForeignKey . ']'); | ||
| 692 | + } | ||
| 693 | + if (!in_array($relationPrimaryKey, $fieldArr)) { | ||
| 694 | + throw new Exception('table [' . $modelTableName . '] must be contain field [' . $relationPrimaryKey . ']'); | ||
| 695 | + } | ||
| 696 | + $relation['relationColumnList'] = []; | ||
| 697 | + $relation['relationFieldList'] = []; | ||
| 698 | + } | ||
| 699 | + $relation['relationForeignKey'] = $relationForeignKey; | ||
| 700 | + $relation['relationPrimaryKey'] = $relationPrimaryKey; | ||
| 701 | + $relation['relationClassName'] = $modelNamespace != $relation['relationNamespace'] ? $relation['relationNamespace'] . '\\' . $relation['relationName'] : $relation['relationName']; | ||
| 702 | + } | ||
| 703 | + unset($relation); | ||
| 704 | + | ||
| 705 | + try { | ||
| 706 | + Form::setEscapeHtml(false); | ||
| 707 | + $setAttrArr = []; | ||
| 708 | + $getAttrArr = []; | ||
| 709 | + $getEnumArr = []; | ||
| 710 | + $appendAttrList = []; | ||
| 711 | + $controllerAssignList = []; | ||
| 712 | + $headingHtml = '{:build_heading()}'; | ||
| 713 | + $controllerImport = ''; | ||
| 714 | + $importHtml = ''; | ||
| 715 | + $multipleHtml = ''; | ||
| 716 | + $recyclebinHtml = ''; | ||
| 717 | + | ||
| 718 | + if ($import) { | ||
| 719 | + $controllerImport = $this->getReplacedStub('mixins/import', []); | ||
| 720 | + $importHtml = '<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>'; | ||
| 721 | + } | ||
| 722 | + | ||
| 723 | + //循环所有字段,开始构造视图的HTML和JS信息 | ||
| 724 | + foreach ($columnList as $k => $v) { | ||
| 725 | + $field = $v['COLUMN_NAME']; | ||
| 726 | + $itemArr = []; | ||
| 727 | + // 这里构建Enum和Set类型的列表数据 | ||
| 728 | + if (in_array($v['DATA_TYPE'], ['enum', 'set', 'tinyint']) || $this->headingFilterField == $field) { | ||
| 729 | + if ($v['DATA_TYPE'] !== 'tinyint') { | ||
| 730 | + $itemArr = substr($v['COLUMN_TYPE'], strlen($v['DATA_TYPE']) + 1, -1); | ||
| 731 | + $itemArr = explode(',', str_replace("'", '', $itemArr)); | ||
| 732 | + } | ||
| 733 | + $itemArr = $this->getItemArray($itemArr, $field, $v['COLUMN_COMMENT']); | ||
| 734 | + //如果类型为tinyint且有使用备注数据 | ||
| 735 | + if ($itemArr && !in_array($v['DATA_TYPE'], ['enum', 'set'])) { | ||
| 736 | + $v['DATA_TYPE'] = 'enum'; | ||
| 737 | + } | ||
| 738 | + } | ||
| 739 | + // 语言列表 | ||
| 740 | + if ($v['COLUMN_COMMENT'] != '') { | ||
| 741 | + $langList[] = $this->getLangItem($field, $v['COLUMN_COMMENT']); | ||
| 742 | + } | ||
| 743 | + $inputType = ''; | ||
| 744 | + //保留字段不能修改和添加 | ||
| 745 | + if ($v['COLUMN_KEY'] != 'PRI' && !in_array($field, $this->reservedField) && !in_array($field, $this->ignoreFields)) { | ||
| 746 | + $inputType = $this->getFieldType($v); | ||
| 747 | + | ||
| 748 | + // 如果是number类型时增加一个步长 | ||
| 749 | + $step = $inputType == 'number' && $v['NUMERIC_SCALE'] > 0 ? "0." . str_repeat(0, $v['NUMERIC_SCALE'] - 1) . "1" : 0; | ||
| 750 | + | ||
| 751 | + $attrArr = ['id' => "c-{$field}"]; | ||
| 752 | + $cssClassArr = ['form-control']; | ||
| 753 | + $fieldName = "row[{$field}]"; | ||
| 754 | + $defaultValue = $v['COLUMN_DEFAULT']; | ||
| 755 | + $editValue = "{\$row.{$field}|htmlentities}"; | ||
| 756 | + // 如果默认值非null,则是一个必选项 | ||
| 757 | + if ($v['IS_NULLABLE'] == 'NO') { | ||
| 758 | + $attrArr['data-rule'] = 'required'; | ||
| 759 | + } | ||
| 760 | + | ||
| 761 | + //如果字段类型为无符号型,则设置<input min=0> | ||
| 762 | + if (stripos($v['COLUMN_TYPE'], 'unsigned') !== false) { | ||
| 763 | + $attrArr['min'] = 0; | ||
| 764 | + } | ||
| 765 | + | ||
| 766 | + if ($inputType == 'select') { | ||
| 767 | + $cssClassArr[] = 'selectpicker'; | ||
| 768 | + $attrArr['class'] = implode(' ', $cssClassArr); | ||
| 769 | + if ($v['DATA_TYPE'] == 'set') { | ||
| 770 | + $attrArr['multiple'] = ''; | ||
| 771 | + $fieldName .= "[]"; | ||
| 772 | + } | ||
| 773 | + $attrArr['name'] = $fieldName; | ||
| 774 | + | ||
| 775 | + $this->getEnum($getEnumArr, $controllerAssignList, $field, $itemArr, $v['DATA_TYPE'] == 'set' ? 'multiple' : 'select'); | ||
| 776 | + | ||
| 777 | + $itemArr = $this->getLangArray($itemArr, false); | ||
| 778 | + //添加一个获取器 | ||
| 779 | + $this->getAttr($getAttrArr, $field, $v['DATA_TYPE'] == 'set' ? 'multiple' : 'select'); | ||
| 780 | + if ($v['DATA_TYPE'] == 'set') { | ||
| 781 | + $this->setAttr($setAttrArr, $field, $inputType); | ||
| 782 | + } | ||
| 783 | + $this->appendAttr($appendAttrList, $field); | ||
| 784 | + $formAddElement = $this->getReplacedStub('html/select', ['field' => $field, 'fieldName' => $fieldName, 'fieldList' => $this->getFieldListName($field), 'attrStr' => Form::attributes($attrArr), 'selectedValue' => $defaultValue]); | ||
| 785 | + $formEditElement = $this->getReplacedStub('html/select', ['field' => $field, 'fieldName' => $fieldName, 'fieldList' => $this->getFieldListName($field), 'attrStr' => Form::attributes($attrArr), 'selectedValue' => "\$row.{$field}"]); | ||
| 786 | + } elseif ($inputType == 'datetime') { | ||
| 787 | + $cssClassArr[] = 'datetimepicker'; | ||
| 788 | + $attrArr['class'] = implode(' ', $cssClassArr); | ||
| 789 | + $format = "YYYY-MM-DD HH:mm:ss"; | ||
| 790 | + $phpFormat = "Y-m-d H:i:s"; | ||
| 791 | + $fieldFunc = ''; | ||
| 792 | + switch ($v['DATA_TYPE']) { | ||
| 793 | + case 'year': | ||
| 794 | + $format = "YYYY"; | ||
| 795 | + $phpFormat = 'Y'; | ||
| 796 | + break; | ||
| 797 | + case 'date': | ||
| 798 | + $format = "YYYY-MM-DD"; | ||
| 799 | + $phpFormat = 'Y-m-d'; | ||
| 800 | + break; | ||
| 801 | + case 'time': | ||
| 802 | + $format = "HH:mm:ss"; | ||
| 803 | + $phpFormat = 'H:i:s'; | ||
| 804 | + break; | ||
| 805 | + case 'timestamp': | ||
| 806 | + $fieldFunc = 'datetime'; | ||
| 807 | + // no break | ||
| 808 | + case 'datetime': | ||
| 809 | + $format = "YYYY-MM-DD HH:mm:ss"; | ||
| 810 | + $phpFormat = 'Y-m-d H:i:s'; | ||
| 811 | + break; | ||
| 812 | + default: | ||
| 813 | + $fieldFunc = 'datetime'; | ||
| 814 | + $this->getAttr($getAttrArr, $field, $inputType); | ||
| 815 | + $this->setAttr($setAttrArr, $field, $inputType); | ||
| 816 | + $this->appendAttr($appendAttrList, $field); | ||
| 817 | + break; | ||
| 818 | + } | ||
| 819 | + $defaultDateTime = "{:date('{$phpFormat}')}"; | ||
| 820 | + $attrArr['data-date-format'] = $format; | ||
| 821 | + $attrArr['data-use-current'] = "true"; | ||
| 822 | + $formAddElement = Form::text($fieldName, $defaultDateTime, $attrArr); | ||
| 823 | + $formEditElement = Form::text($fieldName, ($fieldFunc ? "{:\$row.{$field}?{$fieldFunc}(\$row.{$field}):''}" : "{\$row.{$field}{$fieldFunc}}"), $attrArr); | ||
| 824 | + } elseif ($inputType == 'datetimerange') { | ||
| 825 | + $cssClassArr[] = 'datetimerange'; | ||
| 826 | + $attrArr['class'] = implode(' ', $cssClassArr); | ||
| 827 | + $attrArr['data-locale'] = '{"format":"YYYY-MM-DD HH:mm:ss"}'; | ||
| 828 | + $fieldFunc = ''; | ||
| 829 | + $defaultDateTime = ""; | ||
| 830 | + $formAddElement = Form::text($fieldName, $defaultDateTime, $attrArr); | ||
| 831 | + $formEditElement = Form::text($fieldName, $editValue, $attrArr); | ||
| 832 | + } elseif ($inputType == 'checkbox' || $inputType == 'radio') { | ||
| 833 | + unset($attrArr['data-rule']); | ||
| 834 | + $fieldName = $inputType == 'checkbox' ? $fieldName .= "[]" : $fieldName; | ||
| 835 | + $attrArr['name'] = "row[{$fieldName}]"; | ||
| 836 | + | ||
| 837 | + $this->getEnum($getEnumArr, $controllerAssignList, $field, $itemArr, $inputType); | ||
| 838 | + $itemArr = $this->getLangArray($itemArr, false); | ||
| 839 | + //添加一个获取器 | ||
| 840 | + $this->getAttr($getAttrArr, $field, $inputType); | ||
| 841 | + if ($inputType == 'checkbox') { | ||
| 842 | + $this->setAttr($setAttrArr, $field, $inputType); | ||
| 843 | + } | ||
| 844 | + $this->appendAttr($appendAttrList, $field); | ||
| 845 | + $defaultValue = $inputType == 'radio' && !$defaultValue ? key($itemArr) : $defaultValue; | ||
| 846 | + | ||
| 847 | + $formAddElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'fieldList' => $this->getFieldListName($field), 'attrStr' => Form::attributes($attrArr), 'selectedValue' => $defaultValue]); | ||
| 848 | + $formEditElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'fieldList' => $this->getFieldListName($field), 'attrStr' => Form::attributes($attrArr), 'selectedValue' => "\$row.{$field}"]); | ||
| 849 | + } elseif ($inputType == 'textarea' && !$this->isMatchSuffix($field, $this->selectpagesSuffix) && !$this->isMatchSuffix($field, $this->imageField)) { | ||
| 850 | + $cssClassArr[] = $this->isMatchSuffix($field, $this->editorSuffix) ? $this->editorClass : ''; | ||
| 851 | + $attrArr['class'] = implode(' ', $cssClassArr); | ||
| 852 | + $attrArr['rows'] = 5; | ||
| 853 | + $formAddElement = Form::textarea($fieldName, $defaultValue, $attrArr); | ||
| 854 | + $formEditElement = Form::textarea($fieldName, $editValue, $attrArr); | ||
| 855 | + } elseif ($inputType == 'switch') { | ||
| 856 | + unset($attrArr['data-rule']); | ||
| 857 | + if ($defaultValue === '1' || $defaultValue === 'Y') { | ||
| 858 | + $yes = $defaultValue; | ||
| 859 | + $no = $defaultValue === '1' ? '0' : 'N'; | ||
| 860 | + } else { | ||
| 861 | + $no = $defaultValue; | ||
| 862 | + $yes = $defaultValue === '0' ? '1' : 'Y'; | ||
| 863 | + } | ||
| 864 | + if (!$itemArr) { | ||
| 865 | + $itemArr = [$yes => 'Yes', $no => 'No']; | ||
| 866 | + } | ||
| 867 | + $stateNoClass = 'fa-flip-horizontal text-gray'; | ||
| 868 | + $formAddElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'fieldYes' => $yes, 'fieldNo' => $no, 'attrStr' => Form::attributes($attrArr), 'fieldValue' => $defaultValue, 'fieldSwitchClass' => $defaultValue == $no ? $stateNoClass : '']); | ||
| 869 | + $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}"]); | ||
| 870 | + } elseif ($inputType == 'citypicker') { | ||
| 871 | + $attrArr['class'] = implode(' ', $cssClassArr); | ||
| 872 | + $attrArr['data-toggle'] = "city-picker"; | ||
| 873 | + $formAddElement = sprintf("<div class='control-relative'>%s</div>", Form::input('text', $fieldName, $defaultValue, $attrArr)); | ||
| 874 | + $formEditElement = sprintf("<div class='control-relative'>%s</div>", Form::input('text', $fieldName, $editValue, $attrArr)); | ||
| 875 | + } elseif ($inputType == 'tagsinput') { | ||
| 876 | + $attrArr['class'] = implode(' ', $cssClassArr); | ||
| 877 | + $attrArr['data-role'] = "tagsinput"; | ||
| 878 | + $formAddElement = Form::input('text', $fieldName, $defaultValue, $attrArr); | ||
| 879 | + $formEditElement = Form::input('text', $fieldName, $editValue, $attrArr); | ||
| 880 | + } elseif ($inputType == 'fieldlist') { | ||
| 881 | + $itemArr = $this->getItemArray($itemArr, $field, $v['COLUMN_COMMENT']); | ||
| 882 | + $templateName = !isset($itemArr['key']) && !isset($itemArr['value']) && count($itemArr) > 0 ? 'fieldlist-template' : 'fieldlist'; | ||
| 883 | + $itemKey = isset($itemArr['key']) ? ucfirst($itemArr['key']) : 'Key'; | ||
| 884 | + $itemValue = isset($itemArr['value']) ? ucfirst($itemArr['value']) : 'Value'; | ||
| 885 | + $theadListArr = $tbodyListArr = []; | ||
| 886 | + foreach ($itemArr as $index => $item) { | ||
| 887 | + $theadListArr[] = "<td>{:__('" . $item . "')}</td>"; | ||
| 888 | + $tbodyListArr[] = '<td><input type="text" name="<%=name%>[<%=index%>][' . $index . ']" class="form-control" value="<%=row.' . $index . '%>"/></td>'; | ||
| 889 | + } | ||
| 890 | + $colspan = count($theadListArr) + 1; | ||
| 891 | + $commonFields = ['field' => $field, 'fieldName' => $fieldName, 'itemKey' => $itemKey, 'itemValue' => $itemValue, 'theadList' => implode("\n", $theadListArr), 'tbodyList' => implode("\n", $tbodyListArr), 'colspan' => $colspan]; | ||
| 892 | + $formAddElement = $this->getReplacedStub('html/' . $templateName, array_merge($commonFields, ['fieldValue' => $defaultValue])); | ||
| 893 | + $formEditElement = $this->getReplacedStub('html/' . $templateName, array_merge($commonFields, ['fieldValue' => $editValue])); | ||
| 894 | + } else { | ||
| 895 | + $search = $replace = ''; | ||
| 896 | + //特殊字段为关联搜索 | ||
| 897 | + if ($this->isMatchSuffix($field, $this->selectpageSuffix)) { | ||
| 898 | + $inputType = 'text'; | ||
| 899 | + $defaultValue = ''; | ||
| 900 | + $attrArr['data-rule'] = 'required'; | ||
| 901 | + $cssClassArr[] = 'selectpage'; | ||
| 902 | + $selectpageTable = substr($field, 0, strripos($field, '_')); | ||
| 903 | + $selectpageField = ''; | ||
| 904 | + $selectpageController = str_replace('_', '/', $selectpageTable); | ||
| 905 | + $attrArr['data-source'] = $selectpageController . "/index"; | ||
| 906 | + //如果是类型表需要特殊处理下 | ||
| 907 | + if ($selectpageController == 'category') { | ||
| 908 | + $attrArr['data-source'] = 'category/selectpage'; | ||
| 909 | + $attrArr['data-params'] = '##replacetext##'; | ||
| 910 | + $search = '"##replacetext##"'; | ||
| 911 | + $replace = '\'{"custom[type]":"' . $table . '"}\''; | ||
| 912 | + } elseif ($selectpageController == 'admin') { | ||
| 913 | + $attrArr['data-source'] = 'auth/admin/selectpage'; | ||
| 914 | + } elseif ($selectpageController == 'user') { | ||
| 915 | + $attrArr['data-source'] = 'user/user/index'; | ||
| 916 | + $attrArr['data-field'] = 'nickname'; | ||
| 917 | + } | ||
| 918 | + if ($this->isMatchSuffix($field, $this->selectpagesSuffix)) { | ||
| 919 | + $attrArr['data-multiple'] = 'true'; | ||
| 920 | + } | ||
| 921 | + | ||
| 922 | + $tableInfo = null; | ||
| 923 | + try { | ||
| 924 | + $tableInfo = \think\Db::name($selectpageTable)->getTableInfo(); | ||
| 925 | + if (isset($tableInfo['fields'])) { | ||
| 926 | + foreach ($tableInfo['fields'] as $m => $n) { | ||
| 927 | + if (in_array($n, ['nickname', 'title', 'name'])) { | ||
| 928 | + $selectpageField = $n; | ||
| 929 | + break; | ||
| 930 | + } | ||
| 931 | + } | ||
| 932 | + } | ||
| 933 | + } catch (\Exception $e) { | ||
| 934 | + } | ||
| 935 | + if (!$selectpageField) { | ||
| 936 | + foreach ($this->fieldSelectpageMap as $m => $n) { | ||
| 937 | + if (in_array($field, $n)) { | ||
| 938 | + $attrArr['data-field'] = $m; | ||
| 939 | + break; | ||
| 940 | + } | ||
| 941 | + } | ||
| 942 | + } | ||
| 943 | + } | ||
| 944 | + //因为有自动完成可输入其它内容 | ||
| 945 | + $step = array_intersect($cssClassArr, ['selectpage']) ? 0 : $step; | ||
| 946 | + $attrArr['class'] = implode(' ', $cssClassArr); | ||
| 947 | + $isUpload = false; | ||
| 948 | + if ($this->isMatchSuffix($field, array_merge($this->imageField, $this->fileField))) { | ||
| 949 | + $isUpload = true; | ||
| 950 | + } | ||
| 951 | + //如果是步长则加上步长 | ||
| 952 | + if ($step) { | ||
| 953 | + $attrArr['step'] = $step; | ||
| 954 | + } | ||
| 955 | + //如果是图片加上个size | ||
| 956 | + if ($isUpload) { | ||
| 957 | + $attrArr['size'] = 50; | ||
| 958 | + } | ||
| 959 | + | ||
| 960 | + //字段默认值判断 | ||
| 961 | + if ('NULL' == $defaultValue || "''" == $defaultValue) { | ||
| 962 | + $defaultValue = ''; | ||
| 963 | + } | ||
| 964 | + | ||
| 965 | + $formAddElement = Form::input($inputType, $fieldName, $defaultValue, $attrArr); | ||
| 966 | + $formEditElement = Form::input($inputType, $fieldName, $editValue, $attrArr); | ||
| 967 | + if ($search && $replace) { | ||
| 968 | + $formAddElement = str_replace($search, $replace, $formAddElement); | ||
| 969 | + $formEditElement = str_replace($search, $replace, $formEditElement); | ||
| 970 | + } | ||
| 971 | + //如果是图片或文件 | ||
| 972 | + if ($isUpload) { | ||
| 973 | + $formAddElement = $this->getImageUpload($field, $formAddElement); | ||
| 974 | + $formEditElement = $this->getImageUpload($field, $formEditElement); | ||
| 975 | + } | ||
| 976 | + } | ||
| 977 | + //构造添加和编辑HTML信息 | ||
| 978 | + $addList[] = $this->getFormGroup($field, $formAddElement); | ||
| 979 | + $editList[] = $this->getFormGroup($field, $formEditElement); | ||
| 980 | + } | ||
| 981 | + | ||
| 982 | + //过滤text类型字段 | ||
| 983 | + if ($v['DATA_TYPE'] != 'text' && $inputType != 'fieldlist') { | ||
| 984 | + //主键 | ||
| 985 | + if ($v['COLUMN_KEY'] == 'PRI' && !$priDefined) { | ||
| 986 | + $priDefined = true; | ||
| 987 | + $javascriptList[] = "{checkbox: true}"; | ||
| 988 | + } | ||
| 989 | + if ($this->deleteTimeField == $field) { | ||
| 990 | + $recyclebinHtml = $this->getReplacedStub('html/recyclebin-html', ['controllerUrl' => $controllerUrl]); | ||
| 991 | + continue; | ||
| 992 | + } | ||
| 993 | + if (!$fields || in_array($field, explode(',', $fields))) { | ||
| 994 | + //构造JS列信息 | ||
| 995 | + $javascriptList[] = $this->getJsColumn($field, $v['DATA_TYPE'], $inputType && in_array($inputType, ['select', 'checkbox', 'radio']) ? '_text' : '', $itemArr, $v); | ||
| 996 | + } | ||
| 997 | + if ($this->headingFilterField && $this->headingFilterField == $field && $itemArr) { | ||
| 998 | + $headingHtml = $this->getReplacedStub('html/heading-html', ['field' => $field, 'fieldName' => Loader::parseName($field, 1, false)]); | ||
| 999 | + $multipleHtml = $this->getReplacedStub('html/multiple-html', ['field' => $field, 'fieldName' => Loader::parseName($field, 1, false), 'controllerUrl' => $controllerUrl]); | ||
| 1000 | + } | ||
| 1001 | + //排序方式,如果有指定排序字段,否则按主键排序 | ||
| 1002 | + $order = $field == $this->sortField ? $this->sortField : $order; | ||
| 1003 | + } | ||
| 1004 | + } | ||
| 1005 | + | ||
| 1006 | + //循环关联表,追加语言包和JS列 | ||
| 1007 | + foreach ($relations as $index => $relation) { | ||
| 1008 | + if ($relation['relationMode'] == 'hasmany') { | ||
| 1009 | + $relationFieldText = ucfirst(strtolower($relation['relationName'])) . ' List'; | ||
| 1010 | + // 语言列表 | ||
| 1011 | + if ($relation['relationTableInfo']['Comment']) { | ||
| 1012 | + $langList[] = $this->getLangItem($relationFieldText, rtrim($relation['relationTableInfo']['Comment'], "表") . "列表"); | ||
| 1013 | + } | ||
| 1014 | + | ||
| 1015 | + $relationTableName = $relation['relationTableName']; | ||
| 1016 | + $relationTableName = stripos($relationTableName, $prefix) === 0 ? substr($relationTableName, strlen($prefix)) : $relationTableName; | ||
| 1017 | + | ||
| 1018 | + list($realtionControllerNamespace, $realtionControllerName, $realtionControllerFile, $realtionControllerArr) = $this->getControllerData($moduleName, $relation['relationController'], $relationTableName); | ||
| 1019 | + $realtionControllerArr = array_map("strtolower", $realtionControllerArr); | ||
| 1020 | + if (count($realtionControllerArr) > 1) { | ||
| 1021 | + $realtionControllerArr = [implode('.', $realtionControllerArr)]; | ||
| 1022 | + } | ||
| 1023 | + $realtionControllerArr[] = 'index'; | ||
| 1024 | + $realtionControllerArr[] = $relation['relationForeignKey'] . '/{ids}'; | ||
| 1025 | + $relationControllerUrl = implode('/', $realtionControllerArr); | ||
| 1026 | + | ||
| 1027 | + //构造JS列信息 | ||
| 1028 | + $operateButtonList[] = "{name: 'addtabs',title: __('{$relationFieldText}'),text: __('{$relationFieldText}'),classname: 'btn btn-xs btn-info btn-dialog',icon: 'fa fa-list',url: '" . $relationControllerUrl . "'}"; | ||
| 1029 | + //echo "php think crud -t {$relation['relationTableName']} -c {$relation['relationController']} -m {$relation['relationModel']} -i " . implode(',', $relation['relationFields']); | ||
| 1030 | + //不存在关联表控制器的情况下才进行生成 | ||
| 1031 | + if (!is_file($realtionControllerFile)) { | ||
| 1032 | + exec("php think crud -t {$relation['relationTableName']} -c {$relation['relationController']} -m {$relation['relationModel']} -i " . implode(',', $relation['relationFields'])); | ||
| 1033 | + } | ||
| 1034 | + } | ||
| 1035 | + foreach ($relation['relationColumnList'] as $k => $v) { | ||
| 1036 | + // 不显示的字段直接过滤掉 | ||
| 1037 | + if ($relation['relationFields'] && !in_array($v['COLUMN_NAME'], $relation['relationFields'])) { | ||
| 1038 | + continue; | ||
| 1039 | + } | ||
| 1040 | + | ||
| 1041 | + $relationField = strtolower($relation['relationName']) . "." . $v['COLUMN_NAME']; | ||
| 1042 | + // 语言列表 | ||
| 1043 | + if ($v['COLUMN_COMMENT'] != '') { | ||
| 1044 | + $langList[] = $this->getLangItem($relationField, $v['COLUMN_COMMENT']); | ||
| 1045 | + } | ||
| 1046 | + | ||
| 1047 | + //过滤text类型字段 | ||
| 1048 | + if ($v['DATA_TYPE'] != 'text') { | ||
| 1049 | + //构造JS列信息 | ||
| 1050 | + $javascriptList[] = $this->getJsColumn($relationField, $v['DATA_TYPE'], '', [], $v); | ||
| 1051 | + } | ||
| 1052 | + } | ||
| 1053 | + } | ||
| 1054 | + | ||
| 1055 | + //JS最后一列加上操作列 | ||
| 1056 | + $javascriptList[] = str_repeat(" ", 24) . "{field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, " . ($operateButtonList ? "buttons: [" . implode(',', $operateButtonList) . "], " : "") . "formatter: Table.api.formatter.operate}"; | ||
| 1057 | + $addList = implode("\n", array_filter($addList)); | ||
| 1058 | + $editList = implode("\n", array_filter($editList)); | ||
| 1059 | + $javascriptList = implode(",\n", array_filter($javascriptList)); | ||
| 1060 | + $langList = implode(",\n", array_filter($langList)); | ||
| 1061 | + //数组等号对齐 | ||
| 1062 | + $langList = array_filter(explode(",\n", $langList . ",\n")); | ||
| 1063 | + foreach ($langList as &$line) { | ||
| 1064 | + if (preg_match("/^\s+'([^']+)'\s*=>\s*'([^']+)'\s*/is", $line, $matches)) { | ||
| 1065 | + $line = " '{$matches[1]}'" . str_pad('=>', ($this->fieldMaxLen - strlen($matches[1]) + 3), ' ', STR_PAD_LEFT) . " '{$matches[2]}'"; | ||
| 1066 | + } | ||
| 1067 | + } | ||
| 1068 | + unset($line); | ||
| 1069 | + $langList = implode(",\n", array_filter($langList)); | ||
| 1070 | + $fixedcolumns = count($columnList) >= 10 ? 1 : $fixedcolumns; | ||
| 1071 | + | ||
| 1072 | + $fixedColumnsJs = ''; | ||
| 1073 | + if (is_numeric($fixedcolumns) && $fixedcolumns) { | ||
| 1074 | + $fixedColumnsJs = "\n" . str_repeat(" ", 16) . "fixedColumns: true,\n" . str_repeat(" ", 16) . ($fixedcolumns < 0 ? "fixedNumber" : "fixedRightNumber") . ": " . $fixedcolumns . ","; | ||
| 1075 | + } | ||
| 1076 | + | ||
| 1077 | + //表注释 | ||
| 1078 | + $tableComment = $modelTableInfo['Comment']; | ||
| 1079 | + $tableComment = mb_substr($tableComment, -1) == '表' ? mb_substr($tableComment, 0, -1) . '管理' : $tableComment; | ||
| 1080 | + | ||
| 1081 | + $modelInit = ''; | ||
| 1082 | + if ($priKey != $order) { | ||
| 1083 | + $modelInit = $this->getReplacedStub('mixins' . DS . 'modelinit', ['order' => $order]); | ||
| 1084 | + } | ||
| 1085 | + | ||
| 1086 | + $data = [ | ||
| 1087 | + 'modelConnection' => $db == 'database' ? '' : "protected \$connection = '{$db}';", | ||
| 1088 | + 'controllerNamespace' => $controllerNamespace, | ||
| 1089 | + 'modelNamespace' => $modelNamespace, | ||
| 1090 | + 'validateNamespace' => $validateNamespace, | ||
| 1091 | + 'controllerUrl' => $controllerUrl, | ||
| 1092 | + 'controllerName' => $controllerName, | ||
| 1093 | + 'controllerAssignList' => implode("\n", $controllerAssignList), | ||
| 1094 | + 'modelName' => $modelName, | ||
| 1095 | + 'modelTableName' => $modelTableName, | ||
| 1096 | + 'modelTableType' => $modelTableType, | ||
| 1097 | + 'modelTableTypeName' => $modelTableTypeName, | ||
| 1098 | + 'validateName' => $validateName, | ||
| 1099 | + 'tableComment' => $tableComment, | ||
| 1100 | + 'iconName' => $iconName, | ||
| 1101 | + 'pk' => $priKey, | ||
| 1102 | + 'order' => $order, | ||
| 1103 | + 'fixedColumnsJs' => $fixedColumnsJs, | ||
| 1104 | + 'table' => $table, | ||
| 1105 | + 'tableName' => $modelTableName, | ||
| 1106 | + 'addList' => $addList, | ||
| 1107 | + 'editList' => $editList, | ||
| 1108 | + 'javascriptList' => $javascriptList, | ||
| 1109 | + 'langList' => $langList, | ||
| 1110 | + 'softDeleteClassPath' => in_array($this->deleteTimeField, $fieldArr) ? "use traits\model\SoftDelete;" : '', | ||
| 1111 | + 'softDelete' => in_array($this->deleteTimeField, $fieldArr) ? "use SoftDelete;" : '', | ||
| 1112 | + 'modelAutoWriteTimestamp' => in_array($this->createTimeField, $fieldArr) || in_array($this->updateTimeField, $fieldArr) ? "'integer'" : 'false', | ||
| 1113 | + 'createTime' => in_array($this->createTimeField, $fieldArr) ? "'{$this->createTimeField}'" : 'false', | ||
| 1114 | + 'updateTime' => in_array($this->updateTimeField, $fieldArr) ? "'{$this->updateTimeField}'" : 'false', | ||
| 1115 | + 'deleteTime' => in_array($this->deleteTimeField, $fieldArr) ? "'{$this->deleteTimeField}'" : 'false', | ||
| 1116 | + 'relationSearch' => $relations ? 'true' : 'false', | ||
| 1117 | + 'relationWithList' => '', | ||
| 1118 | + 'relationMethodList' => '', | ||
| 1119 | + 'controllerImport' => $controllerImport, | ||
| 1120 | + 'controllerIndex' => '', | ||
| 1121 | + 'recyclebinJs' => '', | ||
| 1122 | + 'headingHtml' => $headingHtml, | ||
| 1123 | + 'multipleHtml' => $multipleHtml, | ||
| 1124 | + 'importHtml' => $importHtml, | ||
| 1125 | + 'recyclebinHtml' => $recyclebinHtml, | ||
| 1126 | + 'visibleFieldList' => $fields ? "\$row->visible(['" . implode("','", array_filter(in_array($priKey, explode(',', $fields)) ? explode(',', $fields) : explode(',', $priKey . ',' . $fields))) . "']);" : '', | ||
| 1127 | + 'appendAttrList' => implode(",\n", $appendAttrList), | ||
| 1128 | + 'getEnumList' => implode("\n\n", $getEnumArr), | ||
| 1129 | + 'getAttrList' => implode("\n\n", $getAttrArr), | ||
| 1130 | + 'setAttrList' => implode("\n\n", $setAttrArr), | ||
| 1131 | + 'modelInit' => $modelInit, | ||
| 1132 | + ]; | ||
| 1133 | + | ||
| 1134 | + //如果使用关联模型 | ||
| 1135 | + if ($relations) { | ||
| 1136 | + $relationWithList = $relationMethodList = $relationVisibleFieldList = []; | ||
| 1137 | + $relationKeyArr = ['hasone' => 'hasOne', 'belongsto' => 'belongsTo', 'hasmany' => 'hasMany']; | ||
| 1138 | + foreach ($relations as $index => $relation) { | ||
| 1139 | + //需要构造关联的方法 | ||
| 1140 | + $relation['relationMethod'] = strtolower($relation['relationName']); | ||
| 1141 | + | ||
| 1142 | + //关联的模式 | ||
| 1143 | + $relation['relationMode'] = strtolower($relation['relationMode']); | ||
| 1144 | + $relation['relationMode'] = array_key_exists($relation['relationMode'], $relationKeyArr) ? $relationKeyArr[$relation['relationMode']] : ''; | ||
| 1145 | + | ||
| 1146 | + //关联字段 | ||
| 1147 | + $relation['relationPrimaryKey'] = $relation['relationPrimaryKey'] ? $relation['relationPrimaryKey'] : $priKey; | ||
| 1148 | + | ||
| 1149 | + //构造关联模型的方法 | ||
| 1150 | + $relationMethodList[] = $this->getReplacedStub('mixins' . DS . 'modelrelationmethod' . ($relation['relationMode'] == 'hasMany' ? '-hasmany' : ''), $relation); | ||
| 1151 | + | ||
| 1152 | + if ($relation['relationMode'] == 'hasMany') { | ||
| 1153 | + continue; | ||
| 1154 | + } | ||
| 1155 | + | ||
| 1156 | + //预载入的方法 | ||
| 1157 | + $relationWithList[] = $relation['relationMethod']; | ||
| 1158 | + | ||
| 1159 | + unset($relation['relationColumnList'], $relation['relationFieldList'], $relation['relationTableInfo']); | ||
| 1160 | + | ||
| 1161 | + //如果设置了显示主表字段,则必须显式将关联表字段显示 | ||
| 1162 | + if ($fields) { | ||
| 1163 | + $relationVisibleFieldList[] = "\$row->visible(['{$relation['relationMethod']}']);"; | ||
| 1164 | + } | ||
| 1165 | + | ||
| 1166 | + //显示的字段 | ||
| 1167 | + if ($relation['relationFields']) { | ||
| 1168 | + $relationVisibleFieldList[] = "\$row->getRelation('" . $relation['relationMethod'] . "')->visible(['" . implode("','", $relation['relationFields']) . "']);"; | ||
| 1169 | + } | ||
| 1170 | + } | ||
| 1171 | + | ||
| 1172 | + $data['relationWithList'] = "->with(['" . implode("','", $relationWithList) . "'])"; | ||
| 1173 | + $data['relationMethodList'] = implode("\n\n", $relationMethodList); | ||
| 1174 | + $data['relationVisibleFieldList'] = implode("\n\t\t\t\t", $relationVisibleFieldList); | ||
| 1175 | + | ||
| 1176 | + if ($relationWithList) { | ||
| 1177 | + //需要重写index方法 | ||
| 1178 | + $data['controllerIndex'] = $this->getReplacedStub('controllerindex', $data); | ||
| 1179 | + } | ||
| 1180 | + } elseif ($fields) { | ||
| 1181 | + $data = array_merge($data, ['relationWithList' => '', 'relationMethodList' => '', 'relationVisibleFieldList' => '']); | ||
| 1182 | + //需要重写index方法 | ||
| 1183 | + $data['controllerIndex'] = $this->getReplacedStub('controllerindex', $data); | ||
| 1184 | + } | ||
| 1185 | + | ||
| 1186 | + // 生成控制器文件 | ||
| 1187 | + $this->writeToFile('controller', $data, $controllerFile); | ||
| 1188 | + // 生成模型文件 | ||
| 1189 | + $this->writeToFile('model', $data, $modelFile); | ||
| 1190 | + | ||
| 1191 | + if ($relations) { | ||
| 1192 | + foreach ($relations as $i => $relation) { | ||
| 1193 | + $relation['modelNamespace'] = $relation['relationNamespace']; | ||
| 1194 | + if (!is_file($relation['relationFile'])) { | ||
| 1195 | + // 生成关联模型文件 | ||
| 1196 | + $this->writeToFile('relationmodel', $relation, $relation['relationFile']); | ||
| 1197 | + } | ||
| 1198 | + } | ||
| 1199 | + } | ||
| 1200 | + // 生成验证文件 | ||
| 1201 | + $this->writeToFile('validate', $data, $validateFile); | ||
| 1202 | + // 生成视图文件 | ||
| 1203 | + $this->writeToFile('add', $data, $addFile); | ||
| 1204 | + $this->writeToFile('edit', $data, $editFile); | ||
| 1205 | + $this->writeToFile('index', $data, $indexFile); | ||
| 1206 | + if ($recyclebinHtml) { | ||
| 1207 | + $this->writeToFile('recyclebin', $data, $recyclebinFile); | ||
| 1208 | + $recyclebinTitle = in_array('title', $fieldArr) ? 'title' : (in_array('name', $fieldArr) ? 'name' : ''); | ||
| 1209 | + $recyclebinTitleJs = $recyclebinTitle ? "\n {field: '{$recyclebinTitle}', title: __('" . (ucfirst($recyclebinTitle)) . "'), align: 'left'}," : ''; | ||
| 1210 | + $data['recyclebinJs'] = $this->getReplacedStub('mixins/recyclebinjs', ['deleteTimeField' => $this->deleteTimeField, 'recyclebinTitleJs' => $recyclebinTitleJs, 'controllerUrl' => $controllerUrl]); | ||
| 1211 | + } | ||
| 1212 | + // 生成JS文件 | ||
| 1213 | + $this->writeToFile('javascript', $data, $javascriptFile); | ||
| 1214 | + // 生成语言文件 | ||
| 1215 | + $this->writeToFile('lang', $data, $langFile); | ||
| 1216 | + } catch (ErrorException $e) { | ||
| 1217 | + throw new Exception("Code: " . $e->getCode() . "\nLine: " . $e->getLine() . "\nMessage: " . $e->getMessage() . "\nFile: " . $e->getFile()); | ||
| 1218 | + } | ||
| 1219 | + | ||
| 1220 | + //继续生成菜单 | ||
| 1221 | + if ($menu) { | ||
| 1222 | + exec("php think menu -c {$controllerUrl}"); | ||
| 1223 | + } | ||
| 1224 | + | ||
| 1225 | + $output->info("Build Successed"); | ||
| 1226 | + } | ||
| 1227 | + | ||
| 1228 | + protected function getEnum(&$getEnum, &$controllerAssignList, $field, $itemArr = '', $inputType = '') | ||
| 1229 | + { | ||
| 1230 | + if (!in_array($inputType, ['datetime', 'select', 'multiple', 'checkbox', 'radio'])) { | ||
| 1231 | + return; | ||
| 1232 | + } | ||
| 1233 | + $fieldList = $this->getFieldListName($field); | ||
| 1234 | + $methodName = 'get' . ucfirst($fieldList); | ||
| 1235 | + foreach ($itemArr as $k => &$v) { | ||
| 1236 | + $v = "__('" . mb_ucfirst($v) . "')"; | ||
| 1237 | + } | ||
| 1238 | + unset($v); | ||
| 1239 | + $itemString = $this->getArrayString($itemArr); | ||
| 1240 | + $getEnum[] = <<<EOD | ||
| 1241 | + public function {$methodName}() | ||
| 1242 | + { | ||
| 1243 | + return [{$itemString}]; | ||
| 1244 | + } | ||
| 1245 | +EOD; | ||
| 1246 | + $controllerAssignList[] = <<<EOD | ||
| 1247 | + \$this->view->assign("{$fieldList}", \$this->model->{$methodName}()); | ||
| 1248 | +EOD; | ||
| 1249 | + } | ||
| 1250 | + | ||
| 1251 | + protected function getAttr(&$getAttr, $field, $inputType = '') | ||
| 1252 | + { | ||
| 1253 | + if (!in_array($inputType, ['datetime', 'select', 'multiple', 'checkbox', 'radio'])) { | ||
| 1254 | + return; | ||
| 1255 | + } | ||
| 1256 | + $attrField = ucfirst($this->getCamelizeName($field)); | ||
| 1257 | + $getAttr[] = $this->getReplacedStub("mixins" . DS . $inputType, ['field' => $field, 'methodName' => "get{$attrField}TextAttr", 'listMethodName' => "get{$attrField}List"]); | ||
| 1258 | + } | ||
| 1259 | + | ||
| 1260 | + protected function setAttr(&$setAttr, $field, $inputType = '') | ||
| 1261 | + { | ||
| 1262 | + if (!in_array($inputType, ['datetime', 'checkbox', 'select'])) { | ||
| 1263 | + return; | ||
| 1264 | + } | ||
| 1265 | + $attrField = ucfirst($this->getCamelizeName($field)); | ||
| 1266 | + if ($inputType == 'datetime') { | ||
| 1267 | + $return = <<<EOD | ||
| 1268 | +return \$value === '' ? null : (\$value && !is_numeric(\$value) ? strtotime(\$value) : \$value); | ||
| 1269 | +EOD; | ||
| 1270 | + } elseif (in_array($inputType, ['checkbox', 'select'])) { | ||
| 1271 | + $return = <<<EOD | ||
| 1272 | +return is_array(\$value) ? implode(',', \$value) : \$value; | ||
| 1273 | +EOD; | ||
| 1274 | + } | ||
| 1275 | + $setAttr[] = <<<EOD | ||
| 1276 | + protected function set{$attrField}Attr(\$value) | ||
| 1277 | + { | ||
| 1278 | + $return | ||
| 1279 | + } | ||
| 1280 | +EOD; | ||
| 1281 | + } | ||
| 1282 | + | ||
| 1283 | + protected function appendAttr(&$appendAttrList, $field) | ||
| 1284 | + { | ||
| 1285 | + $appendAttrList[] = <<<EOD | ||
| 1286 | + '{$field}_text' | ||
| 1287 | +EOD; | ||
| 1288 | + } | ||
| 1289 | + | ||
| 1290 | + /** | ||
| 1291 | + * 移除相对的空目录 | ||
| 1292 | + * @param $parseFile | ||
| 1293 | + * @param $parseArr | ||
| 1294 | + * @return bool | ||
| 1295 | + */ | ||
| 1296 | + protected function removeEmptyBaseDir($parseFile, $parseArr) | ||
| 1297 | + { | ||
| 1298 | + if (count($parseArr) > 1) { | ||
| 1299 | + $parentDir = dirname($parseFile); | ||
| 1300 | + for ($i = 0; $i < count($parseArr); $i++) { | ||
| 1301 | + try { | ||
| 1302 | + $iterator = new \FilesystemIterator($parentDir); | ||
| 1303 | + $isDirEmpty = !$iterator->valid(); | ||
| 1304 | + if ($isDirEmpty) { | ||
| 1305 | + rmdir($parentDir); | ||
| 1306 | + $parentDir = dirname($parentDir); | ||
| 1307 | + } else { | ||
| 1308 | + return true; | ||
| 1309 | + } | ||
| 1310 | + } catch (\UnexpectedValueException $e) { | ||
| 1311 | + return false; | ||
| 1312 | + } | ||
| 1313 | + } | ||
| 1314 | + } | ||
| 1315 | + return true; | ||
| 1316 | + } | ||
| 1317 | + | ||
| 1318 | + /** | ||
| 1319 | + * 获取控制器URL | ||
| 1320 | + * @param string $moduleName | ||
| 1321 | + * @param array $baseNameArr | ||
| 1322 | + * @return string | ||
| 1323 | + */ | ||
| 1324 | + protected function getControllerUrl($moduleName, $baseNameArr) | ||
| 1325 | + { | ||
| 1326 | + for ($i = 0; $i < count($baseNameArr) - 1; $i++) { | ||
| 1327 | + $temp = array_slice($baseNameArr, 0, $i + 1); | ||
| 1328 | + $temp[$i] = ucfirst($temp[$i]); | ||
| 1329 | + $controllerFile = APP_PATH . $moduleName . DS . 'controller' . DS . implode(DS, $temp) . '.php'; | ||
| 1330 | + //检测父级目录同名控制器是否存在,存在则变更URL格式 | ||
| 1331 | + if (is_file($controllerFile)) { | ||
| 1332 | + $baseNameArr = [implode('.', $baseNameArr)]; | ||
| 1333 | + break; | ||
| 1334 | + } | ||
| 1335 | + } | ||
| 1336 | + $controllerUrl = strtolower(implode('/', $baseNameArr)); | ||
| 1337 | + return $controllerUrl; | ||
| 1338 | + } | ||
| 1339 | + | ||
| 1340 | + /** | ||
| 1341 | + * 获取控制器相关信息 | ||
| 1342 | + * @param $module | ||
| 1343 | + * @param $controller | ||
| 1344 | + * @param $table | ||
| 1345 | + * @return array | ||
| 1346 | + */ | ||
| 1347 | + protected function getControllerData($module, $controller, $table) | ||
| 1348 | + { | ||
| 1349 | + return $this->getParseNameData($module, $controller, $table, 'controller'); | ||
| 1350 | + } | ||
| 1351 | + | ||
| 1352 | + /** | ||
| 1353 | + * 获取模型相关信息 | ||
| 1354 | + * @param $module | ||
| 1355 | + * @param $model | ||
| 1356 | + * @param $table | ||
| 1357 | + * @return array | ||
| 1358 | + */ | ||
| 1359 | + protected function getModelData($module, $model, $table) | ||
| 1360 | + { | ||
| 1361 | + return $this->getParseNameData($module, $model, $table, 'model'); | ||
| 1362 | + } | ||
| 1363 | + | ||
| 1364 | + /** | ||
| 1365 | + * 获取验证器相关信息 | ||
| 1366 | + * @param $module | ||
| 1367 | + * @param $validate | ||
| 1368 | + * @param $table | ||
| 1369 | + * @return array | ||
| 1370 | + */ | ||
| 1371 | + protected function getValidateData($module, $validate, $table) | ||
| 1372 | + { | ||
| 1373 | + return $this->getParseNameData($module, $validate, $table, 'validate'); | ||
| 1374 | + } | ||
| 1375 | + | ||
| 1376 | + /** | ||
| 1377 | + * 获取已解析相关信息 | ||
| 1378 | + * @param string $module 模块名称 | ||
| 1379 | + * @param string $name 自定义名称 | ||
| 1380 | + * @param string $table 数据表名 | ||
| 1381 | + * @param string $type 解析类型,本例中为controller、model、validate | ||
| 1382 | + * @return array | ||
| 1383 | + */ | ||
| 1384 | + protected function getParseNameData($module, $name, $table, $type) | ||
| 1385 | + { | ||
| 1386 | + $arr = []; | ||
| 1387 | + if (!$name) { | ||
| 1388 | + $parseName = Loader::parseName($table, 1); | ||
| 1389 | + $name = str_replace('_', '/', $table); | ||
| 1390 | + } | ||
| 1391 | + | ||
| 1392 | + $name = str_replace(['.', '/', '\\'], '/', $name); | ||
| 1393 | + $arr = explode('/', $name); | ||
| 1394 | + $parseName = ucfirst(array_pop($arr)); | ||
| 1395 | + $parseArr = $arr; | ||
| 1396 | + array_push($parseArr, $parseName); | ||
| 1397 | + //类名不能为内部关键字 | ||
| 1398 | + if (in_array(strtolower($parseName), $this->internalKeywords)) { | ||
| 1399 | + throw new Exception('Unable to use internal variable:' . $parseName); | ||
| 1400 | + } | ||
| 1401 | + $appNamespace = Config::get('app_namespace'); | ||
| 1402 | + $parseNamespace = "{$appNamespace}\\{$module}\\{$type}" . ($arr ? "\\" . implode("\\", $arr) : ""); | ||
| 1403 | + $moduleDir = APP_PATH . $module . DS; | ||
| 1404 | + $parseFile = $moduleDir . $type . DS . ($arr ? implode(DS, $arr) . DS : '') . $parseName . '.php'; | ||
| 1405 | + return [$parseNamespace, $parseName, $parseFile, $parseArr]; | ||
| 1406 | + } | ||
| 1407 | + | ||
| 1408 | + /** | ||
| 1409 | + * 写入到文件 | ||
| 1410 | + * @param string $name | ||
| 1411 | + * @param array $data | ||
| 1412 | + * @param string $pathname | ||
| 1413 | + * @return mixed | ||
| 1414 | + */ | ||
| 1415 | + protected function writeToFile($name, $data, $pathname) | ||
| 1416 | + { | ||
| 1417 | + foreach ($data as $index => &$datum) { | ||
| 1418 | + $datum = is_array($datum) ? '' : $datum; | ||
| 1419 | + } | ||
| 1420 | + unset($datum); | ||
| 1421 | + $content = $this->getReplacedStub($name, $data); | ||
| 1422 | + | ||
| 1423 | + if (!is_dir(dirname($pathname))) { | ||
| 1424 | + mkdir(dirname($pathname), 0755, true); | ||
| 1425 | + } | ||
| 1426 | + return file_put_contents($pathname, $content); | ||
| 1427 | + } | ||
| 1428 | + | ||
| 1429 | + /** | ||
| 1430 | + * 获取替换后的数据 | ||
| 1431 | + * @param string $name | ||
| 1432 | + * @param array $data | ||
| 1433 | + * @return string | ||
| 1434 | + */ | ||
| 1435 | + protected function getReplacedStub($name, $data) | ||
| 1436 | + { | ||
| 1437 | + foreach ($data as $index => &$datum) { | ||
| 1438 | + $datum = is_array($datum) ? '' : $datum; | ||
| 1439 | + } | ||
| 1440 | + unset($datum); | ||
| 1441 | + $search = $replace = []; | ||
| 1442 | + foreach ($data as $k => $v) { | ||
| 1443 | + $search[] = "{%{$k}%}"; | ||
| 1444 | + $replace[] = $v; | ||
| 1445 | + } | ||
| 1446 | + $stubname = $this->getStub($name); | ||
| 1447 | + if (isset($this->stubList[$stubname])) { | ||
| 1448 | + $stub = $this->stubList[$stubname]; | ||
| 1449 | + } else { | ||
| 1450 | + $this->stubList[$stubname] = $stub = file_get_contents($stubname); | ||
| 1451 | + } | ||
| 1452 | + $content = str_replace($search, $replace, $stub); | ||
| 1453 | + return $content; | ||
| 1454 | + } | ||
| 1455 | + | ||
| 1456 | + /** | ||
| 1457 | + * 获取基础模板 | ||
| 1458 | + * @param string $name | ||
| 1459 | + * @return string | ||
| 1460 | + */ | ||
| 1461 | + protected function getStub($name) | ||
| 1462 | + { | ||
| 1463 | + return __DIR__ . DS . 'Crud' . DS . 'stubs' . DS . $name . '.stub'; | ||
| 1464 | + } | ||
| 1465 | + | ||
| 1466 | + protected function getLangItem($field, $content) | ||
| 1467 | + { | ||
| 1468 | + if ($content || !Lang::has($field)) { | ||
| 1469 | + $this->fieldMaxLen = strlen($field) > $this->fieldMaxLen ? strlen($field) : $this->fieldMaxLen; | ||
| 1470 | + $content = str_replace(',', ',', $content); | ||
| 1471 | + if (stripos($content, ':') !== false && stripos($content, '=') !== false) { | ||
| 1472 | + list($fieldLang, $item) = explode(':', $content); | ||
| 1473 | + $itemArr = [$field => $fieldLang]; | ||
| 1474 | + foreach (explode(',', $item) as $k => $v) { | ||
| 1475 | + $valArr = explode('=', $v); | ||
| 1476 | + if (count($valArr) == 2) { | ||
| 1477 | + list($key, $value) = $valArr; | ||
| 1478 | + $itemArr[$field . ' ' . $key] = $value; | ||
| 1479 | + if ($this->headingFilterField == $field) { | ||
| 1480 | + $itemArr['Set ' . $field . ' to ' . $key] = '设为' . $value; | ||
| 1481 | + } | ||
| 1482 | + $this->fieldMaxLen = strlen($field . ' ' . $key) > $this->fieldMaxLen ? strlen($field . ' ' . $key) : $this->fieldMaxLen; | ||
| 1483 | + } | ||
| 1484 | + } | ||
| 1485 | + } else { | ||
| 1486 | + $itemArr = [$field => $content]; | ||
| 1487 | + } | ||
| 1488 | + $resultArr = []; | ||
| 1489 | + foreach ($itemArr as $k => $v) { | ||
| 1490 | + $resultArr[] = " '" . mb_ucfirst($k) . "' => '{$v}'"; | ||
| 1491 | + } | ||
| 1492 | + return implode(",\n", $resultArr); | ||
| 1493 | + } else { | ||
| 1494 | + return ''; | ||
| 1495 | + } | ||
| 1496 | + } | ||
| 1497 | + | ||
| 1498 | + /** | ||
| 1499 | + * 读取数据和语言数组列表 | ||
| 1500 | + * @param array $arr | ||
| 1501 | + * @param boolean $withTpl | ||
| 1502 | + * @return array | ||
| 1503 | + */ | ||
| 1504 | + protected function getLangArray($arr, $withTpl = true) | ||
| 1505 | + { | ||
| 1506 | + $langArr = []; | ||
| 1507 | + foreach ($arr as $k => $v) { | ||
| 1508 | + $langArr[$k] = is_numeric($k) ? ($withTpl ? "{:" : "") . "__('" . mb_ucfirst($v) . "')" . ($withTpl ? "}" : "") : $v; | ||
| 1509 | + } | ||
| 1510 | + return $langArr; | ||
| 1511 | + } | ||
| 1512 | + | ||
| 1513 | + /** | ||
| 1514 | + * 将数据转换成带字符串 | ||
| 1515 | + * @param array $arr | ||
| 1516 | + * @return string | ||
| 1517 | + */ | ||
| 1518 | + protected function getArrayString($arr) | ||
| 1519 | + { | ||
| 1520 | + if (!is_array($arr)) { | ||
| 1521 | + return $arr; | ||
| 1522 | + } | ||
| 1523 | + $stringArr = []; | ||
| 1524 | + foreach ($arr as $k => $v) { | ||
| 1525 | + $is_var = in_array(substr($v, 0, 1), ['$', '_']); | ||
| 1526 | + if (!$is_var) { | ||
| 1527 | + $v = str_replace("'", "\'", $v); | ||
| 1528 | + $k = str_replace("'", "\'", $k); | ||
| 1529 | + } | ||
| 1530 | + $stringArr[] = "'" . $k . "' => " . ($is_var ? $v : "'{$v}'"); | ||
| 1531 | + } | ||
| 1532 | + return implode(", ", $stringArr); | ||
| 1533 | + } | ||
| 1534 | + | ||
| 1535 | + protected function getItemArray($item, $field, $comment) | ||
| 1536 | + { | ||
| 1537 | + $itemArr = []; | ||
| 1538 | + $comment = str_replace(',', ',', $comment); | ||
| 1539 | + if (stripos($comment, ':') !== false && stripos($comment, ',') && stripos($comment, '=') !== false) { | ||
| 1540 | + list($fieldLang, $item) = explode(':', $comment); | ||
| 1541 | + $itemArr = []; | ||
| 1542 | + foreach (explode(',', $item) as $k => $v) { | ||
| 1543 | + $valArr = explode('=', $v); | ||
| 1544 | + if (count($valArr) == 2) { | ||
| 1545 | + list($key, $value) = $valArr; | ||
| 1546 | + $itemArr[$key] = $field . ' ' . $key; | ||
| 1547 | + } | ||
| 1548 | + } | ||
| 1549 | + } else { | ||
| 1550 | + foreach ($item as $k => $v) { | ||
| 1551 | + $itemArr[$v] = is_numeric($v) ? $field . ' ' . $v : $v; | ||
| 1552 | + } | ||
| 1553 | + } | ||
| 1554 | + return $itemArr; | ||
| 1555 | + } | ||
| 1556 | + | ||
| 1557 | + protected function getFieldType(&$v) | ||
| 1558 | + { | ||
| 1559 | + $inputType = 'text'; | ||
| 1560 | + switch ($v['DATA_TYPE']) { | ||
| 1561 | + case 'bigint': | ||
| 1562 | + case 'int': | ||
| 1563 | + case 'mediumint': | ||
| 1564 | + case 'smallint': | ||
| 1565 | + case 'tinyint': | ||
| 1566 | + $inputType = 'number'; | ||
| 1567 | + break; | ||
| 1568 | + case 'enum': | ||
| 1569 | + case 'set': | ||
| 1570 | + $inputType = 'select'; | ||
| 1571 | + break; | ||
| 1572 | + case 'decimal': | ||
| 1573 | + case 'double': | ||
| 1574 | + case 'float': | ||
| 1575 | + $inputType = 'number'; | ||
| 1576 | + break; | ||
| 1577 | + case 'longtext': | ||
| 1578 | + case 'text': | ||
| 1579 | + case 'mediumtext': | ||
| 1580 | + case 'smalltext': | ||
| 1581 | + case 'tinytext': | ||
| 1582 | + $inputType = 'textarea'; | ||
| 1583 | + break; | ||
| 1584 | + case 'year': | ||
| 1585 | + case 'date': | ||
| 1586 | + case 'time': | ||
| 1587 | + case 'datetime': | ||
| 1588 | + case 'timestamp': | ||
| 1589 | + $inputType = 'datetime'; | ||
| 1590 | + break; | ||
| 1591 | + default: | ||
| 1592 | + break; | ||
| 1593 | + } | ||
| 1594 | + $fieldsName = $v['COLUMN_NAME']; | ||
| 1595 | + // 指定后缀说明也是个时间字段 | ||
| 1596 | + if ($this->isMatchSuffix($fieldsName, $this->intDateSuffix)) { | ||
| 1597 | + $inputType = 'datetime'; | ||
| 1598 | + } | ||
| 1599 | + // 指定后缀结尾且类型为enum,说明是个单选框 | ||
| 1600 | + if ($this->isMatchSuffix($fieldsName, $this->enumRadioSuffix) && $v['DATA_TYPE'] == 'enum') { | ||
| 1601 | + $inputType = "radio"; | ||
| 1602 | + } | ||
| 1603 | + // 指定后缀结尾且类型为set,说明是个复选框 | ||
| 1604 | + if ($this->isMatchSuffix($fieldsName, $this->setCheckboxSuffix) && $v['DATA_TYPE'] == 'set') { | ||
| 1605 | + $inputType = "checkbox"; | ||
| 1606 | + } | ||
| 1607 | + // 指定后缀结尾且类型为char或tinyint且长度为1,说明是个Switch复选框 | ||
| 1608 | + if ($this->isMatchSuffix($fieldsName, $this->switchSuffix) && ($v['COLUMN_TYPE'] == 'tinyint(1)' || $v['COLUMN_TYPE'] == 'char(1)') && $v['COLUMN_DEFAULT'] !== '' && $v['COLUMN_DEFAULT'] !== null) { | ||
| 1609 | + $inputType = "switch"; | ||
| 1610 | + } | ||
| 1611 | + // 指定后缀结尾城市选择框 | ||
| 1612 | + if ($this->isMatchSuffix($fieldsName, $this->citySuffix) && ($v['DATA_TYPE'] == 'varchar' || $v['DATA_TYPE'] == 'char')) { | ||
| 1613 | + $inputType = "citypicker"; | ||
| 1614 | + } | ||
| 1615 | + // 指定后缀结尾城市选择框 | ||
| 1616 | + if ($this->isMatchSuffix($fieldsName, $this->rangeSuffix) && ($v['DATA_TYPE'] == 'varchar' || $v['DATA_TYPE'] == 'char')) { | ||
| 1617 | + $inputType = "datetimerange"; | ||
| 1618 | + } | ||
| 1619 | + // 指定后缀结尾JSON配置 | ||
| 1620 | + if ($this->isMatchSuffix($fieldsName, $this->jsonSuffix) && ($v['DATA_TYPE'] == 'varchar' || $v['DATA_TYPE'] == 'text')) { | ||
| 1621 | + $inputType = "fieldlist"; | ||
| 1622 | + } | ||
| 1623 | + // 指定后缀结尾标签配置 | ||
| 1624 | + if ($this->isMatchSuffix($fieldsName, $this->tagSuffix) && ($v['DATA_TYPE'] == 'varchar' || $v['DATA_TYPE'] == 'text')) { | ||
| 1625 | + $inputType = "tagsinput"; | ||
| 1626 | + } | ||
| 1627 | + return $inputType; | ||
| 1628 | + } | ||
| 1629 | + | ||
| 1630 | + /** | ||
| 1631 | + * 判断是否符合指定后缀 | ||
| 1632 | + * @param string $field 字段名称 | ||
| 1633 | + * @param mixed $suffixArr 后缀 | ||
| 1634 | + * @return boolean | ||
| 1635 | + */ | ||
| 1636 | + protected function isMatchSuffix($field, $suffixArr) | ||
| 1637 | + { | ||
| 1638 | + $suffixArr = is_array($suffixArr) ? $suffixArr : explode(',', $suffixArr); | ||
| 1639 | + foreach ($suffixArr as $k => $v) { | ||
| 1640 | + if (preg_match("/{$v}$/i", $field)) { | ||
| 1641 | + return true; | ||
| 1642 | + } | ||
| 1643 | + } | ||
| 1644 | + return false; | ||
| 1645 | + } | ||
| 1646 | + | ||
| 1647 | + /** | ||
| 1648 | + * 获取表单分组数据 | ||
| 1649 | + * @param string $field | ||
| 1650 | + * @param string $content | ||
| 1651 | + * @return string | ||
| 1652 | + */ | ||
| 1653 | + protected function getFormGroup($field, $content) | ||
| 1654 | + { | ||
| 1655 | + $langField = mb_ucfirst($field); | ||
| 1656 | + return <<<EOD | ||
| 1657 | + <div class="form-group"> | ||
| 1658 | + <label class="control-label col-xs-12 col-sm-2">{:__('{$langField}')}:</label> | ||
| 1659 | + <div class="col-xs-12 col-sm-8"> | ||
| 1660 | + {$content} | ||
| 1661 | + </div> | ||
| 1662 | + </div> | ||
| 1663 | +EOD; | ||
| 1664 | + } | ||
| 1665 | + | ||
| 1666 | + /** | ||
| 1667 | + * 获取图片模板数据 | ||
| 1668 | + * @param string $field | ||
| 1669 | + * @param string $content | ||
| 1670 | + * @return string | ||
| 1671 | + */ | ||
| 1672 | + protected function getImageUpload($field, $content) | ||
| 1673 | + { | ||
| 1674 | + $uploadfilter = $selectfilter = ''; | ||
| 1675 | + if ($this->isMatchSuffix($field, $this->imageField)) { | ||
| 1676 | + $uploadfilter = ' data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp,image/webp"'; | ||
| 1677 | + $selectfilter = ' data-mimetype="image/*"'; | ||
| 1678 | + } | ||
| 1679 | + $multiple = substr($field, -1) == 's' ? ' data-multiple="true"' : ' data-multiple="false"'; | ||
| 1680 | + $preview = ' data-preview-id="p-' . $field . '"'; | ||
| 1681 | + $previewcontainer = $preview ? '<ul class="row list-inline faupload-preview" id="p-' . $field . '"></ul>' : ''; | ||
| 1682 | + return <<<EOD | ||
| 1683 | +<div class="input-group"> | ||
| 1684 | + {$content} | ||
| 1685 | + <div class="input-group-addon no-border no-padding"> | ||
| 1686 | + <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> | ||
| 1687 | + <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> | ||
| 1688 | + </div> | ||
| 1689 | + <span class="msg-box n-right" for="c-{$field}"></span> | ||
| 1690 | + </div> | ||
| 1691 | + {$previewcontainer} | ||
| 1692 | +EOD; | ||
| 1693 | + } | ||
| 1694 | + | ||
| 1695 | + /** | ||
| 1696 | + * 获取JS列数据 | ||
| 1697 | + * @param string $field | ||
| 1698 | + * @param string $datatype | ||
| 1699 | + * @param string $extend | ||
| 1700 | + * @param array $itemArr | ||
| 1701 | + * @param array $fieldConfig | ||
| 1702 | + * @return string | ||
| 1703 | + */ | ||
| 1704 | + protected function getJsColumn($field, $datatype = '', $extend = '', $itemArr = [], $fieldConfig = []) | ||
| 1705 | + { | ||
| 1706 | + $lang = mb_ucfirst($field); | ||
| 1707 | + $formatter = ''; | ||
| 1708 | + foreach ($this->fieldFormatterSuffix as $k => $v) { | ||
| 1709 | + if (preg_match("/{$k}$/i", $field)) { | ||
| 1710 | + if (is_array($v)) { | ||
| 1711 | + if (in_array($datatype, $v['type'])) { | ||
| 1712 | + $formatter = $v['name']; | ||
| 1713 | + break; | ||
| 1714 | + } | ||
| 1715 | + } else { | ||
| 1716 | + $formatter = $v; | ||
| 1717 | + break; | ||
| 1718 | + } | ||
| 1719 | + } | ||
| 1720 | + } | ||
| 1721 | + $html = str_repeat(" ", 24) . "{field: '{$field}', title: __('{$lang}')"; | ||
| 1722 | + | ||
| 1723 | + if ($datatype == 'set') { | ||
| 1724 | + $formatter = 'label'; | ||
| 1725 | + } | ||
| 1726 | + foreach ($itemArr as $k => &$v) { | ||
| 1727 | + if (substr($v, 0, 3) !== '__(') { | ||
| 1728 | + $v = "__('" . mb_ucfirst($v) . "')"; | ||
| 1729 | + } | ||
| 1730 | + } | ||
| 1731 | + unset($v); | ||
| 1732 | + $searchList = json_encode($itemArr, JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE); | ||
| 1733 | + $searchList = str_replace(['":"', '"}', ')","'], ['":', '}', '),"'], $searchList); | ||
| 1734 | + if ($itemArr) { | ||
| 1735 | + $html .= ", searchList: " . $searchList; | ||
| 1736 | + } | ||
| 1737 | + | ||
| 1738 | + // 文件、图片、权重等字段默认不加入搜索栏,字符串类型默认LIKE | ||
| 1739 | + $noSearchFiles = ['file$', 'files$', 'image$', 'images$', '^weigh$']; | ||
| 1740 | + if (preg_match("/" . implode('|', $noSearchFiles) . "/i", $field)) { | ||
| 1741 | + $html .= ", operate: false"; | ||
| 1742 | + } elseif (in_array($datatype, ['varchar'])) { | ||
| 1743 | + $html .= ", operate: 'LIKE'"; | ||
| 1744 | + } | ||
| 1745 | + | ||
| 1746 | + if (in_array($datatype, ['date', 'datetime']) || $formatter === 'datetime') { | ||
| 1747 | + $html .= ", operate:'RANGE', addclass:'datetimerange', autocomplete:false"; | ||
| 1748 | + } elseif (in_array($datatype, ['float', 'double', 'decimal'])) { | ||
| 1749 | + $html .= ", operate:'BETWEEN'"; | ||
| 1750 | + } | ||
| 1751 | + if (in_array($datatype, ['set'])) { | ||
| 1752 | + $html .= ", operate:'FIND_IN_SET'"; | ||
| 1753 | + } | ||
| 1754 | + if (isset($fieldConfig['CHARACTER_MAXIMUM_LENGTH']) && $fieldConfig['CHARACTER_MAXIMUM_LENGTH'] >= 255 && in_array($datatype, ['varchar']) && !$formatter) { | ||
| 1755 | + $formatter = 'content'; | ||
| 1756 | + $html .= ", table: table, class: 'autocontent'"; | ||
| 1757 | + } | ||
| 1758 | + if (in_array($formatter, ['image', 'images'])) { | ||
| 1759 | + $html .= ", events: Table.api.events.image"; | ||
| 1760 | + } | ||
| 1761 | + if (in_array($formatter, ['toggle'])) { | ||
| 1762 | + $html .= ", table: table"; | ||
| 1763 | + } | ||
| 1764 | + if ($itemArr && !$formatter) { | ||
| 1765 | + $formatter = 'normal'; | ||
| 1766 | + } | ||
| 1767 | + if ($formatter) { | ||
| 1768 | + $html .= ", formatter: Table.api.formatter." . $formatter . "}"; | ||
| 1769 | + } else { | ||
| 1770 | + $html .= "}"; | ||
| 1771 | + } | ||
| 1772 | + return $html; | ||
| 1773 | + } | ||
| 1774 | + | ||
| 1775 | + protected function getCamelizeName($uncamelized_words, $separator = '_') | ||
| 1776 | + { | ||
| 1777 | + $uncamelized_words = $separator . str_replace($separator, " ", strtolower($uncamelized_words)); | ||
| 1778 | + return ltrim(str_replace(" ", "", ucwords($uncamelized_words)), $separator); | ||
| 1779 | + } | ||
| 1780 | + | ||
| 1781 | + protected function getFieldListName($field) | ||
| 1782 | + { | ||
| 1783 | + return $this->getCamelizeName($field) . 'List'; | ||
| 1784 | + } | ||
| 1785 | +} |
| 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-primary btn-embossed disabled">{:__('OK')}</button> | ||
| 8 | + </div> | ||
| 9 | + </div> | ||
| 10 | +</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 | +{%controllerImport%} | ||
| 29 | + | ||
| 30 | + /** | ||
| 31 | + * 默认生成的控制器所继承的父类中有index/add/edit/del/multi五个基础方法、destroy/restore/recyclebin三个回收站方法 | ||
| 32 | + * 因此在当前控制器中可不用编写增删改查的代码,除非需要自己控制这部分逻辑 | ||
| 33 | + * 需要将application/admin/library/traits/Backend.php中对应的方法复制到当前控制器,然后进行修改 | ||
| 34 | + */ | ||
| 35 | + | ||
| 36 | +{%controllerIndex%} | ||
| 37 | +} |
| 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-primary btn-embossed disabled">{:__('OK')}</button> | ||
| 8 | + </div> | ||
| 9 | + </div> | ||
| 10 | +</form> |
| 1 | + | ||
| 2 | + <table class="table fieldlist" data-name="{%fieldName%}" data-template="{%fieldName%}tpl"> | ||
| 3 | + <tr> | ||
| 4 | + {%theadList%} | ||
| 5 | + <td width="90">{:__('Operate')}</td> | ||
| 6 | + </tr> | ||
| 7 | + <tr><td colspan="{%colspan%}"> | ||
| 8 | + <a href="javascript:;" class="btn btn-sm btn-success btn-append"><i class="fa fa-plus"></i> {:__('Append')}</a> | ||
| 9 | + <textarea name="{%fieldName%}" class="form-control hide" cols="30" rows="5">{%fieldValue%}</textarea> | ||
| 10 | + </td></tr> | ||
| 11 | + </table> | ||
| 12 | + <script type="text/html" id="{%fieldName%}tpl"> | ||
| 13 | + <tr> | ||
| 14 | + {%tbodyList%} | ||
| 15 | + <td width="90"> | ||
| 16 | + <span class="btn btn-sm btn-danger btn-remove"><i class="fa fa-times"></i></span> | ||
| 17 | + <span class="btn btn-sm btn-primary btn-dragsort"><i class="fa fa-arrows"></i></span> | ||
| 18 | + </td> | ||
| 19 | + </tr> | ||
| 20 | + </script> |
| 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 | +<div class="dropdown btn-group {:$auth->check('{%controllerUrl%}/multi')?'':'hide'}"> | ||
| 2 | + <a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> {:__('More')}</a> | ||
| 3 | + <ul class="dropdown-menu text-left" role="menu"> | ||
| 4 | + {foreach name="{%fieldName%}List" item="vo"} | ||
| 5 | + <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:" data-params="{%field%}={$key}">{:__('Set {%field%} to ' . $key)}</a></li> | ||
| 6 | + {/foreach} | ||
| 7 | + </ul> | ||
| 8 | + </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 | + <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 | + {%importHtml%} | ||
| 14 | + | ||
| 15 | + {%multipleHtml%} | ||
| 16 | + | ||
| 17 | + {%recyclebinHtml%} | ||
| 18 | + </div> | ||
| 19 | + <table id="table" class="table table-striped table-bordered table-hover table-nowrap" | ||
| 20 | + data-operate-edit="{:$auth->check('{%controllerUrl%}/edit')}" | ||
| 21 | + data-operate-del="{:$auth->check('{%controllerUrl%}/del')}" | ||
| 22 | + width="100%"> | ||
| 23 | + </table> | ||
| 24 | + </div> | ||
| 25 | + </div> | ||
| 26 | + | ||
| 27 | + </div> | ||
| 28 | + </div> | ||
| 29 | +</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%}',{%fixedColumnsJs%} | ||
| 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 | + | ||
| 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 | + |
| 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: '140px', | ||
| 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 | +<?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 {%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 | +} |
-
请 注册 或 登录 后发表评论