vue3+element-plus encapsulates list pages, paging, sorting, and exporting

Table of Contents

Background description:

Development Process:

Detailed development:

Summarize:


Background description:

Many times on the web, we need to develop a list page to display a large amount of data and provide some interactive functions, such as sorting and paging, and export functions. Sometimes we will encapsulate components in order to avoid trouble. >. Let’s use Vue 3 and Element-Plus to develop a list page.

Here, take a system log list page as an example.

Development process:

Create a component named recordList.vue, which will be used to display system logs.

Generally, this kind of page consists of three parts:

1.Page name

2. Search box

3. Tables and pagination

The first two parts are often in the same style as other pages. Just reuse the styles. The styles of tables and pagination are almost the same, so most of the entire page is simple and easy to encapsulate.

As shown in the picture:

Detailed development:

1. Subcomponent – template (the search component is a set encapsulated by myself)

<template>
  <div class="dashboard-container">
    <el-card class="card-style">
      <div class="mt-1">
        <h2 class="fwb-mb-1">{<!-- -->{ listName }}</h2>
        <el-row>
         <!---Search component--->
          <SearchBox
            v-if="dataReady"
            :search-options="searchOptions"
            :showButton="true"
            :btnLoading="btnLoading"
            button-name="Export"
            @search="searchData"
          />
        </el-row>
      </div>
      <el-table
        v-loading="loading"
        :data="tableData"
        class="table-small-custom"
        height="calc(100vh - 240px)"
        stripe
        @sort-change="changeTableSort"
      >
        <el-table-column type="index" width="70" label="serial number">
          <template #default="scope">
            <span v-text="getIndex(scope.$index)"></span>
          </template>
        </el-table-column>
        <el-table-column
          v-for="(col, index) in tableColumn"
          :key="index"
          :prop="col.prop"
          :label="col.label"
          :min-width="col.minWidth"
          :sortable="col.sortable"
          :show-overflow-tooltip="col.showOverflowTooltip"
        ></el-table-column>
        <el-table-column v-if="pageName === 'operationRecord'" label="Operation" min-width="80" fixed="right">
          <template #default="scope">
            <el-button type="primary" size="small" @click="seeDetail(scope.row)">Details</el-button>
          </template>
        </el-table-column>
      </el-table>
      <el-pagination
        v-model:current-page="params.page"
        v-model:page-size="params.pageSize"
        class="pg-block"
        layout="total, sizes, prev, pager, next, jumper"
        :total="pageTotal"
        background
        small
        @current-change="handleCurrentChange"
        @size-change="handleSizeChange"
      />
    </el-card>
  </div>
</template>

Some of the styles involved above are also common. They control margin and padding. There is nothing special. It depends on the project.

2. Subcomponent–script

<script setup>
import { ref, reactive, defineProps, computed, defineEmits, onMounted} from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import SearchOptionsBox from '@/components/searchOptionsBox.vue' //Search component

const emit = defineEmits(['openDialog']) //This is a pop-up window to view details
const props = defineProps({
  listName: { //The name of the list
    default: '',
    type: String
  },
  tableColumn: { // Columns in the table
    default: [],
    type: Array
  },
  url: { //URL to request data
    default: '',
    type: String
  },
  pageName: { //When used on multiple pages, some pages will have special requirements for differentiation.
    default: '',
    type: String
  }
})

let pageTotal = ref(0)
let params = reactive({
  page: 1,
  pageSize: 20
})
//Search component options, personally defined
let searchOptions = ref([])
let dataReady = ref(false)
let btnLoading = ref(false)


let tableData = ref([])
let loading = ref(true)

onMounted(() => {
 
  getOperationRecordData()
  
  //...
})
//download controls export and is defined by yourself
const getOperationRecordData = async (download) => {
  let tempParams = { ...params, ...searchParams }
  if (!download) {
    download = 0
    loading.value = true
  } else {
    btnLoading.value = true
  }

  let res = (
    await axios.get(props.url, {
      params: {
        ...tempParams,
        download
      }
    })
  ).data
  if (res.code == 200) {
    if (download) {
      ElMessage.success(res.message || 'Success!')
      btnLoading.value = false
      return
    }
    tableData.value = res.data
    params.page = res.page
    params.pageSize = res.pageSize
    pageTotal.value = res.total
    loading.value = false
  } else {
    ElMessage.error(res.message || 'Failed to obtain!')
    loading.value = false
    btnLoading.value = false
  }
}


//Process the data of the search component

let searchParams = reactive({})
const searchData = (searchForm, download) => {
  params.page = 1
  searchParams = JSON.parse(JSON.stringify(searchForm))
 
  if (!download) {
    download = 0
  }
  getOperationRecordData(download)
}
//The following is paging and sorting
const handleSizeChange = (val) => {
  params.page = 1
  params.pageSize = val
  getOperationRecordData()
}
const handleCurrentChange = (val) => {
  params.page = val
  getOperationRecordData()
}

//serial number
const getIndex = (index) => {
  return (params.page - 1) * params.pageSize + index + 1
}

//Set the default sort field and positive and negative order
const changeTableSort = (column) => {
  params.sort_key = 'create_at'
  params.sort_val = 'asc'
  if (column.order == 'ascending') {
    params.sort_val = 'asc'
  } else if (column.order == 'descending') {
    params.sort_val = 'desc'
  } else {
    delete params.sort_key
    delete params.sort_val
  }
  getOperationRecordData()
}

//The pop-up window to view details opens
const seeDetail = (row) => {
  emit('openDialog', row)
}

3. Parent component – tableColumn is a column of the table, and the specific format can be defined according to requirements.

<template>
  <RecordList
    list-name="System log"
    :table-column="tableColumn"
    url="/log-list"
    page-name="operationRecord"
    @openDialog="openDialog"
  />

  <!-- Operation details pop-up window!-->
  <el-dialog v-model="seeDialogVisible" title="Operation details" width="40%" :close-on-click-modal="false" top="10vh">
    <el-descriptions v-if="ready" size="large" column="1" style="margin-left: 20px">
      <el-descriptions-item v-for="(item, index) in tableColumn" :key="index" :label="`${item.label} :`">
        {<!-- -->{ recordInfo[item.prop] }}
      </el-descriptions-item>
    </el-descriptions>
    <template #footer>
      <el-button @click="seeDialogVisible = false">Cancel</el-button>
    </template>
  </el-dialog>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import RecordList from '@/components/recordList.vue'

let tableColumn = ref([
  {
    prop: 'user_user',
    label: 'operator',
    minWidth: '140',
    sortable: false,
    showOverflowTooltip: false
  },
  {
    prop: 'create_at',
    label: 'operation time',
    minWidth: '160',
    sortable: true,
    showOverflowTooltip: false
  },
  {
    prop: 'content_type',
    label: 'operation type',
    minWidth: '140',
    sortable: false,
    showOverflowTooltip: false
  },
  {
    prop: 'content',
    label: 'operation content',
    minWidth: '330',
    sortable: false,
    showOverflowTooltip: true
  },
  {
    prop: 'user_role',
    label: 'role',
    minWidth: '150',
    sortable: false,
    showOverflowTooltip: false
  },
  {
    prop: 'operation_ip',
    label: 'IP address',
    minWidth: '120',
    sortable: false,
    showOverflowTooltip: false
  }
])

const seeDialogVisible = ref(false)
const ready = ref(false)

const recordInfo = ref({})
const openDialog = (row) => {
  recordInfo.value = row
  ready.value = true
  seeDialogVisible.value = true
}
</script>

<style lang="scss" scoped>
:deep(.el-dialog__body) {
  padding-bottom: 0px !important;
  height: 450px;
  overflow-y: auto;
}
.el-descriptions--large {
  height: 420px;
  overflow-y: auto;
  :deep(.el-descriptions__cell) {
    display: flex;
    flex-direction: row;
    justify-content: flex-start;
  }
  :deep(.el-descriptions__label) {
    font-weight: bold !important;
    width: auto;
  }
  :deep(.el-descriptions__content) {
    width: 70%;
    display: flex;
    flex-direction: row;
  }
}
</style>

4. There are many ways to export the function in sub-components. If the backend returns a base64, it can be processed directly, as follows:

 let res = (await axios.post(props.apiUrl, tempParams)).data
  if (res.code == 200) {
    if (download) {
      let b64 = res.data.res
      let a = document.createElement('a')
      a.href = 'data:application/vnd.ms-excel;base64,' + b64
      a.download = `filename.xlsx`
      a.click()
      ElMessage.success(res.message || 'Success!')
      changeButtonLoading(false) //Update some data or status of the page in the parent component
      updateParams() //Update some data or status of the page in the parent component
      return
    }
    tableData.value = res.data.res
    columns.value = res.data.field_list
    params.page = res.page
    params.pageSize = res.pageSize
    pageTotal.value = res.total

    loading.value = false
    updateParams()
  } else {
    ElMessage.error(res.message || 'Failed to obtain table data!')
    loading.value = false
    changeButtonLoading(false) //Update some data or status of the page in the parent component
    updateParams() //Update some data or status of the page in the parent component
  }

Summary:

Although it is simple business logic, it is more convenient to encapsulate it. Here is the way to implement the business requirements of recording this common type of list page.

There is also an implementation of filterable table columns.

syntaxbug.com © 2021 All Rights Reserved.