面对后台管理系统,全是表格类型的页面,如何快速开发

项目开始

当我们拿到设计稿或者原型图时,看到如下图展示的页面。我们就要想如何能够减少工作量,做出可复用的组件。

在这里插入图片描述
既然每个页面都长得差不多,那我们观察可以发现,这个页面分成四个部分,
【搜索部分、新增部分、表格展示部分、分页部分】
由于本人使用的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') {
           
        }
    }
}

到此结束

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值