项目开始
当我们拿到设计稿或者原型图时,看到如下图展示的页面。我们就要想如何能够减少工作量,做出可复用的组件。
既然每个页面都长得差不多,那我们观察可以发现,这个页面分成四个部分,
【搜索部分、新增部分、表格展示部分、分页部分】
由于本人使用的vue3+ts+element-plus+sass 开发,以下是我写的代码。
搜索部分
在我们的管理后台中,常见搜索包含【输入框,下拉列表,时间选择】;当页面存在0个或者多个搜索框时,一般采用列表或者对象来传递数据。那如何定义每一个数据的数据格式,以下是我使用TS定义的属性接口
export interface searchItemInterface {
key: string //这个很关键,这个是搜索框的字段
type: string //输入框的类型
modelValue?: string | number | undefined | boolean | string[] | Date[] //输入框的值类型
timeFormat?: string //日期输入框显示的格式
placeholder?: string //输入框的提示
clearable?: boolean //是否显示删除icon
name?: string //输入框前面的字段名
width?: number //输入框的宽度
change?: (arg?: any) => void //监听输入框变化函数
options?: any[] //选择下拉框的下拉列表
}
搜索vue模板
首先我们要在components文件夹中,创建 search 文件夹,并创建,Index.vue 和index.ts 两个文件
Index.vue 文件
<template>
<div class="search-context">
<div class="search-box">
<div class="search-block" v-for="(item, index) in data.list" :key="index">
<div class="search-title" v-if="item.name !== ''">{{ item.name }}</div>
<div class="search-content">
<!-- 数字或文本输入框 -->
<template v-if="item.type === 'text' || item.type === 'number'">
<el-input
class="search-input"
:style="{ width: item.width + 'px' }"
:type="item.type"
v-model="item.modelValue"
:placeholder="item.placeholder ? item.placeholder : '请输入' + item.name"
:clearable="item.clearable"
@input="item.change"
/>
</template>
<!-- 下拉框 -->
<template v-else-if="item.type === 'select'">
<el-select
:style="{ width: item.width + 'px' }"
v-model="item.modelValue"
:placeholder="item.placeholder ? item.placeholder : '请选择' + item.name"
:clearable="item.clearable"
@change="item.change"
>
<el-option
v-for="itemOption in item.options"
:key="itemOption.value ? itemOption.value : itemOption.id"
:label="itemOption.label ? itemOption.label : item.name"
:value="itemOption.value ? itemOption.value : itemOption.id"
/>
</el-select>
</template>
<!-- 下拉框 多选框 -->
<template v-else-if="item.type === 'select-multiple'">
<el-select
:style="{ width: item.width + 'px' }"
v-model="item.modelValue"
:placeholder="item.placeholder ? item.placeholder : '请选择' + item.name"
:clearable="item.clearable"
multiple
collapse-tags
@change="item.change"
>
<el-option
v-for="itemOption in item.options"
:key="itemOption.value ? itemOption.value : itemOption.id"
:label="itemOption.label ? itemOption.label : item.name"
:value="itemOption.value ? itemOption.value : itemOption.id"
/>
</el-select>
</template>
<!-- 时间选择器 -->
<template v-else-if="item.type === 'date'">
<el-config-provider :locale="locale">
<el-date-picker
:style="{ width: item.width + 'px' }"
v-model="item.modelValue"
type="date"
format="YYYY-MM-DD"
:value-format="item.timeFormat"
:placeholder="item.placeholder ? item.placeholder : '请选择' + item.name"
@change="item.change"
/>
</el-config-provider>
</template>
<!-- 时间段选择器 -->
<template v-else-if="item.type === 'daterange'">
<el-config-provider :locale="locale">
<el-date-picker
style="width: 380px"
v-model="item.modelValue"
type="daterange"
range-separator="到"
start-placeholder="开始时间"
end-placeholder="结束时间"
:value-format="item.timeFormat"
:locale="locale"
@change="item.change"
:disabled-date="limitDate"
/>
</el-config-provider>
</template>
<!-- 时间段选择器 精确到秒 -->
<template v-else-if="item.type === 'datetimerange'">
<el-config-provider :locale="locale">
<el-date-picker
style="width: 380px"
v-model="item.modelValue"
type="datetimerange"
range-separator="到"
start-placeholder="开始时间"
end-placeholder="结束时间"
format="YYYY-MM-DD HH:mm:ss"
date-format="YYYY/MM/DD ddd"
:locale="locale"
@change="item.change"
:shortcuts="shortcuts"
:disabled-date="limitDate"
:default-time="defaultTime"
/>
</el-config-provider>
</template>
<!-- 月份选择器 -->
<template v-else-if="item.type === 'month'">
<el-date-picker
:style="{ width: item.width + 'px' }"
v-model="item.modelValue"
value-format="YYYY-MM"
type="month"
:placeholder="item.placeholder ? item.placeholder : '请选择' + item.name"
/>
</template>
<!-- 开关选择器 -->
<template v-else-if="item.type === 'switch'">
<el-switch v-model="item.modelValue" />
</template>
<!-- slot -->
<template v-else-if="item.type === 'slot'">
<div :style="{ width: item.width + 'px' }"><slot name="custom"></slot></div>
</template>
</div>
</div>
<div class="search-block btn-box">
<div class="btn">
<el-button @click="onSearch" type="primary">
<!-- <el-icon class="el-icon--left"><Search /></el-icon> -->
搜索
</el-button>
<el-button @click="onReset">
<!-- <el-icon class="el-icon--left"><Refresh /></el-icon> -->
清空
</el-button>
<slot name="btn"></slot>
</div>
<div class="add">
<slot name="add"></slot>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import { searchItemInterface, SearchData } from './index'
import { formatTime } from '@/utils'
/**
* 引入element-plus中文包
*/
const locale = ref(zhCn)
/**
* 注册父组件回调方法
*/
const emits = defineEmits(['search', 'reset'])
/**
* 接受来自父组件的传参
*/
const props = defineProps({
list: {
type: Array<searchItemInterface | string | string[]>,
default: () => {
return []
}
},
resetKeys: {
type: Array<searchItemInterface | string>,
default: () => {
return []
}
},
permission: {
type: Array<string>,
default: () => {
return []
}
}
})
/**
* 初始化数据
*/
const data = reactive(new SearchData(props.list))
const defaultTime: [Date, Date] = [new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)] // '00:00:00', '23:59:59'
const shortcuts = [
{
text: '今天',
value: () => {
const start = new Date()
return [
new Date(start.getFullYear(), start.getMonth(), start.getDate(), 0, 0, 0),
new Date(start.getFullYear(), start.getMonth(), start.getDate(), 23, 59, 59)
]
}
},
{
text: '昨天',
value: () => {
const start = new Date()
start.setDate(start.getDate() - 1)
return [
new Date(start.getFullYear(), start.getMonth(), start.getDate(), 0, 0, 0),
new Date(start.getFullYear(), start.getMonth(), start.getDate(), 23, 59, 59)
]
}
},
{
text: '上一周',
value: () => {
const end = new Date()
const start = new Date()
start.setDate(start.getDate() - 7)
return [
new Date(start.getFullYear(), start.getMonth(), start.getDate(), 0, 0, 0),
new Date(end.getFullYear(), end.getMonth(), end.getDate(), 23, 59, 59)
]
}
},
{
text: '上个月',
value: () => {
const end = new Date()
const start = new Date()
start.setMonth(start.getMonth() - 1)
return [
new Date(start.getFullYear(), start.getMonth(), start.getDate(), 0, 0, 0),
new Date(end.getFullYear(), end.getMonth(), end.getDate(), 23, 59, 59)
]
}
},
{
text: '最近三个月',
value: () => {
const end = new Date()
const start = new Date()
start.setMonth(start.getMonth() - 3)
return [
new Date(start.getFullYear(), start.getMonth(), start.getDate(), 0, 0, 0),
new Date(end.getFullYear(), end.getMonth(), end.getDate(), 23, 59, 59)
]
}
}
]
/**
* 监听点击搜索按钮时
*/
const onSearch = () => {
/**
* 通过forEach检索出 传递的key及对应的modelValue,然后emits
*/
const obj: any = {}
data.list.forEach((item: searchItemInterface) => {
if (item.type === 'datetimerange') {
console.log(item.modelValue)
obj[item.key] =
item.modelValue !== '' && item.modelValue !== null
? [formatTime((item.modelValue as Date[])[0]), formatTime((item.modelValue as Date[])[1])]
: []
} else {
obj[item.key] = item.modelValue
}
})
console.log('search', obj)
emits('search', obj)
}
/**
* 点击重置按钮时,设置所有的modelValue='',然后emits
*/
const onReset = () => {
data.list.forEach((item: searchItemInterface) => {
item.modelValue = ''
})
emits('reset')
}
//限制只能选择今天之前(包括今天)
const limitDate = (time: Date) => {
return time.getTime() > Date.now()
}
// //限制只能选择今天之前(不包括今天)
// const limitDate = (time: Date) => {
// return time.getTime() > Date.now() - 8.64e7
// }
</script>
<style lang="scss" scoped>
.search-context {
background-color: #fff;
box-sizing: border-box;
padding: 0;
display: flex;
}
.search-box {
display: flex;
flex-wrap: wrap;
.search-block {
display: flex;
align-items: center;
margin-bottom: 16px;
.search-title {
font-size: 14px;
margin-right: 8px;
min-width: 56px;
text-align: right;
}
.search-content {
margin: 0 16px 0 0px;
}
}
}
.btn-box {
flex-grow: 0;
flex-shrink: 0;
flex-basis: 132px;
// margin-left: 64px;
}
</style>
index.ts
import { DICT_DATA } from '@/utils/dict-data'
/**
* 定义搜索每项的接口内容
*/
export interface searchItemInterface {
key: string
type: string
modelValue?: string | number | undefined | boolean | string[] | Date[]
timeFormat?: string
placeholder?: string
clearable?: boolean
name?: string
width?: number
change?: (arg?: any) => void
required?: boolean
options?: { label: string; value: string }[]
}
/**
* 定义一个基础的数据
*/
const basicValue: searchItemInterface = {
key: '',
placeholder: '',
type: 'text',
timeFormat: 'YYYY-MM-DD', //时间组件返回的格式 'YYYY-MM-DD'=》 年月日 ,'x'=》 时间戳
options: [],
clearable: true,
modelValue: '',
name: '',
width: 160,
change: (arg?: any) => {},
required: false
}
/**
* 默认存在的list
*/
const defaultList: searchItemInterface[] = [
{
key: 'slot',
type: 'slot'
},
{
key: 'status',
type: 'select',
name: '状态',
width: 160,
modelValue: '',
change: (arg?: any) => {},
options: [
{
value: '0',
label: '开启'
},
{
value: '1',
label: '关闭'
}
]
}
]
export class SearchData {
list: Array<searchItemInterface>
constructor(list: Array<searchItemInterface | string | string[]>) {
this.list = []
const arr: Array<searchItemInterface | string | string[]> = [...list]
//创建map数据结构
const map = new Map()
/**
* 然后遍历数组
*/
arr.forEach((item: searchItemInterface | string | string[]) => {
/**
* 判断item值是object还是string
*/
if (Object.prototype.toString.call(item) === '[object Object]') {
/**
* 自定义对象,用于接收 item 变量
*/
const obj = item as searchItemInterface
if (!map.has(obj.key)) {
const basicObj: searchItemInterface = JSON.parse(JSON.stringify(basicValue))
const result = Object.assign(basicObj, obj)
//先行判断字典列表中是否存在【key】
if (DICT_DATA.has(result.key)) {
result.options!.length = 0
result.options?.push(...DICT_DATA.get(result.key))
}
this.list.push(result)
map.set(obj.key, obj)
}
} else if (Object.prototype.toString.call(item) === '[object String]') {
/**
* 默认字符串
* 用默认的字符串 与 默认的 list 进行对比
* 有则添加进 this.list , 反之不做任何操作
*/
const obj = item as string
if (!map.has(obj)) {
defaultList.map(m => {
if (m.key === item) {
this.list.push(m)
}
})
map.set(obj, obj)
}
} else if (Object.prototype.toString.call(item) === '[object Array]') {
/**
* 数组
*/
const key: string[] = item as []
this.list.push({
key: 'custom',
type: 'slot',
width: 160,
name: key[0]
})
}
})
}
}
当然到此部分,还没有结束
【最重要的部分就是创建一个模板结构】
第一部分 创建基本是数据结构
创建utils文件夹中创建一个名为 basic-data.ts 的文件
import { searchItemInterface } from '@/components/search'
import { reactive, ref } from 'vue'
interface dynamicProp {
[name: string]: string
}
interface searchProp {
[name: string]: string | any[]
}
//判断操作按钮显示的条件类型
export interface visibleType {
property: string //属性
condition: string //条件 > >= == === !== != <= <
value: number | string //值
}
//操作属性类型
export interface operationType {
type?: string //类型
visible?: boolean | visibleType //
permission?: string[] //全选
className?: string //class名称
name?: string //按钮名称
}
export interface headerOperationType extends operationType {
button?: string //是否是按钮 button | radio-button
buttonType?: string //按钮颜色类型 primary success info warning danger
modelValue?: string //radio-button 默认值
radioGroup?: string[] //radio-button 选项值
}
//定义表格描述的接口
export interface DescribeInterface {
prop?: string //根据接口返回的属性定义
label?: string //根据接口返回的属性实现该属性的名字
switch?: boolean //是否是滑块按钮
operation: operationType[] //操作栏
width?: string //操作栏的宽度
children?: DescribeInterface[] //表格header字段
}
//定义提示接口
interface tooltipInterface {
visible: boolean //提示是否显示 默认false
content: string //提示内容
}
//定义扩展数据接口
export interface expandInterface {
key: string
name: string
value: any
}
//定义switch类型接口
export interface SwitchInterface {
disabled: boolean //滑块是否禁用 默认flase
permission: string[] //滑块权限
tooltip: tooltipInterface //滑块鼠标移上去提示内容
key: string
}
//表格数据接口
export interface DataInterface {
data: any[] //表格数据
fixed: string[] //左侧固定列
describe: DescribeInterface[] //表格header字段
selection: boolean //是否是可选 默认不显示
expand: boolean //是否是展开 默认不显示
index: boolean //是否显示序号 默认不显示
showSummary: boolean //是否显示底部统计栏 默认不显示
summaryArray: any[] //统计栏数据
summaryType: string //统计类型 consumeCollect | consumeDetailed | topUpCollectSuperAdmin | topUpCollect | topUpDetailed | reconciliationCollectSuperAdmin | reconciliationDetailed
sortable: any[] //设置prop栏是否可排序
isDialog: boolean //是否是弹窗 默认不是
switch: SwitchInterface //定义存在滑块操作类型
expandKey: dynamicProp //扩展数据的key及name
}
//分页属性类型
interface paginationInterface {
pageNo: number
pageSize: number
total: number
}
//pageData类型
export interface pageDataInterface {
headerOperation: headerOperationType[] // 搜索与表格之间的操作按钮 button | radio-button 两种
permission: string[] //查询表格和显示顶部搜索的权限
pagination: paginationInterface //分页
table: DataInterface //表格数据
search: Array<searchItemInterface | string | string[]> //需要搜索的属性
}
//需要实现的基本方法
export interface BaseMethod {
getData(arg?: any): void //获取数据
onSearch(arg?: any): void //点击搜索
onReset(arg?: any): void //点击重置搜索
onPaginationChange(arg?: any): void //监听分页改变
onPaginationSize(arg?: any): void //监听分页大小改变
onOperation(arg?: any): void //监听表格操作按钮
}
export class BaseData {
//整个页面的数据结构
pageData = reactive<pageDataInterface>({
headerOperation: [],
permission: [],
pagination: {
pageNo: 1,
pageSize: 15,
total: 0
},
table: {
fixed: [],
data: [],
expandKey: {},
describe: [],
summaryArray: [],
summaryType: '',
sortable: [],
showSummary: false,
selection: false,
expand: false,
index: false,
isDialog: false,
switch: {
key: '',
disabled: false,
permission: [],
tooltip: {
visible: false,
content: ''
}
}
},
search: []
})
//接收传递的表格name
propName: any
//是否有操作栏
operation: operationType[] = []
//表格操作栏的宽度
operationWidth = '120px'
//搜索与表格之间的操作按钮
headerOperation: operationType[] = reactive([])
//搜索及表格查询
permission: string[] = reactive([])
oDate = new Date()
currentYear = ref(this.oDate.getFullYear())
currentMonth = ref(this.oDate.getMonth() + 1) //取当月
currentDate = ref(this.oDate.getDate()) //取今日
/**
* 构造器
* @param propName 表格字段
* @param operation 操作类型
* @param width 操作栏宽度 120
*/
constructor(tableConfig: any) {
this.propName = tableConfig.table.propName
this.operation.length = 0
this.operation.push(...(tableConfig.table.operation || []))
this.operationWidth = tableConfig.table.operationWidth || '120px'
this.pageData.search.length = 0
this.pageData.search.push(...(tableConfig.search || []))
this.pageData.table.fixed.length = 0
this.pageData.table.fixed.push(...(tableConfig.table.fixed || []))
this.pageData.table.showSummary = tableConfig.table.showSummary || false
this.pageData.table.selection = tableConfig.table.selection || false
this.pageData.table.expand = tableConfig.table.expand || false
this.pageData.table.expandKey = tableConfig.table.expandPropName || {}
this.pageData.table.index = tableConfig.table.index || false
this.pageData.table.isDialog = tableConfig.table.isDialog || false
this.pageData.table.summaryType = tableConfig.table.summaryType || ''
this.pageData.table.summaryArray.length = 0
this.pageData.table.summaryArray.push(...(tableConfig.table.summaryArray || []))
this.pageData.table.sortable.length = 0
this.pageData.table.sortable.push(...(tableConfig.table.sortable || []))
this.pageData.table.switch.permission.length = 0
this.pageData.table.switch.permission.push(...(tableConfig.table.switch.permission || []))
this.pageData.table.switch.tooltip.visible = tableConfig.table.switch.tooltip.visible || false
this.pageData.table.switch.tooltip.content = tableConfig.table.switch.tooltip.content || ''
this.pageData.table.switch.key = tableConfig.table.switch.key || false
this.pageData.permission.length = 0
this.pageData.permission.push(...(tableConfig.permission || []))
this.pageData.headerOperation.length = 0
this.pageData.headerOperation.push(...(tableConfig.headerOperation || []))
this.createTablePropName()
}
/**
* 根据传递的 propName 构建 表格需要显示的字段名称
*/
createTablePropName() {
this.pageData.table.describe.length = 0
for (const key in this.propName) {
// console.log('key', this.propName[key], Object.prototype.toString.call(this.propName[key]))
if (Object.prototype.toString.call(this.propName[key]) === '[object Object]') {
// 多级表头
const child: DescribeInterface[] = []
if (this.propName[key].children) {
this.propName[key].children.forEach((item: any) => {
for (const k in item) {
child.push({
prop: k,
label: item[k as string],
switch: k == 'status' ? true : false,
operation: [],
width: this.setTablePropWidth(k),
children: []
})
}
})
}
this.pageData.table.describe.push({
label: this.propName[key].label,
switch: false,
operation: [],
width: '',
children: child
})
} else {
//普通的表头
this.pageData.table.describe.push({
prop: key,
label: this.propName[key],
switch: key == 'status' ? true : false,
operation: [],
width: this.setTablePropWidth(key),
children: []
})
}
}
if (this.operation.length) {
this.pageData.table.describe.push({
prop: 'operate',
label: '操作',
switch: false,
width: this.operationWidth,
operation: [...this.operation],
children: []
})
}
}
resetPagination() {
this.pageData.pagination.pageNo = 1
this.pageData.pagination.pageSize = 20
this.pageData.pagination.total = 0
}
//设置表格头prop的宽度
setTablePropWidth(key: string) {
//表头的字段
const arr: dynamicProp = {
phone: '160px',
dataSource: '250px',
lastViewedTime: '180px',
createTime: '180px',
updateTime: '180px',
referrerStr: '200px',
lastFollowMsg: '180px',
follower: '180px'
}
return arr[key] || ''
}
}
在components文件夹中创建 template文件夹,然后在template文件夹中创建Index.vue文件
Index.vue
<template>
<!-- 搜索 v-permission="props.permission"-->
<searchVue
:list="page.search"
@search="onSearch"
@reset="onReset"
ref="searchRef"
v-if="searchVisible"
v-permission="props.page.permission"
>
<!-- 在搜索后面添加的内容 -->
<template #add>
<slot name="add"></slot>
</template>
<!-- 通过输入内容获取企业id -->
<template #custom>
<searchTrainSiteNameVue
:data="searchTrainSiteName"
@change="onSelectCompanyId"
:placeholder="placeholderText"
></searchTrainSiteNameVue>
</template>
</searchVue>
<div class="table-container">
<!-- 搜索与表格之间的按钮 -->
<div :class="['slot', page.headerOperation.length ? 'insert' : '']" ref="insertRef">
<template v-for="item in page.headerOperation" :key="item.type">
<section
v-if="item.button === 'button'"
:class="['button', 'border', item.buttonType]"
@click="emits('operation', { type: item.type, data: null })"
v-permission="item.permission"
>
{{ item.name }}
</section>
<section
v-else-if="item.button === 'file'"
:class="['button', 'border', 'file', item.buttonType]"
v-permission="item.permission"
>
<input
type="file"
@change="
(res: any) => {
console.log(res)
emits('operation', { type: item.type, data: res.target.files })
}
"
/>{{ item.name }}
</section>
<el-radio-group
v-else-if="item.button === 'radio-button'"
v-model="item.modelValue"
@change="
(res: any) => {
item.modelValue = res
emits('operation', { type: item.key, data: res })
}
"
>
<el-radio-button v-for="child in item.radioGroup" :key="child" :label="child" />
</el-radio-group>
</template>
</div>
<!-- 表格 -->
<div
:class="['table', page.headerOperation.length ? 'insert' : '', page.table.isDialog ? 'dialog' : '']"
ref="tableRef"
:style="{
maxHeight: page.headerOperation.length
? `calc(100% - ${searchHeight}px - 32px - 64px)`
: `calc(100% - ${searchHeight}px - 32px - 16px)`
}"
>
<el-table
ref="multipleTableRef"
:data="page.table.data"
style="width: 100%; height: 100%; max-height: 100%"
:border="false"
@selection-change="handleSelectionChange"
:header-cell-style="{ background: '#F5F5F5' }"
:row-style="{}"
>
<!-- 选择框 -->
<el-table-column fixed type="selection" width="40" v-if="page.table.selection"></el-table-column>
<!-- 扩展框 -->
<el-table-column fixed type="expand" width="40" v-if="page.table.expand">
<template #default="scope">
<slot name="expand">
<div class="expand-container">
<div class="line" v-for="(item, key) in page.table.expandKey" :key="item">
<p>{{ item }}:</p>
<span v-if="DICT_DATA.has(key + '')">
<template v-if="scope.row[key] !== '' && scope.row[key] !== null">
<DictVue
:data="DICT_DATA.get(key + '')"
:value="val"
v-for="(val, index) in scope.row[key].toString().split(',')"
:key="val"
:style="{
'margin-right':
index < scope.row[key].toString().split(',').length ? '8px' : 0
}"
></DictVue>
</template>
</span>
<span v-else>{{ scope.row[key] }}</span>
</div>
</div>
</slot>
</template>
</el-table-column>
<!-- 序号 -->
<el-table-column fixed type="index" width="54" v-if="page.table.index" label="序号" align="center" />
<template v-for="(item, index) in defaultProperty" :key="item">
<!-- 操作按钮 -->
<el-table-column
fixed="right"
v-if="item.operation.length"
:label="item.label"
:width="item.width"
align="center"
>
<template #header>
<div class="operation-header">
<h1>操作</h1>
<h2>
<el-popover placement="top-end" width="50%" trigger="click" @show="onPopoverShow">
<template #reference>
<el-icon><SetUp /></el-icon>
</template>
<div class="pull-down-box">
<div style="margin-bottom: 8px">
<p style="display: flex; align-items: center">
选择显示的列
<el-tooltip
class="box-item"
effect="dark"
content="长按选项可拖动,改变列的位置"
placement="top"
>
<el-icon><QuestionFilled /></el-icon>
</el-tooltip>
<el-button
style="margin-left: 8px"
size="small"
@click="onResetProperty"
>重置</el-button
>
</p>
</div>
<el-checkbox-group
v-model="selectProperty.list"
@change="handleCheckedChange"
id="checkbox-group"
>
<template v-for="p in originalProperty" :key="p.prop">
<el-checkbox :label="p.prop" v-if="p.prop !== 'operate'">{{
p.label
}}</el-checkbox>
</template>
</el-checkbox-group>
</div>
</el-popover>
</h2>
</div>
</template>
<template #default="scope">
<!-- v-permission="opera.permission" -->
<div class="operation">
<div v-for="opera in item.operation" :key="opera.type">
<span
:class="['custom-button', opera.className]"
v-if="setVisible(opera.visible as visibleType, scope.row)"
@click="emits('operation', { type: opera.type, data: scope.row })"
v-permission="opera.permission"
>{{ opera.name }}</span
>
</div>
</div>
</template>
</el-table-column>
<!-- 滑块 开关按钮 -->
<el-table-column v-else-if="item.switch" :label="item.label" align="center" width="90px">
<template #default="scope">
<template v-if="page.table.switch.tooltip.visible">
<!-- 鼠标移到开关按钮上显示有提示 -->
<el-tooltip :content="page.table.switch.tooltip.content" :disabled="!scope.row.status">
<el-switch
v-permission="page.table.switch.permission"
v-model="scope.row.status"
inline-prompt
active-text="启用"
inactive-text="禁用"
style="height: 24px"
@change="emits('operation', { type: 'switch', data: scope.row })"
/>
</el-tooltip>
</template>
<template v-else>
<el-switch
v-permission="page.table.switch.permission"
v-model="scope.row.status"
inline-prompt
active-text="启用"
inactive-text="禁用"
style="height: 24px"
@change="emits('operation', { type: 'switch', data: scope.row })"
/>
</template>
</template>
</el-table-column>
<!-- 判断是否存在某个字典 -->
<el-table-column
v-else-if="DICT_DATA.has(item.prop)"
:label="item.label"
:sortable="page.table.sortable.includes(item.prop)"
:prop="item.prop"
:show-overflow-tooltip="true"
align="center"
:width="item.width || '100px'"
min-width="100px"
>
<template #default="scope">
<template v-if="scope.row[item.prop] !== '' && scope.row[item.prop] !== null">
<DictVue
:data="DICT_DATA.get(item.prop)"
:value="val"
v-for="(val, index) in scope.row[item.prop].toString().split(',')"
:key="val"
:style="{
'margin-right':
index < scope.row[item.prop].toString().split(',').length ? '8px' : 0
}"
></DictVue>
</template>
</template>
</el-table-column>
<!-- 多级表头 -->
<el-table-column v-else-if="item.children.length" :label="item.label" align="center">
<el-table-column
:label="child.label"
v-for="(child, index) in item.children"
:prop="child.prop"
:key="child"
align="center"
>
</el-table-column>
</el-table-column>
<!-- 图片 -->
<el-table-column v-else-if="item.prop === 'imageUrl'" :label="item.label" align="center">
<template #default="scope">
<el-image
style="width: 50px; height: 40px"
:src="scope.row[item.prop]"
:zoom-rate="1.2"
:max-scale="7"
:min-scale="0.2"
:preview-src-list="[scope.row[item.prop]]"
:initial-index="4"
:preview-teleported="true"
:lazy="true"
fit="cover"
/>
</template>
</el-table-column>
<!-- 颜色 -->
<el-table-column v-else-if="item.prop === 'color'" :label="item.label" align="center" width="120">
<template #default="scope">
<div style="display: flex">
<span
:style="{
'background-color': scope.row[item.prop],
display: 'block',
width: '24px',
height: '24px',
'border-radius': '4px',
'margin-right': '8px'
}"
></span>
<span>{{ scope.row[item.prop] }}</span>
</div>
</template>
</el-table-column>
<!-- 默认情况 -->
<!-- width 根据表格的属性进行区分设置宽度 -->
<el-table-column
v-else
:prop="item.prop"
:label="item.label"
:sortable="page.table.sortable.includes(item.prop)"
:show-overflow-tooltip="true"
align="center"
:width="item.width"
min-width="100px"
:fixed="page.table.fixed.includes(item.prop)"
></el-table-column>
</template>
</el-table>
</div>
</div>
<!-- 分页 -->
<paginationVue :pagination="page.pagination" @change="onPaginationChange" @page-size="onPaginationSize" />
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watchEffect, reactive, watch } from 'vue'
import searchVue from '../search/Index.vue'
import paginationVue from '../Pagination.vue'
import DictVue from '../dict/Index.vue'
import { DICT_DATA } from '@/utils/dict-data'
import { ElTable } from 'element-plus'
import type { TableColumnCtx } from 'element-plus'
import searchTrainSiteNameVue from '@/components/searchTrainSiteName.vue'
import { visibleType, pageDataInterface } from '@/utils/basc-data'
import { useRoute } from 'vue-router'
import { storage } from '@/store/sessionStorage'
import Sortable from 'sortablejs'
const route = useRoute()
/**
* 监听table高度
*/
const tableRef = ref(0)
/**
* 监听搜索栏高度
*/
const searchRef = ref()
/**
* 设置搜索栏高度
*/
const searchHeight = ref(50)
/**
* 设置搜索栏是否显示 根据传递的list参数是否为空判断
*/
const searchVisible = ref(true)
//监听是否有插入的内容
const insertRef = ref()
const insert = ref(false)
//搜索企业id输入提示
const placeholderText = ref('')
// const issearchTrainSiteName = ref(false)
const searchTrainSiteName = reactive({
width: '100%',
name: '',
disabled: false
})
const companyId = ref('')
/**
* 监听检索选择的公司id
* @param id
*/
const onSelectCompanyId = (res: any) => {
console.log('companyId', res)
companyId.value = res.value
searchTrainSiteName.name = res.name
}
/**
* 接收父组件传递的参数
*/
const props = defineProps({
page: {
type: Object,
default: () => {
return {
headerOperation: [],
permission: [],
pagination: {
pageNo: 1,
pageSize: 20,
total: 0
},
table: {
data: [],
describe: [],
selection: false,
expand: false,
index: true,
list: [],
showSummary: false,
summaryArray: [],
summaryType: '',
sortable: [],
isDialog: false,
switch: {
disabled: false,
permission: [],
tooltip: {
visible: false,
content: ''
}
}
},
search: []
}
}
}
})
/**
* 注册父组件的回调
*/
const emits = defineEmits(['search', 'reset', 'paginationChange', 'paginationSize', 'multiSelection', 'operation'])
//选择表格默认显示的字段
const defaultProperty: any[] = reactive([])
const selectProperty: { list: string[] } = reactive({
list: []
})
//初始化表格显示的属性
const originalProperty: any[] = reactive([])
//创建map保存用户勾选显示的列
const propertyMap = new Map<string, any>()
//创建map保存表格默认排序的列
const defaultSortMap = new Map<string, any>()
//监听选择显示属性的变化
const handleCheckedChange = (list: any[]) => {
setSelectProperty(list)
propertyMap.set(route.fullPath, list)
localStorage.setItem('propertyMap', JSON.stringify(Object.fromEntries(propertyMap)))
}
//监听拖动显示的列
const onPopoverShow = () => {
const el = document.querySelector('#checkbox-group') as HTMLElement
Sortable.create(el, {
animation: 150,
ghostClass: 'blue-background-class',
onEnd: (/**Event*/ evt: any) => {
const { oldIndex, newIndex } = evt
let el = originalProperty.splice(oldIndex, 1)[0]
originalProperty.splice(newIndex, 0, el)
//根据排序重新更新表格排列顺序
const list: string[] = []
defaultProperty.length = 0
selectProperty.list.forEach(str => {})
originalProperty.forEach((item: any, index: number) => {
if (selectProperty.list.includes(item.prop)) {
list.push(item.prop)
}
})
setSelectProperty(list)
//保存返回的key
defaultSortMap.set(
route.fullPath,
originalProperty.map((item: any) => item.prop)
)
localStorage.setItem('defaultSortMap', JSON.stringify(Object.fromEntries(defaultSortMap)))
}
})
}
//重置显示属性
const onResetProperty = () => {
defaultProperty.length = 0
selectProperty.list.length = 0
originalProperty.length = 0
propertyMap.delete(route.fullPath)
localStorage.setItem('propertyMap', JSON.stringify(Object.fromEntries(propertyMap)))
defaultSortMap.delete(route.fullPath)
localStorage.setItem('defaultSortMap', JSON.stringify(Object.fromEntries(defaultSortMap)))
props.page.table.describe.forEach((item: any) => {
console.log('item', item)
defaultProperty.push(item)
selectProperty.list.push(item.prop)
originalProperty.push(item)
})
}
/**
* 监听点击搜索按钮
* @param res
*/
const onSearch = (res: any) => {
if ('custom' in res) {
res['companyId'] = companyId.value
res['custom'] = { companyId: companyId.value, companyName: searchTrainSiteName.name }
}
emits('search', res)
}
/**
* 监听点击重置按钮
*/
const onReset = () => {
searchTrainSiteName.name = ''
companyId.value = ''
emits('reset')
}
/**
* 监听选择的当前分页
* @param index
*/
const onPaginationChange = (index: number) => {
emits('paginationChange', index)
}
/**
* 监听分页大小的改变
* @param index
*/
const onPaginationSize = (index: number) => {
emits('paginationSize', index)
}
const multipleTableRef = ref<InstanceType<typeof ElTable>>()
/**
* 表格全选框回调
* @param res
*/
const handleSelectionChange = (rows: any[]) => {
emits('multiSelection', rows)
}
/**
* 获取搜索栏的高度
*/
const getSearchHeight = () => {
searchHeight.value = searchVisible.value === true ? searchRef.value.$el.clientHeight : 0
// console.log('insertRef', insertRef)
insert.value = insertRef.value.children.length ? true : false
}
/**
* 页面初次挂载完成
*/
onMounted(() => {
getSearchHeight()
window.addEventListener<'resize'>('resize', getSearchHeight)
})
/**
* 页面卸载之前
*/
onBeforeUnmount(() => {
window.removeEventListener<'resize'>('resize', getSearchHeight)
})
/**
* 监听 props 参数变化
*/
watchEffect(() => {
searchVisible.value = props.page.search.length ? true : false
if (searchVisible.value) {
props.page.search.forEach((item: any) => {
if (Object.prototype.toString.call(item) === '[object Array]') {
placeholderText.value = item.length > 1 ? item[1] : '请输入公司名称|编号'
}
})
}
})
watch(props.page, (newVal: any, old: any) => {
//从本地缓存获取默认排序的列
originalProperty.length = 0
const describeArr: any = JSON.parse(JSON.stringify(newVal.table.describe))
const sortStr = localStorage.getItem('defaultSortMap')
if (sortStr && sortStr !== null) {
const obj = JSON.parse(sortStr)
for (const key in obj) {
defaultSortMap.set(key, obj[key])
}
if (defaultSortMap.has(route.fullPath)) {
const list: any[] = defaultSortMap.get(route.fullPath)
const sortMap = list.reduce((acc, prop, index) => {
acc[prop] = index
return acc
}, {})
describeArr.sort((a: any, b: any) => sortMap[a.prop] - sortMap[b.prop])
originalProperty.push(...describeArr)
} else {
originalProperty.push(...describeArr)
}
} else {
originalProperty.push(...describeArr)
}
//首先从本地缓存中获取用户勾选显示的列
// propertyMap.has
const str = localStorage.getItem('propertyMap')
if (str && str !== null) {
const obj = JSON.parse(str)
for (const key in obj) {
propertyMap.set(key, obj[key])
}
if (propertyMap.has(route.fullPath)) {
selectProperty.list.length = 0
const list = propertyMap.get(route.fullPath)
selectProperty.list.push(...list)
setSelectProperty(list)
} else {
initProperty(setDefaultNotSelectProperty(originalProperty))
}
} else {
initProperty(setDefaultNotSelectProperty(originalProperty))
}
})
//默认不勾选【所在区域】和【分销人员】
function setDefaultNotSelectProperty(list: any[]) {
const arr: any[] = []
list.forEach((item: any) => {
if (item.prop !== 'area' && item.prop !== 'referrerStr') {
arr.push(item)
}
})
return arr
}
//设置初始化默认属性
function initProperty(list: any[]) {
selectProperty.list.length = 0
defaultProperty.length = 0
list.forEach((item: any) => {
defaultProperty.push(item)
if (item.propName !== 'operate') {
selectProperty.list.push(item.prop)
}
})
}
//设置筛选属性
function setSelectProperty(list: any[]) {
defaultProperty.length = 0
//记录哪个列的位置
const sortNum: number[] = []
list.forEach((item: any) => {
originalProperty.forEach((child: any, index: number) => {
if (child.prop === item) {
sortNum.push(index)
}
})
})
//从小到大排列
sortNum.sort((a, b) => {
return a - b
})
sortNum.forEach(item => {
defaultProperty.push(originalProperty[item])
})
}
</script>
<style scoped lang="scss">
.operation-header {
display: flex;
align-items: center;
justify-content: center;
h1 {
font-size: 14px;
font-weight: bold;
line-height: 24px;
}
h2 {
font-size: 14px;
line-height: 24px;
width: 24px;
display: flex;
align-items: center;
justify-content: center;
margin-left: 4px;
cursor: pointer;
}
}
.operation {
display: flex;
div {
&:first-child {
margin-left: 12px;
}
}
}
.table-container {
box-sizing: border-box;
padding: 0;
background-color: #fff;
// margin-top: 8px;
}
.table {
overflow-y: auto;
&.insert {
height: auto;
}
}
.slot.insert {
margin: 0 0 16px 0;
display: flex;
align-items: center;
.button {
margin-right: 16px;
}
}
.expand-container {
box-sizing: border-box;
padding: 16px;
display: flex;
flex-wrap: wrap;
.line {
width: 50%;
display: block;
display: flex;
align-items: center;
line-height: 32px;
p {
width: 100px;
min-width: 100px;
text-align: right;
margin-right: 10px;
}
}
}
.swich-box {
width: 48px;
height: 20px;
background-color: #409eff;
border-radius: 10px;
overflow: hidden;
position: relative;
cursor: pointer;
transition: all 0.2s;
&.active {
background-color: #dcdfe6;
p {
text-align: left;
}
span {
left: calc(100% - 2px - 16px);
}
}
p {
font-size: 12px;
box-sizing: border-box;
line-height: 20px;
color: #fff;
padding: 0 4px;
display: block;
width: 100%;
text-align: right;
transition: all 0.2s;
}
span {
transition: all 0.2s;
display: block;
width: 16px;
height: 16px;
background-color: #fff;
border-radius: 50%;
position: absolute;
top: 2px;
left: 2px;
}
}
</style>
自从,重点部分已经结束。
接下来是看如何使用
首先你要创建一个文件夹,包含【config.ts,index.ts,Index.vue】
config.ts配置表
export const tableConfig = {
//搜索与表格中间部分的按钮
headerOperation: [
// {
// type: 'batch',
// visible: true,
// permission: [],
// className: '',
// name: '批量导入',
// button: 'file',
// radioGroup: []
// },
// {
// type: 'download',
// visible: true,
// permission: [],
// className: '',
// name: '模板下载',
// button: 'button',
// radioGroup: []
// },
{
type: 'create',
visible: true,
permission: ['POST:/api/system/schoolStore/manage/add'],
className: '',
name: '新增门店',
button: 'button',
radioGroup: []
}
],
//搜索列表
search: [{ key: 'name', type: 'text', name: '门店名称', placeholder: '' }],
//页面的数据权限
permission: ['GET:/api/system/schoolStore/manage/getPageList'],
//表格配置
table: {
operationWidth: '82px',
operation: [
{
type: 'edit',
visible: true,
permission: [
'POST:/api/system/schoolStore/manage/edit',
'GET:/api/system/schoolStore/manage/getDetailById'
],
className: '',
name: '编辑'
}
],
propName: {
name: '门店名称',
ndame: '门店代码',
schoolName: '所属驾校',
district: '所在片区',
city: '所在城市',
area: '所在区域',
status: '状态',
createBy: '操作人',
updateTime: '操作时间'
},
showSummary: false,
selection: false,
index: false,
isDialog: false,
summaryArray: [],
summaryType: '',
sortable: [],
switch: {
disabled: false,
permission: ['POST:/api/system/schoolStore/manage/updateDisable'],
tooltip: {
visible: false,
content: ''
}
}
}
}
Index.vue
<template>
<div class="page-box">
<TemplateVue
:page="data.pageData"
@search="data.onSearch"
@reset="data.onReset"
@paginationChange="data.onPaginationChange"
@paginationSize="data.onPaginationSize"
@operation="data.onOperation"
>
</TemplateVue>
</div>
</template>
<script setup lang="ts">
import TemplateVue from '@/components/template/Index.vue'
import ShopData from './index'
import { reactive, ref } from 'vue'
const data = reactive(new ShopData())
</script>
<style scoped lang="scss"></style>
index.ts
import { BaseData, BaseMethod } from '@/utils/basc-data'
import { tableConfig } from './config'
import { reactive } from 'vue'
interface RuleForm {
schoolName: string
siteShopName: string
address: string
lat: string
lng: string
regionId: string
schoolId: string
city: string
id: string
regionName: string
}
export default class ShopData extends BaseData implements BaseMethod {
//这里定义的搜索字段与配置表中的搜索字段需一直
searchData = { name: '' }
constructor() {
super(tableConfig)
//这里可以修图配置表中的信息
this.uStore.queryDrivingSchoolSimple((res: Map<string, any>) => {
res.forEach(item => {
this.drivingSchoolOptions.push({ label: item.shortName, value: item.id })
})
})
this.getData()
}
getData = (arg?: any) => {
}
onSearch = (arg?: any) => {
console.log(arg)
this.searchData = arg
this.getData()
}
onReset = (arg?: any) => {
this.searchData = { name: '' }
this.getData()
}
onPaginationChange = (arg?: any) => {
this.pageData.pagination.pageNo = arg
this.getData()
}
onPaginationSize = (arg?: any) => {
this.pageData.pagination.pageNo = 1
this.pageData.pagination.pageSize = arg
this.getData()
}
onOperation = (arg?: any) => {
const { type, data } = arg
console.log(type, data)
if (type === 'create') {
} else if (type === 'edit' || type === 'look') {
} else if (type === 'switch') {
}
}
}
到此结束