baTable.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. import {reactive, onUnmounted, onActivated, onDeactivated, defineEmits} from 'vue'
  2. import {getArrayKey} from '/@/utils/common'
  3. import useCurrentInstance from '/@/utils/useCurrentInstance'
  4. import type {baTableApi} from '/@/api/common'
  5. import Sortable from 'sortablejs'
  6. import {findIndexRow} from '/@/components/table'
  7. import {ElNotification, ElForm} from 'element-plus'
  8. import {onBeforeRouteUpdate, useRoute} from 'vue-router'
  9. import _, {assign} from 'lodash'
  10. export default class baTable {
  11. public api
  12. public activate
  13. /* 表格状态-s */
  14. public table: BaTable = reactive({
  15. ref: undefined,
  16. // 主键字段
  17. pk: 'id',
  18. // 数据源
  19. data: [],
  20. // 路由remark
  21. remark: null,
  22. // 表格加载状态
  23. loading: false,
  24. // 是否展开所有子项
  25. expandAll: false,
  26. // 选中项
  27. selection: [],
  28. // 不需要'双击编辑'的字段
  29. dblClickNotEditColumn: [undefined],
  30. // 列数据
  31. column: [],
  32. // 数据总量
  33. total: 0,
  34. // 字段搜索,快速搜索,分页等数据
  35. filter: {},
  36. // 拖动排序限位字段:例如拖动行pid=1,那么拖动目的行pid也需要为1
  37. dragSortLimitField: 'pid',
  38. // 接受url的query参数并自动触发通用搜索
  39. acceptQuery: true,
  40. // 扩展数据
  41. extend: {},
  42. actionStruct: {data: null, name: ''},
  43. tableName: '',
  44. rowStyle: '',
  45. headerRowStyle: '',
  46. })
  47. /* 表格状态-e */
  48. /* 表单状态-s */
  49. public form: BaTableForm = reactive({
  50. // 表单ref,new时无需传递
  51. ref: undefined,
  52. // 表单label宽度
  53. labelWidth: 160,
  54. // 当前操作:add=添加,edit=编辑
  55. operate: '',
  56. // 被操作数据ID,支持批量编辑:add=[0],edit=[1,2,n]
  57. operateIds: [],
  58. // 表单数据
  59. items: {},
  60. // 提交按钮状态
  61. submitLoading: false,
  62. // 默认表单数据(添加)
  63. defaultItems: {},
  64. // 表单字段加载状态
  65. loading: false,
  66. // 扩展数据
  67. extend: {},
  68. })
  69. /* 表单状态-e */
  70. // BaTable前置处理函数列表(前置埋点)
  71. public before
  72. // BaTable后置处理函数列表(后置埋点)
  73. public after
  74. // 通用搜索数据-需要响应性
  75. public comSearch: ComSearch = reactive({
  76. form: {},
  77. fieldData: new Map(),
  78. })
  79. constructor(api: baTableApi, table: BaTable, form: BaTableForm = {}, before: BaTableBefore = {}, after: BaTableAfter = {}) {
  80. this.api = api
  81. this.form = Object.assign(this.form, form)
  82. this.table = Object.assign(this.table, table)
  83. this.before = before
  84. this.after = after
  85. this.activate = true
  86. const route = useRoute()
  87. this.initComSearch(!_.isUndefined(route) ? route.query : {})
  88. }
  89. runBefore(funName: string, args: any = {}) {
  90. if (this.before && this.before[funName] && typeof this.before[funName] == 'function') {
  91. return this.before[funName]!({...args}) === false ? false : true
  92. }
  93. return true
  94. }
  95. runAfter(funName: string, args: any = {}) {
  96. if (this.after && this.after[funName] && typeof this.after[funName] == 'function') {
  97. return this.after[funName]!({...args}) === false ? false : true
  98. }
  99. return true
  100. }
  101. /* API请求方法-s */
  102. // 查看
  103. getIndex = () => {
  104. if (this.runBefore('getIndex') === false) return
  105. this.table.loading = true
  106. return this.api
  107. .index(this.table.filter)
  108. .then((res) => {
  109. this.table.data = res.data.list
  110. this.table.total = typeof res.data.total == 'string' ? parseInt(res.data.total) : res.data.total
  111. this.table.remark = res.data.remark
  112. this.table.loading = false
  113. this.runAfter('getIndex', {res})
  114. })
  115. .catch(() => {
  116. this.table.loading = false
  117. })
  118. }
  119. // 删除
  120. postDel = (ids: string[]) => {
  121. if (this.runBefore('postDel', {ids}) === false) return
  122. this.api.del(ids).then((res) => {
  123. this.onTableHeaderAction('refresh', {})
  124. this.runAfter('postDel', {res})
  125. this.table.actionStruct!.name = "del"
  126. this.table.actionStruct!.data = ids
  127. })
  128. }
  129. // 编辑
  130. requestEdit = (id: string) => {
  131. if (this.runBefore('requestEdit', {id}) === false) return
  132. this.form.loading = true
  133. this.form.items = {}
  134. return this.api
  135. .edit({
  136. id: id,
  137. })
  138. .then((res) => {
  139. this.form.loading = false
  140. // this.form.items = Object.assign(res.data.row, this.form.defaultItems)
  141. this.form.items = res.data.row
  142. if (res.data.row.hasOwnProperty('status')) {
  143. this.form.items!.status = res.data.row.status + ''
  144. }
  145. this.runAfter('requestEdit', {res})
  146. })
  147. }
  148. /* API请求方法-e */
  149. /**
  150. * 双击表格
  151. */
  152. onTableDblclick = (row: TableRow, column: any) => {
  153. if (this.table.dblClickNotEditColumn!.indexOf('all') === -1 && this.table.dblClickNotEditColumn!.indexOf(column.property) === -1) {
  154. if (this.runBefore('onTableDblclick', {row, column}) === false) return
  155. this.toggleForm('edit', [row[this.table.pk!]])
  156. this.runAfter('onTableDblclick', {
  157. row,
  158. column,
  159. })
  160. }
  161. }
  162. /**
  163. * 打开表单
  164. * @param operate 操作:add=添加,edit=编辑
  165. * @param operateIds 被操作项的数组:add=[],edit=[1,2,...]
  166. */
  167. toggleForm = (operate: string = '', operateIds: string[] = []) => {
  168. if (this.runBefore('toggleForm', {operate, operateIds}) === false) return
  169. if (this.form.ref) {
  170. this.form.ref.resetFields()
  171. }
  172. if (operate == 'edit') {
  173. if (!operateIds.length) {
  174. return false
  175. }
  176. this.requestEdit(operateIds[0])
  177. } else if (operate == 'add') {
  178. this.form.items = Object.assign({}, this.form.defaultItems)
  179. }
  180. this.form.operate = operate
  181. this.form.operateIds = operateIds
  182. this.runAfter('toggleForm', {operate, operateIds})
  183. }
  184. onSubmit = (formEl: InstanceType<typeof ElForm> | undefined = undefined) => {
  185. if (this.runBefore('onSubmit', {
  186. formEl: formEl,
  187. operate: this.form.operate!,
  188. items: this.form.items!
  189. }) === false) return
  190. for (const key in this.form.items) {
  191. if (this.form.items[key] === null) {
  192. delete this.form.items[key]
  193. }
  194. }
  195. // 表单验证通过后执行的api请求操作
  196. let submitCallback = () => {
  197. this.form.submitLoading = true
  198. this.api
  199. .postData(this.form.operate!, this.form.items!)
  200. .then((res) => {
  201. this.onTableHeaderAction('refresh', {})
  202. this.form.submitLoading = false
  203. this.form.operateIds?.shift()
  204. if (this.form.operateIds?.length! > 0) {
  205. this.toggleForm('edit', this.form.operateIds)
  206. } else {
  207. this.toggleForm()
  208. }
  209. this.runAfter('onSubmit', {res})
  210. })
  211. .catch((err) => {
  212. this.form.submitLoading = false
  213. })
  214. }
  215. if (formEl) {
  216. this.form.ref = formEl
  217. formEl.validate((valid) => {
  218. if (valid) {
  219. submitCallback()
  220. }
  221. })
  222. } else {
  223. submitCallback()
  224. }
  225. }
  226. /* 获取表格选择项的id数组 */
  227. getSelectionIds() {
  228. let ids: string[] = []
  229. for (const key in this.table.selection) {
  230. ids.push(this.table.selection[key as any][this.table.pk!])
  231. }
  232. return ids
  233. }
  234. /**
  235. * 表格内的事件响应
  236. * @param event 事件:selection-change=选中项改变,page-size-change=每页数量改变,current-page-change=翻页,sort-change=排序
  237. * @param data 携带数据
  238. */
  239. onTableAction = (event: string, data: anyObj) => {
  240. if (this.runBefore('onTableAction', {event, data}) === false) return
  241. const actionFun = new Map([
  242. [
  243. 'selection-change',
  244. () => {
  245. this.table.selection = data as TableRow[]
  246. },
  247. ],
  248. [
  249. 'page-size-change',
  250. () => {
  251. this.table.filter!.limit = data.size
  252. this.getIndex()
  253. },
  254. ],
  255. [
  256. 'current-page-change',
  257. () => {
  258. this.table.filter!.page = data.page
  259. this.getIndex()
  260. },
  261. ],
  262. [
  263. 'sort-change',
  264. () => {
  265. let newOrder = ''
  266. if (!data.prop) {
  267. newOrder = ''
  268. } else if (data.prop) {
  269. newOrder = data.prop + ',' + data.order
  270. }
  271. if (newOrder != this.table.filter!.order) {
  272. this.table.filter!.order = newOrder
  273. this.getIndex()
  274. }
  275. },
  276. ],
  277. [
  278. 'default',
  279. () => {
  280. console.warn('未定义操作')
  281. },
  282. ],
  283. ])
  284. let action = actionFun.get(event) || actionFun.get('default')
  285. action!.call(this)
  286. return this.runAfter('onTableAction', {event, data})
  287. }
  288. /**
  289. * 表格顶栏按钮事件响应
  290. * @param event 事件:refresh=刷新,edit=编辑,delete=删除,quick-search=快速查询
  291. * @param data 携带数据
  292. */
  293. onTableHeaderAction = (event: string, data: anyObj) => {
  294. if (this.runBefore('onTableHeaderAction', {event, data}) === false) return
  295. const actionFun = new Map([
  296. [
  297. 'refresh',
  298. () => {
  299. this.table.data = []
  300. this.getIndex()
  301. },
  302. ],
  303. [
  304. 'add',
  305. () => {
  306. this.toggleForm('add')
  307. },
  308. ],
  309. [
  310. 'edit',
  311. () => {
  312. this.toggleForm('edit', this.getSelectionIds())
  313. },
  314. ],
  315. [
  316. 'delete',
  317. () => {
  318. this.postDel(this.getSelectionIds())
  319. },
  320. ],
  321. [
  322. 'unfold',
  323. () => {
  324. if (!this.table.ref) {
  325. console.warn('折叠/展开失败,因为tableRef未定义,请在onMounted时赋值tableRef')
  326. return
  327. }
  328. this.table.expandAll = data.unfold
  329. this.table.ref.unFoldAll(data.unfold)
  330. },
  331. ],
  332. [
  333. 'quick-search',
  334. () => {
  335. this.table.filter!.quickSearch = data.keyword
  336. this.getIndex()
  337. },
  338. ],
  339. [
  340. 'change-show-column',
  341. () => {
  342. let columnKey = getArrayKey(this.table.column, 'prop', data.field)
  343. this.table.column[columnKey].show = data.value
  344. },
  345. ],
  346. [
  347. 'default',
  348. () => {
  349. console.warn('未定义操作')
  350. },
  351. ],
  352. ])
  353. let action = actionFun.get(event) || actionFun.get('default')
  354. action!.call(this)
  355. return this.runAfter('onTableHeaderAction', {event, data})
  356. }
  357. /**
  358. * 初始化默认排序
  359. * el表格的`default-sort`在自定义排序时无效
  360. * 此方法只有在表格数据请求结束后执行有效
  361. */
  362. initSort = () => {
  363. if (this.table.defaultOrder && this.table.defaultOrder.prop) {
  364. if (!this.table.ref) {
  365. console.warn('初始化默认排序失败,因为tableRef未定义,请在onMounted时赋值tableRef')
  366. return
  367. }
  368. let defaultOrder = this.table.defaultOrder.prop + ',' + this.table.defaultOrder.order
  369. if (this.table.filter && this.table.filter.order != defaultOrder) {
  370. this.table.filter.order = defaultOrder
  371. this.table.ref.getRef()?.sort(this.table.defaultOrder.prop, this.table.defaultOrder.order == 'desc' ? 'descending' : 'ascending')
  372. }
  373. }
  374. }
  375. /**
  376. * 表格拖动排序
  377. */
  378. dragSort = () => {
  379. let buttonsKey = getArrayKey(this.table.column, 'render', 'buttons')
  380. let moveButton = getArrayKey(this.table.column[buttonsKey].buttons, 'render', 'moveButton')
  381. if (moveButton === false) {
  382. return
  383. }
  384. if (!this.table.ref) {
  385. console.warn('初始化拖拽排序失败,因为tableRef未定义,请在onMounted时赋值tableRef')
  386. return
  387. }
  388. let el = this.table.ref.getRef().$el.querySelector('.el-table__body-wrapper .el-table__body tbody')
  389. var sortable = Sortable.create(el, {
  390. animation: 200,
  391. handle: '.table-row-weigh-sort',
  392. ghostClass: 'ba-table-row',
  393. onStart: () => {
  394. for (const key in this.table.column[buttonsKey].buttons) {
  395. this.table.column[buttonsKey].buttons![key as any].disabledTip = true
  396. }
  397. },
  398. onEnd: (evt: Sortable.SortableEvent) => {
  399. for (const key in this.table.column[buttonsKey].buttons) {
  400. this.table.column[buttonsKey].buttons![key as any].disabledTip = false
  401. }
  402. // 找到对应行id
  403. let moveRow = findIndexRow(this.table.data!, evt.oldIndex!) as TableRow
  404. let replaceRow = findIndexRow(this.table.data!, evt.newIndex!) as TableRow
  405. if (this.table.dragSortLimitField && moveRow[this.table.dragSortLimitField] != replaceRow[this.table.dragSortLimitField]) {
  406. this.onTableHeaderAction('refresh', {})
  407. ElNotification({
  408. type: 'error',
  409. message: '移动位置超出了可移动范围!',
  410. })
  411. return
  412. }
  413. this.api.sortableApi(moveRow.id, replaceRow.id).then((res) => {
  414. this.onTableHeaderAction('refresh', {})
  415. })
  416. },
  417. })
  418. }
  419. mount = () => {
  420. if (this.runBefore('mount') === false) return
  421. this.activate = true
  422. const {proxy} = useCurrentInstance()
  423. /**
  424. * 表格内的按钮响应
  425. * @param name 按钮name
  426. * @param row 被操作行数据
  427. */
  428. proxy.eventBus.on('onTableButtonClick', (data: { name: string; row: TableRow }) => {
  429. if (!this.activate) return
  430. if (data.name == 'edit') {
  431. this.toggleForm('edit', [data.row[this.table.pk!]])
  432. } else if (data.name == 'delete') {
  433. this.postDel([data.row[this.table.pk!]])
  434. }
  435. })
  436. /**
  437. * 通用搜索响应
  438. * @param comSearchData 通用搜索数据
  439. */
  440. proxy.eventBus.on('onTableComSearch', (data: comSearchData) => {
  441. if (!this.activate) return
  442. this.table.filter!.search = JSON.stringify(data)
  443. this.getIndex()
  444. })
  445. /**
  446. * 表格内的字段操作响应
  447. * @param value 修改后的值
  448. * @param row 被操作行数据
  449. * @param field 被操作字段名
  450. */
  451. proxy.eventBus.on('onTableFieldChange', (data: { value: any; row: TableRow; field: keyof TableRow; render: string }) => {
  452. if (!this.activate) return
  453. if (data.render == 'switch') {
  454. data.row.loading = true
  455. this.api
  456. .postData('edit', {[this.table.pk!]: data.row[this.table.pk!], [data.field]: data.value})
  457. .then(() => {
  458. data.row.loading = false
  459. data.row[data.field] = data.value
  460. })
  461. .catch(() => {
  462. data.row.loading = false
  463. })
  464. }
  465. })
  466. onUnmounted(() => {
  467. // 考虑到 keepalive 在 onMounted、onActivated 时注册事件;onUnmounted、onDeactivated 时注销事件,但并不是最方便的方案,且注销后在全局事件管理中本来就有留存
  468. // 干脆,不off掉事件,改用 this.activate 来决定事件是否执行
  469. this.activate = false
  470. this.runAfter('mount')
  471. })
  472. onActivated(() => {
  473. this.activate = true
  474. })
  475. onDeactivated(() => {
  476. this.activate = false
  477. })
  478. // 监听路由变化,响应通用搜索更新
  479. onBeforeRouteUpdate((to) => {
  480. this.initComSearch(to.query)
  481. this.getIndex()
  482. })
  483. }
  484. /**
  485. * 通用搜索初始化
  486. */
  487. initComSearch = (query: anyObj = {}) => {
  488. let form: anyObj = {}
  489. if (this.table.column.length <= 0) {
  490. return
  491. }
  492. const field = this.table.column
  493. for (const key in field) {
  494. if (field[key].operator === false) {
  495. continue
  496. }
  497. let prop = field[key].prop
  498. if (typeof field[key].operator == 'undefined') {
  499. field[key].operator = '='
  500. }
  501. if (prop) {
  502. if (field[key].operator == 'RANGE' || field[key].operator == 'NOT RANGE') {
  503. form[prop] = ''
  504. form[prop + '-start'] = ''
  505. form[prop + '-end'] = ''
  506. } else if (field[key].operator == 'NULL' || field[key].operator == 'NOT NULL') {
  507. form[prop] = false
  508. } else {
  509. form[prop] = ''
  510. }
  511. // 初始化来自query中的默认值
  512. if (this.table.acceptQuery && typeof query[prop] != 'undefined') {
  513. let queryProp = (query[prop] as string) ?? ''
  514. if (field[key].operator == 'RANGE' || field[key].operator == 'NOT RANGE') {
  515. let range = queryProp.split(',')
  516. if (field[key].render == 'datetime') {
  517. if (range && range.length >= 2) {
  518. form[prop + '-default'] = [new Date(range[0]), new Date(range[1])]
  519. }
  520. } else {
  521. form[prop + '-start'] = range[0] ?? ''
  522. form[prop + '-end'] = range[1] ?? ''
  523. }
  524. } else if (field[key].operator == 'NULL' || field[key].operator == 'NOT NULL') {
  525. form[prop] = queryProp ? true : false
  526. } else if (field[key].render == 'datetime') {
  527. form[prop + '-default'] = new Date(queryProp)
  528. } else {
  529. form[prop] = queryProp
  530. }
  531. }
  532. this.comSearch.fieldData.set(prop, {
  533. operator: field[key].operator,
  534. render: field[key].render,
  535. })
  536. }
  537. }
  538. // 接受query再搜索
  539. if (this.table.acceptQuery) {
  540. let comSearchData: comSearchData[] = []
  541. for (const key in query) {
  542. let fieldDataTemp = this.comSearch.fieldData.get(key)
  543. comSearchData.push({
  544. field: key,
  545. val: query[key] as string,
  546. operator: fieldDataTemp.operator,
  547. render: fieldDataTemp.render,
  548. })
  549. }
  550. this.table.filter!.search = JSON.stringify(comSearchData)
  551. }
  552. this.comSearch.form = Object.assign(this.comSearch.form, form)
  553. }
  554. }