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

本文涉及的产品
数据可视化DataV,5个大屏 1个月
可视分析地图(DataV-Atlas),3 个项目,100M 存储空间
简介: 封装好用的后台组件(1)

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 搜索 内容
  • 实现效果

网络异常,图片无法展示
|

3、未完待续


相关实践学习
DataV Board用户界面概览
本实验带领用户熟悉DataV Board这款可视化产品的用户界面
阿里云实时数仓实战 - 项目介绍及架构设计
课程简介 1)学习搭建一个数据仓库的过程,理解数据在整个数仓架构的从采集、存储、计算、输出、展示的整个业务流程。 2)整个数仓体系完全搭建在阿里云架构上,理解并学会运用各个服务组件,了解各个组件之间如何配合联动。 3&nbsp;)前置知识要求 &nbsp; 课程大纲 第一章&nbsp;了解数据仓库概念 初步了解数据仓库是干什么的 第二章&nbsp;按照企业开发的标准去搭建一个数据仓库 数据仓库的需求是什么 架构 怎么选型怎么购买服务器 第三章&nbsp;数据生成模块 用户形成数据的一个准备 按照企业的标准,准备了十一张用户行为表 方便使用 第四章&nbsp;采集模块的搭建 购买阿里云服务器 安装 JDK 安装 Flume 第五章&nbsp;用户行为数据仓库 严格按照企业的标准开发 第六章&nbsp;搭建业务数仓理论基础和对表的分类同步 第七章&nbsp;业务数仓的搭建&nbsp; 业务行为数仓效果图&nbsp;&nbsp;
相关文章
|
6月前
|
druid NoSQL Redis
后台组件-配置
配置组件集成了平台所需的各类公用配置
|
6月前
|
JavaScript 前端开发 开发者
Vue的事件处理机制提供了灵活且强大的方式来响应用户的操作和组件间的通信
【5月更文挑战第16天】Vue事件处理包括v-on(@)指令用于绑定事件监听器,如示例中的按钮点击事件。事件修饰符如.stop和.prevent简化逻辑,如阻止表单默认提交。自定义事件允许组件间通信,子组件通过$emit触发事件,父组件用v-on监听并响应。理解这些机制有助于掌握Vue应用的事件控制。
67 4
|
4月前
|
存储 开发框架 前端开发
基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理
基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理
|
4月前
|
开发框架 JavaScript 前端开发
基于Vue的工作流项目模块中,使用动态组件的方式统一呈现不同表单数据的处理方式
基于Vue的工作流项目模块中,使用动态组件的方式统一呈现不同表单数据的处理方式
|
开发框架 JavaScript API
UniApp组件封装
UniApp是一个跨平台的开发框架,允许开发者使用Vue.js编写一次代码,然后将其发布到多个平台,包括iOS、Android和Web。在UniApp中,组件是构建用户界面的基本单元,它们可以重复使用,并且具有可配置的属性和方法。其中组件是一种可重用的UI元素,用于展示信息、接收用户输入或实现特定功能。UniApp提供了一系列内置的组件,如按钮、输入框、列表、滑动组件等,开发者也可以自定义和扩展组件以满足特定需求。
203 1
|
6月前
|
API
uniApp封装请求
uniApp封装请求
55 0
|
设计模式 开发框架 开发者
组件封装使用?
组件封装使用?
|
JavaScript 小程序
UniApp 小程序封装原生组件(使用与交互详细流程)
UniApp 小程序封装原生组件(使用与交互详细流程)
422 0
|
Java fastjson Maven
Android组件化开发(二)--网络请求组件封装
前面一篇文章我们讲解了`maven私服`的搭建,maven私服在`组件化框架`中有一个很重要的地位就是可以将我们的`lib`库放到局域网中,供公司其他开发者使用,实现类库的分享。 下面是这个系列准备实现的一个`组件化实战项目框架`: