1. 需求背景
在管理后台中,有很多的列表页,当多人开发时,可能会造成列表页的整体样式有些偏差,为了列表页的统一,以及效率的提升,将列表拆分,封装组件,开发者只需关心数据即可。
一般的列表页都长下边这样。按照绿色框进行划分,划分为【搜索区】和【列表区】,其中【列表区】一般包含【按钮区】、【表格区】、【分页区】
2.【搜索区】-> TYSearch.vue,【源码】
集合了常见用户交互组件:
el-inputel-selectel-date-pickerel-checkbox-groupel-radio-groupel-cascader
<div class="ty-search-item" v-for="(item, idx) in data" :key="idx">
<span class="ty-search-label">{{ item.label }}:</span>
<!-- input -->
<el-input
v-if="item.type === 'input'"
v-model="searchQuery[item.param]"
clearable
v-bind="item.config"
/>
<!-- input number -->
<el-input-number
v-if="item.type === 'inputnumber'"
v-model="searchQuery[item.param]"
clearable
v-bind="item.config"
/>
<!-- select -->
<el-select
v-if="item.type === 'select'"
v-model="searchQuery[item.param]"
clearable
v-bind="item.config"
>
<el-option
v-for="option in item.options"
:key="option.value"
:label="option.label"
:value="option.value"
></el-option>
</select>
<!-- cascader -->
<el-cascader
v-if="item.type === 'cascader'"
v-model="searchQuery[item.param]"
clearable
v-bind="item.config"
/>
<!-- date -->
<el-date-picker
v-if="dateTypes.includes(item.type)"
v-model="searchQuery[item.param]"
:type="item.type"
v-bind="item.config"
/>
<!-- radio -->
<el-radio-group
v-if="item.type === 'radio'"
v-model="searchQuery[item.param]"
v-bind="item.config"
>
<el-radio
v-for="(option, idx) in item.options"
:key="idx"
:label="option.value"
>
{{ option.label }}
</el-radio>
</el-radio-group>
<!-- checkbox -->
<el-checkbox-group
v-if="item.type === 'checkbox'"
v-model="searchQuery[item.param]"
v-bind="item.config"
>
<el-checkbox
v-for="(option, idx) in item.options"
:key="idx"
:label="option.value"
>
{{ option.label }}
</el-checkbox>
</el-checkbox-group>
<!-- 按钮区 -->
<div class="ty-search-item">
<el-button type="primary" @click="search">查询</el-button>
<el-button type="primary" plain @click="reset">重置</el-button>
<el-button v-if="showExport" @click="exportData">导出</el-button>
</div>
</div>
自此,一个基本的【搜索区域】组件基本就完成了
2.1 TYSearch props
- 定义接口,限制props的类型
interface SearchData {
data: SearchItem[] // 搜索项
showExport: boolean // 导出
}
- 其中,SearchItem为每一个搜索项的参数,具体配置如下
interface SearchItem {
label: string // 搜索项名称
type: string // 搜索项类型,input,select, date, daterange...
defaultValue?: any // 默认value值
param: string // v-model绑定的名称
config?: object // element-plus的attribute,默认所有的搜索都可以clearable,当不需要clearable时,传false
options?: any[] // 下拉选项,type为select,checkbox,radio时必传
// 事件
events?: {
[key: string]: any
}
// 下拉默认属性
props?: {
label: string
value: string
}
}
- 为props设置默认值
const props = withDefaults(defineProps<SearchData>(), {
showExport: false
})
2.2 初始化搜索项绑定值
- 定义一个响应式变量,在
onMounted时,设置默认值
const searchQuery = reactive({})
function getObjectOwnKey(targetObj: object, key: string) {
return Object.prototype.hasOwnProperty.call(targetObj, key)
}
onMounted(() => {
props.data.forEach((item) => {
searchQuery[item.param] = getObjectOwnKey(item, "defaultValue") ? item.defaultValue : ""
})
})
- 当搜索项类型type为
select,radio,checkbox时,则必须传入选择项,所以做个校验
props.data.forEach((item) => {
searchQuery[item.param] = getObjectOwnKey(item, "defaultValue") ? item.defaultValue : ""
try {
if (
item.type === "select" ||
item.type === "radio" ||
item.type === "checkbox"
) {
// select、radio、checkbox时options为必传
if (!getObjectOwnKey(item, "options")) {
throw new Error(item.type + "必须要有options")
}
}
} catch (e) {
console.error(e)
}
})
2.3 select,radio,checkbox选择项的可配置性
-
默认使用的字段为:名称 - label,值 - value,也可自定义,通过
SearchItem.props传入 -
修改相应代码
TYSearch.vue
<!-- select -->
<el-select
v-if="item.type === 'select'"
v-model="searchQuery[item.param]"
clearable
v-bind="item.config"
>
<el-option
v-for="option in item.options"
:key="item.props ? option[item.props.value] : option.value"
:label="item.props ? option[item.props.label] : option.label"
:value="item.props ? option[item.props.value] : option.value"
></el-option>
</el-select>
<!-- radio -->
<el-radio-group
v-if="item.type === 'radio'"
v-model="searchQuery[item.param]"
v-bind="item.config"
>
<el-radio
v-for="(option, idx) in item.options"
:key="idx"
:label="item.props ? option[item.props.value] : option.value"
>
{{ item.props ? option[item.props.label] : option.label }}
</el-radio>
</el-radio-group>
<!-- checkbox -->
<el-checkbox-group
v-if="item.type === 'checkbox'"
v-model="searchQuery[item.param]"
v-bind="item.config"
>
<el-checkbox
v-for="(option, idx) in item.options"
:key="idx"
:label="item.props ? option[item.props.value] : option.value"
>
{{ item.props ? option[item.props.label] : option.label }}
</el-checkbox>
</el-checkbox-group>
- 使用
<TYSearch
:data="data"
/>
<script setup lang="ts">
const data = [
{
props: {
label: 'key',
value: 'val'
}
}
]
</script>
2.4 搜索项事件
- 当需要一些事件处理时,需要在
SearchItem.events中进行配置 - 修改相应代码
TYSearch.vue
因为
el-input-number的change事件有两个参数,所以单独写了一份
<el-input-number
v-if="item.type === 'inputnumber'"
v-model="searchQuery[item.param]"
clearable
v-bind="item.config"
@blur="componentEvent(item, 'blur', $event)"
@focus="componentEvent(item, 'focus', $event)"
@change="(v1: number | undefined, v2: number | undefined) => inputNumberChange(v1, v2, item)"
/>
<script setup lang="ts">
// 组件事件 - 通用
function componentEvent(item: SearchItem, eventType: string, e?: any) {
if (!item.events) return
if (getObjectOwnKey(item.events, eventType)) {
item.events[eventType](searchQuery[item.param], e)
}
}
// el-input-number change事件
function inputNumberChange(
newValue: number | undefined,
oldValue: number | undefined,
item: SearchItem
) {
if (!item.events) return
if (getObjectOwnKey(item.events, "change")) {
item.events["change"](newValue, oldValue)
}
}
</script>
- 使用
<TYSearch
:data="data"
/>
<script setup lang="ts">
const data = [
{
events: {
change: nameChange
}
}
]
function nameChange(val) {
// TODO
}
</script>
2.5 TYSearch Emits
- 包含【查询】和【导出】
const emits = defineEmits(["search", "export"])
2.6 TYSearch Exposes
- 修改搜索项的默认值,多用于不同搜索项联动,修改了一个搜索项的值,改变另一个的初始值
defineExpose({
changeParam
})
function changeParam(key: string, val: any) {
if (getObjectOwnKey(searchQuery, key)) {
searchQuery[key] = val
}
}
2.7 完整使用示例
<TYSearch
ref="tySearchRef"
:data="searchQuery"
show-export
@search="search"
@export="exportData"
/>
<script setup lang="ts">
import TYSearch from 'your/component/url'
// 因为下拉框的options有接口请求的内容,所以这里使用computed来返回搜索项
const searchQuery = computed<SearchItem[]>(() => {
return [
{
type: "input",
label: "预约单号",
param: "orderNo",
defaultValue: null,
config: {
clearable: false,
placeholder: "请输入预约单号",
maxlength: "20",
"show-word-limit": true
}
},
{
type: "cascader",
label: "区镇",
param: "collectionPointIds",
defaultValue: [],
config: {
options: regionList.value,
filterable: true,
props: { multiple: true },
"collapse-tags": true,
"collapse-tags-tooltip": true,
"show-all-levels": false,
placeholder: "请选择区镇"
}
},
{
type: "select",
label: "状态",
param: "orderStatus",
defaultValue: null,
options: appointmentOrderStatusEnum.getList(),
config: {
placeholder: "请选择状态"
},
events: {
change: changeStatus
},
props: {
label: "value",
value: "key"
}
},
{
type: "daterange",
label: "清运完成时间",
param: "actualCleanTime",
defaultValue: null,
config: {
"start-placeholder": "开始日期",
"end-placeholder": "结束日期",
"value-format": "YYYY-MM-DD"
}
}
]
})
const searchData = reactive({
orderNo: null as any,
collectionPointIds: [] as any[],
actualCleanTime: null as any,
orderStatus: null as any,
})
// 查询
function search(querys?: any) {
// TODO
}
// 导出
function exportData() {
// TODO
}
const tySearchRef = ref()
function changeStatus(val: string) {
tySearchRef.value.changeParam("orderNo", val)
}
</script>
3.【列表】-> TYTable.vue,【源码】
包含了列表顶部的操作按钮,表格数据,分页器内容
<main class="ty-list">
<!-- 列表 -->
<div class="ty-table-wrap">
<el-table
ref="elTableRef"
:data="tableData"
v-bind="tableConfig"
height="100%"
v-loading="loading"
>
<el-table-column
v-for="(item, idx) in columns"
:key="idx"
v-bind="item"
show-overflow-tooltip
>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="ty-page-wrap">
<el-pagination
v-model:current-page="page.pageBean.pageNum"
v-model:page-size="page.pageBean.pageSize"
:page-sizes="pageSizes"
background
layout="total, prev, pager, next, sizes, jumper"
:total="page.total"
@size-change="handleSizeChange"
@current-change="getTableList"
/>
</div>
</main>
3.1 TYTable props
- 定义接口,限制props的类型
interface ColumnItem {
[key: string]: any
}
interface PropsType {
action: any // 接口封装函数
searchData?: object // 搜索项
showCheckbox?: boolean // 是否表格多选
showIndex?: boolean // 是否有序号列
columns: ColumnItem[] // 表格列
tableConfig?: object // el-table的attribute
defalutPageSize?: number // 默认的每页数量
pageSizes?: number[] // 页码Enum
onSuccess?: (res: any) => void
onError?: (err: any) => void
responseProps?: {
list: string
total: string
} // api请求默认赋值名称
pageProps?: {
pageNum: string
pageSize: string
} // 分页器参数
}
- 为props设置默认值
const props = withDefaults(defineProps<PropsType>(), {
defalutPageSize: 10,
pageSizes: () => [10, 20, 50, 100],
showCheckbox: false,
showIndex: false,
responseProps: () => {
return {
list: "items",
total: "total"
}
},
pageProps: () => {
return {
pageNum: "pageNum",
pageSize: "pageSize"
}
}
})
3.2 多选表格或有序号列表格
- 在
props传入showCheckbox或showIndex - 修改
TYTable.vue代码,添加el-table-column列
<el-table-column
v-if="showCheckbox"
type="selection"
fixed="left"
width="50px"
></el-table-column>
<el-table-column
v-if="showIndex"
label="序号"
type="index"
width="55px"
></el-table-column>
3.3 TYTable Slots
- 修改
TYTable.vue代码
<el-table>
<template #empty>
<slot name="empty"></slot>
</template>
</el-table>
- 使用
<TYTable>
<template #empty>
<span>没有数据,请重试</span>
</template>
</TYTable>
3.4 TYTable Column Slots
- 修改
TYTable.vue代码
<el-table-column
v-for="(item, idx) in columns"
:key="idx"
v-bind="item"
show-overflow-tooltip
>
<template v-if="item.slot" #default="scope">
<slot :name="item.prop" :row="scope.row"></slot>
</template>
</el-table-column>
- 使用时需要在
props.columns中需要插槽的内容添加slot: true属性
<TYTable :columns="columns">
<template #operate="{ row }">
<el-button link @click="edit(row.id)">编辑</el-button>
</template>
</TYTable>
<script setup lang="ts">
const columns = [
{
slot: true,
label: "操作",
prop: "operate",
fixed: "right",
width: "200px"
}
]
</script>
3.5 TYTable Events
- 修改
TYTable.vue代码,这里只实现了以下5个事件
<el-table
@row-click="elRowClick"
@select="elSelect"
@select-all="elSelectAll"
@selection-change="elSelectionChange"
@cell-click="elCellClick"
>
<script setup lang="ts">
/**
* emits
*/
const emits = defineEmits([
"rowClick",
"select",
"selectAll",
"selectionChange",
"cellClick"
])
// element-plus的表格事件
function elRowClick(row: any, column: any, event: any) {
emits("rowClick", row, column, event)
}
function elSelect(selection: any, row: any) {
emits("select", selection, row)
}
function elSelectAll(selection: any) {
emits("selectAll", selection)
}
function elSelectionChange(selection: any) {
emits("selectionChange", selection)
}
function elCellClick(row: any, column: any, cell: any, event: any) {
emits("cellClick", row, column, cell, event)
}
</script>
3.6 TYTable Exposes
- 修改
TYTable.vue代码,这里只实现了以下5个方法
<script setup lang="ts">
/**
* expose
*/
defineExpose({
getList: handleSizeChange, // 获取列表数据方法
clearSelection,
getSelectionRows,
toggleRowSelection,
toggleAllSelection
})
// element-plus方法
function clearSelection() {
elTableRef.value.clearSelection()
}
function getSelectionRows() {
elTableRef.value.getSelectionRows()
}
function toggleRowSelection(row: any, selected?: boolean) {
elTableRef.value.toggleRowSelection(row, selected)
}
function toggleAllSelection() {
elTableRef.value.toggleAllSelection()
}
</script>
3.7 表格顶部操作按钮插槽
- 修改
TYTable.vue代码,在el-table前添加一个插槽
<main class="ty-list">
<!-- 按钮 -->
<slot name="header"></slot>
<!-- 列表 -->
<div class="ty-table-wrap">
<el-table></el-table>
</div>
</main>
- 使用
<TYTable>
<template #header>
<el-button type="primary" @click="addUser">添加用户</el-button>
</template>
</TYTable>
4. 全局的css样式
在引用TYSearch.vue和TYTable.vue的外层组件上添加该css类名,即可实现一个自动计算表格高度,撑满整个浏览器的列表页
.ty-list-container {
height: 100%;
display: flex;
flex-direction: column;
}
完整使用如下:
<template>
<main class="page-container ty-list-container">
<TYSearch />
<TYTable />
</main>
</template>
5. api文档
6. 更新日志,代码见github
| 日期 | 更新内容 |
|---|---|
| 2023-05-25 | 1. TYSearch新增el-radio-button和el-checkbox-button 2. TYSearch搜索项type为select,checkbox,radio时,增加每一项属性的配置 |