You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

765 lines
23 KiB

5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
  1. <?php
  2. /*
  3. * The MIT License
  4. *
  5. * Copyright 2019 Blobt.
  6. *
  7. * Permission is hereby granted, free of charge, to any person obtaining a copy
  8. * of this software and associated documentation files (the "Software"), to deal
  9. * in the Software without restriction, including without limitation the rights
  10. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. * copies of the Software, and to permit persons to whom the Software is
  12. * furnished to do so, subject to the following conditions:
  13. *
  14. * The above copyright notice and this permission notice shall be included in
  15. * all copies or substantial portions of the Software.
  16. *
  17. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. * THE SOFTWARE.
  24. */
  25. namespace iron\grid;
  26. use Closure;
  27. use Yii;
  28. use yii\base\InvalidConfigException;
  29. use yii\base\Model;
  30. use yii\helpers\Html;
  31. use yii\helpers\Json;
  32. use yii\helpers\Url;
  33. use yii\helpers\ArrayHelper;
  34. use yii\i18n\Formatter;
  35. use yii\widgets\BaseListView;
  36. use iron\web\GridViewAsset;
  37. use blobt\grid\DataColumn;
  38. /**
  39. * @author Blobt
  40. * @email 380255922@qq.com
  41. * @created Aug 13, 2019
  42. */
  43. class GridView extends BaseListView
  44. {
  45. /**
  46. * @var string 渲染列数据的类,默认是'yii\grid\DataColumn'
  47. */
  48. public $dataColumnClass;
  49. /**
  50. * @var array 表格说明的html属性
  51. */
  52. public $captionOptions = [];
  53. /**
  54. * @var array 表格外层div的属性
  55. */
  56. public $options = ['class' => 'card'];
  57. /**
  58. * @var array table的html属性
  59. */
  60. public $tableOptions = ['class' => 'table table-bordered table-striped dataTable'];
  61. /**
  62. * @var array 表格头部html属性
  63. */
  64. public $headerRowOptions = [];
  65. /**
  66. * @var array 表格脚部html属性
  67. */
  68. public $footerRowOptions = [];
  69. /**
  70. * @var array|Cloure 表格每一行的html属性
  71. * 这个参数除了可以是一个options数组外,还可以是一个匿名函数,该函数必须返回一个options数组,
  72. * 渲染每一行都会调用该函数
  73. * 该函数必须遵循以下声明规则
  74. * ```php
  75. * function ($model, $key, $index, $grid)
  76. * ```
  77. *
  78. * - `$model`: 每行的模型
  79. * - `$key`: id值
  80. * - `$index`: [[dataProvider]]提供的索引号
  81. * - `$grid`: GridView 对象
  82. */
  83. public $rowOptions = [];
  84. /**
  85. * @var Closure an 一个匿名函数(结构和[[rowOptions]]一样),每行渲染前后都会被调用
  86. */
  87. public $beforeRow;
  88. public $afterRow;
  89. /**
  90. * @var bool 是否显示表格头
  91. */
  92. public $showHeader = true;
  93. /**
  94. * @var bool 是否显示表格脚
  95. */
  96. public $showFooter = false;
  97. /**
  98. * @var bool 没有数据情况下是否显示
  99. */
  100. public $showOnEmpty = true;
  101. /**
  102. * @var array|Formatter 用来格式化输出属性值
  103. */
  104. public $formatter;
  105. /**
  106. * @var string 摘要的显示样式
  107. *
  108. *
  109. * - `{begin}`: 开始条数
  110. * - `{end}`: 结束条数
  111. * - `{count}`: 显示条数
  112. * - `{totalCount}`: 总记录条数
  113. * - `{page}`: 显示分页
  114. * - `{pageCount}`: 总分页数
  115. * - `{select}`: 显示页数
  116. */
  117. public $summary = "{select} 显示{begin}~{end}条 共{totalCount}条";
  118. /**
  119. * @var array 摘要的html属性
  120. */
  121. public $summaryOptions = ['class' => 'summary'];
  122. /**
  123. * @var array 列配置数组. 数组每一项代表一个列,列数组可以包括class、attribute、format、label等。
  124. * 例子:
  125. * ```php
  126. * [
  127. * ['class' => SerialColumn::className()],
  128. * [
  129. * 'class' => DataColumn::className(), //渲染用到的类,没一列都默认使用[[DataColumn]]渲染,所以这里可以忽略
  130. * 'attribute' => 'name', //代表每一行的数据原
  131. * 'format' => 'text', //输出的格式
  132. * 'label' => 'Name', //label
  133. * '' => ''
  134. * ],
  135. * ['class' => CheckboxColumn::className()],
  136. * ]
  137. * ```
  138. *
  139. * 当然,也支持简写成这样:[[DataColumn::attribute|attribute]], [[DataColumn::format|format]],
  140. * [[DataColumn::label|label]] options: `"attribute:format:label"`.
  141. * 所以上面例子的 "name" 列能简写成这样 : `"name:text:Name"`.
  142. * 甚至"format""label"都是可以不制定的,因为它们都有默认值。
  143. *
  144. * 其实大多数情况下都可以使用简写:
  145. *
  146. * ```php
  147. * [
  148. * 'id',
  149. * 'amount:currency:Total Amount',
  150. * 'created_at:datetime',
  151. * ]
  152. * ```
  153. *
  154. * [[dataProvider]]提供active records, 且active record又和其它 active record建立了关联关系的,
  155. * 例如 the `name` 属性是 `author` 关联,那么你可以这样制定数据:
  156. *
  157. * ```php
  158. * // shortcut syntax
  159. * 'author.name',
  160. * // full syntax
  161. * [
  162. * 'attribute' => 'author.name',
  163. * // ...
  164. * ]
  165. * ```
  166. */
  167. public $columns = [];
  168. /**
  169. * @var string 当单元格数据为空时候显示的内容。
  170. */
  171. public $emptyCell = '&nbsp;';
  172. /**
  173. * @var string TODO:这个目前用来做页数选择,具体原理没有研究清楚
  174. */
  175. public $filterSelector = 'select[name="per-page"]';
  176. /**
  177. * @var type
  178. */
  179. public $filter;
  180. /**
  181. * @var array 批量操作的选项
  182. */
  183. public $batch;
  184. /**
  185. * @var string 表格的layout:
  186. *
  187. * - `{summary}`: 摘要.
  188. * - `{items}`: 表格项.
  189. * - `{pager}`: 分页.
  190. * - `{batch}`: 批量处理
  191. */
  192. public $layout = <<< HTML
  193. <div class="card-body">
  194. <div id="example2_wrapper" class="dataTables_wrapper dt-bootstrap4">
  195. <div class="row">
  196. {filter}
  197. </div>
  198. <div class="row">
  199. <div class="col-sm-12 col-md-6">
  200. {batch}
  201. {create}
  202. <!-- <a href="#" data-url='export' class="export btn btn-default"><i class="fa fa-file-excel-o"></i>导出</a>-->
  203. {export}
  204. <!-- <button type="button" id="export" class="btn btn-default"><i class="fa fa-file-excel-o"></i>导出</button>-->
  205. {content}
  206. </div>
  207. <!-- <div class="col-sm-12 col-md-6">-->
  208. <!-- {filter}-->
  209. <!-- </div>-->
  210. </div>
  211. <div class="row">
  212. <div class="col-sm-12">
  213. {items}
  214. </div>
  215. </div>
  216. <div class="row">
  217. <div class="col-sm-12 col-md-5">
  218. <div class="dataTables_length" id="example2_info" role="status" aria-live="polite">
  219. {summary}
  220. </div>
  221. </div>
  222. <div class="col-sm-12 col-md-7">
  223. <div class="dataTables_paginate paging_simple_numbers">
  224. {pager}
  225. </div>
  226. </div>
  227. </div>
  228. </div>
  229. </div>
  230. HTML;
  231. public $batchTemplate = <<< HTML
  232. <div class="btn-group">
  233. <button type="button" class="btn btn-default btn checkbox-toggle"><i class="far fa-square"></i></button>
  234. <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false">批量操作</button>
  235. <ul class="dropdown-menu" role="menu">
  236. {items}
  237. </ul>
  238. </div>
  239. HTML;
  240. public $export =<<<HTML
  241. <div class="btn-group">
  242. <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false"><i class="fas fa-file-upload mr-2"></i>导出</button>
  243. <ul class="dropdown-menu" role="menu">
  244. <li> <a class="dropdown-item export-page" href="#" data-url="{url}/export">本页</a></li>
  245. <li> <a class="dropdown-item export-all" href="#" data-url="{url}/export">全部</a></li>
  246. </ul>
  247. </div>
  248. HTML;
  249. public $create =<<<HTML
  250. <a href="create" class="btn btn-default"><i class="fas fa-plus-square mr-2"></i>添加</a>
  251. HTML;
  252. /**
  253. * @var
  254. * 表单头部内容
  255. */
  256. public $content;
  257. /**
  258. * 初始化 grid view.
  259. * 初始化必须的属性和每个列对象
  260. * @return
  261. */
  262. public function init()
  263. {
  264. parent::init();
  265. if ($this->formatter === null) {
  266. $this->formatter = Yii::$app->getFormatter();
  267. } elseif (is_array($this->formatter)) {
  268. $this->formatter = Yii::createObject($this->formatter);
  269. }
  270. if (!$this->formatter instanceof Formatter) {
  271. throw new InvalidConfigException('The "formatter" property must be either a Format object or a configuration array.');
  272. }
  273. $this->pager = [
  274. 'options' => ['class' => ['justify-content-end', 'pagination']],
  275. 'linkOptions' => ['class' => 'page-link'],
  276. 'pageCssClass' => 'paginate_button page-item',
  277. 'disabledPageCssClass' => 'page-link disabled',
  278. 'firstPageLabel' => '&laquo;',
  279. 'prevPageLabel' => '&lsaquo;',
  280. 'nextPageLabel' => '&rsaquo;',
  281. 'lastPageLabel' => '&raquo;',];
  282. $this->initColumns();
  283. }
  284. public function run()
  285. {
  286. $view = $this->getView();
  287. GridViewAsset::register($view);
  288. $this->registerGridJs();
  289. $this->registerIcheckJs();
  290. $this->registerConfirmJs();
  291. $this->registerExportJs();
  292. $this->registerCss();
  293. parent::run();
  294. }
  295. /**
  296. * 注册GridView Js
  297. */
  298. protected function registerGridJs()
  299. {
  300. $options = Json::htmlEncode(['filterUrl' => Url::to(Yii::$app->request->url),
  301. 'filterSelector' => $this->filterSelector]);
  302. $id = $this->options['id'];
  303. $this->getView()->registerJs("jQuery('#$id').yiiGridView($options);");
  304. }
  305. /**
  306. * 注册icheck Js
  307. */
  308. protected function registerIcheckJs()
  309. {
  310. $js = <<<SCRIPT
  311. $('.dataTable input[type="checkbox"]').iCheck({
  312. checkboxClass: 'icheckbox_flat-blue',
  313. radioClass: 'iradio_flat-blue'
  314. });
  315. $(".checkbox-toggle").click(function () {
  316. var clicks = $(this).data('clicks');
  317. if (clicks) {
  318. //Uncheck all checkboxes
  319. $(".dataTable input[type='checkbox']").iCheck("uncheck");
  320. $(".far", this).removeClass("fa-check-square").addClass('fa-square');
  321. } else {
  322. //Check all checkboxes
  323. $(".dataTable input[type='checkbox']").iCheck("check");
  324. $(".far", this).removeClass("fa-square").addClass('fa-check-square');
  325. }
  326. $(this).data("clicks", !clicks);
  327. });
  328. SCRIPT;
  329. $this->getView()->registerJs($js);
  330. }
  331. /**
  332. * 注册批量操作js
  333. */
  334. protected function registerBatchJs()
  335. {
  336. $js = <<<SCRIPT
  337. $("a.batch_item").click(function(){
  338. var url = $(this).data("url");
  339. var act = $(this).text();
  340. var selected = [];
  341. $(".checked input").each(function(){
  342. selected.push($(this).val());
  343. });
  344. if(selected.length > 0){
  345. alertify.confirm('系统提示', "确定执行批量 '"+act+"' 操作?", function(){
  346. $.ajax({
  347. type: "POST",
  348. url: url,
  349. traditional:true,
  350. data:{ 'ids[]':selected},
  351. dataType: "json",
  352. async:false
  353. });
  354. window.location.reload();
  355. },function(){
  356. });
  357. }
  358. return false;
  359. })
  360. SCRIPT;
  361. $this->getView()->registerJs($js);
  362. }
  363. protected function registerConfirmJs()
  364. {
  365. $js = <<<SCRIPT
  366. $("a[alertify-confirm]").click(function(){
  367. var message = $(this).attr('alertify-confirm');
  368. var url = $(this).attr('href');
  369. var id = $(this).data('id');
  370. alertify.confirm('系统提示', message,function(){
  371. $.ajax({
  372. type: "POST",
  373. url: url,
  374. traditional:true,
  375. data:{ id:id },
  376. dataType: "json",
  377. async:false
  378. });
  379. window.location.reload();
  380. },function(){
  381. });
  382. return false;
  383. });
  384. SCRIPT;
  385. $this->getView()->registerJs($js);
  386. }
  387. protected function registerExportJs()
  388. {
  389. $js = <<<SCRIPT
  390. $("a.export-all").click(function(url){
  391. var url = $(this).data("url");
  392. if(!location.search){
  393. window.location.replace(url+"?page-type=all");
  394. }else{
  395. window.location.replace(url+location.search+"&page-type=all");
  396. }
  397. });
  398. $("a.export-page").click(function(url){
  399. var url = $(this).data("url")+location.search;
  400. if(!location.search){
  401. window.location.replace(url+"?page-type=page");
  402. }else{
  403. window.location.replace(url+location.search+"&page-type=page");
  404. }
  405. });
  406. SCRIPT;
  407. $this->getView()->registerJs($js);
  408. }
  409. /**
  410. * 渲染局部
  411. * @return string|bool
  412. */
  413. public function renderSection($name)
  414. {
  415. switch ($name) {
  416. case '{summary}':
  417. return $this->renderSummary();
  418. case '{items}':
  419. return $this->renderItems();
  420. case '{pager}':
  421. return $this->renderPager();
  422. case '{sorter}':
  423. return $this->renderSorter();
  424. case '{filter}':
  425. return $this->renderFilter();
  426. case '{batch}':
  427. return $this->renderBatch();
  428. case '{url}':
  429. return Yii::$app->request->url;
  430. case '{export}':
  431. return $this->renderExport();
  432. case '{create}':
  433. return $this->renderCreate();
  434. case '{content}':
  435. return $this->renderContent();
  436. default:
  437. return false;
  438. }
  439. }
  440. /**
  441. * 渲染表格的html真实table
  442. * @return string
  443. */
  444. public function renderItems()
  445. {
  446. $tableHeader = $this->showHeader ? $this->renderTableHeader() : false;
  447. $tableBody = $this->renderTableBody();
  448. $content = array_filter([
  449. $tableHeader,
  450. $tableBody
  451. ]);
  452. return Html::tag('table', implode("\n", $content), $this->tableOptions);
  453. }
  454. /**
  455. * 初始化每列
  456. * @throws InvalidConfigException
  457. */
  458. protected function initColumns()
  459. {
  460. if (empty($this->columns)) {
  461. throw new InvalidConfigException('The "columns" property must be set.');
  462. }
  463. foreach ($this->columns as $i => $column) {
  464. if (is_string($column)) {
  465. $column = $this->createDataColumn($column);
  466. } else {
  467. $column = Yii::createObject(array_merge([
  468. 'class' => $this->dataColumnClass ?: DataColumn::className(),
  469. 'grid' => $this,
  470. ], $column));
  471. }
  472. if (!$column->visible) {
  473. unset($this->columns[$i]);
  474. continue;
  475. }
  476. $this->columns[$i] = $column;
  477. }
  478. }
  479. /**
  480. * 渲染表头
  481. * @return string
  482. */
  483. public function renderTableHeader()
  484. {
  485. $cells = [];
  486. foreach ($this->columns as $column) {
  487. /* @var $column Column */
  488. $cells[] = $column->renderHeaderCell();
  489. }
  490. $content = Html::tag('tr', implode('', $cells), $this->headerRowOptions);
  491. return "<thead>\n" . $content . "\n</thead>";
  492. }
  493. /**
  494. * 渲染表格体
  495. * @return string
  496. */
  497. public function renderTableBody()
  498. {
  499. $models = $this->dataProvider->getModels();
  500. $keys = $this->dataProvider->getKeys();
  501. $rows = [];
  502. foreach ($models as $index => $model) {
  503. $key = $keys[$index];
  504. if ($this->beforeRow !== null) {
  505. $row = call_user_func($this->beforeRow, $model, $key, $index, $this);
  506. if (!empty($row)) {
  507. $rows[] = $row;
  508. }
  509. }
  510. $rows[] = $this->renderTableRow($model, $key, $index);
  511. if ($this->afterRow !== null) {
  512. $row = call_user_func($this->afterRow, $model, $key, $index, $this);
  513. if (!empty($row)) {
  514. $rows[] = $row;
  515. }
  516. }
  517. }
  518. if (empty($rows) && $this->emptyText !== false) {
  519. $colspan = count($this->columns);
  520. return "<tbody>\n<tr><td colspan=\"$colspan\">" . $this->renderEmpty() . "</td></tr>\n</tbody>";
  521. }
  522. return "<tbody>\n" . implode("\n", $rows) . "\n</tbody>";
  523. }
  524. /**
  525. * 渲染表格的每行
  526. * @param Objetc $model
  527. * @param int $key
  528. * @param int $index
  529. * @return string
  530. */
  531. public function renderTableRow($model, $key, $index)
  532. {
  533. $cells = [];
  534. foreach ($this->columns as $column) {
  535. $cells[] = $column->renderDataCell($model, $key, $index);
  536. }
  537. if ($this->rowOptions instanceof Closure) {
  538. $options = call_user_func($this->rowOptions, $model, $key, $index, $this);
  539. } else {
  540. $options = $this->rowOptions;
  541. }
  542. $options['data-key'] = is_array($key) ? json_encode($key) : (string)$key;
  543. //TODO 各行变色放到这里不合理
  544. if ($index % 2 == 0) {
  545. $oddEven = 'odd';
  546. } else {
  547. $oddEven = 'even';
  548. }
  549. if (isset($options['class'])) {
  550. $options['class'] += " " . $oddEven;
  551. } else {
  552. $options['class'] = $oddEven;
  553. }
  554. return Html::tag('tr', implode('', $cells), $options);
  555. }
  556. /**
  557. * 渲染摘要显示
  558. * @return string
  559. */
  560. public function renderSummary()
  561. {
  562. $count = $this->dataProvider->getCount();
  563. if ($count <= 0) {
  564. return '';
  565. }
  566. $summaryOptions = $this->summaryOptions;
  567. $tag = ArrayHelper::remove($summaryOptions, 'tag', 'div');
  568. if (($pagination = $this->dataProvider->getPagination()) !== false) {
  569. $totalCount = $this->dataProvider->getTotalCount();
  570. $begin = $pagination->getPage() * $pagination->pageSize + 1;
  571. $end = $begin + $count - 1;
  572. if ($begin > $end) {
  573. $begin = $end;
  574. }
  575. $page = $pagination->getPage() + 1;
  576. $pageCount = $pagination->pageCount;
  577. }
  578. return Yii::$app->getI18n()->format($this->summary, [
  579. 'begin' => $begin,
  580. 'end' => $end,
  581. 'count' => $count,
  582. 'totalCount' => $totalCount,
  583. 'page' => $page,
  584. 'pageCount' => $pageCount,
  585. 'select' => $this->renderCountSelect()
  586. ], Yii::$app->language);
  587. }
  588. /**
  589. * 渲染批量操作
  590. */
  591. public function renderBatch()
  592. {
  593. if (empty($this->batch) && !is_array($this->batch)) {
  594. return "";
  595. }
  596. $this->registerBatchJs();
  597. $items = "";
  598. foreach ($this->batch as $item) {
  599. $items .= Html::tag('li', Html::a(Html::encode($item['label']), '#', ["data-url" => Html::encode($item['url']), "class" => "batch_item dropdown-item"]));
  600. }
  601. return strtr($this->batchTemplate, [
  602. "{items}" => $items
  603. ]);
  604. }
  605. /**
  606. * 渲染表格的页数select
  607. * @return string
  608. */
  609. protected function renderCountSelect()
  610. {
  611. $items = [
  612. "20" => 20,
  613. "50" => 50,
  614. "100" => 100
  615. ];
  616. $per = "条/页";
  617. $options = [];
  618. foreach ($items as $key => $val) {
  619. $options[$val] = "{$key}{$per}";
  620. }
  621. $perPage = !empty($_GET['per-page']) ? $_GET['per-page'] : 20;
  622. return Html::dropDownList('per-page', $perPage, $options, ["class" => "custom-select"]);
  623. }
  624. /**
  625. * 渲染表格的筛选部分
  626. * @return string
  627. */
  628. protected function renderFilter()
  629. {
  630. return $this->filter;
  631. }
  632. /**
  633. * 根据给定格式,创建一个 [[DataColumn]] 对象
  634. * @param string $text DataColumn 格式
  635. * @return DataColumn 实例
  636. * @throws InvalidConfigException
  637. */
  638. protected function createDataColumn($text)
  639. {
  640. if (!preg_match('/^([^:]+)(:(\w*))?(:(.*))?$/', $text, $matches)) {
  641. throw new InvalidConfigException('The column must be specified in the format of "attribute", "attribute:format" or "attribute:format:label"');
  642. }
  643. return Yii::createObject([
  644. 'class' => $this->dataColumnClass ?: DataColumn::className(),
  645. 'grid' => $this,
  646. 'attribute' => $matches[1],
  647. 'format' => isset($matches[3]) ? $matches[3] : 'text',
  648. 'label' => isset($matches[5]) ? $matches[5] : null,
  649. ]);
  650. }
  651. /**
  652. * 渲染导出部分
  653. * @return string
  654. */
  655. protected function renderExport()
  656. {
  657. return $this->export;
  658. }
  659. /**
  660. * 渲染创建部分
  661. * @return string
  662. */
  663. protected function renderCreate()
  664. {
  665. return $this->create;
  666. }
  667. /**
  668. * 渲染表单头部内容
  669. * @return string
  670. */
  671. protected function renderContent()
  672. {
  673. return Html::tag('div', $this->content, ['class' => 'btn-group']);
  674. }
  675. /**
  676. * 注册css
  677. */
  678. protected function registerCss()
  679. {
  680. $css =<<<CSS
  681. .form-control{
  682. width: 192px;
  683. }
  684. CSS;
  685. $this->getView()->registerCss($css);
  686. }
  687. }