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.

691 lines
21 KiB

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