前期回顾
《 穿越时空的代码、在回首:Evil.js两年后的全新解读 》-CSDN博客
态表格 自由编辑
目录
👉 在线预览
-
- <div id="hello">
-
- <p class="tips">单击 右键菜单,单击 左键编辑p>
- <el-table
- :data="tableData"
- height="500px"
- border
- style="width: 100%; margin-top: 10px"
- @cell-click="cellDblclick"
- @header-contextmenu="(column, event) => rightClick(null, column, event)"
- @row-contextmenu="rightClick"
- :row-class-name="tableRowClassName"
- >
- <el-table-column
- v-if="columnList.length > 0"
- type="index"
- :label="'No.'"
- />
- <el-table-column
- v-for="(col, idx) in columnList"
- :key="col.prop"
- :prop="col.prop"
- :label="col.label"
- :index="idx"
- />
- el-table>
-
- <div>
- <h3 style="text-align: center">实时数据展示h3>
- <label>当前目标:label>
- <p>{{ JSON.stringify(curTarget) }}p>
- <label>表头:label>
- <p v-for="col in columnList" :key="col.prop">{{ JSON.stringify(col) }}p>
- <label>数据:label>
- <p v-for="(data, idx) in tableData" :key="idx">
- {{ JSON.stringify(data) }}
- p>
- div>
-
-
- <div v-show="showMenu" id="contextmenu" @mouseleave="showMenu = false">
- <p style="margin-bottom: 10px">列:p>
- <el-button size="mini" type="primary" @click="addColumn()">
- 前方插入一列
- el-button>
- <el-button size="mini" type="primary" @click="addColumn(true)">
- 后方插入一列
- el-button>
-
- <el-button
- type="primary"
- size="mini"
- @click="openColumnOrRowSpringFrame('列')"
- >
- 删除当前列
- el-button>
-
- <el-button size="mini" type="primary" @click="renameCol($event)">
- 更改列名
- el-button>
-
- <div class="line">div>
-
- <p style="margin-bottom: 12px">行:p>
- <el-button
- size="mini"
- type="primary"
- @click="addRow()"
- v-show="!curTarget.isHead"
- >
- 上方插入一行
- el-button>
- <el-button
- size="mini"
- type="primary"
- @click="addRow(true)"
- v-show="!curTarget.isHead"
- >
- 下方插入一行
- el-button>
- <el-button
- size="mini"
- type="primary"
- @click="addRowHead(true)"
- v-show="curTarget.isHead"
- >
- 下方插入一行
- el-button>
- <el-button
- type="primary"
- size="mini"
- @click="openColumnOrRowSpringFrame('行')"
- v-show="!curTarget.isHead"
- >
- 删除当前行
- el-button>
- div>
-
-
- <div v-show="showEditInput" id="editInput">
- <el-input
- v-focus
- placeholder="请输入内容"
- v-model="curTarget.val"
- clearable
- @change="updTbCellOrHeader"
- @blur="showEditInput = false"
- @keyup="onKeyUp($event)"
- >
- <template #prepend>{{ curColumn.label || curColumn.prop }}template>
- el-input>
- div>
- div>
-
- <script>
- export default {
- data() {
- return {
- columnList: [
- { prop: "name", label: "姓名" },
- { prop: "age", label: "年龄" },
- { prop: "city", label: "城市" },
- { prop: "tel", label: "电话" }
- ],
- tableData: [
- { name: "张三", age: 24, city: "广州", tel: "13312345678" },
- { name: "李四", age: 25, city: "九江", tel: "18899998888" }
- ],
- showMenu: false, // 显示右键菜单
- showEditInput: false, // 显示单元格/表头内容编辑输入框
- curTarget: {
- // 当前目标信息
- rowIdx: null, // 行下标
- colIdx: null, // 列下标
- val: null, // 单元格内容/列名
- isHead: undefined // 当前目标是表头?
- },
- countCol: 0 // 新建列计数
- };
- },
- computed: {
- curColumn() {
- return this.columnList[this.curTarget.colIdx] || {};
- }
- },
- methods: {
- // 删除当前列或当前行
- openColumnOrRowSpringFrame(type) {
- this.$confirm(
- `此操作将永久删除该 ${type === "列" ? "列" : "行"}, 是否继续 ?, '提示'`,
- {
- confirmButtonText: "确定",
- cancelButtonText: "取消",
- type: "warning"
- }
- )
- .then(() => {
- if (type === "列") {
- this.delColumn();
- } else if (type === "行") {
- this.delRow();
- }
- this.$message({
- type: "success",
- message: "删除成功!"
- });
- })
- .catch(() => {
- this.$message({
- type: "info",
- message: "已取消删除"
- });
- });
- },
- // 回车键关闭编辑框
- onKeyUp(e) {
- if (e.keyCode === 13) {
- this.showEditInput = false;
- }
- },
- // 单元格双击事件 - 更改单元格数值
- cellDblclick(row, column, cell, event) {
- this.showEditInput = false;
- if (column.index == null) return;
- this.locateMenuOrEditInput("editInput", 200, event); // 编辑框定位
- this.showEditInput = true;
- // 当前目标
- this.curTarget = {
- rowIdx: row.row_index,
- colIdx: column.index,
- val: row[column.property],
- isHead: false
- };
- },
- // 单元格/表头右击事件 - 打开菜单
- rightClick(row, column, event) {
- // 阻止浏览器自带的右键菜单弹出
- event.preventDefault(); // window.event.returnValue = false
- this.showMenu = false;
- if (column.index == null) return;
- this.locateMenuOrEditInput("contextmenu", 140, event); // 菜单定位
- this.showMenu = true;
- // 当前目标
- this.curTarget = {
- rowIdx: row ? row.row_index : null, // 目标行下标,表头无 row_index
- colIdx: column.index, // 目标项下标
- val: row ? row[column.property] : column.label, // 目标值,表头记录名称
- isHead: !row
- };
- },
- // 去更改列名
- renameCol($event) {
- this.showEditInput = false;
- if (this.curTarget.colIdx === null) return;
- this.locateMenuOrEditInput("editInput", 200, $event); // 编辑框定位
- this.showEditInput = true;
- },
- // 更改单元格内容/列名
- updTbCellOrHeader(val) {
- if (!this.curTarget.isHead)
- // 更改单元格内容
- this.tableData[this.curTarget.rowIdx][this.curColumn.prop] = val;
- else {
- // 更改列名
- if (!val) return;
- this.columnList[this.curTarget.colIdx].label = val;
- }
- },
- // 新增行
- addRow(later) {
- this.showMenu = false;
- const idx = later ? this.curTarget.rowIdx + 1 : this.curTarget.rowIdx;
- let obj = {};
- this.columnList.forEach((p) => (obj[p.prop] = ""));
- this.tableData.splice(idx, 0, obj);
- },
- // 表头下新增行
- addRowHead() {
- // 关闭菜单
- this.showMenu = false;
- // 新增行
- let obj = {};
- // 初始化行数据
- this.columnList.forEach((p) => (obj[p.prop] = ""));
- // 插入行数据
- this.tableData.unshift(obj);
- },
- // 删除行
- delRow() {
- this.showMenu = false;
- this.curTarget.rowIdx !== null &&
- this.tableData.splice(this.curTarget.rowIdx, 1);
- },
- // 新增列
- addColumn(later) {
- this.showMenu = false;
- const idx = later ? this.curTarget.colIdx + 1 : this.curTarget.colIdx;
- const colStr = { prop: "col_" + ++this.countCol, label: "" };
- this.columnList.splice(idx, 0, colStr);
- this.tableData.forEach((p) => (p[colStr.prop] = ""));
- },
- // 删除列
- delColumn() {
- this.showMenu = false;
- this.tableData.forEach((p) => {
- delete p[this.curColumn.prop];
- });
- this.columnList.splice(this.curTarget.colIdx, 1);
- },
- // 添加表格行下标
- tableRowClassName({ row, rowIndex }) {
- row.row_index = rowIndex;
- },
- // 定位菜单/编辑框
- locateMenuOrEditInput(eleId, eleWidth, event) {
- let ele = document.getElementById(eleId);
- ele.style.top = event.clientY - 100 + "px";
- ele.style.left = event.clientX - 50 + "px";
- if (window.innerWidth - eleWidth < event.clientX) {
- ele.style.left = "unset";
- ele.style.right = 0;
- }
- }
- }
- };
- script>
-
- <style lang="scss" scoped>
- #hello {
- position: relative;
- height: 100%;
- width: 100%;
- }
- .tips {
- margin-top: 10px;
- color: #999;
- }
- #contextmenu {
- position: absolute;
- top: 0;
- left: 0;
- height: auto;
- width: 120px;
- border-radius: 3px;
- box-shadow: 0 0 10px 10px #e4e7ed;
- background-color: #fff;
- border-radius: 6px;
- padding: 15px 0 10px 15px;
- button {
- display: block;
- margin: 0 0 5px;
- }
- }
- .hideContextMenu {
- position: absolute;
- top: -4px;
- right: -5px;
- }
- #editInput,
- #headereditInput {
- position: absolute;
- top: 0;
- left: 0;
- height: auto;
- min-width: 200px;
- max-width: 400px;
- padding: 0;
- }
- #editInput .el-input,
- #headereditInput .el-input {
- outline: 0;
- border: 1px solid #c0f2f9;
- border-radius: 5px;
- box-shadow: 0px 0px 10px 0px #c0f2f9;
- }
- .line {
- width: 100%;
- border: 1px solid #e4e7ed;
- margin: 10px 0;
- }
- style>
- <div id="table-wrap">
-
- <el-table
- :data="questionChoiceVOlist"
- stripe
- border
- @cell-click="cellClick"
- @row-contextmenu="rightClick"
- :row-class-name="tableRowClassName"
- @header-contextmenu="(column: any, event: MouseEvent) => rightClick(null, column, event)"
- >
- <el-table-column
- type="index"
- label="序号"
- align="center"
- :resizable="false"
- width="70"
- />
-
- <template #empty>
- <el-empty description="暂无数据" />
- template>
-
- <el-table-column
- :resizable="false"
- align="center"
- v-for="(col, idx) in columnList"
- :key="col.prop"
- :prop="col.prop"
- :label="col.label"
- :index="idx"
- >
- <template #default="{ row }">
- <div
- v-if="col.type === 'button'"
- style="height: 75px; padding-top: 26px; width: 100%"
- >
- <el-badge type="warning" :value="getRiskLenght(row.riskIds)">
- <el-button size="small">
- {{ paramsIdType == 'detail' ? '查看' : '选择' }}
- el-button>
- el-badge>
- div>
- <el-input-number
- v-if="col.type === 'input-number'"
- v-model.number="row[col.prop]"
- :min="0"
- :max="10"
- :step="0.1"
- :precision="2"
- />
- template>
- el-table-column>
- el-table>
-
-
- <div v-show="showMenu" id="contextmenu" @mouseleave="showMenu = false">
- <p style="margin-bottom: 10px; text-align: left">列:p>
- <el-button :icon="CaretTop" @click="addColumn(false)"> 前方插入一列 el-button>
- <el-button :icon="CaretBottom" @click="addColumn(true)"> 后方插入一列 el-button>
- <el-button :icon="DeleteFilled" @click="openColumnOrRowSpringFrame('列')">
- 删除当前列
- el-button>
- <el-button @click="renameCol" :icon="EditPen"> 更改列名 el-button>
-
- <div style="color: #ccc">-----------------------div>
-
- <p style="margin-bottom: 12px">行:p>
- <el-button :icon="CaretTop" @click="addRow(false)" v-show="!curTarget.isHead">
- 上方插入一行
- el-button>
- <el-button :icon="CaretBottom" @click="addRow(true)" v-show="!curTarget.isHead">
- 下方插入一行
- el-button>
- <el-button :icon="DeleteFilled" @click="addRowHead" v-show="curTarget.isHead">
- 下方插入一行
- el-button>
- <el-button
- :icon="DeleteFilled"
- @click="openColumnOrRowSpringFrame('行')"
- v-show="!curTarget.isHead"
- >
- 删除当前行
- el-button>
- div>
-
-
- <div v-show="showEditInput" id="editInput">
- <el-input
- ref="iptRef"
- placeholder="请输入内容"
- v-model="curTarget.val"
- clearable
- @change="updTbCellOrHeader"
- @blur="showEditInput = false"
- @keyup="onKeyUp($event)"
- >
- <template #prepend>{{ curColumn.label || curColumn.prop }}template>
- el-input>
- div>
-
-
-
- <div>
- <h3 style="text-align: center; margin-top: 15px">实时数据展示h3>
- <label>当前目标:label>
- <pre><code>{{ JSON.stringify(curTarget, null, 2) }}code>pre>
- <div style="width: 100%; height: auto">
- <label>表头:label>
- <pre><code v-for="col in columnList" :key="col.prop">{{ JSON.stringify(col, null, 2) }}code>pre>
- div>
-
- <div>
- <label>数据:label>
- <pre><code v-for="(data, idx) in questionChoiceVOlist" :key="idx">
- {{ JSON.stringify(data, null, 2) }}
- code>pre>
- div>
- div>
-
- div>
-
- <script setup lang="ts">
- import { ref, reactive, computed, toRefs, nextTick } from 'vue';
- import { ElMessage, ElMessageBox } from 'element-plus';
- import { DeleteFilled, CaretBottom, CaretTop, EditPen } from '@element-plus/icons-vue';
- // Tips: locateMenuOrEditInput 可调整编辑框位置
- interface Column {
- prop: string;
- label: string;
- type?: string;
- }
-
- interface Data {
- choiceCode: string;
- choiceContent: string;
- riskIds: string;
- itemScore: string | number;
- [key: string]: unknown;
- }
-
- interface Target {
- rowIdx: number | null;
- colIdx: number | null;
- val: string | null;
- isHead: boolean | undefined;
- }
-
- // 接收addEdit父组件传过来的数据,用于判断是新增、编辑、详情页面
- const paramsIdType = 'detail';
-
- const state = reactive({
- columnList: [
- { prop: 'choiceCode', label: '选项编码' },
- { prop: 'choiceContent', label: '选项内容' },
- { prop: 'riskIds', label: '风险点', type: 'button' },
- { prop: 'itemScore', label: '选项分值', type: 'input-number' },
- ] as Column[],
- questionChoiceVOlist: [
- {
- choiceCode: 'A',
- choiceContent: '是',
- riskIds: '45,47',
- itemScore: 1,
- isClickCheckBtn: true,
- id: 1,
- },
- {
- choiceCode: 'B',
- choiceContent: '否',
- riskIds: '46',
- itemScore: 4,
- isClickCheckBtn: true,
- id: 2,
- },
- {
- choiceCode: 'C',
- choiceContent: '否',
- riskIds: '',
- itemScore: 4,
- isClickCheckBtn: true,
- id: 3,
- },
- ] as Data[],
- showMenu: false, // 显示右键菜单
- showEditInput: false, // 显示单元格/表头内容编辑输入框
- curTarget: {
- // 当前目标信息
- rowIdx: null, // 行下标
- colIdx: null, // 列下标
- val: null, // 单元格内容/列名
- isHead: undefined, // 当前目标是表头?
- } as Target,
- countCol: 0, // 新建列计数
- });
- const iptRef = ref();
-
- const { columnList, questionChoiceVOlist, showMenu, showEditInput, curTarget } = toRefs(state);
-
- // 当前列
- const curColumn = computed(() => {
- return curTarget.value.colIdx !== null
- ? columnList.value[curTarget.value.colIdx]
- : { prop: '', label: '' };
- });
-
- // 计算风险点数量
- const getRiskLenght = computed(() => {
- return (riskIds: string) => riskIds.split(',').filter(Boolean).length;
- });
-
- /**
- * 删除列/行
- * @method delColumn
- * @param { string } type - '列' | '行'
- **/
- const openColumnOrRowSpringFrame = (type: string) => {
- ElMessageBox.confirm(`此操作将永久删除该${type === '列' ? '列' : '行'}, 是否继续 ?, '提示'`, {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning',
- })
- .then(() => {
- if (type === '列') {
- delColumn();
- } else if (type === '行') delRow();
-
- ElMessage.success('删除成功');
- })
- .catch(() => ElMessage.info('已取消删除'));
- };
-
- // 回车键关闭编辑框
- const onKeyUp = (e: KeyboardEvent) => {
- if (e.key === 'Enter') {
- showEditInput.value = false;
- }
- };
-
- // 控制某字段不能打开弹框
- const isPop = (column: { label: string }) => {
- return column.label === '风险点' || column.label === '选项分值';
- };
-
- // 左键输入框
- const cellClick = (
- row: { [x: string]: any; row_index: any },
- column: { index: null; property: string | number; label: string },
- _cell: any,
- event: MouseEvent
- ) => {
- // 如果是风险点或选项分值,不执行后续代码
- if (isPop(column)) return;
-
- iptRef.value.focus();
- if (column.index == null) return;
- locateMenuOrEditInput('editInput', -300, event); // 左键输入框定位 Y
- showEditInput.value = true;
- iptRef.value.focus();
-
- // 当前目标
- curTarget.value = {
- rowIdx: row.row_index,
- colIdx: column.index,
- val: row[column.property],
- isHead: false,
- };
- };
-
- // 表头右击事件 - 打开菜单
- const rightClick = (row: any, column: any, event: MouseEvent) => {
- event.preventDefault();
-
- if (column.index == null) return;
- // 如果tableData有数据并且当前目标是表头,那么就返回,不执行后续操作
- // if (questionChoiceVOlist.value.length > 0 && !row) return;
- if (isPop(column)) return;
-
- showMenu.value = false;
- locateMenuOrEditInput('contextmenu', -500, event); // 右键输入框
- showMenu.value = true;
- curTarget.value = {
- rowIdx: row ? row.row_index : null,
- colIdx: column.index,
- val: row ? row[column.property] : column.label,
- isHead: !row,
- };
- };
-
- // 更改列名
- const renameCol = () => {
- showEditInput.value = false;
- if (curTarget.value.colIdx === null) return;
- showEditInput.value = true;
- nextTick(() => {
- iptRef.value.focus();
- });
- };
-
- // 更改单元格内容/列名
- const updTbCellOrHeader = (val: string) => {
- if (!curTarget.value.isHead) {
- if (curTarget.value.rowIdx !== null) {
- (questionChoiceVOlist.value[curTarget.value.rowIdx] as Data)[curColumn.value.prop] =
- val;
- }
- } else {
- if (!val) return;
- if (curTarget.value.colIdx !== null) {
- columnList.value[curTarget.value.colIdx].label = val;
- }
- }
- };
- // 新增行
- const addRow = (later: boolean) => {
- showMenu.value = false;
- const idx = later ? curTarget.value.rowIdx! + 1 : curTarget.value.rowIdx!;
- let obj: any = {};
- columnList.value.forEach((p) => obj[p.prop]);
- questionChoiceVOlist.value.splice(idx, 0, obj);
- // 设置新增行数据默认值
- questionChoiceVOlist.value[idx] = {
- choiceCode: '',
- choiceContent: '',
- riskIds: '',
- itemScore: 0,
- id: Math.floor(Math.random() * 100000),
- };
- };
-
- // 表头下新增行
- const addRowHead = () => {
- showMenu.value = false;
- let obj: any = {};
- columnList.value.forEach((p) => obj[p.prop]);
- questionChoiceVOlist.value.unshift(obj);
- questionChoiceVOlist.value[0] = {
- choiceCode: '',
- choiceContent: '',
- riskIds: '',
- itemScore: 0,
- id: Math.floor(Math.random() * 100000),
- };
- };
- // 删除行
- const delRow = () => {
- showMenu.value = false;
- curTarget.value.rowIdx !== null &&
- questionChoiceVOlist.value.splice(curTarget.value.rowIdx!, 1);
- };
-
- // 新增列
- const addColumn = (later: boolean) => {
- showMenu.value = false;
- const idx = later ? curTarget.value.colIdx! + 1 : curTarget.value.colIdx!;
- const colStr = { prop: 'Zk-NewCol - ' + ++state.countCol, label: '' };
- columnList.value.splice(idx, 0, colStr);
- questionChoiceVOlist.value.forEach((p) => (p[colStr.prop] = ''));
- };
-
- // 删除列
- const delColumn = () => {
- showMenu.value = false;
- questionChoiceVOlist.value.forEach((p) => {
- delete p[curColumn.value.prop];
- });
- columnList.value.splice(curTarget.value.colIdx!, 1);
- };
-
- // 添加表格行下标
- const tableRowClassName = ({ row, rowIndex }: { row: any; rowIndex: number }) => {
- row.row_index = rowIndex;
- };
-
- // 定位菜单/编辑框
- const locateMenuOrEditInput = (eleId: string, distance: number, event: MouseEvent) => {
- if (window.innerWidth < 1130 || window.innerWidth < 660)
- return ElMessage.warning('窗口太小,已经固定菜单位置,或请重新调整窗口大小');
- const ele = document.getElementById(eleId) as HTMLElement;
- const x = event.pageX;
- const y = event.clientY + 200; //右键菜单位置 Y
- let left = x + distance + 200; //右键菜单位置 X
- let top;
- if (eleId == 'editInput') {
- // 左键
- top = y + distance;
- left = x + distance - 120;
- } else {
- // 右键
- top = y + distance + 170;
- }
- ele.style.left = `${left}px`;
- ele.style.top = `${top}px`;
- };
-
- defineExpose({
- questionChoiceVOlist,
- });
- script>
-
- <style lang="scss" scoped>
- #table-wrap {
- width: 100%;
- height: 100%;
- /* 左键 */
- #contextmenu {
- position: absolute;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- z-index: 999999;
- top: 0;
- left: 0;
- height: auto;
- width: 200px;
- border-radius: 3px;
- border: #e2e2e2 1px solid;
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
- background-color: #fff;
- border-radius: 6px;
- padding: 15px 10px 14px 12px;
-
- button {
- display: block;
- margin: 0 0 5px;
- }
- }
- /* 右键 */
- #editInput {
- position: absolute;
- top: 0;
- left: 0;
- z-index: 999999;
- }
- /* 实时数据 */
-
- pre {
- border: 1px solid #cccccc;
- padding: 10px;
- overflow: auto;
- }
- }
- style>
_______________________________ 期待再见 _______________________________