• Vue3 + Element-plus + TS —— 动态表格自由编辑


    9a69fede8b2044a79dd834e3e48f20b4.png前期回顾   

    《 穿越时空的代码、在回首:Evil.js两年后的全新解读 》-CSDN博客

    el-tree-CSDN博客">Vue3 + TS + Element-Plus 封装Tree组件 《亲测可用》_ icon-default.png?t=N7T8https://blog.csdn.net/m0_57904695/article/details/131664157?spm=1001.2014.3001.5501  

    态表格 自由编辑

     

    目录

    ♻️ 效果图 

     🚩 Vue2 版本

    🐗 Vue3 版本


    ♻️ 效果图 

    👉 在线预览

     🚩 Vue2 版本

    1. <script>
    2. export default {
    3. data() {
    4. return {
    5. columnList: [
    6. { prop: "name", label: "姓名" },
    7. { prop: "age", label: "年龄" },
    8. { prop: "city", label: "城市" },
    9. { prop: "tel", label: "电话" }
    10. ],
    11. tableData: [
    12. { name: "张三", age: 24, city: "广州", tel: "13312345678" },
    13. { name: "李四", age: 25, city: "九江", tel: "18899998888" }
    14. ],
    15. showMenu: false, // 显示右键菜单
    16. showEditInput: false, // 显示单元格/表头内容编辑输入框
    17. curTarget: {
    18. // 当前目标信息
    19. rowIdx: null, // 行下标
    20. colIdx: null, // 列下标
    21. val: null, // 单元格内容/列名
    22. isHead: undefined // 当前目标是表头?
    23. },
    24. countCol: 0 // 新建列计数
    25. };
    26. },
    27. computed: {
    28. curColumn() {
    29. return this.columnList[this.curTarget.colIdx] || {};
    30. }
    31. },
    32. methods: {
    33. // 删除当前列或当前行
    34. openColumnOrRowSpringFrame(type) {
    35. this.$confirm(
    36. `此操作将永久删除该 ${type === "列" ? "列" : "行"}, 是否继续 ?, '提示'`,
    37. {
    38. confirmButtonText: "确定",
    39. cancelButtonText: "取消",
    40. type: "warning"
    41. }
    42. )
    43. .then(() => {
    44. if (type === "列") {
    45. this.delColumn();
    46. } else if (type === "行") {
    47. this.delRow();
    48. }
    49. this.$message({
    50. type: "success",
    51. message: "删除成功!"
    52. });
    53. })
    54. .catch(() => {
    55. this.$message({
    56. type: "info",
    57. message: "已取消删除"
    58. });
    59. });
    60. },
    61. // 回车键关闭编辑框
    62. onKeyUp(e) {
    63. if (e.keyCode === 13) {
    64. this.showEditInput = false;
    65. }
    66. },
    67. // 单元格双击事件 - 更改单元格数值
    68. cellDblclick(row, column, cell, event) {
    69. this.showEditInput = false;
    70. if (column.index == null) return;
    71. this.locateMenuOrEditInput("editInput", 200, event); // 编辑框定位
    72. this.showEditInput = true;
    73. // 当前目标
    74. this.curTarget = {
    75. rowIdx: row.row_index,
    76. colIdx: column.index,
    77. val: row[column.property],
    78. isHead: false
    79. };
    80. },
    81. // 单元格/表头右击事件 - 打开菜单
    82. rightClick(row, column, event) {
    83. // 阻止浏览器自带的右键菜单弹出
    84. event.preventDefault(); // window.event.returnValue = false
    85. this.showMenu = false;
    86. if (column.index == null) return;
    87. this.locateMenuOrEditInput("contextmenu", 140, event); // 菜单定位
    88. this.showMenu = true;
    89. // 当前目标
    90. this.curTarget = {
    91. rowIdx: row ? row.row_index : null, // 目标行下标,表头无 row_index
    92. colIdx: column.index, // 目标项下标
    93. val: row ? row[column.property] : column.label, // 目标值,表头记录名称
    94. isHead: !row
    95. };
    96. },
    97. // 去更改列名
    98. renameCol($event) {
    99. this.showEditInput = false;
    100. if (this.curTarget.colIdx === null) return;
    101. this.locateMenuOrEditInput("editInput", 200, $event); // 编辑框定位
    102. this.showEditInput = true;
    103. },
    104. // 更改单元格内容/列名
    105. updTbCellOrHeader(val) {
    106. if (!this.curTarget.isHead)
    107. // 更改单元格内容
    108. this.tableData[this.curTarget.rowIdx][this.curColumn.prop] = val;
    109. else {
    110. // 更改列名
    111. if (!val) return;
    112. this.columnList[this.curTarget.colIdx].label = val;
    113. }
    114. },
    115. // 新增行
    116. addRow(later) {
    117. this.showMenu = false;
    118. const idx = later ? this.curTarget.rowIdx + 1 : this.curTarget.rowIdx;
    119. let obj = {};
    120. this.columnList.forEach((p) => (obj[p.prop] = ""));
    121. this.tableData.splice(idx, 0, obj);
    122. },
    123. // 表头下新增行
    124. addRowHead() {
    125. // 关闭菜单
    126. this.showMenu = false;
    127. // 新增行
    128. let obj = {};
    129. // 初始化行数据
    130. this.columnList.forEach((p) => (obj[p.prop] = ""));
    131. // 插入行数据
    132. this.tableData.unshift(obj);
    133. },
    134. // 删除行
    135. delRow() {
    136. this.showMenu = false;
    137. this.curTarget.rowIdx !== null &&
    138. this.tableData.splice(this.curTarget.rowIdx, 1);
    139. },
    140. // 新增列
    141. addColumn(later) {
    142. this.showMenu = false;
    143. const idx = later ? this.curTarget.colIdx + 1 : this.curTarget.colIdx;
    144. const colStr = { prop: "col_" + ++this.countCol, label: "" };
    145. this.columnList.splice(idx, 0, colStr);
    146. this.tableData.forEach((p) => (p[colStr.prop] = ""));
    147. },
    148. // 删除列
    149. delColumn() {
    150. this.showMenu = false;
    151. this.tableData.forEach((p) => {
    152. delete p[this.curColumn.prop];
    153. });
    154. this.columnList.splice(this.curTarget.colIdx, 1);
    155. },
    156. // 添加表格行下标
    157. tableRowClassName({ row, rowIndex }) {
    158. row.row_index = rowIndex;
    159. },
    160. // 定位菜单/编辑框
    161. locateMenuOrEditInput(eleId, eleWidth, event) {
    162. let ele = document.getElementById(eleId);
    163. ele.style.top = event.clientY - 100 + "px";
    164. ele.style.left = event.clientX - 50 + "px";
    165. if (window.innerWidth - eleWidth < event.clientX) {
    166. ele.style.left = "unset";
    167. ele.style.right = 0;
    168. }
    169. }
    170. }
    171. };
    172. script>
    173. <style lang="scss" scoped>
    174. #hello {
    175. position: relative;
    176. height: 100%;
    177. width: 100%;
    178. }
    179. .tips {
    180. margin-top: 10px;
    181. color: #999;
    182. }
    183. #contextmenu {
    184. position: absolute;
    185. top: 0;
    186. left: 0;
    187. height: auto;
    188. width: 120px;
    189. border-radius: 3px;
    190. box-shadow: 0 0 10px 10px #e4e7ed;
    191. background-color: #fff;
    192. border-radius: 6px;
    193. padding: 15px 0 10px 15px;
    194. button {
    195. display: block;
    196. margin: 0 0 5px;
    197. }
    198. }
    199. .hideContextMenu {
    200. position: absolute;
    201. top: -4px;
    202. right: -5px;
    203. }
    204. #editInput,
    205. #headereditInput {
    206. position: absolute;
    207. top: 0;
    208. left: 0;
    209. height: auto;
    210. min-width: 200px;
    211. max-width: 400px;
    212. padding: 0;
    213. }
    214. #editInput .el-input,
    215. #headereditInput .el-input {
    216. outline: 0;
    217. border: 1px solid #c0f2f9;
    218. border-radius: 5px;
    219. box-shadow: 0px 0px 10px 0px #c0f2f9;
    220. }
    221. .line {
    222. width: 100%;
    223. border: 1px solid #e4e7ed;
    224. margin: 10px 0;
    225. }
    226. style>

    🐗 Vue3 版本

    1. <script setup lang="ts">
    2. import { ref, reactive, computed, toRefs, nextTick } from 'vue';
    3. import { ElMessage, ElMessageBox } from 'element-plus';
    4. import { DeleteFilled, CaretBottom, CaretTop, EditPen } from '@element-plus/icons-vue';
    5. // Tips: locateMenuOrEditInput 可调整编辑框位置
    6. interface Column {
    7. prop: string;
    8. label: string;
    9. type?: string;
    10. }
    11. interface Data {
    12. choiceCode: string;
    13. choiceContent: string;
    14. riskIds: string;
    15. itemScore: string | number;
    16. [key: string]: unknown;
    17. }
    18. interface Target {
    19. rowIdx: number | null;
    20. colIdx: number | null;
    21. val: string | null;
    22. isHead: boolean | undefined;
    23. }
    24. // 接收addEdit父组件传过来的数据,用于判断是新增、编辑、详情页面
    25. const paramsIdType = 'detail';
    26. const state = reactive({
    27. columnList: [
    28. { prop: 'choiceCode', label: '选项编码' },
    29. { prop: 'choiceContent', label: '选项内容' },
    30. { prop: 'riskIds', label: '风险点', type: 'button' },
    31. { prop: 'itemScore', label: '选项分值', type: 'input-number' },
    32. ] as Column[],
    33. questionChoiceVOlist: [
    34. {
    35. choiceCode: 'A',
    36. choiceContent: '是',
    37. riskIds: '45,47',
    38. itemScore: 1,
    39. isClickCheckBtn: true,
    40. id: 1,
    41. },
    42. {
    43. choiceCode: 'B',
    44. choiceContent: '否',
    45. riskIds: '46',
    46. itemScore: 4,
    47. isClickCheckBtn: true,
    48. id: 2,
    49. },
    50. {
    51. choiceCode: 'C',
    52. choiceContent: '否',
    53. riskIds: '',
    54. itemScore: 4,
    55. isClickCheckBtn: true,
    56. id: 3,
    57. },
    58. ] as Data[],
    59. showMenu: false, // 显示右键菜单
    60. showEditInput: false, // 显示单元格/表头内容编辑输入框
    61. curTarget: {
    62. // 当前目标信息
    63. rowIdx: null, // 行下标
    64. colIdx: null, // 列下标
    65. val: null, // 单元格内容/列名
    66. isHead: undefined, // 当前目标是表头?
    67. } as Target,
    68. countCol: 0, // 新建列计数
    69. });
    70. const iptRef = ref();
    71. const { columnList, questionChoiceVOlist, showMenu, showEditInput, curTarget } = toRefs(state);
    72. // 当前列
    73. const curColumn = computed(() => {
    74. return curTarget.value.colIdx !== null
    75. ? columnList.value[curTarget.value.colIdx]
    76. : { prop: '', label: '' };
    77. });
    78. // 计算风险点数量
    79. const getRiskLenght = computed(() => {
    80. return (riskIds: string) => riskIds.split(',').filter(Boolean).length;
    81. });
    82. /**
    83. * 删除列/行
    84. * @method delColumn
    85. * @param { string } type - '列' | '行'
    86. **/
    87. const openColumnOrRowSpringFrame = (type: string) => {
    88. ElMessageBox.confirm(`此操作将永久删除该${type === '列' ? '列' : '行'}, 是否继续 ?, '提示'`, {
    89. confirmButtonText: '确定',
    90. cancelButtonText: '取消',
    91. type: 'warning',
    92. })
    93. .then(() => {
    94. if (type === '列') {
    95. delColumn();
    96. } else if (type === '行') delRow();
    97. ElMessage.success('删除成功');
    98. })
    99. .catch(() => ElMessage.info('已取消删除'));
    100. };
    101. // 回车键关闭编辑框
    102. const onKeyUp = (e: KeyboardEvent) => {
    103. if (e.key === 'Enter') {
    104. showEditInput.value = false;
    105. }
    106. };
    107. // 控制某字段不能打开弹框
    108. const isPop = (column: { label: string }) => {
    109. return column.label === '风险点' || column.label === '选项分值';
    110. };
    111. // 左键输入框
    112. const cellClick = (
    113. row: { [x: string]: any; row_index: any },
    114. column: { index: null; property: string | number; label: string },
    115. _cell: any,
    116. event: MouseEvent
    117. ) => {
    118. // 如果是风险点或选项分值,不执行后续代码
    119. if (isPop(column)) return;
    120. iptRef.value.focus();
    121. if (column.index == null) return;
    122. locateMenuOrEditInput('editInput', -300, event); // 左键输入框定位 Y
    123. showEditInput.value = true;
    124. iptRef.value.focus();
    125. // 当前目标
    126. curTarget.value = {
    127. rowIdx: row.row_index,
    128. colIdx: column.index,
    129. val: row[column.property],
    130. isHead: false,
    131. };
    132. };
    133. // 表头右击事件 - 打开菜单
    134. const rightClick = (row: any, column: any, event: MouseEvent) => {
    135. event.preventDefault();
    136. if (column.index == null) return;
    137. // 如果tableData有数据并且当前目标是表头,那么就返回,不执行后续操作
    138. // if (questionChoiceVOlist.value.length > 0 && !row) return;
    139. if (isPop(column)) return;
    140. showMenu.value = false;
    141. locateMenuOrEditInput('contextmenu', -500, event); // 右键输入框
    142. showMenu.value = true;
    143. curTarget.value = {
    144. rowIdx: row ? row.row_index : null,
    145. colIdx: column.index,
    146. val: row ? row[column.property] : column.label,
    147. isHead: !row,
    148. };
    149. };
    150. // 更改列名
    151. const renameCol = () => {
    152. showEditInput.value = false;
    153. if (curTarget.value.colIdx === null) return;
    154. showEditInput.value = true;
    155. nextTick(() => {
    156. iptRef.value.focus();
    157. });
    158. };
    159. // 更改单元格内容/列名
    160. const updTbCellOrHeader = (val: string) => {
    161. if (!curTarget.value.isHead) {
    162. if (curTarget.value.rowIdx !== null) {
    163. (questionChoiceVOlist.value[curTarget.value.rowIdx] as Data)[curColumn.value.prop] =
    164. val;
    165. }
    166. } else {
    167. if (!val) return;
    168. if (curTarget.value.colIdx !== null) {
    169. columnList.value[curTarget.value.colIdx].label = val;
    170. }
    171. }
    172. };
    173. // 新增行
    174. const addRow = (later: boolean) => {
    175. showMenu.value = false;
    176. const idx = later ? curTarget.value.rowIdx! + 1 : curTarget.value.rowIdx!;
    177. let obj: any = {};
    178. columnList.value.forEach((p) => obj[p.prop]);
    179. questionChoiceVOlist.value.splice(idx, 0, obj);
    180. // 设置新增行数据默认值
    181. questionChoiceVOlist.value[idx] = {
    182. choiceCode: '',
    183. choiceContent: '',
    184. riskIds: '',
    185. itemScore: 0,
    186. id: Math.floor(Math.random() * 100000),
    187. };
    188. };
    189. // 表头下新增行
    190. const addRowHead = () => {
    191. showMenu.value = false;
    192. let obj: any = {};
    193. columnList.value.forEach((p) => obj[p.prop]);
    194. questionChoiceVOlist.value.unshift(obj);
    195. questionChoiceVOlist.value[0] = {
    196. choiceCode: '',
    197. choiceContent: '',
    198. riskIds: '',
    199. itemScore: 0,
    200. id: Math.floor(Math.random() * 100000),
    201. };
    202. };
    203. // 删除行
    204. const delRow = () => {
    205. showMenu.value = false;
    206. curTarget.value.rowIdx !== null &&
    207. questionChoiceVOlist.value.splice(curTarget.value.rowIdx!, 1);
    208. };
    209. // 新增列
    210. const addColumn = (later: boolean) => {
    211. showMenu.value = false;
    212. const idx = later ? curTarget.value.colIdx! + 1 : curTarget.value.colIdx!;
    213. const colStr = { prop: 'Zk-NewCol - ' + ++state.countCol, label: '' };
    214. columnList.value.splice(idx, 0, colStr);
    215. questionChoiceVOlist.value.forEach((p) => (p[colStr.prop] = ''));
    216. };
    217. // 删除列
    218. const delColumn = () => {
    219. showMenu.value = false;
    220. questionChoiceVOlist.value.forEach((p) => {
    221. delete p[curColumn.value.prop];
    222. });
    223. columnList.value.splice(curTarget.value.colIdx!, 1);
    224. };
    225. // 添加表格行下标
    226. const tableRowClassName = ({ row, rowIndex }: { row: any; rowIndex: number }) => {
    227. row.row_index = rowIndex;
    228. };
    229. // 定位菜单/编辑框
    230. const locateMenuOrEditInput = (eleId: string, distance: number, event: MouseEvent) => {
    231. if (window.innerWidth < 1130 || window.innerWidth < 660)
    232. return ElMessage.warning('窗口太小,已经固定菜单位置,或请重新调整窗口大小');
    233. const ele = document.getElementById(eleId) as HTMLElement;
    234. const x = event.pageX;
    235. const y = event.clientY + 200; //右键菜单位置 Y
    236. let left = x + distance + 200; //右键菜单位置 X
    237. let top;
    238. if (eleId == 'editInput') {
    239. // 左键
    240. top = y + distance;
    241. left = x + distance - 120;
    242. } else {
    243. // 右键
    244. top = y + distance + 170;
    245. }
    246. ele.style.left = `${left}px`;
    247. ele.style.top = `${top}px`;
    248. };
    249. defineExpose({
    250. questionChoiceVOlist,
    251. });
    252. script>
    253. <style lang="scss" scoped>
    254. #table-wrap {
    255. width: 100%;
    256. height: 100%;
    257. /* 左键 */
    258. #contextmenu {
    259. position: absolute;
    260. display: flex;
    261. flex-direction: column;
    262. align-items: center;
    263. justify-content: center;
    264. z-index: 999999;
    265. top: 0;
    266. left: 0;
    267. height: auto;
    268. width: 200px;
    269. border-radius: 3px;
    270. border: #e2e2e2 1px solid;
    271. box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
    272. background-color: #fff;
    273. border-radius: 6px;
    274. padding: 15px 10px 14px 12px;
    275. button {
    276. display: block;
    277. margin: 0 0 5px;
    278. }
    279. }
    280. /* 右键 */
    281. #editInput {
    282. position: absolute;
    283. top: 0;
    284. left: 0;
    285. z-index: 999999;
    286. }
    287. /* 实时数据 */
    288. pre {
    289. border: 1px solid #cccccc;
    290. padding: 10px;
    291. overflow: auto;
    292. }
    293. }
    294. style>

    7730e2bd39d64179909767e1967da702.jpeg

     _______________________________  期待再见  _______________________________

  • 相关阅读:
    [论文笔记]UNILM
    第65篇 QML 之 JS中的对象创建、删除属性、遍历对象
    【含泪提速!】一文全解相似度算法、跟踪算法在各个AI场景的应用(附代码)
    2.2 IOC之基于XML管理bean
    qt hiRedis封装使用
    14种主流的RTOS 单片机操作系统~来学!
    4.四大类(DDL、DML、DQL、DCL)
    Winsows QT5.15安装教程——组件务必要选上Sources
    2.3.4 交换机的DHCP技术
    21天学习挑战赛-剖析快速排序
  • 原文地址:https://blog.csdn.net/m0_57904695/article/details/139838176