likes
comments
collection
share

封装好用的后台组件(1)

作者站长头像
站长
· 阅读数 19

1、目的

  • 为啥 需要将这边拿出来?
  • 做个记录
  • 帮助其他人 更多的学习思路

ps: 基于 element ui(需在项目中引入) 注意 更多 展示 思路, 非直接 拷过去使用 , 非完美版本 , 还有很多优化点 ,仅供参考

2、data-table

  • 主要 是三部分 内容 一部分 搜索, 另一部分 表单展示 , 还有部分 翻页

  • 主要 组件展示

  • index.vue


<template>
    <div class="data-table">
        <Search v-if="searchItems.length > 0" @search="handleSearch" ref="search-form" :formItems="searchItems" />
        <mk-table
            ref="mk-table"
            :loading="loading"
            :columns="columns"
            @radio-change="radioChange"
            :data="data"
            @selection-change="selectionChange"
            @sort-change="onSortChange"
            :row-key="rowKey"
            :span-method="spanMethod"
        >
        </mk-table>
        <el-pagination
            transfer
            class="page-table layout-footer"
            placement="top"
            :total="total"
            :page-size="pageSize"
            :current="page"
            showElevator
            showTotal
            showSizer
            :page-size-opts="[10, 20, 30, 40, 50, 100]"
            @on-change="handlePageChange"
            @on-page-size-change="handlePageSizeChange"
        ></el-pagination>
    </div>
</template>

<script>
import MkTable from '@/components/mk-table'
import Search from './search.vue'

export default {
    components: { Search, MkTable },
    props: {
        columns: {
            type: Array,
            default: () => [],
        },
        func: {
            type: [Function, Object],
        },
        spanMethod: {
            type: Function,
        },
        // 切换页数是否是偏移逻辑
        isOffset: {
            type: Boolean,
            default: false,
        },
        // 分页是否从0开始
        isFromZero: {
            type: Boolean,
            default: false,
        },
        props: {
            type: Object,
            default() {
                return {
                    total: 'total',
                    page: 'page',
                    pageSize: 'pageSize',
                    list: 'data',
                }
            },
        },
        // 额外参数
        extraQuery: {
            type: Object,
            default: () => ({}),
        },
        searchItems: {
            type: Array,
            default: () => [],
        },
        rowKey: {
            type: [Function, String],
        },
    },
    data() {
        return {
            data: [],
            total: 0,
            page: 1,
            pageSize: 10,
            loading: false,
            saveQuery: {},
            reqFunc: null,
            canDo: false,
        }
    },
    watch: {
        func: {
            handler(v) {
                if (!v) return
                this.reqFunc = v
                // 有搜索项时 handleSearch会默认执行
                if (this.searchItems.length === 0 || this.canDo) this.getList()
            },
            immediate: true,
        },
    },
    computed: {
        hasReserveSelection() {
            return !!this.columns.find((v) => v && v.type === 'selection' && v['reserve-selection'])
        },
    },
    methods: {
        handleSearch(v) {
            this.$emit('searchChange', v)
            this.page = 1
            this.saveQuery = { ...v }
            // 默认执行
            this.getList()
            // 有搜索且初始化请求函数未赋值时 watch请求函数变化时继续请求列表
            this.canDo = !this.reqFunc
        },
        async getList(params = {}) {
            if (!this.reqFunc) return
            this.loading = true
            const { page = 'page', pageSize = 'pageSize', list = 'data', total = 'total' } = this.props
            // 设置页面和数量
            this.page = params.page || this.page
            this.pageSize = params.pageSize || this.pageSize
            Reflect.deleteProperty(params, 'page')
            Reflect.deleteProperty(params, 'pageSize')

            const obj = { ...this.saveQuery, ...this.extraQuery, ...params }
            // 兼容后端偏移量逻辑
            if (this.isOffset) {
                obj[page] = (this.page - 1) * this.pageSize
            } else {
                obj[page] = this.isFromZero ? this.page - 1 : this.page
            }
            obj[pageSize] = this.pageSize
            try {
                const { data } = await this.reqFunc(obj)
                if(!data[list]) data[list] = [] // 兼容列表不存在的情况
                // 处理临界条件
                if (data[list].length === 0 && this.page > 1) {
                    this.page -= 1
                    this.getList()
                }
                this.$set(this, 'data', data[list])
                this.total = Number(data[total]) // Number转换->兼容后台int64返回是字符串
            } finally {
                this.loading = false
                // 重新请求时保留选中不清空
                !this.hasReserveSelection && this.$emit('update:selected', [])
            }
        },
        // 重置搜索后获取列表
        searchList(v) {
            this.$refs['search-form'].initSearch(v)
        },
        handlePageChange(page) {
            this.page = page
            this.getList()
        },
        handlePageSizeChange(pageSize) {
            this.page = 1
            this.pageSize = pageSize
            this.getList()
        },
        clearSelection() {
            this.$refs['mk-table'].$refs['mk-table'].clearSelection()
        },
        selectionChange(v) {
            this.$emit('update:selected', v)
        },
        onSortChange(v) {
            this.$emit('on-sort-change', v)
        },
        radioChange(v) {
            this.$emit('radio-change', v)
        },
    },
}
</script>

  • Search.vue
    <Form class="search-form" ref="seachForm" :model="form" inline label-position="left">
        <FormItem v-for="item in formItems" :key="item.prop" :prop="item.prop" :label="item.label" :label-width="item.labelWidth || 100">
            <el-input
                v-if="item.itemType === 'input'"
                :type="item.type || 'text'"
                v-model="form[item.prop]"
                clearable
                v-bind="item"
                :placeholder="item.placeholder || '请输入'"
            />
            <Select
                style="width: 150px"
                v-if="item.itemType === 'select'"
                :placeholder="item.placeholder || '请选择'"
                v-model="form[item.prop]"
                v-bind="item"
                :clearable="item.clearable == undefined ? true : item.clearable"
            >
                <Option v-for="option in item.options" :key="option.value" :value="option.value"> {{ option.label }}</Option>
            </Select>
            <DatePicker v-if="item.itemType === 'date-picker'" v-model="form[item.prop]" v-bind="item" :placeholder="item.placeholder || '请选择'" />
            <el-cascader
                v-if="item.itemType === 'cascader'"
                style="width: 250px"
                v-bind="item"
                :clearable="item.clearable == undefined ? true : item.clearable"
                :filterable="item.filterable == undefined ? true : item.filterable"
                :collapse-tags="item['collapse-tags'] == undefined ? true : item['collapse-tags']"
                size="small"
                :placeholder="item.placeholder || '可搜索选择'"
                v-model="form[item.prop]"
                :props="item.props || { multiple: true, emitPath: false }"
                :options="item.options"
            ></el-cascader>
        </FormItem>
        <FormItem>
            <Button type="primary" @click="search" icon="search">搜索</Button>
            <Button type="ghost" @click="reset">重置</Button>
        </FormItem>
    </Form>
</template>

<script>
export default {
    props: {
        formItems: {
            type: Array,
            default: () => [],
        },
    },
    data() {
        return {
            form: {},
            outputEnum: {},
        }
    },
    created() {
        this.initSearch()
    },
    methods: {
        initSearch(init = {}) {
            this.form = this.formItems.reduce((pre, cur) => {
                if (cur.output) {
                    this.outputEnum[cur.prop] = {
                        output: cur.output,
                        outputMul: cur.outputMul || cur.type === 'daterange',
                    }
                }
                pre[cur.prop] = Reflect.has(init, cur.prop) ? init[cur.prop] : cur.default === undefined ? '' : cur.default
                return pre
            }, {})
            this.search()
        },
        search() {
            const form = Object.entries(this.form).reduce((pre, [key, value]) => {
                if (typeof value === 'string') {
                    value = value.trim()
                    // 排除掉空数据筛选
                    if (!value) return pre
                }
                // 不需要格式化输出
                if (!this.outputEnum[key]) {
                    pre[key] = value
                    return pre
                }
                // 格式化输出
                if (this.outputEnum[key].outputMul) {
                    pre = { ...pre, ...this.outputEnum[key].output(value) }
                } else {
                    pre[key] = this.outputEnum[key].output(value)
                }
                return pre
            }, {})
            this.$emit('search', form)
        },
        async reset() {
            await this.$refs['seachForm'].resetFields()
            this.search()
        },
    },
}
</script>

<style lang="less" scoped>
.search-form {
    margin: 20px 0 0 16px;
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
}
</style>

  • Mk.vue
<template>
    <el-table
        ref="mk
        -table"
        v-loading="loading"
        v-bind="$attrs"
        :data="dataValue"
        :row-key="rowKey || '_expandedKey'"
        :expand-row-keys="expandRowKeys || ttEpdRowKeys"
        v-on="$listeners"
        :row-class-name="setClassName"
        :header-cell-style="{ 'background-color': '#f8f8f9', color: '#606571' }"
    >
        <template v-for="(item, index) in columns">
            <!-- 兼容computed columns -->
            <template v-if="item">
                <el-table-column
                    v-if="item.type === 'radio'"
                    :key="`${item.key || item.type || 'key'}-${index}`"
                    :prop="item.key"
                    :label="item.title"
                    v-bind="item"
                >
                    <template slot-scope="scope">
                        <pf-checkbox @on-change="radioChange(scope)" v-model="scope.row._radioChecked"></pf-checkbox>
                    </template>
                </el-table-column>
                <el-table-column v-if="item.render" :prop="item.key" :label="item.title" :key="`${item.key || item.type || 'key'}-${index}`" v-bind="item">
                    <template slot-scope="scope">
                        <Render :render="item.render" :scope="scope" />
                    </template>
                </el-table-column>
                <el-table-column
                    v-if="!item.render && item.type !== 'radio'"
                    :key="`${item.key || item.type || 'key'}-${index}`"
                    :prop="item.key"
                    :label="item.title"
                    v-bind="item"
                ></el-table-column>
            </template>
        </template>
    </el-table>
</template>

<script>
import Render from './render'

export default {
    name: 'TtTable',
    components: {
        Render,
    },
    props: {
        columns: {
            type: Array,
            default: () => [],
        },
        data: {
            type: Array,
            default: () => [],
        },
        loading: {
            type: Boolean,
            default: false,
        },
        rowKey: {
            type: [Function, String],
        },
        expandRowKeys: {
            type: Array,
        },
        rowClassName: {
            type: [Function, String],
        },
    },
    data() {
        return {
            dataValue: [],
            ttEpdRowKeys: [],
        }
    },
    watch: {
        data: {
            handler(v) {
                this.ttEpdRowKeys = []
                this.dataValue = v.map((v, i) => {
                    const item = {
                        ...v,
                        _expandedKey: `${i}-${Math.random()}`,
                        _radioChecked: false,
                    }
                    item._expanded && this.ttEpdRowKeys.push(item._expandedKey)
                    return item
                })
            },
            immediate: true,
        },
    },
    methods: {
        setClassName({ row, rowIndex }) {
            const className = !this.rowClassName ? '' : typeof this.rowClassName === 'string' ? this.rowClassName : this.rowClassName({ row, rowIndex })
            return row._disableExpand ? `${className} mk-table-disable-expanded` : className
        },
        radioChange({ $index, row }) {
            this.dataValue.forEach((item, i) => {
                // 排他,每次选择时把其他选项都清除
                if (i !== $index) {
                    item._radioChecked = false
                }
            })
            this.$emit('radio-change', row._radioChecked ? row : {})
        },
    },
}
</script>

<style lang="less">
.mk-table-disable-expanded .el-table__expand-column .cell {
    display: none;
}
</style>

  • render.js
export default {
    name: 'Render',
    functional: true,
    props: {
        scope: Object,
        render: Function,
    },
    render: (h, ctx) => {
        const params = {
            ...ctx.props.scope,
        }
        return ctx.props.render(h, params)
    },
}

  • 如何使用 ?

  • 引入

        <data-table
            ref="mk-table"
            :props="{ list: 'list', page: 'page', pageSize: 'size', total: 'total' }"
            :func="getList"
            :columns="columns"
            :searchItems="searchForm"
        >
        </data-table>
  • func 请求数据

  • columns 表格表头

  • searchForm 搜索 内容

  • 实现效果

封装好用的后台组件(1)

3、未完待续