grid.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751
  1. import _ from 'lodash'
  2. import {grid} from '../Defaults'
  3. import {baseConfig} from "./base";
  4. import {lookupFn, lookupScope} from "../lib/lib";
  5. import LAY_EXCEL from "lay-excel"
  6. import {
  7. disabled,
  8. fieldLabel,
  9. gravity, height, metaId,
  10. PropertyDescriptionTable,
  11. tooltip,
  12. value, width,
  13. YvBase
  14. } from "../PropertyDescriptionTable";
  15. import {PropertyDescription} from "../PropertyDescription";
  16. import {gridInvokeBuild} from "./stores";
  17. import {msg} from 'src/message';
  18. const defaultGrid = grid
  19. export default function () {
  20. Ext.define('Yvan.Grid', {
  21. extend: 'Ext.grid.Panel',
  22. xtype: 'yvgrid',
  23. constructor(config) {
  24. const me = this
  25. const {dataSource} = config
  26. if (!window["IS_DESIGN_MODE"]) {
  27. this.columnConfigCacheKey = this.makeColumnConfigCacheKey(config)
  28. if (Array.isArray(config.columns) && config.columns.length > 0) {
  29. const cacheData = this.getColumnConfigCache()
  30. if (Array.isArray(cacheData) && cacheData.length > 0) {
  31. const newColumns = []
  32. for (let j = 0; j < cacheData.length; j++) {
  33. const itData = cacheData[j]
  34. for (let i = 0; i < config.columns.length; i++) {
  35. const column = config.columns[i]
  36. if (itData.dataIndex === column.dataIndex) {
  37. if (itData.width) {
  38. column.width = itData.width
  39. }
  40. column.hidden = itData.hidden
  41. column.locked = itData.locked
  42. newColumns.push(column)
  43. break
  44. }
  45. }
  46. }
  47. config.columns = newColumns
  48. }
  49. }
  50. }
  51. const newConfig = _.defaultsDeep({
  52. // 强制性属性
  53. }, baseConfig(config, 'row-item'), config, grid)
  54. // 在面板上的组件
  55. const scope = newConfig.$initParent.yvanScope || newConfig.$initParent.lookupReferenceHolder().yvanScope;
  56. const buttons = []
  57. const {getRowClass} = newConfig
  58. if (typeof getRowClass === 'string' && (
  59. _.startsWith(getRowClass, "scope.") ||
  60. _.startsWith(getRowClass, "system."))
  61. ) {
  62. const fn = lookupFn(scope, getRowClass)
  63. _.set(newConfig, 'viewConfig.getRowClass', fn)
  64. }
  65. if (!newConfig.hideExport) {
  66. buttons.push({
  67. xtype: 'button',
  68. tooltip: '导出Excel',
  69. iconCls: 'x-fa fa-download',
  70. listeners: {
  71. click: this.exportExcel
  72. },
  73. })
  74. }
  75. if (!newConfig.hideAutoSize) {
  76. buttons.push({
  77. xtype: 'button',
  78. iconCls: 'x-fa fa-text-width',
  79. tooltip: '自适应宽度',
  80. listeners: {
  81. click: this.autoSizeColumns
  82. }
  83. })
  84. }
  85. if (!newConfig.hideClearFilter) {
  86. buttons.push({
  87. xtype: 'button',
  88. tooltip: '清空筛选',
  89. iconCls: 'x-fa fa-filter',
  90. handler: this.clearFilter
  91. })
  92. }
  93. if (!newConfig.hideSaveGridUIConfig) {
  94. buttons.push({
  95. xtype: 'button',
  96. tooltip: '保存表格自定义配置',
  97. iconCls: 'x-fa fa-cogs',
  98. handler: this.saveGridUIConfig
  99. })
  100. }
  101. if (!newConfig.hideClearGridUIConfig) {
  102. buttons.push({
  103. xtype: 'button',
  104. tooltip: '还原表格自定义配置',
  105. iconCls: 'x-fa fa-reply-all',
  106. handler: this.clearGridUIConfig
  107. })
  108. }
  109. if (!newConfig.hideFootbar) {
  110. if (newConfig.pagination) {
  111. newConfig.bbar = new Ext.PagingToolbar({
  112. // pageSize: newConfig.pageSize, 这个值是无效的
  113. displayInfo: true,
  114. store: this.store,
  115. emptyMsg: '没有记录',
  116. items: [
  117. {
  118. xtype: 'combobox',
  119. tooltip: '分页',
  120. queryMode: 'local',
  121. editable: false,
  122. allowBlank: true,
  123. labelAlign: 'right',
  124. width: 90,
  125. // labelWidth: 30,
  126. listConfig: {
  127. minWidth: null
  128. },
  129. value: 50,
  130. valueField: undefined,
  131. displayField: undefined,
  132. hideClear: true,
  133. store: newConfig.pageSizeOption,
  134. listeners: {
  135. change: (sender, nv, ov) => {
  136. this.store.pageSize = nv;
  137. this.store.loadPage(1);
  138. }
  139. }
  140. },
  141. ...buttons
  142. ]
  143. })
  144. } else {
  145. newConfig.bbar = {
  146. xtype: 'toolbar', overflowHandler: 'menu',
  147. items: [
  148. {
  149. xtype: 'button',
  150. tooltip: '刷新',
  151. iconCls: 'x-fa fa-refresh',
  152. handler: () => {
  153. this.reload()
  154. }
  155. },
  156. '-',
  157. ...buttons,
  158. ]
  159. }
  160. }
  161. }
  162. _.each(newConfig.columns, c => {
  163. const {renderer} = c
  164. if (typeof renderer === 'string' && (
  165. _.startsWith(renderer, "scope.") ||
  166. _.startsWith(renderer, "system."))
  167. ) {
  168. if (newConfig.$initParent) {
  169. if (scope) {
  170. const rendererFn = lookupFn(scope, renderer)
  171. c.renderer = rendererFn
  172. }
  173. }
  174. }
  175. })
  176. this.superclass.constructor.call(this, newConfig)
  177. this.store.pageSize = newConfig.pageSize
  178. },
  179. setData(value) {
  180. const me = this
  181. me._setDataReal(value)
  182. },
  183. /**
  184. * 添加行,并进入编辑状态
  185. * @param record 新行的属性集
  186. * @param editRowCol 要编辑的列序号
  187. */
  188. appendEditRow(record, editRowCol) {
  189. const records = this.getStore().add(record)
  190. const recNew = records[0]
  191. this.setSelection(records)
  192. if (typeof editRowCol === 'number') {
  193. const ce = this.findPlugin('cellediting')
  194. this.editingPlugin = ce
  195. ce.startEdit(recNew, editRowCol)
  196. }
  197. },
  198. /**
  199. * 移除行
  200. * @param record 如果记录传空,就是当前选中的行
  201. */
  202. removeEditRow(record) {
  203. if (!record) {
  204. record = this.refs.grid2.selection
  205. }
  206. if (!record) {
  207. msg('请选中要删除的行')
  208. return
  209. }
  210. this.getStore().remove(record)
  211. },
  212. _transform(data) {
  213. // 无论是 grid._setDataReal 还是 stores.gridInvokeBuild 都会走这个函数,设值前都可以改变表格值
  214. _.forEach(data, row => {
  215. row._origin = _.clone(row)
  216. })
  217. },
  218. _setDataReal(value) {
  219. const me = this
  220. this._transform(value)
  221. me.setStore(new Ext.data.Store({
  222. fields: getFileds(this),
  223. data: value
  224. }))
  225. },
  226. /**
  227. * 轻量级刷新
  228. */
  229. refreshData() {
  230. const store = this.getStore()
  231. if (store) {
  232. store.reload()
  233. }
  234. },
  235. /**
  236. * 为表格强制设置焦点
  237. * @param seq 顺序号
  238. */
  239. focusRow(seq) {
  240. this.setSelection(this.store.getAt(seq))
  241. this.getView().focusRow(seq)
  242. },
  243. /**
  244. * 重新载入数据(重新计算参数)
  245. */
  246. reload(reloadParams = {}) {
  247. const me = this
  248. const {config} = me
  249. if (config.dataSourceCallbackFn) {
  250. // 函数请求刷新
  251. const scope = lookupScope(this)
  252. _.defer(() => {
  253. me.setLoading(true)
  254. })
  255. config.dataSourceCallbackFn.call(scope, me, {
  256. successCallback(value) {
  257. me._setDataReal(value)
  258. _.defer(() => {
  259. me.setLoading(false)
  260. })
  261. me.fireEvent('dataLoadComplete', me, true, value);
  262. },
  263. failCallback(error) {
  264. _.defer(() => {
  265. me.setLoading(false)
  266. })
  267. me.fireEvent('dataLoadComplete', me, false, error);
  268. }
  269. })
  270. return
  271. }
  272. // if (this.store) {
  273. // this.store.reload({aaaa: 1, bbbb: 2})
  274. // }
  275. const {dataSource} = config
  276. if (_.isPlainObject(dataSource) && !window["IS_DESIGN_MODE"]) {
  277. const scope = lookupScope(me)
  278. gridInvokeBuild(scope, me, config, dataSource, reloadParams)
  279. }
  280. },
  281. exportCurrentExcelClick() {
  282. const me = this
  283. const {config} = me
  284. const scope = lookupScope(me)
  285. let excelFileName = config.excelFileName || scope.vjson.title || _.uniqueId("excel-")
  286. if (excelFileName.endsWith(".xlsx")) {
  287. excelFileName = excelFileName.split(".xlsx")[0]
  288. }
  289. excelFileName += ".xlsx"
  290. const rowsAll = this.getStore().getData().items?.map(r => r.data);
  291. const excelData = me.makeExcelData(rowsAll)
  292. LAY_EXCEL.exportExcel(excelData, excelFileName, 'xlsx')
  293. },
  294. exportExcelClick(excelExportParams) {
  295. const me = this
  296. const {config} = me
  297. excelExportParams.isExcelExport = true
  298. const scope = lookupScope(me)
  299. const {dataSource} = config
  300. let excelFileName = config.excelFileName || scope.vjson.title || _.uniqueId("excel-")
  301. gridInvokeBuild(scope, me, config, dataSource, excelExportParams, true, (responseData) => {
  302. let page = parseInt(responseData.pagination?.current) || 1
  303. const size = parseInt(responseData.pagination?.size) || me.exportExcelPageSize
  304. const total = parseInt(responseData.pagination?.total) || responseData.data?.length || 0
  305. me.exportExcelCurrentPage = page
  306. me.exportExcelPageSize = size
  307. me.exportExcelTotal = total
  308. if (excelFileName.endsWith(".xlsx")) {
  309. excelFileName = excelFileName.split(".xlsx")[0]
  310. }
  311. excelFileName += "(第" + me.exportExcelCurrentPage + "页,共" + Math.ceil(total / size) + "页、" + total + "条)"
  312. excelFileName += ".xlsx"
  313. const excelData = me.makeExcelData(responseData.data)
  314. LAY_EXCEL.exportExcel(excelData, excelFileName, 'xlsx')
  315. if (page < total / size) {
  316. page++
  317. }
  318. })
  319. },
  320. makeExcelData(jsonData) {
  321. if (!Array.isArray(jsonData) || jsonData.length === 0) {
  322. return
  323. }
  324. const me = this
  325. const data = [];
  326. // 获取表格的列定义
  327. const headerTextArr = []
  328. const headers = []
  329. for (let i = 0; i < me.headerCt.getGridColumns().length; i++) {
  330. const header = me.headerCt.getGridColumns()[i]
  331. if (!header.isHidden()) {
  332. const textStr = _.trim(header.text)
  333. const dataIndexStr = _.trim(header.dataIndex)
  334. if (dataIndexStr) {
  335. if (textStr === '') {
  336. headerTextArr.push(dataIndexStr)
  337. } else {
  338. headerTextArr.push(textStr)
  339. }
  340. headers.push(header)
  341. }
  342. }
  343. }
  344. if (headers.length === 0) {
  345. return
  346. }
  347. data.push(headerTextArr)
  348. for (let i = 0; i < jsonData.length; i++) {
  349. const dataRow = jsonData[i]
  350. const row = []
  351. for (let j = 0; j < headers.length; j++) {
  352. const key = headers[j].dataIndex
  353. let value = dataRow[key]
  354. if (typeof headers[j].renderer === 'function') {
  355. value = headers[j].renderer(value)
  356. }
  357. row.push(value || "")
  358. }
  359. data.push(row)
  360. }
  361. return data
  362. },
  363. initComponent() {
  364. const me = this
  365. const {config} = me
  366. const scope = lookupScope(this)
  367. if (!window["IS_DESIGN_MODE"]) {
  368. // 转换 dataSource 属性
  369. convertDataSource(me, scope, config)
  370. }
  371. this.on({
  372. afterrender(sender) {
  373. const me = this
  374. const {config} = this
  375. const {dataSource} = config
  376. if (config.autoLoad) {
  377. if (config.dataSourceCallbackFn) {
  378. me.reload()
  379. } else if (_.isPlainObject(dataSource)) {
  380. me.reload()
  381. }
  382. }
  383. if (config.contextMenu === true && _.isArray(config.tbar)) {
  384. const vm = this.lookupViewModel()
  385. this.contextMenu = this.add(new Ext.menu.Menu({
  386. viewModel: vm,
  387. items: _.map(config.tbar, item => {
  388. const menuItem = {
  389. ...item
  390. }
  391. if (menuItem.xtype === 'button') {
  392. delete menuItem.xtype
  393. }
  394. return menuItem
  395. })
  396. }))
  397. } else if (_.isPlainObject(config.contextMenu)) {
  398. this.contextMenu = this.add(config.contextMenu)
  399. }
  400. const $dom = $(sender.el.dom)
  401. $dom.on('keydown', (e) => {
  402. me.fireEvent('keydown', me, e,)
  403. }).on('keyup', (e) => {
  404. me.fireEvent('keyup', me, e,)
  405. })
  406. },
  407. itemcontextmenu(view, rec, node, index, e) {
  408. if (this.contextMenu) {
  409. e.stopEvent();
  410. this.contextMenu.show().setLocalXY(e.getXY());
  411. return false;
  412. }
  413. },
  414. // columnmove(sender, column, fromIndex, toIndex, eOpts) {
  415. // this.setColumnConfigCache()
  416. // },
  417. // columnhide(sender, column, eOpts) {
  418. // this.setColumnConfigCache()
  419. // },
  420. // columnshow(sender, column, eOpts) {
  421. // this.setColumnConfigCache()
  422. // },
  423. // columnresize(sender, column, width, eOpts) {
  424. // this.setColumnConfigCache()
  425. // },
  426. destory() {
  427. },
  428. })
  429. if (this.store?.proxy) {
  430. // 为 stores.proxy.buildRequest 做准备
  431. this.store.proxy.$owner = this
  432. }
  433. this.superclass.initComponent.call(this)
  434. },
  435. // 生成列自定义的缓存key
  436. makeColumnConfigCacheKey(config) {
  437. const me = this
  438. const scope = config.$initParent.yvanScope || config.$initParent.lookupReferenceHolder().yvanScope;
  439. let key = "gridColumnCache-" + scope.scopeKey + "-"
  440. if (config.reference) {
  441. key += config.reference
  442. } else {
  443. let subKey = ""
  444. for (let i = 0; i < config.columns.length; i++) {
  445. const column = config.columns[i]
  446. if (column.dataIndex) {
  447. subKey += column.dataIndex
  448. }
  449. }
  450. key += subKey
  451. }
  452. return key
  453. },
  454. getColumnConfigCache() {
  455. const key = this.columnConfigCacheKey
  456. const dataStr = localStorage.getItem(key)
  457. if (dataStr) {
  458. return JSON.parse(dataStr)
  459. }
  460. return ""
  461. },
  462. setColumnConfigCache() {
  463. const key = this.columnConfigCacheKey
  464. const cacheData = []
  465. const columns = this.headerCt.getGridColumns()
  466. let index = 0
  467. for (let i = 0; i < columns.length; i++) {
  468. const column = columns[i]
  469. if (column.dataIndex) {
  470. cacheData.push({
  471. dataIndex: column.dataIndex,
  472. width: column.width,
  473. hidden: column.hidden,
  474. locked: column.locked,
  475. index
  476. })
  477. index++
  478. }
  479. }
  480. localStorage.setItem(key, JSON.stringify(cacheData))
  481. },
  482. autoSizeColumns(sender) {
  483. const grid = sender.up('grid')
  484. // const columns = grid.columns;
  485. // for (let i = 0; i < columns.length; i++) {
  486. // const column = columns[i];
  487. // grid.getView().autoSizeColumn(column);
  488. // column.setWidth(column.getWidth() + 5);
  489. // }
  490. for (let i = 1; i < grid.headerCt.getColumnCount(); i++) {
  491. grid.headerCt.getGridColumns()[i].autoSize(i);
  492. grid.headerCt.getGridColumns()[i].setWidth(grid.headerCt.getGridColumns()[i].getWidth() + 15);
  493. }
  494. },
  495. clearFilter(sender) {
  496. sender.up('grid').filters.clearFilters();
  497. },
  498. saveGridUIConfig(sender) {
  499. const grid = sender.up('grid')
  500. grid.setColumnConfigCache()
  501. msg('保存设置成功!')
  502. },
  503. clearGridUIConfig(sender) {
  504. const grid = sender.up('grid')
  505. const key = grid.columnConfigCacheKey
  506. localStorage.setItem(key, "")
  507. msg('清空设置成功,重新打开后生效!')
  508. },
  509. setLoading(value) {
  510. if (value) {
  511. this.mask('读取中')
  512. } else {
  513. this.unmask()
  514. }
  515. },
  516. exportExcel(sender) {
  517. const rect = sender.btnEl.dom.getBoundingClientRect()
  518. const scope = lookupScope(this)
  519. const grid = sender.up('grid')
  520. const treeMenu = new Ext.menu.Menu(
  521. {
  522. xtype: 'menu',
  523. floated: false,
  524. width: 300,
  525. docked: 'left',
  526. items: [{
  527. text: '导出表格当前的数据',
  528. iconCls: 'x-fa fa-download',
  529. listeners: {
  530. click: (sender, value) => {
  531. grid.exportCurrentExcelClick()
  532. }
  533. }
  534. }, {
  535. xtype: "textfield",
  536. fieldLabel: '当前导出页',
  537. maskRe: /[0-9]/,
  538. value: grid.exportExcelCurrentPage,
  539. listeners: {
  540. render: (sender) => {
  541. grid.exportExcelCurrentPageCmp = sender
  542. },
  543. change: (sender, value) => {
  544. let v = parseInt(value)
  545. if (isNaN(v) || v === 0) {
  546. window['system'].msg("页码不能为0")
  547. v = 1
  548. sender.setValue(v)
  549. }
  550. const size = parseInt(grid.exportExcelPageSize)
  551. const total = parseInt(grid.exportExcelTotal)
  552. if (v > total / size) {
  553. v = parseInt(total / size + "")
  554. }
  555. grid.exportExcelCurrentPage = v + ""
  556. }
  557. }
  558. }, {
  559. xtype: "textfield",
  560. fieldLabel: '导出页大小',
  561. maskRe: /[0-9]/,
  562. value: grid.exportExcelPageSize,
  563. listeners: {
  564. render: (sender) => {
  565. grid.exportExcelPageSizeCmp = sender
  566. },
  567. change: (sender, value) => {
  568. let v = parseInt(value)
  569. if (isNaN(v) || v === 0) {
  570. window['system'].msg("导出页大小不能为0")
  571. v = defaultGrid.exportExcelPageSize
  572. sender.setValue(v);
  573. }
  574. if (v >= 10000) {
  575. window['system'].msg("导出页大小不能大于10000")
  576. v = 10000
  577. sender.setValue(v);
  578. }
  579. let page = parseInt(grid.exportExcelCurrentPage)
  580. const total = parseInt(grid.exportExcelTotal)
  581. if (page > total / v) {
  582. page = parseInt(total / v + "") + 1
  583. grid.exportExcelCurrentPageCmp.setValue(page)
  584. }
  585. grid.exportExcelPageSize = v + ""
  586. }
  587. }
  588. }, {
  589. xtype: "textfield",
  590. fieldLabel: '总条数',
  591. value: grid.exportExcelTotal,
  592. readOnly: true
  593. }, {
  594. text: '导出',
  595. iconCls: 'x-fa fa-download',
  596. listeners: {
  597. click: (sender, value) => {
  598. grid.exportExcelClick({
  599. exportExcelPageSize: grid.exportExcelPageSize,
  600. exportExcelCurrentPage: grid.exportExcelCurrentPage
  601. })
  602. }
  603. }
  604. }]
  605. }
  606. );
  607. treeMenu.showAt(rect.left, rect.top - 120);
  608. // for (let i = 1; i < grid.headerCt.getColumnCount(); i++) {
  609. // grid.headerCt.getGridColumns()[i].autoSize(i);
  610. // grid.headerCt.getGridColumns()[i].setWidth(grid.headerCt.getGridColumns()[i].getWidth() + 15);
  611. // }
  612. }
  613. // reload() {
  614. // dataSourceReload(this)
  615. // },
  616. })
  617. PropertyDescriptionTable.set(
  618. 'yvgrid',
  619. new PropertyDescription(YvBase, {
  620. props: [
  621. fieldLabel, value, disabled,
  622. gravity, tooltip, metaId, width, height
  623. ],
  624. })
  625. )
  626. }
  627. /**
  628. * 获取 columns 中所有的 dataIndex
  629. */
  630. function getFileds(newConfig) {
  631. const fields = []
  632. _.forEach(newConfig.columns, c => {
  633. if (c.dataIndex) {
  634. fields.push(c.dataIndex)
  635. }
  636. })
  637. return fields
  638. }
  639. function convertDataSource(sender, scope, newConfig) {
  640. if (typeof newConfig.store !== 'undefined') {
  641. // 有 store 属性的情况下,不做任何事
  642. return
  643. }
  644. if (typeof newConfig.dataSource === 'undefined') {
  645. // 没有定义 dataSource 的情况下,不做任何事
  646. return
  647. }
  648. if (_.isArray(newConfig.data)) {
  649. // 有 data 属性赋值的情况下
  650. newConfig.store = {
  651. fields: getFileds(newConfig),
  652. data: newConfig.data
  653. }
  654. delete newConfig.data
  655. return
  656. }
  657. let {dataSource} = newConfig
  658. if (typeof dataSource === 'string') {
  659. // dataSource 是字符串的情况下,找到成员函数
  660. dataSource = lookupFn(scope, dataSource)
  661. }
  662. if (typeof dataSource === 'function') {
  663. // dataSource 是函数的情况下,在 afterrender 之后进行回调
  664. newConfig.store = new Ext.data.Store({
  665. fields: getFileds(newConfig),
  666. // data: [],
  667. autoLoad: true,
  668. proxy: {
  669. type: 'memory',
  670. data: [],
  671. // reader: {
  672. // type: 'json',
  673. // rootProperty: 'users'
  674. // }
  675. }
  676. })
  677. newConfig.dataSourceCallbackFn = dataSource
  678. return
  679. }
  680. // throw new TypeError('无法识别的调用方法')
  681. }