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.

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