背景

脑图可以很好的展示层级结构,但展示信息有限,并且当前的脑图无法做到真正的增量,每次保存都是整个脑图数据。而树形的表格可以根据需要展示更多节点相关的信息,如更新者、更新时间、关联的Stroy等等都能更加直观,且能做到时时增量保存。

Elemen UI 的table 自身提供了不错的功能支持,但经过一番对比,最终选取了第三方组件 Vxe-table , 下面就直接以 Vxe-table 演示下。

效果:

image.png

全局安装

  1. npm install xe-utils@3 vxe-table@3

main.js 添加引入,同时也可以根据需要按需引入,可以减少文件大小

  1. import 'xe-utils'
  2. import VXETable from 'vxe-table'
  3. import 'vxe-table/lib/style.css'
  4. Vue.use(VXETable)

Demo 使用

创建Data.js 格式如下:

  1. {
  2. "code": "200",
  3. "message": "成功",
  4. "data": {
  5. "spaceId": "5453",
  6. "template": "right",
  7. "theme": "fresh-green",
  8. "mapVersion": "1.4.43",
  9. "root": {
  10. "data": {
  11. "createUser": "xiaomin.huang01@luckincoffee.com",
  12. "createTime": "2020-09-15 20:50:39",
  13. "updateUser": "xiaomin.huang01@luckincoffee.com",
  14. "updateTime": "2020-09-18 13:50:55",
  15. "id": "c5nyfcxxfc00",
  16. "text": "取餐方式",
  17. "note": "",
  18. "progress": null,
  19. "priority": null,
  20. "hyberlinks": {
  21. "link": "",
  22. "title": ""
  23. },
  24. "testResult": 0,
  25. "auto": 0,
  26. "resource": null,
  27. "leafNode": 0,
  28. "presentationJson": "{\"created\":1600174238683,\"expandState\":\"expand\"}",
  29. "storyNo": "",
  30. "latestTestResult": 0,
  31. "latestReviewResult": 0,
  32. "testCount": "0"
  33. },
  34. "children": [
  35. {
  36. "data": {
  37. "createUser": "xiaomin.huang01@luckincoffee.com",
  38. "createTime": "2020-09-15 20:56:25",
  39. "updateUser": "xiaomin.huang01@luckincoffee.com",
  40. "updateTime": "2020-09-18 13:50:55",
  41. "id": "c5nyjpjpwo00",
  42. "text": "取餐码",
  43. "note": "",
  44. "progress": null,
  45. "priority": null,
  46. "hyberlinks": {
  47. "link": "",
  48. "title": ""
  49. },
  50. "testResult": null,
  51. "auto": null,
  52. "resource": null,
  53. "leafNode": 0,
  54. "presentationJson": "{\"created\":1600174579578,\"expandState\":\"expand\"}",
  55. "storyNo": "",
  56. "latestTestResult": 0,
  57. "latestReviewResult": 0,
  58. "testCount": "0"
  59. },
  60. "children": [
  61. {
  62. "data": {
  63. "createUser": "xiaomin.huang01@luckincoffee.com",
  64. "createTime": "2020-09-15 22:04:27",
  65. "updateUser": "xiaomin.huang01@luckincoffee.com",
  66. "updateTime": "2020-09-18 13:50:55",
  67. "id": "c5nzzt1xf680",
  68. "text": "取餐码制作完成",
  69. "note": "",
  70. "progress": null,
  71. "priority": null,
  72. "hyberlinks": {
  73. "link": "",
  74. "title": ""
  75. },
  76. "testResult": null,
  77. "auto": null,
  78. "resource": null,
  79. "leafNode": 0,
  80. "presentationJson": "{\"created\":1600178662146,\"expandState\":\"expand\"}",
  81. "storyNo": "",
  82. "latestTestResult": 0,
  83. "latestReviewResult": 0,
  84. "testCount": "0"
  85. },
  86. "children": [
  87. {
  88. "data": {
  89. "createUser": "xiaomin.huang01@luckincoffee.com",
  90. "createTime": "2020-09-15 22:12:03",
  91. "updateUser": "xiaomin.huang01@luckincoffee.com",
  92. "updateTime": "2020-09-18 13:50:55",
  93. "id": "c5o05mduurk0",
  94. "text": "制作成功",
  95. "note": "",
  96. "progress": null,
  97. "priority": null,
  98. "hyberlinks": {
  99. "link": "",
  100. "title": ""
  101. },
  102. "testResult": null,
  103. "auto": null,
  104. "resource": null,
  105. "leafNode": 0,
  106. "presentationJson": "{\"created\":1600179117815,\"expandState\":\"expand\"}",
  107. "storyNo": "",
  108. "latestTestResult": 0,
  109. "latestReviewResult": 0,
  110. "testCount": "0"
  111. },
  112. "children": [
  113. {
  114. "data": {
  115. "createUser": "xiaomin.huang01@luckincoffee.com",
  116. "createTime": "2020-09-15 22:14:40",
  117. "updateUser": "xiaomin.huang01@luckincoffee.com",
  118. "updateTime": "2021-03-15 15:13:15",
  119. "id": "c5o07infjls0",
  120. "text": "toast提示",
  121. "note": "",
  122. "progress": null,
  123. "priority": 2,
  124. "hyberlinks": {
  125. "link": "",
  126. "title": ""
  127. },
  128. "testResult": null,
  129. "auto": null,
  130. "resource": null,
  131. "leafNode": 1,
  132. "presentationJson": "{\"created\":1600179266415,\"expandState\":\"expand\",\"updated\":1615792395856}",
  133. "storyNo": "",
  134. "latestTestResult": 0,
  135. "latestReviewResult": 0,
  136. "testCount": "0"
  137. },
  138. "children": []
  139. },
  140. {
  141. "data": {
  142. "createUser": "xiaomin.huang01@luckincoffee.com",
  143. "createTime": "2020-09-15 22:14:40",
  144. "updateUser": "xiaomin.huang01@luckincoffee.com",
  145. "updateTime": "2020-09-18 13:50:55",
  146. "id": "c5o07klqfvs0",
  147. "text": "提示语",
  148. "note": "",
  149. "progress": null,
  150. "priority": null,
  151. "hyberlinks": {
  152. "link": "",
  153. "title": ""
  154. },
  155. "testResult": null,
  156. "auto": null,
  157. "resource": null,
  158. "leafNode": 0,
  159. "presentationJson": "{\"created\":1600179270666,\"expandState\":\"expand\"}",
  160. "storyNo": "",
  161. "latestTestResult": 0,
  162. "latestReviewResult": 0,
  163. "testCount": "0"
  164. },
  165. "children": [
  166. {
  167. "data": {
  168. "createUser": "xiaomin.huang01@luckincoffee.com",
  169. "createTime": "2020-09-15 22:14:40",
  170. "updateUser": "xiaomin.huang01@luckincoffee.com",
  171. "updateTime": "2021-03-15 15:13:18",
  172. "id": "c5o07klr4xc0",
  173. "text": "取餐码xxx的订单已制作完成",
  174. "note": "",
  175. "progress": null,
  176. "priority": 2,
  177. "hyberlinks": {
  178. "link": "",
  179. "title": ""
  180. },
  181. "testResult": null,
  182. "auto": null,
  183. "resource": null,
  184. "leafNode": 1,
  185. "presentationJson": "{\"created\":1600179270667,\"expandState\":\"expand\",\"updated\":1615792398055}",
  186. "storyNo": "",
  187. "latestTestResult": 0,
  188. "latestReviewResult": 0,
  189. "testCount": "0"
  190. },
  191. "children": []
  192. }
  193. ]
  194. }
  195. ]
  196. }
  197. ]
  198. }
  199. ]
  200. }
  201. ]
  202. }
  203. }
  204. }

HTML

  1. <template>
  2. <div>
  3. <vxe-toolbar :refresh="{query: reload}" custom>
  4. <template #buttons>
  5. <vxe-button @click="$refs.xTree.setAllTreeExpand(true)">
  6. 展开所有
  7. </vxe-button>
  8. <vxe-button @click="$refs.xTree.clearTreeExpand()">
  9. 关闭所有
  10. </vxe-button>
  11. <vxe-button @click="insertEvent()">
  12. 新增
  13. </vxe-button>
  14. <!-- <vxe-button @click="getInsertEvent">获取新增</vxe-button>-->
  15. <vxe-button @click="getNewData">
  16. 获取新增数据
  17. </vxe-button>
  18. <vxe-button @click="getRemoveEvent">
  19. 获取删除
  20. </vxe-button>
  21. <vxe-button @click="getUpdateEvent">
  22. 获取修改
  23. </vxe-button>
  24. </template>
  25. </vxe-toolbar>
  26. <vxe-table
  27. ref="xTree"
  28. height="1024"
  29. resizable
  30. row-key
  31. :data="tableData"
  32. highlight-hover-row
  33. highlight-current-row
  34. highlight-hover-column
  35. highlight-current-column
  36. keep-source
  37. :keyboard-config="{isArrow: true, isEnter: true}"
  38. :tree-config="{children: 'children',iconClose: 'el-icon-folder',iconOpen: 'el-icon-folder-opened',line: true}"
  39. :edit-config="{trigger: 'dblclick', mode: 'cell',showStatus: true}"
  40. @edit-closed="editClosedEvent"
  41. >
  42. <!-- <vxe-column type="seq"></vxe-column>-->
  43. <!-- 行拖拽-->
  44. <vxe-table-column width="60">
  45. <template v-slot>
  46. <span class="drag-btn">
  47. <i class="vxe-icon--menu"></i>
  48. </span>
  49. </template>
  50. <template v-slot:header>
  51. <vxe-tooltip v-model="showHelpTip" content="按住后可以上下拖动排序!" enterable>
  52. <i class="vxe-icon--question" @click="showHelpTip = !showHelpTip"></i>
  53. </vxe-tooltip>
  54. </template>
  55. </vxe-table-column>
  56. <vxe-column
  57. field="data.text"
  58. title="用例描述"
  59. width="600"
  60. tree-node
  61. :edit-render="{name: 'input', attrs: {type: 'text'}}"
  62. />
  63. <vxe-column
  64. field="data.priority"
  65. title="优先级"
  66. width="100"
  67. :edit-render="{name: '$select', options: priorityList, optionProps: {value: 'value', label: 'label'}}"
  68. :filters="[{label: 'P0', value: '0'}, {label: 'P1', value: '1'},{label: 'P2', value: '2'},{label: 'P3', value: '3'}]"
  69. />
  70. <vxe-column field="data.resource" title="标签"/>
  71. <vxe-column field="data.storyNo" title="关联Story"/>
  72. <vxe-column field="data.latestReviewResult" title="评审结果"/>
  73. <vxe-column field="data.note" title="备注"/>
  74. <vxe-column field="data.updateTime" title="更新时间"/>
  75. <vxe-column field="data.updateUser" title="更新者"/>
  76. <!-- <vxe-column field="data.createTime" title="创建时间" />-->
  77. <!-- <vxe-column field="data.createUser" title="创建者" />-->
  78. <!-- <vxe-table-column field="attr3" title="Image">
  79. <template #default>
  80. <img src="/vxe-table/static/other/img1.gif" height="50">
  81. </template>
  82. </vxe-table-column>-->
  83. <vxe-table-column title="操作" fixed="right" width="300">
  84. <template v-slot="scope">
  85. <vxe-button size="small" @click="handleAddChildren(scope.rowIndex, scope.row)">
  86. 添加子节点
  87. </vxe-button>
  88. <vxe-button size="small" @click="removeNode(scope.rowIndex, scope.row)">
  89. 删除节点
  90. </vxe-button>
  91. <vxe-button size="small" @click="showNodeData(scope.rowIndex, scope.row)">
  92. 查看详情
  93. </vxe-button>
  94. </template>
  95. </vxe-table-column>
  96. </vxe-table>
  97. </div>
  98. </template>

JavaScript:

  1. import {TREE_DATA} from './data'
  2. import Sortable from 'sortablejs'
  3. import XEUtils from 'xe-utils'
  4. export default {
  5. name: "TreeTable",
  6. data() {
  7. return {
  8. tableData: TREE_DATA.data.root.children,
  9. showHelpTip: false,
  10. priorityList: [
  11. {label: 'P0', value: '0'},
  12. {label: 'P1', value: '1'},
  13. {label: 'P2', value: '2'},
  14. {label: 'P3', value: '3'}
  15. ],
  16. }
  17. },
  18. created() {
  19. // 行拖动 https://xuliangzhan_admin.gitee.io/vxe-table/#/table/other/sortableRow
  20. this.rowDrop()
  21. },
  22. beforeDestroy() {
  23. if (this.sortable) {
  24. this.sortable.destroy()
  25. }
  26. },
  27. methods: {
  28. reload() {
  29. console.info('清除数据')
  30. // 清除所有状态
  31. this.$refs.xTree.clearAll();
  32. return this.getNewRow();
  33. },
  34. // 自动保存
  35. editClosedEvent({row, column}) {
  36. const $table = this.$refs.xTree
  37. const field = column.property
  38. const arr = field.toString().split(".");
  39. const cellValue = row.data[arr[1]]
  40. // 判断单元格值是否被修改
  41. if ($table.isUpdateByRow(row, field)) {
  42. setTimeout(() => {
  43. console.info(row);
  44. this.$notify({
  45. title: '保存成功',
  46. message: `局部保存成功! ${field}=${cellValue}`,
  47. type: 'success'
  48. })
  49. // 局部更新单元格为已保存状态
  50. $table.reloadRow(row, null, field)
  51. console.info(row);
  52. }, 300)
  53. }
  54. },
  55. //移除节点
  56. removeNode(index, row) {
  57. // 删除可能需要后端帮忙, 前端告知删除的ID 后端遍历,然后返回最新数据
  58. console.info(index, row);
  59. this.tableData.splice(index, index);
  60. // const $table = this.$refs.xTree
  61. // $table.remove(row)
  62. // console.info(this.tableData)
  63. },
  64. // 查看节点详情
  65. showNodeData(index, row) {
  66. console.info(row);
  67. },
  68. //获取所有的新增数据
  69. getNewData() {
  70. const insertRecords = this.tableData.filter(row => row.data._isNew)
  71. console.info(insertRecords);
  72. },
  73. insertEvent(row) {
  74. // 从最后新增
  75. this.tableData.push(this.getNewRow())
  76. },
  77. // 添加子节点 https://codesandbox.io/s/vxe-table-charuzijiedianwentiyanshi-forked-3t0k4?file=/src/views/Demo1.vue:562-797
  78. handleAddChildren(index, row) {
  79. console.info("row", row)
  80. if (!row.children) {
  81. this.$set(row, "children", [this.getNewRow()]);
  82. } else {
  83. row.children.push(this.getNewRow());
  84. }
  85. this.tableData = [...this.tableData];
  86. },
  87. // 插入节点信息
  88. getNewRow() {
  89. // 通过_isNew 来判断是否是新增节点
  90. return {
  91. "data": {
  92. "_isNew": true,
  93. "createUser": "shijin.huang01@luckincoffee.com",
  94. "createTime": new Date().getTime(),
  95. "updateUser": "shijin.huang@luckincoffee.com",
  96. "updateTime": new Date().getTime(),
  97. "id": new Date().getTime(),
  98. "text": "",
  99. "note": "",
  100. "progress": null,
  101. "priority": null,
  102. "hyberlinks": {
  103. "link": "",
  104. "title": ""
  105. },
  106. "testResult": null,
  107. "auto": null,
  108. "resource": null,
  109. "leafNode": 0,
  110. "presentationJson": "{\"created\":" + new Date().getTime() + ",\"expandState\":\"expand\"}",
  111. "storyNo": "",
  112. "latestTestResult": 0,
  113. "latestReviewResult": 0,
  114. "testCount": "0"
  115. },
  116. "children": []
  117. }
  118. },
  119. // 获取插入的数据, 这个对数子节点不适用
  120. getInsertEvent() {
  121. const $table = this.$refs.xTree
  122. const insertRecords = $table.getInsertRecords()
  123. console.info(insertRecords.length)
  124. },
  125. // 获取删除的数据, 这个对树子节点不适用
  126. getRemoveEvent() {
  127. const $table = this.$refs.xTree
  128. const removeRecords = $table.getRemoveRecords()
  129. console.info(removeRecords.length)
  130. },
  131. // 获取更新的数据, 这个对树子节点不适用
  132. getUpdateEvent() {
  133. // 需要 表格设置加入 keep-source
  134. const $table = this.$refs.xTree
  135. const updateRecords = $table.getUpdateRecords()
  136. console.info(updateRecords)
  137. console.info(updateRecords.length)
  138. },
  139. // 拖拽 后保存怎么处理?
  140. rowDrop() {
  141. this.$nextTick(() => {
  142. const xTable = this.$refs.xTree
  143. this.sortable = Sortable.create(xTable.$el.querySelector('.body--wrapper>.vxe-table--body tbody'), {
  144. handle: '.drag-btn',
  145. onEnd: ({item, oldIndex}) => {
  146. const options = {children: 'children'}
  147. const targetTrElem = item
  148. const wrapperElem = targetTrElem.parentNode
  149. const prevTrElem = targetTrElem.previousElementSibling
  150. const tableTreeData = this.tableData
  151. const selfRow = xTable.getRowNode(targetTrElem).item
  152. const selfNode = XEUtils.findTree(tableTreeData, row => row === selfRow, options)
  153. if (prevTrElem) {
  154. // 移动到节点
  155. const prevRow = xTable.getRowNode(prevTrElem).item
  156. const prevNode = XEUtils.findTree(tableTreeData, row => row === prevRow, options)
  157. if (XEUtils.findTree(selfRow[options.children], row => prevRow === row, options)) {
  158. // 错误的移动
  159. const oldTrElem = wrapperElem.children[oldIndex]
  160. wrapperElem.insertBefore(targetTrElem, oldTrElem)
  161. return this.$notify({
  162. message: '不允许自己给自己拖动!',
  163. type: 'error',
  164. duration: 3000
  165. });
  166. }
  167. const currRow = selfNode.items.splice(selfNode.index, 1)[0]
  168. if (xTable.isTreeExpandByRow(prevRow)) {
  169. // 移动到当前的子节点
  170. prevRow[options.children].splice(0, 0, currRow)
  171. } else {
  172. // 移动到相邻节点
  173. prevNode.items.splice(prevNode.index + (selfNode.index < prevNode.index ? 0 : 1), 0, currRow)
  174. }
  175. } else {
  176. // 移动到第一行
  177. const currRow = selfNode.items.splice(selfNode.index, 1)[0]
  178. tableTreeData.unshift(currRow)
  179. }
  180. // 如果变动了树层级,需要刷新数据
  181. this.tableData = [...tableTreeData]
  182. console.info(this.tableData)
  183. }
  184. })
  185. })
  186. }
  187. }
  188. }

CSS;

  1. .sortable-row-demo .drag-btn {
  2. cursor: move;
  3. font-size: 12px;
  4. }
  5. .sortable-row-demo .vxe-body--row.sortable-ghost,
  6. .sortable-row-demo .vxe-body--row.sortable-chosen {
  7. background-color: #dfecfb;
  8. }