Просмотр исходного кода

Merge branch 'master' into dev

jqh 6 лет назад
Родитель
Сommit
721410edcb
100 измененных файлов с 1864 добавлено и 579 удалено
  1. 8 7
      README.md
  2. 397 0
      resources/assets/bootstrap-validator/validator.js
  3. 8 0
      resources/assets/bootstrap-validator/validator.min.js
  4. 19 12
      resources/assets/dcat-admin/form.js
  5. 5 0
      resources/assets/dcat-admin/main.css
  6. 51 17
      resources/assets/dcat-admin/main.js
  7. 0 0
      resources/assets/dcat-admin/main.min.css
  8. 0 0
      resources/assets/dcat-admin/main.min.js
  9. 29 6
      resources/assets/dcat-admin/modal-form.js
  10. 10 3
      resources/assets/dcat-admin/select-resource.js
  11. 0 0
      resources/assets/dcat-admin/select-resource.min.js
  12. 29 24
      resources/lang/en/admin.php
  13. 29 24
      resources/lang/zh-CN/admin.php
  14. 2 2
      resources/views/dashboard/title.blade.php
  15. 1 1
      resources/views/filter/between.blade.php
  16. 1 1
      resources/views/filter/betweenDatetime.blade.php
  17. 2 2
      resources/views/filter/button.blade.php
  18. 1 1
      resources/views/filter/gt.blade.php
  19. 1 1
      resources/views/filter/lt.blade.php
  20. 1 1
      resources/views/filter/where.blade.php
  21. 1 0
      resources/views/form/error.blade.php
  22. 13 34
      src/Admin.php
  23. 2 2
      src/Console/ImportCommand.php
  24. 3 2
      src/Controllers/AuthController.php
  25. 9 1
      src/Controllers/PermissionController.php
  26. 10 1
      src/Controllers/RoleController.php
  27. 6 5
      src/Controllers/UserController.php
  28. 54 34
      src/Form.php
  29. 10 1
      src/Form/Builder.php
  30. 24 9
      src/Form/Concerns/HasFieldValidator.php
  31. 3 0
      src/Form/Condition.php
  32. 2 2
      src/Form/EmbeddedForm.php
  33. 38 24
      src/Form/Field.php
  34. 1 1
      src/Form/Field/BootstrapFile.php
  35. 2 2
      src/Form/Field/BootstrapImage.php
  36. 1 1
      src/Form/Field/BootstrapMultipleFile.php
  37. 1 1
      src/Form/Field/BootstrapMultipleImage.php
  38. 1 1
      src/Form/Field/Captcha.php
  39. 1 1
      src/Form/Field/Currency.php
  40. 1 1
      src/Form/Field/Date.php
  41. 1 2
      src/Form/Field/DateRange.php
  42. 2 2
      src/Form/Field/Email.php
  43. 1 1
      src/Form/Field/Embeds.php
  44. 1 1
      src/Form/Field/File.php
  45. 1 1
      src/Form/Field/HasMany.php
  46. 1 1
      src/Form/Field/Image.php
  47. 1 1
      src/Form/Field/Ip.php
  48. 1 1
      src/Form/Field/KeyValue.php
  49. 1 1
      src/Form/Field/ListField.php
  50. 1 1
      src/Form/Field/Mobile.php
  51. 1 1
      src/Form/Field/MultipleFile.php
  52. 1 1
      src/Form/Field/MultipleImage.php
  53. 1 1
      src/Form/Field/MultipleSelect.php
  54. 1 1
      src/Form/Field/Number.php
  55. 2 2
      src/Form/Field/SelectResource.php
  56. 1 1
      src/Form/Field/SwitchField.php
  57. 1 1
      src/Form/Field/Table.php
  58. 1 1
      src/Form/Field/Tags.php
  59. 14 0
      src/Form/Field/Tel.php
  60. 91 1
      src/Form/Field/Text.php
  61. 1 1
      src/Form/Field/Tree.php
  62. 1 1
      src/Form/Field/Url.php
  63. 2 2
      src/Form/NestedForm.php
  64. 10 16
      src/Grid.php
  65. 3 1
      src/Grid/Column.php
  66. 3 0
      src/Grid/Column/Condition.php
  67. 32 49
      src/Grid/Concerns/HasExporter.php
  68. 8 0
      src/Grid/Concerns/HasSelector.php
  69. 1 1
      src/Grid/Concerns/HasTools.php
  70. 23 10
      src/Grid/Displayers/DropdownActions.php
  71. 93 13
      src/Grid/Exporter.php
  72. 177 32
      src/Grid/Exporters/AbstractExporter.php
  73. 0 94
      src/Grid/Exporters/CsvExporter.php
  74. 41 0
      src/Grid/Exporters/ExcelExporter.php
  75. 10 4
      src/Grid/Filter/Presenter/Select.php
  76. 5 2
      src/Grid/Filter/Presenter/SelectResource.php
  77. 4 1
      src/Grid/Filter/Scope.php
  78. 37 9
      src/Grid/Model.php
  79. 26 1
      src/Grid/Row.php
  80. 6 3
      src/Grid/Tools/ExportButton.php
  81. 1 3
      src/Grid/Tools/FilterButton.php
  82. 12 4
      src/Grid/Tools/QuickSearch.php
  83. 6 6
      src/Grid/Tools/Selector.php
  84. 3 7
      src/Repositories/Proxy.php
  85. 1 1
      src/Show.php
  86. 22 0
      src/Support/Helper.php
  87. 1 0
      src/Traits/HasAssets.php
  88. 7 2
      src/Widgets/Accordion.php
  89. 49 10
      src/Widgets/AjaxRequestBuilder.php
  90. 36 3
      src/Widgets/Alert.php
  91. 1 1
      src/Widgets/Box.php
  92. 63 10
      src/Widgets/Chart/Chart.php
  93. 2 9
      src/Widgets/Colors.php
  94. 43 11
      src/Widgets/DataCard/Card.php
  95. 9 4
      src/Widgets/DataCard/DoughnutChartCard.php
  96. 79 1
      src/Widgets/Dropdown.php
  97. 8 0
      src/Widgets/Dump.php
  98. 80 12
      src/Widgets/Form.php
  99. 2 1
      src/Widgets/Markdown.php
  100. 54 7
      src/Widgets/Sparkline/Sparkline.php

+ 8 - 7
README.md

@@ -1,14 +1,15 @@
 
-<p align="center">
-<a href="https://jqhph.gitee.io/dcatadmin/">
-<img height="60px" src="https://jqhph.gitee.io/dcatadmin/assets/img/logo.png" alt="dcat-admin">
-</a>
+<div align="center">
+
+# DCAT ADMIN
+
+</div>
 
 <p align="center"><code>Dcat Admin</code>是一个基于<a href="https://www.laravel-admin.org/" target="_blank">laravel-admin</a>二次开发而成的后台构建工具,只需使用很少的代码即可快速构建出一个功能完善的漂亮的管理后台。</p>
 
 <p align="center">
-<a href="https://jqhph.gitee.io/dcatadmin/">文档</a> |
-<a href="https://jqhph.gitee.io/dcatadmin/demo.html">Demo</a> |
+<a href="https://jqhph.github.io/dcat-admin">文档</a> |
+<a href="https://jqhph.github.io/dcat-admin/demo.html">Demo</a> |
 <a href="https://github.com/jqhph/dcat-admin-demo">Demo源码</a> |
 <a href="#extensions">扩展</a>
 </p>
@@ -27,7 +28,7 @@
 
 ## 前言
 
-就我个人的感受而言,`Laravel Admin`是我使用过的最好用的后台构建工具,API简洁易用,入门也很容易,没有那么多花里胡哨的东西。而我之所以要开发这个项目,主要是想对`Laravel Admin`的一些细节做一些补充调整,增加一些比较常用的功能,优化开发体验(比如增加前端静态资源按需加载支持、美化界面和布局、增加表单弹窗、双表头表格等等比较实用的功能),总的来说可以把这个项目看做`Laravel Admin`“2.0”,更详细的异同点查看请[点击这里](https://jqhph.gitee.io/dcatadmin/docs-master-new.html)。
+就我个人的感受而言,`Laravel Admin`是我使用过的最好用的后台构建工具,API简洁易用,入门也很容易,没有那么多花里胡哨的东西。而我之所以要开发这个项目,主要是想对`Laravel Admin`的一些细节做一些补充调整,增加一些比较常用的功能,优化开发体验(比如增加前端静态资源按需加载支持、美化界面和布局、增加表单弹窗、双表头表格等等比较实用的功能),总的来说可以把这个项目看做`Laravel Admin`“2.0”,更详细的异同点查看请[点击这里](https://jqhph.github.io/dcat-admin/docs-master-new.html)。
 
 > 有的同学可能想问:现在都流行前后端分离这么久了,还搞这种后端渲染的项目有意义吗?答案是当然有意义。因为开发一个前后端分离项目也是需要一定成本和资源的(例如你得有个熟悉前端的开发人员),实际项目中也需要考量一下为一个管理后台耗费这些成本资源值不值得,并不是所有项目用前后端分离就更好。当然如果条件允许的话,用前后端分离的架构会更好一些。
 

+ 397 - 0
resources/assets/bootstrap-validator/validator.js

@@ -0,0 +1,397 @@
+/*!
+ * Validator v0.11.9 for Bootstrap 3, by @1000hz
+ * Copyright 2017 Cina Saffary
+ * Licensed under http://opensource.org/licenses/MIT
+ *
+ * https://github.com/1000hz/bootstrap-validator
+ */
+
++function ($) {
+  'use strict';
+
+  // VALIDATOR CLASS DEFINITION
+  // ==========================
+
+  function getValue($el) {
+    return $el.is('[type="checkbox"]') ? $el.prop('checked')                                     :
+        $el.is('[type="radio"]')    ? !!$('[name="' + $el.attr('name') + '"]:checked').length :
+            $el.is('select[multiple]')  ? ($el.val() || []).length                                :
+                $el.val()
+  }
+
+  var Validator = function (element, options) {
+    this.options    = options
+    this.validators = $.extend({}, Validator.VALIDATORS, options.custom)
+    this.$element   = $(element)
+    this.$btn       = $('button[type="submit"], input[type="submit"]')
+        .filter('[form="' + this.$element.attr('id') + '"]')
+        .add(this.$element.find('input[type="submit"], button[type="submit"]'))
+
+    this.update()
+
+    this.$element.on('input.bs.validator change.bs.validator focusout.bs.validator', $.proxy(this.onInput, this))
+    this.$element.on('submit.bs.validator', $.proxy(this.onSubmit, this))
+    this.$element.on('reset.bs.validator', $.proxy(this.reset, this))
+
+    this.$element.find('[data-match]').each(function () {
+      var $this  = $(this)
+      var target = $this.attr('data-match')
+
+      $(target).on('input.bs.validator', function (e) {
+        getValue($this) && $this.trigger('input.bs.validator')
+      })
+    })
+
+    // run validators for fields with values, but don't clobber server-side errors
+    this.$inputs.filter(function () {
+      return getValue($(this)) && !$(this).closest('.has-error').length
+    }).trigger('focusout')
+
+    this.$element.attr('novalidate', true) // disable automatic native validation
+  }
+
+  Validator.VERSION = '0.11.9'
+
+  Validator.INPUT_SELECTOR = ':input:not([type="hidden"], [type="submit"], [type="reset"], button)'
+
+  Validator.FOCUS_OFFSET = 20
+
+  Validator.DEFAULTS = {
+    delay: 500,
+    html: false,
+    disable: true,
+    focus: true,
+    custom: {},
+    errors: {
+      match: 'Does not match',
+      minlength: 'Not long enough'
+    },
+    feedback: {
+      success: 'glyphicon-ok',
+      error: 'glyphicon-remove'
+    }
+  }
+
+  Validator.VALIDATORS = {
+    'native': function ($el) {
+      var el = $el[0]
+      if (el.checkValidity) {
+        return !el.checkValidity() && !el.validity.valid && (el.validationMessage || "error!")
+      }
+    },
+    'match': function ($el) {
+      var target = $el.attr('data-match')
+      return $el.val() !== $(target).val() && Validator.DEFAULTS.errors.match
+    },
+    'minlength': function ($el) {
+      var minlength = $el.attr('data-minlength')
+      return $el.val().length < minlength && Validator.DEFAULTS.errors.minlength
+    }
+  }
+
+  Validator.prototype.update = function () {
+    var self = this
+
+    this.$inputs = this.$element.find(Validator.INPUT_SELECTOR)
+        .add(this.$element.find('[data-validate="true"]'))
+        .not(this.$element.find('[data-validate="false"]')
+            .each(function () { self.clearErrors($(this)) })
+        )
+
+    this.toggleSubmit()
+
+    return this
+  }
+
+  Validator.prototype.onInput = function (e) {
+    var self        = this
+    var $el         = $(e.target)
+    var deferErrors = e.type !== 'focusout'
+
+    if (!this.$inputs.is($el)) return
+
+    this.validateInput($el, deferErrors).done(function () {
+      self.toggleSubmit()
+    })
+  }
+
+  Validator.prototype.validateInput = function ($el, deferErrors) {
+    var value      = getValue($el)
+    var prevErrors = $el.data('bs.validator.errors')
+
+    if ($el.is('[type="radio"]')) $el = this.$element.find('input[name="' + $el.attr('name') + '"]')
+
+    var e = $.Event('validate.bs.validator', {relatedTarget: $el[0]})
+    this.$element.trigger(e)
+    if (e.isDefaultPrevented()) return
+
+    var self = this
+
+    return this.runValidators($el).done(function (errors) {
+      $el.data('bs.validator.errors', errors)
+
+      errors.length
+          ? deferErrors ? self.defer($el, self.showErrors) : self.showErrors($el)
+          : self.clearErrors($el)
+
+      if (!prevErrors || errors.toString() !== prevErrors.toString()) {
+        e = errors.length
+            ? $.Event('invalid.bs.validator', {relatedTarget: $el[0], detail: errors})
+            : $.Event('valid.bs.validator', {relatedTarget: $el[0], detail: prevErrors})
+
+        self.$element.trigger(e)
+      }
+
+      self.toggleSubmit()
+
+      self.$element.trigger($.Event('validated.bs.validator', {relatedTarget: $el[0]}))
+    })
+  }
+
+
+  Validator.prototype.runValidators = function ($el) {
+    var errors   = []
+    var deferred = $.Deferred()
+
+    $el.data('bs.validator.deferred') && $el.data('bs.validator.deferred').reject()
+    $el.data('bs.validator.deferred', deferred)
+
+    function getValidatorSpecificError(key) {
+      return $el.attr('data-' + key + '-error')
+    }
+
+    function getValidityStateError() {
+      var validity = $el[0].validity
+      return validity.typeMismatch    ? $el.attr('data-type-error')
+          : validity.patternMismatch ? $el.attr('data-pattern-error')
+              : validity.stepMismatch    ? $el.attr('data-step-error')
+                  : validity.rangeOverflow   ? $el.attr('data-max-error')
+                      : validity.rangeUnderflow  ? $el.attr('data-min-error')
+                          : validity.valueMissing    ? $el.attr('data-required-error')
+                              :                            null
+    }
+
+    function getGenericError() {
+      return $el.attr('data-error')
+    }
+
+    function getErrorMessage(key) {
+      return getValidatorSpecificError(key)
+          || getValidityStateError()
+          || getGenericError()
+    }
+
+    $.each(this.validators, $.proxy(function (key, validator) {
+      var error = null, rule = $el.attr('data-' + key) || null
+
+      if (
+          (getValue($el) || $el.attr('required') || key == 'match') &&
+          (rule || key == 'native') &&
+          (error = validator.call(this, $el))
+      ) {
+        error = getErrorMessage(key) || error
+        !~errors.indexOf(error) && errors.push(error)
+      }
+    }, this))
+
+    if (!errors.length && getValue($el) && $el.attr('data-remote')) {
+      this.defer($el, function () {
+        var data = {}
+        data[$el.attr('name')] = getValue($el)
+        $.get($el.attr('data-remote'), data)
+            .fail(function (jqXHR, textStatus, error) { errors.push(getErrorMessage('remote') || error) })
+            .always(function () { deferred.resolve(errors)})
+      })
+    } else deferred.resolve(errors)
+
+    return deferred.promise()
+  }
+
+  Validator.prototype.validate = function () {
+    var self = this
+
+    $.when(this.$inputs.map(function (el) {
+      return self.validateInput($(this), false)
+    })).then(function () {
+      self.toggleSubmit()
+      self.focusError()
+    })
+
+    return this
+  }
+
+  Validator.prototype.focusError = function () {
+    if (!this.options.focus) return
+
+    var $input = this.$element.find(".has-error:first :input")
+    if ($input.length === 0) return
+
+    $('html, body').animate({scrollTop: $input.offset().top - Validator.FOCUS_OFFSET}, 250)
+    $input.focus()
+  }
+
+  Validator.prototype.showErrors = function ($el) {
+    var method = this.options.html ? 'html' : 'text'
+    var errors = $el.data('bs.validator.errors')
+    var $group = $el.closest('.form-group')
+    var $block = $group.find('.help-block.with-errors')
+    var $feedback = $group.find('.form-control-feedback')
+
+    if (!errors.length) return
+
+    errors = $('<ul/>')
+        .addClass('list-unstyled')
+        .append($.map(errors, function (error) { return $('<li/>')[method](error) }))
+
+    $block.data('bs.validator.originalContent') === undefined && $block.data('bs.validator.originalContent', $block.html())
+    $block.empty().append(errors)
+    $group.addClass('has-error has-danger')
+
+    $group.hasClass('has-feedback')
+    && $feedback.removeClass(this.options.feedback.success)
+    && $feedback.addClass(this.options.feedback.error)
+    && $group.removeClass('has-success')
+  }
+
+  Validator.prototype.clearErrors = function ($el) {
+    var $group = $el.closest('.form-group')
+    var $block = $group.find('.help-block.with-errors')
+    var $feedback = $group.find('.form-control-feedback')
+
+    $block.html($block.data('bs.validator.originalContent'))
+    $group.removeClass('has-error has-danger has-success')
+
+    $group.hasClass('has-feedback')
+    && $feedback.removeClass(this.options.feedback.error)
+    && $feedback.removeClass(this.options.feedback.success)
+    && getValue($el)
+    && $feedback.addClass(this.options.feedback.success)
+    && $group.addClass('has-success')
+  }
+
+  Validator.prototype.hasErrors = function () {
+    function fieldErrors() {
+      return !!($(this).data('bs.validator.errors') || []).length
+    }
+
+    return !!this.$inputs.filter(fieldErrors).length
+  }
+
+  Validator.prototype.isIncomplete = function () {
+    function fieldIncomplete() {
+      var value = getValue($(this))
+      return !(typeof value == "string" ? $.trim(value) : value)
+    }
+
+    return !!this.$inputs.filter('[required]').filter(fieldIncomplete).length
+  }
+
+  Validator.prototype.onSubmit = function (e) {
+    this.validate()
+    if (this.isIncomplete() || this.hasErrors()) e.preventDefault()
+  }
+
+  Validator.prototype.toggleSubmit = function () {
+    if (!this.options.disable) return
+    this.$btn.toggleClass('disabled', this.isIncomplete() || this.hasErrors())
+  }
+
+  Validator.prototype.defer = function ($el, callback) {
+    callback = $.proxy(callback, this, $el)
+    if (!this.options.delay) return callback()
+    window.clearTimeout($el.data('bs.validator.timeout'))
+    $el.data('bs.validator.timeout', window.setTimeout(callback, this.options.delay))
+  }
+
+  Validator.prototype.reset = function () {
+    this.$element.find('.form-control-feedback')
+        .removeClass(this.options.feedback.error)
+        .removeClass(this.options.feedback.success)
+
+    this.$inputs
+        .removeData(['bs.validator.errors', 'bs.validator.deferred'])
+        .each(function () {
+          var $this = $(this)
+          var timeout = $this.data('bs.validator.timeout')
+          window.clearTimeout(timeout) && $this.removeData('bs.validator.timeout')
+        })
+
+    this.$element.find('.help-block.with-errors')
+        .each(function () {
+          var $this = $(this)
+          var originalContent = $this.data('bs.validator.originalContent')
+
+          $this
+              .removeData('bs.validator.originalContent')
+              .html(originalContent)
+        })
+
+    this.$btn.removeClass('disabled')
+
+    this.$element.find('.has-error, .has-danger, .has-success').removeClass('has-error has-danger has-success')
+
+    return this
+  }
+
+  Validator.prototype.destroy = function () {
+    this.reset()
+
+    this.$element
+        .removeAttr('novalidate')
+        .removeData('bs.validator')
+        .off('.bs.validator')
+
+    this.$inputs
+        .off('.bs.validator')
+
+    this.options    = null
+    this.validators = null
+    this.$element   = null
+    this.$btn       = null
+    this.$inputs    = null
+
+    return this
+  }
+
+  // VALIDATOR PLUGIN DEFINITION
+  // ===========================
+
+
+  function Plugin(option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var options = $.extend({}, Validator.DEFAULTS, $this.data(), typeof option == 'object' && option)
+      var data    = $this.data('bs.validator')
+
+      if (!data && option == 'destroy') return
+      if (!data) $this.data('bs.validator', (data = new Validator(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  var old = $.fn.validator
+
+  $.fn.validator             = Plugin
+  $.fn.validator.Constructor = Validator
+
+
+  // VALIDATOR NO CONFLICT
+  // =====================
+
+  $.fn.validator.noConflict = function () {
+    $.fn.validator = old
+    return this
+  }
+
+
+  // VALIDATOR DATA-API
+  // ==================
+
+  $(window).on('load', function () {
+    $('form[data-toggle="validator"]').each(function () {
+      var $form = $(this)
+      Plugin.call($form, $form.data())
+    })
+  })
+
+}(jQuery);

Разница между файлами не показана из-за своего большого размера
+ 8 - 0
resources/assets/bootstrap-validator/validator.min.js


+ 19 - 12
resources/assets/dcat-admin/form.js

@@ -15,7 +15,8 @@
             disableRedirect: false, //
             columnSelectors: {}, //
             disableRemoveError: false,
-            after: function (success, data) {},
+            before: function () {},
+            after: function () {},
         }, opts);
 
         var originalVals = {},
@@ -33,12 +34,18 @@
                 return $("[href='#" + id + "'] .text-red");
             };
 
+        var self = this;
+
         // 移除错误信息
         remove_field_error();
 
         $form.ajaxSubmit({
             beforeSubmit: function (d, f, o) {
-                if (call_events(LA._form_.before, d, f, o) === false) {
+                if (opts.before(d, f, o, self) === false) {
+                    return false;
+                }
+
+                if (fire(LA._form_.before, d, f, o, self) === false) {
                     return false;
                 }
 
@@ -47,11 +54,11 @@
             success: function (d) {
                 LA.NP.done();
 
-                if (opts.after(true, d) === false) {
+                if (opts.after(true, d, self) === false) {
                     return;
                 }
 
-                if (call_events(LA._form_.success, d) === false) {
+                if (fire(LA._form_.success, d, self) === false) {
                     return;
                 }
 
@@ -73,11 +80,11 @@
             error: function (v) {
                 LA.NP.done();
 
-                if (opts.after(false, v) === false) {
+                if (opts.after(false, v, self) === false) {
                     return;
                 }
 
-                if (call_events(LA._form_.error, v) === false) {
+                if (fire(LA._form_.error, v, self) === false) {
                     return;
                 }
 
@@ -101,13 +108,13 @@
         });
 
         // 触发钩子事件
-        function call_events(evs) {
-            var i, r, a = arguments, j, p = [];
-            delete a[0];
-            a = a || [];
+        function fire(evs) {
+            var i, j, r, args = arguments, p = [];
+            delete args[0];
+            args = args || [];
 
-            for (j in a) {
-                p.push(a[j]);
+            for (j in args) {
+                p.push(args[j]);
             }
 
             for (i in evs) {

+ 5 - 0
resources/assets/dcat-admin/main.css

@@ -2148,6 +2148,11 @@ div.layui-layer-btn{
     color:var(--danger-dark)!important;
 }
 
+.material .dropdown-btn-group .btn.btn-primary,.material .open .dropdown-toggle.btn {
+    background-color: var(--40) !important;
+    border-color: var(--40) !important;
+}
+
 .quick-search-clear {
     color:transparent;
     display:none;

+ 51 - 17
resources/assets/dcat-admin/main.js

@@ -400,6 +400,14 @@ window.require = window.define = window.exports = window.module = undefined;
             });
         };
 
+        // 注册自定义验证器
+        LA.extendValidator = function (rule, callback, message) {
+            var GLOBAL = $.fn.validator.Constructor.DEFAULTS;
+
+            GLOBAL.custom[rule] = callback;
+            GLOBAL.errors[rule] = message || null;
+        };
+
         function layer_position(idx, p) {
             switch (p) {
                 case 'rb':
@@ -734,7 +742,8 @@ window.require = window.define = window.exports = window.module = undefined;
             disableRedirect: false, //
             columnSelectors: {}, //
             disableRemoveError: false,
-            after: function (success, data) {},
+            before: function () {},
+            after: function () {},
         }, opts);
 
         var originalVals = {},
@@ -752,12 +761,18 @@ window.require = window.define = window.exports = window.module = undefined;
                 return $("[href='#" + id + "'] .text-red");
             };
 
+        var self = this;
+
         // 移除错误信息
         remove_field_error();
 
         $form.ajaxSubmit({
             beforeSubmit: function (d, f, o) {
-                if (call_events(LA._form_.before, d, f, o) === false) {
+                if (opts.before(d, f, o, self) === false) {
+                    return false;
+                }
+
+                if (fire(LA._form_.before, d, f, o, self) === false) {
                     return false;
                 }
 
@@ -766,11 +781,11 @@ window.require = window.define = window.exports = window.module = undefined;
             success: function (d) {
                 LA.NP.done();
 
-                if (opts.after(true, d) === false) {
+                if (opts.after(true, d, self) === false) {
                     return;
                 }
 
-                if (call_events(LA._form_.success, d) === false) {
+                if (fire(LA._form_.success, d, self) === false) {
                     return;
                 }
 
@@ -792,11 +807,11 @@ window.require = window.define = window.exports = window.module = undefined;
             error: function (v) {
                 LA.NP.done();
 
-                if (opts.after(false, v) === false) {
+                if (opts.after(false, v, self) === false) {
                     return;
                 }
 
-                if (call_events(LA._form_.error, v) === false) {
+                if (fire(LA._form_.error, v, self) === false) {
                     return;
                 }
 
@@ -820,13 +835,13 @@ window.require = window.define = window.exports = window.module = undefined;
         });
 
         // 触发钩子事件
-        function call_events(evs) {
-            var i, r, a = arguments, j, p = [];
-            delete a[0];
-            a = a || [];
+        function fire(evs) {
+            var i, j, r, args = arguments, p = [];
+            delete args[0];
+            args = args || [];
 
-            for (j in a) {
-                p.push(a[j]);
+            for (j in args) {
+                p.push(args[j]);
             }
 
             for (i in evs) {
@@ -1068,7 +1083,7 @@ window.require = window.define = window.exports = window.module = undefined;
             tpl = LA.AssetsLoader.filterScriptAndAutoLoad(tpl).render();
             var t = $(tpl), $form, btns = [lang.submit], opts = {
                 type: 1,
-                area: area,
+                area: formatArea(area),
                 content: tpl,
                 title: title,
                 yes: submit,
@@ -1098,15 +1113,24 @@ window.require = window.define = window.exports = window.module = undefined;
             $layWin[num] = w.$('#layui-layer' + idx[num]);
 
             // 提交表单
-            function submit (index, layero) {
+            function submit () {
                 if (submitting) return;
-                submitting = 1;
                 $form = $form || w.$('#'+t.find('form').attr('id'));  // 此处必须重新创建jq对象,否则无法操作页面元素
-                $layWin[num].find('.layui-layer-btn0').button('loading');
 
                 LA.Form({
                     $form: $form,
                     disableRedirect: true,
+                    before: function () {
+                        $form.validator('validate');
+
+                        if ($form.find('.has-error').length > 0) {
+                            return false;
+                        }
+
+                        submitting = 1;
+
+                        $layWin[num].find('.layui-layer-btn0').button('loading');
+                    },
                     after: function (success, res) {
                         $layWin[num].find('.layui-layer-btn0').button('reset');
                         submitting = 0;
@@ -1114,7 +1138,7 @@ window.require = window.define = window.exports = window.module = undefined;
                         handlers.saved(success, res);
 
                         if (!success) {
-                            return handlers.error(success, res);;
+                            return handlers.error(success, res);
                         }
                         if (res.status) {
                             handlers.success(success, res);
@@ -1127,9 +1151,19 @@ window.require = window.define = window.exports = window.module = undefined;
                     }
                 });
 
+                return false;
+
             }
         }
 
+        function formatArea(area) {
+            if (w.screen.width <= 800) {
+                return ['100%', '100%',];
+            }
+
+            return area;
+        }
+
         // 移除弹窗
         function rm(num) {
             lay.close(idx[num]);

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
resources/assets/dcat-admin/main.min.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
resources/assets/dcat-admin/main.min.js


+ 29 - 6
resources/assets/dcat-admin/modal-form.js

@@ -69,16 +69,22 @@
 
             $.get(url, function (tpl) {
                 building = 0;
-                (!$btn) || $btn.button('reset');
+                if ($btn) {
+                    $btn.button('reset');
+                    setTimeout(function () {
+                        $btn.find('.waves-ripple').remove();
+                    }, 50);
+                }
                 popup(tpl, num);
             });
         }
 
         // 弹出弹窗
         function popup(tpl, num) {
+            tpl = LA.AssetsLoader.filterScriptAndAutoLoad(tpl).render();
             var t = $(tpl), $form, btns = [lang.submit], opts = {
                 type: 1,
-                area: area,
+                area: formatArea(area),
                 content: tpl,
                 title: title,
                 yes: submit,
@@ -108,15 +114,24 @@
             $layWin[num] = w.$('#layui-layer' + idx[num]);
 
             // 提交表单
-            function submit (index, layero) {
+            function submit () {
                 if (submitting) return;
-                submitting = 1;
                 $form = $form || w.$('#'+t.find('form').attr('id'));  // 此处必须重新创建jq对象,否则无法操作页面元素
-                $layWin[num].find('.layui-layer-btn0').button('loading');
 
                 LA.Form({
                     $form: $form,
                     disableRedirect: true,
+                    before: function () {
+                        $form.validator('validate');
+
+                        if ($form.find('.has-error').length > 0) {
+                            return false;
+                        }
+
+                        submitting = 1;
+
+                        $layWin[num].find('.layui-layer-btn0').button('loading');
+                    },
                     after: function (success, res) {
                         $layWin[num].find('.layui-layer-btn0').button('reset');
                         submitting = 0;
@@ -124,7 +139,7 @@
                         handlers.saved(success, res);
 
                         if (!success) {
-                            return handlers.error(success, res);;
+                            return handlers.error(success, res);
                         }
                         if (res.status) {
                             handlers.success(success, res);
@@ -140,6 +155,14 @@
             }
         }
 
+        function formatArea(area) {
+            if (w.screen.width <= 800) {
+                return ['100%', '100%'];
+            }
+
+            return area;
+        }
+
         // 移除弹窗
         function rm(num) {
             lay.close(idx[num]);

+ 10 - 3
resources/assets/dcat-admin/select-resource.js

@@ -5,7 +5,7 @@
         options = $.extend({
             title: '选择', // 弹窗标题
             selector: '', // 选择按钮选择器
-            column: "", // 字段名称
+            column: '', // 字段名称
             source: '', // 资源地址
             maxItem: 1, // 最大选项数量,0为不限制
             area: ['80%', '90%'],
@@ -63,7 +63,7 @@
                 maxmin: false,
                 shade: false,
                 skin: 'select-resource',
-                area: options.area,
+                area: format_area(options.area),
                 content: options.source + '?_mini=1',
                 btn: options.showCloseButton ? [options.closeButtonText] : null,
                 success: function (layero) {
@@ -230,6 +230,14 @@
             render_tags(originalItems);
         }
 
+        function format_area(area) {
+            if (w.screen.width <= 750) {
+                return ['100%', '100%'];
+            }
+
+            return area;
+        }
+
         render_tags(originalItems);
     }
 
@@ -318,7 +326,6 @@
                 $app.html(build_many(tag));
             }
 
-
             function build_many(tag) {
                 var html = [];
 

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
resources/assets/dcat-admin/select-resource.min.js


+ 29 - 24
resources/lang/en/admin.php

@@ -154,35 +154,40 @@ return [
         'filter_clear'       => 'Show all',
         'filter_placeholder' => 'Filter',
     ],
-    'responsive'            => [
-        'display'               => '  ',
-        'display_all'           => ' <i class="glyphicon glyphicon-th icon-th"></i> ',
-        'focus'                 => 'Focus',
+    'responsive' => [
+        'display'     => '  ',
+        'display_all' => ' <i class="glyphicon glyphicon-th icon-th"></i> ',
+        'focus'       => 'Focus',
     ],
     'uploader' => [
-        'add_new_media' => 'Browse',
-        'drag_file' => 'Or drag file here',
-        'max_file_limit' => 'The :attribute may not be greater than :max.',
-        'exceed_size' => 'Exceeds the maximum file-size',
-        'interrupt' => 'Interrupt',
-        'upload_failed' => 'Upload failed! Please try again.',
-        'selected_files' => ':num files selected,size: :size。',
-        'selected_has_failed' => 'Uploaded: :success, failed: :fail, <a class="retry"  href="javascript:"";">retry</a>or<a class="ignore" href="javascript:"";">ignore</a>',
-        'selected_success' => ':num(:size) files selected, Uploaded: :success.',
-        'dot' => ', ',
-        'failed_num' => 'failed::fail.',
-        'pause_upload' => 'Pause',
-        'go_on_upload' => 'Go On',
-        'start_upload' => 'Upload',
+        'add_new_media'          => 'Browse',
+        'drag_file'              => 'Or drag file here',
+        'max_file_limit'         => 'The :attribute may not be greater than :max.',
+        'exceed_size'            => 'Exceeds the maximum file-size',
+        'interrupt'              => 'Interrupt',
+        'upload_failed'          => 'Upload failed! Please try again.',
+        'selected_files'         => ':num files selected,size: :size。',
+        'selected_has_failed'    => 'Uploaded: :success, failed: :fail, <a class="retry"  href="javascript:"";">retry</a>or<a class="ignore" href="javascript:"";">ignore</a>',
+        'selected_success'       => ':num(:size) files selected, Uploaded: :success.',
+        'dot'                    => ', ',
+        'failed_num'             => 'failed::fail.',
+        'pause_upload'           => 'Pause',
+        'go_on_upload'           => 'Go On',
+        'start_upload'           => 'Upload',
         'upload_success_message' => ':success files uploaded successfully',
-        'go_on_add' => 'New File',
-        'Q_TYPE_DENIED' => 'Sorry, the type of this file is not allowed!',
-        'Q_EXCEED_NUM_LIMIT' => 'Sorry, maximum number of allowable file uploads has been exceeded!',
-        'F_EXCEED_SIZE' => 'Sorry,the maximum file-size has been exceeded!',
-        'Q_EXCEED_SIZE_LIMIT' => 'Sorry, the maximum file-size has been exceeded!',
-        'F_DUPLICATE' => 'Duplicate file.',
+        'go_on_add'              => 'New File',
+        'Q_TYPE_DENIED'          => 'Sorry, the type of this file is not allowed!',
+        'Q_EXCEED_NUM_LIMIT'     => 'Sorry, maximum number of allowable file uploads has been exceeded!',
+        'F_EXCEED_SIZE'          => 'Sorry,the maximum file-size has been exceeded!',
+        'Q_EXCEED_SIZE_LIMIT'    => 'Sorry, the maximum file-size has been exceeded!',
+        'F_DUPLICATE'            => 'Duplicate file.',
     ],
     'import_extension_confirm' => 'Are you sure import the extension?',
     'selected_must_less_then'  => 'Only supports maximum :num options.',
+    'validation' => [
+        'match'     => 'The :attribute and :other must match.',
+        'minlength' => 'The :attribute must be at least :min characters.',
+        'maxlength' => 'The :attribute may not be greater than :max characters.',
+    ],
     'menu_titles' => [],
 ];

+ 29 - 24
resources/lang/zh-CN/admin.php

@@ -155,35 +155,40 @@ return [
         'filter_clear'       => '显示全部',
         'filter_placeholder' => '过滤',
     ],
-    'responsive'            => [
-        'display'               => '  ',
-        'display_all'           => ' <i class="glyphicon glyphicon-th icon-th"></i> ',
-        'focus'                 => '聚焦',
+    'responsive'      => [
+        'display'     => '  ',
+        'display_all' => ' <i class="glyphicon glyphicon-th icon-th"></i> ',
+        'focus'       => '聚焦',
     ],
     'uploader' => [
-        'add_new_media' => '添加文件',
-        'drag_file' => '或将文件拖到这里',
-        'max_file_limit' => 'The :attribute may not be greater than :max.',
-        'exceed_size' => '文件大小超出',
-        'interrupt' => '上传暂停',
-        'upload_failed' => '上传失败,请重试',
-        'selected_files' => '选中:num个文件,共:size。',
-        'selected_has_failed' => '已成功上传:success个文件,:fail个文件上传失败,<a class="retry"  href="javascript:"";">重新上传</a>失败文件或<a class="ignore" href="javascript:"";">忽略</a>',
-        'selected_success' => '共:num个(:size),已上传:success个。',
-        'dot' => ',',
-        'failed_num' => '失败:fail个。',
-        'pause_upload' => '暂停上传',
-        'go_on_upload' => '继续上传',
-        'start_upload' => '开始上传',
+        'add_new_media'          => '添加文件',
+        'drag_file'              => '或将文件拖到这里',
+        'max_file_limit'         => 'The :attribute may not be greater than :max.',
+        'exceed_size'            => '文件大小超出',
+        'interrupt'              => '上传暂停',
+        'upload_failed'          => '上传失败,请重试',
+        'selected_files'         => '选中:num个文件,共:size。',
+        'selected_has_failed'    => '已成功上传:success个文件,:fail个文件上传失败,<a class="retry"  href="javascript:"";">重新上传</a>失败文件或<a class="ignore" href="javascript:"";">忽略</a>',
+        'selected_success'       => '共:num个(:size),已上传:success个。',
+        'dot' =>                 ',',
+        'failed_num'             => '失败:fail个。',
+        'pause_upload'           => '暂停上传',
+        'go_on_upload'           => '继续上传',
+        'start_upload'           => '开始上传',
         'upload_success_message' => '已成功上传:success个文件',
-        'go_on_add' => '继续添加',
-        'Q_TYPE_DENIED' => '对不起,不允许上传此类型文件',
-        'Q_EXCEED_NUM_LIMIT' => '对不起,已超出文件上传数量限制,最多只能上传:num个文件',
-        'F_EXCEED_SIZE' => '对不起,当前选择的文件过大',
-        'Q_EXCEED_SIZE_LIMIT' => '对不起,已超出文件大小限制',
-        'F_DUPLICATE' => '文件重复',
+        'go_on_add'              => '继续添加',
+        'Q_TYPE_DENIED'          => '对不起,不允许上传此类型文件',
+        'Q_EXCEED_NUM_LIMIT'     => '对不起,已超出文件上传数量限制,最多只能上传:num个文件',
+        'F_EXCEED_SIZE'          => '对不起,当前选择的文件过大',
+        'Q_EXCEED_SIZE_LIMIT'    => '对不起,已超出文件大小限制',
+        'F_DUPLICATE'            => '文件重复',
     ],
     'import_extension_confirm' => '确认导入拓展?',
     'selected_must_less_then'  => '最多只能选择:num个选项',
+    'validation' => [
+        'match'     => '与 :attribute 不匹配。',
+        'minlength' => ':attribute 字符长度不能小于 :min。',
+        'maxlength' => ':attribute 字符长度不能大于 :max。',
+    ],
     'menu_titles' => [],
 ];

+ 2 - 2
resources/views/dashboard/title.blade.php

@@ -29,6 +29,6 @@
 </div>
 <div class="links">
     <a href="https://github.com/jqhph/dcat-admin" target="_blank">Github</a>
-    <a href="https://jqhph.gitee.io/dcatadmin/docs.html" id="doc-link" target="_blank">Documentation</a>
-    <a href="https://jqhph.gitee.io/dcatadmin/demo.html" id="demo-link" target="_blank">Demo</a>
+    <a href="https://jqhph.github.io/dcat-admin/docs.html" id="doc-link" target="_blank">Documentation</a>
+    <a href="https://jqhph.github.io/dcat-admin/demo.html" id="demo-link" target="_blank">Demo</a>
 </div>

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

@@ -1,4 +1,4 @@
-<div class="filter-input col-sm-{{ $width }} "  style="{!! $style !!}">
+<div class="filter-input col-md-{{ $width }} "  style="{!! $style !!}">
     <div class="form-group" >
         <div class="input-group input-group-sm">
             <span class="input-group-addon"><b>{!! $label !!}</b></span>

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

@@ -1,4 +1,4 @@
-<div class="filter-input col-sm-{{ $width }}"  style="{!! $style !!}">
+<div class="filter-input col-md-{{ $width }}"  style="{!! $style !!}">
     <div class="form-group">
         <div class="input-group input-group-sm">
             <span class="input-group-addon"><b>{{$label}}</b>  &nbsp;<i class="fa fa-calendar"></i></span>

+ 2 - 2
resources/views/filter/button.blade.php

@@ -1,6 +1,6 @@
-<div class="btn-group" style="margin-right:3px">
+<div class="btn-group filter-button-group" style="margin-right:3px">
     <label class="btn btn-primary {{ $btn_class }} btn-sm" @if($show_filter_text)data-toggle="dropdown"@endif>
-        <i class=" ti-filter"></i>@if(!$show_filter_text)<span class="hidden-xs">&nbsp;&nbsp;{{ trans('admin.filter') }}</span>@endif
+        <i class=" ti-filter"></i>@if($show_filter_text)<span class="hidden-xs">&nbsp;&nbsp;{{ trans('admin.filter') }}</span>@endif
     </label>
     @if($scopes->isNotEmpty())
         <button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown">

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

@@ -1,3 +1,3 @@
-<div class="filter-input col-sm-{{ $width }} " >
+<div class="filter-input col-md-{{ $width }} " >
     <div class="form-group">@include($presenter->view())</div>
 </div>

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

@@ -1,3 +1,3 @@
-<div class="filter-input col-sm-{{ $width }} " >
+<div class="filter-input col-md-{{ $width }} " >
     <div class="form-group">@include($presenter->view())</div>
 </div>

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

@@ -1,3 +1,3 @@
-<div class="filter-input col-sm-{{ $width }} " style="{!! $style !!}">
+<div class="filter-input col-md-{{ $width }} " style="{!! $style !!}">
     <div class="form-group">@include($presenter->view())</div>
 </div>

+ 1 - 0
resources/views/form/error.blade.php

@@ -1,4 +1,5 @@
 <error></error>
+<div class="help-block  with-errors"></div>
 @if(is_array($errorKey))
     @foreach($errorKey as $key => $col)
         @if($errors->has($col.$key))

+ 13 - 34
src/Admin.php

@@ -16,12 +16,9 @@ use Dcat\Admin\Layout\Menu;
 use Dcat\Admin\Layout\Navbar;
 use Illuminate\Auth\GuardHelpers;
 use Illuminate\Contracts\Auth\Authenticatable;
-use Illuminate\Contracts\Auth\Guard;
 use Illuminate\Database\Eloquent\Model;
-use Illuminate\Support\Facades\Artisan;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Event;
-use phpDocumentor\Reflection\Types\Mixed_;
 
 /**
  * Class Admin.
@@ -343,13 +340,13 @@ class Admin
     }
 
     /**
-     * Create a repository instance
+     * Create a repository instance.
      *
      * @param $class
      * @param array $args
      * @return Repository
      */
-    public static function createRepository($class, array $args = [])
+    public static function repository($class, array $args = [])
     {
         $repository = $class;
         if (is_string($repository)) {
@@ -432,35 +429,7 @@ class Admin
 
         $config[$name]['enable'] = $enable;
 
-        return static::updateExtensionConfig($config);
-    }
-
-    /**
-     * @return Handler
-     */
-    public static function makeExceptionHandler()
-    {
-        return app(
-            config('admin.exception_handler') ?: Handler::class
-        );
-    }
-
-    /**
-     * @param array $config
-     * @return bool
-     */
-    public static function updateExtensionConfig(array $config)
-    {
-        $files  = app('files');
-        $result = (bool)$files->put(config_path('admin-extensions.php'), Helper::exportArrayPhp($config));
-
-        if ($result && is_file(base_path('bootstrap/cache/config.php'))) {
-            Artisan::call('config:cache');
-        }
-
-        \config(['admin-extensions' => $config]);
-
-        return $result;
+        return Helper::updateExtensionConfig($config);
     }
 
     /**
@@ -474,6 +443,16 @@ class Admin
         return static::enableExtenstion($class, false);
     }
 
+    /**
+     * @return Handler
+     */
+    public static function makeExceptionHandler()
+    {
+        return app(
+            config('admin.exception_handler') ?: Handler::class
+        );
+    }
+
     /**
      * @param callable $callback
      */

+ 2 - 2
src/Console/ImportCommand.php

@@ -4,7 +4,7 @@ namespace Dcat\Admin\Console;
 
 use Dcat\Admin\Admin;
 use Dcat\Admin\Extension;
-use Illuminate\Console\Command;
+use Dcat\Admin\Support\Helper;
 use Illuminate\Foundation\Console\VendorPublishCommand;
 use Illuminate\Support\Arr;
 
@@ -89,7 +89,7 @@ class ImportCommand extends VendorPublishCommand
         $config[$name]['imported']    = true;
         $config[$name]['imported_at'] = date('Y-m-d H:i:s');
 
-        return Admin::updateExtensionConfig($config);
+        return Helper::updateExtensionConfig($config);
 
     }
 }

+ 3 - 2
src/Controllers/AuthController.php

@@ -170,14 +170,15 @@ class AuthController extends Controller
         $form->password('old_password', trans('admin.old_password'));
 
         $form->password('password', trans('admin.password'))
-            ->rules('confirmed')
+            ->minLength(5)
+            ->maxLength(20)
             ->customFormat(function ($v) {
                 if ($v == $this->password) {
                     return;
                 }
                 return $v;
             });
-        $form->password('password_confirmation', trans('admin.password_confirmation'));
+        $form->password('password_confirmation', trans('admin.password_confirmation'))->same('password');
 
         $form->setAction(admin_url('auth/setting'));
 

+ 9 - 1
src/Controllers/PermissionController.php

@@ -286,9 +286,17 @@ class PermissionController extends Controller
     {
         $form = new Form(new Permission());
 
+        $permissionTable = config('admin.database.permissions_table');
+        $connection      = config('admin.database.connection');
+
+        $id = $form->getKey();
+
         $form->display('id', 'ID');
 
-        $form->text('slug', trans('admin.slug'))->required();
+        $form->text('slug', trans('admin.slug'))
+            ->required()
+            ->creationRules(['required', "unique:{$connection}.{$permissionTable}"])
+            ->updateRules(['required', "unique:{$connection}.{$permissionTable},slug,$id"]);
         $form->text('name', trans('admin.name'))->required();
 
         $form->multipleSelect('http_method', trans('admin.http.method'))

+ 10 - 1
src/Controllers/RoleController.php

@@ -160,9 +160,18 @@ class RoleController extends Controller
     public function form()
     {
         return Admin::form(new Role('permissions'), function (Form $form) {
+            $roleTable  = config('admin.database.roles_table');
+            $connection = config('admin.database.connection');
+
+            $id = $form->getKey();
+
             $form->display('id', 'ID');
 
-            $form->text('slug', trans('admin.slug'))->required();
+            $form->text('slug', trans('admin.slug'))
+                ->required()
+                ->creationRules(['required', "unique:{$connection}.{$roleTable}"])
+                ->updateRules(['required', "unique:{$connection}.{$roleTable},slug,$id"]);
+
             $form->text('name', trans('admin.name'))->required();
 
             $form->tree('permissions')

+ 6 - 5
src/Controllers/UserController.php

@@ -243,22 +243,23 @@ class UserController extends Controller
 
             if ($id) {
                 $form->password('password', trans('admin.password'))
-                    ->rules('confirmed')
+                    ->minLength(5)
+                    ->maxLength(20)
                     ->customFormat(function ($v) {
                         if ($v == $this->password) {
                             return;
                         }
                         return $v;
                     });
-                $form->password('password_confirmation', trans('admin.password_confirmation'));
             } else {
                 $form->password('password', trans('admin.password'))
                     ->required()
-                    ->rules('confirmed');
-
-                $form->password('password_confirmation', trans('admin.password_confirmation'));
+                    ->minLength(5)
+                    ->maxLength(20);
             }
 
+            $form->password('password_confirmation', trans('admin.password_confirmation'))->same('password');
+
             $form->ignore(['password_confirmation']);
 
             $form->multipleSelect('roles', trans('admin.roles'))

+ 54 - 34
src/Form.php

@@ -13,6 +13,7 @@ use Dcat\Admin\Traits\HasBuilderEvents;
 use Dcat\Admin\Widgets\ModalForm;
 use Illuminate\Contracts\Support\MessageProvider;
 use Illuminate\Contracts\Support\Renderable;
+use Illuminate\Http\Request;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Fluent;
 use Illuminate\Support\MessageBag;
@@ -74,6 +75,7 @@ use Dcat\Admin\Form\Concerns;
  * @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\BootstrapFile          bootstrapFile($column, $label = '')
  * @method Field\BootstrapImage         bootstrapImage($column, $label = '')
@@ -150,6 +152,7 @@ class Form implements Renderable
         'list'           => Field\ListField::class,
         'timezone'       => Field\Timezone::class,
         'keyValue'       => Field\KeyValue::class,
+        'tel'            => Field\Tel::class,
 
         'bootstrapFile'          => Field\BootstrapFile::class,
         'bootstrapImage'         => Field\BootstrapImage::class,
@@ -181,6 +184,11 @@ class Form implements Renderable
      */
     protected $callback;
 
+    /**
+     * @var Request
+     */
+    protected $request;
+
     /**
      * @var bool
      */
@@ -264,14 +272,12 @@ class Form implements Renderable
      * @param Repository $model
      * @param \Closure $callback
      */
-    public function __construct(Repository $repository, ?Closure $callback = null)
+    public function __construct(Repository $repository, ?Closure $callback = null, Request $request = null)
     {
-        $this->repository = Admin::createRepository($repository);
-
+        $this->repository = Admin::repository($repository);
         $this->callback = $callback;
-
+        $this->request = $request ?: request();
         $this->builder = new Builder($this);
-
         $this->isSoftDeletes = $this->repository->isSoftDeletes();
 
         $this->setModel(new Fluent());
@@ -312,6 +318,17 @@ class Form implements Renderable
         return $this;
     }
 
+    /**
+     * Get specify field.
+     *
+     * @param string $name
+     * @return Field
+     */
+    public function field($name)
+    {
+        return $this->builder->field($name);
+    }
+
     /**
      * @param $column
      * @return $this
@@ -547,11 +564,11 @@ class Form implements Renderable
      *
      * @param array|null $data
      * @param string|string $redirectTo
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\Http\JsonResponse
+     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\Http\JsonResponse|Response
      */
     public function store(?array $data = null, $redirectTo = null)
     {
-        $data = $data ?: request()->all();
+        $data = $data ?: $this->request->all();
 
         $this->build();
 
@@ -572,11 +589,7 @@ class Form implements Renderable
 
         // Handle validation errors.
         if ($validationMessages = $this->validationMessages($data)) {
-            if (!$this->isAjaxRequest()) {
-                return back()->withInput()->withErrors($validationMessages);
-            } else {
-                return response()->json(['errors' => $validationMessages->getMessages()], 422);
-            }
+            return $this->makeValidationErrorsResponse($validationMessages);
         }
 
         if (($response = $this->prepare($data))) {
@@ -629,9 +642,7 @@ class Form implements Renderable
      */
     public function isAjaxRequest()
     {
-        $request = request();
-
-        return $request->ajax() && !$request->pjax();
+        return $this->request->ajax() && ! $this->request->pjax();
     }
 
     /**
@@ -695,7 +706,7 @@ class Form implements Renderable
      * @param int   $id
      * @param array $input
      *
-     * @return bool
+     * @return bool||Response
      */
     protected function handleOrderable(array $input = [])
     {
@@ -713,7 +724,7 @@ class Form implements Renderable
      * @param $id
      * @param array|null $data
      * @param string|null $redirectTo
-     * @return $this|bool|\Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse|mixed|null
+     * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse||Response
      */
     public function update(
         $id,
@@ -721,7 +732,7 @@ class Form implements Renderable
         $redirectTo = null
     )
     {
-        $data = $data ?: request()->all();
+        $data = $data ?: $this->request->all();
 
         $this->builder->setResourceId($id);
         $this->builder->setMode(Builder::MODE_EDIT);
@@ -751,13 +762,9 @@ class Form implements Renderable
 
         // Handle validation errors.
         if ($validationMessages = $this->validationMessages($data)) {
-            if (!$isEditable && !$this->isAjaxRequest()) {
-                return back()->withInput()->withErrors($validationMessages);
-            } else {
-                return response()->json([
-                    'errors' => $isEditable ? Arr::dot($validationMessages->getMessages()) : $validationMessages->getMessages()
-                ], 422);
-            }
+            return $this->makeValidationErrorsResponse(
+                $isEditable ? Arr::dot($validationMessages->toArray()) : $validationMessages
+            );
         }
 
         if (($response = $this->prepare($data))) {
@@ -778,6 +785,21 @@ class Form implements Renderable
         );
     }
 
+    /**
+     * @param array|MessageBag $validationMessages
+     * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse
+     */
+    protected function makeValidationErrorsResponse($validationMessages)
+    {
+        if (! $this->isAjaxRequest()) {
+            return back()->withInput()->withErrors($validationMessages);
+        }
+
+        return response()->json([
+            'errors' => is_array($validationMessages) ? $validationMessages : $validationMessages->getMessages()
+        ], 422);
+    }
+
     /**
      * Get redirect response.
      *
@@ -796,14 +818,12 @@ class Form implements Renderable
 
         $status = (bool) ($options['status'] ?? true);
 
-        // 判断是否是ajax请求
         if ($this->isAjaxRequest()) {
             $message = $message ?: trans('admin.save_succeeded');
 
             return $this->ajaxResponse($message, $url, $status);
         }
 
-        // 非ajax请求
         $status = (int) ($options['status_code'] ?? 302);
 
         if ($message) {
@@ -826,23 +846,23 @@ class Form implements Renderable
         $resourcesPath = $this->builder->isCreating() ?
             $this->getResource(0) : $this->getResource(-1);
 
-        if (request('after-save') == 1) {
+        if ($this->request->get('after-save') == 1) {
             // continue editing
             if ($this->builder->isEditing() && $this->isAjaxRequest()) {
                 return false;
             }
             return rtrim($resourcesPath, '/')."/{$key}/edit";
         }
-        if (request('after-save') == 2) {
+        if ($this->request->get('after-save') == 2) {
             // continue creating
             return rtrim($resourcesPath, '/').'/create';
         }
-        if (request('after-save') == 3) {
+        if ($this->request->get('after-save') == 3) {
             // view resource
             return rtrim($resourcesPath, '/')."/{$key}";
         }
 
-        return request(Builder::PREVIOUS_URL_KEY) ?: $resourcesPath;
+        return $this->request->get(Builder::PREVIOUS_URL_KEY) ?: $resourcesPath;
     }
 
     /**
@@ -904,7 +924,7 @@ class Form implements Renderable
 
             $value = $this->getDataByColumn($updates, $columns);
 
-            $value = $field->prepareInputValue($value);
+            $value = $field->prepare($value);
 
             if (is_array($columns)) {
                 foreach ($columns as $name => $column) {
@@ -953,7 +973,7 @@ class Form implements Renderable
                 continue;
             }
 
-            $inserts[$column] = $field->prepareInputValue($value);
+            $inserts[$column] = $field->prepare($value);
         }
 
         $prepared = [];
@@ -1134,7 +1154,7 @@ class Form implements Renderable
      *
      * @param bool|\Closure $condition
      *
-     * @return Condition|$this
+     * @return Condition
      */
     public function if($condition)
     {

+ 10 - 1
src/Form/Builder.php

@@ -642,6 +642,7 @@ class Builder
         $attributes['action'] = $this->getAction();
         $attributes['method'] = Arr::get($options, 'method', 'post');
         $attributes['accept-charset'] = 'UTF-8';
+        $attributes['data-toggle'] = 'validator';
 
         $attributes['class'] = Arr::get($options, 'class');
 
@@ -776,10 +777,18 @@ var f = $('#{$this->getFormId()}');
 
 f.find('[type="submit"]').click(function () {
     var t = $(this);
-    t.button('loading');
     
     LA.Form({
         \$form: f,
+        before: function () {
+            f.validator('validate');
+    
+            if (f.find('.has-error').length > 0) {
+                return false;
+            }
+            
+            t.button('loading');
+        },
         after: function () {
             t.button('reset');
         }

+ 24 - 9
src/Form/Concerns/HasFieldValidator.php

@@ -29,9 +29,9 @@ trait HasFieldValidator
     /**
      * Validation rules.
      *
-     * @var string|\Closure
+     * @var array|\Closure
      */
-    protected $rules = '';
+    protected $rules = [];
 
     /**
      * @var \Closure
@@ -93,14 +93,12 @@ trait HasFieldValidator
             $this->rules = $rules;
         }
 
-        if (is_array($rules)) {
-            $thisRuleArr = array_filter(explode('|', $this->rules));
+        $originalRules = is_array($this->rules) ? $this->rules : [];
 
-            $this->rules = array_merge($thisRuleArr, $rules);
+        if (is_array($rules)) {
+            $this->rules = array_merge($originalRules, $rules);
         } elseif (is_string($rules)) {
-            $rules = array_filter(explode('|', "{$this->rules}|$rules"));
-
-            $this->rules = implode('|', $rules);
+            $this->rules = array_merge($originalRules, array_filter(explode('|', $rules)));
         }
 
         $this->setValidationMessages('default', $messages);
@@ -135,7 +133,7 @@ trait HasFieldValidator
             return $rules;
         }
 
-        if (!$id = $this->form->getKey()) {
+        if (method_exists($this->form, 'getKey') || ! $id = $this->form->getKey()) {
             return $rules;
         }
 
@@ -412,4 +410,21 @@ trait HasFieldValidator
     }
 
 
+    /**
+     * Set error messages for individual form field.
+     *
+     * @see http://1000hz.github.io/bootstrap-validator/
+     *
+     * @param string $error
+     * @param string $key
+     * @return $this
+     */
+    public function setClientValidationError(string $error, string $key = null)
+    {
+        $key = $key ? "{$key}-" : '';
+
+        return $this->attribute("data-{$key}error", $error);
+    }
+
+
 }

+ 3 - 0
src/Form/Condition.php

@@ -4,6 +4,9 @@ namespace Dcat\Admin\Form;
 
 use Dcat\Admin\Form;
 
+/**
+ * @mixin Form
+ */
 class Condition
 {
     /**

+ 2 - 2
src/Form/EmbeddedForm.php

@@ -178,8 +178,8 @@ class EmbeddedForm
             return in_array($key, (array) $field->column());
         });
 
-        if (method_exists($field, 'prepareInputValue')) {
-            return $field->prepareInputValue($record);
+        if (method_exists($field, 'prepare')) {
+            return $field->prepare($record);
         }
 
         return $record;

+ 38 - 24
src/Form/Field.php

@@ -4,11 +4,13 @@ namespace Dcat\Admin\Form;
 
 use Dcat\Admin\Admin;
 use Dcat\Admin\Form;
+use Dcat\Admin\Widgets\Form as WidgetForm;
 use Dcat\Admin\Form\Concerns;
 use Illuminate\Contracts\Support\Arrayable;
 use Illuminate\Contracts\Support\Renderable;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Fluent;
+use Illuminate\Support\Str;
 use Illuminate\Support\Traits\Macroable;
 
 /**
@@ -16,8 +18,7 @@ use Illuminate\Support\Traits\Macroable;
  */
 class Field implements Renderable
 {
-    use Macroable,
-        Concerns\HasFieldValidator;
+    use Macroable, Concerns\HasFieldValidator;
 
     const FILE_DELETE_FLAG = '_file_del_';
 
@@ -136,7 +137,7 @@ class Field implements Renderable
     /**
      * Parent form.
      *
-     * @var Form
+     * @var Form|WidgetForm
      */
     protected $form = null;
 
@@ -205,7 +206,7 @@ class Field implements Renderable
     /**
      * @var \Closure
      */
-    protected $prepare;
+    protected $prepareCallback;
 
     /**
      * Field constructor.
@@ -223,7 +224,7 @@ class Field implements Renderable
     /**
      * Get the field element id.
      *
-     * @return string
+     * @return string|array
      */
     public function getElementId()
     {
@@ -239,11 +240,19 @@ class Field implements Renderable
      */
     protected function formatId($column)
     {
+        $random = Str::random(5);
+
         if (is_array($column)) {
-            return str_replace('.', '-', $column);
+            $id = [];
+
+            foreach (str_replace('.', '-', $column) as $k => $v) {
+                $id[$k] = "{$v}-{$random}";
+            }
+
+            return $id;
         }
 
-        return 'form-field-'.str_replace('.', '-', $column);
+        return 'form-field-'.str_replace('.', '-', $column).'-'.$random;
     }
 
     /**
@@ -396,11 +405,11 @@ class Field implements Renderable
     }
 
     /**
-     * @param Form $form
+     * @param Form|WidgetForm $form
      *
      * @return $this
      */
-    public function setForm(Form $form = null)
+    public function setForm($form = null)
     {
         $this->form = $form;
 
@@ -658,12 +667,17 @@ class Field implements Renderable
     /**
      * Specifies a regular expression against which to validate the value of the input.
      *
+     * @param string $error
      * @param string $regexp
      *
      * @return $this
      */
-    public function pattern($regexp)
+    public function pattern($regexp, $error = null)
     {
+        if ($error) {
+            $this->attribute('data-pattern-error', $error);
+        }
+
         return $this->attribute('pattern', $regexp);
     }
 
@@ -745,7 +759,7 @@ class Field implements Renderable
      * @param mixed $value
      * @return mixed
      */
-    public function prepare($value)
+    protected function prepareToSave($value)
     {
         return $value;
     }
@@ -756,7 +770,7 @@ class Field implements Renderable
      */
     public function saving(\Closure $closure)
     {
-        $this->prepare = $closure;
+        $this->prepareCallback = $closure;
 
         return $this;
     }
@@ -767,11 +781,11 @@ class Field implements Renderable
      * @param mixed $value
      * @return mixed
      */
-    final public function prepareInputValue($value)
+    final public function prepare($value)
     {
-        $value = $this->prepare($value);
+        $value = $this->prepareToSave($value);
 
-        if ($handler = $this->prepare) {
+        if ($handler = $this->prepareCallback) {
             $handler->bindTo($this->data);
 
             return $handler($value);
@@ -933,11 +947,9 @@ class Field implements Renderable
      */
     public function addElementClass($class)
     {
-        if (is_array($class) || is_string($class)) {
-            $this->elementClass = array_merge($this->elementClass, (array) $class);
-
-            $this->elementClass = array_unique($this->elementClass);
-        }
+        $this->elementClass = array_unique(
+            array_merge($this->elementClass, (array) $class)
+        );
 
         return $this;
     }
@@ -989,13 +1001,15 @@ class Field implements Renderable
     }
 
     /**
-     * @param array $labelClass
-     *
+     * @param array|string $labelClass
+     * @param bool $append
      * @return $this
      */
-    public function setLabelClass(array $labelClass)
+    public function setLabelClass($labelClass, bool $append = true)
     {
-        $this->labelClass = $labelClass;
+        $this->labelClass = $append
+            ? array_unique(array_merge($this->labelClass, (array) $labelClass))
+            : (array) $labelClass;
 
         return $this;
     }

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

@@ -100,7 +100,7 @@ class BootstrapFile extends Field
      *
      * @return mixed|string
      */
-    public function prepare($file)
+    protected function prepareToSave($file)
     {
         if (request()->has(static::FILE_DELETE_FLAG)) {
             return $this->destroy();

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

@@ -60,14 +60,14 @@ class BootstrapImage extends BootstrapFile
      *
      * @var string
      */
-    protected $rules = 'image';
+    protected $rules = ['image'];
 
     /**
      * @param array|UploadedFile $image
      *
      * @return string
      */
-    public function prepare($image)
+    protected function prepareToSave($image)
     {
         if (request()->has(static::FILE_DELETE_FLAG)) {
             return $this->destroy();

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

@@ -109,7 +109,7 @@ class BootstrapMultipleFile extends Field
      *
      * @return mixed|string
      */
-    public function prepare($files)
+    protected function prepareToSave($files)
     {
         if (request()->has(static::FILE_DELETE_FLAG)) {
             return $this->destroy(request(static::FILE_DELETE_FLAG));

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

@@ -18,7 +18,7 @@ class BootstrapMultipleImage extends BootstrapMultipleFile
      *
      * @var string
      */
-    protected $rules = 'image';
+    protected $rules = ['image'];
 
     /**
      * Prepare for each file.

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

@@ -6,7 +6,7 @@ use Dcat\Admin\Form;
 
 class Captcha extends Text
 {
-    protected $rules = 'required|captcha';
+    protected $rules = ['required', 'captcha'];
 
     protected $view = 'admin::form.captcha';
 

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

@@ -52,7 +52,7 @@ class Currency extends Text
     /**
      * {@inheritdoc}
      */
-    public function prepare($value)
+    protected function prepareToSave($value)
     {
         return (float) $value;
     }

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

@@ -15,7 +15,7 @@ class Date extends Text
         return $this;
     }
 
-    public function prepare($value)
+    protected function prepareToSave($value)
     {
         if ($value === '') {
             $value = null;

+ 1 - 2
src/Form/Field/DateRange.php

@@ -4,7 +4,6 @@ namespace Dcat\Admin\Form\Field;
 
 use Dcat\Admin\Admin;
 use Dcat\Admin\Form\Field;
-use Psy\Util\Str;
 
 class DateRange extends Field
 {
@@ -29,7 +28,7 @@ class DateRange extends Field
         $this->options(['format' => $this->format]);
     }
 
-    public function prepare($value)
+    protected function prepareToSave($value)
     {
         if ($value === '') {
             $value = null;

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

@@ -4,11 +4,11 @@ namespace Dcat\Admin\Form\Field;
 
 class Email extends Text
 {
-    protected $rules = 'nullable|email';
+    protected $rules = ['nullable', 'email'];
 
     public function render()
     {
-        $this->prepend('<i class="fa fa-envelope fa-fw"></i>')
+        $this->prepend('<i class="ti-email"></i>')
             ->defaultAttribute('type', 'email');
 
         return parent::render();

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

@@ -42,7 +42,7 @@ class Embeds extends Field
      *
      * @return array
      */
-    public function prepare($input)
+    protected function prepareToSave($input)
     {
         $form = $this->buildEmbeddedForm();
 

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

@@ -101,7 +101,7 @@ class File extends Field
      *
      * @return mixed|string
      */
-    public function prepare($file)
+    protected function prepareToSave($file)
     {
         if (request()->has(static::FILE_DELETE_FLAG)) {
             return $this->destroy();

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

@@ -306,7 +306,7 @@ class HasMany extends Field
      *
      * @return array
      */
-    public function prepare($input)
+    protected function prepareToSave($input)
     {
         $form = $this->buildNestedForm($this->column, $this->builder);
 

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

@@ -51,7 +51,7 @@ class Image extends File
 {
     use ImageField;
 
-    protected $rules = 'image';
+    protected $rules = ['image'];
 
     protected $view = 'admin::form.file';
 

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

@@ -6,7 +6,7 @@ use Dcat\Admin\Admin;
 
 class Ip extends Text
 {
-    protected $rules = 'nullable|ip';
+    protected $rules = ['nullable', 'ip'];
 
     /**
      * @see https://github.com/RobinHerbots/Inputmask#options

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

@@ -94,7 +94,7 @@ class KeyValue extends Field
 JS;
     }
 
-    public function prepare($value)
+    protected function prepareToSave($value)
     {
         unset($value[static::DEFAULT_FLAG_NAME]);
 

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

@@ -145,7 +145,7 @@ JS;
     /**
      * {@inheritdoc}
      */
-    public function prepare($value)
+    protected function prepareToSave($value)
     {
         unset($value['values'][static::DEFAULT_FLAG_NAME]);
 

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

@@ -19,7 +19,7 @@ class Mobile extends Text
     {
         $this->inputmask($this->options);
 
-        $this->prepend('<i class="fa fa-phone fa-fw"></i>')
+        $this->prepend('<i class="ti-mobile"></i>')
             ->defaultAttribute('style', 'width: 200px');
 
         return parent::render();

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

@@ -34,7 +34,7 @@ class MultipleFile extends File
      * @param string|array $file
      * @return array
      */
-    public function prepare($file)
+    protected function prepareToSave($file)
     {
         if ($path = request(static::FILE_DELETE_FLAG)) {
             $this->deleteFile($path);

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

@@ -33,7 +33,7 @@ class MultipleImage extends Image
      * @param string|array $file
      * @return array
      */
-    public function prepare($file)
+    protected function prepareToSave($file)
     {
         if ($path = request(static::FILE_DELETE_FLAG)) {
             $this->deleteFile($path);

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

@@ -12,7 +12,7 @@ class MultipleSelect extends Select
         return Helper::array(Arr::get($data, $this->column));
     }
 
-    public function prepare($value)
+    protected function prepareToSave($value)
     {
         return Helper::array($value, true);
     }

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

@@ -29,7 +29,7 @@ JS;
         return parent::render();
     }
 
-    public function prepare($value)
+    protected function prepareToSave($value)
     {
         return (int)$value;
     }

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

@@ -16,7 +16,7 @@ class SelectResource extends Field
         'vendor/dcat-admin/dcat-admin/select-resource.min.js'
     ];
 
-    protected $area = ['60%', '68%'];
+    protected $area = ['55%', '68%'];
 
     protected $source;
 
@@ -146,7 +146,7 @@ class SelectResource extends Field
         }
     }
 
-    public function prepare($value)
+    protected function prepareToSave($value)
     {
         if ($this->maxItem == 1) {
             if ($value === null || $value === '') {

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

@@ -84,7 +84,7 @@ class SwitchField extends Field
      * @param mixed $value
      * @return int
      */
-    public function prepare($value)
+    protected function prepareToSave($value)
     {
         return $value ? 1 : 0;
     }

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

@@ -58,7 +58,7 @@ class Table extends HasMany
         return $forms;
     }
 
-    public function prepare($input)
+    protected function prepareToSave($input)
     {
         $form = $this->buildNestedForm($this->column, $this->builder);
         $prepare = $form->prepare($input);

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

@@ -100,7 +100,7 @@ class Tags extends Field
     /**
      * {@inheritdoc}
      */
-    public function prepare($value)
+    protected function prepareToSave($value)
     {
         $value = array_filter($value, 'strlen');
 

+ 14 - 0
src/Form/Field/Tel.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace Dcat\Admin\Form\Field;
+
+class Tel extends Text
+{
+    public function render()
+    {
+        $this->prepend('<i class="fa fa-phone fa-fw"></i>')
+            ->defaultAttribute('type', 'tel');
+
+        return parent::render();
+    }
+}

+ 91 - 1
src/Form/Field/Text.php

@@ -2,6 +2,7 @@
 
 namespace Dcat\Admin\Form\Field;
 
+use Dcat\Admin\Admin;
 use Dcat\Admin\Form\Field;
 
 class Text extends Field
@@ -33,6 +34,91 @@ class Text extends Field
         return parent::render();
     }
 
+    /**
+     * Set input type.
+     *
+     * @param string $type
+     * @return $this
+     */
+    public function type(string $type)
+    {
+        return $this->attribute('type', $type);
+    }
+
+    /**
+     * Set "data-match" attribute.
+     *
+     * @see http://1000hz.github.io/bootstrap-validator/
+     *
+     * @param string|Field $field
+     * @param string $error
+     * @return $this
+     */
+    public function same($field, ?string $error = null)
+    {
+        $field = $field instanceof Field ? $field : $this->form->field($field);
+        $name  = $field->column();
+
+        if ($name.'_confirmation' === $this->column) {
+            $field->rules('confirmed');
+        } else {
+            $this->rules('nullable|same:'.$name);
+        }
+
+        $attributes = [
+            'data-match'       => '#'.$field->getElementId(),
+            'data-match-error' => str_replace([':attribute', ':other'], [$this->column, $name], $error ?: trans('admin.validation.match'))
+        ];
+
+        return $this->attribute($attributes);
+    }
+
+    /**
+     * @param int $length
+     * @param string|null $error
+     * @return $this
+     */
+    public function minLength(int $length, ?string $error = null)
+    {
+        $this->rules('nullable|min:'.$length);
+
+        return $this->attribute([
+            'data-minlength'       => $length,
+            'data-minlength-error' => str_replace(
+                [':attribute', ':min'],
+                [$this->column, $length],
+                $error ?: trans('admin.validation.minlength')
+            ),
+        ]);
+    }
+
+    /**
+     * @param int $length
+     * @param string|null $error
+     * @return $this
+     */
+    public function maxLength(int $length, ?string $error = null)
+    {
+        Admin::script(
+            <<<'JS'
+LA.extendValidator('maxlength', function ($el) {
+    return $el.val().length > $el.attr('data-maxlength');
+});
+JS
+        );
+
+        $this->rules('max:'.$length);
+
+        return $this->attribute([
+            'data-maxlength'       => $length,
+            'data-maxlength-error' => str_replace(
+                [':attribute', ':max'],
+                [$this->column, $length],
+                $error ?: trans('admin.validation.maxlength')
+            ),
+        ]);
+    }
+
     /**
      * Add inputmask to an elements.
      *
@@ -108,10 +194,14 @@ class Text extends Field
 
         $datalist = "<datalist id=\"list-{$this->id}\">";
         foreach ($entries as $k => $v) {
-            $datalist .= "<option value=\"{$k}\">{$v}</option>";
+            $value = is_string($k) ? "value=\"{$k}\"" : '';
+
+            $datalist .= "<option {$value}>{$v}</option>";
         }
         $datalist .= '</datalist>';
 
+        Admin::script("$('#list-{$this->id}').parent().hide()");
+
         return $this->append($datalist);
     }
 }

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

@@ -213,7 +213,7 @@ class Tree extends Field
      * @param string|array $value
      * @return array
      */
-    public function prepare($value)
+    protected function prepareToSave($value)
     {
         return Helper::array($value, true);
     }

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

@@ -4,7 +4,7 @@ namespace Dcat\Admin\Form\Field;
 
 class Url extends Text
 {
-    protected $rules = 'nullable|url';
+    protected $rules = ['nullable', 'url'];
 
     public function render()
     {

+ 2 - 2
src/Form/NestedForm.php

@@ -243,8 +243,8 @@ class NestedForm
                 continue;
             }
 
-            if (method_exists($field, 'prepareInputValue')) {
-                $value = $field->prepareInputValue($value);
+            if (method_exists($field, 'prepare')) {
+                $value = $field->prepare($value);
             }
 
             if (($field instanceof \Dcat\Admin\Form\Field\Hidden) || $value != $field->original()) {

+ 10 - 16
src/Grid.php

@@ -160,22 +160,16 @@ class Grid
         'show_quick_create_btn'  => false,
         'show_bordered'          => false,
         'show_toolbar'           => true,
+        'show_exporter'          => false,
 
-        'row_selector_style'      => 'primary',
-        'row_selector_circle'     => true,
-        'row_selector_clicktr'    => false,
+        'row_selector_style'     => 'primary',
+        'row_selector_circle'    => true,
+        'row_selector_clicktr'   => false,
         'row_selector_label_key' => null,
-        'row_selector_bg'         => 'var(--20)',
-
-        'show_exporter'             => false,
-        'show_export_all'           => true,
-        'show_export_current_page'  => true,
-        'show_export_selected_rows' => true,
-        'export_limit'              => 50000,
+        'row_selector_bg'        => 'var(--20)',
 
         'dialog_form_area'   => ['700px', '670px'],
         'table_header_style' => 'table-header-gray',
-
     ];
 
     /**
@@ -190,7 +184,7 @@ class Grid
         if ($repository) {
             $this->keyName = $repository->getKeyName();
         }
-        $this->model    = new Model($repository);
+        $this->model    = new Model(request(), $repository);
         $this->columns  = new Collection();
         $this->rows     = new Collection();
         $this->builder  = $builder;
@@ -241,7 +235,7 @@ class Grid
      * @param string $name
      * @param string $label
      *
-     * @return Column|Collection
+     * @return Column
      */
     public function column($name, $label = '')
     {
@@ -456,7 +450,7 @@ HTML
 
         if ($this->rowsCallback) {
             foreach ($this->rowsCallback as $value) {
-                $this->rows->map($value);
+                $value($this->rows);
             }
         }
     }
@@ -895,7 +889,7 @@ HTML;
      */
     public function render()
     {
-        $this->handleExportRequest(true);
+        $this->handleExportRequest();
 
         try {
             $this->callComposing();
@@ -926,7 +920,7 @@ HTML;
      * Add column to grid.
      *
      * @param string $name
-     * @return Column|Collection
+     * @return Column
      */
     public function __get($name)
     {

+ 3 - 1
src/Grid/Column.php

@@ -48,6 +48,8 @@ use Illuminate\Support\Str;
  * @method $this studly()
  * @method $this substr($start, $length = null)
  * @method $this ucfirst()
+ *
+ * @mixin Collection
  */
 class Column
 {
@@ -269,7 +271,7 @@ class Column
      *         })
      *
      * @param \Closure $condition
-     * @return Column\Condition|$this
+     * @return Column\Condition
      */
     public function if(\Closure $condition)
     {

+ 3 - 0
src/Grid/Column/Condition.php

@@ -4,6 +4,9 @@ namespace Dcat\Admin\Grid\Column;
 
 use Dcat\Admin\Grid\Column;
 
+/**
+ * @mixin Column
+ */
 class Condition
 {
     /**

+ 32 - 49
src/Grid/Concerns/HasExporter.php

@@ -24,15 +24,24 @@ trait HasExporter
     /**
      * Set exporter driver for Grid to export.
      *
-     * @param string|Grid\Exporters\AbstractExporter $exporter
+     * @param string|Grid\Exporters\AbstractExporter|array $exporter
      *
-     * @return $this
+     * @return Grid\Exporters\AbstractExporter
      */
-    public function exporter($exporter)
+    public function exporter($exporter = null)
     {
-        $this->exportDriver = $exporter;
+        $titles = [];
+
+        if (is_array($exporter) || $exporter === false) {
+            $titles   = $exporter;
+            $exporter = null;
+        }
+
+        $this->showExporter();
+
+        $driver = $this->exportDriver ?: ($this->exportDriver = $this->getExporter()->resolve($exporter));
 
-        return $this;
+        return $driver->titles($titles);
     }
 
     /**
@@ -44,27 +53,22 @@ trait HasExporter
      */
     protected function handleExportRequest($forceExport = false)
     {
-        if (
-            ! $this->allowExportBtn()
-            || ! $scope = request($this->getExporter()->getQueryName())
-        ) {
+        if (! $scope = request($this->getExporter()->getQueryName())) {
             return;
         }
 
-        // clear output buffer.
-        if (ob_get_length()) {
-            ob_end_clean();
-        }
-
-        $this->model()->usePaginate(false);
-
         if ($this->builder) {
             call_user_func($this->builder, $this);
 
-            return $this->resolveExportDriver($scope)->export();
+            $this->builder = null;
+        }
+
+        // clear output buffer.
+        if (ob_get_length()) {
+            ob_end_clean();
         }
 
-        if ($forceExport) {
+        if ($forceExport || $this->allowExporter()) {
             return $this->resolveExportDriver($scope)->export();
         }
     }
@@ -72,7 +76,7 @@ trait HasExporter
     /**
      * @return Exporter
      */
-    protected function getExporter()
+    public function getExporter()
     {
         return $this->exporter ?: ($this->exporter = new Exporter($this));
     }
@@ -80,9 +84,9 @@ trait HasExporter
     /**
      * @param string $gridName
      */
-    protected function setExporterQueryName($gridName)
+    public function setExporterQueryName($gridName)
     {
-        if (! $this->allowExportBtn()) {
+        if (! $this->allowExporter()) {
             return;
         }
 
@@ -96,7 +100,11 @@ trait HasExporter
      */
     protected function resolveExportDriver($scope)
     {
-        return $this->getExporter()->resolve($this->exportDriver)->withScope($scope);
+        if (! $this->exportDriver) {
+            $this->exportDriver = $this->getExporter()->resolve();
+        }
+
+        return $this->exportDriver->withScope($scope);
     }
 
 
@@ -119,31 +127,6 @@ trait HasExporter
         return $this->getResource().'?'.http_build_query($input);
     }
 
-    /**
-     * @param array $options
-     * @return $this
-     */
-    public function setExportOptions(array $options)
-    {
-        if (isset($options['limit'])) {
-            $this->options['export_limit'] = $options['limit'];
-        }
-
-        if (isset($options['all'])) {
-            $this->options['show_export_all'] = $options['show_all'];
-        }
-
-        if (isset($options['current_page'])) {
-            $this->options['show_export_current_page'] = $options['current_page'];
-        }
-
-        if (isset($options['selected_rows'])) {
-            $this->options['show_export_selected_rows'] = $options['selected_rows'];
-        }
-
-        return $this;
-    }
-
     /**
      * Render export button.
      *
@@ -151,7 +134,7 @@ trait HasExporter
      */
     public function renderExportButton()
     {
-        if (! $this->allowExportBtn()) {
+        if (! $this->allowExporter()) {
             return '';
         }
         
@@ -183,7 +166,7 @@ trait HasExporter
      *
      * @return bool
      */
-    public function allowExportBtn()
+    public function allowExporter()
     {
         return $this->options['show_exporter'];
     }

+ 8 - 0
src/Grid/Concerns/HasSelector.php

@@ -66,6 +66,14 @@ trait HasSelector
         return $this;
     }
 
+    /**
+     * @return Selector
+     */
+    public function getSelector()
+    {
+        return $this->_selector;
+    }
+
     /**
      * Render grid selector.
      *

+ 1 - 1
src/Grid/Concerns/HasTools.php

@@ -174,7 +174,7 @@ trait HasTools
             $this->option('show_toolbar')
             && (
                 $this->getTools()->has() ||
-                $this->allowExportBtn() ||
+                $this->allowExporter() ||
                 $this->allowCreateBtn() ||
                 $this->allowQuickCreateBtn() ||
                 $this->allowResponsive() ||

+ 23 - 10
src/Grid/Displayers/DropdownActions.php

@@ -35,17 +35,30 @@ class DropdownActions extends Actions
      */
     protected function addScript()
     {
-        $script = <<<'SCRIPT'
-(function ($) {
-    $('.table-responsive').on('show.bs.dropdown', function () {
-         $('.table-responsive').css("overflow", "inherit" );
+        $script = <<<'JS'
+$(function() {
+  $('.table-responsive').on('shown.bs.dropdown', function(e) {
+    var t = $(this),
+      m = $(e.target).find('.dropdown-menu'),
+      tb = t.offset().top + t.height(),
+      mb = m.offset().top + m.outerHeight(true),
+      d = 20; // Space for shadow + scrollbar.   
+      
+    if (t[0].scrollWidth > t.innerWidth()) {
+      if (mb + d > tb) {
+        t.css('padding-bottom', ((mb + d) - tb));
+      }
+    } else {
+      t.css('overflow', 'visible');
+    }
+  }).on('hidden.bs.dropdown', function() {
+    $(this).css({
+      'padding-bottom': '',
+      'overflow': ''
     });
-    
-    $('.table-responsive').on('hide.bs.dropdown', function () {
-         $('.table-responsive').css("overflow", "auto");
-    })
-})(jQuery);
-SCRIPT;
+  });
+});
+JS;
 
         Admin::script($script);
     }

+ 93 - 13
src/Grid/Exporter.php

@@ -3,7 +3,7 @@
 namespace Dcat\Admin\Grid;
 
 use Dcat\Admin\Grid;
-use Dcat\Admin\Grid\Exporters\CsvExporter;
+use Dcat\Admin\Grid\Exporters\ExporterInterface;
 
 class Exporter
 {
@@ -33,6 +33,16 @@ class Exporter
      */
     protected $grid;
 
+    /**
+     * @var array
+     */
+    protected $options = [
+        'show_export_all'           => true,
+        'show_export_current_page'  => true,
+        'show_export_selected_rows' => true,
+        'chunk_size'                => 5000,
+    ];
+
     /**
      * Create a new Exporter instance.
      *
@@ -57,6 +67,66 @@ class Exporter
         return $this;
     }
 
+    /**
+     *  Get or set option for exporter.
+     *
+     * @param string $key
+     * @param mixed|null $value
+     * @return $this|mixed|null
+     */
+    public function option($key, $value = null)
+    {
+        if ($value === null) {
+            return $this->options[$key] ?? null;
+        }
+
+        $this->options[$key] = $value;
+
+        return $this;
+    }
+
+    /**
+     * Disable export all.
+     *
+     * @param bool $value
+     * @return $this
+     */
+    public function disableExportAll(bool $value = true)
+    {
+        return $this->option('show_export_all', ! $value);
+    }
+
+    /**
+     * Disable export current page.
+     *
+     * @param bool $value
+     * @return $this
+     */
+    public function disableExportCurrentPage(bool $value = true)
+    {
+        return $this->option('show_export_current_page', ! $value);
+    }
+
+    /**
+     * Disable export selected rows.
+     *
+     * @param bool $value
+     * @return $this
+     */
+    public function disableExportSelectedRow(bool $value = true)
+    {
+        return $this->option('show_export_selected_rows', ! $value);
+    }
+
+    /**
+     * @param int $value
+     * @return $this
+     */
+    public function chunkSize(int $value)
+    {
+        return $this->option('chunk_size', $value);
+    }
+
     /**
      * Get export query name.
      *
@@ -83,14 +153,18 @@ class Exporter
      *
      * @param string $driver
      *
-     * @return CsvExporter
+     * @return Grid\Exporters\AbstractExporter
      */
-    public function resolve($driver)
+    public function resolve($driver = null)
     {
-        if ($driver instanceof Grid\Exporters\AbstractExporter) {
+        if ($driver && $driver instanceof Grid\Exporters\AbstractExporter) {
             return $driver->setGrid($this->grid);
         }
 
+        if ($driver && $driver instanceof ExporterInterface) {
+            return $driver;
+        }
+
         return $this->getExporter($driver);
     }
 
@@ -99,25 +173,31 @@ class Exporter
      *
      * @param string $driver
      *
-     * @return CsvExporter
+     * @return Grid\Exporters\AbstractExporter
      */
-    protected function getExporter($driver)
+    protected function getExporter($driver): ExporterInterface
     {
-        if (!array_key_exists($driver, static::$drivers)) {
+        if (! $driver || ! array_key_exists($driver, static::$drivers)) {
             return $this->getDefaultExporter();
         }
 
-        return (new static::$drivers[$driver]())->setGrid($this->grid);
+        $driver = (new static::$drivers[$driver]());
+
+        if (method_exists($driver, 'setGrid')) {
+            $driver->setGrid($this->grid);
+        }
+
+        return $driver;
     }
 
     /**
      * Get default exporter.
      *
-     * @return CsvExporter
+     * @return Grid\Exporters\ExcelExporter
      */
     public function getDefaultExporter()
     {
-        return (new CsvExporter())->setGrid($this->grid);
+        return Grid\Exporters\ExcelExporter::make()->setGrid($this->grid);
     }
 
     /**
@@ -133,15 +213,15 @@ class Exporter
         $query = '';
 
         if ($scope == static::SCOPE_ALL) {
-            $query = 'all';
+            $query = $scope;
         }
 
         if ($scope == static::SCOPE_CURRENT_PAGE) {
-            $query = "page:$args";
+            $query = "$scope:$args";
         }
 
         if ($scope == static::SCOPE_SELECTED_ROWS) {
-            $query = "selected:$args";
+            $query = "$scope:$args";
         }
 
         return [$this->queryName => $query];

+ 177 - 32
src/Grid/Exporters/AbstractExporter.php

@@ -5,6 +5,11 @@ namespace Dcat\Admin\Grid\Exporters;
 use Dcat\Admin\Grid;
 use Illuminate\Support\Str;
 
+/**
+ * @method $this disableExportAll(bool $value = true)
+ * @method $this disableExportCurrentPage(bool $value = true)
+ * @method $this disableExportSelectedRow(bool $value = true)
+ */
 abstract class AbstractExporter implements ExporterInterface
 {
     /**
@@ -12,6 +17,11 @@ abstract class AbstractExporter implements ExporterInterface
      */
     protected $grid;
 
+    /**
+     * @var Grid\Exporter
+     */
+    protected $parent;
+
     /**
      * @var \Closure
      */
@@ -20,32 +30,125 @@ abstract class AbstractExporter implements ExporterInterface
     /**
      * @var array
      */
-    public $titles;
+    protected $titles = [];
 
     /**
      * @var array
      */
-    public $data;
+    protected $data;
+
+    /**
+     * @var string
+     */
+    protected $filename;
 
     /**
      * @var string
      */
-    public $filename;
+    protected $scope;
+
+    /**
+     * @var string
+     */
+    protected $extension = 'xlsx';
 
     /**
      * Create a new exporter instance.
      *
-     * @param $builder
+     * @param array $titles
      */
-    public function __construct($builder = null)
+    public function __construct($titles = [])
     {
-        if ($builder instanceof \Closure) {
-            $builder->bindTo($this);
+        $this->titles($titles);
+    }
 
-            $this->builder = $builder;
-        } elseif (is_array($builder)) {
-            $this->titles = $builder;
+    /**
+     * Set the headings of excel sheet.
+     *
+     * @param array|false $titles
+     * @return $this
+     */
+    public function titles($titles)
+    {
+        if (is_array($titles) || $titles === false) {
+            $this->titles = $titles;
         }
+
+        return $this;
+    }
+
+    /**
+     * Set filename.
+     *
+     * @param string|\Closure $filename
+     * @return $this
+     */
+    public function filename($filename)
+    {
+        $this->filename = value($filename);
+
+        return $this;
+    }
+
+    /**
+     * Set export data.
+     *
+     * @param array $data
+     * @return $this
+     */
+    public function data($data)
+    {
+        $this->data = $data;
+
+        return $this;
+    }
+
+    /**
+     * Set export data callback function.
+     *
+     * @param \Closure $builder
+     * @return $this
+     */
+    public function rows(\Closure $builder)
+    {
+        $this->builder = $builder;
+
+        return $this;
+    }
+
+    /**
+     * @return $this
+     */
+    public function xlsx()
+    {
+        return $this->extension('xlsx');
+    }
+
+    /**
+     * @return $this
+     */
+    public function csv()
+    {
+        return $this->extension('csv');
+    }
+
+    /**
+     * @return $this
+     */
+    public function ods()
+    {
+        return $this->extension('ods');
+    }
+
+    /**
+     * @param string $ext e.g. csv/xlsx/ods
+     * @return $this
+     */
+    public function extension(string $ext)
+    {
+        $this->extension = $ext;
+
+        return $this;
     }
 
     /**
@@ -57,7 +160,8 @@ abstract class AbstractExporter implements ExporterInterface
      */
     public function setGrid(Grid $grid)
     {
-        $this->grid = $grid;
+        $this->grid   = $grid;
+        $this->parent = $grid->getExporter();
 
         return $this;
     }
@@ -68,17 +172,55 @@ abstract class AbstractExporter implements ExporterInterface
      */
     public function getFilename()
     {
-        return $this->filename ?? (date('Ymd-His') . '-' . Str::random(6));
+        return $this->filename ?: (admin_trans_label().'-'.date('Ymd-His').'-'.Str::random(6));
     }
 
     /**
      * Get data with export query.
      *
-     * @return array
+     * @param int $page
+     * @param int $perPage
+     * @return array|\Illuminate\Support\Collection|mixed
+     */
+    public function buildData(?int $page = null, ?int $perPage = null)
+    {
+        if (! is_null($this->data)) {
+            return $this->data;
+        }
+
+        $model = $this->grid->model();
+
+        // current page
+        if ($this->scope === Grid\Exporter::SCOPE_CURRENT_PAGE) {
+            $page    = $model->getCurrentPage();
+            $perPage = $model->getPerPage();
+        }
+
+        $model->usePaginate(false);
+
+        if ($page && $this->scope !== Grid\Exporter::SCOPE_SELECTED_ROWS) {
+            $perPage = $perPage ?: $this->getChunkSize();
+
+            $model->forPage($page, $perPage);
+        }
+
+        $array = $this->grid->getFilter()->execute(true);
+
+        $model->reset();
+
+        if ($this->builder) {
+            return ($this->builder)($array);
+        }
+
+        return $array;
+    }
+
+    /**
+     * @return int
      */
-    public function getData()
+    protected function getChunkSize()
     {
-        return $this->data ?? $this->grid->getFilter()->execute(true);
+        return $this->parent->option('chunk_size') ?: 5000;
     }
 
     /**
@@ -90,27 +232,17 @@ abstract class AbstractExporter implements ExporterInterface
      */
     public function withScope($scope)
     {
-        $model = $this->grid->model();
+        $data = explode(':', $scope);
 
-        $model->usePaginate(false);
+        $scope = $data[0] ?? '';
+        $args  = $data[1] ?? '';
 
-        if ($scope == Grid\Exporter::SCOPE_ALL) {
-            $model->usePaginate(true);
-            $model->setPerPage($this->grid->option('export_limit'));
-            $model->setCurrentPage(1);
-
-            return $this;
-        }
-
-        list($scope, $args) = explode(':', $scope);
-
-        if ($scope == Grid\Exporter::SCOPE_CURRENT_PAGE) {
-            $model->usePaginate(true);
-        }
+        $this->scope = $scope;
 
         if ($scope == Grid\Exporter::SCOPE_SELECTED_ROWS) {
             $selected = explode(',', $args);
-            $model->whereIn($this->grid->getKeyName(), $selected);
+
+            $this->grid->model()->whereIn($this->grid->getKeyName(), $selected);
         }
 
         return $this;
@@ -121,13 +253,26 @@ abstract class AbstractExporter implements ExporterInterface
      */
     abstract public function export();
 
+    /**
+     * @param $method
+     * @param $arguments
+     * @return mixed
+     */
+    public function __call($method, $arguments)
+    {
+        $this->parent->{$method}(...$arguments);
+
+        return $this;
+    }
+
     /**
      * Create a new exporter instance.
      *
      * @param \Closure|array $closure
      */
-    public static function create($builder = null)
+    public static function make($builder = null)
     {
         return new static($builder);
     }
+
 }

+ 0 - 94
src/Grid/Exporters/CsvExporter.php

@@ -1,94 +0,0 @@
-<?php
-
-namespace Dcat\Admin\Grid\Exporters;
-
-use Illuminate\Contracts\Support\Arrayable;
-use Illuminate\Support\Arr;
-use Illuminate\Support\Str;
-
-class CsvExporter extends AbstractExporter
-{
-    /**
-     * {@inheritdoc}
-     */
-    public function export()
-    {
-        $filename = $this->getFilename().'.csv';
-
-        $headers = [
-            'Content-Encoding'    => 'UTF-8',
-            'Content-Type'        => 'text/csv;charset=UTF-8',
-            'Content-Disposition' => "attachment; filename=\"$filename\"",
-        ];
-
-        response()->stream(function () {
-            $handle = fopen('php://output', 'w');
-
-            $titles = $this->titles;
-
-            if ($titles) {
-                // Add CSV headers
-                fputcsv($handle, $titles);
-            }
-
-            $records = $this->getData();
-
-            if ($this->builder) {
-                $records = $this->builder->call($records);
-            }
-
-            if (empty($titles)) {
-                $titles = $this->getHeaderRowFromRecords($records);
-
-                // Add CSV headers
-                fputcsv($handle, $titles);
-            }
-
-            foreach ($records as $record) {
-                fputcsv($handle, $this->getFormattedRecord($titles, $record));
-            }
-
-            // Close the output stream
-            fclose($handle);
-        }, 200, $headers)->send();
-
-        exit;
-    }
-
-    /**
-     * @param array $records
-     *
-     * @return array
-     */
-    public function getHeaderRowFromRecords(array $records): array
-    {
-        $titles = [];
-
-        collect(Arr::dot($records[0] ?? []))->keys()->map(
-            function ($key) use(&$titles) {
-                if (Str::contains($key, '.')) return;
-
-                $titles[$key] = Str::ucfirst($key);
-            }
-        );
-
-        return $titles;
-    }
-
-    /**
-     * @param $titles
-     * @param $record
-     * @return array
-     */
-    public function getFormattedRecord($titles, $record)
-    {
-        $result = [];
-
-        $record = Arr::dot($record);
-        foreach ($titles as $k => $label) {
-            $result[] = $record[$k] ?? '';
-        }
-
-        return $result;
-    }
-}

+ 41 - 0
src/Grid/Exporters/ExcelExporter.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace Dcat\Admin\Grid\Exporters;
+
+use Dcat\EasyExcel\Excel;
+use Dcat\Admin\Grid;
+
+class ExcelExporter extends AbstractExporter
+{
+    public function __construct($titles = [])
+    {
+        parent::__construct($titles);
+
+        if (! class_exists(Excel::class)) {
+            throw new \Exception('To use exporter, please install [dcat/easy-excel] first.');
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function export()
+    {
+        $filename = $this->getFilename().'.'.$this->extension;
+
+        $exporter = Excel::export();
+
+        if ($this->scope === Grid\Exporter::SCOPE_ALL) {
+            $exporter->chunk(function (int $times) {
+                return $this->buildData($times);
+            });
+        } else {
+            $exporter->data($this->buildData());
+        }
+
+        $exporter->headings($this->titles)->download($filename);
+
+        exit;
+    }
+
+}

+ 10 - 4
src/Grid/Filter/Presenter/Select.php

@@ -10,6 +10,11 @@ use Illuminate\Support\Arr;
 
 class Select extends Presenter
 {
+    /**
+     * @var string
+     */
+    protected $elementClass = null;
+
     /**
      * Options of select.
      *
@@ -273,7 +278,8 @@ JS;
      */
     protected function getElementClass() : string
     {
-        return str_replace('.', '_', $this->filter->getColumn());
+        return $this->elementClass ?:
+            ($this->elementClass = $this->getClass($this->filter->getColumn()));
     }
 
     /**
@@ -288,11 +294,11 @@ JS;
      */
     public function load($target, $resourceUrl, $idField = 'id', $textField = 'text') : self
     {
-        $column = $this->filter->getColumn();
+        $class = $this->getElementClass();
 
         $script = <<<JS
-$(document).off('change', ".{$this->getClass($column)}");
-$(document).on('change', ".{$this->getClass($column)}", function () {
+$(document).off('change', ".{$class}");
+$(document).on('change', ".{$class}", function () {
     var target = $(this).closest('form').find(".{$this->getClass($target)}");
     $.get("$resourceUrl?q="+this.value, function (data) {
         target.find("option").remove();

+ 5 - 2
src/Grid/Filter/Presenter/SelectResource.php

@@ -17,7 +17,7 @@ class SelectResource extends Presenter
      */
     protected $placeholder = '';
 
-    protected $area = ['60%', '68%'];
+    protected $area = ['55%', '68%'];
 
     protected $source;
 
@@ -172,7 +172,10 @@ class SelectResource extends Presenter
      */
     public function variables() : array
     {
-        $this->value = request($this->filter->getColumn(), $this->filter->getValue() ?: $this->filter->getDefault());
+        $this->value = request(
+            $this->filter->getColumn(),
+            $this->filter->getValue() ?: $this->filter->getDefault()
+        );
 
         $this->formatOptions();
         $this->formatValue();

+ 4 - 1
src/Grid/Filter/Scope.php

@@ -3,9 +3,12 @@
 namespace Dcat\Admin\Grid\Filter;
 
 use Illuminate\Contracts\Support\Renderable;
+use Illuminate\Database\Query\Builder;
 use Illuminate\Support\Collection;
-use Illuminate\Support\Str;
 
+/**
+ * @mixin Builder
+ */
 class Scope implements Renderable
 {
     const QUERY_NAME = '_scope_';

+ 37 - 9
src/Grid/Model.php

@@ -8,15 +8,24 @@ use Dcat\Admin\Middleware\Pjax;
 use Dcat\Admin\Repositories\Repository;
 use Illuminate\Database\Eloquent\Model as EloquentModel;
 use Illuminate\Database\Eloquent\Relations\Relation;
+use Illuminate\Database\Query\Builder;
 use Illuminate\Pagination\AbstractPaginator;
 use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Collection;
-use Illuminate\Support\Facades\Request;
+use Illuminate\Http\Request;
 use Illuminate\Support\Str;
 
+/**
+ * @mixin Builder
+ */
 class Model
 {
+    /**
+     * @var Request
+     */
+    protected $request;
+
     /**
      * @var Repository
      */
@@ -115,13 +124,15 @@ class Model
      * Create a new grid model instance.
      *
      * @param Repository $repository
+     * @param Request $request
      */
-    public function __construct(?Repository $repository = null)
+    public function __construct(Request $request, ?Repository $repository = null)
     {
         if ($repository) {
-            $this->repository = Admin::createRepository($repository);
+            $this->repository = Admin::repository($repository);
         }
 
+        $this->request = $request;
         $this->queries = collect();
     }
 
@@ -380,7 +391,7 @@ class Model
      *
      * @return Collection|LengthAwarePaginator
      */
-    protected function get()
+    public function get()
     {
         if (
             $this->model instanceof LengthAwarePaginator
@@ -429,8 +440,12 @@ class Model
      */
     protected function handleInvalidPage(LengthAwarePaginator $paginator)
     {
-        if ($paginator->lastPage() && $paginator->currentPage() > $paginator->lastPage()) {
-            $lastPageUrl = Request::fullUrlWithQuery([
+        if (
+            $this->usePaginate
+            && $paginator->lastPage()
+            && $paginator->currentPage() > $paginator->lastPage()
+        ) {
+            $lastPageUrl = $this->request->fullUrlWithQuery([
                 $paginator->getPageName() => $paginator->lastPage(),
             ]);
 
@@ -449,7 +464,7 @@ class Model
             return null;
         }
 
-        return $this->currentPage ?: ($this->currentPage = \request($this->pageName, 1));
+        return $this->currentPage ?: ($this->currentPage = ($this->request->get($this->pageName) ?: 1));
     }
 
     /**
@@ -472,7 +487,8 @@ class Model
         if (!$this->usePaginate) {
             return null;
         }
-        return \request($this->perPageName, $this->perPage);
+
+        return $this->request->get($this->perPageName) ?: $this->perPage;
     }
 
     /**
@@ -560,7 +576,7 @@ class Model
      */
     protected function setSort()
     {
-        $this->sort = request($this->sortName, []);
+        $this->sort = $this->request->get($this->sortName, []);
 
         if (empty($this->sort['column']) || empty($this->sort['type'])) {
             return;
@@ -686,4 +702,16 @@ class Model
             return $data[$key];
         }
     }
+
+    /**
+     * @return $this
+     */
+    public function reset()
+    {
+        $this->data    = null;
+        $this->model   = null;
+        $this->queries = collect();
+
+        return $this;
+    }
 }

+ 26 - 1
src/Grid/Row.php

@@ -4,12 +4,13 @@ namespace Dcat\Admin\Grid;
 
 use Closure;
 use Dcat\Admin\Grid;
+use Illuminate\Contracts\Support\Arrayable;
 use Illuminate\Contracts\Support\Htmlable;
 use Illuminate\Contracts\Support\Jsonable;
 use Illuminate\Contracts\Support\Renderable;
 use Illuminate\Support\Arr;
 
-class Row
+class Row implements Arrayable
 {
     /**
      * @var Grid
@@ -141,6 +142,18 @@ class Row
         return Arr::get($this->data, $attr);
     }
 
+    /**
+     * Setter.
+     *
+     * @param mixed $attr
+     * @param mixed $value
+     * @return void
+     */
+    public function __set($attr, $value)
+    {
+        Arr::set($this->data, $attr, $value);
+    }
+
     /**
      * Get or set value of column in this row.
      *
@@ -166,6 +179,18 @@ class Row
         return $this;
     }
 
+    /**
+     * @return array
+     */
+    public function toArray()
+    {
+        if ($this->data instanceof Arrayable) {
+            return $this->data->toArray();
+        }
+
+        return (array) $this->data;
+    }
+
     /**
      * Output column value.
      *

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

@@ -51,7 +51,7 @@ JS;
      */
     protected function renderExportAll()
     {
-        if (! $this->grid->option('show_export_all')) {
+        if (! $this->grid->getExporter()->option('show_export_all')) {
             return;
         }
         $all = trans('admin.all');
@@ -64,7 +64,7 @@ JS;
      */
     protected function renderExportCurrentPage()
     {
-        if (! $this->grid->option('show_export_current_page')) {
+        if (! $this->grid->getExporter()->option('show_export_current_page')) {
             return;
         }
 
@@ -79,7 +79,10 @@ JS;
      */
     protected function renderExportSelectedRows()
     {
-        if (! $this->grid->option('show_row_selector') || ! $this->grid->option('show_export_selected_rows')) {
+        if (
+            ! $this->grid->option('show_row_selector')
+            || ! $this->grid->getExporter()->option('show_export_selected_rows')
+        ) {
             return;
         }
 

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

@@ -92,15 +92,13 @@ JS
 
         $this->setupScripts();
 
-        $showText = ((!$filters || $this->grid->option('show_filter') === false) && !$scopres->isEmpty()) ? true : false;
-
         $variables = [
             'scopes'           => $scopres,
             'current_label'    => $this->getCurrentScopeLabel(),
             'url_no_scopes'    => $filter->urlWithoutScopes(),
             'btn_class'        => $this->getElementClassName(),
             'expand'           => $filter->expand,
-            'show_filter_text' => $showText,
+            'show_filter_text' => true,
         ];
 
         return view($this->view, $variables)->render();

+ 12 - 4
src/Grid/Tools/QuickSearch.php

@@ -71,6 +71,14 @@ class QuickSearch extends AbstractTool
         return $this;
     }
 
+    /**
+     * @return string
+     */
+    public function getInput()
+    {
+        return request($this->queryName);
+    }
+
     /**
      * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
      */
@@ -88,11 +96,11 @@ class QuickSearch extends AbstractTool
         ]);
 
         $vars = [
-            'action' => $request->url() . '?' . http_build_query($query),
-            'key' => $this->queryName,
-            'value' => request($this->queryName),
+            'action'      => $request->url().'?'.http_build_query($query),
+            'key'         => $this->queryName,
+            'value'       => $this->getInput(),
             'placeholder' => $this->placeholder ?: trans('admin.search'),
-            'width' => $this->width,
+            'width'       => $this->width,
         ];
 
         return view($this->view, $vars);

+ 6 - 6
src/Grid/Tools/Selector.php

@@ -32,7 +32,7 @@ class Selector
     /**
      * @var string
      */
-    protected $queryKey;
+    protected $queryName;
 
     /**
      * Selector constructor.
@@ -43,7 +43,7 @@ class Selector
         $this->request   = request();
         $this->selectors = new Collection();
 
-        $this->queryKey = $grid->getName().'_selector';
+        $this->queryName = $grid->getName().'_selector';
     }
 
     /**
@@ -118,7 +118,7 @@ class Selector
             return $this->selected;
         }
 
-        $selected = $this->request->input($this->queryKey, []);
+        $selected = $this->request->get($this->queryName, []);
         if (! is_array($selected)) {
             return [];
         }
@@ -150,7 +150,7 @@ class Selector
         $options = Arr::get($selected, $column, []);
 
         if (is_null($value)) {
-            Arr::forget($query, "{$this->queryKey}.{$column}");
+            Arr::forget($query, "{$this->queryName}.{$column}");
 
             return $this->request->fullUrlWithQuery($query);
         }
@@ -165,9 +165,9 @@ class Selector
         }
 
         if (! empty($options)) {
-            Arr::set($query, "{$this->queryKey}.{$column}", implode(',', $options));
+            Arr::set($query, "{$this->queryName}.{$column}", implode(',', $options));
         } else {
-            Arr::forget($query, "{$this->queryKey}.{$column}");
+            Arr::forget($query, "{$this->queryName}.{$column}");
         }
 
         return $this->request->fullUrlWithQuery($query);

+ 3 - 7
src/Repositories/Proxy.php

@@ -13,8 +13,8 @@ class Proxy implements \Dcat\Admin\Contracts\Repository
     protected $__listeners = [];
 
     protected $__caches = [
-        'edit' => [],
-        'detail' => [],
+        'edit'     => [],
+        'detail'   => [],
         'updating' => [],
     ];
 
@@ -52,11 +52,7 @@ class Proxy implements \Dcat\Admin\Contracts\Repository
 
     public function get(Grid\Model $model)
     {
-        if (array_key_exists('get', $this->__caches)) {
-            return $this->__caches['get'];
-        }
-
-        return $this->__caches['get'] = $this->repository->get($model);
+        return $this->repository->get($model);
     }
 
     public function edit(Form $form): array

+ 1 - 1
src/Show.php

@@ -86,7 +86,7 @@ class Show implements Renderable
     public function __construct(?Repository $repository = null, ?\Closure $builder = null)
     {
         if ($repository) {
-            $this->repository = Admin::createRepository($repository);
+            $this->repository = Admin::repository($repository);
         }
         $this->builder = $builder;
 

+ 22 - 0
src/Support/Helper.php

@@ -6,10 +6,32 @@ use Illuminate\Contracts\Support\Arrayable;
 use Illuminate\Contracts\Support\Htmlable;
 use Illuminate\Contracts\Support\Renderable;
 use Illuminate\Support\Arr;
+use Illuminate\Support\Facades\Artisan;
 use Illuminate\Support\Str;
 
 class Helper
 {
+    /**
+     * Update extension config.
+     *
+     * @param array $config
+     * @return bool
+     */
+    public static function updateExtensionConfig(array $config)
+    {
+        $files  = app('files');
+        $result = (bool)$files->put(config_path('admin-extensions.php'), Helper::exportArrayPhp($config));
+
+        if ($result && is_file(base_path('bootstrap/cache/config.php'))) {
+            Artisan::call('config:cache');
+        }
+
+        \config(['admin-extensions' => $config]);
+
+        return $result;
+    }
+
+
     /**
      * Converts the given value to an array.
      *

+ 1 - 0
src/Traits/HasAssets.php

@@ -53,6 +53,7 @@ trait HasAssets
      */
     public static $baseJs = [
         'bootstrap'         => 'vendor/dcat-admin/AdminLTE/bootstrap/js/bootstrap.min.js',
+        'validator'         => 'vendor/dcat-admin/bootstrap-validator/validator.min.js',
         'jquery.slimscroll' => 'vendor/dcat-admin/AdminLTE/plugins/slimScroll/jquery.slimscroll.min.js',
         'adminLTE'          => 'vendor/dcat-admin/AdminLTE/dist/js/app.min.js',
         'layer'             => 'vendor/dcat-admin/layer/layer.js',

+ 7 - 2
src/Widgets/Accordion.php

@@ -2,10 +2,9 @@
 
 namespace Dcat\Admin\Widgets;
 
-use Illuminate\Contracts\Support\Renderable;
 use Illuminate\Support\Str;
 
-class Accordion extends Widget implements Renderable
+class Accordion extends Widget
 {
     /**
      * @var string
@@ -31,11 +30,17 @@ class Accordion extends Widget implements Renderable
         $this->id('accordion-'.Str::random(8));
     }
 
+    /**
+     * @return $this
+     */
     public function white()
     {
         return $this->panelStyle('white');
     }
 
+    /**
+     * @return $this
+     */
     public function panelStyle(string $style)
     {
         $this->panelStyle = 'panel-'.$style;

+ 49 - 10
src/Widgets/AjaxRequestBuilder.php

@@ -9,21 +9,29 @@ trait AjaxRequestBuilder
     /**
      * @var string
      */
-    protected $url;
+    protected $__url;
 
     /**
      * @var array
      */
     protected $buttonSelectors = [];
 
+    /**
+     * @var string
+     */
     protected $fn;
 
+    /**
+     * @var array
+     */
     protected $javascripts = [
         'fetching' => [],
         'fetched'  => [],
     ];
 
     /**
+     * Set request url.
+     *
      * @param string $url
      * @return $this
      */
@@ -32,27 +40,38 @@ trait AjaxRequestBuilder
         return $this->setUrl($url);
     }
 
+    /**
+     * Set current url to request.
+     *
+     * @param string $url
+     * @return $this
+     */
     public function requestCurrent(array $query = [])
     {
-        $this->url = url(request()->getPathInfo()).'?'.http_build_query($query);
+        $this->__url = url(request()->getPathInfo()).'?'.http_build_query($query);
 
         return $this;
     }
 
     /**
+     * Set request url.
+     *
      * @param string $url
      * @return $this
      */
     public function setUrl(string $url)
     {
-        $this->url = admin_url($url);
+        $this->__url = admin_url($url);
 
         return $this;
     }
 
+    /**
+     * @return string
+     */
     public function getUrl()
     {
-        return $this->url;
+        return $this->__url;
     }
 
     /**
@@ -64,7 +83,7 @@ trait AjaxRequestBuilder
     }
 
     /**
-     * 绑定重新获取数据按钮css选择器
+     * Set css selectors of refetch links.
      *
      * @param string|array $selector
      * @return $this
@@ -85,6 +104,12 @@ trait AjaxRequestBuilder
         return $this->buttonSelectors;
     }
 
+    /**
+     * Set the script before fetch data.
+     *
+     * @param string $script
+     * @return $this
+     */
     public function fetching(string $script)
     {
         $this->javascripts['fetching'][] = $script;
@@ -92,6 +117,12 @@ trait AjaxRequestBuilder
         return $this;
     }
 
+    /**
+     * Set the script after fetch data.
+     *
+     * @param string $script
+     * @return $this
+     */
     public function fetched(string $script)
     {
         $this->javascripts['fetched'][] = $script;
@@ -99,11 +130,17 @@ trait AjaxRequestBuilder
         return $this;
     }
 
+    /**
+     * @return bool
+     */
     public function allowBuildFetchingScript()
     {
-        return $this->url === null ? false : true;
+        return $this->__url === null ? false : true;
     }
 
+    /**
+     * @return bool|string
+     */
     public function buildFetchingScript()
     {
         if (!$this->allowBuildFetchingScript()) {
@@ -120,26 +157,28 @@ trait AjaxRequestBuilder
             $binding .= "$('{$v}').click(function () { {$this->fn}($(this).data()) });";
         }
 
-        return <<<SCRIPT
+        return <<<JS
 window.{$this->fn} = function (p) {
     $fetching;     
-    $.getJSON('{$this->url}', $.extend({_token:LA.token}, p || {}), function (result) {
+    $.getJSON('{$this->__url}', $.extend({_token:LA.token}, p || {}), function (result) {
         {$fetched};
     });
 }
 {$this->fn}();
 $binding;
-SCRIPT;
+JS;
 
     }
 
     /**
+     * Copy the given AjaxRequestBuilder.
+     *
      * @param AjaxRequestBuilder $fetcher
      * @return $this
      */
     public function copy($fetcher)
     {
-        $this->url = $fetcher->getUrl();
+        $this->__url = $fetcher->getUrl();
 
         $this->buttonSelectors = $fetcher->getButtonSelectors();
 

+ 36 - 3
src/Widgets/Alert.php

@@ -4,7 +4,7 @@ namespace Dcat\Admin\Widgets;
 
 use Illuminate\Contracts\Support\Renderable;
 
-class Alert extends Widget implements Renderable
+class Alert extends Widget
 {
     /**
      * @var string
@@ -52,6 +52,12 @@ class Alert extends Widget implements Renderable
         $this->style($style);
     }
 
+    /**
+     * Set title.
+     *
+     * @param string $title
+     * @return $this
+     */
     public function title($title)
     {
         $this->title = $title;
@@ -59,6 +65,12 @@ class Alert extends Widget implements Renderable
         return $this;
     }
 
+    /**
+     * Set contents.
+     *
+     * @param string|\Closure|Renderable $content
+     * @return $this
+     */
     public function content($content)
     {
         $this->content = $this->toString($content);
@@ -66,24 +78,45 @@ class Alert extends Widget implements Renderable
         return $this;
     }
 
+    /**
+     * Set info style.
+     *
+     * @return $this
+     */
     public function info()
     {
         return $this->style('info')->icon('fa fa-info');
     }
 
+    /**
+     * Set success style.
+     *
+     * @return $this
+     */
     public function success()
     {
         return $this->style('success')->icon('fa fa-check');
     }
 
+    /**
+     * Set warning style.
+     *
+     * @return $this
+     */
     public function warning()
     {
         return $this->style('warning')->icon('fa fa-warning');
     }
 
-    public function disableCloseButton()
+    /**
+     * Disable close button.
+     *
+     * @param bool $value
+     * @return $this
+     */
+    public function disableCloseButton(bool $value = true)
     {
-        $this->showCloseBtn = false;
+        $this->showCloseBtn = ! $value;
 
         return $this;
     }

+ 1 - 1
src/Widgets/Box.php

@@ -4,7 +4,7 @@ namespace Dcat\Admin\Widgets;
 
 use Illuminate\Contracts\Support\Renderable;
 
-class Box extends Widget implements Renderable
+class Box extends Widget
 {
     /**
      * @var string

+ 63 - 10
src/Widgets/Chart/Chart.php

@@ -43,6 +43,10 @@ abstract class Chart extends Widget
 
     protected $containerStyle = '';
 
+    /**
+     * Chart constructor.
+     * @param mixed ...$params
+     */
     public function __construct(...$params)
     {
         if (count($params) == 2) {
@@ -60,9 +64,7 @@ abstract class Chart extends Widget
             }
         }
 
-        if (!$this->colors) {
-            $this->colors = Colors::$charts['blue'];
-        }
+        $this->setDefaultColors();
     }
 
     /**
@@ -167,11 +169,17 @@ abstract class Chart extends Widget
         return $this;
     }
 
+    /**
+     * @return $this
+     */
     public function disableLegend()
     {
         return $this->legend(['display' => false]);
     }
 
+    /**
+     * @return $this
+     */
     public function legendPosition(string $val)
     {
         return $this->legend(['position' => $val]);
@@ -192,6 +200,11 @@ abstract class Chart extends Widget
         return $this;
     }
 
+    /**
+     * Disable tooltip.
+     *
+     * @return $this
+     */
     public function disableTooltip()
     {
         return $this->tooltips(['enabled' => false]);
@@ -268,12 +281,23 @@ abstract class Chart extends Widget
         return $this;
     }
 
+    /**
+     * Set width of container.
+     *
+     * @param string $width
+     * @return Chart
+     */
     public function width($width)
     {
         return $this->setContainerStyle('width:'.$width);
-
     }
 
+    /**
+     * Set height of container.
+     *
+     * @param string $height
+     * @return Chart
+     */
     public function height($height)
     {
         return $this->setContainerStyle('height:'.$height);
@@ -299,6 +323,7 @@ abstract class Chart extends Widget
      * Fill default color.
      *
      * @param array $colors
+     * @return void
      */
     protected function fillColor(array $colors = [])
     {
@@ -313,6 +338,8 @@ abstract class Chart extends Widget
 
     /**
      * Make element id.
+     *
+     * @return void
      */
     protected function makeId()
     {
@@ -343,8 +370,15 @@ abstract class Chart extends Widget
         ];
         $options = json_encode($config);
 
+        // Global configure.
+        $globalSettings = '';
+        foreach (self::$globalSettings as $k => $v) {
+            $globalSettings .= sprintf('Chart.defaults.global.%s="%s";', $k, $v);
+        }
+
         if (!$this->allowBuildFetchingScript()) {
             return <<<JS
+{$globalSettings}
 setTimeout(function(){ new Chart($("#{$this->id}").get(0).getContext("2d"), $options) },60)
 JS;
         }
@@ -363,7 +397,7 @@ window['obj'+id] = new Chart($("#"+id).get(0).getContext("2d"), opt);
 JS
         );
 
-        return $this->buildFetchingScript();
+        return $globalSettings.$this->buildFetchingScript();
 
     }
 
@@ -384,16 +418,14 @@ JS
 
     }
 
+    /**
+     * @return string
+     */
     public function render()
     {
         $this->makeId();
         $this->fillColor();
 
-        // Global configure.
-        foreach (self::$globalSettings as $k => $v) {
-            Admin::script(sprintf('Chart.defaults.global.%s="%s";', $k, $v));
-        }
-
         $this->script = $this->script();
 
         $this->setHtmlAttribute([
@@ -411,6 +443,11 @@ JS
 HTML;
     }
 
+    /**
+     * @param string $method
+     * @param array $parameters
+     * @return $this
+     */
     public function __call($method, $parameters)
     {
         if (isset(Colors::$charts[$method])) {
@@ -441,9 +478,25 @@ HTML;
         ));
     }
 
+    /**
+     * @return void
+     */
+    protected function setDefaultColors()
+    {
+        if (! $this->colors) {
+            $this->colors = Colors::$charts['blue'];
+        }
+    }
+
+    /**
+     * Collect assets.
+     *
+     * @return void
+     */
     public function collectAssets()
     {
         $this->script && Admin::script($this->script);
+
         Admin::collectComponentAssets('chartjs');
     }
 }

+ 2 - 9
src/Widgets/Colors.php

@@ -59,27 +59,20 @@ class Colors
             '#483D8B', // blue darker
         ],
 
-       'green' => [ // 绿色系
+       'green' => [
            'rgba(64,153,222,.5)', // primary
             '#21b978', // success
             '#47C1BF', // tear
             '#8FC15D', // green
         ],
 
-        'orange' => [ // 橙色系
+        'orange' => [
             'rgba(64,153,222,.5)', // primary
             '#F99037', // orange
             '#F5573B', // red
             '#F2CB22', // yellow
         ],
 
-//        'red2' => [ // 红色系
-//            '#F99037', // orange
-//            '#F5573B', // red
-//            '#F2CB22', // yellow
-//            '#ff5b5b', // danger
-//        ],
-
         'purple' => [
             'rgba(64,153,222,.5)', // primary
             'rgba(121,134,203, 1)', // purple

+ 43 - 11
src/Widgets/DataCard/Card.php

@@ -6,6 +6,7 @@ use Dcat\Admin\Admin;
 use Dcat\Admin\Widgets\Dropdown;
 use Dcat\Admin\Widgets\AjaxRequestBuilder;
 use Dcat\Admin\Widgets\Widget;
+use Illuminate\Contracts\Support\Renderable;
 use Illuminate\Support\Str;
 
 class Card extends Widget
@@ -65,26 +66,31 @@ class Card extends Widget
     }
 
     /**
-     * @param $val
+     * @param string|\Closure|Renderable $content
      * @param string $position
      * @return $this
      */
-    public function content($val, string $position = 'left')
+    public function content($content, string $position = 'left')
     {
-        $this->options['content'][$position] = $this->toString($val);
+        $this->options['content'][$position] = $this->toString($content);
 
         return $this;
     }
 
     /**
-     * @param $val
+     * @param string|\Closure|Renderable $content
      * @return $this
      */
-    public function rightContent($val)
+    public function rightContent($content)
     {
-        return $this->content($val, 'right');
+        return $this->content($content, 'right');
     }
 
+    /**
+     * @param int $number
+     * @param string $style
+     * @return $this
+     */
     public function progress($number, $style = 'primary')
     {
         $this->options['progress'] = [
@@ -95,6 +101,10 @@ class Card extends Widget
         return $this;
     }
 
+    /**
+     * @param string|\Closure|Renderable $content
+     * @return $this
+     */
     public function tool($content)
     {
         $this->options['tools'][] = $this->toString($content);
@@ -102,6 +112,12 @@ class Card extends Widget
         return $this;
     }
 
+    /**
+     * @param array $options
+     * @param \Closure $builder
+     * @param string|null $defaultLabel
+     * @return $this
+     */
     public function dropdown(array $options, \Closure $builder, ?string $defaultLabel = null)
     {
         return $this->tool(
@@ -113,6 +129,11 @@ class Card extends Widget
         );
     }
 
+    /**
+     * Setup scripts.
+     *
+     * @return string
+     */
     protected function script()
     {
         if (!$this->allowBuildFetchingScript()) {
@@ -124,19 +145,22 @@ class Card extends Widget
         return $this->buildFetchingScript();
     }
 
+    /**
+     * @return void
+     */
     protected function setupFetchScript()
     {
         $id = $this->getHtmlAttribute('id');
 
         $this->fetching(
-            <<<SCRIPT
+            <<<JS
 var card = $('#{$id}');   
 card.loading({style:'bottom:20px'})      
-SCRIPT
+JS
         );
 
         $this->fetched(
-            <<<SCRIPT
+            <<<JS
 if (!result.status) {
     return LA.error(result.message || 'Server internal error.');
 }     
@@ -148,11 +172,13 @@ card.find('.main-content').html(result.content.left || '');
 pg.css({width: 0});
 setTimeout(function(){ pg.css({width: w});}, 150);
 card.find('number').counterUp({time: 550});
-SCRIPT
-
+JS
         );
     }
 
+    /**
+     * @return string
+     */
     public function render()
     {
         $this->script = $this->script();
@@ -160,6 +186,9 @@ SCRIPT
         return parent::render(); // TODO: Change the autogenerated stub
     }
 
+    /**
+     * @return array
+     */
     public function buildJsonResponseArray()
     {
         return [
@@ -180,6 +209,9 @@ SCRIPT
         return response()->json(array_merge($this->buildJsonResponseArray(), $data));
     }
 
+    /**
+     * @return void
+     */
     public function collectAssets()
     {
         parent::collectAssets();

+ 9 - 4
src/Widgets/DataCard/DoughnutChartCard.php

@@ -15,17 +15,22 @@ class DoughnutChartCard extends Card
      */
     protected $chart;
 
-    protected $dotColors = [];
+    /**
+     * @var array
+     */
+    public $dotColors = [];
 
+    /**
+     * @var array
+     */
     protected $dots = [];
 
     public function __construct($title = null, $description = null)
     {
-        parent::__construct($title, $description);
-
         $this->setupChart();
-
         $this->setupDotColors();
+
+        parent::__construct($title, $description);
     }
 
     protected function setupChart()

+ 79 - 1
src/Widgets/Dropdown.php

@@ -4,6 +4,7 @@ namespace Dcat\Admin\Widgets;
 
 use Dcat\Admin\Admin;
 use Illuminate\Contracts\Support\Arrayable;
+use Illuminate\Contracts\Support\Renderable;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Str;
 
@@ -11,9 +12,15 @@ class Dropdown extends Widget
 {
     const DIVIDER = '_divider';
 
+    /**
+     * @var string
+     */
     protected static $dividerHtml = '<li class="divider"></li>';
 
-    public $template = '<span class="dropdown" style="display:inline-block">%s<ul class="dropdown-menu">%s</ul></span>';
+    /**
+     * @var string
+     */
+    protected $template = '<span class="dropdown" style="display:inline-block">%s<ul class="dropdown-menu">%s</ul></span>';
 
     /**
      * @var array
@@ -54,6 +61,13 @@ class Dropdown extends Widget
         $this->options($options);
     }
 
+    /**
+     * Set the options of dropdown menus.
+     *
+     * @param array $options
+     * @param string|null $title
+     * @return $this
+     */
     public function options($options = [], string $title = null)
     {
         if (!$options) return $this;
@@ -73,6 +87,12 @@ class Dropdown extends Widget
         return $this;
     }
 
+    /**
+     * Set the button text.
+     *
+     * @param string|null $text
+     * @return $this
+     */
     public function button(?string $text)
     {
         $this->button['text'] = $text;
@@ -80,11 +100,22 @@ class Dropdown extends Widget
         return $this;
     }
 
+    /**
+     * Without text of button.
+     *
+     * @return $this
+     */
     public function withoutTextButton()
     {
         return $this->button('');
     }
 
+    /**
+     * Set the button class.
+     *
+     * @param string $class
+     * @return $this
+     */
     public function buttonClass(string $class)
     {
         $this->button['class'] = $class;
@@ -92,6 +123,12 @@ class Dropdown extends Widget
         return $this;
     }
 
+    /**
+     * Set the button style.
+     *
+     * @param string $class
+     * @return $this
+     */
     public function buttonStyle(string $style)
     {
         $this->button['style'] = $style;
@@ -99,6 +136,12 @@ class Dropdown extends Widget
         return $this;
     }
 
+    /**
+     * Show divider.
+     *
+     * @param string $class
+     * @return $this
+     */
     public function divider()
     {
         $this->divider = true;
@@ -106,6 +149,12 @@ class Dropdown extends Widget
         return $this;
     }
 
+    /**
+     * Applies the callback to the elements of the options.
+     *
+     * @param string $class
+     * @return $this
+     */
     public function map(\Closure $builder)
     {
         $this->builder = $builder;
@@ -113,6 +162,12 @@ class Dropdown extends Widget
         return $this;
     }
 
+    /**
+     * Add click event listener.
+     *
+     * @param string|null $defaultLabel
+     * @return $this
+     */
     public function click(?string $defaultLabel = null)
     {
         $this->click = true;
@@ -126,6 +181,12 @@ class Dropdown extends Widget
         return $this;
     }
 
+    /**
+     * Set the template of dropdown menu.
+     *
+     * @param string|\Closure|Renderable $template
+     * @return $this
+     */
     public function template($template)
     {
         $this->template = $this->toString($template);
@@ -133,6 +194,9 @@ class Dropdown extends Widget
         return $this;
     }
 
+    /**
+     * @return string
+     */
     protected function renderButton()
     {
         if (is_null($this->button['text']) && !$this->click) return;
@@ -170,11 +234,17 @@ HTML
         );
     }
 
+    /**
+     * @return string
+     */
     public function getButtonId()
     {
         return $this->buttonId;
     }
 
+    /**
+     * @return string
+     */
     protected function renderOptions()
     {
         $opt = '';
@@ -194,6 +264,11 @@ HTML
         return $opt;
     }
 
+    /**
+     * @param mixed $k
+     * @param mixed $v
+     * @return mixed|string
+     */
     protected function renderOption($k, $v)
     {
         if ($v === static::DIVIDER) {
@@ -215,6 +290,9 @@ HTML
         return $v;
     }
 
+    /**
+     * @return string
+     */
     public function render()
     {
         if (is_null($this->button['text']) && !$this->options) {

+ 8 - 0
src/Widgets/Dump.php

@@ -50,6 +50,10 @@ class Dump extends Widget
         return $this;
     }
 
+    /**
+     * @param string|null $padding
+     * @return $this
+     */
     public function padding(?string $padding)
     {
         if ($padding) {
@@ -59,6 +63,10 @@ class Dump extends Widget
         return $this;
     }
 
+    /**
+     * @param string $width
+     * @return $this
+     */
     public function maxWidth($width)
     {
         $this->maxWidth = $width;

+ 80 - 12
src/Widgets/Form.php

@@ -5,10 +5,13 @@ namespace Dcat\Admin\Widgets;
 use Closure;
 use Dcat\Admin\Admin;
 use Dcat\Admin\Form\Field;
+use Dcat\Admin\Support\Helper;
 use Dcat\Admin\Traits\HasHtmlAttributes;
+use Dcat\EasyExcel\Support\Traits\Macroable;
 use Illuminate\Contracts\Support\Arrayable;
 use Illuminate\Contracts\Support\Renderable;
 use Illuminate\Support\Arr;
+use Illuminate\Support\Fluent;
 use Illuminate\Support\Str;
 
 /**
@@ -63,6 +66,7 @@ use Illuminate\Support\Str;
  * @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\BootstrapFile          bootstrapFile($column, $label = '')
  * @method Field\BootstrapImage         bootstrapImage($column, $label = '')
@@ -71,7 +75,9 @@ use Illuminate\Support\Str;
  */
 class Form implements Renderable
 {
-    use HasHtmlAttributes;
+    use HasHtmlAttributes, Macroable {
+        __call as macroCall;
+    }
 
     /**
      * @var Field[]
@@ -84,9 +90,14 @@ class Form implements Renderable
     protected $useAjaxSubmit = true;
 
     /**
-     * @var array
+     * @var Fluent
      */
-    protected $data = [];
+    protected $data;
+
+    /**
+     * @var mixed
+     */
+    protected $primaryKey;
 
     /**
      * Available buttons.
@@ -117,16 +128,12 @@ class Form implements Renderable
      * Form constructor.
      *
      * @param array $data
+     * @param mixed $key
      */
-    public function __construct($data = [])
+    public function __construct($data = [], $key = null)
     {
-        if ($data instanceof Arrayable) {
-            $data = $data->toArray();
-        }
-
-        if (!empty($data)) {
-            $this->data = $data;
-        }
+        $this->data($data);
+        $this->key($key);
 
         $this->initFormAttributes();
     }
@@ -177,6 +184,46 @@ class Form implements Renderable
         return $this->setHtmlAttribute('method', strtoupper($method));
     }
 
+    /**
+     * Set primary key.
+     *
+     * @param mixed $value
+     * @return $this
+     */
+    public function key($value)
+    {
+        $this->primaryKey = $value;
+
+        return $this;
+    }
+
+    /**
+     * @return mixed
+     */
+    public function getKey()
+    {
+        return $this->primaryKey;
+    }
+
+    /**
+     * @param $data
+     * @return $this
+     */
+    public function data($data)
+    {
+        $this->data = new Fluent(Helper::array($data));
+
+        return $this;
+    }
+
+    /**
+     * @return Fluent
+     */
+    public function model()
+    {
+        return $this->data;
+    }
+
     /**
      * Add a fieldset to form.
      *
@@ -198,6 +245,22 @@ class Form implements Renderable
         return $fieldset;
     }
 
+    /**
+     * Get specify field.
+     *
+     * @param string $name
+     * @return Field|null
+     */
+    public function field($name)
+    {
+        foreach ($this->fields as $field) {
+            if ($field->column() === $name) {
+                return $field;
+            }
+        }
+    }
+
+
     /**
      * Disable Pjax.
      *
@@ -298,6 +361,7 @@ class Form implements Renderable
     {
         array_push($this->fields, $field);
 
+        $field->setForm($this);
         $field->setWidth($this->width['field'], $this->width['label']);
 
         $field::collectAssets();
@@ -313,7 +377,7 @@ class Form implements Renderable
     protected function getVariables()
     {
         foreach ($this->fields as $field) {
-            $field->fill($this->data);
+            $field->fill($this->data->toArray());
         }
 
         return [
@@ -378,6 +442,10 @@ class Form implements Renderable
 
             return $element;
         }
+
+        if (static::hasMacro($method)) {
+            return $this->macroCall($method, $arguments);
+        }
     }
 
     /**

+ 2 - 1
src/Widgets/Markdown.php

@@ -4,6 +4,7 @@ namespace Dcat\Admin\Widgets;
 
 use Dcat\Admin\Admin;
 use Illuminate\Contracts\Support\Renderable;
+use Illuminate\Support\Str;
 
 class Markdown extends Widget
 {
@@ -84,7 +85,7 @@ EOF;
 
     public function render()
     {
-        $id = uniqid();
+        $id = 'mkd-'.Str::random();
 
         $this->defaultHtmlAttribute('id', $id);
 

+ 54 - 7
src/Widgets/Sparkline/Sparkline.php

@@ -84,6 +84,12 @@ class Sparkline extends Widget
         $this->options['type'] = $this->type;
     }
 
+    /**
+     * Get or set the sparkline values.
+     *
+     * @param mixed|null $values
+     * @return $this|array
+     */
     public function values($values = null)
     {
         if ($values === null) {
@@ -101,6 +107,12 @@ class Sparkline extends Widget
         return $this;
     }
 
+    /**
+     * Set width of sparkline.
+     *
+     * @param int $width
+     * @return $this
+     */
     public function width($width)
     {
         $this->options['width'] = $width;
@@ -108,6 +120,12 @@ class Sparkline extends Widget
         return $this;
     }
 
+    /**
+     * Set height of sparkline.
+     *
+     * @param int $width
+     * @return $this
+     */
     public function height($height)
     {
         $this->options['height'] = $height;
@@ -117,6 +135,12 @@ class Sparkline extends Widget
         return $this;
     }
 
+    /**
+     * Composite the given sparkline.
+     *
+     * @param int $width
+     * @return $this
+     */
     public function composite(Sparkline $chart)
     {
         $options = $chart->getOptions();
@@ -128,7 +152,12 @@ class Sparkline extends Widget
         return $this;
     }
 
-
+    /**
+     * Setup scripts.
+     *
+     * @param int $width
+     * @return string
+     */
     protected function script()
     {
         $values  = json_encode($this->values);
@@ -165,6 +194,9 @@ JS
         return $this->buildFetchingScript();
     }
 
+    /**
+     * @return string
+     */
     public function render()
     {
         $this->makeId();
@@ -182,6 +214,23 @@ HTML;
 
     }
 
+    /**
+     * Get element id.
+     *
+     * @return string
+     */
+    public function getId()
+    {
+        $this->makeId();
+
+        return $this->id;
+    }
+
+    /**
+     * @param string $method
+     * @param array $parameters
+     * @return Sparkline|Widget
+     */
     public function __call($method, $parameters)
     {
         if (in_array($method, static::$optionMethods)) {
@@ -191,15 +240,11 @@ HTML;
         return parent::__call($method, $parameters); // TODO: Change the autogenerated stub
     }
 
-    public function getId()
-    {
-        $this->makeId();
-
-        return $this->id;
-    }
 
     /**
      * Make element id.
+     *
+     * @return void
      */
     protected function makeId()
     {
@@ -230,6 +275,8 @@ HTML;
 
     /**
      * Collect assets.
+     *
+     * @return void
      */
     protected function collectAssets()
     {

Некоторые файлы не были показаны из-за большого количества измененных файлов