Explorar o código

Merge branch 'selectable'

jqh %!s(int64=5) %!d(string=hai) anos
pai
achega
660ae6fc75

+ 10 - 2
resources/assets/dcat/js/extensions/Helpers.js

@@ -246,15 +246,23 @@ export default class Helpers {
     }
 
     // 异步加载
-    asyncRender(url, callback) {
+    asyncRender(url, done, error) {
         let Dcat = this.dcat;
 
         $.ajax(url).then(function (data) {
-            callback(
+            done(
                 Dcat.assets.executeScripts(data, function () {
                     Dcat.triggerReady();
                 }).render()
             );
+        }, function (a, b, c) {
+            if (error) {
+                if (error(a, b, c) === false) {
+                    return false;
+                }
+            }
+
+            Dcat.handleAjaxError(a, b, c);
         })
     }
 }

+ 3 - 3
resources/assets/dcat/js/extensions/RowSelector.js

@@ -22,10 +22,10 @@ export default class RowSelector {
     _bind() {
         let options = this.options,
             checkboxSelector = options.checkboxSelector,
-            $selectAllSelector = $(options.selectAllSelector),
+            $selectAll = $(options.selectAllSelector),
             $checkbox = $(checkboxSelector);
 
-        $selectAllSelector.on('change', function() {
+        $selectAll.on('change', function() {
             $(this).parents(options.container).find(checkboxSelector).prop('checked', this.checked).trigger('change');
         });
         if (options.clickRow) {
@@ -47,7 +47,7 @@ export default class RowSelector {
                 tr.css('background-color', options.background);
 
                 if ($(checkboxSelector + ':checked').length === $checkbox.length) {
-                    $selectAllSelector.prop('checked', true)
+                    $selectAll.prop('checked', true)
                 }
             } else {
                 tr.css('background-color', '');

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

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

+ 41 - 0
resources/assets/dcat/sass/components/_grid.scss

@@ -51,3 +51,44 @@
     color: #777;
   }
 }
+
+
+.grid-modal {
+  .modal-body {
+    //background:$body-bg;
+    padding:1.5rem
+  }
+}
+
+.table-card {
+  .filter-box {
+    background: transparent;
+    box-shadow: none!important;
+    padding: 0!important;
+    margin: 1rem 0 -1rem!important;
+    padding-bottom: 0!important;
+
+    .form-group {
+      margin-bottom: 1rem;
+    }
+  }
+
+  .custom-data-table-header .table-responsive .top .dataTables_filter .form-control {
+    border-radius: .4rem;
+    border: 1px solid $input-border-color;
+    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
+  }
+}
+
+body:not(.dark-mode) .grid-modal {
+  .table-collapse .table.custom-data-table {
+    padding: 5px 0 0;
+  }
+  .table-collapse table.custom-data-table.dataTable thead th {
+    height: 20px;
+  }
+
+  .table-collapse .custom-data-table.dataTable tbody td {
+    height: 35px;
+  }
+}

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

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

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

@@ -1,5 +1,5 @@
 <div class="hidden">
-    <div class="right-side-filter-container" style="{!! $style !!}"  id="{{ $filterID }}">
+    <div class="filter-box right-side-filter-container" style="{!! $style !!}"  id="{{ $filterID }}">
         <form action="{!! $action !!}" class="form-horizontal" pjax-container method="get">
             <div class="mb-1" style="height: 55px">
                 <div class="p-1 position-fixed d-flex justify-content-between header">

+ 27 - 0
resources/views/filter/tile-container.blade.php

@@ -0,0 +1,27 @@
+<div class="filter-box card p-2 {{ $expand ? '' : 'd-none' }} {{$containerClass}}" style="padding-bottom: .5rem!important;margin-top: 10px;margin-bottom: 8px;box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.04);">
+    <div class="card-body" style="{!! $style !!}"  id="{{ $filterID }}">
+        <form action="{!! $action !!}" class="form-horizontal" pjax-container method="get">
+            <div class="row mt-1 mb-0">
+                @foreach($layout->columns() as $column)
+                    @foreach($column->filters() as $filter)
+                        {!! $filter->render() !!}
+                    @endforeach
+                @endforeach
+
+                <div class="btn-group ml-1 mb-1" style="height: fit-content;margin-right: 10px">
+                    <button class="btn btn-primary btn-sm btn-mini submit">
+                        <i class="feather icon-search"></i><span class="d-none d-sm-inline">&nbsp;&nbsp;{{ trans('admin.search') }}</span>
+                    </button>
+                </div>
+                <div class="btn-group btn-group-sm default btn-mini" style="height: fit-content"  >
+                    @if(!$disableResetButton)
+                        <a  href="{!! $action !!}" class="reset btn btn-white btn-sm ">
+                            <i class="feather icon-rotate-ccw"></i><span class="d-none d-sm-inline">&nbsp;&nbsp;{{ trans('admin.reset') }}</span>
+                        </a>
+                    @endif
+                </div>
+            </div>
+
+        </form>
+    </div>
+</div>

+ 27 - 0
resources/views/form/selecttable.blade.php

@@ -0,0 +1,27 @@
+<div class="{{$viewClass['form-group']}} {!! !$errors->has($column) ?: 'has-error' !!}">
+    <label class="{{$viewClass['label']}} control-label">{!! $label !!}</label>
+    <div class="{{$viewClass['field']}} select-resource">
+        @include('admin::form.error')
+
+        <div class="input-group">
+            <div {!! $attributes !!}>
+                <span class="default-text" style="opacity:0.75">{{ $placeholder }}</span>
+                <span class="option d-none"></span>
+            </div>
+
+            @if(! $disabled)
+                <input name="{{ $name }}" type="hidden" id="hidden-{{ $id }}" value="{{ implode(',', \Dcat\Admin\Support\Helper::array($value)) }}" />
+            @endif
+            <div class="input-group-append">
+                <div class="btn btn-{{ $style }} " data-toggle="modal" data-target="#{{ $id }}">
+                    &nbsp;<i class="feather icon-arrow-up"></i>&nbsp;
+                </div>
+            </div>
+        </div>
+
+        {!! $modal !!}
+
+        @include('admin::form.help-block')
+
+    </div>
+</div>

+ 4 - 4
resources/views/grid/pagination.blade.php

@@ -1,9 +1,9 @@
 <ul class="pagination pagination-sm no-margin pull-right shadow-100" style="border-radius: 1.5rem">
     <!-- Previous Page Link -->
     @if ($paginator->onFirstPage())
-    <li class="page-item previous disabled"><span class="page-link">{{ __('admin.prev_page') }}</span></li>
+    <li class="page-item previous disabled"><span class="page-link"></span></li>
     @else
-    <li class="page-item previous"><a class="page-link" href="{{ $paginator->previousPageUrl() }}" rel="prev">{{ __('admin.prev_page') }}</a></li>
+    <li class="page-item previous"><a class="page-link" href="{{ $paginator->previousPageUrl() }}" rel="prev"></a></li>
     @endif
 
     <!-- Pagination Elements -->
@@ -27,8 +27,8 @@
 
     <!-- Next Page Link -->
     @if ($paginator->hasMorePages())
-    <li class="page-item next"><a class="page-link" href="{{ $paginator->nextPageUrl() }}" rel="next">{{ __('admin.next_page') }}</a></li>
+    <li class="page-item next"><a class="page-link" href="{{ $paginator->nextPageUrl() }}" rel="next"></a></li>
     @else
-    <li class="page-item next disabled"><span class="page-link">{{ __('admin.next_page') }}</span></li>
+    <li class="page-item next disabled"><span class="page-link"></span></li>
     @endif
 </ul>

+ 4 - 0
src/Color.php

@@ -55,6 +55,7 @@ use Illuminate\Support\Traits\Macroable;
  * @method string grayBg(int $amt = 0)
  * @method string border(int $amt = 0)
  * @method string inputBorder(int $amt = 0)
+ * @method string background(int $amt = 0)
  */
 class Color
 {
@@ -176,6 +177,9 @@ class Color
 
         // 表单边框
         'input-border' => '#d9d9d9',
+
+        // 背景色
+        'background' => '#eff3f8',
     ];
 
     /**

+ 2 - 0
src/Form.php

@@ -83,6 +83,7 @@ use Symfony\Component\HttpFoundation\Response;
  * @method Field\Range                  range($start, $end, $label = '')
  * @method Field\Color                  color($column, $label = '')
  * @method Field\ArrayField             array($column, $labelOrCallback, $callback = null)
+ * @method Field\SelectTable            selectTable($column, $label = '')
  */
 class Form implements Renderable
 {
@@ -165,6 +166,7 @@ class Form implements Renderable
         'range'          => Field\Range::class,
         'color'          => Field\Color::class,
         'array'          => Field\ArrayField::class,
+        'selectTable'    => Field\SelectTable::class,
     ];
 
     /**

+ 3 - 0
src/Form/Field/SelectResource.php

@@ -8,6 +8,9 @@ use Dcat\Admin\IFrameGrid;
 use Dcat\Admin\Support\Helper;
 use Illuminate\Contracts\Support\Arrayable;
 
+/**
+ * @deprecated 即将在2.0版本中废弃
+ */
 class SelectResource extends Field
 {
     use PlainInput;

+ 259 - 0
src/Form/Field/SelectTable.php

@@ -0,0 +1,259 @@
+<?php
+
+namespace Dcat\Admin\Form\Field;
+
+use Dcat\Admin\Form\Field;
+use Dcat\Admin\Support\Helper;
+use Dcat\Admin\Widgets\TableModal;
+use Dcat\Admin\Grid\LazyRenderable;
+
+class SelectTable extends Field
+{
+    use PlainInput;
+
+    /**
+     * @var TableModal
+     */
+    protected $modal;
+
+    protected $style = 'primary';
+
+    public function __construct($column, $arguments = [])
+    {
+        parent::__construct($column, $arguments);
+
+        $this->modal = TableModal::title($this->label);
+    }
+
+    /**
+     * 设置弹窗标题.
+     *
+     * @param string $title
+     *
+     * @return $this
+     */
+    public function title($title)
+    {
+        $this->modal->title($title);
+
+        return $this;
+    }
+
+    /**
+     * 设置尺寸为 xl.
+     *
+     * @return $this
+     */
+    public function xl()
+    {
+        $this->modal->xl();
+
+        return $this;
+    }
+
+    /**
+     * 设置表格异步渲染实例.
+     *
+     * @param LazyRenderable $renderable
+     *
+     * @return $this
+     */
+    public function from(LazyRenderable $renderable)
+    {
+        $this->modal->body($renderable);
+
+        return $this;
+    }
+
+    /**
+     * 转化为数组格式保存.
+     *
+     * @param mixed $value
+     *
+     * @return array|mixed
+     */
+    public function prepareInputValue($value)
+    {
+        return Helper::array($value, true);
+    }
+
+    protected function formatOptions()
+    {
+        $value = Helper::array(old($this->column, $this->value()));
+
+        if ($this->options instanceof \Closure) {
+            $this->options = $this->options->call($this->values(), $value, $this);
+        }
+
+        $values = [];
+
+        foreach (Helper::array($this->options) as $id => $label) {
+            foreach ($value as $v) {
+                if ($v == $id && $v !== null) {
+                    $values[] = ['id' => $v, 'label' => $label];
+                }
+            }
+        }
+
+        $this->options = json_encode($values);
+    }
+
+    protected function addScript()
+    {
+        $this->script .= <<<JS
+(function () {
+    var modal = $(replaceNestedFormIndex('#{$this->modal->getId()}'));
+    var input = $(replaceNestedFormIndex('#hidden-{$this->id}'));
+    var options = {$this->options};
+    
+    function getSelectedRows() {
+        var selected = [], ids = [];
+    
+        modal.find('.checkbox-grid-column input[type="checkbox"]:checked').each(function() {
+            var id = $(this).data('id'), i, exist;
+    
+            for (i in selected) {
+                if (selected[i].id === id) {
+                    exist = true
+                }
+            }
+    
+            if (! exist) {
+                selected.push({'id': id, 'label': $(this).data('label')});
+                ids.push(id)
+            }
+        });
+    
+        return [selected, ids];
+    }
+    
+    function setKeys(ids) {
+        input.val(ids.length ? ids.join(',') : '');
+    }
+            
+    $(replaceNestedFormIndex('#{$this->getButtonId()}')).on('click', function () {
+        var selected = getSelectedRows();
+        
+        setKeys(selected[1]);
+        
+        render(selected[0]);
+        
+        $(this).parents('.modal').modal('toggle');
+    });
+    
+    function render(selected) {
+        var box = $('{$this->getElementClassSelector()}'),
+            placeholder = box.find('.default-text'),
+            option = box.find('.option');
+        
+        if (! selected) {
+            placeholder.removeClass('d-none');
+            option.addClass('d-none');
+            
+            return;
+        }
+        
+        placeholder.addClass('d-none');
+        option.removeClass('d-none');
+        
+        var remove = $("<div class='pull-right ' style='font-weight:bold;cursor:pointer'>×</div>");
+
+        option.text(selected[0]['label']);
+        option.append(remove);
+        
+        remove.on('click', function () {
+            setKeys([]);
+            placeholder.removeClass('d-none');
+            option.addClass('d-none');
+        });
+    }
+    
+    render(options[0]);
+})();
+JS;
+    }
+
+    protected function setUpModal()
+    {
+        $this->modal
+            ->join()
+            ->id($this->getElementId())
+            ->runScript(false)
+            ->footer($this->renderFooter())
+            ->onLoad($this->getOnLoadScript());
+    }
+
+    protected function getOnLoadScript()
+    {
+        // 实现单选效果
+        return <<<JS
+$(this).find('.checkbox-grid-header').remove();
+
+var checkbox = $(this).find('.checkbox-grid-column input[type="checkbox"]');
+
+checkbox.on('change', function () {
+    var id = $(this).data('id');
+    
+    checkbox.each(function () {
+        if ($(this).data('id') != id) {
+            $(this).prop('checked', false);
+            $(this).parents('tr').css('background-color', '');
+        }
+    });
+});
+JS;
+    }
+
+    public function render()
+    {
+        $this->setUpModal();
+        $this->formatOptions();
+
+        $name = $this->getElementName();
+
+        $this->prepend('<i class="feather icon-arrow-up"></i>')
+            ->defaultAttribute('class', 'form-control '. $this->getElementClassString())
+            ->defaultAttribute('type', 'text')
+            ->defaultAttribute('name', $name);
+
+        $this->addVariables([
+            'prepend'     => $this->prepend,
+            'append'      => $this->append,
+            'style'       => $this->style,
+            'modal'       => $this->modal->render(),
+            'placeholder' => $this->placeholder(),
+        ]);
+
+        $this->script = $this->modal->getScript();
+
+        $this->addScript();
+        //dd($this->script);
+        return parent::render();
+    }
+
+    /**
+     * 弹窗底部内容构建.
+     *
+     * @return string
+     */
+    protected function renderFooter()
+    {
+        $submit = trans('admin.submit');
+        $cancel = trans('admin.cancel');
+
+        return <<<HTML
+<a id="{$this->getButtonId()}" class="btn btn-primary" style="color: #fff">&nbsp;{$submit}&nbsp;</a>&nbsp;
+<a onclick="$(this).parents('.modal').modal('toggle')" class="btn btn-white">&nbsp;{$cancel}&nbsp;</a>
+HTML;
+    }
+
+    /**
+     * 提交按钮ID
+     *
+     * @return string
+     */
+    protected function getButtonId()
+    {
+        return 'submit-'.$this->id;
+    }
+}

+ 0 - 18
src/Grid.php

@@ -801,24 +801,6 @@ HTML;
         return new static(...$params);
     }
 
-    /**
-     * @return $this
-     */
-    public function inIframe()
-    {
-        $this->setName('_dialog_');
-        $this->disableCreateButton();
-        $this->disableActions();
-        $this->disablePerPages();
-        $this->disableBatchActions();
-
-        $this->rowSelector()->click();
-
-        Admin::style('#app{padding: 1.4rem 1rem 1rem}');
-
-        return $this;
-    }
-
     /**
      * Enable responsive tables.
      *

+ 15 - 0
src/Grid/Column/Filter.php

@@ -155,6 +155,21 @@ abstract class Filter implements Renderable
         );
     }
 
+    /**
+     * @return string
+     */
+    protected function renderFormButtons()
+    {
+        return <<<HMLT
+<li class="dropdown-divider"></li>
+<li>
+    <button class="btn btn-sm btn-primary column-filter-submit "><i class="feather icon-search"></i></button>&nbsp;
+    <a href="{$this->urlWithoutFilter()}" class="btn btn-sm btn-default"><i class="feather icon-rotate-ccw"></i></a>
+</li>
+HMLT;
+
+    }
+
     /**
      * Get form action url.
      *

+ 1 - 5
src/Grid/Column/Filter/Between.php

@@ -191,11 +191,7 @@ JS;
                 value="{$value['end']}" 
                 autocomplete="off"/>
         </li>
-        <li class="dropdown-divider"></li>
-        <li class="dropdown-item">
-            <button class="btn btn-sm btn-primary column-filter-submit "><i class="feather icon-search"></i></button>
-            <span onclick="Dcat.reload('{$this->urlWithoutFilter()}')" class="btn btn-sm btn-default"><i class="feather icon-rotate-ccw"></i></span>
-        </li>
+        {$this->renderFormButtons()}
     </ul>
     </form>
 </span>

+ 1 - 5
src/Grid/Column/Filter/Checkbox.php

@@ -62,11 +62,7 @@ JS;
                 {$this->renderOptions($value)}
             </ul>
         </li>
-        <li class="dropdown-divider"></li>
-       <li class="dropdown-item">
-            <button class="btn btn-sm btn-primary column-filter-submit "><i class="feather icon-search"></i></button>
-            <span onclick="Dcat.reload('{$this->urlWithoutFilter()}')" class="btn btn-sm btn-default"><i class="feather icon-rotate-ccw"></i></span>
-        </li>
+        {$this->renderFormButtons()}
     </ul>
 </form>
 </span>

+ 1 - 5
src/Grid/Column/Filter/Input.php

@@ -59,11 +59,7 @@ JS;
         <li>
             <input placeholder="{$this->placeholder}" type="text" name="{$this->getQueryName()}" value="{$value}" class="form-control input-sm {$this->class}" autocomplete="off"/>
         </li>
-        <li class="dropdown-divider"></li>
-        <li>
-            <button class="btn btn-sm btn-primary column-filter-submit "><i class="feather icon-search"></i></button>
-            <span onclick="Dcat.reload('{$this->urlWithoutFilter()}')" class="btn btn-sm btn-default"><i class="feather icon-rotate-ccw"></i></span>
-        </li>
+        {$this->renderFormButtons()}
     </ul>
     </form>
 </span>

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

@@ -94,6 +94,6 @@ class Sorter implements Renderable
             ]);
         }
 
-        return "&nbsp;<a href='{$url}' class='feather icon-arrow-{$icon} {$active}'></a>";
+        return "&nbsp;<a href='{$url}' class='grid-sort feather icon-arrow-{$icon} {$active}'></a>";
     }
 }

+ 42 - 0
src/Grid/LazyRenderable.php

@@ -0,0 +1,42 @@
+<?php
+
+namespace Dcat\Admin\Grid;
+
+use Dcat\Admin\Grid;
+use Dcat\Admin\Support\LazyRenderable as Renderable;
+
+abstract class LazyRenderable extends Renderable
+{
+    abstract public function grid(): Grid;
+
+    public function render()
+    {
+        return $this->prepare($this->grid())->render();
+    }
+
+    protected function prepare(Grid $grid)
+    {
+        if (! $grid->getName()) {
+            $grid->setName($this->getDefaultName());
+        }
+
+        $grid->disableCreateButton();
+        $grid->disableActions();
+        $grid->disablePerPages();
+        $grid->disableBatchActions();
+        $grid->disableRefreshButton();
+
+        $grid->filter()
+            ->panel()
+            ->view('admin::filter.tile-container');
+
+        $grid->rowSelector()->click();
+
+        return $grid;
+    }
+
+    protected function getDefaultName()
+    {
+        return strtolower(str_replace('\\', '-', static::class));
+    }
+}

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

@@ -54,7 +54,7 @@ class RowSelector
     public function renderHeader()
     {
         return <<<HTML
-<div class="vs-checkbox-con vs-checkbox-{$this->style} checkbox-grid">
+<div class="vs-checkbox-con vs-checkbox-{$this->style} checkbox-grid checkbox-grid-header">
     <input type="checkbox" class="select-all {$this->grid->getSelectAllName()}">
     <span class="vs-checkbox"><span class="vs-checkbox--check"><i class="vs-icon feather icon-check"></i></span></span>
 </div>
@@ -67,7 +67,7 @@ HTML;
         $title = e($this->getTitle($row, $id));
 
         return <<<EOT
-<div class="vs-checkbox-con vs-checkbox-{$this->style} checkbox-grid">
+<div class="vs-checkbox-con vs-checkbox-{$this->style} checkbox-grid checkbox-grid-column">
     <input type="checkbox" class="{$this->grid->getRowName()}-checkbox" data-id="{$id}" data-label="{$title}">
     <span class="vs-checkbox"><span class="vs-checkbox--check"><i class="vs-icon feather icon-check"></i></span></span>
 </div>        

+ 5 - 1
src/Middleware/Bootstrap.php

@@ -25,7 +25,11 @@ class Bootstrap
 
     protected function setUpDarkMode()
     {
-        if (config('admin.layout.dark_mode_switch')) {
+        if (
+            config('admin.layout.dark_mode_switch')
+            && ! Helper::isAjaxRequest()
+            && ! request()->routeIs(admin_api_route('*'))
+        ) {
             Admin::navbar()->right((new DarkModeSwitcher())->render());
         }
     }

+ 1 - 1
src/Traits/AsyncRenderable.php

@@ -26,7 +26,7 @@ trait AsyncRenderable
      *
      * @return $this
      */
-    public function setRenderable(LazyRenderable $renderable)
+    public function setRenderable(?LazyRenderable $renderable)
     {
         $this->renderable = $renderable;
 

+ 8 - 5
src/Widgets/ApexCharts/Chart.php

@@ -243,7 +243,7 @@ JS;
     /**
      * @return string
      */
-    public function script()
+    public function addScript()
     {
         if (! $this->allowBuildRequest()) {
             return $this->buildDefaultScript();
@@ -271,7 +271,7 @@ if (chartBox.length) {
 JS
         );
 
-        return $this->buildRequestScript();
+        return $this->script = $this->buildRequestScript();
     }
 
     /**
@@ -284,6 +284,11 @@ JS
         }
         $this->built = true;
 
+        return parent::render();
+    }
+
+    public function html()
+    {
         $hasSelector = $this->containerSelector ? true : false;
 
         if (! $hasSelector) {
@@ -293,9 +298,7 @@ JS
             $this->selector('#'.$id);
         }
 
-        $this->script = $this->script();
-
-        $this->collectAssets();
+        $this->addScript();
 
         if ($hasSelector) {
             return;

+ 141 - 0
src/Widgets/AsyncTable.php

@@ -0,0 +1,141 @@
+<?php
+
+namespace Dcat\Admin\Widgets;
+
+use Dcat\Admin\Admin;
+use Dcat\Admin\Grid\LazyRenderable;
+use Dcat\Admin\Traits\AsyncRenderable;
+use Illuminate\Support\Str;
+
+class AsyncTable extends Widget
+{
+    use AsyncRenderable;
+
+    protected $load = true;
+
+    public function __construct(LazyRenderable $renderable = null, bool $load = true)
+    {
+        $this->setRenderable($renderable);
+        $this->load($load);
+
+        $this->id('table-card-'.Str::random(8));
+        $this->class('table-card');
+    }
+
+    /**
+     * 设置是否自动加载.
+     *
+     * @param bool $value
+     *
+     * @return $this
+     */
+    public function load(bool $value)
+    {
+        $this->load = $value;
+
+        return $this;
+    }
+
+    /**
+     * 监听异步渲染完成事件.
+     *
+     * @param string $script
+     *
+     * @return $this
+     */
+    public function onLoad(string $script)
+    {
+        $this->script .= "$(replaceNestedFormIndex('{$this->getElementSelector()}')).on('table:loaded', function (event) { {$script} });";
+
+        return $this;
+    }
+
+    protected function addScript()
+    {
+        Admin::script(<<<'JS'
+(function () {
+    function load(url, box) {
+        var $this = $(this);
+        
+        box = box || $this;
+        
+        url = $this.data('url') || url;
+        if (! url) {
+            return;
+        }
+        
+        box.loading({background: 'transparent!important'});
+        
+        Dcat.helpers.asyncRender(url, function (html) {
+            box.loading(false);
+            box.html(html);
+            bind(box);
+            box.trigger('table:loaded');
+        });
+    }            
+                
+    function bind(box) {
+        function loadLink() {
+            load($(this).attr('href'), box);
+            
+            return false;
+        }
+        
+        box.find('.pagination .page-link').on('click', loadLink);
+        box.find('.grid-column-header a').on('click', loadLink);
+  
+        box.find('form').on('submit', function () {
+            load($(this).attr('action')+'&'+$(this).serialize(), box);
+            
+            return false;
+        });
+         
+        box.find('.filter-box .reset').on('click', loadLink);
+    }
+    
+    $('.table-card').on('table:load', load);
+})();
+JS
+        );
+
+        if ($this->load) {
+            $this->script .= $this->getLoadScript();
+        }
+    }
+
+    /**
+     * @return string
+     */
+    public function getElementSelector()
+    {
+        return '#'.$this->getHtmlAttribute('id');
+    }
+
+    /**
+     * @return string
+     */
+    public function getLoadScript()
+    {
+        return <<<JS
+$(replaceNestedFormIndex('{$this->getElementSelector()}')).trigger('table:load');
+JS;
+    }
+
+    public function render()
+    {
+        $this->addScript();
+
+        return parent::render();
+    }
+
+    public function html()
+    {
+        $this->setHtmlAttribute([
+            'data-url' => $this->getRequestUrl(),
+        ]);
+
+        return <<<HTML
+<div {$this->formatHtmlAttributes()} style="min-height: 200px"></div>        
+HTML;
+    }
+}

+ 3 - 3
src/Widgets/Metrics/Card.php

@@ -433,7 +433,7 @@ class Card extends Widget
     /**
      * @return mixed
      */
-    public function script()
+    public function addScript()
     {
         if (! $this->allowBuildRequest()) {
             return;
@@ -470,7 +470,7 @@ JS
         }
 
         // 按钮显示选中文本
-        return <<<JS
+        return $this->script = <<<JS
 $('{$clickable}').on('click', function () {
     $(this).parents('.dropdown').find('.btn').html($(this).text());
 });
@@ -533,7 +533,7 @@ JS;
         $this->setUpChart();
         $this->setUpCardHeight();
 
-        $this->script = $this->script();
+        $this->addScript();
 
         $this->variables['icon'] = $this->icon;
         $this->variables['title'] = $this->title;

+ 106 - 24
src/Widgets/Modal.php

@@ -15,19 +15,19 @@ class Modal extends Widget
     use AsyncRenderable;
 
     /**
-     * @var string
+     * @var string|Closure|Renderable
      */
-    protected $id;
+    protected $title;
 
     /**
      * @var string|Closure|Renderable
      */
-    protected $title;
+    protected $content;
 
     /**
      * @var string|Closure|Renderable
      */
-    protected $content;
+    protected $footer;
 
     /**
      * @var string|Closure|Renderable
@@ -49,6 +49,16 @@ class Modal extends Widget
      */
     protected $delay = 10;
 
+    /**
+     * @var string
+     */
+    protected $load = '';
+
+    /**
+     * @var bool
+     */
+    protected $join = false;
+
     /**
      * Modal constructor.
      *
@@ -60,20 +70,8 @@ class Modal extends Widget
         $this->id('modal-'.Str::random(8));
         $this->title($title);
         $this->content($content);
-    }
-
-    /**
-     * 设置弹窗ID.
-     *
-     * @param string $id
-     *
-     * @return $this
-     */
-    public function id(string $id)
-    {
-        $this->id = $id;
 
-        return $this;
+        $this->class('modal fade');
     }
 
     /**
@@ -181,7 +179,7 @@ class Modal extends Widget
     }
 
     /**
-     * @param $content
+     * @param string|Closure|Renderable|LazyRenderable $content
      *
      * @return $this
      */
@@ -190,6 +188,32 @@ class Modal extends Widget
         return $this->content($content);
     }
 
+    /**
+     * 设置是否返回弹窗HTML.
+     *
+     * @param bool $value
+     *
+     * @return $this
+     */
+    public function join(bool $value = true)
+    {
+        $this->join = $value;
+
+        return $this;
+    }
+
+    /**
+     * @param string|Closure|Renderable|LazyRenderable $footer
+     *
+     * @return $this
+     */
+    public function footer($footer)
+    {
+        $this->footer = $footer;
+
+        return $this;
+    }
+
     /**
      * 监听弹窗事件.
      *
@@ -205,6 +229,18 @@ class Modal extends Widget
         return $this;
     }
 
+    /**
+     * 监听弹窗显示事件.
+     *
+     * @param string $script
+     *
+     * @return $this
+     */
+    public function onShow(string $script)
+    {
+        return $this->on('show.bs.modal', $script);
+    }
+
     /**
      * 监听弹窗已显示事件.
      *
@@ -217,6 +253,18 @@ class Modal extends Widget
         return $this->on('shown.bs.modal', $script);
     }
 
+    /**
+     * 监听弹窗隐藏事件.
+     *
+     * @param string $script
+     *
+     * @return $this
+     */
+    public function onHide(string $script)
+    {
+        return $this->on('hide.bs.modal', $script);
+    }
+
     /**
      * 监听弹窗已隐藏事件.
      *
@@ -229,12 +277,26 @@ class Modal extends Widget
         return $this->on('hidden.bs.modal', $script);
     }
 
+    /**
+     * 监听弹窗异步渲染完成事件.
+     *
+     * @param string $script
+     *
+     * @return $this
+     */
+    public function onLoad(string $script)
+    {
+        $this->load .= "(function () { {$script} })();";
+
+        return $this;
+    }
+
     /**
      * @return string
      */
     public function getId()
     {
-        return $this->id;
+        return $this->getHtmlAttribute('id');
     }
 
     /**
@@ -263,7 +325,7 @@ class Modal extends Widget
 
         $this->script = <<<JS
 (function () {
-    var modal = $('{$this->getElementSelector()}');
+    var modal = $(replaceNestedFormIndex('{$this->getElementSelector()}'));
     {$script}
 })();
 JS;
@@ -276,13 +338,15 @@ JS;
         }
 
         $this->on('show.bs.modal', <<<JS
-var modal = $(this).find('.modal-body');
+var modal = $(this), body = modal.find('.modal-body');
 
-modal.html('<div style="min-height:150px"></div>').loading();
+body.html('<div style="min-height:150px"></div>').loading();
         
 setTimeout(function () {
     Dcat.helpers.asyncRender('{$url}', function (html) {
-        modal.html(html);
+        body.html(html);
+
+        {$this->load}
     });
 }, {$this->delay});
 JS
@@ -294,6 +358,10 @@ JS
         $this->addRenderableScript();
         $this->addEventScript();
 
+        if ($this->join) {
+            return $this->renderButton().parent::render();
+        }
+
         Admin::html(parent::render());
 
         return $this->renderButton();
@@ -302,7 +370,7 @@ JS
     public function html()
     {
         return <<<HTML
-<div class="modal fade" id="{$this->getId()}" role="dialog">
+<div {$this->formatHtmlAttributes()} role="dialog">
     <div class="modal-dialog modal-{$this->size}">
         <div class="modal-content">
             <div class="modal-header">
@@ -310,6 +378,7 @@ JS
                 <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
             </div>
             <div class="modal-body">{$this->renderContent()}</div>
+            {$this->renderFooter()}
         </div>
     </div>
 </div>
@@ -326,6 +395,19 @@ HTML;
         return Helper::render($this->content);
     }
 
+    protected function renderFooter()
+    {
+        $footer = Helper::render($this->footer);
+
+        if (! $footer) {
+            return;
+        }
+
+        return <<<HTML
+<div class="modal-footer">{$footer}</div>
+HTML;
+    }
+
     protected function renderButton()
     {
         if (! $this->button) {

+ 8 - 5
src/Widgets/Sparkline/Sparkline.php

@@ -155,13 +155,13 @@ class Sparkline extends Widget
     /**
      * @return string
      */
-    protected function script()
+    protected function addScript()
     {
         $values = json_encode($this->values);
         $options = json_encode($this->options);
 
         if (! $this->allowBuildRequest()) {
-            return <<<JS
+            return $this->script = <<<JS
 $('#{$this->getId()}').sparkline($values, $options);
 {$this->buildCombinationScript()};
 JS;
@@ -178,7 +178,7 @@ $('#'+id).sparkline(response.values || $values, opt);
 JS
         );
 
-        return $this->buildRequestScript();
+        return $this->script = $this->buildRequestScript();
     }
 
     /**
@@ -205,14 +205,17 @@ JS;
      */
     public function render()
     {
-        Admin::script($this->script());
+        $this->addScript();
 
         $this->setHtmlAttribute([
             'id' => $this->getId(),
         ]);
 
-        $this->collectAssets();
+        return parent::render();
+    }
 
+    public function html()
+    {
         return <<<HTML
 <span {$this->formatHtmlAttributes()}></span>
 HTML;

+ 158 - 0
src/Widgets/TableModal.php

@@ -0,0 +1,158 @@
+<?php
+
+namespace Dcat\Admin\Widgets;
+
+use Dcat\Admin\Grid\LazyRenderable;
+use Illuminate\Contracts\Support\Renderable;
+
+/**
+ * Class TableModal
+ *
+ * @method $this title(string $title)
+ * @method $this button(string|\Closure $title)
+ * @method $this join(bool $value = true)
+ * @method $this xl()
+ * @method $this on(string $script)
+ * @method $this onShown(string $script)
+ * @method $this onShow(string $script)
+ * @method $this onHidden(string $script)
+ * @method $this onHide(string $script)
+ * @method $this footer(string|\Closure|Renderable $footer)
+ * @method $this getId()
+ * @method $this getElementSelector()
+ */
+class TableModal extends Widget
+{
+    /**
+     * @var Modal
+     */
+    protected $modal;
+
+    /**
+     * @var AsyncTable
+     */
+    protected $table;
+
+    /**
+     * @var array
+     */
+    protected $allowMethods = [
+        'id',
+        'title',
+        'button',
+        'join',
+        'xl',
+        'on',
+        'onShown',
+        'onShow',
+        'onHidden',
+        'onHide',
+        'getId',
+        'getElementSelector',
+        'footer',
+    ];
+
+    /**
+     * @var string
+     */
+    protected $loadScript;
+
+    /**
+     * TableModal constructor.
+     *
+     * @param null $title
+     * @param \Dcat\Admin\Grid\LazyRenderable|null $renderable
+     */
+    public function __construct($title = null, LazyRenderable $renderable = null)
+    {
+        $this->modal = Modal::make()
+            ->lg()
+            ->title($title)
+            ->class('grid-modal', true);
+
+        $this->body($renderable);
+    }
+
+    /**
+     * 设置异步表格实例.
+     *
+     * @param LazyRenderable|null $renderable
+     *
+     * @return $this
+     */
+    public function body(?LazyRenderable $renderable)
+    {
+        if (! $renderable) {
+            return $this;
+        }
+
+        $this->table = AsyncTable::make($renderable, false);
+
+        $this->modal
+            ->body($this->table)
+            ->onShow($this->table->getLoadScript());
+
+        return $this;
+    }
+
+    /**
+     * 监听弹窗异步渲染完成事件.
+     *
+     * @param string $script
+     *
+     * @return $this
+     */
+    public function onLoad(string $script)
+    {
+        $this->loadScript .= $script.';';
+
+        return $this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function html()
+    {
+        if ($this->loadScript) {
+            $this->table->onLoad($this->loadScript);
+        }
+
+        $this->table->runScript($this->runScript);
+        $this->modal->runScript($this->runScript);
+
+        return $this->modal->render();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getScript()
+    {
+        return parent::getScript()
+            .$this->modal->getScript()
+            .$this->table->getScript();
+    }
+
+    public static function __callStatic($method, $arguments)
+    {
+        return static::make()->$method(...$arguments);
+    }
+
+    public function __call($method, $parameters)
+    {
+        if (in_array($method, $this->allowMethods, true)) {
+            $result = $this->modal->$method(...$parameters);
+
+            if (in_array($method, ['getElementSelector', 'getId'], true)) {
+                return $result;
+            }
+
+            return $this;
+        }
+
+        throw new \Exception(
+            sprintf('Call to undefined method "%s::%s"', static::class, $method)
+        );
+    }
+}

+ 36 - 5
src/Widgets/Widget.php

@@ -49,6 +49,11 @@ abstract class Widget implements Renderable
      */
     protected $options = [];
 
+    /**
+     * @var bool
+     */
+    protected $runScript = true;
+
     /**
      * @param mixed ...$params
      *
@@ -141,14 +146,22 @@ abstract class Widget implements Renderable
     /**
      * 收集静态资源.
      */
-    protected function collectAssets()
+    public static function collectAssets()
     {
-        $this->script && Admin::script($this->script);
-
         static::$js && Admin::js(static::$js);
         static::$css && Admin::css(static::$css);
     }
 
+    /**
+     * 运行JS.
+     */
+    protected function withScript()
+    {
+        if ($this->runScript && $this->script) {
+            Admin::script($this->script);
+        }
+    }
+
     /**
      * @param $value
      *
@@ -164,9 +177,13 @@ abstract class Widget implements Renderable
      */
     public function render()
     {
-        $this->collectAssets();
+        static::collectAssets();
+
+        $html = $this->html();
 
-        return $this->html();
+        $this->withScript();
+
+        return $html;
     }
 
     /**
@@ -205,6 +222,20 @@ abstract class Widget implements Renderable
         $this->view = $view;
     }
 
+    /**
+     * 设置是否执行JS代码.
+     *
+     * @param bool $run
+     *
+     * @return $this
+     */
+    public function runScript(bool $run = true)
+    {
+        $this->runScript = $run;
+
+        return $this;
+    }
+
     /**
      * @return string
      */