记录一个表格类的提交表单

317 阅读6分钟

记录一个表格类的提交表单

1. 需求

使用一些奇奇怪怪的词汇来描述,就不使用具体的名词了。

在一个大类下有很多子类(数量不一,但结构是一样的,你可以新增这些子类),每个子类又有自己的子类(数量不一,但结构是一样的,你可以新增这些子类),各个类都有自己的一些描述项,待到这些描述项都填写完毕后,再一起提交给后端保存。

某些项有必填字段,保存的时候如果选择了项目,必填字段却没有填写,就提示用户,定位到表格的行,方便用户查看和填写。如果用户只是填写了一些必填字段,但是没有选择项目,则点击保存的时候过滤掉这些数据。

2. 需求分析

对于这类提交的数据,基本上都会使用到数组,大类是数组存储,可存储多个,大类里面的子类也是数组,可以存储多个。又因为各个类都有自己的描述项,那就说明数组里面的元素都是一个一个的对象。

所有的操作,都是在操作这个数组,然后去改变数组里面的数据即可完成需求。其实主要的思路是我们要非常清楚的知道我们当前正在操作这个数组的哪一项。

理解了简单的部分,后面遇到更多的嵌套,依然可以按照这种方式去实现。应该也不会遇到多层嵌套的吧。

我们的项目是三个大类,使用 自己封装的类似于 tabs 来控制用户当前操作的大类,其实展示的都是同一个组件,只是操作的当前数据不同罢了,感兴趣的可以自己去实现。

创建 Demo 的时候手抖选到了 vue2 + ts,所以将就用这个来写了,其实都一样,写出来怪怪的代码,不过主要是功能实现。UI 组件库使用的是 IView。

3. 预览效果

GIF 2022-7-3 19-41-44.gif

4. 代码

因为只是记录一下大概的实现,所以并没有优化这些代码,还有有些优化的空间,如果需要可以自行优化~

4.1 表格类表单

src/views/FormDemo/TableForm/index.vue

<template>
  <div>
    <Row :gutter="16">
      <Col :span="12">
        <Card title="项目" :dis-hover="true">
          <a href="javascript:void(0);" slot="extra" @click="addProject">添加项目</a>

          <Table
            class="table-con"
            :columns="columns1"
            :data="form"
            :row-class-name="rowClassName"
            @on-row-click="onRowClick"
            stripe
          >
            <template #projectId="{ row, index }">
              <Select :value="row.projectId" @on-change="value => handleChangeProject(value, index)" transfer>
                <Option
                  v-for="project in projectArr"
                  :value="project.projectId"
                  :key="project.projectId"
                  :disabled="project.disabled"
                >
                  {{ project.projectName }}
                </Option>
              </Select>
            </template>

            <template #remark="{ row, index }">
              <Input :value="row.remark" @on-change="e => handleChangeRemark(e.target.value, index)"></Input>
            </template>

            <template #action="{ row, index }">
              <span @click="delProject(index)">删除</span>
            </template>
          </Table>
        </Card>
      </Col>

      <Col :span="12" v-if="curIndex !== -1">
        <Card title="子项目" :dis-hover="true">
          <a href="javascript:void(0);" slot="extra" @click="addChildProject">
            添加子项目
          </a>

          <Table
            :columns="columns2"
            :data="curFormChildArr"
            :row-class-name="rowClassNameChild"
            stripe
            class="table-con"
          >
            <template #projectId="{ row, index }">
              <Select :value="row.projectId" @on-change="value => handleChangeChildProject(value, index)" transfer>
                <Option
                  v-for="project in childProjectArr"
                  :value="project.projectId"
                  :key="project.projectId"
                  :disabled="project.disabled"
                >
                  {{ project.projectName }}
                </Option>
              </Select>
            </template>

            <!-- <template #field1="{ row, index }">
              <Input :value="row.field1" @on-change="e => handleChangeChildField(e.target.value, index, 'field1')"></Input>
            </template> -->

            <template #[field]="{ row, index }" v-for="field in ['field1', 'field2', 'remark']">
              <Input :value="row[field]" @on-change="e => handleChangeChildField(e.target.value, index, field)" :key="field"></Input>
            </template>

            <template #action="{ row, index }">
              <span @click="delChildProject(index)">删除</span>
            </template>
          </Table>
        </Card>
      </Col>
    </Row>

    <Button @click="handleSave">保存</Button>
  </div>
</template>

<script lang="ts">
import Vue from 'vue';

export default Vue.extend<
  // 这里的代码可以忽略,只是我创建 Demo 的时候选错了,写出来的奇奇怪怪的代码
  // 其实这里的代码不写,就和常规的差不多了
  {
    columns1: Array<unknown>;
    columns2: Array<unknown>;
    projectArr: TableFormType.ProjectItem[];
    childProjectArr: TableFormType.ChildProjectItem[]
    form: TableFormType.ProjectFormItem[];
    curIndex: number;
    curChildIndex: number;
  },
  {
    getProjectArr: () => void;
    getChildProjectArr: () => void;
    addProject: () => void;
    handleChangeProject: (value: number, index: number) => void;
    handleChangeRemark: (value: string, index: number) => void;
    handleProjectDouble: () => void;
    handleSave: () => void;
    addChildProject: () => void;
    handleChangeChildProject: (value: number, index: number) => void;
    handleChangeChildField: (value: string, index: number, fieldName: 'field1' | 'field2' | 'remark') => void;
    rowClassName: (row: unknown, index: number) => void;
    onRowClick: (row: TableFormType.ProjectFormItem, index: number) => void;
    delProject: (index: number) => void;
    delChildProject: (index: number) => void;
    handleChildProjectDouble: () => void;
    rowClassNameChild: (row: unknown, index: number) => void;
  },
  {
    curFormChildArr: TableFormType.ProjectFormChildItem[];
  }
>({
  name: 'TableForm',

  data() {
    return {
      columns1: [
        {
          title: '项目名称',
          key: 'projectId',
          slot: 'projectId'
        },
        {
          title: '字段1',
          key: 'field1'
        },
        {
          title: '字段2',
          key: 'field2'
        },
        {
          title: '备注',
          key: 'remark',
          slot: 'remark'
        },
        {
          title: '操作',
          key: 'action',
          slot: 'action'
        }
      ],
      columns2: [
        {
          title: '子项目名称',
          key: 'projectId',
          slot: 'projectId'
        },
        {
          title: '字段1',
          key: 'field1',
          slot: 'field1',
          // 为必填字段的表头叫上红 *
          renderHeader: (h: any, params: any) => {
            return h('div', [
              h('span', {
                style: {
                  color: 'red',
                  marginRight: '5px'
                }
              }, '*'),
              h('strong', params.column.title)
            ]);
          }
        },
        {
          title: '字段2',
          key: 'field2',
          slot: 'field2',
          // 为必填字段的表头叫上红 *
          renderHeader: (h: any, params: any) => {
            return h('div', [
              h('span', {
                style: {
                  color: 'red',
                  marginRight: '5px'
                }
              }, '*'),
              h('strong', params.column.title)
            ]);
          }
        },
        {
          title: '备注',
          key: 'remark',
          slot: 'remark'
        },
        {
          title: '操作',
          key: 'action',
          slot: 'action'
        }
      ],
      projectArr: [],
      childProjectArr: [],
      form: [],
      curIndex: -1, // 当前操作的项目索引
      curChildIndex: -1 // 当前操作的子项目索引
    }
  },

  computed: {
    // 当前操作的子类
    curFormChildArr() {
      return this.curIndex === -1 ? [] : this.form[this.curIndex].childProjectList
    }
  },

  methods: {
    // 模拟获取项目数组
    getProjectArr() {
      setTimeout(() => {
        // 一般从后端获取到的没有 disabled 这个字段,
        // 这个字段是前端用于判断当前项目是否已经选中,如果选中了就不能再次选中这个项目
        const _arr: TableFormType.ProjectItem[] = [
          { projectName: '项目1', projectId: 1, field1: '项目1的字段1', field2: '项目1的字段2' },
          { projectName: '项目2', projectId: 2, field1: '项目2的字段1', field2: '项目2的字段2' },
          { projectName: '项目3', projectId: 3, field1: '项目3的字段1', field2: '项目3的字段2' }
        ]
        this.projectArr = _arr.map(item => {
          item.disabled = false
          return item
        })
      })
    },
    // 模拟获取子项目数组
    getChildProjectArr() {
      setTimeout(() => {
        // 一般从后端获取到的没有 disabled 这个字段,
        // 这个字段是前端用于判断当前项目是否已经选中,如果选中了就不能再次选中这个项目
        const _arr: TableFormType.ChildProjectItem[] = [
          { projectName: '子项目1', projectId: 1 },
          { projectName: '子项目2', projectId: 2 },
          { projectName: '子项目3', projectId: 3 }
        ]
        this.childProjectArr = _arr.map(item => {
          item.disabled = false
          return item
        })
      })
    },
    // 添加项目
    addProject() {
      if (this.form.length < this.projectArr.length) {
        this.form.push({
          projectId: undefined,
          field1: '--',
          field2: '--',
          remark: '',
          childProjectList: []
        })
      }
    },
    // 选择项目时,将对应的字段填充
    handleChangeProject(value, index) {
      const _projectObj = this.projectArr.find(item => item.projectId === value)
      if (_projectObj) {
        this.form[index].projectId = value
        this.form[index].field1 = _projectObj.field1
        this.form[index].field2 = _projectObj.field2
        // console.log(this.form.filter(item => item.projectId !== undefined))
        this.curIndex = index

        this.handleProjectDouble()
        this.handleChildProjectDouble()
      }
    },
    // 输入项目备注
    handleChangeRemark(value, index) {
      this.form[index].remark = value
    },
    // 处理项目不能重复选
    handleProjectDouble() {
      const projectIdArr: Array<number> = []
      this.form.forEach(item => {
        if (item.projectId !== undefined) {
          projectIdArr.push(item.projectId)
        }
      })

      this.projectArr.forEach((item, i) => {
        if (projectIdArr.findIndex(projectId => projectId === item.projectId) > -1) {
          this.projectArr[i].disabled = true
        } else {
          this.projectArr[i].disabled = false
        }
      })
    },
    // 为选中的项目单独设置选中样式
    rowClassName(row, index) {
      if (index === this.curIndex) {
        return 'table-info-row'
      }
      return ''
    },
    // 项目行点击的时候,选中该项
    onRowClick(row, index) {
      if (row.projectId !== undefined && (this.form.length - 1) >= index) {
        this.curIndex = index
        this.rowClassName(row, index)
        this.handleChildProjectDouble()
      }
    },
    // 删除项目
    delProject(index) {
      if (this.curIndex === index) {
        this.curIndex = -1
      } else if (this.curIndex > index) {
        this.curIndex -= 1
      }
      this.form.splice(index, 1)
      this.handleProjectDouble()
    },

    // 添加子项目
    addChildProject() {
      if (this.form[this.curIndex].childProjectList.length < this.childProjectArr.length) {
        this.form[this.curIndex].childProjectList.push({
          projectId: undefined,
          field1: '',
          field2: '',
          remark: ''
        })
      }
    },
    // 选择子项目
    handleChangeChildProject(value, index) {
      this.form[this.curIndex].childProjectList[index].projectId = value
      this.curChildIndex = index
      this.handleChildProjectDouble()
    },
    // 设置子项目的字段值
    handleChangeChildField(value, index, fieldName) {
      this.form[this.curIndex].childProjectList[index][fieldName] = value
      this.curChildIndex = index
    },
    // 删除子项目
    delChildProject(index) {
      this.form[this.curIndex].childProjectList.splice(index, 1)
      this.handleChildProjectDouble()
    },
    // 处理子项目不能重复选择
    handleChildProjectDouble() {
      if (this.curIndex === -1) return
      const projectIdArr: Array<number> = []
      this.form[this.curIndex].childProjectList.forEach(item => {
        if (item.projectId !== undefined) {
          projectIdArr.push(item.projectId)
        }
      })

      this.childProjectArr.forEach((item, i) => {
        if (projectIdArr.findIndex(projectId => projectId === item.projectId) > -1) {
          this.childProjectArr[i].disabled = true
        } else {
          this.childProjectArr[i].disabled = false
        }
      })
    },
    // 为子项添加选中类
    rowClassNameChild(row, index) {
      if (index === this.curChildIndex) {
        return 'table-info-row'
      }
      return ''
    },
    // 保存数据
    handleSave() {
      let _data: TableFormType.ProjectFormItem[] = JSON.parse(JSON.stringify(this.form))
      // 是否有必填字段没有填写
      let flag = false
      // 过滤掉用户没有选择的项目,并且判断必填字段是否填写
      _data = _data.filter((item, index) => {
        item.childProjectList = item.childProjectList.filter((obj, i) => {
          // 如果选了子项目,且必填字段没有填写,则提示用户
          if (obj.projectId !== undefined && (obj.field1 === '' || obj.field2 === '')) {
            flag = true
            this.curIndex = index
            this.curChildIndex = i
            item.childProjectList.length = 0
          }
          return obj.projectId !== undefined
        })
        return item.projectId !== undefined
      })

      if (flag) {
        // 提示用户有必填字段没有填写
        this.$Message.warning('请完成必填字段的填写')
        return
      }
      console.log(_data)
      // 调用保存接口
    }
  },

  created() {
    this.getProjectArr()
    this.getChildProjectArr()
  }
})
</script>

<style lang="scss" scoped>
.table-con ::v-deep .ivu-table .table-info-row td {
    background-color: #daeffd;
    color: #333;
}
.table-con ::v-deep .ivu-table td.table-info-column {
    background-color: #daeffd;
    color: #333;
}
.table-con ::v-deep .ivu-table .table-info-cell-name {
    background-color: #daeffd;
    color: #333;
}

.table-con ::v-deep .ivu-table-row {
    cursor: pointer;
}
</style>

4.2 一些 ts 接口

src/views/FormDemo/TableForm/typing.d.ts

为了方便上面代码的理解还是将这个代码贴出来了……

declare namespace TableFormType {
  interface ProjectItem {
    projectName?: string;
    projectId: number;
    field1: string;
    field2: string;
    disabled?: boolean;
  }

  interface ChildProjectItem {
    projectName: string;
    projectId: number;
    disabled?: boolean;
  }

  interface ProjectFormItem {
    projectId: number | undefined
    field1: string;
    field2: string;
    remark: string;
    key?: string | number;
    childProjectList: ProjectFormChildItem[];
  }

  interface ProjectFormChildItem {
    projectName?: string;
    projectId: number | undefined;
    field1: string;
    field2: string;
    remark: string;
    key?: string | number;
  }
}