فهرست منبع

Merge pull request #16 from jqhph/master

拉取代码
yxx 5 سال پیش
والد
کامیت
5296d69b6c
100فایلهای تغییر یافته به همراه1596 افزوده شده و 637 حذف شده
  1. 4 2
      README.md
  2. 4 2
      resources/assets/adminlte/scss/_variables.scss
  3. 20 2
      resources/assets/dcat/extra/upload.js
  4. 1 1
      resources/assets/dcat/js/dcat-app.js
  5. 1 1
      resources/assets/dcat/sass/components/_box.scss
  6. 30 0
      resources/assets/dcat/sass/components/_button.scss
  7. 1 1
      resources/assets/dcat/sass/components/_card.scss
  8. 3 0
      resources/assets/dcat/sass/components/_form.scss
  9. 1 1
      resources/assets/dcat/sass/components/_layer.scss
  10. 2 1
      resources/assets/dcat/sass/components/_menu.scss
  11. 2 2
      resources/assets/dcat/sass/components/_tab.scss
  12. 1 0
      resources/assets/dcat/sass/components/_table.scss
  13. 4 0
      resources/assets/dcat/sass/dcat-app.scss
  14. 2 1
      resources/assets/dcat/sass/theme/_colors.scss
  15. 1 0
      resources/assets/dcat/sass/variables/_variables.scss
  16. 0 0
      resources/dist/adminlte/adminlte-blue-dark.css
  17. 0 0
      resources/dist/adminlte/adminlte-blue-light.css
  18. 0 0
      resources/dist/adminlte/adminlte-blue.css
  19. 0 0
      resources/dist/adminlte/adminlte-green.css
  20. 0 0
      resources/dist/adminlte/adminlte.css
  21. 0 0
      resources/dist/dcat/css/dcat-app-blue-dark.css
  22. 0 0
      resources/dist/dcat/css/dcat-app-blue-light.css
  23. 0 0
      resources/dist/dcat/css/dcat-app-blue.css
  24. 0 0
      resources/dist/dcat/css/dcat-app-green.css
  25. 0 0
      resources/dist/dcat/css/dcat-app.css
  26. 0 0
      resources/dist/dcat/extra/upload.js
  27. 0 0
      resources/dist/dcat/extra/upload.js.map
  28. 0 0
      resources/dist/dcat/js/dcat-app.js.map
  29. 6 6
      resources/lang/zh-TW/admin.php
  30. 1 1
      resources/lang/zh-TW/select2.php
  31. 1 1
      resources/views/filter/container.blade.php
  32. 7 4
      resources/views/form/container.blade.php
  33. 1 1
      resources/views/form/hasmany.blade.php
  34. 14 15
      resources/views/form/hasmanytab.blade.php
  35. 3 3
      resources/views/form/hasmanytable.blade.php
  36. 1 1
      resources/views/form/tab.blade.php
  37. 5 7
      resources/views/grid/data-table.blade.php
  38. 11 24
      resources/views/grid/table.blade.php
  39. 3 0
      resources/views/pages/login.blade.php
  40. 11 3
      resources/views/show/panel.blade.php
  41. 7 0
      resources/views/show/row.blade.php
  42. 3 3
      resources/views/tree/branch.blade.php
  43. 1 1
      src/Actions/HasActionHandler.php
  44. 14 4
      src/Admin.php
  45. 6 7
      src/AdminServiceProvider.php
  46. 148 0
      src/Application.php
  47. 3 0
      src/Color.php
  48. 65 0
      src/Console/AppCommand.php
  49. 1 1
      src/Console/GeneratorCommand.php
  50. 12 2
      src/Console/InstallCommand.php
  51. 10 6
      src/Console/MinifyCommand.php
  52. 359 0
      src/Console/stubs/config.stub
  53. 72 100
      src/Controllers/ExtensionController.php
  54. 51 68
      src/Controllers/LogController.php
  55. 52 61
      src/Controllers/MenuController.php
  56. 30 99
      src/Controllers/PermissionController.php
  57. 0 24
      src/Controllers/RoleController.php
  58. 0 32
      src/Controllers/UserController.php
  59. 33 2
      src/Form.php
  60. 59 21
      src/Form/Concerns/HasFiles.php
  61. 0 8
      src/Form/Field/Currency.php
  62. 1 1
      src/Form/Field/Editor.php
  63. 25 10
      src/Form/Field/File.php
  64. 20 11
      src/Form/Field/HasMany.php
  65. 1 1
      src/Form/Field/ImageField.php
  66. 1 1
      src/Form/Field/Markdown.php
  67. 2 2
      src/Form/Field/Number.php
  68. 10 4
      src/Form/Field/Select.php
  69. 0 9
      src/Form/Field/Table.php
  70. 0 12
      src/Form/Field/WebUploader.php
  71. 8 2
      src/Form/NestedForm.php
  72. 55 1
      src/Form/Row.php
  73. 1 1
      src/Form/Tab.php
  74. 4 2
      src/Grid.php
  75. 4 0
      src/Grid/Column.php
  76. 27 34
      src/Grid/Column/HasDisplayers.php
  77. 1 1
      src/Grid/Column/ValueFilter.php
  78. 12 0
      src/Grid/Concerns/HasTools.php
  79. 1 1
      src/Grid/Displayers/Button.php
  80. 2 2
      src/Grid/Displayers/DialogTree.php
  81. 107 0
      src/Grid/Displayers/Editable.php
  82. 15 3
      src/Grid/Displayers/Expand.php
  83. 61 0
      src/Grid/Displayers/Limit.php
  84. 1 1
      src/Grid/Displayers/Modal.php
  85. 1 3
      src/Grid/Displayers/QRCode.php
  86. 0 1
      src/Grid/Exporters/AbstractExporter.php
  87. 10 1
      src/Grid/Model.php
  88. 57 1
      src/Grid/Tools.php
  89. 3 1
      src/Grid/Tools/CreateButton.php
  90. 5 3
      src/Grid/Tools/ExportButton.php
  91. 38 2
      src/Grid/Tools/QuickCreate.php
  92. 2 1
      src/Grid/Tools/RowSelector.php
  93. 1 0
      src/IFrameGrid.php
  94. 2 1
      src/Layout/Content.php
  95. 17 0
      src/Middleware/Application.php
  96. 1 1
      src/Middleware/LogOperation.php
  97. 2 2
      src/Models/Repositories/Menu.php
  98. 1 1
      src/Repositories/EloquentRepository.php
  99. 29 1
      src/Show.php
  100. 1 0
      src/Show/Panel.php

+ 4 - 2
README.md

@@ -42,13 +42,15 @@
 ![](https://cdn.learnku.com/uploads/images/202004/24/38389/bP75OeDbWH.png!large)
 
 
-## 功能
+## 功能特性
 
+- [x] 简洁优雅、灵活可扩展的API
 - [x] 用户管理
 - [x] RBAC权限管理,支持无限极权限节点
 - [x] 菜单管理
 - [x] 使用pjax构建无刷新页面,支持**按需加载**静态资源,可以无限扩展组件而不影响整体性能
 - [x] 松耦合的页面构建与数据操作设计,可轻松切换数据源
+- [x] 自定义页面
 - [x] 自定义主题配色
 - [x] 多主题切换功能,内置多种主题色
 - [x] 可轻松构建无菜单栏的独立页面(如可用于构建弹窗选择器等功能)
@@ -64,7 +66,7 @@
 - [x] 内置丰富的常用页面组件(如图表、数据统计卡片、下拉菜单、Tab卡片、提示工具等)
 - [x] `Section`功能(类似`Wordpress`的`Filter`和`blade`模板的`section`标签)
 - [x] 异步文件上传表单,支持分块多线程上传
-- [ ] 多应用(多后台)
+- [x] 多应用
 - [ ] 插件市场,只需在管理页面轻轻点击鼠标即可完成插件的安装、更新和卸载等操作
 
 

+ 4 - 2
resources/assets/adminlte/scss/_variables.scss

@@ -105,7 +105,9 @@ $main-footer-bg: transparent !default;
 // --------------------------------------------------------
 
 // Dark sidebar
-$sidebar-dark-bg: darken(#505b6b, 14%) !default;
+//$sidebar-dark-bg: darken(#505b6b, 14%) !default;
+//$sidebar-dark-bg: #1e1e2d !default;
+$sidebar-dark-bg: lighten(#1a1a27, 1%) !default;
 $sidebar-dark-hover-bg: hsla(100, 100%, 100%, 0.1) !default;
 $sidebar-dark-color: lighten(#C2C7D0, 10%) !default;
 $sidebar-dark-hover-color: $white !default;
@@ -116,7 +118,7 @@ $sidebar-dark-submenu-hover-color: $white !default;
 $sidebar-dark-submenu-hover-bg: $sidebar-dark-hover-bg !default;
 $sidebar-dark-submenu-active-color: $sidebar-dark-bg !default;
 $sidebar-dark-submenu-active-bg: hsla(100, 100%, 100%, 0.9) !default;
-$sidebar-dark-header-color: $white !default;
+$sidebar-dark-header-color: hsla(0,0%,100%,.7) !default;
 
 // Light sidebar
 $sidebar-light-bg: $white !default;

+ 20 - 2
resources/assets/dcat/extra/upload.js

@@ -3,6 +3,7 @@
         opts = $.extend({
             wrapper: '.web-uploader', // 图片显示容器选择器
             addFileButton: '.add-file-button', // 继续添加按钮选择器
+            inputSelector: '',
             isImage: false,
             preview: [], // 数据预览
             server: '',
@@ -66,6 +67,7 @@
 
         var $selector = $(opts.selector),
             updateColumn = opts.upload.formData.upload_column || ('webup' + Math.floor(Math.random()*10000)),
+            relation = opts.upload.formData._relation, // 一对多关联关系名称
             elementName = opts.elementName;
 
         if (typeof opts.upload.formData._id == "undefined" || !opts.upload.formData._id) {
@@ -100,7 +102,7 @@
             originalFilesNum = Dcat.helpers.len(opts.preview),
 
             // 上传表单
-            $input = $selector.find('input[name="' + elementName + '"]'),
+            $input = $selector.find(opts.inputSelector),
 
             // 获取文件视图选择器
             getFileViewSelector = function (fileId) {
@@ -357,6 +359,7 @@
                                 return uploader.removeFile(file);
                             }
                             post._column = updateColumn;
+                            post._relation = relation;
 
                             Dcat.loading();
                             $.post(opts.deleteUrl, post, function (result) {
@@ -484,7 +487,21 @@
                 return;
             }
 
-            form[updateColumn] = values.join(',');
+            if (relation) {
+                if (! relation[1]) {
+                    // 新增子表记录,则不调用update接口
+                    return;
+                }
+
+                form[relation[0]] = {};
+
+                form[relation[0]][relation[1]] = {};
+                form[relation[0]][relation[1]][updateColumn] = values.join(',');
+            } else {
+                form[updateColumn] = values.join(',');
+            }
+
+            delete form['_relation'];
             delete form['upload_column'];
 
             $.post(opts.updateServer, form);
@@ -867,6 +884,7 @@
                 Dcat.confirm(__('confirm_delete_file'), file.serverId, function () {
                     post.key = fileId;
                     post._column = updateColumn;
+                    post._relation = relation;
 
                     Dcat.loading();
                     $.post(opts.deleteUrl, post, function (result) {

+ 1 - 1
resources/assets/dcat/js/dcat-app.js

@@ -10,7 +10,7 @@
 
 import Dcat from './Dcat'
 
-import NProgress from './nprogress/NProgress.min'
+import NProgress from './NProgress/NProgress.min'
 import Ajax from './extensions/Ajax'
 import Toastr from './extensions/Toastr'
 import SweetAlert2 from './extensions/SweetAlert2'

+ 1 - 1
resources/assets/dcat/sass/components/_box.scss

@@ -1,6 +1,6 @@
 .box {
   position: relative;
-  border-radius: .45rem;
+  border-radius: $card-border-radius;
   background: #ffffff;
   margin-bottom: 10px;
   width: 100%;

+ 30 - 0
resources/assets/dcat/sass/components/_button.scss

@@ -119,3 +119,33 @@ a.btn-sm, .btn-group-sm > a.btn {
   margin-bottom: .15rem;
 }
 
+
+.btn-outline {
+  background: transparent;
+}
+.btn-primary.btn-outline {
+  color: $primary;
+  border-color: $primary;
+}
+.btn-info.btn-outline {
+  color: $info;
+  border-color: $info;
+}
+.btn-success.btn-outline {
+  color: $success;
+  border-color: $success;
+}
+.btn-danger.btn-outline {
+  color: $danger;
+  border-color: $danger;
+}
+.btn-warning.btn-outline {
+  color: $warning;
+  border-color: $warning;
+}
+
+.btn-primary.btn-outline:hover, .btn-info.btn-outline:hover, .btn-success.btn-outline:hover, .btn-danger.btn-outline:hover, .btn-warning.btn-outline:hover,
+//.btn-primary.btn-outline:focus, .btn-info.btn-outline:focus, .btn-success.btn-outline:focus, .btn-danger.btn-outline:focus, .btn-warning.btn-outline:focus,
+.btn-primary.btn-outline.active, .btn-info.btn-outline.active, .btn-success.btn-outline.active, .btn-danger.btn-outline.active, .btn-warning.btn-outline.active{
+  color: $white;
+}

+ 1 - 1
resources/assets/dcat/sass/components/_card.scss

@@ -1,7 +1,7 @@
 .card {
   box-shadow: $shadow;
   margin-bottom: 2rem;
-  border-radius: .45rem;
+  border-radius: $card-border-radius;
 }
 
 .card.dcat-box {

+ 3 - 0
resources/assets/dcat/sass/components/_form.scss

@@ -8,6 +8,7 @@
 input.form-control, div.form-control {
   max-height: 36px;
   min-height: 36px;
+  border-radius: $card-border-radius;
 }
 
 .form-control-sm {
@@ -34,6 +35,7 @@ select.form-control:not([multiple=multiple]) {
 
 .input-group-text {
   padding: 0.6rem 0.9rem;
+  border-radius: $card-border-radius;
 }
 
 .help-block {
@@ -121,6 +123,7 @@ select.form-control:not([multiple=multiple]) {
 .select2-container--default .select2-selection--multiple,
 .select2-container--default .select2-search--dropdown .select2-search__field {
   border-color: $input-border-color!important;
+  border-radius: $card-border-radius!important;
 }
 
 .select2-container--default .select2-dropdown .select2-search__field:focus, .select2-container--default .select2-search--inline .select2-search__field:focus {

+ 1 - 1
resources/assets/dcat/sass/components/_layer.scss

@@ -1,6 +1,6 @@
 .layui-layer {
   box-shadow: 0 2px 4px 0 rgba(0,0,0,.2)!important;
-  border-radius: .4rem!important;
+  border-radius: $card-border-radius!important;
 }
 
 .layui-layer-title {

+ 2 - 1
resources/assets/dcat/sass/components/_menu.scss

@@ -76,7 +76,8 @@ body.sidebar-collapse {
 
 .main-sidebar {
   .nav-sidebar .nav-item > .nav-link {
-    font-size: 1.1rem;
+    font-size: 1.05rem;
+    border-radius: .1rem;
   }
 }
 

+ 2 - 2
resources/assets/dcat/sass/components/_tab.scss

@@ -57,7 +57,7 @@
   .nav.nav-tabs {
     background: $primary;
     padding-left: 10px;
-    border-radius: .4rem;
+    border-radius: $card-border-radius;
     box-shadow: $shadow;
     border-bottom: 0;
   }
@@ -82,7 +82,7 @@
   //  background: $white;
   //  box-shadow: $shadow;
   //  margin-bottom: 5px;
-  //  border-radius: .4rem;
+  //  border-radius: $card-border-radius;
   //}
 }
 .nav-theme-success .nav.nav-tabs {

+ 1 - 0
resources/assets/dcat/sass/components/_table.scss

@@ -56,6 +56,7 @@ table.dataTable {
 .table-responsive {
   border: 0!important;
   margin-bottom: 0!important;
+  overflow-x: inherit;
 }
 .custom-data-table-header .table-responsive {
   overflow: visible!important;

+ 4 - 0
resources/assets/dcat/sass/dcat-app.scss

@@ -35,6 +35,10 @@ body, .header-navbar, .navigation, .breadcrumb, h1, h2, h3, h4, h5, h6, .h1, .h2
   font-family: $font-family-sans-serif;
 }
 
+li {
+  list-style-type: none;
+}
+
 // 颜色class定义
 @import "./colors";
 // 常用class定义

+ 2 - 1
resources/assets/dcat/sass/theme/_colors.scss

@@ -90,7 +90,8 @@ $font-color: #414750;
 
 // body背景颜色
 //$body-bg: #f9fafc;
-$body-bg: lighten($dark40, 1.5%);
+//$body-bg: lighten($dark40, 1.2%);
+$body-bg: darken(#f4f7fa, 1.2%);
 
 // 灰背景色
 $gray-bg: #f1f1f1;

+ 1 - 0
resources/assets/dcat/sass/variables/_variables.scss

@@ -28,3 +28,4 @@ $btn-group-btn-shadow: 0 2px 0 0 rgba(0,0,0,.08);
 // 菜单阴影
 $menu-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.05);
 
+$card-border-radius: .4rem;

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
resources/dist/adminlte/adminlte-blue-dark.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
resources/dist/adminlte/adminlte-blue-light.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
resources/dist/adminlte/adminlte-blue.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
resources/dist/adminlte/adminlte-green.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
resources/dist/adminlte/adminlte.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
resources/dist/dcat/css/dcat-app-blue-dark.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
resources/dist/dcat/css/dcat-app-blue-light.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
resources/dist/dcat/css/dcat-app-blue.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
resources/dist/dcat/css/dcat-app-green.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
resources/dist/dcat/css/dcat-app.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
resources/dist/dcat/extra/upload.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
resources/dist/dcat/extra/upload.js.map


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
resources/dist/dcat/js/dcat-app.js.map


+ 6 - 6
resources/lang/zh-TW/admin.php

@@ -3,8 +3,8 @@
 return [
     'scaffold' => [
         'header'            => '代碼生成器',
-        'choose'            => '選擇有數據表',
-        'table'             => '表',
+        'choose'            => '選擇有數據表',
+        'table'             => '表',
         'model'             => '模型',
         'controller'        => '控制器',
         'add_field'         => '添加欄位',
@@ -96,8 +96,8 @@ return [
     'update_failed'         => '更新失敗 !',
     'save_succeeded'        => '儲存成功!',
     'save_failed'           => '儲存失敗 !',
-    'refresh_succeeded'     => '成功重新整理!',
-    'login_successful'      => '成功登入!',
+    'refresh_succeeded'     => '刷新成功!',
+    'login_successful'      => '登入成功!',
     'choose'                => '選擇',
     'choose_file'           => '選擇檔案',
     'choose_image'          => '選擇圖片',
@@ -110,7 +110,7 @@ return [
     'created_at'            => '建立時間',
     'updated_at'            => '更新時間',
     'alert'                 => '警告',
-    'parent_id'             => '父目錄',
+    'parent_id'             => '父',
     'icon'                  => '圖示',
     'uri'                   => '路徑',
     'operation_log'         => '操作記錄',
@@ -140,7 +140,7 @@ return [
         'method' => 'HTTP方法',
         'path'   => 'HTTP路徑',
     ],
-    'all_methods_if_empty'  => '為空默認為所有方法',
+    'all_methods_if_empty'  => '所有方法預設為空',
     'all'                   => '全部',
     'current_page'          => '現在頁面',
     'selected_rows'         => '選擇的行',

+ 1 - 1
resources/lang/zh-TW/select2.php

@@ -6,6 +6,6 @@ return [
     'input_too_short'  => '請輸入至少:num個字符',
     'loading_more'     => '載入更多結果...',
     'maximum_selected' => '最多只能選擇:num個項目',
-    'no_results'       => '為者到結果',
+    'no_results'       => '未找到結果',
     'searching'        => '搜尋中...',
 ];

+ 1 - 1
resources/views/filter/container.blade.php

@@ -1,4 +1,4 @@
-<div class="card p-2 {{ $expand ? '' : 'd-none' }} {{$containerClass}}" style="{{$border}};margin-top: 10px;margin-bottom: 8px">
+<div class="card p-2 {{ $expand ? '' : 'd-none' }} {{$containerClass}}" style="{{$border}};margin-top: 10px;margin-bottom: 8px;box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.04);border-radius: .3rem">
     <div class="card-body" style="{!! $style !!}"  id="{{ $filterID }}">
         <form action="{!! $action !!}" class="form-horizontal" pjax-container method="get">
             <div class="row">

+ 7 - 4
resources/views/form/container.blade.php

@@ -4,15 +4,18 @@
         <div class="pull-right">{!! $form->renderTools() !!}</div>
     </div>
 @endif
-<div class="box-body">
+<div class="box-body" {!! $tabObj->isEmpty() ? 'style="margin-top: 10px"' : '' !!} >
     @if(!$tabObj->isEmpty())
         @include('admin::form.tab', compact('tabObj', 'form'))
     @else
         <div class="fields-group">
             @if($form->hasRows())
-                @foreach($form->rows() as $row)
-                    {!! $row->render() !!}
-                @endforeach
+                <div class="ml-2 mb-2">
+                    <input type="hidden" name="_token" value="{{ csrf_token() }}">
+                    @foreach($form->rows() as $row)
+                        {!! $row->render() !!}
+                    @endforeach
+                </div>
             @else
                 @foreach($form->fields() as $field)
                     {!! $field->render() !!}

+ 1 - 1
resources/views/form/hasmany.blade.php

@@ -52,7 +52,7 @@
     <div class="form-group row">
         <label class="{{$viewClass['label']}} control-label"></label>
         <div class="{{$viewClass['field']}}">
-            <div class="add btn btn-success btn-sm"><i class="feather icon-save"></i>&nbsp;{{ trans('admin.new') }}</div>
+            <div class="add btn btn-success btn-sm"><i class="feather icon-plus"></i>&nbsp;{{ trans('admin.new') }}</div>
         </div>
     </div>
     @endif

+ 14 - 15
resources/views/form/hasmanytab.blade.php

@@ -5,30 +5,29 @@
     .close-tab {
         position: absolute;
         font-size: 10px;
-        top: 2px;
-        right: 5px;
-        color: #94A6B0;
+        top: 20px;
+        right: 0;
         cursor: pointer;
         display: none;
     }
 </style>
 <div id="has-many-{{$column}}" class="nav-tabs-custom has-many-{{$column}}">
     <div class="row header">
-        <div class="col-md-{{$viewClass['label']}}"><h4 class="pull-right">{!! $label !!}</h4></div>
-        <div class="col-md-{{$viewClass['field']}}">
-            <button type="button" class="btn btn-white btn-sm add"><i class="fa fa-plus-circle" style="font-size: large;"></i></button>
+        <div class="{{$viewClass['label']}}"><h4 class="pull-right">{!! $label !!}</h4></div>
+        <div class="{{$viewClass['field']}}">
+            <div class="add btn btn-white btn-sm"><i class="feather icon-plus"></i>&nbsp;{{ trans('admin.new') }}</div>
         </div>
     </div>
 
-    <hr style="margin-top: 0px;">
+    <hr class="mb-0 mt-0">
 
     <ul class="nav nav-tabs">
         @foreach($forms as $pk => $form)
-            <li class="@if ($form == reset($forms)) active @endif ">
-                <a href="#{{ $relationName . '_' . $pk }}" data-toggle="tab">
-                    {{ $pk }} <i class="fa fa-exclamation-circle text-red hide"></i>
+            <li class="nav-item ">
+                <a href="#{{ $relationName . '_' . $pk }}" class="nav-link @if ($form == reset($forms)) active @endif " data-toggle="tab">
+                    {{ $pk }} <i class="feather icon-alert-circle text-red d-none"></i>
                 </a>
-                <i class="close-tab fa fa-times" ></i>
+                <i class="close-tab feather icon-trash text-red"></i>
             </li>
         @endforeach
 
@@ -46,11 +45,11 @@
     </div>
 
     <template class="nav-tab-tpl">
-        <li class="new">
-            <a href="#{{ $relationName . '_new_' . \Dcat\Admin\Form\NestedForm::DEFAULT_KEY_NAME }}" data-toggle="tab">
-                &nbsp;New {{ \Dcat\Admin\Form\NestedForm::DEFAULT_KEY_NAME }} <i class="fa fa-exclamation-circle text-red hide"></i>
+        <li class="new nav-item">
+            <a href="#{{ $relationName . '_new_' . \Dcat\Admin\Form\NestedForm::DEFAULT_KEY_NAME }}" class="nav-link" data-toggle="tab">
+                &nbsp;New {{ \Dcat\Admin\Form\NestedForm::DEFAULT_KEY_NAME }} <i class="feather icon-alert-circle text-red d-none"></i>
             </a>
-            <i class="close-tab fa fa-times" ></i>
+            <i class="close-tab feather icon-trash text-red" ></i>
         </li>
     </template>
     <template class="pane-tpl">

+ 3 - 3
resources/views/form/hasmanytable.blade.php

@@ -42,7 +42,7 @@
                         @if($options['allowDelete'])
                             <td class="form-group">
                                 <div>
-                                    <div class="remove btn btn-white btn-sm pull-right"><i class="feather icon-trash">&nbsp;</i>{{ trans('admin.remove') }}</div>
+                                    <div class="remove btn btn-white btn-sm pull-right"><i class="feather icon-trash"></i></div>
                                 </div>
                             </td>
                         @endif
@@ -58,7 +58,7 @@
 
                     <td class="form-group">
                         <div>
-                            <div class="remove btn btn-white btn-sm pull-right"><i class="feather icon-trash">&nbsp;</i>{{ trans('admin.remove') }}</div>
+                            <div class="remove btn btn-white btn-sm pull-right"><i class="feather icon-trash"></i></div>
                         </div>
                     </td>
                 </tr>
@@ -67,7 +67,7 @@
             @if($options['allowCreate'])
                 <div class="form-group row m-t-10">
                     <div class="{{$viewClass['field']}}" style="margin-top: 8px">
-                        <div class="add btn btn-success btn-sm"><i class="feather icon-save"></i>&nbsp;{{ trans('admin.new') }}</div>
+                        <div class="add btn btn-success btn-sm"><i class="feather icon-plus"></i>&nbsp;{{ trans('admin.new') }}</div>
                     </div>
                 </div>
             @endif

+ 1 - 1
resources/views/form/tab.blade.php

@@ -1,5 +1,5 @@
 <div>
-    <ul class="nav nav-tabs pl-1">
+    <ul class="nav nav-tabs pl-1" style="margin-top: -1rem">
         @foreach($tabObj->getTabs() as $tab)
             <li class="nav-item">
                 <a class="nav-link {{ $tab['active'] ? 'active' : '' }}" href="#{{ $tab['id'] }}" data-toggle="tab">

+ 5 - 7
resources/views/grid/data-table.blade.php

@@ -4,23 +4,21 @@
     @if ($grid->allowToolbar())
         <div class="custom-data-table-header">
             <div class="table-responsive">
-                <div class="top" style="padding: 0">
+                <div class="top d-block" style="padding: 0">
                 @if(!empty($title))
-                    <h4 style="margin:5px 10px 0;">
+                    <h4 class="pull-left" style="margin:5px 10px 0;">
                         {!! $title !!}&nbsp;
                         @if(!empty($description))
                             <small>{!! $description!!}</small>
                         @endif
                     </h4>
-                    <div data-responsive-table-toolbar="{{$tableId}}">
+                    <div class="pull-right" data-responsive-table-toolbar="{{$tableId}}">
                         {!! $grid->renderTools() !!} {!! $grid->renderCreateButton() !!} {!! $grid->renderExportButton() !!}  {!! $grid->renderQuickSearch() !!}
                     </div>
                 @else
-                    <div>
-                        {!! $grid->renderTools() !!}  {!! $grid->renderQuickSearch() !!}
-                    </div>
+                    {!! $grid->renderTools() !!}  {!! $grid->renderQuickSearch() !!}
 
-                    <div data-responsive-table-toolbar="{{$tableId}}">
+                    <div class="pull-right" data-responsive-table-toolbar="{{$tableId}}">
                         {!! $grid->renderCreateButton() !!} {!! $grid->renderExportButton() !!}
                     </div>
                 @endif

+ 11 - 24
resources/views/grid/table.blade.php

@@ -2,25 +2,23 @@
 <div class="card dcat-box dt-bootstrap4">
 
     @if ($grid->allowToolbar())
-        <div class="data-list-view-header card-header">
-            <div class="table-responsive">
-                <div class="top" style="padding: 0;margin: 0">
+        <div class="data-list-view-header card-header p-1 d-block">
+            <div class="table-responsive d-block">
+                <div class="top d-block" style="padding: 0">
                     @if(!empty($title))
-                        <h4 style="margin:5px 10px 0;">
+                        <h4 class="pull-left" style="margin:5px 10px 0;">
                             {!! $title !!}&nbsp;
                             @if(!empty($description))
                                 <small>{!! $description!!}</small>
                             @endif
                         </h4>
-                        <div data-responsive-table-toolbar="{{$tableId}}">
-                            {!! $grid->renderTools() !!} {!! $grid->renderCreateButton() !!} {!! $grid->renderExportButton() !!} {!! $grid->renderQuickSearch() !!}
+                        <div class="pull-right" data-responsive-table-toolbar="{{$tableId}}">
+                            {!! $grid->renderTools() !!} {!! $grid->renderCreateButton() !!} {!! $grid->renderExportButton() !!}  {!! $grid->renderQuickSearch() !!}
                         </div>
                     @else
-                        <div>
-                            {!! $grid->renderTools() !!}  {!! $grid->renderQuickSearch() !!}
-                        </div>
+                        {!! $grid->renderTools() !!}  {!! $grid->renderQuickSearch() !!}
 
-                        <div data-responsive-table-toolbar="{{$tableId}}">
+                        <div class="pull-right" data-responsive-table-toolbar="{{$tableId}}">
                             {!! $grid->renderCreateButton() !!} {!! $grid->renderExportButton() !!}
                         </div>
                     @endif
@@ -33,7 +31,7 @@
 
     {!! $grid->renderHeader() !!}
 
-    <div class="table-responsive table-wrapper complex-container" style="{!! $grid->option('show_bordered') ? 'padding:3px 10px 10px' : '' !!};border-bottom: 1px solid #f8f8f8!important;">
+    <div class="table-responsive table-wrapper complex-container table-middle" style="{!! $grid->option('show_bordered') ? 'padding:3px 10px 10px' : '' !!};border-bottom: 1px solid #f8f8f8!important;">
         <table
                 class="table dt-checkboxes-select
                 {{ $grid->getComplexHeaders() ? 'complex-headers' : ''}}
@@ -84,11 +82,11 @@
     {!! $grid->renderFooter() !!}
 
     @if ($paginator = $grid->paginator())
-        <div class="box-footer clearfix mt-1" style="padding:0 1rem;">
+        <div class="box-footer clearfix d-block" style="border-top: 0">
             {!! $paginator->render() !!}
         </div>
     @else
-        <div class="box-footer clearfix  mt-1 " style="height:48px;line-height:25px;">
+        <div class="box-footer clearfix" style="height:48px;line-height:25px;">
             @if ($grid->rows()->isEmpty())
                 {!! trans('admin.pagination.range', ['first' => '<b>0</b>', 'last' => '<b>'.$grid->rows()->count().'</b>', 'total' => '<b>'.$grid->rows()->count().'</b>',]) !!}
             @else
@@ -107,15 +105,4 @@
         left: 1.1rem;
     }
 </style>
-<script>
-(function () {
-    function change() {
-        $('#{{ $tableId }}').parents('.card').find('.data-list-view-header').first().find('.btn').addClass('btn-sm');
-    }
-    change();
 
-    Dcat.ready(function () {
-        setTimeout(change, 100);
-    })
-})();
-</script>

+ 3 - 0
resources/views/pages/login.blade.php

@@ -16,6 +16,9 @@
     .content {
         overflow-x: hidden;
     }
+    .form-group .control-label {
+        text-align: left;
+    }
 </style>
 
 <div class="login-page bg-40">

+ 11 - 3
resources/views/show/panel.blade.php

@@ -6,9 +6,17 @@
 @endif
 <div class="box-body">
     <div class="form-horizontal mt-1">
-        @foreach($fields as $field)
-            {!! $field->render() !!}
-        @endforeach
+        @if($rows->isEmpty())
+            @foreach($fields as $field)
+                {!! $field->render() !!}
+            @endforeach
+        @else
+            <div>
+                @foreach($rows as $row)
+                    {!! $row->render() !!}
+                @endforeach
+            </div>
+        @endif
         <div class="clearfix"></div>
     </div>
 </div>

+ 7 - 0
resources/views/show/row.blade.php

@@ -0,0 +1,7 @@
+<div class="row">
+    @foreach($fields as $field)
+        <div class="col-md-{{ $field['width'] }}">
+            {!! $field['element']->render() !!}
+        </div>
+    @endforeach
+</div>

+ 3 - 3
resources/views/tree/branch.blade.php

@@ -3,15 +3,15 @@
         {!! $branchCallback($branch) !!}
         <span class="pull-right dd-nodrag">
             @if($useEdit)
-            <a href="{{ $path }}/{{ $branch[$keyName] }}/edit"><i class="feather icon-edit-1"></i>&nbsp;</a>
+            <a href="{{ $currentUrl }}/{{ $branch[$keyName] }}/edit"><i class="feather icon-edit-1"></i>&nbsp;</a>
             @endif
 
             @if($useQuickEdit)
-                <a href="javascript:void(0);" data-url="{{ $path }}/{{ $branch[$keyName] }}/edit" class="tree-quick-edit"><i class="feather icon-edit"></i></a>
+                <a href="javascript:void(0);" data-url="{{ $currentUrl }}/{{ $branch[$keyName] }}/edit" class="tree-quick-edit"><i class="feather icon-edit"></i></a>
             @endif
 
             @if($useDelete)
-            <a href="javascript:void(0);" data-url="{{ $path }}/{{ $branch[$keyName] }}" data-action="delete"><i class="feather icon-trash"></i></a>
+            <a href="javascript:void(0);" data-message="ID - {{ $branch[$keyName] }}" data-url="{{ $currentUrl }}/{{ $branch[$keyName] }}" data-action="delete"><i class="feather icon-trash"></i></a>
             @endif
         </span>
     </div>

+ 1 - 1
src/Actions/HasActionHandler.php

@@ -66,7 +66,7 @@ trait HasActionHandler
      */
     public function handlerRoute()
     {
-        return route('dcat.api.action');
+        return route(admin_api_route('action'));
     }
 
     /**

+ 14 - 4
src/Admin.php

@@ -35,7 +35,7 @@ class Admin
      *
      * @var string
      */
-    const VERSION = '1.3.0';
+    const VERSION = '1.4.0';
 
     /**
      * @var array
@@ -230,14 +230,16 @@ class Admin
     /**
      * 注册api路由.
      *
+     * @param string $as
+     *
      * @return void
      */
-    public static function registerApiRoutes()
+    public static function registerApiRoutes(string $as = null)
     {
         $attributes = [
-            'prefix' => admin_base_path('dcat-api'),
+            'prefix'     => admin_base_path('dcat-api'),
             'middleware' => config('admin.route.middleware'),
-            'as' => 'dcat.api.',
+            'as'         => $as ?: static::app()->getApiRoutePrefix(Application::DEFAULT),
         ];
 
         app('router')->group($attributes, function ($router) {
@@ -311,6 +313,14 @@ class Admin
         return new Proxy($repository);
     }
 
+    /**
+     * @return Application
+     */
+    public static function app()
+    {
+        return app('admin.app');
+    }
+
     /**
      * 获取所有已注册的扩展.
      *

+ 6 - 7
src/AdminServiceProvider.php

@@ -33,6 +33,7 @@ class AdminServiceProvider extends ServiceProvider
         Console\ActionCommand::class,
         Console\MenuCacheCommand::class,
         Console\MinifyCommand::class,
+        Console\AppCommand::class,
     ];
 
     /**
@@ -55,6 +56,7 @@ class AdminServiceProvider extends ServiceProvider
         'admin.bootstrap'  => Middleware\Bootstrap::class,
         'admin.session'    => Middleware\Session::class,
         'admin.upload'     => Middleware\WebUploader::class,
+        'admin.app'        => Middleware\Application::class,
     ];
 
     /**
@@ -77,7 +79,7 @@ class AdminServiceProvider extends ServiceProvider
         $this->registerDefaultSections();
         $this->registerViews();
         $this->ensureHttps();
-        $this->registerRoutes();
+        $this->bootApplication();
         $this->registerPublishing();
         $this->compatibleBlade();
     }
@@ -127,13 +129,9 @@ class AdminServiceProvider extends ServiceProvider
     /**
      * 路由注册.
      */
-    protected function registerRoutes()
+    protected function bootApplication()
     {
-        Admin::registerApiRoutes();
-
-        if (is_file($routes = admin_path('routes.php'))) {
-            $this->loadRoutesFrom($routes);
-        }
+        Admin::app()->boot();
     }
 
     /**
@@ -211,6 +209,7 @@ class AdminServiceProvider extends ServiceProvider
 
     protected function registerServices()
     {
+        $this->app->singleton('admin.app', Application::class);
         $this->app->singleton('admin.asset', Asset::class);
         $this->app->singleton('admin.color', Color::class);
         $this->app->singleton('admin.sections', SectionManager::class);

+ 148 - 0
src/Application.php

@@ -0,0 +1,148 @@
+<?php
+
+namespace Dcat\Admin;
+
+use Illuminate\Contracts\Container\Container;
+use Illuminate\Support\Facades\Route;
+
+class Application
+{
+    const DEFAULT = 'admin';
+
+    /**
+     * @var Container
+     */
+    protected $app;
+
+    /**
+     * 所有启用应用的配置.
+     *
+     * @var array
+     */
+    protected $configs = [];
+
+    /**
+     * 当前应用名称.
+     *
+     * @var string
+     */
+    protected $name;
+
+    public function __construct(Container $app)
+    {
+        $this->app = $app;
+    }
+
+    /**
+     * 设置当前应用配置.
+     *
+     * @param string $app
+     */
+    public function current(string $app = null)
+    {
+        $this->withName($app);
+
+        $this->withConfig($this->name);
+    }
+
+    /**
+     * 设置应用名称.
+     *
+     * @param string $app
+     */
+    public function withName(string $app)
+    {
+        $this->name = $app;
+    }
+
+    /**
+     * 获取当前应用名称.
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->name ?: static::DEFAULT;
+    }
+
+    /**
+     * 注册应用.
+     */
+    public function boot()
+    {
+        $this->registerRoute(static::DEFAULT);
+
+        if ($this->app->runningInConsole()) {
+            return;
+        }
+        foreach ((array) config('admin.multi_app') as $app => $enable) {
+            if ($enable) {
+                $this->registerRoute($app);
+            }
+        }
+    }
+
+    /**
+     * @return string
+     */
+    public function getCurrentApiRoutePrefix()
+    {
+        return $this->getApiRoutePrefix($this->getName());
+    }
+
+    /**
+     * @param string|null $app
+     *
+     * @return string
+     */
+    public function getApiRoutePrefix(?string $app)
+    {
+        return "dcat.api.{$app}.";
+    }
+
+    /**
+     * 注册应用路由.
+     *
+     * @param string|null $app
+     */
+    protected function registerRoute(?string $app)
+    {
+        $this->withConfig($app);
+
+        Admin::registerApiRoutes($this->getApiRoutePrefix($app));
+
+        if (is_file($routes = admin_path('routes.php'))) {
+            $this->loadRoutesFrom($routes, $app);
+        }
+    }
+
+    /**
+     * 设置应用配置.
+     *
+     * @param string $app
+     */
+    protected function withConfig(string $app)
+    {
+        if (! isset($this->configs[$app])) {
+            $this->configs[$app] = config($app);
+            $this->configs[$app]['current_app'] = $app;
+        }
+
+        config(['admin' => $this->configs[$app]]);
+    }
+
+    /**
+     * 加载路由文件.
+     *
+     * @param  string  $path
+     * @param  string  $app
+     *
+     * @return void
+     */
+    protected function loadRoutesFrom(string $path, ?string $app)
+    {
+        if (! $this->app->routesAreCached()) {
+            Route::middleware('admin.app:'.$app)->group($path);
+        }
+    }
+}

+ 3 - 0
src/Color.php

@@ -3,6 +3,7 @@
 namespace Dcat\Admin;
 
 use Dcat\Admin\Support\Helper;
+use Illuminate\Support\Traits\Macroable;
 
 /**
  * Class Color.
@@ -57,6 +58,8 @@ use Dcat\Admin\Support\Helper;
  */
 class Color
 {
+    use Macroable;
+
     const DEFAULT_COLOR = 'indigo';
 
     /**

+ 65 - 0
src/Console/AppCommand.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace Dcat\Admin\Console;
+
+use Dcat\Admin\Support\Helper;
+use Illuminate\Filesystem\Filesystem;
+
+class AppCommand extends InstallCommand
+{
+    /**
+     * The console command name.
+     *
+     * @var string
+     */
+    protected $signature = 'admin:app {name}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Create new application';
+
+    /**
+     * Execute the console command.
+     *
+     * @return void
+     */
+    public function handle()
+    {
+        $this->addConfig();
+        $this->initAdminDirectory();
+
+        $this->info('Done.');
+    }
+
+    protected function addConfig()
+    {
+        /* @var Filesystem $files */
+        $files = $this->laravel['files'];
+
+        $app = Helper::slug($namespace = $this->argument('name'));
+
+        $files->put(
+            $config = config_path($app.'.php'),
+            str_replace(
+                ['DummyNamespace', 'DummyApp'],
+                [$namespace, $app],
+                $files->get(__DIR__.'/stubs/config.stub')
+            )
+        );
+
+        config(['admin' => include $config]);
+    }
+
+    /**
+     * Set admin directory.
+     *
+     * @return void
+     */
+    protected function setDirectory()
+    {
+        $this->directory = app_path($this->argument('name'));
+    }
+}

+ 1 - 1
src/Console/GeneratorCommand.php

@@ -57,7 +57,7 @@ abstract class GeneratorCommand extends BaseCommand
         if (! Str::startsWith(config('admin.route.namespace'), 'App')) {
             $dir = explode('\\', config('admin.route.namespace'))[0];
 
-            $this->baseDirectory = trim($this->ask('Please enter the destination class path', Helper::slug($dir)));
+            $this->baseDirectory = trim($this->ask('Please enter the application path', Helper::slug($dir)));
         }
     }
 }

+ 12 - 2
src/Console/InstallCommand.php

@@ -59,13 +59,23 @@ class InstallCommand extends Command
     }
 
     /**
-     * Initialize the admAin directory.
+     * Set admin directory.
      *
      * @return void
      */
-    protected function initAdminDirectory()
+    protected function setDirectory()
     {
         $this->directory = config('admin.directory');
+    }
+
+    /**
+     * Initialize the admin directory.
+     *
+     * @return void
+     */
+    protected function initAdminDirectory()
+    {
+        $this->setDirectory();
 
         if (is_dir($this->directory)) {
             $this->warn("{$this->directory} directory already exists !");

+ 10 - 6
src/Console/MinifyCommand.php

@@ -151,16 +151,20 @@ class MinifyCommand extends Command
         $mixFile = $this->getMixFile();
         $mixBakFile = $this->getMixBakFile();
 
-        $this->files->delete($mixFile);
-        $this->files->copy($mixBakFile, $mixFile);
-        $this->files->delete($mixBakFile);
+        if (is_file($mixBakFile)) {
+            $this->files->delete($mixFile);
+            $this->files->copy($mixBakFile, $mixFile);
+            $this->files->delete($mixBakFile);
+        }
 
         $colorFile = $this->getColorFile();
         $colorBakFile = $this->getColorBakFile();
 
-        $this->files->delete($colorFile);
-        $this->files->copy($colorBakFile, $colorFile);
-        $this->files->delete($colorBakFile);
+        if (is_file($colorBakFile)) {
+            $this->files->delete($colorFile);
+            $this->files->copy($colorBakFile, $colorFile);
+            $this->files->delete($colorBakFile);
+        }
     }
 
     /**

+ 359 - 0
src/Console/stubs/config.stub

@@ -0,0 +1,359 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin name
+    |--------------------------------------------------------------------------
+    |
+    | This value is the name of dcat-admin, This setting is displayed on the
+    | login page.
+    |
+    */
+    'name' => 'Dcat Admin',
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin logo
+    |--------------------------------------------------------------------------
+    |
+    | The logo of all admin pages. You can also set it as an image by using a
+    | `img` tag, eg '<img src="http://logo-url" alt="Admin logo">'.
+    |
+    */
+    'logo' => '<img src="/vendors/dcat-admin/images/logo.png" width="35"> &nbsp;Dcat Admin',
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin mini logo
+    |--------------------------------------------------------------------------
+    |
+    | The logo of all admin pages when the sidebar menu is collapsed. You can
+    | also set it as an image by using a `img` tag, eg
+    | '<img src="http://logo-url" alt="Admin logo">'.
+    |
+    */
+    'logo-mini' => '<img src="/vendors/dcat-admin/images/logo.png">',
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin route settings
+    |--------------------------------------------------------------------------
+    |
+    | The routing configuration of the admin page, including the path prefix,
+    | the controller namespace, and the default middleware. If you want to
+    | access through the root path, just set the prefix to empty string.
+    |
+    */
+    'route' => [
+
+        'prefix' => 'DummyApp',
+
+        'namespace' => 'App\\DummyNamespace\\Controllers',
+
+        'middleware' => ['web', 'admin'],
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin install directory
+    |--------------------------------------------------------------------------
+    |
+    | The installation directory of the controller and routing configuration
+    | files of the administration page. The default is `app/Admin`, which must
+    | be set before running `artisan admin::install` to take effect.
+    |
+    */
+    'directory' => app_path('DummyNamespace'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin html title
+    |--------------------------------------------------------------------------
+    |
+    | Html title for all pages.
+    |
+    */
+    'title' => 'Admin',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Assets hostname
+    |--------------------------------------------------------------------------
+    |
+   */
+    'assets_server' => env('ADMIN_ASSETS_SERVER'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Access via `https`
+    |--------------------------------------------------------------------------
+    |
+    | If your page is going to be accessed via https, set it to `true`.
+    |
+    */
+    'https' => env('ADMIN_HTTPS', false),
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin auth setting
+    |--------------------------------------------------------------------------
+    |
+    | Authentication settings for all admin pages. Include an authentication
+    | guard and a user provider setting of authentication driver.
+    |
+    | You can specify a controller for `login` `logout` and other auth routes.
+    |
+    */
+    'auth' => [
+        'enable' => true,
+
+        'controller' => App\DummyNamespace\Controllers\AuthController::class,
+
+        'guard' => 'admin',
+
+        'guards' => [
+            'admin' => [
+                'driver'   => 'session',
+                'provider' => 'admin',
+            ],
+        ],
+
+        'providers' => [
+            'admin' => [
+                'driver' => 'eloquent',
+                'model'  => Dcat\Admin\Models\Administrator::class,
+            ],
+        ],
+
+        // Add "remember me" to login form
+        'remember' => true,
+
+        // All method to path like: auth/users/*/edit
+        // or specific method to path like: get:auth/users.
+        'except' => [
+            'auth/login',
+            'auth/logout',
+        ],
+
+    ],
+
+    'grid' => [
+
+        /*
+        |--------------------------------------------------------------------------
+        | The global Grid action display class.
+        |--------------------------------------------------------------------------
+        */
+        'grid_action_class' => Dcat\Admin\Grid\Displayers\DropdownActions::class,
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin helpers setting.
+    |--------------------------------------------------------------------------
+    */
+    'helpers' => [
+        'enable' => true,
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin permission setting
+    |--------------------------------------------------------------------------
+    |
+    | Permission settings for all admin pages.
+    |
+    */
+    'permission' => [
+        // Whether enable permission.
+        'enable' => true,
+
+        // All method to path like: auth/users/*/edit
+        // or specific method to path like: get:auth/users.
+        'except' => [
+            '/',
+            'auth/login',
+            'auth/logout',
+            'auth/setting',
+        ],
+
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin menu setting
+    |--------------------------------------------------------------------------
+    |
+    */
+    'menu' => [
+        'cache' => [
+            // enable cache or not
+            'enable' => false,
+            'store'  => 'file',
+        ],
+
+        // Whether enable menu bind to a permission.
+        'bind_permission' => true,
+
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin upload setting
+    |--------------------------------------------------------------------------
+    |
+    | File system configuration for form upload files and images, including
+    | disk and upload path.
+    |
+    */
+    'upload' => [
+
+        // Disk in `config/filesystem.php`.
+        'disk' => 'admin',
+
+        // Image and file upload path under the disk above.
+        'directory' => [
+            'image' => 'images',
+            'file'  => 'files',
+        ],
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | dcat-admin database settings
+    |--------------------------------------------------------------------------
+    |
+    | Here are database settings for dcat-admin builtin model & tables.
+    |
+    */
+    'database' => [
+
+        // Database connection for following tables.
+        'connection' => '',
+
+        // User tables and model.
+        'users_table' => 'admin_users',
+        'users_model' => Dcat\Admin\Models\Administrator::class,
+
+        // Role table and model.
+        'roles_table' => 'admin_roles',
+        'roles_model' => Dcat\Admin\Models\Role::class,
+
+        // Permission table and model.
+        'permissions_table' => 'admin_permissions',
+        'permissions_model' => Dcat\Admin\Models\Permission::class,
+
+        // Menu table and model.
+        'menu_table' => 'admin_menu',
+        'menu_model' => Dcat\Admin\Models\Menu::class,
+
+        // Pivot table for table above.
+        'operation_log_table'    => 'admin_operation_log',
+        'user_permissions_table' => 'admin_user_permissions',
+        'role_users_table'       => 'admin_role_users',
+        'role_permissions_table' => 'admin_role_permissions',
+        'role_menu_table'        => 'admin_role_menu',
+        'permission_menu_table'  => 'admin_permission_menu',
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | User operation log setting
+    |--------------------------------------------------------------------------
+    |
+    | By setting this option to open or close operation log in dcat-admin.
+    |
+    */
+    'operation_log' => [
+
+        'enable' => true,
+
+        // Only logging allowed methods in the list
+        'allowed_methods' => ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'],
+
+        'secret_fields' => [
+            'password',
+            'password_confirmation',
+        ],
+
+        // Routes that will not log to database.
+        // All method to path like: auth/logs/*/edit
+        // or specific method to path like: get:auth/logs.
+        'except' => [
+            'auth/logs*',
+        ],
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Admin map field provider
+    |--------------------------------------------------------------------------
+    |
+    | Supported: "tencent", "google", "yandex".
+    |
+    */
+    'map_provider' => 'google',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Application layout
+    |--------------------------------------------------------------------------
+    |
+    | This value is the layout of admin pages.
+    */
+    'layout' => [
+        // indigo, blue, blue-light, blue-dark, green
+        'color' => 'indigo',
+
+        'body_class' => '',
+
+        'sidebar_collapsed' => false,
+
+        'sidebar_dark' => false,
+
+        // bg-primary, bg-info, bg-warning, bg-success, bg-danger, bg-dark
+        'navbar_color' => '',
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Login page background image
+    |--------------------------------------------------------------------------
+    |
+    | This value is used to set the background image of login page.
+    |
+    */
+    'login_background_image' => '',
+
+    /*
+    |--------------------------------------------------------------------------
+    | The exception handler class
+    |--------------------------------------------------------------------------
+    |
+    */
+    'exception_handler' => \Dcat\Admin\Exception\Handler::class,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Enable default breadcrumb
+    |--------------------------------------------------------------------------
+    |
+    | Whether enable default breadcrumb for every page content.
+    */
+    'enable_default_breadcrumb' => true,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Settings for extensions.
+    |--------------------------------------------------------------------------
+    |
+    | You can find all available extensions here
+    | https://github.com/dcat-admin-extensions.
+    |
+    */
+    'extensions' => [
+
+    ],
+];

+ 72 - 100
src/Controllers/ExtensionController.php

@@ -19,13 +19,6 @@ class ExtensionController extends Controller
 {
     use HasResourceActions;
 
-    /**
-     * Index interface.
-     *
-     * @param Content $content
-     *
-     * @return Content
-     */
     public function index(Content $content)
     {
         $this->define();
@@ -36,9 +29,6 @@ class ExtensionController extends Controller
             ->body($this->grid());
     }
 
-    /**
-     * @return \Illuminate\Http\JsonResponse
-     */
     public function import()
     {
         $extension = request('id');
@@ -63,87 +53,82 @@ class ExtensionController extends Controller
      */
     protected function grid()
     {
-        $grid = new Grid(new Extension());
-
-        $grid->number();
-        $grid->name;
-        $grid->version;
-        $grid->alias;
-
-        $grid->description
-            ->if(function () {
-                return mb_strlen($this->description) > 14;
-            })
-            ->limit(14)
-            ->expand(function ($expand) {
-                if (! $this->description) {
-                    return;
-                }
-
-                return "<div style='padding:10px 20px'>{$this->description}</div>";
-            });
+        return new Grid(new Extension(), function (Grid $grid) {
+            $grid->number();
+            $grid->name;
+            $grid->version;
+            $grid->alias;
+
+            $grid->description
+                ->if(function () {
+                    return mb_strlen($this->description) > 14;
+                })
+                ->display(function ($v) {
+                    return mb_substr($v, 0, 14);
+                })
+                ->expand(function ($expand) {
+                    if (! $this->description) {
+                        return;
+                    }
 
-        $grid->authors;
-        $grid->enable->switch();
-        $grid->imported;
-
-        $view = trans('admin.view');
-        $grid->config
-            ->if(function () {
-                return $this->config ? true : false;
-            })
-            ->display($view)
-            ->expand($this->getExpandHandler('config'))
-            ->else()
-            ->emptyString();
-
-        $grid->require
-            ->if(function () {
-                return $this->require ? true : false;
-            })
-            ->display($view)
-            ->expand($this->getExpandHandler())
-            ->else()
-            ->emptyString();
-
-        $grid->require_dev
-            ->if(function () {
-                return $this->require_dev ? true : false;
-            })
-            ->display($view)
-            ->expand($this->getExpandHandler('require_dev'))
-            ->else()
-            ->emptyString();
-
-        $grid->disablePagination();
-        $grid->disableCreateButton();
-        $grid->disableDeleteButton();
-        $grid->disableBatchDelete();
-        $grid->disableFilterButton();
-        $grid->disableFilter();
-        $grid->disableQuickEditButton();
-        $grid->disableEditButton();
-        $grid->disableDeleteButton();
-        $grid->disableViewButton();
-
-        $grid->actions(new ImportButton());
-
-        $grid->quickCreate(function (Grid\Tools\QuickCreate $create) {
-            $create->text('package_name')->required();
-            $create->text('namespace')
-                ->attribute('style', 'width:240px')
-                ->required()
-                ->default('Dcat\\Admin\\Extension\\:Name');
+                    return "<div style='padding:10px 20px'>{$this->description}</div>";
+                });
+
+            $grid->authors;
+            $grid->enable->switch();
+            $grid->imported;
+
+            $view = trans('admin.view');
+            $grid->config
+                ->if(function () {
+                    return $this->config ? true : false;
+                })
+                ->display($view)
+                ->expand($this->getExpandHandler('config'))
+                ->else()
+                ->emptyString();
+
+            $grid->require
+                ->if(function () {
+                    return $this->require ? true : false;
+                })
+                ->display($view)
+                ->expand($this->getExpandHandler())
+                ->else()
+                ->emptyString();
+
+            $grid->require_dev
+                ->if(function () {
+                    return $this->require_dev ? true : false;
+                })
+                ->display($view)
+                ->expand($this->getExpandHandler('require_dev'))
+                ->else()
+                ->emptyString();
+
+            $grid->disablePagination();
+            $grid->disableCreateButton();
+            $grid->disableDeleteButton();
+            $grid->disableBatchDelete();
+            $grid->disableFilterButton();
+            $grid->disableFilter();
+            $grid->disableQuickEditButton();
+            $grid->disableEditButton();
+            $grid->disableDeleteButton();
+            $grid->disableViewButton();
+
+            $grid->actions(new ImportButton());
+
+            $grid->quickCreate(function (Grid\Tools\QuickCreate $create) {
+                $create->text('package_name')->required();
+                $create->text('namespace')
+                    ->attribute('style', 'width:240px')
+                    ->required()
+                    ->default('Dcat\\Admin\\Extension\\:Name');
+            });
         });
-
-        return $grid;
     }
 
-    /**
-     * Make a form builder.
-     *
-     * @return Form
-     */
     public function form()
     {
         $form = new Form(new Extension());
@@ -177,11 +162,6 @@ class ExtensionController extends Controller
         return $form;
     }
 
-    /**
-     * 创建扩展.
-     *
-     * @return string
-     */
     public function createExtension($package, $namespace)
     {
         $namespace = trim($namespace, '\\');
@@ -196,11 +176,6 @@ class ExtensionController extends Controller
         return $output->getContent();
     }
 
-    /**
-     * @param string $key
-     *
-     * @return \Closure
-     */
     protected function getExpandHandler($key = 'require')
     {
         return function () use ($key) {
@@ -219,9 +194,6 @@ class ExtensionController extends Controller
         };
     }
 
-    /**
-     * 字段显示定义.
-     */
     protected function define()
     {
         $name = function ($v) {

+ 51 - 68
src/Controllers/LogController.php

@@ -11,13 +11,6 @@ use Illuminate\Support\Arr;
 
 class LogController extends Controller
 {
-    /**
-     * Index interface.
-     *
-     * @param Content $content
-     *
-     * @return Content
-     */
     public function index(Content $content)
     {
         return $content
@@ -26,84 +19,74 @@ class LogController extends Controller
             ->body($this->grid());
     }
 
-    /**
-     * @return Grid
-     */
     protected function grid()
     {
-        $grid = new Grid(new OperationLog());
-
-        $grid->id('ID')->sortable();
-        $grid->user(trans('admin.user'))
-            ->get('name')
-            ->link(function () {
-                if ($this->user) {
-                    return admin_url('auth/users/'.$this->user['id']);
-                }
-            })
-            ->responsive();
+        return new Grid(new OperationLog(), function (Grid $grid) {
+            $grid->id('ID')->sortable();
+            $grid->user(trans('admin.user'))
+                ->get('name')
+                ->link(function () {
+                    if ($this->user) {
+                        return admin_url('auth/users/'.$this->user['id']);
+                    }
+                })
+                ->responsive();
 
-        $grid->method(trans('admin.method'))
-            ->responsive()
-            ->label(OperationLogModel::$methodColors)
-            ->filterByValue();
+            $grid->method(trans('admin.method'))
+                ->responsive()
+                ->label(OperationLogModel::$methodColors)
+                ->filterByValue();
 
-        $grid->path(trans('admin.uri'))->responsive()->display(function ($v) {
-            return "<code>$v</code>";
-        })->filterByValue();
+            $grid->path(trans('admin.uri'))->responsive()->display(function ($v) {
+                return "<code>$v</code>";
+            })->filterByValue();
 
-        $grid->ip('IP')->filterByValue()->responsive();
+            $grid->ip('IP')->filterByValue()->responsive();
 
-        $grid->input->responsive()->display(function ($input) {
-            $input = json_decode($input, true);
-            $input = Arr::except($input, ['_pjax', '_token', '_method', '_previous_']);
-            if (empty($input)) {
-                return '';
-            }
+            $grid->input->responsive()->display(function ($input) {
+                $input = json_decode($input, true);
+                $input = Arr::except($input, ['_pjax', '_token', '_method', '_previous_']);
+                if (empty($input)) {
+                    return '';
+                }
 
-            return '<pre class="dump">'.json_encode($input, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE).'</pre>';
-        });
+                return '<pre class="dump">'.json_encode($input, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE).'</pre>';
+            });
 
-        $grid->created_at(trans('admin.created_at'))->responsive();
+            $grid->created_at(trans('admin.created_at'))->responsive();
 
-        $grid->model()->with('user')->orderBy('id', 'DESC');
+            $grid->model()->with('user')->orderBy('id', 'DESC');
 
-        $grid->disableCreateButton();
-        $grid->disableQuickEditButton();
-        $grid->disableEditButton();
-        $grid->disableViewButton();
-        $grid->setActionClass(Grid\Displayers\Actions::class);
+            $grid->disableCreateButton();
+            $grid->disableQuickEditButton();
+            $grid->disableEditButton();
+            $grid->disableViewButton();
+            $grid->setActionClass(Grid\Displayers\Actions::class);
 
-        $grid->filter(function (Grid\Filter $filter) {
-            $filter->equal('user_id', trans('admin.user'))
-                ->selectResource('auth/users')
-                ->options(function ($v) {
-                    if (! $v) {
-                        return $v;
-                    }
-                    $userModel = config('admin.database.users_model');
+            $grid->filter(function (Grid\Filter $filter) {
+                $filter->equal('user_id', trans('admin.user'))
+                    ->selectResource('auth/users')
+                    ->options(function ($v) {
+                        if (! $v) {
+                            return $v;
+                        }
+                        $userModel = config('admin.database.users_model');
 
-                    return $userModel::findOrFail($v)->pluck('name', 'id');
-                });
+                        return $userModel::find((array) $v)->pluck('name', 'id');
+                    });
 
-            $filter->equal('method', trans('admin.method'))
-                ->select(
-                    array_combine(OperationLogModel::$methods, OperationLogModel::$methods)
-                );
+                $filter->equal('method', trans('admin.method'))
+                    ->select(
+                        array_combine(OperationLogModel::$methods, OperationLogModel::$methods)
+                    );
 
-            $filter->like('path', trans('admin.uri'));
-            $filter->equal('ip', 'IP');
-            $filter->between('created_at')->datetime();
+                $filter->like('path', trans('admin.uri'));
+                $filter->equal('ip', 'IP');
+                $filter->between('created_at')->datetime();
+            });
         });
-
-        return $grid;
     }
 
-    /**
-     * @param mixed $id
-     *
-     * @return \Illuminate\Http\JsonResponse
-     */
     public function destroy($id)
     {
         $ids = explode(',', $id);

+ 52 - 61
src/Controllers/MenuController.php

@@ -9,26 +9,15 @@ use Dcat\Admin\Layout\Row;
 use Dcat\Admin\Models\Repositories\Menu;
 use Dcat\Admin\Tree;
 use Dcat\Admin\Widgets\Box;
+use Dcat\Admin\Widgets\Form as WidgetForm;
 
 class MenuController extends AdminController
 {
-    /**
-     * Get content title.
-     *
-     * @return string
-     */
     public function title()
     {
         return trans('admin.menu');
     }
 
-    /**
-     * Index interface.
-     *
-     * @param Content $content
-     *
-     * @return Content
-     */
     public function index(Content $content)
     {
         return $content
@@ -38,7 +27,7 @@ class MenuController extends AdminController
                 $row->column(7, $this->treeView()->render());
 
                 $row->column(5, function (Column $column) {
-                    $form = new \Dcat\Admin\Widgets\Form();
+                    $form = new WidgetForm();
                     $form->action(admin_url('auth/menu'));
 
                     $menuModel = config('admin.database.menu_model');
@@ -72,28 +61,27 @@ class MenuController extends AdminController
     {
         $menuModel = config('admin.database.menu_model');
 
-        $tree = new Tree(new $menuModel());
+        return new Tree(new $menuModel(), function (Tree $tree) {
+            $tree->disableCreateButton();
+            $tree->disableQuickCreateButton();
+            $tree->disableEditButton();
 
-        $tree->disableCreateButton();
-        $tree->disableQuickCreateButton();
+            $tree->branch(function ($branch) {
+                $payload = "<i class='fa {$branch['icon']}'></i>&nbsp;<strong>{$branch['title']}</strong>";
 
-        $tree->branch(function ($branch) {
-            $payload = "<i class='fa {$branch['icon']}'></i>&nbsp;<strong>{$branch['title']}</strong>";
+                if (! isset($branch['children'])) {
+                    if (url()->isValidUrl($branch['uri'])) {
+                        $uri = $branch['uri'];
+                    } else {
+                        $uri = admin_base_path($branch['uri']);
+                    }
 
-            if (! isset($branch['children'])) {
-                if (url()->isValidUrl($branch['uri'])) {
-                    $uri = $branch['uri'];
-                } else {
-                    $uri = admin_base_path($branch['uri']);
+                    $payload .= "&nbsp;&nbsp;&nbsp;<a href=\"$uri\" class=\"dd-nodrag\">$uri</a>";
                 }
 
-                $payload .= "&nbsp;&nbsp;&nbsp;<a href=\"$uri\" class=\"dd-nodrag\">$uri</a>";
-            }
-
-            return $payload;
+                return $payload;
+            });
         });
-
-        return $tree;
     }
 
     /**
@@ -104,48 +92,51 @@ class MenuController extends AdminController
     public function form()
     {
         $menuModel = config('admin.database.menu_model');
-        $permissionModel = config('admin.database.permissions_model');
-        $roleModel = config('admin.database.roles_model');
 
-        $form = new Form(new Menu());
+        $relations = $menuModel::withPermission() ? ['permissions', 'roles'] : 'roles';
 
-        $form->tools(function (Form\Tools $tools) {
-            $tools->disableView();
-        });
+        return Form::make(new Menu($relations), function (Form $form) use ($menuModel) {
+            $permissionModel = config('admin.database.permissions_model');
+            $roleModel = config('admin.database.roles_model');
 
-        $form->display('id', 'ID');
+            $form->tools(function (Form\Tools $tools) {
+                $tools->disableView();
+            });
 
-        $form->select('parent_id', trans('admin.parent_id'))->options(function () use ($menuModel) {
-            return $menuModel::selectOptions();
-        });
-        $form->text('title', trans('admin.title'))->required();
-        $form->icon('icon', trans('admin.icon'))->help($this->iconHelp());
-        $form->text('uri', trans('admin.uri'));
-        $form->multipleSelect('roles', trans('admin.roles'))
-            ->options(function () use ($roleModel) {
-                return $roleModel::all()->pluck('name', 'id');
-            })
-            ->customFormat(function ($v) {
-                return array_column($v, 'id');
+            $form->display('id', 'ID');
+
+            $form->select('parent_id', trans('admin.parent_id'))->options(function () use ($menuModel) {
+                return $menuModel::selectOptions();
+            })->saving(function ($v) {
+                return (int) $v;
             });
-        if ($menuModel::withPermission()) {
-            $form->tree('permissions', trans('admin.permission'))
-                ->nodes(function () use ($permissionModel) {
-                    return (new $permissionModel())->allNodes();
+            $form->text('title', trans('admin.title'))->required();
+            $form->icon('icon', trans('admin.icon'))->help($this->iconHelp());
+            $form->text('uri', trans('admin.uri'));
+            $form->multipleSelect('roles', trans('admin.roles'))
+                ->options(function () use ($roleModel) {
+                    return $roleModel::all()->pluck('name', 'id');
                 })
                 ->customFormat(function ($v) {
-                    if (! $v) {
-                        return [];
-                    }
-
                     return array_column($v, 'id');
                 });
-        }
-
-        $form->display('created_at', trans('admin.created_at'));
-        $form->display('updated_at', trans('admin.updated_at'));
+            if ($menuModel::withPermission()) {
+                $form->tree('permissions', trans('admin.permission'))
+                    ->nodes(function () use ($permissionModel) {
+                        return (new $permissionModel())->allNodes();
+                    })
+                    ->customFormat(function ($v) {
+                        if (! $v) {
+                            return [];
+                        }
+
+                        return array_column($v, 'id');
+                    });
+            }
 
-        return $form;
+            $form->display('created_at', trans('admin.created_at'));
+            $form->display('updated_at', trans('admin.updated_at'));
+        });
     }
 
     /**

+ 30 - 99
src/Controllers/PermissionController.php

@@ -8,29 +8,16 @@ use Dcat\Admin\Grid;
 use Dcat\Admin\IFrameGrid;
 use Dcat\Admin\Layout\Content;
 use Dcat\Admin\Models\Repositories\Permission;
-use Dcat\Admin\Show;
 use Dcat\Admin\Tree;
 use Illuminate\Support\Str;
 
 class PermissionController extends AdminController
 {
-    /**
-     * Get content title.
-     *
-     * @return string
-     */
     protected function title()
     {
         return trans('admin.permissions');
     }
 
-    /**
-     * Index interface.
-     *
-     * @param Content $content
-     *
-     * @return Content
-     */
     public function index(Content $content)
     {
         if (request(IFrameGrid::QUERY_NAME)) {
@@ -59,112 +46,59 @@ class PermissionController extends AdminController
         return $grid;
     }
 
-    /**
-     * @return \Dcat\Admin\Tree
-     */
     protected function treeView()
     {
         $model = config('admin.database.permissions_model');
 
-        $tree = new Tree(new $model());
-
-        $tree->disableCreateButton();
+        return new Tree(new $model(), function (Tree $tree) {
+            $tree->disableCreateButton();
+            $tree->disableEditButton();
 
-        $tree->branch(function ($branch) {
-            $payload = "<div class='pull-left' style='min-width:310px'><b>{$branch['name']}</b>&nbsp;&nbsp;[<span class='text-primary'>{$branch['slug']}</span>]";
+            $tree->branch(function ($branch) {
+                $payload = "<div class='pull-left' style='min-width:310px'><b>{$branch['name']}</b>&nbsp;&nbsp;[<span class='text-primary'>{$branch['slug']}</span>]";
 
-            $path = array_filter($branch['http_path']);
+                $path = array_filter($branch['http_path']);
 
-            if (! $path) {
-                return $payload.'</div>&nbsp;';
-            }
-
-            $max = 3;
-            if (count($path) > $max) {
-                $path = array_slice($path, 0, $max);
-                array_push($path, '...');
-            }
-
-            $method = $branch['http_method'] ?: [];
-
-            $path = collect($path)->map(function ($path) use ($branch, &$method) {
-                if (Str::contains($path, ':')) {
-                    [$me, $path] = explode(':', $path);
-
-                    $method = array_merge($method, explode(',', $me));
-                }
-                if ($path !== '...' && ! empty(config('admin.route.prefix'))) {
-                    $path = trim(admin_base_path($path), '/');
+                if (! $path) {
+                    return $payload.'</div>&nbsp;';
                 }
 
-                $color = Admin::color()->primaryDarker();
-
-                return "<code style='color:{$color}'>$path</code>";
-            })->implode('&nbsp;&nbsp;');
-
-            $method = collect($method ?: ['ANY'])->unique()->map(function ($name) {
-                return strtoupper($name);
-            })->map(function ($name) {
-                return "<span class='label bg-primary'>{$name}</span>";
-            })->implode('&nbsp;').'&nbsp;';
-
-            $payload .= "</div>&nbsp; $method<a class=\"dd-nodrag\">$path</a>";
+                $max = 3;
+                if (count($path) > $max) {
+                    $path = array_slice($path, 0, $max);
+                    array_push($path, '...');
+                }
 
-            return $payload;
-        });
+                $method = $branch['http_method'] ?: [];
 
-        return $tree;
-    }
-
-    /**
-     * Make a show builder.
-     *
-     * @param mixed $id
-     *
-     * @return Show
-     */
-    protected function detail($id)
-    {
-        $show = new Show($id, new Permission());
+                $path = collect($path)->map(function ($path) use ($branch, &$method) {
+                    if (Str::contains($path, ':')) {
+                        [$me, $path] = explode(':', $path);
 
-        $show->id;
-        $show->slug;
-        $show->name;
+                        $method = array_merge($method, explode(',', $me));
+                    }
+                    if ($path !== '...' && ! empty(config('admin.route.prefix'))) {
+                        $path = trim(admin_base_path($path), '/');
+                    }
 
-        $show->http_path->unescape()->as(function ($path) {
-            return collect($path)->filter()->map(function ($path) {
-                $method = $this->http_method ?: ['ANY'];
+                    $color = Admin::color()->primaryDarker();
 
-                if (Str::contains($path, ':')) {
-                    [$method, $path] = explode(':', $path);
-                    $method = explode(',', $method);
-                }
+                    return "<code style='color:{$color}'>$path</code>";
+                })->implode('&nbsp;&nbsp;');
 
-                $method = collect($method)->map(function ($name) {
+                $method = collect($method ?: ['ANY'])->unique()->map(function ($name) {
                     return strtoupper($name);
                 })->map(function ($name) {
                     return "<span class='label bg-primary'>{$name}</span>";
-                })->implode('&nbsp;');
+                })->implode('&nbsp;').'&nbsp;';
 
-                if (! empty(config('admin.route.prefix'))) {
-                    $path = '/'.trim(config('admin.route.prefix'), '/').$path;
-                }
+                $payload .= "</div>&nbsp; $method<a class=\"dd-nodrag\">$path</a>";
 
-                return "<div style='margin-bottom: 5px;'>$method<code>$path</code></div>";
-            })->implode('');
+                return $payload;
+            });
         });
-
-        $show->created_at;
-        $show->updated_at;
-
-        return $show;
     }
 
-    /**
-     * Make a form builder.
-     *
-     * @return Form
-     */
     public function form()
     {
         return Form::make(new Permission(), function (Form $form) {
@@ -203,9 +137,6 @@ class PermissionController extends AdminController
         });
     }
 
-    /**
-     * @return array
-     */
     public function getRoutes()
     {
         $prefix = config('admin.route.prefix');

+ 0 - 24
src/Controllers/RoleController.php

@@ -14,11 +14,6 @@ use Dcat\Admin\Widgets\Tree;
 
 class RoleController extends AdminController
 {
-    /**
-     * Get content title.
-     *
-     * @return string
-     */
     public function title()
     {
         return trans('admin.roles');
@@ -63,13 +58,6 @@ class RoleController extends AdminController
         return $grid;
     }
 
-    /**
-     * Make a show builder.
-     *
-     * @param mixed $id
-     *
-     * @return Show
-     */
     protected function detail($id)
     {
         return Show::make($id, new Role('permissions'), function (Show $show) {
@@ -101,11 +89,6 @@ class RoleController extends AdminController
         });
     }
 
-    /**
-     * Make a form builder.
-     *
-     * @return Form
-     */
     public function form()
     {
         return Form::make(new Role('permissions'), function (Form $form) {
@@ -147,13 +130,6 @@ class RoleController extends AdminController
         });
     }
 
-    /**
-     * Remove the specified resource from storage.
-     *
-     * @param int $id
-     *
-     * @return \Illuminate\Http\Response
-     */
     public function destroy($id)
     {
         if (in_array(RoleModel::ADMINISTRATOR_ID, Helper::array($id))) {

+ 0 - 32
src/Controllers/UserController.php

@@ -14,21 +14,11 @@ use Dcat\Admin\Widgets\Tree;
 
 class UserController extends AdminController
 {
-    /**
-     * Get content title.
-     *
-     * @return string
-     */
     public function title()
     {
         return trans('admin.administrator');
     }
 
-    /**
-     * Make a grid builder.
-     *
-     * @return Grid
-     */
     protected function grid()
     {
         return Grid::make(new Administrator('roles'), function (Grid $grid) {
@@ -74,9 +64,6 @@ class UserController extends AdminController
         });
     }
 
-    /**
-     * @return IFrameGrid
-     */
     protected function iFrameGrid()
     {
         $grid = new IFrameGrid(new Administrator());
@@ -91,13 +78,6 @@ class UserController extends AdminController
         return $grid;
     }
 
-    /**
-     * Make a show builder.
-     *
-     * @param mixed $id
-     *
-     * @return Show
-     */
     protected function detail($id)
     {
         return Show::make($id, new Administrator('roles'), function (Show $show) {
@@ -148,11 +128,6 @@ class UserController extends AdminController
         });
     }
 
-    /**
-     * Make a form builder.
-     *
-     * @return Form
-     */
     public function form()
     {
         return Form::make(new Administrator('roles'), function (Form $form) {
@@ -216,13 +191,6 @@ class UserController extends AdminController
         });
     }
 
-    /**
-     * Remove the specified resource from storage.
-     *
-     * @param int $id
-     *
-     * @return \Illuminate\Http\Response
-     */
     public function destroy($id)
     {
         if (in_array(AdministratorModel::DEFAULT_ID, Helper::array($id))) {

+ 33 - 2
src/Form.php

@@ -10,6 +10,7 @@ use Dcat\Admin\Form\Builder;
 use Dcat\Admin\Form\Concerns;
 use Dcat\Admin\Form\Condition;
 use Dcat\Admin\Form\Field;
+use Dcat\Admin\Form\NestedForm;
 use Dcat\Admin\Form\Row;
 use Dcat\Admin\Form\Tab;
 use Dcat\Admin\Traits\HasBuilderEvents;
@@ -780,14 +781,14 @@ class Form implements Renderable
         $this->builder->setResourceId($id);
         $this->builder->mode(Builder::MODE_EDIT);
 
+        $this->inputs = $data;
+
         $this->model(new Fluent($this->repository->getDataWhenUpdating($this)));
 
         $this->build();
 
         $this->setFieldOriginalValue();
 
-        $this->inputs = $data;
-
         if ($response = $this->callSubmitted()) {
             return $response;
         }
@@ -802,6 +803,8 @@ class Form implements Renderable
 
         $this->inputs = $this->handleFileDelete($this->inputs);
 
+        $this->inputs = $this->handleHasManyValues($this->inputs);
+
         if ($response = $this->handleOrderable($this->inputs)) {
             return $response;
         }
@@ -818,6 +821,34 @@ class Form implements Renderable
         }
     }
 
+    /**
+     * @param array $inputs
+     *
+     * @return array
+     */
+    protected function handleHasManyValues(array $inputs)
+    {
+        foreach ($inputs as $column => &$input) {
+            $field = $this->builder()->field($column);
+
+            if (is_array($input) && $field instanceof Field\HasMany) {
+                $keyName = $field->getKeyName();
+
+                foreach ($input as $k => &$v) {
+                    if (! array_key_exists($keyName, $v)) {
+                        $v[$keyName] = $k;
+                    }
+
+                    if (empty($v[NestedForm::REMOVE_FLAG_NAME])) {
+                        $v[NestedForm::REMOVE_FLAG_NAME] = null;
+                    }
+                }
+            }
+        }
+
+        return $inputs;
+    }
+
     /**
      * @param $key
      * @param $redirectTo

+ 59 - 21
src/Form/Concerns/HasFiles.php

@@ -5,6 +5,7 @@ namespace Dcat\Admin\Form\Concerns;
 use Dcat\Admin\Contracts\UploadField as UploadFieldInterface;
 use Dcat\Admin\Form\Builder;
 use Dcat\Admin\Form\Field;
+use Dcat\Admin\Form\NestedForm;
 use Dcat\Admin\Support\WebUploader;
 use Symfony\Component\HttpFoundation\File\UploadedFile;
 use Symfony\Component\HttpFoundation\Response;
@@ -15,6 +16,8 @@ use Symfony\Component\HttpFoundation\Response;
 trait HasFiles
 {
     /**
+     * 文件上传操作.
+     *
      * @param array $data
      *
      * @return Response|void
@@ -28,7 +31,16 @@ trait HasFiles
             return;
         }
 
-        $field = $this->findFieldByName($column);
+        $relation = $data['_relation'] ?? null;
+
+        if (empty($relation)) {
+            $field = $this->findFieldByName($column);
+        } else {
+            // hasMany表单文件上传
+            $relation = explode(',', $relation)[0];
+
+            $field = $this->getFieldByRelationName($relation, $column);
+        }
 
         if ($field && $field instanceof UploadFieldInterface) {
             if (($results = $this->callUploading($field, $file)) && $results instanceof Response) {
@@ -58,24 +70,12 @@ trait HasFiles
             return $field;
         }
 
-        if (mb_strpos($column, '.')) {
-            [$relation, $column] = explode('.', $column);
-
-            $relation = $this->findFieldByName($relation);
-
-            if ($relation instanceof Field\HasMany) {
-                return $relation->buildNestedForm()->fields()->first(function ($field) use ($column) {
-                    return $field->column() === $column;
-                });
-            }
-
-            return null;
-        }
-
         return $this->builder->field($column) ?: $this->builder->stepField($column);
     }
 
     /**
+     * 新增之前删除文件操作.
+     *
      * @param array $data
      *
      * @return \Illuminate\Http\JsonResponse|void
@@ -88,12 +88,17 @@ trait HasFiles
 
         $column = $data['_column'] ?? null;
         $file = $data['key'] ?? null;
+        $relation = $data['_relation'] ?? null;
 
         if (! $column && ! $file) {
             return;
         }
 
-        $field = $this->builder->field($column) ?: $this->builder->stepField($column);
+        if (empty($relation)) {
+            $field = $this->builder->field($column) ?: $this->builder->stepField($column);
+        } else {
+            $field = $this->getFieldByRelationName($relation[0], $column);
+        }
 
         if ($field && $field instanceof UploadFieldInterface) {
             $field->deleteFile($file);
@@ -102,6 +107,25 @@ trait HasFiles
         }
     }
 
+    /**
+     * 获取hasMany的子表单字段.
+     *
+     * @param string $relation
+     * @param string $column
+     *
+     * @return mixed
+     */
+    protected function getFieldByRelationName($relation, $column)
+    {
+        $relation = $this->findFieldByName($relation);
+
+        if ($relation && $relation instanceof Field\HasMany) {
+            return $relation->buildNestedForm()->fields()->first(function ($field) use ($column) {
+                return $field->column() === $column;
+            });
+        }
+    }
+
     /**
      * @param array $input
      *
@@ -122,7 +146,7 @@ trait HasFiles
     }
 
     /**
-     * Remove files in record.
+     * 根据传入数据删除文件.
      *
      * @param array $data
      * @param bool  $forceDelete
@@ -145,6 +169,8 @@ trait HasFiles
     }
 
     /**
+     * 编辑页面删除上传文件操作.
+     *
      * @param array $input
      *
      * @return array
@@ -156,14 +182,26 @@ trait HasFiles
         }
 
         $input[Field::FILE_DELETE_FLAG] = $input['key'];
-        unset($input['key']);
 
         if (! empty($input['_column'])) {
-            $input[$input['_column']] = '';
-
-            unset($input['_column']);
+            if (empty($input['_relation'])) {
+                $input[$input['_column']] = '';
+            } else {
+                [$relation, $relationKey] = $input['_relation'];
+                $keyName = $this->builder()->field($relation)->getKeyName();
+
+                $input[$relation] = [
+                    $relationKey => [
+                        $keyName                     => $relationKey,
+                        $input['_column']            => '',
+                        NestedForm::REMOVE_FLAG_NAME => null,
+                    ],
+                ];
+            }
         }
 
+        unset($input['key'], $input['_column'], $input['_relation']);
+
         $this->request->replace($input);
 
         return $input;

+ 0 - 8
src/Form/Field/Currency.php

@@ -50,14 +50,6 @@ class Currency extends Text
         return $this->options(compact('digits'));
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    protected function prepareInputValue($value)
-    {
-        return (float) $value;
-    }
-
     /**
      * {@inheritdoc}
      */

+ 1 - 1
src/Form/Field/Editor.php

@@ -135,7 +135,7 @@ class Editor extends Field
      */
     protected function defaultImageUploadUrl()
     {
-        return $this->formatUrl(route('dcat.api.tinymce.upload'));
+        return $this->formatUrl(route(admin_api_route('tinymce.upload')));
     }
 
     /**

+ 25 - 10
src/Form/Field/File.php

@@ -4,6 +4,7 @@ namespace Dcat\Admin\Form\Field;
 
 use Dcat\Admin\Contracts\UploadField as UploadFieldInterface;
 use Dcat\Admin\Form\Field;
+use Dcat\Admin\Form\NestedForm;
 use Dcat\Admin\Support\Helper;
 use Dcat\Admin\Support\JavaScript;
 use Illuminate\Support\Arr;
@@ -112,11 +113,13 @@ class File extends Field implements UploadFieldInterface
      *
      * @return $this
      */
-    public function setRelation(?string $name)
+    public function setRelation(?string $name, $key)
     {
         $this->relationName = $name;
+        $this->options['formData']['_relation'] = [$name, $key];
 
-        $this->options['formData']['upload_column'] = $name.'.'.$this->column();
+        $this->containerId .= NestedForm::DEFAULT_KEY_NAME;
+        $this->id .= NestedForm::DEFAULT_KEY_NAME;
 
         return $this;
     }
@@ -182,7 +185,7 @@ class File extends Field implements UploadFieldInterface
 
         $this->forceOptions();
         $this->formatValue();
-        $this->setupScript();
+        $this->setUpScript();
 
         $this->addVariables([
             'fileType'    => $this->options['isImage'] ? '' : 'file',
@@ -192,31 +195,43 @@ class File extends Field implements UploadFieldInterface
         return parent::render();
     }
 
-    protected function setupScript()
+    protected function setUpScript()
     {
         $newButton = trans('admin.uploader.add_new_media');
         $options = JavaScript::format($this->options);
+        $hasManyKey = NestedForm::DEFAULT_KEY_NAME;
 
         $this->script = <<<JS
 (function () {
-    var uploader, newPage, options = {$options};
+    var uploader, 
+    newPage, 
+    cID = '#{$this->containerId}',
+    ID = '#{$this->id}',
+    hasManyKey = '{$hasManyKey}',
+    options = {$options};
+
+    if (typeof nestedIndex !== "undefined") {
+        cID = cID.replace(hasManyKey, nestedIndex);
+        ID = ID.replace(hasManyKey, nestedIndex);
+    }
 
     build();
 
     function build() {
         var opts = $.extend({
-            selector: '#{$this->containerId}',
-            addFileButton: '#{$this->containerId} .add-file-button',
+            selector: cID,
+            addFileButton: cID+' .add-file-button',
+            inputSelector: ID,
         }, options);
 
         opts.upload = $.extend({
             pick: {
-                id: '#{$this->containerId} .file-picker',
+                id: cID+' .file-picker',
                 name: '_file_',
                 label: '<i class="feather icon-folder"></i>&nbsp; {$newButton}'
             },
-            dnd: '#{$this->containerId} .dnd-area',
-            paste: '#{$this->containerId} .web-uploader'
+            dnd: cID+' .dnd-area',
+            paste: cID+' .web-uploader'
         }, opts);
 
         uploader = Dcat.Uploader(opts);

+ 20 - 11
src/Form/Field/HasMany.php

@@ -6,6 +6,7 @@ use Dcat\Admin\Admin;
 use Dcat\Admin\Form;
 use Dcat\Admin\Form\Field;
 use Dcat\Admin\Form\NestedForm;
+use Dcat\Admin\Support\Helper;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Facades\Validator;
 use Illuminate\Support\Str;
@@ -120,6 +121,12 @@ class HasMany extends Field
                 continue;
             }
 
+            if ($field instanceof File) {
+                $fieldRules = is_string($fieldRules) ? explode('|', $fieldRules) : $fieldRules;
+
+                Helper::deleteByValue($fieldRules, ['image', 'file']);
+            }
+
             $column = $field->column();
 
             if (is_array($column)) {
@@ -340,7 +347,7 @@ class HasMany extends Field
      *
      * @return string
      */
-    protected function getKeyName()
+    public function getKeyName()
     {
         if (is_null($this->form)) {
             return;
@@ -475,14 +482,14 @@ class HasMany extends Field
          */
         $script = <<<JS
 (function () {
-    var index = 0;
+    var nestedIndex = 0;
 $('#has-many-{$this->column}').on('click', '.add', function () {
 
     var tpl = $('template.{$this->column}-tpl');
 
-    index++;
+    nestedIndex++;
 
-    var template = tpl.html().replace(/{$defaultKey}/g, index);
+    var template = tpl.html().replace(/{$defaultKey}/g, nestedIndex);
     $('.has-many-{$this->column}-forms').append(template);
     {$templateScript}
 });
@@ -527,11 +534,11 @@ JS;
     }
 });
 
-var index = 0;
+var nestedIndex = 0;
 $('#has-many-{$this->column} > .header').off('click', '.add').on('click', '.add', function(){
-    index++;
-    var navTabHtml = $('#has-many-{$this->column} > template.nav-tab-tpl').html().replace(/{$defaultKey}/g, index);
-    var paneHtml = $('#has-many-{$this->column} > template.pane-tpl').html().replace(/{$defaultKey}/g, index);
+    nestedIndex++;
+    var navTabHtml = $('#has-many-{$this->column} > template.nav-tab-tpl').html().replace(/{$defaultKey}/g, nestedIndex);
+    var paneHtml = $('#has-many-{$this->column} > template.pane-tpl').html().replace(/{$defaultKey}/g, nestedIndex);
     $('#has-many-{$this->column} > .nav').append(navTabHtml);
     $('#has-many-{$this->column} > .tab-content').append(paneHtml);
     $('#has-many-{$this->column} > .nav > li:last-child a').click();
@@ -574,13 +581,13 @@ JS;
          */
         $script = <<<JS
 (function () {
-    var index = 0;
+    var nestedIndex = 0;
     $('#has-many-{$this->column}').on('click', '.add', function () {
         var tpl = $('template.{$this->column}-tpl');
     
-        index++;
+        nestedIndex++;
     
-        var template = tpl.html().replace(/{$defaultKey}/g, index);
+        var template = tpl.html().replace(/{$defaultKey}/g, nestedIndex);
         $('.has-many-{$this->column}-forms').append(template);
         {$templateScript}
     });
@@ -701,6 +708,8 @@ JS;
         // specify a view to render.
         $this->view = $this->views[$this->viewMode];
 
+        Admin::style('.table-has-many .input-group{flex-wrap: nowrap!important}');
+
         return parent::render()->with([
             'headers'      => $headers,
             'forms'        => $this->buildRelatedForms(),

+ 1 - 1
src/Form/Field/ImageField.php

@@ -76,7 +76,7 @@ trait ImageField
     public function __call($method, $arguments)
     {
         if (static::hasMacro($method)) {
-            return $this;
+            return parent::__call($method, $arguments);
         }
 
         if (! class_exists(ImageManagerStatic::class)) {

+ 1 - 1
src/Form/Field/Markdown.php

@@ -166,7 +166,7 @@ JS
      */
     protected function defaultImageUploadUrl()
     {
-        return $this->formatUrl(route('dcat.api.editor-md.upload'));
+        return $this->formatUrl(route(admin_api_route('editor-md.upload')));
     }
 
     /**

+ 2 - 2
src/Form/Field/Number.php

@@ -14,8 +14,8 @@ class Number extends Text
 $('{$this->getElementClassSelector()}:not(.initialized)')
     .addClass('initialized')
     .bootstrapNumber({
-        upClass: 'success',
-        downClass: 'primary',
+        upClass: 'primary',
+        downClass: 'white',
         center: true
     });
 JS;

+ 10 - 4
src/Form/Field/Select.php

@@ -142,14 +142,20 @@ JS;
      */
     public function loads($fields = [], $sourceUrls = [], string $idField = 'id', string $textField = 'text')
     {
-        $fieldsStr = implode('.', (array) $fields);
+        $fieldsStr = implode('^', array_map(function ($field) {
+            if (Str::contains($field, '.')) {
+                return str_replace('.', '_', $field).'_';
+            }
+
+            return $field;
+        }, (array) $fields));
         $urlsStr = implode('^', array_map(function ($url) {
             return admin_url($url);
         }, (array) $sourceUrls));
 
         $script = <<<JS
 (function () {
-    var fields = '$fieldsStr'.split('.');
+    var fields = '$fieldsStr'.split('^');
     var urls = '$urlsStr'.split('^');
     
     var refreshOptions = function(url, target) {
@@ -161,7 +167,7 @@ JS;
                     d.text = d.$textField;
                     return d;
                 })
-            }).trigger('change');
+            }).val(target.data('value')).trigger('change');
         });
     };
     
@@ -172,7 +178,7 @@ JS;
 
         fields.forEach(function(field, index){
             var target = $(_this).closest('.fields-group').find('.' + fields[index]);
-            
+
             if (_this.value !== '0' && ! _this.value) {
                 return;
             }

+ 0 - 9
src/Form/Field/Table.php

@@ -88,15 +88,6 @@ class Table extends HasMany
         return parent::value($value);
     }
 
-    protected function getKeyName()
-    {
-        if (is_null($this->form)) {
-            return;
-        }
-
-        return 'id';
-    }
-
     public function buildNestedForm($key = null)
     {
         $form = new NestedForm($this->column);

+ 0 - 12
src/Form/Field/WebUploader.php

@@ -221,16 +221,4 @@ trait WebUploader
     {
         $this->options['preview'] = $this->initialPreviewConfig();
     }
-
-    /**
-     * @param array $options
-     *
-     * @return $this
-     */
-    public function options($options = [])
-    {
-        $this->options = array_merge($options, $this->options);
-
-        return $this;
-    }
 }

+ 8 - 2
src/Form/NestedForm.php

@@ -256,7 +256,7 @@ class NestedForm
 
             $value = $this->fetchColumnValue($record, $columns);
 
-            if (is_null($value)) {
+            if ($value === false) {
                 continue;
             }
 
@@ -291,6 +291,10 @@ class NestedForm
     protected function fetchColumnValue($data, $columns)
     {
         if (is_string($columns)) {
+            if (! Arr::has($data, $columns)) {
+                return false;
+            }
+
             return Arr::get($data, $columns);
         }
 
@@ -305,6 +309,8 @@ class NestedForm
 
             return $value;
         }
+
+        return false;
     }
 
     /**
@@ -322,7 +328,7 @@ class NestedForm
         }
 
         if ($field instanceof UploadField) {
-            $field->setRelation($this->relationName);
+            $field->setRelation($this->relationName, $this->key);
         }
 
         $field::collectAssets();

+ 55 - 1
src/Form/Row.php

@@ -5,6 +5,60 @@ namespace Dcat\Admin\Form;
 use Dcat\Admin\Form;
 use Illuminate\Contracts\Support\Renderable;
 
+/**
+ * Class Row.
+ *
+ * @method Field\Text                   text($column, $label = '')
+ * @method Field\Checkbox               checkbox($column, $label = '')
+ * @method Field\Radio                  radio($column, $label = '')
+ * @method Field\Select                 select($column, $label = '')
+ * @method Field\MultipleSelect         multipleSelect($column, $label = '')
+ * @method Field\Textarea               textarea($column, $label = '')
+ * @method Field\Hidden                 hidden($column, $label = '')
+ * @method Field\Id                     id($column, $label = '')
+ * @method Field\Ip                     ip($column, $label = '')
+ * @method Field\Url                    url($column, $label = '')
+ * @method Field\Email                  email($column, $label = '')
+ * @method Field\Mobile                 mobile($column, $label = '')
+ * @method Field\Slider                 slider($column, $label = '')
+ * @method Field\Map                    map($latitude, $longitude, $label = '')
+ * @method Field\Editor                 editor($column, $label = '')
+ * @method Field\Date                   date($column, $label = '')
+ * @method Field\Datetime               datetime($column, $label = '')
+ * @method Field\Time                   time($column, $label = '')
+ * @method Field\Year                   year($column, $label = '')
+ * @method Field\Month                  month($column, $label = '')
+ * @method Field\DateRange              dateRange($start, $end, $label = '')
+ * @method Field\DateTimeRange          datetimeRange($start, $end, $label = '')
+ * @method Field\TimeRange              timeRange($start, $end, $label = '')
+ * @method Field\Number                 number($column, $label = '')
+ * @method Field\Currency               currency($column, $label = '')
+ * @method Field\SwitchField            switch ($column, $label = '')
+ * @method Field\Display                display($column, $label = '')
+ * @method Field\Rate                   rate($column, $label = '')
+ * @method Field\Divide                 divider()
+ * @method Field\Password               password($column, $label = '')
+ * @method Field\Decimal                decimal($column, $label = '')
+ * @method Field\Html                   html($html, $label = '')
+ * @method Field\Tags                   tags($column, $label = '')
+ * @method Field\Icon                   icon($column, $label = '')
+ * @method Field\Embeds                 embeds($column, $label = '')
+ * @method Field\Captcha                captcha()
+ * @method Field\Listbox                listbox($column, $label = '')
+ * @method Field\SelectResource         selectResource($column, $label = '')
+ * @method Field\File                   file($column, $label = '')
+ * @method Field\Image                  image($column, $label = '')
+ * @method Field\MultipleFile           multipleFile($column, $label = '')
+ * @method Field\MultipleImage          multipleImage($column, $label = '')
+ * @method Field\HasMany                hasMany($column, $labelOrCallback, $callback = null)
+ * @method Field\Tree                   tree($column, $label = '')
+ * @method Field\Table                  table($column, $labelOrCallback, $callback = null)
+ * @method Field\ListField              list($column, $label = '')
+ * @method Field\Timezone               timezone($column, $label = '')
+ * @method Field\KeyValue               keyValue($column, $label = '')
+ * @method Field\Tel                    tel($column, $label = '')
+ * @method Field\Markdown               markdown($column, $label = '')
+ */
 class Row implements Renderable
 {
     /**
@@ -55,7 +109,7 @@ class Row implements Renderable
      *
      * @return array
      */
-    public function getFields()
+    public function fields()
     {
         return $this->fields;
     }

+ 1 - 1
src/Form/Tab.php

@@ -72,7 +72,7 @@ class Tab
         foreach ($this->form->rows() as $row) {
             $rowFields = array_map(function ($field) {
                 return $field['element'];
-            }, $row->getFields());
+            }, $row->fields());
 
             $match = false;
 

+ 4 - 2
src/Grid.php

@@ -523,11 +523,13 @@ class Grid
     }
 
     /**
+     * @param bool $value
+     *
      * @return $this
      */
-    public function withBorder()
+    public function withBorder(bool $value = true)
     {
-        $this->options['show_bordered'] = true;
+        $this->options['show_bordered'] = $value;
 
         return $this;
     }

+ 4 - 0
src/Grid/Column.php

@@ -16,6 +16,7 @@ use Illuminate\Support\Str;
 use Illuminate\Support\Traits\Macroable;
 
 /**
+ * @method $this editable(bool $refresh = false)
  * @method $this switch(string $color = '')
  * @method $this switchGroup($columns = [], string $color = '')
  * @method $this image($server = '', int $width = 200, int $height = 200)
@@ -35,6 +36,7 @@ use Illuminate\Support\Traits\Macroable;
  * @method $this downloadable($server = '', $disk = null)
  * @method $this copyable()
  * @method $this orderable()
+ * @method $this limit(int $limit = 100, string $end = '...')
  * @method $this ascii()
  * @method $this camel()
  * @method $this finish($cap)
@@ -87,6 +89,8 @@ class Column
         'downloadable'     => Displayers\Downloadable::class,
         'copyable'         => Displayers\Copyable::class,
         'orderable'        => Displayers\Orderable::class,
+        'limit'            => Displayers\Limit::class,
+        'editable'         => Displayers\Editable::class,
     ];
 
     /**

+ 27 - 34
src/Grid/Column/HasDisplayers.php

@@ -6,7 +6,7 @@ use Dcat\Admin\Admin;
 use Dcat\Admin\Grid;
 use Dcat\Admin\Grid\Column;
 use Dcat\Admin\Grid\Displayers\AbstractDisplayer;
-use Dcat\Admin\Support\Helper;
+use Dcat\Admin\Grid\RowAction;
 use Illuminate\Contracts\Support\Arrayable;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Collection;
@@ -218,39 +218,6 @@ trait HasDisplayers
         });
     }
 
-    /**
-     * Limit the number of characters in a string, or the number of element in a array.
-     *
-     * @param int    $limit
-     * @param string $end
-     *
-     * @return $this
-     */
-    public function limit($limit = 100, $end = '...')
-    {
-        return $this->display(function ($value) use ($limit, $end) {
-            if ($value !== null && ! is_scalar($value)) {
-                $value = Helper::array($value);
-
-                if (count($value) <= $limit) {
-                    return $value;
-                }
-
-                $value = array_slice($value, 0, $limit);
-
-                array_push($value, $end);
-
-                return $value;
-            }
-
-            if (mb_strlen($value, 'UTF-8') <= $limit) {
-                return $value;
-            }
-
-            return mb_substr($value, 0, $limit).$end;
-        });
-    }
-
     /**
      * @return $this
      */
@@ -284,4 +251,30 @@ trait HasDisplayers
 
         return $this->displayUsing(Grid\Displayers\Tree::class);
     }
+
+    /**
+     * Display column using a grid row action.
+     *
+     * @param string $action
+     *
+     * @return $this
+     */
+    public function action($action)
+    {
+        if (! is_subclass_of($action, RowAction::class)) {
+            throw new \InvalidArgumentException("Action class [$action] must be sub-class of [Dcat\Admin\Grid\RowAction]");
+        }
+
+        $grid = $this->grid;
+
+        return $this->display(function ($_, $column) use ($action, $grid) {
+            /** @var RowAction $action */
+            $action = $action::make();
+
+            return $action
+                ->setGrid($grid)
+                ->setColumn($column)
+                ->setRow($this);
+        });
+    }
 }

+ 1 - 1
src/Grid/Column/ValueFilter.php

@@ -70,7 +70,7 @@ class ValueFilter
     protected function wrap($value)
     {
         if (! preg_match('/<[^>]+>(.*)<\/[^>]+>/', $value)) {
-            return "<span class='dashed'>{$value}</span>";
+            return "<span>{$value}</span>";
         }
 
         return $value;

+ 12 - 0
src/Grid/Concerns/HasTools.php

@@ -26,6 +26,18 @@ trait HasTools
         $this->tools = new Tools($this);
     }
 
+    /**
+     * @param bool $value
+     *
+     * @return $this
+     */
+    public function toolsWithOutline(bool $value = true)
+    {
+        $this->tools->withOutline($value);
+
+        return $this;
+    }
+
     /**
      * Get or setup grid tools.
      *

+ 1 - 1
src/Grid/Displayers/Button.php

@@ -4,7 +4,7 @@ namespace Dcat\Admin\Grid\Displayers;
 
 class Button extends AbstractDisplayer
 {
-    public function display($style)
+    public function display($style = 'primary')
     {
         $style = collect((array) $style)->map(function ($style) {
             return 'btn-'.$style;

+ 2 - 2
src/Grid/Displayers/DialogTree.php

@@ -15,7 +15,7 @@ class DialogTree extends AbstractDisplayer
 
     protected $title;
 
-    protected $area = ['650px', '600px'];
+    protected $area = ['580px', '600px'];
 
     protected $options = [
         'plugins' => ['checkbox', 'types'],
@@ -172,7 +172,7 @@ EOF;
 
     protected function getSelectorPrefix()
     {
-        return $this->grid->getName().'_'.$this->column->getName();
+        return $this->grid->getName().'_'.$this->column->getName().'_'.$this->getKey();
     }
 
     protected function setupScript()

+ 107 - 0
src/Grid/Displayers/Editable.php

@@ -0,0 +1,107 @@
+<?php
+
+namespace Dcat\Admin\Grid\Displayers;
+
+use Dcat\Admin\Admin;
+
+class Editable extends AbstractDisplayer
+{
+    protected $selector = 'grid-editable';
+
+    public function display($refresh = false)
+    {
+        $this->addScript();
+        $this->addStyle();
+
+        $label = __('admin.save');
+
+        return <<<HTML
+<div>
+    <span class="{$this->selector}" contenteditable="true">
+        {$this->value}
+    </span>
+    <span class="save hidden" 
+        data-value="{$this->value}" 
+        data-name="{$this->column->getName()}" 
+        data-id="{$this->getKey()}" 
+        data-refresh="{$refresh}"
+        data-url="{$this->getUrl()}">
+        {$label}
+    </span>
+</div>
+HTML;
+    }
+
+    protected function getUrl()
+    {
+        return $this->resource().'/'.$this->getKey();
+    }
+
+    protected function addStyle()
+    {
+        $color = Admin::color()->link();
+
+        Admin::style(
+            <<<CSS
+.grid-editable{border-bottom:dashed 1px $color;color: $color;display: inline-block}
+.grid-editable+.save{margin-left: 0.55rem;color: $color}
+CSS
+        );
+    }
+
+    protected function addScript()
+    {
+        $script = <<<JS
+$(".{$this->selector}").on("click", function() {
+    $(this).next().removeClass("hidden");
+}).on('blur', function () {
+    var icon = $(this).next();
+    setTimeout(function () {
+        icon.addClass("hidden")
+    }, 200)
+});
+$('.{$this->selector}+.save').on("click",function() {
+    var obj = $(this),
+        url = obj.data('url'),
+        name = obj.data('name'),
+        refresh = obj.data('refresh'),
+        old_value = obj.data('value'),
+        value = obj.prev().html().replace(new RegExp("<br>","g"), '').replace(new RegExp("&nbsp;","g"), '').trim();
+    
+    var data = {
+        _token: Dcat.token,
+        _method: 'PUT'
+    };
+    data[name] = value;
+    Dcat.NP.start();
+    $.ajax({
+        url: url,
+        type: "POST",
+        data: data,
+        success: function (data) {
+            if (data.status) {
+                obj.attr('data-value',value).addClass("hidden").prev().html(value);
+                Dcat.success(data.message);
+                
+                refresh && Dcat.reload()
+            } else {
+                obj.prev().html(old_value);
+                Dcat.error(data.message);
+            }
+        },
+        error:function(a,b,c) {
+            obj.prev().html(old_value);
+            Dcat.handleAjaxError(a, b, c);
+        },
+        complete:function(a,b) {
+            Dcat.NP.done();
+        }
+    });
+    
+    return false;
+})
+JS;
+
+        Admin::script($script);
+    }
+}

+ 15 - 3
src/Grid/Displayers/Expand.php

@@ -66,16 +66,28 @@ EOT;
     {
         $script = <<<'JS'
 $('.grid-expand').off('click').on('click', function () {
+    var _th = $(this);
     
     if ($(this).data('inserted') == '0') {
     
-        var key = $(this).data('key');
-        var row = $(this).closest('tr');
+        var key = _th.data('key');
+        var row = _th.closest('tr');
         var html = $('template.grid-expand-'+key).html();
+        var id = 'expand-'+key+Dcat.helpers.random(10);
+        
+        $(this).attr('data-expand', '#'+id);
 
-        row.after("<tr><td colspan='"+(row.find('td').length)+"' style='padding:0 !important; border:0;height:0;'>"+html+"</td></tr>");
+        row.after("<tr id="+id+"><td colspan='"+(row.find('td').length)+"' style='padding:0 !important; border:0;height:0;'>"+html+"</td></tr>");
 
         $(this).data('inserted', 1);
+    } else {
+        if ($("i", this).hasClass('icon-chevrons-right')) {
+            $(_th.data('expand')).show();
+        } else {
+            setTimeout(function() {
+              $(_th.data('expand')).hide();
+            }, 250);
+        }
     }
     
     $("i", this).toggleClass("icon-chevrons-right icon-chevrons-down");

+ 61 - 0
src/Grid/Displayers/Limit.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace Dcat\Admin\Grid\Displayers;
+
+use Dcat\Admin\Admin;
+use Dcat\Admin\Support\Helper;
+use Illuminate\Support\Str;
+
+class Limit extends AbstractDisplayer
+{
+    protected function addScript()
+    {
+        $script = <<<'JS'
+$('.limit-more').click(function () {
+    $(this).parent('.limit-text').toggleClass('d-none').siblings().toggleClass('d-none');
+});
+JS;
+
+        Admin::script($script);
+    }
+
+    public function display($limit = 100, $end = '...')
+    {
+        // 数组
+        if ($this->value !== null && ! is_scalar($this->value)) {
+            $value = Helper::array($this->value);
+
+            if (count($value) <= $limit) {
+                return $value;
+            }
+
+            $value = array_slice($value, 0, $limit);
+
+            array_push($value, $end);
+
+            return $value;
+        }
+
+        // 字符串
+        $this->addScript();
+
+        $value = Str::limit($this->value, $limit, $end);
+
+        $original = $this->column->getOriginal();
+
+        if ($value == $original) {
+            return $value;
+        }
+
+        return <<<HTML
+<div class="limit-text">
+    <span class="text">{$value}</span>
+    &nbsp;<a href="javascript:void(0);" class="limit-more">&nbsp;<i class="fa fa-angle-double-down"></i></a>
+</div>
+<div class="limit-text d-none">
+    <span class="text">{$original}</span>
+    &nbsp;<a href="javascript:void(0);" class="limit-more">&nbsp;<i class="fa fa-angle-double-up"></i></a>
+</div>
+HTML;
+    }
+}

+ 1 - 1
src/Grid/Displayers/Modal.php

@@ -16,7 +16,7 @@ class Modal extends AbstractDisplayer
 
     protected function generateElementId()
     {
-        $key = $this->getKey() ?: Str::random(8);
+        $key = Str::random(8);
 
         return 'grid-modal-'.$this->grid->getName().$key;
     }

+ 1 - 3
src/Grid/Displayers/QRCode.php

@@ -27,9 +27,7 @@ JS;
         $content = $this->column->getOriginal();
 
         if ($formatter instanceof \Closure) {
-            $formatter->bindTo($this->row);
-
-            $content = call_user_func($formatter, $content);
+            $content = $formatter->call($this->row, $content);
         }
 
         $img = "<img src='https://api.qrserver.com/v1/create-qr-code/?size={$width}x{$height}&data={$content}' style='height: {$width}px;width: {$height}px;'/>";

+ 0 - 1
src/Grid/Exporters/AbstractExporter.php

@@ -189,7 +189,6 @@ abstract class AbstractExporter implements ExporterInterface
         $array = $this->grid->processFilter(true);
 
         $model->reset();
-        $model->rejectQuery('forPage');
 
         return $this->callBuilder($array);
     }

+ 10 - 1
src/Grid/Model.php

@@ -140,7 +140,15 @@ class Model
         }
 
         $this->request = $request;
-        $this->queries = collect();
+        $this->initQueries();
+    }
+
+    /**
+     * @return void
+     */
+    protected function initQueries()
+    {
+        $this->queries = new Collection();
     }
 
     /**
@@ -720,5 +728,6 @@ class Model
     {
         $this->data = null;
         $this->model = null;
+        $this->initQueries();
     }
 }

+ 57 - 1
src/Grid/Tools.php

@@ -12,6 +12,7 @@ use Dcat\Admin\Support\Helper;
 use Illuminate\Contracts\Support\Htmlable;
 use Illuminate\Contracts\Support\Renderable;
 use Illuminate\Support\Collection;
+use Illuminate\Support\Str;
 
 class Tools implements Renderable
 {
@@ -29,6 +30,11 @@ class Tools implements Renderable
      */
     protected $tools;
 
+    /**
+     * @var bool
+     */
+    protected $outline = true;
+
     /**
      * Create a new Tools instance.
      *
@@ -121,6 +127,18 @@ class Tools implements Renderable
         });
     }
 
+    /**
+     * @param bool $value
+     *
+     * @return $this
+     */
+    public function withOutline(bool $value)
+    {
+        $this->outline = $value;
+
+        return $this;
+    }
+
     /**
      * Disable refresh button.
      *
@@ -185,12 +203,50 @@ class Tools implements Renderable
      */
     public function render()
     {
-        return $this->tools->map(function ($tool) {
+        $value = $this->tools->map(function ($tool) {
             if ($tool instanceof Action && ! $tool->allowed()) {
                 return;
             }
 
             return Helper::render($tool);
         })->implode(' ');
+
+        return $this->addButtonOutline($value);
+    }
+
+    /**
+     * @param string $value
+     *
+     * @return string
+     */
+    public function format(string $value)
+    {
+        return $this->addButtonOutline($value);
+    }
+
+    /**
+     * @param string $value
+     *
+     * @return string
+     */
+    protected function addButtonOutline($value)
+    {
+        if (! $this->outline) {
+            return $value;
+        }
+
+        return preg_replace_callback('/class=[\'|"]([a-z0-9A-Z-_\s]*)[\'|"]/', function (&$text) {
+            $class = array_filter(explode(' ', $text[1]));
+
+            if (
+                in_array('btn', $class, true)
+                && ! in_array('disable-outline', $class, true)
+                && Str::contains($text[1], 'btn-')
+            ) {
+                $class[] = 'btn-outline';
+            }
+
+            return sprintf('class="%s"', implode(' ', $class));
+        }, $value);
     }
 }

+ 3 - 1
src/Grid/Tools/CreateButton.php

@@ -58,6 +58,8 @@ class CreateButton implements Renderable
 
     public function render()
     {
-        return "<div class='btn-group' style='margin-right:3px'>{$this->renderCreateButton()}{$this->renderDialogCreateButton()}</div>";
+        return $this->grid->tools()->format(
+            "<div class='btn-group' style='margin-right:3px'>{$this->renderCreateButton()}{$this->renderDialogCreateButton()}</div>"
+        );
     }
 }

+ 5 - 3
src/Grid/Tools/ExportButton.php

@@ -101,8 +101,9 @@ JS;
 
         $export = trans('admin.export');
 
-        return <<<EOT
-<div class="btn-group dropdown shadow-none" style="margin-right:3px">
+        return $this->grid->tools()->format(
+            <<<EOT
+<div class="btn-group dropdown" style="margin-right:3px">
     <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
         <i class="feather icon-download"></i>
         <span class="d-none d-sm-inline">&nbsp;{$export}&nbsp;</span>
@@ -115,6 +116,7 @@ JS;
         {$this->renderExportSelectedRows()}
     </ul>
 </div>
-EOT;
+EOT
+        );
     }
 }

+ 38 - 2
src/Grid/Tools/QuickCreate.php

@@ -23,6 +23,16 @@ class QuickCreate implements Renderable
      */
     protected $fields;
 
+    /**
+     * @var string
+     */
+    protected $action;
+
+    /**
+     * @var string
+     */
+    protected $method = 'POST';
+
     /**
      * QuickCreate constructor.
      *
@@ -32,6 +42,7 @@ class QuickCreate implements Renderable
     {
         $this->parent = $grid;
         $this->fields = Collection::make();
+        $this->action = request()->url();
     }
 
     protected function formatPlaceholder($placeholder)
@@ -261,9 +272,34 @@ class QuickCreate implements Renderable
         return "admin::grid.quick-create.{$name}";
     }
 
+    /**
+     * @param string $action
+     *
+     * @return $this
+     */
+    public function action(?string $action)
+    {
+        $this->action = admin_url($action);
+
+        return $this;
+    }
+
+    /**
+     * @param string $method
+     *
+     * @return $this
+     */
+    public function method(?string $method = 'POST')
+    {
+        $this->method = $method;
+
+        return $this;
+    }
+
     protected function script()
     {
-        $url = request()->url();
+        $url = $this->action;
+        $method = $this->method;
 
         $uniqueName = $this->parent->getName();
 
@@ -305,7 +341,7 @@ class QuickCreate implements Renderable
     
         $.ajax({
             url: '{$url}',
-            type: 'POST',
+            type: '{$method}',
             data: $(this).serialize(),
             success: function(data) {
                 ctr.attr('submitting', '');

+ 2 - 1
src/Grid/Tools/RowSelector.php

@@ -4,6 +4,7 @@ namespace Dcat\Admin\Grid\Tools;
 
 use Dcat\Admin\Admin;
 use Dcat\Admin\Grid;
+use Illuminate\Support\Arr;
 
 class RowSelector
 {
@@ -93,7 +94,7 @@ JS
     protected function getTitle($row, $id)
     {
         if ($key = $this->titleColumn) {
-            $label = $row->{$key};
+            $label = Arr::get($row->toArray(), $key);
             if ($label !== null && $label !== '') {
                 return $label;
             }

+ 1 - 0
src/IFrameGrid.php

@@ -18,6 +18,7 @@ class IFrameGrid extends Grid
         $this->disablePerPages();
         $this->disableBatchActions();
         $this->disableFilterButton();
+        $this->toolsWithOutline();
 
         $this->rowSelector()->click();
 

+ 2 - 1
src/Layout/Content.php

@@ -6,11 +6,12 @@ use Closure;
 use Dcat\Admin\Admin;
 use Dcat\Admin\Traits\HasBuilderEvents;
 use Illuminate\Contracts\Support\Renderable;
+use Illuminate\Support\Traits\Macroable;
 use Illuminate\Support\ViewErrorBag;
 
 class Content implements Renderable
 {
-    use HasBuilderEvents;
+    use HasBuilderEvents, Macroable;
 
     /**
      * @var string

+ 17 - 0
src/Middleware/Application.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace Dcat\Admin\Middleware;
+
+use Dcat\Admin\Admin;
+
+class Application
+{
+    public function handle($request, \Closure $next, $app = null)
+    {
+        if ($app) {
+            Admin::app()->current($app);
+        }
+
+        return $next($request);
+    }
+}

+ 1 - 1
src/Middleware/LogOperation.php

@@ -98,7 +98,7 @@ class LogOperation
      */
     protected function inExceptArray($request)
     {
-        if ($request->routeIs('dcat.api.value')) {
+        if ($request->routeIs(admin_api_route('value'))) {
             return true;
         }
 

+ 2 - 2
src/Models/Repositories/Menu.php

@@ -6,10 +6,10 @@ use Dcat\Admin\Repositories\EloquentRepository;
 
 class Menu extends EloquentRepository
 {
-    public function __construct()
+    public function __construct($modelOrRelations = [])
     {
         $this->eloquentClass = config('admin.database.menu_model');
 
-        parent::__construct();
+        parent::__construct($modelOrRelations);
     }
 }

+ 1 - 1
src/Repositories/EloquentRepository.php

@@ -720,7 +720,7 @@ class EloquentRepository extends Repository implements TreeRepository
                 || $relation instanceof Relations\MorphOne
                 || $relation instanceof Relations\BelongsTo;
 
-            $prepared = $form->prepareUpdate([$name => $values], $oneToOneRelation);
+            $prepared = $oneToOneRelation ? $form->prepareUpdate([$name => $values]) : [$name => $values];
 
             if (empty($prepared)) {
                 continue;

+ 29 - 1
src/Show.php

@@ -2,6 +2,7 @@
 
 namespace Dcat\Admin;
 
+use Closure;
 use Dcat\Admin\Contracts\Repository;
 use Dcat\Admin\Show\AbstractTool;
 use Dcat\Admin\Show\Divider;
@@ -9,6 +10,7 @@ use Dcat\Admin\Show\Field;
 use Dcat\Admin\Show\Newline;
 use Dcat\Admin\Show\Panel;
 use Dcat\Admin\Show\Relation;
+use Dcat\Admin\Show\Row;
 use Dcat\Admin\Show\Tools;
 use Dcat\Admin\Traits\HasBuilderEvents;
 use Illuminate\Contracts\Support\Arrayable;
@@ -85,6 +87,10 @@ class Show implements Renderable
      * @var Panel
      */
     protected $panel;
+    /**
+     * @var \Illuminate\Support\Collection
+     */
+    protected $rows;
 
     /**
      * Show constructor.
@@ -108,7 +114,7 @@ class Show implements Renderable
             default:
                 $this->setKey($id);
         }
-
+        $this->rows = new Collection();
         $this->builder = $builder;
 
         $this->initModel($model);
@@ -693,6 +699,28 @@ class Show implements Renderable
         }
     }
 
+    /**
+     * Add a row in Show.
+     *
+     * @param Closure $callback
+     *
+     * @return $this
+     */
+    public function row(Closure $callback)
+    {
+        $this->rows->push(new Row($callback, $this));
+
+        return $this;
+    }
+
+    /**
+     * @return \Illuminate\Support\Collection
+     */
+    public function rows()
+    {
+        return $this->rows;
+    }
+
     /**
      * Add a model field to show.
      *

+ 1 - 0
src/Show/Panel.php

@@ -59,6 +59,7 @@ class Panel implements Renderable
         $this->data = [
             'fields' => new Collection(),
             'tools'  => new Tools($this),
+            'rows'   => $this->parent->rows(),
             'style'  => 'default',
             'title'  => trans('admin.detail'),
         ];

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است