vue2表格组件封装

Vue2.6之后才有这种写法v-slot:header="scope"  2.6之前的写法 slot="header" slot-scope="scope"

父组件 调用 

<template>
  <div class="recommend">
    <el-tabs v-model="reviewTabs" @tab-click="handleTabsClick">
      <el-tab-pane label="待审核" name="audit" lazy>
        <search-form
          v-model="searchAudit"
          :search-option="searchOptionInner"
          :title-fun="titleFunOption"
          :customClear="true"
          @clear="onClearSearch"
          @search="onSearchClick"
          @table-click="handleClick">
        </search-form>

        <table-list
          ref="tableAudit"
          :dialogLoading="dialogLoading"
          :columns="tableColOptionInner"
          :data="listData"
          :total="total"
          :page.sync="searchAudit.curpage"
          :page-size.sync="searchAudit.pagesize"
          @page-change="listPageChange"
          @table-click="handleClick">
          <template v-slot:data1header>
            <span>选择</span>
          </template>

          <template v-slot:data="{row,index}">
            <el-checkbox :value="isChecked(row)"
                         @change="onCheckedChange(row)"></el-checkbox>
          </template>
        </table-list>
      </el-tab-pane>
      <el-tab-pane label="全部审核" name="complete" lazy>
        <search-form
          v-model="searchComplete"
          :search-option="searchOptionInner"
          :title-fun="titleFunOption"
          :customClear="true"
          @clear="onClearSearch"
          @search="onSearchClick"
          @table-click="handleClick">
        </search-form>

        <table-list
          ref="tableComplete"
          :dialogLoading="dialogLoading"
          :columns="tableColOptionInner"
          :data="listData"
          :total="total"
          :page.sync="searchComplete.curpage"
          :page-size.sync="searchComplete.pagesize"
          @page-change="listPageChange"
          @table-click="handleClick">
          <template v-slot:data1header>
            <span>选择</span>
          </template>

          <template v-slot:data="{row,index}">
            <el-checkbox :value="isChecked(row)"
                         @change="onCheckedChange(row)"></el-checkbox>
          </template>
          <template v-slot:status="{row,index}">
            <span v-if="row.status === '10'">待审核</span>
            <span v-else-if="row.status === '20'">通过</span>
            <span v-else-if="row.status === '30'">驳回</span>
          </template>
        </table-list>
      </el-tab-pane>
    </el-tabs>

    <div class="multiple-selection-operation">
      <el-button type="primary" size="small" @click="onSelectAllChange">全选</el-button>
      <el-button type="primary" size="small" @click="onSelectInvertChange">反选</el-button>
      <el-button v-show="reviewTabs !== 'complete'"
                 type="primary"
                 size="small"
                 @click="toExamineSelected">批量通过
      </el-button>
      <el-button v-show="reviewTabs === 'complete'"
                 type="primary"
                 size="small"
                 @click="onDeleteSelected">一键删除
      </el-button>
    </div>

    <el-dialog
      title="评论驳回"
      width="34%"
      :visible.sync="rejectDialog"
      :close-on-click-modal="false"
      :close-on-press-escape="false"
      destroy-on-close
      @open="openRejectDialog">
      <el-form
        ref="rejectFrom"
        :model="rejectFrom"
        label-width="115px"
        :rules="rejectFromRules">

        <el-form-item label="拒绝理由" prop="classA">
          <el-radio-group v-model="rejectFrom.classA" @change="onRejectChange">
            <el-radio v-for="item in rejectList" :key="item.id" :label="item.id">{{ item.content }}</el-radio>
          </el-radio-group>

          <div class="sub-reject-container">
            <div v-for="(label) in rejectChildList"
                 :key="label.id"
                 class="sub-reject-item"
                 :class="{'reject-active': rejectFrom.classB.includes(label.id)}"
                 @click="onSelectRejectItem(label.id)">
              {{ label.id }}.{{ label.content }}
            </div>
          </div>
        </el-form-item>
        <el-form-item label="审核备注">
          <el-input type="textarea"
                    :rows="3"
                    maxlength="30"
                    placeholder="请输入拒绝理由"
                    v-model="rejectFrom.reason"></el-input>
        </el-form-item>
      </el-form>

      <div slot="footer" class="dialog-footer">
        <el-button @click="rejectDialog = false">取 消</el-button>
        <el-button type="primary" @click="onSubmitDialog">确 定</el-button>
      </div>
    </el-dialog>

    <el-dialog
      title="社区评论详情"
      width="34%"
      :visible.sync="detailDialogVisible"
      :close-on-click-modal="false"
      :close-on-press-escape="false"
      destroy-on-close
      custom-class="custom-detail-log">
      <div class="community-details">
        <el-descriptions :column="1"
                         :contentStyle="{fontSize: '16px',marginBottom: '12px',color:'#333333'}"
                         :labelStyle="{fontSize: '16px',marginBottom: '12px',color:'#333333'}">
          <el-descriptions-item label="评论内容">{{detailInfo.content}}</el-descriptions-item>
          <el-descriptions-item label="评论图片">
            <template v-if="detailInfo.img">
              <el-image
                style="width: 100px; height: 100px"
                :src="detailInfo.img"
                :preview-src-list="[detailInfo.img]">
              </el-image>
            </template>
          </el-descriptions-item>
          <el-descriptions-item label="目标作品">{{detailInfo.sourseid}}</el-descriptions-item>
          <el-descriptions-item label="审核结果">
            <span class="descriptions-result" v-if="detailInfo.status === '20'">通过</span>
            <span class="descriptions-result" v-else-if="detailInfo.status === '30'">拒绝</span>
          </el-descriptions-item>
          <el-descriptions-item v-if="detailInfo.status === '30'" label="驳回理由">{{detailInfo.reason}}</el-descriptions-item>
        </el-descriptions>
      </div>
    </el-dialog>
    <checked-log :father-dialog-visible.sync="checkedLogVisible" :log-type="6"></checked-log>

  </div>
</template>

<script>
import SearchForm from "@/components/list/SearchFrom.vue";
import TableList from "@/components/list/TableList.vue";
import C from "@/util/constants";
import audit from "@/api/audit-reply";

import moment from 'moment'
import community from "@/api/feedback";
import CheckedLog from "@/components/checkedLog.vue";

export default {
  name: "BaseUserInfo",
  components: { CheckedLog, TableList, SearchForm },
  data() {
    return {
      reviewTabs: "audit",
      searchAudit: {
        content: '',
        member_name: '',
        status: '10',
        member_id: '',
        sourseid: '',
        start_time: undefined,
        end_time: undefined,
        curpage: 1,
        // 每页条数
        pagesize: C.LIST_PAGE_SIZE
      },
      searchComplete: {
        content: '',
        member_name: '',
        sourseid: '',
        status: '',
        starttime: undefined,
        endtime: undefined,
        // 当前页
        curpage: 1,
        // 每页条数
        pagesize: C.LIST_PAGE_SIZE
      },
      titleFunOption: [{
        label: '审核日志',
        class: '',
        fun: 'log',
        btnType: 'primary',
        // limitsMenu: '拉新活动',
        // limitsDown: '新增',
      }],
      listData: [],
      total: 0,
      selectsList: [],
      dialogLoading: false,
      // 拒绝弹框
      rejectDialog: false,
      rejectFrom: {
        classA: '',
        classB: [],
        reason: '',
      },
      rejectFromRules: {
        classA: [
          { required: true, message: '请选择拒绝理由', trigger: 'blur' }
        ]
      },
      rejectList: [],
      rejectChildList: [],
      rowDetail: null,
      checkedLogVisible: false,
      detailDialogVisible: false,
      detailInfo:{}
    }
  },
  computed: {
    searchOptionInner() {
      if (this.reviewTabs === 'complete') {
        return [
          { label: '评论内容关键词', prop: 'content', clearable: true },
          { label: '发布者昵称/ID', prop: 'member_name', clearable: true },
          { label: '目标作品ID', prop: 'sourseid', clearable: true },
          {
            label: '审核状态',
            prop: 'status',
            type: 'select',
            default: null,
            clearable: true,
            option: [
              { label: "通过", value: 20 },
              { label: "驳回", value: 30 }]
          },
          { prop: 'time', label: '发布时间', type: 'dateRange', start: "starttime", end: "endtime", clearable: true },
        ]
      } else {
        return [
          { label: '评论内容关键词', prop: 'content', clearable: true },
          { label: '发布者昵称', prop: 'member_name', clearable: true },
          { label: '发布者ID', prop: 'member_id', clearable: true },
          { label: '目标作品ID', prop: 'sourseid', clearable: true },
          { prop: 'time', label: '发布时间', type: 'dateRange', start: "start_time", end: "end_time", clearable: true },
        ]
      }
    },
    tableColOptionInner() {
      if (this.reviewTabs === 'complete') {
        return [
          { prop: 'data', slot: true, headerSlot: "data1header", width: 50 },
          { label: '序号', type: 'index' },
          { label: '评论内容', prop: 'content' },
          { label: '评论图片', prop: 'img', propType: 'image' },
          { label: '目标作品ID', prop: 'sourseid' },
          { label: '发布者昵称', prop: 'member_name' },
          { label: '发布者ID', prop: 'member_id' },
          { label: '点赞量', prop: 'hot_num', },
          { label: '审核状态', prop: 'status', slot: true },
          { label: '审核人', prop: 'ename' },
          { label: '发布时间', prop: 'itime', propType: "time" },
          { label: '审核时间', prop: 'etime', propType: "time" },
          {
            label: '操作', type: 'action', actions: [
              {
                label: '详情',
                // limitsMenu: '拉新活动',
                // limitsDown: '启用/停用',
                fun: 'detail',
                showWithProp: 'status',
                showWithPropValue: ["20"]
              },
              {
                label: '删除',
                // limitsMenu: '拉新活动',
                // limitsDown: '启用/停用',
                fun: 'delete',
                customClass: 'del',
                showWithProp: 'status',
                showWithPropValue: ["20"]
              }
            ]
          }
        ]
      } else {
        return [
          { prop: 'data', slot: true, headerSlot: "data1header", width: 50 },
          { label: '序号', type: 'index' },
          { label: '评论内容', prop: 'member_id' },
          { label: '评论图片', prop: 'img', propType: 'image' },
          { label: '目标作品ID', prop: 'sourseid' },
          { label: '发布者昵称', prop: 'member_name' },
          { label: '发布者ID', prop: 'member_id' },
          { label: '发布时间', prop: 'itime', propType: "time" },
          {
            label: '操作', type: 'action', actions: [
              {
                label: '驳回',
                // limitsMenu: '拉新活动',
                // limitsDo wn: '启用/停用',
                customClass: 'del',
                fun: 'reject',
                showWithProp: 'exam_status',
                showWithPropValue: ["10"]
              },
              {
                label: '通过',
                // limitsMenu: '拉新活动',
                // limitsDown: '启用/停用',
                fun: 'pass',
                showWithProp: 'exam_status',
                showWithPropValue: ["10"]
              }]
          }
        ]
      }
    },

  },
  mounted() {
    this.getList()
  },
  methods: {

    onSelectRejectItem(id) {
      const { classB } = this.rejectFrom
      if (classB.includes(id)) {
        classB.splice(classB.indexOf(id), 1)
      } else {
        classB.push(id)
      }
    },
    onRejectChange(value) {
      this.rejectFrom.classB = []
      this.rejectChildList = this.rejectList.find(item => item.id === value).childs
    },
    async onSubmitDialog() {
      await this.$refs.rejectFrom.validate()
      const { classA, classB } = this.rejectFrom
      if (classB.length <= 0) {
        this.$message.error('请至少选择一个驳回理由')
        return
      }
      await this.auditProcessing(this.rowDetail, '30', {
        reason: this.rejectFrom.reason,
        reject_id: { id: classA, childs: classB }
      })
      this.$common.notifySuccess('审核驳回', '已更终端数据')
      this.rejectDialog = false
    },
    async openRejectDialog() {
      const { data, message, code } = await community.RejectConfigAPI()
      if (code === 1) {
        this.rejectList = data.list
        this.rejectFrom.classA = data.list[0].id
        this.rejectChildList = data.list[0].childs
      } else {
        this.$message.error(message)
      }
    },
    isChecked(item) {
      return this.selectsList.findIndex((it) => it.id === item.id) >= 0;
    },
    onCheckedChange(item) {
      const index = this.selectsList.findIndex((it) => it.id === item.id);
      if (index >= 0) {
        this.selectsList.splice(index, 1);
      } else {
        this.selectsList.push(item);
      }
    },
    // 全选
    onSelectAllChange() {
      const selects = [...(this.selectsList), ...(this.listData)]
      this.selectsList = [...(new Set(selects))]
    },
    onSelectInvertChange() {
      const { listData, selectsList } = this;
      this.selectsList = listData.filter(item => selectsList.findIndex(it => it.id === item.id) < 0)
    },
    // 一键删除
    onDeleteSelected() {
      if (this.selectsList.length <= 0) {
        this.$common.notifyError('请至少选择一条数据')
        return false
      }
      const ids = this.selectsList.map(item => {
        return item.id
      }).join(',')
      this.$common.openConfirm(
        `是否确定<span style="color: red">删除</span>全部选中评论吗?<br/><p>删除后对应终端将无法展示</p>`,
        '确认提示',
        {
          type: 'warning',
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          confirmCallBack: ({ confirmButtonLoadingClose }) => {
            audit.delCommunitySelected(ids).then(() => {
              this.$common.notifySuccess('删除成功', '已更终端数据')
              this.getList()
              this.selectsList = []
            }).catch(() => {
              this.$common.notifyError('删除失败')
            }).finally(() => {
              confirmButtonLoadingClose()
            })
          }
        }
      )
    },
    // 一键审核
    toExamineSelected() {
      if (this.selectsList.length <= 0) {
        this.$common.notifyError('请至少选择一条数据')
        return false
      }
      let flag
      if (this.reviewTabs === 'audit') {
        flag = this.selectsList.every(it => it.exam_status === '10')
      } else {
        flag = this.selectsList.every(it => it.status === '10')
      }
      if (!flag) {
        this.$common.notifyError('只能选择待审核状态数据')
        return false
      }
      const ids = this.selectsList.map(item => {
        return item.id
      }).join(',')
      this.$confirm(
        `是否通过选择的全部数据?<br/><p>通过后将会显示在终端,全部用户可见</p>`,
        '确认提示',
        {
          type: 'warning',
          confirmButtonText: '通过',
          cancelButtonText: '不通过',
          dangerouslyUseHTMLString: true,
          closeOnClickModal: false,
          distinguishCancelAndClose: true,
        }).then(async () => {
        const { code, message } = await audit.auditProcessingApi({
          exam_status: '20',
          ids,
          k_type: 'comment'
        })
        if (code === 1) {
          this.$common.notifySuccess('审核已通过', '已更终端数据')
          await this.getList()
          this.selectsList = []
        } else {
          this.$message.error(message)
        }
      }).catch(async (action) => {
        if (action == 'cancel') {
          const { code, message } = await audit.auditProcessingApi({
            exam_status: '30',
            ids,
            reason: this.rejectFrom.reason,
            k_type: 'comment'
          })
          if (code === 1) {
            this.$common.notifySuccess('审核已驳回', '已更终端数据')
            await this.getList()
            this.selectsList = []
          } else {
            this.$message.error(message)
          }
        }
      });
    },
    handleTabsClick() {
      this.$nextTick(() => {
        switch (this.reviewTabs) {
          case "audit": {
            this.$refs.tableAudit.calculateTableListHeight();
            break
          }
          case "complete": {
            this.$refs.tableComplete.calculateTableListHeight();
            break
          }
        }
      })

      this.selectsList = []
      this.onClearSearch()
      this.getList()
    },
    onClearSearch() {
      if (this.reviewTabs !== 'complete') {
        this.searchAudit = {
          content: '',
          member_name: '',
          member_id: '',
          status: '10',
          sourseid: '',
          start_time: undefined,
          end_time: undefined,
          curpage: 1,
          // 每页条数
          pagesize: C.LIST_PAGE_SIZE
        }
      } else {
        this.searchComplete = {
          content: '',
          member_name: '',
          sourseid: '',
          status: '',
          starttime: undefined,
          endtime: undefined,
          // 当前页
          curpage: 1,
          // 每页条数
          pagesize: C.LIST_PAGE_SIZE
        }
      }

    },
    onSearchClick() {
      this.getList();
    },
    // 分页变化事件
    listPageChange() {
      this.selectsList = []
      this.getList()
    },
    async auditProcessing(row = {}, mark, params = {}) {
      const { code, message } = await audit.auditProcessingApi({
        exam_status: mark,
        ids: row.id,
        k_type: 'comment',
        ...params
      })
      if (code === 1) {
        await this.getList()
      } else {
        this.$message.error(message)
      }
    },
    //表格点击事件
    async handleClick(fun, row) {
      switch (fun) {
        case "log":
          this.checkedLogVisible = true
          break;
        case 'detail':
          this.detailInfo = {}
            this.detailDialogVisible = true
          const { data, message, code } = await audit.getCommunityDetail({ id: row.id})
          if (code === 1) {
            this.detailInfo = data
          } else {
            this.$message.error(message)
          }
          break;

        case 'delete':
          await this.$common.openConfirm(
            `是否确定<span style="color: red">删除</span>选中评论吗?<br/><p>删除后对应终端将无法展示</p>`,
            '确认提示',
            {
              type: 'warning',
              confirmButtonText: '确定',
              cancelButtonText: '取消',
              confirmCallBack: async ({ confirmButtonLoadingClose }) => {
                audit.delCommunityCurrent({ id: row.id }).then(() => {
                  this.$common.notifySuccess('删除成功', '已更终端数据')
                  this.getList()
                }).catch(() => {
                  this.$common.notifyError('删除失败')
                }).finally(() => {
                  confirmButtonLoadingClose()
                })
              }
            })
          break;

        case "pass":
          await this.$common.openConfirm(
            `是否确定通过该条数据的审核?<br/><p>通过后所有用户可见</p>`,
            '确认提示',
            {
              type: 'warning',
              confirmButtonText: '确定',
              cancelButtonText: '取消',
              confirmCallBack: async ({ confirmButtonLoadingClose }) => {
                await this.auditProcessing(row, '20')
                this.$common.notifySuccess('审核通过', '已更终端数据')
                this.rejectDialog = false
                confirmButtonLoadingClose()
              }
            }
          )
          break;
        case "reject":
          this.rejectDialog = true
          this.rowDetail = row
          break;
        case "offShelf":
          await this.$common.openConfirm(
            `是否确定下架该推荐?<br/><p>下架后该推荐将会在用户端全部隐藏,请确认是否继续该操作?</p>`,
            '确认提示',
            {
              type: 'warning',
              confirmButtonText: '确定',
              cancelButtonText: '取消',
              confirmCallBack: async ({ confirmButtonLoadingClose }) => {
                // 下架Api
                confirmButtonLoadingClose()
              }
            })
          break;
      }
    },
    //获取列表
    async getList() {
      try {
        this.dialogLoading = true
        let params = {}
        if (this.reviewTabs === 'audit') {
          const { content, member_name, member_id, sourseid, start_time, end_time } = this.searchAudit
          params = {
            content,
            member_name,
            member_id,
            status: '10',
            sourseid,
            start_time: start_time ? moment(start_time).unix() : undefined,
            end_time: end_time ? moment(end_time).unix() : undefined,
          }

        } else {
          const { content, member_name, sourseid, status, starttime, endtime } = this.searchComplete
          params = {
            content,
            member_name,
            sourseid,
            status,
            start_time: starttime ? moment(starttime).unix() : undefined,
            end_time: endtime ? moment(endtime).unix() : undefined,
          }
        }
        const {
          code,
          data,
          message
        } = await (this.reviewTabs === 'audit' ? audit.getAuditListApi : audit.getCompleteListApi)(params)
        if (code === 1) {
          this.listData = data.list
          this.total = Number(data.list.length)
        } else {
          this.$message.error(message)
        }
      } finally {
        this.dialogLoading = false
      }
    },
  }
}
</script>
<style scoped lang="scss">
.recommend {
  .el-tabs {
    height: 60px !important;
  }

  .table-list {
    height: 100% !important;
  }

  .list__table {
    flex: 1;
  }

  ::v-deep {
    .el-radio-group {
      display: grid;
      grid-template-columns: repeat(auto-fill, 100px);
      grid-row-gap: 20px;
      grid-column-gap: 20px;
    }
  }
}


.multiple-selection-operation {
  position: absolute;
  left: 39px;
  bottom: 33px;
  display: flex;
}

.sub-reject-container {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-row-gap: 20px;
  grid-column-gap: 10px;
  margin-top: 20px;
}

.sub-reject-item {
  padding: 4px 8px;
  border: 1px solid #7a7a7a;

  &:hover {
    background-color: #d3d3d3;
    border-color: #bdbdbd;
    cursor: pointer;
  }
}

.reject-active {
  background-color: #d3d3d3;
  border-color: #bdbdbd;
  cursor: pointer;
}

::v-deep {
  .el-tabs__item {
    font-size: 16px;
  }

  .el-tabs {
    height: 100% !important;
  }

  .el-table {
    .cell {
      padding: 4px 5px 4px !important;
    }
  }
}
</style>

<style lang="scss">
.custom-detail-log {
  .el-dialog__body {
    padding-top: 8px !important;
  }
}
</style>

SearchFrom.vue 搜索组件

<!--搜索表单组件-->
<template>
  <el-form
    class="search-box"
    ref="searchForm"
    :model="search"
    :size="size"
    :inline="true"
    :label-width="width"
    label-position="right">
    <template v-for="form in searchOption">
      <!--   :label="form.label"   -->
      <el-form-item
        :label-width="form.width || width">
        <!--选择框-->
        <template v-if="form.type === 'select'">
          <el-select v-model="search[form.prop]"
                     :multiple="form.multiple || false"
                     :filterable="form.filterable || true"
                     :placeholder="form.placeholder || `请选择${form.label}`"
                     :clearable="form.clearable">
            <template v-if="form['optionSlot']">
              <slot :name="form['optionSlot']" :option="form"></slot>
            </template>
            <template v-else>
              <el-option v-for="(opt, odx) in form.option"
                         :key="'search_option_' + odx"
                         :label="opt[form.selectionLabel] || opt.label"
                         :value="opt[form.selectionId] || opt.value"
              />
            </template>
          </el-select>
        </template>
        <!--时间-->
        <template v-else-if="form.type === 'time'">
          <el-time-picker
            is-range
            v-model="search[form.prop]"
            range-separator="-"
            start-placeholder="开始时间"
            end-placeholder="结束时间"
            :value-format="form.format || 'HH:mm:ss'"
            :placeholder="form.placeholder || `请选择${form.label}`"
            :clearable="form.clearable">
          </el-time-picker>
        </template>
        <!--日期-->
        <template v-else-if="form.type === 'date'">
          <el-date-picker v-model="search[form.prop]"
                          type="date"
                          :placeholder="form.placeholder || `请选择${form.label}`"
                          :value-format="form.format || 'yyyy-MM-dd'"
                          :clearable="form.clearable"></el-date-picker>
        </template>
        <!--级联-->
        <template v-else-if="form.type === 'cascader'">
          <el-cascader v-model="search[form.prop]"
                       :placeholder="form.placeholder || `请选择${form.label}`"
                       :props="form.groupingProps"
                       :options="form.option"
                       :clearable="form.clearable"></el-cascader>
        </template>

        <!--范围日期-->
        <template v-else-if="form.type === 'dateRange'">
          <el-date-picker v-model="search[form.prop]"
                          type="daterange"
                          range-separator="-"
                          :value-format="form.format || 'yyyy-MM-dd'"
                          start-placeholder="开始日期"
                          end-placeholder="结束日期"
                          :clearable="form.clearable"></el-date-picker>
        </template>
        <!--搜索框-->
        <template v-else>
          <el-input class="search-input"
                    autocomplete="off"
                    v-model="search[form.prop]"
                    :placeholder="form.placeholder || `请输入${form.label}`"
                    :clearable="form.clearable" />
        </template>
      </el-form-item>
    </template>
    <!--扩展插槽-->
    <slot name="form"></slot>
    <div class="search-btn-box" v-if="searchOption.length > 0">
      <el-button type="primary"
                 icon="el-icon-search"
                 @click="handleSearch">查询
      </el-button>
      <el-button
        icon="el-icon-refresh"
        @click="handleClear">重置
      </el-button>

      <div class="list-header__fun">
        <template v-for="(fun, funIdx) in titleFun">
          <template v-if="fun.slot">
            <slot :name="fun.fun" :fun="fun" :funIdx="funIdx"></slot>
          </template>
          <template v-else>
            <template
              v-if="fun.limitsMenu == null && fun.limitsMenu == null || $common.findAuthFun(fun.limitsMenu, fun.limitsDown)">
              <el-button
                :key="`list_fun_${funIdx}`"
                :class="['fun-btn hover', fun.class || '']"
                :icon="fun.icon"
                :type="fun.btnType"
                @click="headerFunClick(fun.fun)">
                {{ fun.label }}
              </el-button>
            </template>

          </template>
        </template>
      </div>
    </div>
  </el-form>
</template>

<script>
import C from '@/util/constants'

import _ from 'lodash'

export default {
  name: 'SearchForm',
  model: {
    prop: 'form',
    event: 'update'
  },
  props: {
    // 配置项
    searchOption: {
      default() {
        return [];
      },
      type: Array
    },
    // 表单参数 (支持v-model)
    form: {
      type: Object,
      default() {
        return {};
      }
    },
    // 头部按钮
    titleFun: {
      type: Array,
      default() {
        return []
      }
    },
    // 尺寸
    size: {
      default: C.COMPONENT_SIZE,
      type: String
    },
    // 标题宽度
    width: {
      default: '70px',
      type: String
    },
    // 是否自定义清空
    customClear: {
      type: Boolean,
      default: false
    },
    // 清空时是否触发搜索
    searchOnClear: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      search: {}
    };
  },
  computed: {
    searchOptionMap() {
      const searchOptionMap = {}

      const searchOption = this.searchOption

      for (const option of searchOption) {
        searchOptionMap[option.prop] = option
      }

      return searchOptionMap
    }
  },
  watch: {
    // 监听值的变化
    form: {
      handler(form) {
        if (!_.isEqual(form, this.search)) {
          this.search = { ...form }
        }
      },
      deep: true,
      immediate: true
    },
    search: {
      handler(search) {
        this.$emit('update', search)
      },
      deep: true
    }
  },
  methods: {
    /** 点击搜索 */
    handleSearch() {
      // 格式化搜索参数
      const searchOptionMap = this.searchOptionMap;
      const search = this.search;

      for (const key in searchOptionMap) {
        const option = searchOptionMap[key];

        const value = search[key];
        if (option.type === 'dateRange' || option.type === 'time') {
          const [start = '', end = ''] = value ?? [];
          search[option.start] = start;
          search[option.end] = end;
        } else {
          search[key] = value ?? '';
        }
      }
      this.search = { ...search }

      this.$emit('search', this.search);
    },
    /** 头部点击事件 */
    headerFunClick(funName) {
      this.$emit('table-click', funName, {}, -1)
    },
    /** 清空搜索条件 */
    handleClear() {
      if (!this.customClear) {
        const searchOptionMap = this.searchOptionMap
        const search = this.search

        for (const key in searchOptionMap) {
          const option = searchOptionMap[key]

          const def = option.default

          const defaults = def === undefined ? '' : def
          search[key] = defaults

          if (option.type === 'dateRange') {
            search[option.start] = defaults
            search[option.end] = defaults
          }
        }

        this.search = { ...search }
      }

      this.$emit('clear', this.search)

      if (this.searchOnClear) {
        this.$emit('search', this.search)
      }
    }
  }
}
</script>

<style lang="scss" scoped>
.search-box {
  display: flex;
  flex-wrap: wrap;
  box-sizing: border-box;
  $select-input-width: 170px;
  $search-btn-width: 90px;

  ::v-deep {
    .el-input--medium {
      .el-input__inner {
        height: 40px !important;
        line-height: 40px !important;
      }
    }
  }

  .el-form-item {
    margin-bottom: 10px;

    .el-form-item__label {
      font-size: 14px;
    }
  }

  .search-btn-box {
    display: flex;
    flex-direction: row;
    margin-bottom: 10px;

    .el-button {
      //border-radius: 20px;
      //font-size: 13px;
    }
  }

  .list-header__fun {
    margin-left: 10px;
  }

  // 设置圆角
  //.el-input__inner {
  //  border-radius: 15px;
  //}


  // 普通输入框
  .search-input {
    .el-input__inner {
      width: $select-input-width;
    }
  }

  // 单选日期
  .el-date-editor--date {
    width: $select-input-width;

    .el-input__inner {
      padding-right: 20px;
    }
  }

  // 范围日期
  .el-date-editor--daterange {
    width: 240px;
    height: 40px !important;
    line-height: 40px !important;
  }

  // 选择框
  //.el-select {
  //  width: $select-input-width;
  //}

  // 搜索按钮
  .el-button {
    width: $search-btn-width;
  }
}
</style>

TableList.vue 表格组件

<template>
  <div ref="table" :class="['table-list',{ 'box-base': box }]">
    <!--表格列表-->
    <el-table
      ref="multipleTable"
      v-loading="dialogLoading"
      class="list__table"
      :data="data"
      :height="tableHeight"
      :stripe="stripe"
      border
      v-bind="$attrs"
      @selection-change="handleSelectionChange">
      <!--自定义空行-->
      <el-empty slot="empty-text" description="暂无数据"></el-empty>
      <!--判断是否开启多选-->
      <el-table-column
        v-if="isSelection"
        type="selection"
        align="left"
        width="70px"
        :selectable="checkIsSelectable"></el-table-column>
      <template v-for="column in columns">
        <!--序号列-->
        <el-table-column
          v-if="column.type === 'index'"
          type="index"
          :label="column.label || '序号'"
          :align="column.align || 'left'"
          :width="column.width || 100"
          :index="column['tableIndex'] || getTableIndex"
        />
        <!--自定义展开列-->
        <el-table-column v-else-if="column.type === 'expand'" type="expand">
          <template slot-scope="scope">
            <slot :name="column.prop" :row="scope.row" :index="scope.$index"></slot>
          </template>
        </el-table-column>
        <!--操作列-->
        <el-table-column
          v-else-if="column.type === 'action'"
          :label="column.label"
          :align="column.align || 'left'"
          :width="column.width || 150">
          <template slot-scope="scope">
            <template v-for="(action, actIdx) in column.actions">
              <template v-if="actIdx < 5">
                <template
                  v-if="action.showWithProp == null || ((action.showWithPropValue || []).indexOf(scope.row[action.showWithProp]) >= 0)">
                  <template
                    v-if="action.limitsMenu == null && action.limitsMenu == null || $common.findAuthFun(action.limitsMenu, action.limitsDown)">
                    <!--                    getActionOptionValue(action, scope.row,'class')    :class="`${action.class}`"-->

                    <el-button type="text" v-if="getIsBtnShow(scope.row[`${action.fun}BtnShow`])"
                               :key="`action_btn_${actIdx}`"
                               size="small"
                               :icon="`el-icon-${action.icon}`"
                               :class="action.customClass"
                               @click="actionClick(scope, action.fun, getActionOptionValue(action, scope.row, 'option'))">
                      {{ getActionOptionValue(action, scope.row, 'label') }}
                    </el-button>
                  </template>
                </template>
              </template>
            </template>
            <!-- 没有写权限判断哈-->
            <template v-if="column.actions.length > 5">
              <el-dropdown
                trigger="click"
                class="action-more-line"
                @command="command => {actionClick(scope, command)}">
                <span class="el-dropdown-link">更多<i
                  class="el-icon-arrow-down el-icon--right"></i></span>
                <el-dropdown-menu slot="dropdown">
                  <template v-for="(action, actIdx) in column.actions">
                    <template v-if="actIdx >= 2">
                      <el-dropdown-item :command="action.fun">{{ action.label }}
                      </el-dropdown-item>
                    </template>
                  </template>
                </el-dropdown-menu>
              </el-dropdown>
            </template>
          </template>
        </el-table-column>
        <!--自定义列、常规列-->
        <el-table-column
          v-else
          :label="column.label"
          :align="column.align || 'left'"
          :fixed="column.fixed || false"
          :resizable="column.resize || false"
          :min-width="column.width">
          <template v-if="column.headerSlot" v-slot:header="scope">
            <slot :name="column.headerSlot" :column="scope.column" :index="scope.$index"></slot>
          </template>
          <template slot-scope="scope">
            <!--需自定义处理-->
            <slot v-if="column.slot" :name="column.prop" :row="scope.row" :index="scope.$index"></slot>
            <template v-else>
              <!--开关列-->
              <el-switch
                v-if="column.propType === 'state'"
                :value="getValueByProp(scope.row, column.prop)"
                :active-value="column['open'] == null ? true : column['open']"
                :inactive-value="column['close'] == null ? false : column['close']"
                :inactive-color="column['closeColor'] || ''"
                :active-color="column['openColor'] || ''"
                :inactive-text="column['closeText'] || ''"
                :active-text="column['openText'] || ''"
                @change="(value)=>{switchChange(value, column, scope)}"
              ></el-switch>

              <!--图片列-->
              <template v-else-if="column.propType === 'image'">
                <app-image-uploader
                  v-if="getValueByProp(scope.row, column.prop)"
                  :imageList="getValueByProp(scope.row, column.prop)"
                  :trashDisabled="false"></app-image-uploader>
                <span v-else>--</span>
              </template>

              <!--状态文字列-->
              <span v-else-if="column.propType === 'text'"
                    :style="{ color: getOptionValue(column.option, getValueByProp(scope.row, column.prop), 'color') }">{{
                  getOptionValue(column.option, getValueByProp(scope.row, column.prop), 'label', '/')
                }}</span>
              <!--格式化时间-->
              <span v-else-if="column.propType === 'time'" :style="{ color: '' }">
                              {{ getValueByProp(scope.row, column.prop) | dateFormat }}</span>
              <!--范围时间双字段-->
              <!--              <span v-else-if="column.propType === 'time-range'" :style="{ color: '' }">{{-->
              <!--                getValueByProp(scope.row, column.prop[0]) | dateTime(column.format)-->
              <!--              }} ~ {{ getValueByProp(scope.row, column.prop[1]) | dateTime(column.format) }}</span>-->
              <!--链接列-->
              <span v-else-if="column.propType === 'link'">
                <a
                  class="table-link hover-line-center"
                  target="_blank"
                  :href="getValueByProp(scope.row, column.prop)">{{ getValueByProp(scope.row, column.prop) }}</a>
              </span>
              <!--常规类-->
              <span v-else :style="{ color: '' }">{{ getValueByProp(scope.row, column.prop) | defaults }}</span>
            </template>
          </template>
        </el-table-column>
      </template>
    </el-table>
    <!--分页组件-->
    <pagination-box
      v-if="isPagination"
      :page="page"
      :page-size="pageSize"
      :page-sizes="pageSizes"
      v-bind="$attrs"
      @update:page="$emit('update:page', $event)"
      @update:page-size="$emit('update:page-size', $event)"
      v-on="$listeners" />
  </div>
</template>

<script>
import PaginationBox from './PaginationBox.vue'
import appImageUploader from "@/components/list/AppImageUploader.vue";
import Sortable from 'sortablejs';
import C from '@/util/constants'

import _ from 'lodash'

export default {
  name: 'TableList',
  components: { PaginationBox, appImageUploader },
  props: {
    // 列表数据源
    data: {
      default() {
        return []
      },
      type: Array
    },
    dialogLoading: {
      type: [Boolean, Object],
      default: false
    },
    // 表格列配置
    columns: {
      default() {
        return []
      },
      type: Array
    },
    // 是否显示标题栏
    showTitle: {
      type: Boolean,
      default: true
    },
    // 是否开启多选
    // 是否开启多选
    isSelection: {
      default: false,
      type: Boolean
    },
    // checkbox是否可用prop (仅isSelection为true时生效)
    selectableProp: {
      type: String,
      default: null
    },
    draggableDOM: {
      type: String,
      default: ''
    },
    // 是否对selectableProp的值取反  (仅isSelection为true时生效)
    selectableAnti: {
      type: Boolean,
      default: false
    },
    // 是否开启斑马线风格
    stripe: {
      default: true,
      type: Boolean
    },
    // 是否显示阴影
    box: {
      default: true,
      type: Boolean
    },
    // 头部标题
    title: {
      default: '',
      type: String
    },
    // 是否显示分页
    isPagination: {
      default: true,
      type: Boolean
    },
    // 页码
    page: {
      type: Number,
      default: 1
    },
    // 每页条数
    pageSize: {
      type: Number,
      default: C.LIST_PAGE_SIZE
    },
    pageSizes: {
      default() {
        return [C.LIST_PAGE_SIZE, 30, 50, 100, 200, 500];
      },
      type: Array
    },
    // 表格高度
    height: {
      type: [String, Number, Boolean],
      default: true
    },
    //  是否可拖拽
    isDraggable: {
      default: false,
      type: Boolean
    }
  },
  data() {
    return {
      // 当前选择的列
      selectList: [],
      // 表格高度
      tableHeight: undefined
    }
  },
  watch: {
    height(val) {
      if (typeof val !== 'boolean') {
        this.tableHeight = val
      }
    }
  },
  created() {
    this.tableHeight = typeof this.height !== 'boolean' ? this.height : undefined
  },
  mounted() {
    // 如果为bool值且为true时才自动计算高度
    if (typeof this.height === 'boolean' && this.height) {
      this.calculateTableListHeight()

      window.addEventListener("resize", this.calculateTableListHeight);
    }
    if (this.isDraggable) {
      this.rowDrop();
    }
  },
  beforeDestroy() {
    this.destorySortable();

    window.removeEventListener("resize", this.calculateTableListHeight);
  },
  methods: {
    /** 行拖拽 */
    rowDrop() {
      this.destorySortable();
      setTimeout(() => {
        const tbody = document.querySelector(`${this.draggableDOM} .el-table__body-wrapper>.el-table__body tbody`);
        this.sortable = Sortable.create(tbody, {
          animation: 180,
          delay: 0,
          onEnd: ({ newIndex, oldIndex }) => {
            this.$refs.multipleTable.doLayout();
            this.$emit("onDragEnd", newIndex, oldIndex);
          },
        });
      }, 800);
    },
    destorySortable() {
      if (this.sortable != null) {
        this.sortable.destroy();
        this.sortable = null;
      }
    },
    getValueByProp(row, prop) {
      return _.get(row, prop)
    },
    setValueByProp(row, prop, value) {
      _.set(row, prop, value)
    },
    checkIsSelectable(row) {
      const { selectableProp, selectableAnti } = this

      if (selectableProp == null) {
        return true
      }

      const selectable = this.getValueByProp(row, selectableProp)

      return selectableAnti ? !selectable : selectable
    },
    /**
     * @param option 外部传入的配置数组
     * @param propValue 原始值
     * @param key  需要获取的key
     * @param def  默认值
     * @returns {*|string}
     */
    getOptionValue(option, propValue, key, def = '') {
      // 判断值
      propValue = typeof propValue === 'boolean' ? Number(propValue) : propValue
      return option && option[propValue] ? option[propValue][key] : def
    },
    /** 获取操作按钮配置 */
    getActionOptionValue(option, rowData, key) {
      let actionOption = option
      if (option.prop && option.option) {
        const value = Number(rowData[option.prop])
        actionOption = option.option[value]
      }
      return key === 'option' ? actionOption : (actionOption[key] || '')
    },
    /** 获取按钮状态 */
    getIsBtnShow(btnShow) {
      return (typeof btnShow !== 'undefined') ? btnShow : true
    },
    /** 序号 */
    getTableIndex(index) {
      return index + 1 + (this.page - 1) * this.pageSize
    },
    /** 多选事件 */
    handleSelectionChange(selectionList) {
      this.selectList = selectionList
      this.$emit('select-change', selectionList)
    },
    /** 按钮点击事件 */
    actionClick(scope, funName, option = {}) {
      // if (option.confirmText) {
      //   this.$message({
      //     message: option.confirmText,
      //     type: 'warning'
      //   }).then(res => {
      //     if (res === 'confirm') {
      //       this.$emit('table-click', funName, JSON.parse(JSON.stringify(scope.row)), scope.$index)
      //     }
      //   }).catch(() => {
      //   })
      // } else {
      // }
      this.$emit('table-click', funName, JSON.parse(JSON.stringify(scope.row)), scope.$index)
    },
    /** 开关点击事件 */
    switchChange(value, option, scope) {
      this.setValueByProp(scope.row, option.prop, value)
      this.actionClick(scope, option.prop, option)
    },
    // 计算表格高度
    calculateTableListHeight() {
      // 当前视窗高度
      const viewH = window.innerHeight
      this.$nextTick(() => {
        // setTimeout(() => {
        const tableListDom = this.$refs.table

        if (tableListDom == null) {
          return
        }

        // 获取tableDom
        const tableDom = tableListDom.querySelectorAll('.el-table')[0]
        const pageDomAll = tableListDom.querySelectorAll('.pagination-box')

        if (tableDom == null || pageDomAll == null) {
          return
        }

        let pageH = 0
        if (pageDomAll.length > 0) {
          pageH = pageDomAll[0].offsetHeight
        }
        const rect = tableDom.getBoundingClientRect()
        // 表格高度 = 视窗高度 - 表格top - 分页 - 底边距
        this.tableHeight = viewH - rect.top - pageH - 15
        // }, 100)
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.table-list {
  margin-top: 8px;
  overflow: hidden;

  ::v-deep {
    .el-table {
      transition: height 0.3s ease-out;

      .el-table__cell {
        padding: 20px 0 !important;
      }
    }

    .pagination-box {
      text-align: right !important;
    }
  }

  .list__header {
    .list-header__title {
      span::before {
        content: "";
        position: absolute;
        left: -10px;
        width: 2px;
        border-radius: 4px;
        height: 20px;
        top: 50%;
        transform: translate(0, -50%);
      }
    }
  }

  .list__table {
    width: 100%;
    // 处理出现滚动条时的样式错误
    .el-table__fixed-right-patch {
      background-color: #2196f3;
      border-bottom: 1px solid #2196f3;
    }

    thead {
      th {
        color: #333;
        background-color: #fafafa;
        font-weight: 400;
        font-size: 14px;
        border-right: none;
      }
    }

    .el-table-column--selection {
      .el-checkbox {
        &.is-disabled {
          .el-checkbox__inner::after {
            display: none;
          }
        }

        .el-checkbox__inner {
          width: 22px;
          height: 22px;

          &::after {
            left: 7px;
            height: 12px;
            width: 4px;
            border-width: 2px;
          }
        }
      }
    }

    tbody {
      td {
        color: var(--color-3);
        font-size: 13px;

        .table-link {
          //color: var(--app-theme);
          text-decoration: none;
          padding-bottom: 2px;
        }
      }
    }

    // 按钮样式
    .action-more-line {
      .el-dropdown-link {
        color: rgba(0, 0, 0, 0.8);
        font-size: 14px;
        outline: 0;
        cursor: pointer;
      }
    }

    ::v-deep {
      .el-button--text {
        & > span {
          margin-left: 0 !important;
        }
      }
    }
  }
}

// 新增
.add {
  color: #2196f3;
}

// 详情
.detail {
  color: #7b68ee;
}

// 编辑
.edit {
  color: #1e6abc;
}

// 删除
.del {
  color: #ff301f;
}

// 重置
.reset {
  color: #ff7f24;
}

// 审核
.audit {
  color: #ee4000;
}

// 导出
.export {
  color: #911656;
}
</style>

constants.js

const APP = {
    // 每页默认条数
    LIST_PAGE_SIZE: 10,
    // 组件默认尺寸
    COMPONENT_SIZE: 'medium',
    // 上传图片大小默认值
    UPLOAD_IMAGE_SIZE: 5,
    // 附件/文件大小限制
    UPLOAD_FILE_SIZE: 50,
    // 视频大小
    UPLOAD_VIDEO_SIZE: 200,
    // 图片上传格式
    UPLOAD_IMAGE_TYPE: 'jpg,jpeg,png',
    // 附件上传格式
    UPLOAD_FILE_TYPE: 'pdf,docx,xlsx,ppt,txt',
    // 视频上传格式
    UPLOAD_VIDEO_TYPE: 'mp4,mp3'
}


const C = {...APP}
export default C

PaginationBox.vue 分页组件

<template>
  <div class="pagination-box" :style="{textAlign: align}">
    <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        background
        :hide-on-single-page="singHide"
        :size="size"
        :current-page="page"
        :page-sizes="pageSizes"
        :page-size="pageSize"
        :layout="layout"
        :total="total">
    </el-pagination>
  </div>
</template>

<script>
import C from '@/util/constants';

export default {
  name: 'PaginationBox',
  props: {
    // 当前页码
    page: {
      default: 1,
      type: Number
    },
    // 总记录数
    total: {
      default: 0,
      type: Number
    },
    // 每页条数
    pageSize: {
      default: C.LIST_PAGE_SIZE,
      type: Number
    },
    //
    pageSizes: {
      default() {
        return [C.LIST_PAGE_SIZE, 30, 50, 100, 200, 500];
      },
      type: Array
    },
    // 底部菜单
    layout: {
      default() {
        return 'total, sizes, prev, pager, next, jumper';
      },
      type: String
    },
    // 只有一页的时候是否隐藏 默认隐藏
    singHide: {
      default() {
        return false;
      },
      type: Boolean
    },
    // 尺寸
    size: {
      default() {
        return 'small'
      },
      type: String
    },
    // 位置
    align: {
      default: 'center',
      type: String
    }
  },
  methods: {
    // 每页条数变化
    handleSizeChange(size) {
      this.$emit('update:page-size', size)
      this.$emit('page-change', this.page, size);
    },
    // 页面发生变化
    handleCurrentChange(page) {
      this.$emit('update:page', page)
      this.$emit('page-change', page, this.pageSize);
    }
  }
}
</script>

<style lang="scss" scoped>
.pagination-box {
  padding: 20px 30px;
}
</style>

appImageUploader.vue 图片查看组件

<template>
  <div class="app-image">
    <div class="app-image__inner"
         :style="cssVars">
      <div v-for="(item,index) in innerImages"
           :key="index"
           class="app-image__item">
        <el-image
          :src="item"
          fit="cover"
          :preview-src-list="innerImages">
        </el-image>
      </div>
    </div>
  </div>
</template>

<script>

export default {
  name: "AppImage",
  model: {
    prop: 'imageList',
    event: 'input'
  },
  props: {
    imageList: {
      type: [Array, String],
      default() {
        return []
      }
    },
    // 上传图标颜色
    iconColor: {
      type: String,
      default: "#999999"
    },
    iconHoverColor: {
      type: String,
      default: "#409eff"
    },
    //装图标的盒子大小
    itemWidth: {
      type: String,
      default: "90px"
    },
    itemHeight: {
      type: String,
      default: "90px"
    },
    // 第二条备注
    prompt2: {
      type: String,
      default: ''
    },
    // 上传盒子之间间隔
    uploaderMargin: {
      type: String,
      default: "14px"
    },
    uploadType: {
      type: Array,
      default() {
        return []
      }
    },
    uploadSize: {
      type: Number,
      default: 0
    }
  },
  computed: {
    cssVars() {
      return {
        '--uploader-width': this.itemWidth,
        '--uploader-height': this.itemHeight,
        '--uploader-icon': this.iconFontSize,
        '--uploader-icon-color': this.iconColor,
        '--uploader-icon--hover-color': this.iconHoverColor,
        '--uploader-margin-left': this.uploaderMargin
      }
    },
    innerImages: {
      get() {
        if (typeof this.imageList === "string") {
          return this.imageList.length <= 0 ? [] : [this.imageList];
        }

        return this.imageList;
      },
      set(value) {
        if (typeof this.imageList === "string") {
          this.$emit("input", value[0] ? value[0]: "");
        }
        this.$emit("input", value);
      }
    }
  },
  data() {
    return {
    }
  },
  methods: {
  }
}
</script>
<style lang="scss" scoped>
.app-image {
  display: flex;
  align-items: center;
}

.app-image__inner {
  display: flex;
  flex-direction: row;
}

.app-image__item {
  position: relative;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  width: var(--uploader-width);
  height: var(--uploader-height);

  ::v-deep {
    .el-image {
      width: 100%;
      height: 100%;
      border-radius: 10px;

      > image {
        width: 100%;
        height: 100%;
      }
    }
  }

  & + .app-image__item {
    margin-left: var(--uploader-margin-left);
  }
}

.item-trash {
  position: absolute;
  right: -13px;
  top: -9px;
  cursor: pointer;
  font-size: 20px;

  &:hover {
    color: var(--uploader-icon--hover-color);
  }
}

.add {
  border: 1px dashed var(--uploader-icon-color);
  border-radius: 10px;

  &:hover {
    border-color: var(--uploader-icon--hover-color);

    .item-upload {
      color: var(--uploader-icon--hover-color);
    }
  }
}

.item-upload {
  font-size: var(--uploader-icon);
  color: var(--uploader-icon-color);
}

.app-image__icon__upload {
  font-size: 65px;
}

.app-image__file {
  position: absolute;
  top: 0;
  opacity: 0;
  width: var(--uploader-width);
  height: var(--uploader-height);
  cursor: pointer;

  &:hover {
    .item-upload {
      color: var(--uploader-icon--hover-color);
    }
  }
}

.app-image__prompt {
  margin-left: 20px;
  color: var(--color-8);;
}

.app-image__prompt__content {
  display: flex;
  flex-direction: column;
}
</style>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue中,将表格组件封装后需要传值时,可以通过props来进行传递。props是父组件向子组件传递数据的方式,我们可以定义一个props对象来接收父组件传递过来的值,然后在子组件中使用这个值。 例如,我们在父组件中定义了一个表格组件,并且需要向表格组件传递一个数据列表,可以这样写: ``` <template> <div> <my-table :dataList="list"></my-table> </div> </template> <script> import MyTable from './MyTable.vue' export default { components: { 'my-table': MyTable }, data () { return { list: [ { id: 1, name: '张三', age: 20 }, { id: 2, name: '李四', age: 25 }, { id: 3, name: '王五', age: 30 } ] } } } </script> ``` 在子组件中,我们需要定义props对象来接收父组件传递过来的值,并且在组件中使用这个值。例如,我们在子组件中定义了一个表格组件,可以这样写: ``` <template> <table> <thead> <tr> <th>ID</th> <th>姓名</th> <th>年龄</th> </tr> </thead> <tbody> <tr v-for="item in dataList" :key="item.id"> <td>{{ item.id }}</td> <td>{{ item.name }}</td> <td>{{ item.age }}</td> </tr> </tbody> </table> </template> <script> export default { props: { dataList: { type: Array, required: true } } } </script> ``` 在子组件的props对象中,我们定义了一个dataList属性,它的类型为Array,表示需要传递一个数组类型的值。required属性表示这个属性是必须要传递的,如果没有传递,会抛出一个警告。 在子组件的模板中,我们通过v-for循环遍历dataList数组,渲染出表格中的每一行数据。这样,就可以将父组件传递过来的值用于子组件中的渲染。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值