目录
背景描述:
开发流程:
详细开发:
总结:
背景描述:
web很多时候,要开发一个列表页,展示大量数据,并且提供一些交互功能,例如排序和分页,导出功能,有时候为了避免麻烦也会封装组件。下面使用 Vue 3 和 Element-Plus 来开发一个列表页。
这里,以一个系统日志的列表页为例。
开发流程:
创建一个名为
recordList.vue
的组件,该组件将用于展示系统日志。
一般这种页面都由三部分组成:
1.页面名称
2.搜索框
3.表格和分页
前两部分,往往和其他页面都是固定的样式,复用样式就行,表格和分页的样式也差不多一样,所以整个页面,大部分都是简单的,封装也比较好封装。
如图:
详细开发:
1.子组件——template(搜索组件是我自己封装的一套)
<template>
<div class="dashboard-container">
<el-card class="card-style">
<div class="mt-1">
<h2 class="fwb-mb-1">{{ listName }}</h2>
<el-row>
<!---搜索组件--->
<SearchBox
v-if="dataReady"
:search-options="searchOptions"
:showButton="true"
:btnLoading="btnLoading"
button-name="导出"
@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="序号">
<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="操作" min-width="80" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" @click="seeDetail(scope.row)">详情</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>
上面涉及的一些样式也都是常见的,控制margin和padding的,没有什么特殊的,根据项目来。
2.子组件——script
<script setup>
import { ref, reactive, defineProps, computed, defineEmits, onMounted} from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import SearchOptionsBox from '@/components/searchOptionsBox.vue' //搜索组件
const emit = defineEmits(['openDialog']) //这是查看详情的弹窗
const props = defineProps({
listName: { //列表的名称
default: '',
type: String
},
tableColumn: { // 表格里的列
default: [],
type: Array
},
url: { //请求数据的url
default: '',
type: String
},
pageName: { //用在多个页面时,某些页面会有些特殊要求,用于区分的
default: '',
type: String
}
})
let pageTotal = ref(0)
let params = reactive({
page: 1,
pageSize: 20
})
//搜索组件的选项,个人定义的
let searchOptions = ref([])
let dataReady = ref(false)
let btnLoading = ref(false)
let tableData = ref([])
let loading = ref(true)
onMounted(() => {
getOperationRecordData()
//...
})
//download 是控制导出的,自己定义的
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 || '成功!')
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 || '获取失败!')
loading.value = false
btnLoading.value = false
}
}
//处理搜索组件的数据
let searchParams = reactive({})
const searchData = (searchForm, download) => {
params.page = 1
searchParams = JSON.parse(JSON.stringify(searchForm))
if (!download) {
download = 0
}
getOperationRecordData(download)
}
//下面是分页和排序
const handleSizeChange = (val) => {
params.page = 1
params.pageSize = val
getOperationRecordData()
}
const handleCurrentChange = (val) => {
params.page = val
getOperationRecordData()
}
//序号
const getIndex = (index) => {
return (params.page - 1) * params.pageSize + index + 1
}
// 设置默认的排序字段和正序倒序
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()
}
//查看详情的弹窗打开
const seeDetail = (row) => {
emit('openDialog', row)
}
3.父组件——tableColumn是表格的列,具体格式可以根据需求定义。
<template>
<RecordList
list-name="系统日志"
:table-column="tableColumn"
url="/log-list"
page-name="operationRecord"
@openDialog="openDialog"
/>
<!-- 操作详情弹窗!-->
<el-dialog v-model="seeDialogVisible" title="操作详情" 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">取消</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: '操作人',
minWidth: '140',
sortable: false,
showOverflowTooltip: false
},
{
prop: 'create_at',
label: '操作时间',
minWidth: '160',
sortable: true,
showOverflowTooltip: false
},
{
prop: 'content_type',
label: '操作类型',
minWidth: '140',
sortable: false,
showOverflowTooltip: false
},
{
prop: 'content',
label: '操作内容',
minWidth: '330',
sortable: false,
showOverflowTooltip: true
},
{
prop: 'user_role',
label: '角色',
minWidth: '150',
sortable: false,
showOverflowTooltip: false
},
{
prop: 'operation_ip',
label: 'IP地址',
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.子组件里的导出功能,有很多种方式,如果后端返回一段base64,那么就可以直接处理,如下面这样:
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 = `文件名.xlsx`
a.click()
ElMessage.success(res.message || '成功!')
changeButtonLoading(false) //更新父组件里页面的一些数据或者状态
updateParams() //更新父组件里页面的一些数据或者状态
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 || '获取表格数据失败!')
loading.value = false
changeButtonLoading(false) //更新父组件里页面的一些数据或者状态
updateParams() //更新父组件里页面的一些数据或者状态
}
总结:
虽然是简单的业务逻辑,但是封装起来使用更方便,这里是记录这种常见类型的列表页的业务需求实现方式。
还有一篇实现,可以筛选的表格列 的实现方式。