Ver Fonte

代码优化

jqh há 5 anos atrás
pai
commit
91ff0202a8
6 ficheiros alterados com 2186 adições e 2182 exclusões
  1. 1 1
      resources/views/grid/table.blade.php
  2. 921 937
      src/Grid.php
  3. 763 738
      src/Grid/Model.php
  4. 220 220
      src/Grid/Row.php
  5. 106 111
      src/Grid/Tools/Paginator.php
  6. 175 175
      src/Grid/Tools/QuickSearch.php

+ 1 - 1
resources/views/grid/table.blade.php

@@ -75,7 +75,7 @@
 
     @if ($paginator = $grid->paginator())
         <div class="box-footer clearfix " style="padding-bottom:5px;">
-            {!! $paginator !!}
+            {!! $paginator->render() !!}
         </div>
     @else
         <div class="box-footer clearfix text-80 " style="height:48px;line-height:25px;">

+ 921 - 937
src/Grid.php

@@ -1,937 +1,921 @@
-<?php
-
-namespace Dcat\Admin;
-
-use Closure;
-use Dcat\Admin\Contracts\Repository;
-use Dcat\Admin\Grid\Column;
-use Dcat\Admin\Grid\Concerns;
-use Dcat\Admin\Grid\Model;
-use Dcat\Admin\Grid\Responsive;
-use Dcat\Admin\Grid\Row;
-use Dcat\Admin\Grid\Tools;
-use Dcat\Admin\Support\Helper;
-use Dcat\Admin\Traits\HasBuilderEvents;
-use Illuminate\Contracts\Support\Htmlable;
-use Illuminate\Contracts\Support\Renderable;
-use Illuminate\Database\Eloquent\Builder;
-use Illuminate\Support\Collection;
-use Illuminate\Support\Str;
-use Illuminate\Support\Traits\Macroable;
-
-class Grid
-{
-    use HasBuilderEvents,
-        Concerns\HasNames,
-        Concerns\HasFilter,
-        Concerns\HasTools,
-        Concerns\HasActions,
-        Concerns\HasPaginator,
-        Concerns\HasExporter,
-        Concerns\HasMultipleHeaders,
-        Concerns\HasSelector,
-        Concerns\HasQuickCreate,
-        Concerns\HasQuickSearch,
-        Macroable {
-            __call as macroCall;
-        }
-
-    const CREATE_MODE_DEFAULT = 'default';
-    const CREATE_MODE_DIALOG = 'dialog';
-
-    /**
-     * The grid data model instance.
-     *
-     * @var \Dcat\Admin\Grid\Model
-     */
-    protected $model;
-
-    /**
-     * Collection of all grid columns.
-     *
-     * @var \Illuminate\Support\Collection
-     */
-    protected $columns;
-
-    /**
-     * Collection of all data rows.
-     *
-     * @var \Illuminate\Support\Collection
-     */
-    protected $rows;
-
-    /**
-     * Rows callable fucntion.
-     *
-     * @var \Closure[]
-     */
-    protected $rowsCallback = [];
-
-    /**
-     * All column names of the grid.
-     *
-     * @var array
-     */
-    protected $columnNames = [];
-
-    /**
-     * Grid builder.
-     *
-     * @var \Closure
-     */
-    protected $builder;
-
-    /**
-     * Mark if the grid is built.
-     *
-     * @var bool
-     */
-    protected $built = false;
-
-    /**
-     * All variables in grid view.
-     *
-     * @var array
-     */
-    protected $variables = [];
-
-    /**
-     * Resource path of the grid.
-     *
-     * @var
-     */
-    protected $resourcePath;
-
-    /**
-     * Default primary key name.
-     *
-     * @var string
-     */
-    protected $keyName = 'id';
-
-    /**
-     * View for grid to render.
-     *
-     * @var string
-     */
-    protected $view = 'admin::grid.table';
-
-    /**
-     * @var Closure
-     */
-    protected $header;
-
-    /**
-     * @var Closure
-     */
-    protected $footer;
-
-    /**
-     * @var Closure
-     */
-    protected $wrapper;
-
-    /**
-     * @var Responsive
-     */
-    protected $responsive;
-
-    /**
-     * @var bool
-     */
-    protected $addNumberColumn = false;
-
-    /**
-     * @var string
-     */
-    protected $tableId = 'grid-table';
-
-    /**
-     * @var Grid\Tools\RowSelector
-     */
-    protected $rowSelector;
-
-    /**
-     * Options for grid.
-     *
-     * @var array
-     */
-    protected $options = [
-        'show_pagination'        => true,
-        'show_filter'            => true,
-        'show_actions'           => true,
-        'show_quick_edit_button' => false,
-        'show_edit_button'       => true,
-        'show_view_button'       => true,
-        'show_delete_button'     => true,
-        'show_row_selector'      => true,
-        'show_create_button'     => true,
-        'show_bordered'          => false,
-        'show_toolbar'           => true,
-        'create_mode'            => self::CREATE_MODE_DEFAULT,
-        'dialog_form_area'       => ['700px', '670px'],
-        'table_header_style'     => 'table-header-gray',
-    ];
-
-    /**
-     * Create a new grid instance.
-     *
-     * Grid constructor.
-     *
-     * @param Repository|\Illuminate\Database\Eloquent\Model|Builder|null $repository
-     * @param null|\Closure                                       $builder
-     */
-    public function __construct($repository = null, ?\Closure $builder = null)
-    {
-        if ($repository) {
-            $this->keyName($repository->getKeyName());
-        }
-
-        $this->model = new Model(request(), $repository);
-        $this->columns = new Collection();
-        $this->rows = new Collection();
-        $this->builder = $builder;
-
-        $this->model()->setGrid($this);
-
-        $this->setupTools();
-        $this->setupFilter();
-
-        $this->callResolving();
-    }
-
-    /**
-     * Get table ID.
-     *
-     * @return string
-     */
-    public function tableId()
-    {
-        return $this->tableId;
-    }
-
-    /**
-     * Get or set primary key name.
-     *
-     * @param string $name
-     *
-     * @return string|void
-     */
-    public function keyName(string $name = null)
-    {
-        if ($name === null) {
-            return $this->keyName ?: 'id';
-        }
-
-        $this->keyName = $name;
-    }
-
-    /**
-     * Add column to Grid.
-     *
-     * @param string $name
-     * @param string $label
-     *
-     * @return Column
-     */
-    public function column($name, $label = '')
-    {
-        if (strpos($name, '.') !== false) {
-            [$relationName, $relationColumn] = explode('.', $name);
-
-            $label = empty($label) ? admin_trans_field($relationColumn) : $label;
-
-            $name = Str::snake($relationName).'.'.$relationColumn;
-        }
-
-        $column = $this->addColumn($name, $label);
-
-        return $column;
-    }
-
-    /**
-     * Add number column.
-     *
-     * @param null|string $label
-     *
-     * @return Column
-     */
-    public function number(?string $label = null)
-    {
-        return $this->addColumn('#', $label ?: '#')->bold();
-    }
-
-    /**
-     * Batch add column to grid.
-     *
-     * @example
-     * 1.$grid->columns(['name' => 'Name', 'email' => 'Email' ...]);
-     * 2.$grid->columns('name', 'email' ...)
-     *
-     * @param array $columns
-     *
-     * @return Collection|void
-     */
-    public function columns($columns = null)
-    {
-        if ($columns === null) {
-            return $this->columns;
-        }
-
-        if (func_num_args() == 1 && is_array($columns)) {
-            foreach ($columns as $column => $label) {
-                $this->column($column, $label);
-            }
-
-            return;
-        }
-
-        foreach (func_get_args() as $column) {
-            $this->column($column);
-        }
-    }
-
-    /**
-     * Add column to grid.
-     *
-     * @param string $field
-     * @param string $label
-     *
-     * @return Column
-     */
-    protected function addColumn($field = '', $label = '')
-    {
-        $column = $this->newColumn($field, $label);
-
-        $this->columns->put($field, $column);
-
-        return $column;
-    }
-
-    /**
-     * @param string $field
-     * @param string $label
-     *
-     * @return Column
-     */
-    public function newColumn($field = '', $label = '')
-    {
-        $column = new Column($field, $label);
-        $column->setGrid($this);
-
-        return $column;
-    }
-
-    /**
-     * Get Grid model.
-     *
-     * @return Model
-     */
-    public function model()
-    {
-        return $this->model;
-    }
-
-    /**
-     * @return array
-     */
-    public function columnNames()
-    {
-        return $this->columnNames;
-    }
-
-    /**
-     * Apply column filter to grid query.
-     */
-    protected function applyColumnFilter()
-    {
-        $this->columns->each->bindFilterQuery($this->model());
-    }
-
-    /**
-     * Build the grid.
-     *
-     * @return void
-     */
-    public function build()
-    {
-        if ($this->built) {
-            return;
-        }
-
-        $collection = $this->processFilter(false);
-
-        $data = $collection->toArray();
-
-        $this->prependRowSelectorColumn();
-        $this->appendActionsColumn();
-
-        Column::setOriginalGridModels($collection);
-
-        $this->columns->map(function (Column $column) use (&$data) {
-            $column->fill($data);
-
-            $this->columnNames[] = $column->getName();
-        });
-
-        $this->buildRows($data);
-
-        if ($data && $this->responsive) {
-            $this->responsive->build();
-        }
-
-        $this->sortHeaders();
-    }
-
-    /**
-     * @return void
-     */
-    protected function callBuilder()
-    {
-        if ($this->builder && ! $this->built) {
-            call_user_func($this->builder, $this);
-        }
-
-        $this->built = true;
-    }
-
-    /**
-     * Build the grid rows.
-     *
-     * @param array $data
-     *
-     * @return void
-     */
-    protected function buildRows(array $data)
-    {
-        $this->rows = collect($data)->map(function ($model) {
-            return new Row($this, $model);
-        });
-
-        if ($this->rowsCallback) {
-            foreach ($this->rowsCallback as $value) {
-                $value($this->rows);
-            }
-        }
-    }
-
-    /**
-     * Set grid row callback function.
-     *
-     * @param Closure $callable
-     *
-     * @return Collection|void
-     */
-    public function rows(Closure $callable = null)
-    {
-        if (is_null($callable)) {
-            return $this->rows;
-        }
-
-        $this->rowsCallback[] = $callable;
-    }
-
-    /**
-     * Get create url.
-     *
-     * @return string
-     */
-    public function createUrl()
-    {
-        $queryString = '';
-
-        if ($constraints = $this->model()->getConstraints()) {
-            $queryString = http_build_query($constraints);
-        }
-
-        return sprintf(
-            '%s/create%s',
-            $this->resource(),
-            $queryString ? ('?'.$queryString) : ''
-        );
-    }
-
-    /**
-     * @param \Closure $closure
-     *
-     * @return Grid\Tools\RowSelector
-     */
-    public function rowSelector()
-    {
-        return $this->rowSelector ?: ($this->rowSelector = new Grid\Tools\RowSelector($this));
-    }
-
-    /**
-     * Prepend checkbox column for grid.
-     *
-     * @return void
-     */
-    protected function prependRowSelectorColumn()
-    {
-        if (! $this->options['show_row_selector']) {
-            return;
-        }
-
-        $rowSelector = $this->rowSelector();
-        $keyName = $this->keyName();
-
-        $column = $this->newColumn(
-            Grid\Column::SELECT_COLUMN_NAME,
-            $rowSelector->renderHeader()
-        );
-        $column->setGrid($this);
-
-        $column->display(function () use ($rowSelector, $keyName) {
-            return $rowSelector->renderColumn($this, $this->{$keyName});
-        });
-
-        $this->columns->prepend($column, Grid\Column::SELECT_COLUMN_NAME);
-    }
-
-    /**
-     * @param string $width
-     * @param string $height
-     *
-     * @return $this
-     */
-    public function setDialogFormDimensions(string $width, string $height)
-    {
-        $this->options['dialog_form_area'] = [$width, $height];
-
-        return $this;
-    }
-
-    /**
-     * Render create button for grid.
-     *
-     * @return string
-     */
-    public function renderCreateButton()
-    {
-        if (! $this->options['show_create_button']) {
-            return '';
-        }
-
-        return (new Tools\CreateButton($this))->render();
-    }
-
-    /**
-     * @return $this
-     */
-    public function withBorder()
-    {
-        $this->options['show_bordered'] = true;
-
-        return $this;
-    }
-
-    /**
-     * Set grid header.
-     *
-     * @param Closure|string|Renderable $content
-     *
-     * @return $this|Closure
-     */
-    public function header($content = null)
-    {
-        if (! $content) {
-            return $this->header;
-        }
-
-        $this->header = $content;
-
-        return $this;
-    }
-
-    /**
-     * Render grid header.
-     *
-     * @return string
-     */
-    public function renderHeader()
-    {
-        if (! $this->header) {
-            return '';
-        }
-
-        $content = Helper::render($this->header, [$this->processFilter(false)]);
-
-        if (empty($content)) {
-            return '';
-        }
-
-        if ($content instanceof Renderable) {
-            $content = $content->render();
-        }
-
-        if ($content instanceof Htmlable) {
-            $content = $content->toHtml();
-        }
-
-        return <<<HTML
-<div class="box-header clearfix" style="border-top:1px solid #ebeff2">{$content}</div>
-HTML;
-    }
-
-    /**
-     * Set grid footer.
-     *
-     * @param Closure|string|Renderable $content
-     *
-     * @return $this|Closure
-     */
-    public function footer($content = null)
-    {
-        if (! $content) {
-            return $this->footer;
-        }
-
-        $this->footer = $content;
-
-        return $this;
-    }
-
-    /**
-     * Render grid footer.
-     *
-     * @return string
-     */
-    public function renderFooter()
-    {
-        if (! $this->footer) {
-            return '';
-        }
-
-        $content = Helper::render($this->footer, [$this->processFilter(false)]);
-
-        if (empty($content)) {
-            return '';
-        }
-
-        if ($content instanceof Renderable) {
-            $content = $content->render();
-        }
-
-        if ($content instanceof Htmlable) {
-            $content = $content->toHtml();
-        }
-
-        return <<<HTML
-    <div class="box-footer clearfix">{$content}</div>
-HTML;
-    }
-
-    /**
-     * Get or set option for grid.
-     *
-     * @param string $key
-     * @param mixed  $value
-     *
-     * @return $this|mixed
-     */
-    public function option($key, $value = null)
-    {
-        if (is_null($value)) {
-            return $this->options[$key];
-        }
-
-        $this->options[$key] = $value;
-
-        return $this;
-    }
-
-    /**
-     * Disable row selector.
-     *
-     * @return $this
-     */
-    public function disableRowSelector(bool $disable = true)
-    {
-        $this->tools->disableBatchActions($disable);
-
-        return $this->option('show_row_selector', ! $disable);
-    }
-
-    /**
-     * Show row selector.
-     *
-     * @return $this
-     */
-    public function showRowSelector(bool $val = true)
-    {
-        return $this->disableRowSelector(! $val);
-    }
-
-    /**
-     * Remove create button on grid.
-     *
-     * @return $this
-     */
-    public function disableCreateButton(bool $disable = true)
-    {
-        return $this->option('show_create_button', ! $disable);
-    }
-
-    /**
-     * Show create button.
-     *
-     * @return $this
-     */
-    public function showCreateButton(bool $val = true)
-    {
-        return $this->disableCreateButton(! $val);
-    }
-
-    /**
-     * If allow creation.
-     *
-     * @return bool
-     */
-    public function allowCreateButton()
-    {
-        return $this->options['show_create_button'];
-    }
-
-    /**
-     * @param string $mode
-     *
-     * @return $this
-     */
-    public function createMode(string $mode)
-    {
-        return $this->option('create_mode', $mode);
-    }
-
-    /**
-     * @return $this
-     */
-    public function enableDialogCreate()
-    {
-        return $this->createMode(self::CREATE_MODE_DIALOG);
-    }
-
-    /**
-     * Get or set resource path.
-     *
-     * @param string $path
-     *
-     * @return $this|string
-     */
-    public function resource(string $path = null)
-    {
-        if ($path === null) {
-            return $this->resourcePath ?: (
-            $this->resourcePath = url(app('request')->getPathInfo())
-            );
-        }
-
-        if (! empty($path)) {
-            $this->resourcePath = admin_url($path);
-        }
-
-        return $this;
-    }
-
-    /**
-     * Create a grid instance.
-     *
-     * @param mixed ...$params
-     *
-     * @return $this
-     */
-    public static function make(...$params)
-    {
-        return new static(...$params);
-    }
-
-    /**
-     * Enable responsive tables.
-     *
-     * @see https://github.com/nadangergeo/RWD-Table-Patterns
-     *
-     * @return Responsive
-     */
-    public function responsive()
-    {
-        if (! $this->responsive) {
-            $this->responsive = new Responsive($this);
-        }
-
-        return $this->responsive;
-    }
-
-    /**
-     * @return bool
-     */
-    public function allowResponsive()
-    {
-        return $this->responsive ? true : false;
-    }
-
-    /**
-     * @param Closure $closure
-     *
-     * @return $this;
-     */
-    public function wrap(\Closure $closure)
-    {
-        $this->wrapper = $closure;
-
-        return $this;
-    }
-
-    /**
-     * @return bool
-     */
-    public function hasWrapper()
-    {
-        return $this->wrapper ? true : false;
-    }
-
-    /**
-     * Add variables to grid view.
-     *
-     * @param array $variables
-     *
-     * @return $this
-     */
-    public function with($variables = [])
-    {
-        $this->variables = $variables;
-
-        return $this;
-    }
-
-    /**
-     * Get all variables will used in grid view.
-     *
-     * @return array
-     */
-    protected function variables()
-    {
-        $this->variables['grid'] = $this;
-        $this->variables['tableId'] = $this->tableId();
-
-        return $this->variables;
-    }
-
-    /**
-     * Set a view to render.
-     *
-     * @param string $view
-     * @param array  $variables
-     */
-    public function view($view, $variables = [])
-    {
-        if (! empty($variables)) {
-            $this->with($variables);
-        }
-
-        $this->view = $view;
-    }
-
-    /**
-     * Set grid title.
-     *
-     * @param string $title
-     *
-     * @return $this
-     */
-    public function title($title)
-    {
-        $this->variables['title'] = $title;
-
-        return $this;
-    }
-
-    /**
-     * Set grid description.
-     *
-     * @param string $description
-     *
-     * @return $this
-     */
-    public function description($description)
-    {
-        $this->variables['description'] = $description;
-
-        return $this;
-    }
-
-    /**
-     * Set resource path for grid.
-     *
-     * @param string $path
-     *
-     * @return $this
-     */
-    public function setResource($path)
-    {
-        $this->resourcePath = $path;
-
-        return $this;
-    }
-
-    /**
-     * Get the string contents of the grid view.
-     *
-     * @return string
-     */
-    public function render()
-    {
-        $this->handleExportRequest();
-
-        try {
-            $this->callComposing();
-
-            $this->build();
-        } catch (\Throwable $e) {
-            return Admin::makeExceptionHandler()->renderException($e);
-        }
-
-        return $this->doWrap();
-    }
-
-    /**
-     * @return string
-     */
-    protected function doWrap()
-    {
-        $view = view($this->view, $this->variables());
-
-        if (! $wrapper = $this->wrapper) {
-            return $view->render();
-        }
-
-        return $wrapper($view);
-    }
-
-    /**
-     * Add column to grid.
-     *
-     * @param string $name
-     *
-     * @return Column
-     */
-    public function __get($name)
-    {
-        return $this->addColumn($name);
-    }
-
-    /**
-     * Dynamically add columns to the grid view.
-     *
-     * @param $method
-     * @param $arguments
-     *
-     * @return Column
-     */
-    public function __call($method, $arguments)
-    {
-        if (static::hasMacro($method)) {
-            return $this->macroCall($method, $arguments);
-        }
-
-        return $this->addColumn($method, $arguments[0] ?? null);
-    }
-}
+<?php
+
+namespace Dcat\Admin;
+
+use Closure;
+use Dcat\Admin\Contracts\Repository;
+use Dcat\Admin\Grid\Column;
+use Dcat\Admin\Grid\Concerns;
+use Dcat\Admin\Grid\Model;
+use Dcat\Admin\Grid\Responsive;
+use Dcat\Admin\Grid\Row;
+use Dcat\Admin\Grid\Tools;
+use Dcat\Admin\Support\Helper;
+use Dcat\Admin\Traits\HasBuilderEvents;
+use Illuminate\Contracts\Support\Htmlable;
+use Illuminate\Contracts\Support\Renderable;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Str;
+use Illuminate\Support\Traits\Macroable;
+
+class Grid
+{
+    use HasBuilderEvents,
+        Concerns\HasNames,
+        Concerns\HasFilter,
+        Concerns\HasTools,
+        Concerns\HasActions,
+        Concerns\HasPaginator,
+        Concerns\HasExporter,
+        Concerns\HasMultipleHeaders,
+        Concerns\HasSelector,
+        Concerns\HasQuickCreate,
+        Concerns\HasQuickSearch,
+        Macroable {
+            __call as macroCall;
+        }
+
+    const CREATE_MODE_DEFAULT = 'default';
+    const CREATE_MODE_DIALOG = 'dialog';
+
+    /**
+     * The grid data model instance.
+     *
+     * @var \Dcat\Admin\Grid\Model
+     */
+    protected $model;
+
+    /**
+     * Collection of all grid columns.
+     *
+     * @var \Illuminate\Support\Collection
+     */
+    protected $columns;
+
+    /**
+     * Collection of all data rows.
+     *
+     * @var \Illuminate\Support\Collection
+     */
+    protected $rows;
+
+    /**
+     * Rows callable fucntion.
+     *
+     * @var \Closure[]
+     */
+    protected $rowsCallback = [];
+
+    /**
+     * All column names of the grid.
+     *
+     * @var array
+     */
+    protected $columnNames = [];
+
+    /**
+     * Grid builder.
+     *
+     * @var \Closure
+     */
+    protected $builder;
+
+    /**
+     * Mark if the grid is built.
+     *
+     * @var bool
+     */
+    protected $built = false;
+
+    /**
+     * All variables in grid view.
+     *
+     * @var array
+     */
+    protected $variables = [];
+
+    /**
+     * Resource path of the grid.
+     *
+     * @var
+     */
+    protected $resourcePath;
+
+    /**
+     * Default primary key name.
+     *
+     * @var string
+     */
+    protected $keyName = 'id';
+
+    /**
+     * View for grid to render.
+     *
+     * @var string
+     */
+    protected $view = 'admin::grid.table';
+
+    /**
+     * @var Closure
+     */
+    protected $header;
+
+    /**
+     * @var Closure
+     */
+    protected $footer;
+
+    /**
+     * @var Closure
+     */
+    protected $wrapper;
+
+    /**
+     * @var Responsive
+     */
+    protected $responsive;
+
+    /**
+     * @var bool
+     */
+    protected $addNumberColumn = false;
+
+    /**
+     * @var string
+     */
+    protected $tableId = 'grid-table';
+
+    /**
+     * @var Grid\Tools\RowSelector
+     */
+    protected $rowSelector;
+
+    /**
+     * Options for grid.
+     *
+     * @var array
+     */
+    protected $options = [
+        'show_pagination'        => true,
+        'show_filter'            => true,
+        'show_actions'           => true,
+        'show_quick_edit_button' => false,
+        'show_edit_button'       => true,
+        'show_view_button'       => true,
+        'show_delete_button'     => true,
+        'show_row_selector'      => true,
+        'show_create_button'     => true,
+        'show_bordered'          => false,
+        'show_toolbar'           => true,
+        'create_mode'            => self::CREATE_MODE_DEFAULT,
+        'dialog_form_area'       => ['700px', '670px'],
+        'table_header_style'     => 'table-header-gray',
+    ];
+
+    /**
+     * Create a new grid instance.
+     *
+     * Grid constructor.
+     *
+     * @param Repository|\Illuminate\Database\Eloquent\Model|Builder|null $repository
+     * @param null|\Closure                                       $builder
+     */
+    public function __construct($repository = null, ?\Closure $builder = null)
+    {
+        if ($repository) {
+            $this->keyName($repository->getKeyName());
+        }
+
+        $this->model = new Model(request(), $repository);
+        $this->columns = new Collection();
+        $this->rows = new Collection();
+        $this->builder = $builder;
+
+        $this->model()->setGrid($this);
+
+        $this->setupTools();
+        $this->setupFilter();
+
+        $this->callResolving();
+    }
+
+    /**
+     * Get table ID.
+     *
+     * @return string
+     */
+    public function tableId()
+    {
+        return $this->tableId;
+    }
+
+    /**
+     * Get or set primary key name.
+     *
+     * @param string $name
+     *
+     * @return string|void
+     */
+    public function keyName(string $name = null)
+    {
+        if ($name === null) {
+            return $this->keyName ?: 'id';
+        }
+
+        $this->keyName = $name;
+    }
+
+    /**
+     * Add column to Grid.
+     *
+     * @param string $name
+     * @param string $label
+     *
+     * @return Column
+     */
+    public function column($name, $label = '')
+    {
+        if (strpos($name, '.') !== false) {
+            [$relationName, $relationColumn] = explode('.', $name);
+
+            $label = empty($label) ? admin_trans_field($relationColumn) : $label;
+
+            $name = Str::snake($relationName).'.'.$relationColumn;
+        }
+
+        $column = $this->addColumn($name, $label);
+
+        return $column;
+    }
+
+    /**
+     * Add number column.
+     *
+     * @param null|string $label
+     *
+     * @return Column
+     */
+    public function number(?string $label = null)
+    {
+        return $this->addColumn('#', $label ?: '#')->bold();
+    }
+
+    /**
+     * Batch add column to grid.
+     *
+     * @example
+     * 1.$grid->columns(['name' => 'Name', 'email' => 'Email' ...]);
+     * 2.$grid->columns('name', 'email' ...)
+     *
+     * @param array $columns
+     *
+     * @return Collection|void
+     */
+    public function columns($columns = null)
+    {
+        if ($columns === null) {
+            return $this->columns;
+        }
+
+        if (func_num_args() == 1 && is_array($columns)) {
+            foreach ($columns as $column => $label) {
+                $this->column($column, $label);
+            }
+
+            return;
+        }
+
+        foreach (func_get_args() as $column) {
+            $this->column($column);
+        }
+    }
+
+    /**
+     * Add column to grid.
+     *
+     * @param string $field
+     * @param string $label
+     *
+     * @return Column
+     */
+    protected function addColumn($field = '', $label = '')
+    {
+        $column = $this->newColumn($field, $label);
+
+        $this->columns->put($field, $column);
+
+        return $column;
+    }
+
+    /**
+     * @param string $field
+     * @param string $label
+     *
+     * @return Column
+     */
+    public function newColumn($field = '', $label = '')
+    {
+        $column = new Column($field, $label);
+        $column->setGrid($this);
+
+        return $column;
+    }
+
+    /**
+     * Get Grid model.
+     *
+     * @return Model
+     */
+    public function model()
+    {
+        return $this->model;
+    }
+
+    /**
+     * @return array
+     */
+    public function columnNames()
+    {
+        return $this->columnNames;
+    }
+
+    /**
+     * Apply column filter to grid query.
+     */
+    protected function applyColumnFilter()
+    {
+        $this->columns->each->bindFilterQuery($this->model());
+    }
+
+    /**
+     * Build the grid.
+     *
+     * @return void
+     */
+    public function build()
+    {
+        if ($this->built) {
+            return;
+        }
+
+        $collection = $this->processFilter(false);
+
+        $data = $collection->toArray();
+
+        $this->prependRowSelectorColumn();
+        $this->appendActionsColumn();
+
+        Column::setOriginalGridModels($collection);
+
+        $this->columns->map(function (Column $column) use (&$data) {
+            $column->fill($data);
+
+            $this->columnNames[] = $column->getName();
+        });
+
+        $this->buildRows($data);
+
+        if ($data && $this->responsive) {
+            $this->responsive->build();
+        }
+
+        $this->sortHeaders();
+    }
+
+    /**
+     * @return void
+     */
+    protected function callBuilder()
+    {
+        if ($this->builder && ! $this->built) {
+            call_user_func($this->builder, $this);
+        }
+
+        $this->built = true;
+    }
+
+    /**
+     * Build the grid rows.
+     *
+     * @param array $data
+     *
+     * @return void
+     */
+    protected function buildRows(array $data)
+    {
+        $this->rows = collect($data)->map(function ($model) {
+            return new Row($this, $model);
+        });
+
+        if ($this->rowsCallback) {
+            foreach ($this->rowsCallback as $value) {
+                $value($this->rows);
+            }
+        }
+    }
+
+    /**
+     * Set grid row callback function.
+     *
+     * @param Closure $callable
+     *
+     * @return Collection|void
+     */
+    public function rows(Closure $callable = null)
+    {
+        if (is_null($callable)) {
+            return $this->rows;
+        }
+
+        $this->rowsCallback[] = $callable;
+    }
+
+    /**
+     * Get create url.
+     *
+     * @return string
+     */
+    public function createUrl()
+    {
+        $queryString = '';
+
+        if ($constraints = $this->model()->getConstraints()) {
+            $queryString = http_build_query($constraints);
+        }
+
+        return sprintf(
+            '%s/create%s',
+            $this->resource(),
+            $queryString ? ('?'.$queryString) : ''
+        );
+    }
+
+    /**
+     * @param \Closure $closure
+     *
+     * @return Grid\Tools\RowSelector
+     */
+    public function rowSelector()
+    {
+        return $this->rowSelector ?: ($this->rowSelector = new Grid\Tools\RowSelector($this));
+    }
+
+    /**
+     * Prepend checkbox column for grid.
+     *
+     * @return void
+     */
+    protected function prependRowSelectorColumn()
+    {
+        if (! $this->options['show_row_selector']) {
+            return;
+        }
+
+        $rowSelector = $this->rowSelector();
+        $keyName = $this->keyName();
+
+        $column = $this->newColumn(
+            Grid\Column::SELECT_COLUMN_NAME,
+            $rowSelector->renderHeader()
+        );
+        $column->setGrid($this);
+
+        $column->display(function () use ($rowSelector, $keyName) {
+            return $rowSelector->renderColumn($this, $this->{$keyName});
+        });
+
+        $this->columns->prepend($column, Grid\Column::SELECT_COLUMN_NAME);
+    }
+
+    /**
+     * @param string $width
+     * @param string $height
+     *
+     * @return $this
+     */
+    public function setDialogFormDimensions(string $width, string $height)
+    {
+        $this->options['dialog_form_area'] = [$width, $height];
+
+        return $this;
+    }
+
+    /**
+     * Render create button for grid.
+     *
+     * @return string
+     */
+    public function renderCreateButton()
+    {
+        if (! $this->options['show_create_button']) {
+            return '';
+        }
+
+        return (new Tools\CreateButton($this))->render();
+    }
+
+    /**
+     * @return $this
+     */
+    public function withBorder()
+    {
+        $this->options['show_bordered'] = true;
+
+        return $this;
+    }
+
+    /**
+     * Set grid header.
+     *
+     * @param Closure|string|Renderable $content
+     *
+     * @return $this|Closure
+     */
+    public function header($content = null)
+    {
+        if (! $content) {
+            return $this->header;
+        }
+
+        $this->header = $content;
+
+        return $this;
+    }
+
+    /**
+     * Render grid header.
+     *
+     * @return string
+     */
+    public function renderHeader()
+    {
+        if (! $this->header) {
+            return '';
+        }
+
+        $content = Helper::render($this->header, [$this->processFilter(false)]);
+
+        if (empty($content)) {
+            return '';
+        }
+
+        return <<<HTML
+<div class="box-header clearfix" style="border-top:1px solid #ebeff2">{$content}</div>
+HTML;
+    }
+
+    /**
+     * Set grid footer.
+     *
+     * @param Closure|string|Renderable $content
+     *
+     * @return $this|Closure
+     */
+    public function footer($content = null)
+    {
+        if (! $content) {
+            return $this->footer;
+        }
+
+        $this->footer = $content;
+
+        return $this;
+    }
+
+    /**
+     * Render grid footer.
+     *
+     * @return string
+     */
+    public function renderFooter()
+    {
+        if (! $this->footer) {
+            return '';
+        }
+
+        $content = Helper::render($this->footer, [$this->processFilter(false)]);
+
+        if (empty($content)) {
+            return '';
+        }
+
+        return <<<HTML
+<div class="box-footer clearfix">{$content}</div>
+HTML;
+    }
+
+    /**
+     * Get or set option for grid.
+     *
+     * @param string $key
+     * @param mixed  $value
+     *
+     * @return $this|mixed
+     */
+    public function option($key, $value = null)
+    {
+        if (is_null($value)) {
+            return $this->options[$key];
+        }
+
+        $this->options[$key] = $value;
+
+        return $this;
+    }
+
+    /**
+     * Disable row selector.
+     *
+     * @return $this
+     */
+    public function disableRowSelector(bool $disable = true)
+    {
+        $this->tools->disableBatchActions($disable);
+
+        return $this->option('show_row_selector', ! $disable);
+    }
+
+    /**
+     * Show row selector.
+     *
+     * @return $this
+     */
+    public function showRowSelector(bool $val = true)
+    {
+        return $this->disableRowSelector(! $val);
+    }
+
+    /**
+     * Remove create button on grid.
+     *
+     * @return $this
+     */
+    public function disableCreateButton(bool $disable = true)
+    {
+        return $this->option('show_create_button', ! $disable);
+    }
+
+    /**
+     * Show create button.
+     *
+     * @return $this
+     */
+    public function showCreateButton(bool $val = true)
+    {
+        return $this->disableCreateButton(! $val);
+    }
+
+    /**
+     * If allow creation.
+     *
+     * @return bool
+     */
+    public function allowCreateButton()
+    {
+        return $this->options['show_create_button'];
+    }
+
+    /**
+     * @param string $mode
+     *
+     * @return $this
+     */
+    public function createMode(string $mode)
+    {
+        return $this->option('create_mode', $mode);
+    }
+
+    /**
+     * @return $this
+     */
+    public function enableDialogCreate()
+    {
+        return $this->createMode(self::CREATE_MODE_DIALOG);
+    }
+
+    /**
+     * Get or set resource path.
+     *
+     * @param string $path
+     *
+     * @return $this|string
+     */
+    public function resource(string $path = null)
+    {
+        if ($path === null) {
+            return $this->resourcePath ?: (
+            $this->resourcePath = url(app('request')->getPathInfo())
+            );
+        }
+
+        if (! empty($path)) {
+            $this->resourcePath = admin_url($path);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Create a grid instance.
+     *
+     * @param mixed ...$params
+     *
+     * @return $this
+     */
+    public static function make(...$params)
+    {
+        return new static(...$params);
+    }
+
+    /**
+     * Enable responsive tables.
+     *
+     * @see https://github.com/nadangergeo/RWD-Table-Patterns
+     *
+     * @return Responsive
+     */
+    public function responsive()
+    {
+        if (! $this->responsive) {
+            $this->responsive = new Responsive($this);
+        }
+
+        return $this->responsive;
+    }
+
+    /**
+     * @return bool
+     */
+    public function allowResponsive()
+    {
+        return $this->responsive ? true : false;
+    }
+
+    /**
+     * @param Closure $closure
+     *
+     * @return $this;
+     */
+    public function wrap(\Closure $closure)
+    {
+        $this->wrapper = $closure;
+
+        return $this;
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasWrapper()
+    {
+        return $this->wrapper ? true : false;
+    }
+
+    /**
+     * Add variables to grid view.
+     *
+     * @param array $variables
+     *
+     * @return $this
+     */
+    public function with($variables = [])
+    {
+        $this->variables = $variables;
+
+        return $this;
+    }
+
+    /**
+     * Get all variables will used in grid view.
+     *
+     * @return array
+     */
+    protected function variables()
+    {
+        $this->variables['grid'] = $this;
+        $this->variables['tableId'] = $this->tableId();
+
+        return $this->variables;
+    }
+
+    /**
+     * Set a view to render.
+     *
+     * @param string $view
+     * @param array  $variables
+     */
+    public function view($view, $variables = [])
+    {
+        if (! empty($variables)) {
+            $this->with($variables);
+        }
+
+        $this->view = $view;
+    }
+
+    /**
+     * Set grid title.
+     *
+     * @param string $title
+     *
+     * @return $this
+     */
+    public function title($title)
+    {
+        $this->variables['title'] = $title;
+
+        return $this;
+    }
+
+    /**
+     * Set grid description.
+     *
+     * @param string $description
+     *
+     * @return $this
+     */
+    public function description($description)
+    {
+        $this->variables['description'] = $description;
+
+        return $this;
+    }
+
+    /**
+     * Set resource path for grid.
+     *
+     * @param string $path
+     *
+     * @return $this
+     */
+    public function setResource($path)
+    {
+        $this->resourcePath = $path;
+
+        return $this;
+    }
+
+    /**
+     * Get the string contents of the grid view.
+     *
+     * @return string
+     */
+    public function render()
+    {
+        $this->handleExportRequest();
+
+        try {
+            $this->callComposing();
+
+            $this->build();
+        } catch (\Throwable $e) {
+            return Admin::makeExceptionHandler()->renderException($e);
+        }
+
+        return $this->doWrap();
+    }
+
+    /**
+     * @return string
+     */
+    protected function doWrap()
+    {
+        $view = view($this->view, $this->variables());
+
+        if (! $wrapper = $this->wrapper) {
+            return $view->render();
+        }
+
+        return $wrapper($view);
+    }
+
+    /**
+     * Add column to grid.
+     *
+     * @param string $name
+     *
+     * @return Column
+     */
+    public function __get($name)
+    {
+        return $this->addColumn($name);
+    }
+
+    /**
+     * Dynamically add columns to the grid view.
+     *
+     * @param $method
+     * @param $arguments
+     *
+     * @return Column
+     */
+    public function __call($method, $arguments)
+    {
+        if (static::hasMacro($method)) {
+            return $this->macroCall($method, $arguments);
+        }
+
+        return $this->addColumn($method, $arguments[0] ?? null);
+    }
+}

+ 763 - 738
src/Grid/Model.php

@@ -1,738 +1,763 @@
-<?php
-
-namespace Dcat\Admin\Grid;
-
-use Dcat\Admin\Admin;
-use Dcat\Admin\Grid;
-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\Http\Request;
-use Illuminate\Pagination\AbstractPaginator;
-use Illuminate\Pagination\LengthAwarePaginator;
-use Illuminate\Support\Arr;
-use Illuminate\Support\Collection;
-use Illuminate\Support\Str;
-
-/**
- * @mixin Builder
- */
-class Model
-{
-    /**
-     * @var Request
-     */
-    protected $request;
-
-    /**
-     * @var Repository
-     */
-    protected $repository;
-
-    /**
-     * @var EloquentModel
-     */
-    protected $model;
-
-    /**
-     * Array of queries of the model.
-     *
-     * @var \Illuminate\Support\Collection
-     */
-    protected $queries;
-
-    /**
-     * Sort parameters of the model.
-     *
-     * @var array
-     */
-    protected $sort;
-
-    /**
-     * @var Collection|LengthAwarePaginator
-     */
-    protected $data = null;
-
-    /*
-     * 20 items per page as default.
-     *
-     * @var int
-     */
-    protected $perPage = 20;
-
-    /**
-     * @var string
-     */
-    protected $pageName = 'page';
-
-    /**
-     * @var int
-     */
-    protected $currentPage;
-
-    /**
-     * If the model use pagination.
-     *
-     * @var bool
-     */
-    protected $usePaginate = true;
-
-    /**
-     * The query string variable used to store the per-page.
-     *
-     * @var string
-     */
-    protected $perPageName = 'per_page';
-
-    /**
-     * The query string variable used to store the sort.
-     *
-     * @var string
-     */
-    protected $sortName = '_sort';
-
-    /**
-     * Collection callback.
-     *
-     * @var callable[]
-     */
-    protected $collectionCallback = [];
-
-    /**
-     * @var Grid
-     */
-    protected $grid;
-
-    /**
-     * @var Relation
-     */
-    protected $relation;
-
-    /**
-     * @var array
-     */
-    protected $eagerLoads = [];
-
-    /**
-     * @var array
-     */
-    protected $constraints = [];
-
-    /**
-     * Create a new grid model instance.
-     *
-     * @param Repository|\Illuminate\Database\Eloquent\Model $repository
-     * @param Request                                        $request
-     */
-    public function __construct(Request $request, $repository = null)
-    {
-        if ($repository) {
-            $this->repository = Admin::repository($repository);
-        }
-
-        $this->request = $request;
-        $this->queries = collect();
-    }
-
-    /**
-     * @return Repository|null
-     */
-    public function getRepository()
-    {
-        return $this->repository;
-    }
-
-    /**
-     * @return Collection
-     */
-    public function getQueries()
-    {
-        return $this->queries = $this->queries->unique();
-    }
-
-    /**
-     * @param Collection $query
-     *
-     * @return void
-     */
-    public function setQueries(Collection $query)
-    {
-        $this->queries = $query;
-    }
-
-    /**
-     * @return LengthAwarePaginator|Collection
-     */
-    public function paginator()
-    {
-        return $this->get();
-    }
-
-    /**
-     * Get primary key name of model.
-     *
-     * @return string
-     */
-    public function getKeyName()
-    {
-        return $this->grid->keyName();
-    }
-
-    /**
-     * Enable or disable pagination.
-     *
-     * @param bool $use
-     */
-    public function usePaginate($use = true)
-    {
-        $this->usePaginate = $use;
-    }
-
-    /**
-     * Get the query string variable used to store the per-page.
-     *
-     * @return string
-     */
-    public function getPerPageName()
-    {
-        return $this->perPageName;
-    }
-
-    /**
-     * Set the query string variable used to store the per-page.
-     *
-     * @param string $name
-     *
-     * @return $this
-     */
-    public function setPerPageName($name)
-    {
-        $this->perPageName = $name;
-
-        return $this;
-    }
-
-    /**
-     * @param int $perPage
-     */
-    public function setPerPage(int $perPage)
-    {
-        $this->perPage = $perPage;
-
-        return $this;
-    }
-
-    /**
-     * @param string $pageName
-     */
-    public function setPageName(string $pageName)
-    {
-        $this->pageName = $pageName;
-
-        return $this;
-    }
-
-    /**
-     * @return string
-     */
-    public function getPageName()
-    {
-        return $this->pageName;
-    }
-
-    /**
-     * Get the query string variable used to store the sort.
-     *
-     * @return string
-     */
-    public function getSortName()
-    {
-        return $this->sortName;
-    }
-
-    /**
-     * Set the query string variable used to store the sort.
-     *
-     * @param string $name
-     *
-     * @return $this
-     */
-    public function setSortName($name)
-    {
-        $this->sortName = $name;
-
-        return $this;
-    }
-
-    /**
-     * Set parent grid instance.
-     *
-     * @param Grid $grid
-     *
-     * @return $this
-     */
-    public function setGrid(Grid $grid)
-    {
-        $this->grid = $grid;
-
-        return $this;
-    }
-
-    /**
-     * Get parent gird instance.
-     *
-     * @return Grid
-     */
-    public function grid()
-    {
-        return $this->grid;
-    }
-
-    /**
-     * Get filter of Grid.
-     *
-     * @return Filter
-     */
-    public function filter()
-    {
-        return $this->grid->filter();
-    }
-
-    /**
-     * Get constraints.
-     *
-     * @return array|bool
-     */
-    public function getConstraints()
-    {
-        return $this->constraints;
-    }
-
-    /**
-     * @param array $constraints
-     *
-     * @return $this
-     */
-    public function setConstraints(array $constraints)
-    {
-        $this->constraints = $constraints;
-
-        return $this;
-    }
-
-    /**
-     * Set collection callback.
-     *
-     * @param callable $callback
-     *
-     * @return $this
-     */
-    public function collection(callable $callback = null)
-    {
-        $this->collectionCallback[] = $callback;
-
-        return $this;
-    }
-
-    /**
-     * Build.
-     *
-     * @param bool $toArray
-     *
-     * @return array|Collection|mixed
-     */
-    public function buildData(bool $toArray = true)
-    {
-        if (is_null($this->data) || $this->data instanceof \Closure) {
-            $this->setData($this->get());
-        }
-
-        return $toArray ? $this->data->toArray() : $this->data;
-    }
-
-    /**
-     * @param Collection|\Closure|array $data
-     */
-    public function setData($data)
-    {
-        if ($this->collectionCallback) {
-            foreach ($this->collectionCallback as $cb) {
-                $data = call_user_func($cb, $data);
-            }
-        }
-
-        if (
-            ($isA = is_array($data))
-            || $data instanceof Collection
-            || $data instanceof \Closure
-            || ($isP = $data instanceof AbstractPaginator)
-        ) {
-            if ($isA) {
-                $data = collect($data);
-            } elseif (! empty($isP)) {
-                $this->model = $data;
-                $this->data = $data->getCollection();
-
-                return;
-            }
-
-            $this->data = $data;
-        }
-    }
-
-    /**
-     * Add conditions to grid model.
-     *
-     * @param array $conditions
-     *
-     * @return $this
-     */
-    public function addConditions(array $conditions)
-    {
-        foreach ($conditions as $condition) {
-            call_user_func_array([$this, key($condition)], current($condition));
-        }
-
-        return $this;
-    }
-
-    /**
-     * @throws \Exception
-     *
-     * @return Collection|LengthAwarePaginator
-     */
-    public function get()
-    {
-        if (
-            $this->model instanceof LengthAwarePaginator
-            || $this->model instanceof Collection
-        ) {
-            return $this->model;
-        }
-
-        $this->setSort();
-        $this->setPaginate();
-
-        if ($this->data instanceof \Closure) {
-            $data = $this->data;
-            $this->data = collect();
-
-            return $this->model = $data($this);
-        }
-
-        $this->model = $this->repository->get($this);
-
-        if (is_array($this->model)) {
-            $this->model = collect($this->model);
-        }
-
-        if ($this->model instanceof Collection) {
-            return $this->model;
-        }
-
-        if ($this->model instanceof LengthAwarePaginator) {
-            $this->model->setPageName($this->pageName);
-
-            $this->handleInvalidPage($this->model);
-
-            return $this->model->getCollection();
-        }
-
-        throw new \Exception('Grid query error');
-    }
-
-    /**
-     * If current page is greater than last page, then redirect to last page.
-     *
-     * @param LengthAwarePaginator $paginator
-     *
-     * @return void
-     */
-    protected function handleInvalidPage(LengthAwarePaginator $paginator)
-    {
-        if (
-            $this->usePaginate
-            && $paginator->lastPage()
-            && $paginator->currentPage() > $paginator->lastPage()
-        ) {
-            $lastPageUrl = $this->request->fullUrlWithQuery([
-                $paginator->getPageName() => $paginator->lastPage(),
-            ]);
-
-            Pjax::respond(redirect($lastPageUrl));
-        }
-    }
-
-    /**
-     * Get current page.
-     *
-     * @return int|null
-     */
-    public function getCurrentPage()
-    {
-        if (! $this->usePaginate) {
-            return;
-        }
-
-        return $this->currentPage ?: ($this->currentPage = ($this->request->get($this->pageName) ?: 1));
-    }
-
-    /**
-     * @param int $currentPage
-     */
-    public function setCurrentPage(int $currentPage)
-    {
-        $this->currentPage = $currentPage;
-
-        return $this;
-    }
-
-    /**
-     * Get items number of per page.
-     *
-     * @return int|null
-     */
-    public function getPerPage()
-    {
-        if (! $this->usePaginate) {
-            return;
-        }
-
-        return $this->request->get($this->perPageName) ?: $this->perPage;
-    }
-
-    /**
-     * Set the grid paginate.
-     *
-     * @return void
-     */
-    protected function setPaginate()
-    {
-        $paginate = $this->findQueryByMethod('paginate');
-
-        $this->queries = $this->queries->reject(function ($query) {
-            return $query['method'] == 'paginate';
-        });
-
-        if (! $this->usePaginate) {
-            $query = [
-                'method'    => 'get',
-                'arguments' => [],
-            ];
-        } else {
-            $query = [
-                'method'    => 'paginate',
-                'arguments' => $this->resolvePerPage($paginate),
-            ];
-        }
-
-        $this->queries->push($query);
-    }
-
-    /**
-     * Resolve perPage for pagination.
-     *
-     * @param array|null $paginate
-     *
-     * @return array
-     */
-    protected function resolvePerPage($paginate)
-    {
-        if ($perPage = app('request')->input($this->perPageName)) {
-            if (is_array($paginate)) {
-                $paginate['arguments'][0] = (int) $perPage;
-
-                return $paginate['arguments'];
-            }
-
-            $this->perPage = (int) $perPage;
-        }
-
-        return [$this->perPage, '*', $this->pageName, $this->getCurrentPage()];
-    }
-
-    /**
-     * Find query by method name.
-     *
-     * @param $method
-     *
-     * @return static
-     */
-    public function findQueryByMethod($method)
-    {
-        return $this->queries->first(function ($query) use ($method) {
-            return $query['method'] == $method;
-        });
-    }
-
-    /**
-     * Get the grid sort.
-     *
-     * @return array exp: ['name', 'desc']
-     */
-    public function getSort()
-    {
-        if (empty($this->sort['column']) || empty($this->sort['type'])) {
-            return [null, null];
-        }
-
-        return [$this->sort['column'], $this->sort['type']];
-    }
-
-    /**
-     * Set the grid sort.
-     *
-     * @return void
-     */
-    protected function setSort()
-    {
-        $this->sort = $this->request->get($this->sortName, []);
-
-        if (empty($this->sort['column']) || empty($this->sort['type'])) {
-            return;
-        }
-
-        if (Str::contains($this->sort['column'], '.')) {
-            $this->setRelationSort($this->sort['column']);
-        } else {
-            $this->resetOrderBy();
-
-            $this->queries->push([
-                'method'    => 'orderBy',
-                'arguments' => [$this->sort['column'], $this->sort['type']],
-            ]);
-        }
-    }
-
-    /**
-     * Set relation sort.
-     *
-     * @param string $column
-     *
-     * @return void
-     */
-    protected function setRelationSort($column)
-    {
-        [$relationName, $relationColumn] = explode('.', $column);
-
-        if ($this->queries->contains(function ($query) use ($relationName) {
-            return $query['method'] == 'with' && in_array($relationName, $query['arguments']);
-        })) {
-            $this->queries->push([
-                'method'    => 'select',
-                'arguments' => ['*'],
-            ]);
-
-            $this->resetOrderBy();
-
-            $this->queries->push([
-                'method'    => 'orderBy',
-                'arguments' => [
-                    $relationColumn,
-                    $this->sort['type'],
-                ],
-            ]);
-        }
-    }
-
-    /**
-     * @param string|array $method
-     *
-     * @return void
-     */
-    public function rejectQueries($method)
-    {
-        $method = (array) $method;
-
-        $this->queries = $this->queries->reject(function ($query) use ($method) {
-            return in_array($query['method'], $method);
-        });
-    }
-
-    /**
-     * Reset orderBy query.
-     *
-     * @return void
-     */
-    public function resetOrderBy()
-    {
-        $this->rejectQueries(['orderBy', 'orderByDesc']);
-    }
-
-    /**
-     * @param string $method
-     * @param array  $arguments
-     *
-     * @return $this
-     */
-    public function __call($method, $arguments)
-    {
-        $this->queries->push([
-            'method'    => $method,
-            'arguments' => $arguments,
-        ]);
-
-        return $this;
-    }
-
-    /**
-     * Set the relationships that should be eager loaded.
-     *
-     * @param mixed $relations
-     *
-     * @return $this|Model
-     */
-    public function with($relations)
-    {
-        if (is_array($relations)) {
-            if (Arr::isAssoc($relations)) {
-                $relations = array_keys($relations);
-            }
-
-            $this->eagerLoads = array_merge($this->eagerLoads, $relations);
-        }
-
-        if (is_string($relations)) {
-            if (Str::contains($relations, '.')) {
-                $relations = explode('.', $relations)[0];
-            }
-
-            if (Str::contains($relations, ':')) {
-                $relations = explode(':', $relations)[0];
-            }
-
-            if (in_array($relations, $this->eagerLoads)) {
-                return $this;
-            }
-
-            $this->eagerLoads[] = $relations;
-        }
-
-        return $this->__call('with', (array) $relations);
-    }
-
-    /**
-     * @param $key
-     *
-     * @return mixed
-     */
-    public function __get($key)
-    {
-        $data = $this->buildData();
-
-        if (array_key_exists($key, $data)) {
-            return $data[$key];
-        }
-    }
-
-    /**
-     * @return void
-     */
-    public function reset()
-    {
-        $this->data = null;
-        $this->model = null;
-    }
-}
+<?php
+
+namespace Dcat\Admin\Grid;
+
+use Dcat\Admin\Admin;
+use Dcat\Admin\Grid;
+use Dcat\Admin\Middleware\Pjax;
+use Dcat\Admin\Repositories\Repository;
+use Illuminate\Contracts\Support\Arrayable;
+use Illuminate\Database\Eloquent\Relations\Relation;
+use Illuminate\Database\Query\Builder;
+use Illuminate\Http\Request;
+use Illuminate\Pagination\AbstractPaginator;
+use Illuminate\Pagination\LengthAwarePaginator;
+use Illuminate\Support\Arr;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Str;
+
+/**
+ * @mixin Builder
+ */
+class Model
+{
+    /**
+     * @var Request
+     */
+    protected $request;
+
+    /**
+     * @var Repository
+     */
+    protected $repository;
+
+    /**
+     * @var AbstractPaginator
+     */
+    protected $paginator;
+
+    /**
+     * Array of queries of the model.
+     *
+     * @var \Illuminate\Support\Collection
+     */
+    protected $queries;
+
+    /**
+     * Sort parameters of the model.
+     *
+     * @var array
+     */
+    protected $sort;
+
+    /**
+     * @var Collection
+     */
+    protected $data;
+
+    /**
+     * @var callable
+     */
+    protected $builder;
+
+    /*
+     * 20 items per page as default.
+     *
+     * @var int
+     */
+    protected $perPage = 20;
+
+    /**
+     * @var string
+     */
+    protected $pageName = 'page';
+
+    /**
+     * @var int
+     */
+    protected $currentPage;
+
+    /**
+     * If the model use pagination.
+     *
+     * @var bool
+     */
+    protected $usePaginate = true;
+
+    /**
+     * The query string variable used to store the per-page.
+     *
+     * @var string
+     */
+    protected $perPageName = 'per_page';
+
+    /**
+     * The query string variable used to store the sort.
+     *
+     * @var string
+     */
+    protected $sortName = '_sort';
+
+    /**
+     * Collection callback.
+     *
+     * @var callable[]
+     */
+    protected $collectionCallback = [];
+
+    /**
+     * @var Grid
+     */
+    protected $grid;
+
+    /**
+     * @var Relation
+     */
+    protected $relation;
+
+    /**
+     * @var array
+     */
+    protected $eagerLoads = [];
+
+    /**
+     * @var array
+     */
+    protected $constraints = [];
+
+    /**
+     * Create a new grid model instance.
+     *
+     * @param Repository|\Illuminate\Database\Eloquent\Model $repository
+     * @param Request                                        $request
+     */
+    public function __construct(Request $request, $repository = null)
+    {
+        if ($repository) {
+            $this->repository = Admin::repository($repository);
+        }
+
+        $this->request = $request;
+        $this->queries = collect();
+    }
+
+    /**
+     * @return Repository|null
+     */
+    public function getRepository()
+    {
+        return $this->repository;
+    }
+
+    /**
+     * @return Collection
+     */
+    public function getQueries()
+    {
+        return $this->queries = $this->queries->unique();
+    }
+
+    /**
+     * @param Collection $query
+     *
+     * @return void
+     */
+    public function setQueries(Collection $query)
+    {
+        $this->queries = $query;
+    }
+
+    /**
+     * @return AbstractPaginator
+     */
+    public function paginator(): AbstractPaginator
+    {
+        $this->buildData();
+
+        return $this->paginator;
+    }
+
+    /**
+     * @param int              $total
+     * @param Collection|array $data
+     *
+     * @return LengthAwarePaginator
+     */
+    public function makePaginator($total, $data, string $url = null)
+    {
+        $paginator = new LengthAwarePaginator(
+            $data,
+            $total,
+            $this->getPerPage(), // 传入每页显示行数
+            $this->getCurrentPage() // 传入当前页码
+        );
+
+        return $paginator->setPath(
+            $url ?: url()->current()
+        );
+    }
+
+    /**
+     * Get primary key name of model.
+     *
+     * @return string
+     */
+    public function getKeyName()
+    {
+        return $this->grid->keyName();
+    }
+
+    /**
+     * Enable or disable pagination.
+     *
+     * @param bool $use
+     */
+    public function usePaginate($use = true)
+    {
+        $this->usePaginate = $use;
+    }
+
+    /**
+     * Get the query string variable used to store the per-page.
+     *
+     * @return string
+     */
+    public function getPerPageName()
+    {
+        return $this->perPageName;
+    }
+
+    /**
+     * Set the query string variable used to store the per-page.
+     *
+     * @param string $name
+     *
+     * @return $this
+     */
+    public function setPerPageName($name)
+    {
+        $this->perPageName = $name;
+
+        return $this;
+    }
+
+    /**
+     * @param int $perPage
+     */
+    public function setPerPage(int $perPage)
+    {
+        $this->perPage = $perPage;
+
+        return $this;
+    }
+
+    /**
+     * @param string $pageName
+     */
+    public function setPageName(string $pageName)
+    {
+        $this->pageName = $pageName;
+
+        return $this;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPageName()
+    {
+        return $this->pageName;
+    }
+
+    /**
+     * Get the query string variable used to store the sort.
+     *
+     * @return string
+     */
+    public function getSortName()
+    {
+        return $this->sortName;
+    }
+
+    /**
+     * Set the query string variable used to store the sort.
+     *
+     * @param string $name
+     *
+     * @return $this
+     */
+    public function setSortName($name)
+    {
+        $this->sortName = $name;
+
+        return $this;
+    }
+
+    /**
+     * Set parent grid instance.
+     *
+     * @param Grid $grid
+     *
+     * @return $this
+     */
+    public function setGrid(Grid $grid)
+    {
+        $this->grid = $grid;
+
+        return $this;
+    }
+
+    /**
+     * Get parent gird instance.
+     *
+     * @return Grid
+     */
+    public function grid()
+    {
+        return $this->grid;
+    }
+
+    /**
+     * Get filter of Grid.
+     *
+     * @return Filter
+     */
+    public function filter()
+    {
+        return $this->grid->filter();
+    }
+
+    /**
+     * Get constraints.
+     *
+     * @return array|bool
+     */
+    public function getConstraints()
+    {
+        return $this->constraints;
+    }
+
+    /**
+     * @param array $constraints
+     *
+     * @return $this
+     */
+    public function setConstraints(array $constraints)
+    {
+        $this->constraints = $constraints;
+
+        return $this;
+    }
+
+    /**
+     * Set collection callback.
+     *
+     * @param callable $callback
+     *
+     * @return $this
+     */
+    public function collection(callable $callback = null)
+    {
+        $this->collectionCallback[] = $callback;
+
+        return $this;
+    }
+
+    /**
+     * Build.
+     *
+     * @param bool $toArray
+     *
+     * @return array|Collection|mixed
+     */
+    public function buildData(bool $toArray = false)
+    {
+        if (is_null($this->data)) {
+            $this->setData($this->fetch());
+        }
+
+        return $toArray ? $this->data->toArray() : $this->data;
+    }
+
+    /**
+     * @param Collection|callable|array|AbstractPaginator $data
+     *
+     * @return $this
+     */
+    public function setData($data)
+    {
+        if (is_callable($data)) {
+            $this->builder = $data;
+
+            return $this;
+        }
+
+        if ($this->collectionCallback) {
+            foreach ($this->collectionCallback as $cb) {
+                $data = call_user_func($cb, $this->data);
+            }
+        }
+
+        if ($data instanceof AbstractPaginator) {
+            $this->setPaginator($data);
+
+            $data = $data->getCollection();
+        } elseif (is_array($data)) {
+            $data = collect($data);
+        } elseif ($data instanceof Arrayable) {
+            $data = collect($data->toArray());
+        }
+
+        if ($data instanceof Collection) {
+            $this->data = $data;
+        } else {
+            $this->data = collect();
+        }
+
+        return $this;
+    }
+
+    /**
+     * Add conditions to grid model.
+     *
+     * @param array $conditions
+     *
+     * @return $this
+     */
+    public function addConditions(array $conditions)
+    {
+        foreach ($conditions as $condition) {
+            call_user_func_array([$this, key($condition)], current($condition));
+        }
+
+        return $this;
+    }
+
+    /**
+     * @throws \Exception
+     *
+     * @return Collection|array
+     */
+    protected function fetch()
+    {
+        if ($this->paginator) {
+            return $this->paginator->getCollection();
+        }
+
+        $this->setSort();
+        $this->setPaginate();
+
+        if ($this->builder && is_callable($this->builder)) {;
+            $results = call_user_func($this->builder, $this);
+        } else {
+            $results = $this->repository->get($this);
+        }
+
+        if (is_array($results) || $results instanceof Collection) {
+            return $results;
+        }
+
+        if ($results instanceof AbstractPaginator) {
+            $this->setPaginator($results);
+
+            return $results->getCollection();
+        }
+
+        throw new \Exception('Grid query error');
+    }
+
+    /**
+     * @param AbstractPaginator $paginator
+     *
+     * @return void
+     */
+    protected function setPaginator(AbstractPaginator $paginator)
+    {
+        $this->paginator = $paginator;
+
+        $paginator->setPageName($this->pageName);
+
+        if ($paginator instanceof LengthAwarePaginator) {
+            $this->handleInvalidPage($paginator);
+        }
+    }
+
+    /**
+     * If current page is greater than last page, then redirect to last page.
+     *
+     * @param LengthAwarePaginator $paginator
+     *
+     * @return void
+     */
+    protected function handleInvalidPage(LengthAwarePaginator $paginator)
+    {
+        if (
+            $this->usePaginate
+            && $paginator->lastPage()
+            && $paginator->currentPage() > $paginator->lastPage()
+        ) {
+            $lastPageUrl = $this->request->fullUrlWithQuery([
+                $paginator->getPageName() => $paginator->lastPage(),
+            ]);
+
+            Pjax::respond(redirect($lastPageUrl));
+        }
+    }
+
+    /**
+     * Get current page.
+     *
+     * @return int|null
+     */
+    public function getCurrentPage()
+    {
+        if (! $this->usePaginate) {
+            return;
+        }
+
+        return $this->currentPage ?: ($this->currentPage = ($this->request->get($this->pageName) ?: 1));
+    }
+
+    /**
+     * @param int $currentPage
+     */
+    public function setCurrentPage(int $currentPage)
+    {
+        $this->currentPage = $currentPage;
+
+        return $this;
+    }
+
+    /**
+     * Get items number of per page.
+     *
+     * @return int|null
+     */
+    public function getPerPage()
+    {
+        if (! $this->usePaginate) {
+            return;
+        }
+
+        return $this->request->get($this->perPageName) ?: $this->perPage;
+    }
+
+    /**
+     * Set the grid paginate.
+     *
+     * @return void
+     */
+    protected function setPaginate()
+    {
+        $paginate = $this->findQueryByMethod('paginate');
+
+        $this->queries = $this->queries->reject(function ($query) {
+            return $query['method'] == 'paginate';
+        });
+
+        if (! $this->usePaginate) {
+            $query = [
+                'method'    => 'get',
+                'arguments' => [],
+            ];
+        } else {
+            $query = [
+                'method'    => 'paginate',
+                'arguments' => $this->resolvePerPage($paginate),
+            ];
+        }
+
+        $this->queries->push($query);
+    }
+
+    /**
+     * Resolve perPage for pagination.
+     *
+     * @param array|null $paginate
+     *
+     * @return array
+     */
+    protected function resolvePerPage($paginate)
+    {
+        if ($perPage = app('request')->input($this->perPageName)) {
+            if (is_array($paginate)) {
+                $paginate['arguments'][0] = (int) $perPage;
+
+                return $paginate['arguments'];
+            }
+
+            $this->perPage = (int) $perPage;
+        }
+
+        return [$this->perPage, '*', $this->pageName, $this->getCurrentPage()];
+    }
+
+    /**
+     * Find query by method name.
+     *
+     * @param $method
+     *
+     * @return static
+     */
+    public function findQueryByMethod($method)
+    {
+        return $this->queries->first(function ($query) use ($method) {
+            return $query['method'] == $method;
+        });
+    }
+
+    /**
+     * Get the grid sort.
+     *
+     * @return array exp: ['name', 'desc']
+     */
+    public function getSort()
+    {
+        if (empty($this->sort['column']) || empty($this->sort['type'])) {
+            return [null, null];
+        }
+
+        return [$this->sort['column'], $this->sort['type']];
+    }
+
+    /**
+     * Set the grid sort.
+     *
+     * @return void
+     */
+    protected function setSort()
+    {
+        $this->sort = $this->request->get($this->sortName, []);
+
+        if (empty($this->sort['column']) || empty($this->sort['type'])) {
+            return;
+        }
+
+        if (Str::contains($this->sort['column'], '.')) {
+            $this->setRelationSort($this->sort['column']);
+        } else {
+            $this->resetOrderBy();
+
+            $this->queries->push([
+                'method'    => 'orderBy',
+                'arguments' => [$this->sort['column'], $this->sort['type']],
+            ]);
+        }
+    }
+
+    /**
+     * Set relation sort.
+     *
+     * @param string $column
+     *
+     * @return void
+     */
+    protected function setRelationSort($column)
+    {
+        [$relationName, $relationColumn] = explode('.', $column);
+
+        if ($this->queries->contains(function ($query) use ($relationName) {
+            return $query['method'] == 'with' && in_array($relationName, $query['arguments']);
+        })) {
+            $this->queries->push([
+                'method'    => 'select',
+                'arguments' => ['*'],
+            ]);
+
+            $this->resetOrderBy();
+
+            $this->queries->push([
+                'method'    => 'orderBy',
+                'arguments' => [
+                    $relationColumn,
+                    $this->sort['type'],
+                ],
+            ]);
+        }
+    }
+
+    /**
+     * @param string|array $method
+     *
+     * @return void
+     */
+    public function rejectQueries($method)
+    {
+        $method = (array) $method;
+
+        $this->queries = $this->queries->reject(function ($query) use ($method) {
+            return in_array($query['method'], $method);
+        });
+    }
+
+    /**
+     * Reset orderBy query.
+     *
+     * @return void
+     */
+    public function resetOrderBy()
+    {
+        $this->rejectQueries(['orderBy', 'orderByDesc']);
+    }
+
+    /**
+     * @param string $method
+     * @param array  $arguments
+     *
+     * @return $this
+     */
+    public function __call($method, $arguments)
+    {
+        $this->queries->push([
+            'method'    => $method,
+            'arguments' => $arguments,
+        ]);
+
+        return $this;
+    }
+
+    /**
+     * Set the relationships that should be eager loaded.
+     *
+     * @param mixed $relations
+     *
+     * @return $this|Model
+     */
+    public function with($relations)
+    {
+        if (is_array($relations)) {
+            if (Arr::isAssoc($relations)) {
+                $relations = array_keys($relations);
+            }
+
+            $this->eagerLoads = array_merge($this->eagerLoads, $relations);
+        }
+
+        if (is_string($relations)) {
+            if (Str::contains($relations, '.')) {
+                $relations = explode('.', $relations)[0];
+            }
+
+            if (Str::contains($relations, ':')) {
+                $relations = explode(':', $relations)[0];
+            }
+
+            if (in_array($relations, $this->eagerLoads)) {
+                return $this;
+            }
+
+            $this->eagerLoads[] = $relations;
+        }
+
+        return $this->__call('with', (array) $relations);
+    }
+
+    /**
+     * @return void
+     */
+    public function reset()
+    {
+        $this->data = null;
+        $this->model = null;
+    }
+}

+ 220 - 220
src/Grid/Row.php

@@ -1,220 +1,220 @@
-<?php
-
-namespace Dcat\Admin\Grid;
-
-use Closure;
-use Dcat\Admin\Grid;
-use Dcat\Admin\Support\Helper;
-use Illuminate\Contracts\Support\Arrayable;
-use Illuminate\Contracts\Support\Htmlable;
-use Illuminate\Contracts\Support\Jsonable;
-use Illuminate\Contracts\Support\Renderable;
-use Illuminate\Support\Fluent;
-
-class Row implements Arrayable
-{
-    /**
-     * @var Grid
-     */
-    protected $grid;
-
-    /**
-     * Row data.
-     *
-     * @var Fluent
-     */
-    protected $data;
-
-    /**
-     * Attributes of row.
-     *
-     * @var array
-     */
-    protected $attributes = [];
-
-    public function __construct(Grid $grid, $data)
-    {
-        $this->grid = $grid;
-        $this->data = new Fluent(Helper::array($data));
-    }
-
-    /**
-     * Get the value of the model's primary key.
-     *
-     * @return mixed
-     */
-    public function key()
-    {
-        return $this->data->get($this->grid->keyName());
-    }
-
-    /**
-     * Get attributes in html format.
-     *
-     * @return string
-     */
-    public function rowAttributes()
-    {
-        return $this->formatHtmlAttributes($this->attributes);
-    }
-
-    /**
-     * Get column attributes.
-     *
-     * @param string $column
-     *
-     * @return string
-     */
-    public function columnAttributes($column)
-    {
-        if (
-            ($column = $this->grid->columns()->get($column))
-            && ($attributes = $column->getAttributes())
-        ) {
-            return $this->formatHtmlAttributes($attributes);
-        }
-
-        return '';
-    }
-
-    /**
-     * Format attributes to html.
-     *
-     * @param array $attributes
-     *
-     * @return string
-     */
-    private function formatHtmlAttributes($attributes = [])
-    {
-        $attrArr = [];
-        foreach ($attributes as $name => $val) {
-            $attrArr[] = "$name=\"$val\"";
-        }
-
-        return implode(' ', $attrArr);
-    }
-
-    /**
-     * Set attributes.
-     *
-     * @param array $attributes
-     */
-    public function setAttributes(array $attributes)
-    {
-        $this->attributes = $attributes;
-    }
-
-    /**
-     * Set style of the row.
-     *
-     * @param array|string $style
-     */
-    public function style($style)
-    {
-        if (is_array($style)) {
-            $style = implode('', array_map(function ($key, $val) {
-                return "$key:$val";
-            }, array_keys($style), array_values($style)));
-        }
-
-        if (is_string($style)) {
-            $this->attributes['style'] = $style;
-        }
-    }
-
-    /**
-     * Get data of this row.
-     *
-     * @return mixed
-     */
-    public function model()
-    {
-        return $this->data;
-    }
-
-    /**
-     * Getter.
-     *
-     * @param mixed $attr
-     *
-     * @return mixed
-     */
-    public function __get($attr)
-    {
-        return $this->data->get($attr);
-    }
-
-    /**
-     * Setter.
-     *
-     * @param mixed $attr
-     * @param mixed $value
-     *
-     * @return void
-     */
-    public function __set($attr, $value)
-    {
-        $this->data[$attr] = $value;
-    }
-
-    /**
-     * Get or set value of column in this row.
-     *
-     * @param string $name
-     * @param mixed  $value
-     *
-     * @return $this|mixed
-     */
-    public function column($name, $value = null)
-    {
-        if (is_null($value)) {
-            $column = $this->data->get($name);
-
-            return $this->output($column);
-        }
-
-        if ($value instanceof Closure) {
-            $value = $value->call($this, $this->column($name));
-        }
-
-        $this->data[$name] = $value;
-
-        return $this;
-    }
-
-    /**
-     * @return array
-     */
-    public function toArray()
-    {
-        return $this->data->toArray();
-    }
-
-    /**
-     * Output column value.
-     *
-     * @param mixed $value
-     *
-     * @return mixed|string
-     */
-    protected function output($value)
-    {
-        if ($value instanceof Renderable) {
-            $value = $value->render();
-        }
-
-        if ($value instanceof Htmlable) {
-            $value = $value->toHtml();
-        }
-
-        if ($value instanceof Jsonable) {
-            $value = $value->toJson();
-        }
-
-        if (! is_null($value) && ! is_scalar($value)) {
-            return sprintf('<pre class="dump">%s</pre>', json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
-        }
-
-        return $value;
-    }
-}
+<?php
+
+namespace Dcat\Admin\Grid;
+
+use Closure;
+use Dcat\Admin\Grid;
+use Dcat\Admin\Support\Helper;
+use Illuminate\Contracts\Support\Arrayable;
+use Illuminate\Contracts\Support\Htmlable;
+use Illuminate\Contracts\Support\Jsonable;
+use Illuminate\Contracts\Support\Renderable;
+use Illuminate\Support\Fluent;
+
+class Row implements Arrayable
+{
+    /**
+     * @var Grid
+     */
+    protected $grid;
+
+    /**
+     * Row data.
+     *
+     * @var Fluent
+     */
+    protected $data;
+
+    /**
+     * Attributes of row.
+     *
+     * @var array
+     */
+    protected $attributes = [];
+
+    public function __construct(Grid $grid, $data)
+    {
+        $this->grid = $grid;
+        $this->data = new Fluent(Helper::array($data));
+    }
+
+    /**
+     * Get the value of the model's primary key.
+     *
+     * @return mixed
+     */
+    public function key()
+    {
+        return $this->data->get($this->grid->keyName());
+    }
+
+    /**
+     * Get attributes in html format.
+     *
+     * @return string
+     */
+    public function rowAttributes()
+    {
+        return $this->formatHtmlAttributes($this->attributes);
+    }
+
+    /**
+     * Get column attributes.
+     *
+     * @param string $column
+     *
+     * @return string
+     */
+    public function columnAttributes($column)
+    {
+        if (
+            ($column = $this->grid->columns()->get($column))
+            && ($attributes = $column->getAttributes())
+        ) {
+            return $this->formatHtmlAttributes($attributes);
+        }
+
+        return '';
+    }
+
+    /**
+     * Format attributes to html.
+     *
+     * @param array $attributes
+     *
+     * @return string
+     */
+    private function formatHtmlAttributes($attributes = [])
+    {
+        $attrArr = [];
+        foreach ($attributes as $name => $val) {
+            $attrArr[] = "$name=\"$val\"";
+        }
+
+        return implode(' ', $attrArr);
+    }
+
+    /**
+     * Set attributes.
+     *
+     * @param array $attributes
+     */
+    public function setAttributes(array $attributes)
+    {
+        $this->attributes = $attributes;
+    }
+
+    /**
+     * Set style of the row.
+     *
+     * @param array|string $style
+     */
+    public function style($style)
+    {
+        if (is_array($style)) {
+            $style = implode('', array_map(function ($key, $val) {
+                return "$key:$val";
+            }, array_keys($style), array_values($style)));
+        }
+
+        if (is_string($style)) {
+            $this->attributes['style'] = $style;
+        }
+    }
+
+    /**
+     * Get data of this row.
+     *
+     * @return Fluent
+     */
+    public function model()
+    {
+        return $this->data;
+    }
+
+    /**
+     * Getter.
+     *
+     * @param mixed $attr
+     *
+     * @return mixed
+     */
+    public function __get($attr)
+    {
+        return $this->data->get($attr);
+    }
+
+    /**
+     * Setter.
+     *
+     * @param mixed $attr
+     * @param mixed $value
+     *
+     * @return void
+     */
+    public function __set($attr, $value)
+    {
+        $this->data[$attr] = $value;
+    }
+
+    /**
+     * Get or set value of column in this row.
+     *
+     * @param string $name
+     * @param mixed  $value
+     *
+     * @return $this|mixed
+     */
+    public function column($name, $value = null)
+    {
+        if (is_null($value)) {
+            $column = $this->data->get($name);
+
+            return $this->output($column);
+        }
+
+        if ($value instanceof Closure) {
+            $value = $value->call($this, $this->column($name));
+        }
+
+        $this->data[$name] = $value;
+
+        return $this;
+    }
+
+    /**
+     * @return array
+     */
+    public function toArray()
+    {
+        return $this->data->toArray();
+    }
+
+    /**
+     * Output column value.
+     *
+     * @param mixed $value
+     *
+     * @return mixed|string
+     */
+    protected function output($value)
+    {
+        if ($value instanceof Renderable) {
+            $value = $value->render();
+        }
+
+        if ($value instanceof Htmlable) {
+            $value = $value->toHtml();
+        }
+
+        if ($value instanceof Jsonable) {
+            $value = $value->toJson();
+        }
+
+        if (! is_null($value) && ! is_scalar($value)) {
+            return sprintf('<pre class="dump">%s</pre>', json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
+        }
+
+        return $value;
+    }
+}

+ 106 - 111
src/Grid/Tools/Paginator.php

@@ -1,111 +1,106 @@
-<?php
-
-namespace Dcat\Admin\Grid\Tools;
-
-use Dcat\Admin\Grid;
-use Dcat\Admin\Support\Helper;
-use Dcat\Admin\Widgets\Color;
-use Illuminate\Contracts\Support\Renderable;
-use Illuminate\Pagination\LengthAwarePaginator;
-
-class Paginator implements Renderable
-{
-    /**
-     * @var Grid
-     */
-    protected $grid;
-
-    /**
-     * @var \Illuminate\Pagination\LengthAwarePaginator
-     */
-    protected $paginator = null;
-
-    /**
-     * Create a new Paginator instance.
-     *
-     * @param Grid $grid
-     */
-    public function __construct(Grid $grid)
-    {
-        $this->grid = $grid;
-
-        $this->initPaginator();
-    }
-
-    /**
-     * Initialize work for Paginator.
-     *
-     * @return void
-     */
-    protected function initPaginator()
-    {
-        $this->paginator = $this->grid->model()->paginator();
-
-        if ($this->paginator instanceof LengthAwarePaginator) {
-            $this->paginator->appends(request()->all());
-        }
-    }
-
-    /**
-     * Get Pagination links.
-     *
-     * @return string
-     */
-    protected function paginationLinks()
-    {
-        return $this->paginator->render('admin::pagination');
-    }
-
-    /**
-     * Get per-page selector.
-     *
-     * @return PerPageSelector|null
-     */
-    protected function perPageSelector()
-    {
-        if (! $this->grid->getPerPages()) {
-            return;
-        }
-
-        return (new PerPageSelector($this->grid))->render();
-    }
-
-    /**
-     * Get range infomation of paginator.
-     *
-     * @return string|\Symfony\Component\Translation\TranslatorInterface
-     */
-    protected function paginationRanger()
-    {
-        $parameters = [
-            'first' => $this->paginator->firstItem(),
-            'last'  => $this->paginator->lastItem(),
-            'total' => $this->paginator->total(),
-        ];
-
-        $parameters = collect($parameters)->flatMap(function ($parameter, $key) {
-            return [$key => "<b>$parameter</b>"];
-        });
-
-        $color = Color::dark80();
-
-        return "<span style=\"line-height:33px;color:{$color}\">".trans('admin.pagination.range', $parameters->all()).'</span>';
-    }
-
-    /**
-     * Render Paginator.
-     *
-     * @return string
-     */
-    public function render()
-    {
-        return $this->paginationRanger().
-            $this->paginationLinks().
-            $this->perPageSelector();
-    }
-
-    public function __toString()
-    {
-        return Helper::render($this->render());
-    }
-}
+<?php
+
+namespace Dcat\Admin\Grid\Tools;
+
+use Dcat\Admin\Grid;
+use Dcat\Admin\Support\Helper;
+use Dcat\Admin\Widgets\Color;
+use Illuminate\Contracts\Support\Renderable;
+use Illuminate\Pagination\LengthAwarePaginator;
+
+class Paginator implements Renderable
+{
+    /**
+     * @var Grid
+     */
+    protected $grid;
+
+    /**
+     * @var \Illuminate\Pagination\LengthAwarePaginator
+     */
+    protected $paginator = null;
+
+    /**
+     * Create a new Paginator instance.
+     *
+     * @param Grid $grid
+     */
+    public function __construct(Grid $grid)
+    {
+        $this->grid = $grid;
+
+        $this->initPaginator();
+    }
+
+    /**
+     * Initialize work for Paginator.
+     *
+     * @return void
+     */
+    protected function initPaginator()
+    {
+        $this->paginator = $this->grid->model()->paginator();
+
+        if ($this->paginator instanceof LengthAwarePaginator) {
+            $this->paginator->appends(request()->all());
+        }
+    }
+
+    /**
+     * Get Pagination links.
+     *
+     * @return string
+     */
+    protected function paginationLinks()
+    {
+        return $this->paginator->render('admin::pagination');
+    }
+
+    /**
+     * Get per-page selector.
+     *
+     * @return string|null
+     */
+    protected function perPageSelector()
+    {
+        if (! $this->grid->getPerPages()) {
+            return;
+        }
+
+        return (new PerPageSelector($this->grid))->render();
+    }
+
+    /**
+     * Get range infomation of paginator.
+     *
+     * @return string|\Symfony\Component\Translation\TranslatorInterface
+     */
+    protected function paginationRanger()
+    {
+        $parameters = [
+            'first' => $this->paginator->firstItem(),
+            'last'  => $this->paginator->lastItem(),
+            'total' => $this->paginator->total(),
+        ];
+
+        $parameters = collect($parameters)->flatMap(function ($parameter, $key) {
+            return [$key => "<b>$parameter</b>"];
+        });
+
+        $color = Color::dark80();
+
+        return "<span style=\"line-height:33px;color:{$color}\">".trans('admin.pagination.range', $parameters->all()).'</span>';
+    }
+
+    /**
+     * Render Paginator.
+     *
+     * @return string
+     */
+    public function render()
+    {
+        return $this->paginationRanger().
+            $this->paginationLinks().
+            $this->perPageSelector();
+    }
+}

+ 175 - 175
src/Grid/Tools/QuickSearch.php

@@ -1,175 +1,175 @@
-<?php
-
-namespace Dcat\Admin\Grid\Tools;
-
-use Dcat\Admin\Admin;
-use Dcat\Admin\Grid;
-use Illuminate\Support\Arr;
-
-class QuickSearch extends AbstractTool
-{
-    /**
-     * @var string
-     */
-    protected $view = 'admin::grid.quick-search';
-
-    /**
-     * @var string
-     */
-    protected $placeholder = null;
-
-    /**
-     * @var string
-     */
-    protected $queryName = '__search__';
-
-    /**
-     * @var int rem
-     */
-    protected $width = 29;
-
-    public function __construct($key = null, $title = null)
-    {
-        parent::__construct($key, $title);
-    }
-
-    /**
-     * @param string|null $name
-     *
-     * @return $this
-     */
-    public function setQueryName(?string $name)
-    {
-        $this->queryName = $name;
-
-        return $this;
-    }
-
-    public function setGrid(Grid $grid)
-    {
-        $grid->setQuickSearchQueryName();
-
-        return parent::setGrid($grid);
-    }
-
-    /**
-     * @return string
-     */
-    public function queryName()
-    {
-        return $this->queryName;
-    }
-
-    /**
-     * @param int $width
-     *
-     * @return $this
-     */
-    public function width(int $width)
-    {
-        $this->width = $width;
-
-        return $this;
-    }
-
-    /**
-     * Set placeholder.
-     *
-     * @param string $text
-     *
-     * @return $this
-     */
-    public function placeholder(?string $text = '')
-    {
-        $this->placeholder = $text;
-
-        return $this;
-    }
-
-    /**
-     * @return string
-     */
-    public function value()
-    {
-        return trim(request($this->queryName));
-    }
-
-    /**
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
-     */
-    public function render()
-    {
-        $request = request();
-        $query = $request->query();
-
-        $this->setupScript();
-
-        Arr::forget($query, [
-            $this->queryName,
-            $this->parent->model()->getPageName(),
-            '_pjax',
-        ]);
-
-        $vars = [
-            'action'      => $request->url().'?'.http_build_query($query),
-            'key'         => $this->queryName,
-            'value'       => $this->value(),
-            'placeholder' => $this->placeholder ?: trans('admin.search'),
-            'width'       => $this->width,
-        ];
-
-        return view($this->view, $vars);
-    }
-
-    protected function setupScript()
-    {
-        $script = <<<'JS'
-(function () {
-    var inputting = false,
-        $ipt = $('input.quick-search-input'), 
-        val = $ipt.val(),
-        ignoreKeys = [16, 17, 18, 20, 35, 36, 37, 38, 39, 40, 45, 144];
-    
-    var submit = LA.debounce(function (input) {
-        inputting || $(input).parents('form').submit()
-    }, 600);
-    
-    function toggleBtn() {
-        var t = $(this),
-            btn = t.parent().find('.quick-search-clear');
-    
-        if (t.val()) {
-            btn.css({color: '#333'});
-        } else {
-            btn.css({color: '#fff'});
-        }
-        return false;
-    }
-    
-    $ipt.on('focus', toggleBtn)
-        .on('keyup', function (e) {
-            toggleBtn.apply(this);
-            
-            ignoreKeys.indexOf(e.keyCode) == -1 && submit(this)
-        })
-        .on('mousemove', toggleBtn)
-        .on('mouseout', toggleBtn)
-        .on('compositionstart', function(){
-            inputting = true
-        })
-        .on('compositionend', function() {
-            inputting = false
-        });
-    val !== '' && $ipt.val('').focus().val(val);
-    
-    $('.quick-search-clear').click(function () {
-        $(this).parent().find('.quick-search-input').val('');
-    
-        $(this).closest('form').submit();
-    });
-})()
-JS;
-
-        Admin::script($script);
-    }
-}
+<?php
+
+namespace Dcat\Admin\Grid\Tools;
+
+use Dcat\Admin\Admin;
+use Dcat\Admin\Grid;
+use Illuminate\Support\Arr;
+
+class QuickSearch extends AbstractTool
+{
+    /**
+     * @var string
+     */
+    protected $view = 'admin::grid.quick-search';
+
+    /**
+     * @var string
+     */
+    protected $placeholder = null;
+
+    /**
+     * @var string
+     */
+    protected $queryName = '__search__';
+
+    /**
+     * @var int rem
+     */
+    protected $width = 28;
+
+    public function __construct($key = null, $title = null)
+    {
+        parent::__construct($key, $title);
+    }
+
+    /**
+     * @param string|null $name
+     *
+     * @return $this
+     */
+    public function setQueryName(?string $name)
+    {
+        $this->queryName = $name;
+
+        return $this;
+    }
+
+    public function setGrid(Grid $grid)
+    {
+        $grid->setQuickSearchQueryName();
+
+        return parent::setGrid($grid);
+    }
+
+    /**
+     * @return string
+     */
+    public function queryName()
+    {
+        return $this->queryName;
+    }
+
+    /**
+     * @param int $width
+     *
+     * @return $this
+     */
+    public function width(int $width)
+    {
+        $this->width = $width;
+
+        return $this;
+    }
+
+    /**
+     * Set placeholder.
+     *
+     * @param string $text
+     *
+     * @return $this
+     */
+    public function placeholder(?string $text = '')
+    {
+        $this->placeholder = $text;
+
+        return $this;
+    }
+
+    /**
+     * @return string
+     */
+    public function value()
+    {
+        return trim(request($this->queryName));
+    }
+
+    /**
+     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+     */
+    public function render()
+    {
+        $request = request();
+        $query = $request->query();
+
+        $this->setupScript();
+
+        Arr::forget($query, [
+            $this->queryName,
+            $this->parent->model()->getPageName(),
+            '_pjax',
+        ]);
+
+        $vars = [
+            'action'      => $request->url().'?'.http_build_query($query),
+            'key'         => $this->queryName,
+            'value'       => $this->value(),
+            'placeholder' => $this->placeholder ?: trans('admin.search'),
+            'width'       => $this->width,
+        ];
+
+        return view($this->view, $vars);
+    }
+
+    protected function setupScript()
+    {
+        $script = <<<'JS'
+(function () {
+    var inputting = false,
+        $ipt = $('input.quick-search-input'), 
+        val = $ipt.val(),
+        ignoreKeys = [16, 17, 18, 20, 35, 36, 37, 38, 39, 40, 45, 144];
+    
+    var submit = LA.debounce(function (input) {
+        inputting || $(input).parents('form').submit()
+    }, 600);
+    
+    function toggleBtn() {
+        var t = $(this),
+            btn = t.parent().find('.quick-search-clear');
+    
+        if (t.val()) {
+            btn.css({color: '#333'});
+        } else {
+            btn.css({color: '#fff'});
+        }
+        return false;
+    }
+    
+    $ipt.on('focus', toggleBtn)
+        .on('keyup', function (e) {
+            toggleBtn.apply(this);
+            
+            ignoreKeys.indexOf(e.keyCode) == -1 && submit(this)
+        })
+        .on('mousemove', toggleBtn)
+        .on('mouseout', toggleBtn)
+        .on('compositionstart', function(){
+            inputting = true
+        })
+        .on('compositionend', function() {
+            inputting = false
+        });
+    val !== '' && $ipt.val('').focus().val(val);
+    
+    $('.quick-search-clear').click(function () {
+        $(this).parent().find('.quick-search-input').val('');
+    
+        $(this).closest('form').submit();
+    });
+})()
+JS;
+
+        Admin::script($script);
+    }
+}