vue3+element-plus如何做一个动态增删同时具有校验功能的table表格

项目中有个比较麻烦的需求:

一个表格:

  • 能手动向表格第一行增加一行

  • 每一行的每一列在点击某一格时要能输入

  • 每个表单控件需要有校验功能

  • 支持多选

  • 可以删除所选行

  • 保存后需要前端做一个仅前端部分的查询

原型如下:

image.png

上面这些要求看起来就很麻烦。但是更麻烦的是,这个原型还是修改过的,原来的原型已经实现功能了,刚写完,第二天领导又把原型改掉了。

1.先说大致实现思路:

(1) 如何实现每个每个input输入框都带有校验功能?

答:每个输入框给一个<el-form>,就是每个表单都只有一个表单,每个表单的只有一个<el-form-item>,有着对应的校验规则。在点击保存时获取到当前所有的refs,通过调用form表单的validate()方法,实现每个表单都有校验功能。

(2) 如何向当前表格第一行加一个可编辑的行?

答:先定义一个字段模板,每次新增,用unshift方法添加到当前表格数据列表的第一行。

(3)如何实现点击某一格可以编辑?

答:每一个<td>格子里都有两个元素:一个是表单控件,一个是用来展示的文本。同时给每个表单绑定的字段在每一行的数据中加一个对应的'${key}_isEditing'字段,类型为Boolean,点击时修改这个字段为true或false,同时使用v-if控制当前td的表单控件的显示与隐藏。

主要比较难的就是上面三个功能的实现,其他的增删改都是围绕着<el-table>的当前选中行进行操作的。

具体组件源码如下:

<template>
  <div class="attr-table">
    <el-row :gutter="20">
      <el-col :span="10">
        <el-date-picker style="width: 100%" @change="timeChange" v-model="timeRange" type="datetimerange" range-separator="-" start-placeholder="开始时间" end-placeholder="结束时间" />
      </el-col>
      <el-col :span="6">
        <el-input @clear="searchClearFn" clearable v-model="dataForm.attr" placeholder="属性编码" />
      </el-col>
      <el-col :span="4">
        <el-button type="primary" @click="getData">查询</el-button>
      </el-col>
    </el-row>
    <br />
    <el-row :gutter="20">
      <el-col :span="24" style="text-align: right">
        <el-button @click="addAttr()" type="primary">新增</el-button>
        <el-button @click="save()" type="warning">保存</el-button>
        <el-button @click="deleteRow" type="danger">删除</el-button>
      </el-col>
    </el-row>
    <br />
    <div class="main-table">
      <el-table ref="attrTableRef" @selection-change="handleSelectionChange" v-loading="loading" :data="tableData" style="width: 100%">
        <el-table-column type="selection" width="55" />
        <el-table-column prop="ts" label="时间">
          <template v-slot="scope">
            <el-form :ref="`formRef${scope.$index}ts`" v-if="scope.row.ts_editing" :model="scope.row">
              <el-form-item prop="ts" :rules="{ required: true, message: '请输入时间', trigger: 'blur' }">
                <el-date-picker format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" v-model="scope.row.ts" type="datetime" placeholder="时间"> </el-date-picker>
              </el-form-item>
            </el-form>
            <span v-else @click="itemClick(scope)" class="fill-p">{{ scope.row.ts }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="key" label="属性编码">
          <template v-slot="scope">
            <el-form :ref="`formRef${scope.$index}key`" v-if="scope.row.key_editing" :model="scope.row">
              <el-form-item prop="key" :rules="{ required: true, message: '请输入属性编码', trigger: 'blur' }">
                <el-input type="text" size="small" v-model="scope.row.key" placeholder="属性编码" />
              </el-form-item>
            </el-form>
            <span @click="itemClick(scope)" class="fill-p" v-else>{{ scope.row.key }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="value" label="属性值">
          <template v-slot="scope">
            <el-form :ref="`formRef${scope.$index}value`" v-if="scope.row.value_editing" :model="scope.row">
              <el-form-item prop="value" :rules="{ required: true, message: '请输入属性值', trigger: 'blur' }">
                <el-input type="text" size="small" v-model="scope.row.value" placeholder="属性值" />
              </el-form-item>
            </el-form>
            <span class="fill-p" @click="itemClick(scope)" v-else>{{ scope.row.value }}</span>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>

<script setup>
import { ref, getCurrentInstance, defineProps, onMounted, defineEmits, defineExpose, watch } from "vue";
import { ElMessage } from "element-plus";
import { toRefs } from "@vueuse/core";
import baseService from "@/service/baseService";
import { formatDate } from "../../tool.js";

const props = defineProps(["loading", "hasSearch", "defaultData", "thingCode"]);
const emits = defineEmits(["save"]);
const { loading, hasSearch, defaultData, thingCode } = toRefs(props);
const tableData = ref([]);
const initData = ref([]);
const selectedRow = ref([]);
const attrTableRef = ref();
// 查询之前的tableData
const copyTableData = ref([]);
// 用于判断修改之后是否保存
const changeHasSave = ref(true);
const dataForm = ref({
  beginTime: "",
  endTime: "",
  attr: ""
});
const timeRange = ref([]);

watch(defaultData, (v) => {
  tableData.value = v;
});

onMounted(() => {
  const data = [
    // {
    //   code: "1",
    //   startTime: "2022-02-22 06:23:56",
    //   latestTime: "",
    //   frequency: "2"
    // }
  ];
  initData.value = data;

  data.forEach((d) => {
    for (const k in d) {
      d[`${k}_editing`] = false;
    }
  });

  tableData.value = data;
});

// 获取属性编码
function getAttr() {
  if (!changeHasSave.value) {
    ElMessage({
      type: "warning",
      message: "请先保存数据"
    });
    return;
  }
  baseService.get("/cache/cachethingattr/getCacheThingAttrList", { thingCode: thingCode.value, thingAttr: dataForm.value.attr }).then((res) => {
    const data = res?.data || data;
    tableData.value = data.map((e) => {
      return {
        id: e.id,
        ts: e.thingAttr,
        key: e.firstTs,
        value: e.lastTs
      };
    });
  });
}

const context = getCurrentInstance();

const tableKey = ref(0);

const firstLineForm = {
  isUpdate: true,
  isNewLine: true,
  ts: "",
  ts_editing: true,
  key: "",
  key_editing: true,
  value: "",
  value_editing: true
};

/* 用来修改当前表格的某一行的某个数据 */
function upDateTable(index, key, value, newLine) {
  const newTableData = JSON.parse(JSON.stringify(tableData.value));

  if (newLine) {
    newTableData.unshift(newLine);
  } else {
    newTableData[index][key] = value;
  }
  tableData.value = newTableData.map((e, index) => {
    // 给每行加上一个index
    return {
      ...e,
      index
    };
  });
}

function getData() {
  // 查询之前先校验
  if (!tableValidate() || !thingCode.value) return;

  if (!changeHasSave.value) {
    // 没有点击保存,或者没有新增数据
    ElMessage({
      type: "warning",
      message: "当前有没有保存的数据,请先保存"
    });
    return;
  }
  baseService.get("/thing/thing/getSequentialInformationByCondition", { ...dataForm.value, thingCode: thingCode.value }).then((res) => {
    const data = res?.data || [];
    console.log("查询的数据:", data);
    tableData.value = data.map((e) => {
      return {
        ts: formatDate(+e.ts),
        key: e.key || "",
        value: e.value || ""
      };
    });
  });
}

function searchClearFn() {
  if (thingCode.value) {
    getAttr();
    return;
  }
  tableData.value = copyTableData.value;
}

function timeChange(v) {
  dataForm.value.beginTime = formatDate(v[0]);
  dataForm.value.endTime = formatDate(v[1]);
}

function addAttr() {
  upDateTable("", "", "", { ...firstLineForm });
  changeHasSave.value = false;
}

function save(row) {
  /* 物管理的这个保存和缓存设置页面的保存是两个后端写的
   * 这里的基础信息的保存和时许属性的保存是两个分开的接口,所以这里直接用组件保存就行了
   * 缓存设置页面的基础信息的保存和属性的保存是公用一个接口
   * */
  if (!tableValidate()) return;

  if (changeHasSave.value) {
    ElMessage({
      type: "warning",
      message: "没有需要保存的数据"
    });
    return;
  }

  // 都通过了验证,取消编辑状态
  tableData.value.forEach((e) => {
    e.ts_editing = false;
    e.key_editing = false;
    e.value_editing = false;
  });

  // 保存备份用于查询
  // copyTableData.value = JSON.parse(JSON.stringify(tableData.value));

  // 提交
  const tsKvReqParamList = tableData.value.map((e) => {
    return {
      ts: new Date(e.ts).getTime(),
      values: {
        [e.key]: e.value
      }
    };
  });
  baseService.post("/thing/thing/saveSequentialInformationToTb", { thingCode: thingCode.value, tsKvReqParamList }).then((res) => {
    if (res.code === 0) {
      ElMessage({
        type: "success",
        message: "保存成功"
      });
      getData();
    }
  });

  // 已经点击保存
  changeHasSave.value = true;
}

function itemClick(row) {
  const { $index, column } = row;
  /* 修改时已有数据的时间和属性编码不能修改 */
  if (!column.isNewLine && (column.property === "ts" || column.property === "key")) return;
  tableData.value[$index][`${column.property}_editing`] = true;
  if (changeHasSave.value) changeHasSave.value = false;
}

function deleteRow() {
  if (selectedRow.value.length <= 0) {
    ElMessage({
      type: "warning",
      message: "请选择需要删除的属性"
    });
    return;
  }

  // const hasEditing = selectedRow.value.some((e) => {
  //   return e.key_editing || e.ts_editing || e.value_editing;
  // });
  // console.log("当前选择有正在编辑", hasEditing);
  // if(hasEditing){
  //   ElMessage({
  //     type: "warning",
  //     message: "请先保存正在修改的数据"
  //   });
  //   return;
  // }

  // 先删除新增的行。数据库有的暂时不动
  const newLineList = selectedRow.value.filter((e) => {
    return e.isNewLine;
  });
  console.log("是新增:", newLineList);

  const indexs = newLineList.map((e) => e.index).sort((a, b) => b - a); // 把索引从大到小排列
  const newTableData = JSON.parse(JSON.stringify(tableData.value));
  // 从tableData中删除选择的数据
  indexs.forEach((e) => {
    newTableData.splice(e, 1);
  });
  tableData.value = newTableData;
}

function handleSelectionChange(sel) {
  selectedRow.value = sel;
}

// 表格的校验方法
function tableValidate() {
  const { refs } = context;
  let allPassValidate = true;
  // 这里是调用所有行的validate()方法
  for (let ref in refs) {
    if (ref === "attrTableRef") continue;

    refs[ref]?.validate((v) => {
      if (!v) {
        allPassValidate = false;
      }
    });
  }

  return allPassValidate;
}

// 当前组件抛出给父组件的用于清空的方法
function clear() {
  tableData.value = [];
}

// 用于判断是否保存或是否新增了属性
function hasSaved() {
  // 判断之前先校验
  if (!tableValidate()) return false;
  return copyTableData.value.length > 0;
}

defineExpose({ clear, hasSaved });
</script>

<style lang="less">
.fill-p {
  display: inline-block;
  width: 100%;
  height: 24px;
}
</style>

posted @ 2022-06-20 08:50  俄罗斯方块  阅读(1021)  评论(0编辑  收藏  举报