-
2022-03-31 23:50:03
这里写自定义目录标题
1、背景
vue后台管理系统,会有很多表格页面,表格上方会有一些搜索选项。表格直接使用a-table即可,而搜索栏区域每次写起来都很繁琐,且多人开发情况下每个人写的样式都不相同,布局样式无法统一。
所以要考虑对搜索栏做一个封装,统一配置引用,提升开发维护效率和界面统一。
完成后的效果大概就是长这样:
2、分析
项目使用的是antd框架,搜索栏这种表单提交,首先要使用a-form-model组件来封装,而复杂点就是表单项可能有很多种,例如input输入框、select选择框、日期时间选择框、日期时间范围选择框、cascader级联选择框等,每一项的字段名prop、名称label、绑定的属性方法都不尽相同。所以不能通过普通的绑定个别属性的方式来处理,而slot插槽的方式也无法简化,最终决定通过传递一个配置项数组的形式来解析生成相应的结构。
3、实现
目前实现的方式由两部分组成,一部分是form表单组件,接受父组件传递的配置项数组,一部分是封装一些常用的表单项组件,通过v-if来控制,form表单组件里引入该表单项组件,循环遍历,根据传递的表单项类型来匹配显示具体的表单项。
(1)form表单组件(searchForm.vue)示例代码:
<div class="search-form-box"> <a-row> <a-form-model :model="formData" ref="formRef" layout="horizontal" :label-col="labelCol" :wrapper-col="wrapperCol" > <a-col :span="6" v-for="(item, index) in formOptions" :key="newKeys[index]"> <a-form-model-item :prop="item.prop" :label="item.label ? item.label + ':' : ''" :rules="item.rules" > <formItem v-model="formData[item.prop]" :itemOptions="item" :needParams="needParams" /> </a-form-model-item> </a-col> <!-- 自定义插槽,可用于特殊表单块 --> <slot></slot> </a-form-model> </a-row> <a-row type="flex" justify="end"> <!-- 提交按钮 --> <div class="btn-box"> <a-button v-if="btnItems.includes('search')" type="primary" class="btn-search" @click="onSearch" >搜索</a-button > <a-button v-if="btnItems.includes('export')" type="primary" class="btn-export" @click="onExport" >导出</a-button > <a-button v-if="btnItems.includes('reset')" type="default" class="btn-reset" @click="onReset" >重置</a-button > </div> </a-row> </div>
(2)formItem表单项组件(formItem.vue)示例代码:
<div class="form-item"> <a-input v-if="isInput" v-model="currentVal" v-bind="bindProps" v-on="bindEvents" allowClear /> <a-input-number v-if="isInputNumber" v-model="currentVal" v-bind="bindProps" v-on="bindEvents" allowClear /> <a-select v-if="isSelect" v-model="currentVal" v-bind="bindProps" v-on="bindEvents" allowClear > <a-select-option v-for="item in itemOptions.options" :key="item.value" :label="item.label" :value="item.value" ></a-select-option> </a-select> <!-- datetimerange/daterange --> <a-range-picker v-if="isDatePickerDateRange" v-model="currentVal" v-bind="bindProps" v-on="bindEvents" allowClear :placeholder="['开始日期', '结束日期']" separator="至" :default-time="['00:00:00', '23:59:59']" valueFormat="YYYY-MM-DD" /> <!-- monthrange --> <a-date-picker v-if="isDatePickerMonthRange" v-model="currentVal" v-bind="bindProps" v-on="bindEvents" type="monthrange" allowClear :placeholder="['开始日期', '结束日期']" range-separator="至" valueFormat="YYYY-MM" ></a-date-picker> <!-- others --> <a-date-picker v-if="isDatePickerOthers" v-model="currentVal" v-bind="bindProps" v-on="bindEvents" allowClear placeholder="请选择日期" valueFormat="YYYY-MM-DD" ></a-date-picker> <a-cascader v-if="isCascader" v-model="currentVal" v-bind="bindProps" v-on="bindEvents" :options="itemOptions.options" allowClear ></a-cascader> </div>
4、关键点
由于antd表单组件本身有很多配置属性,不可能把所有的属性和方法都写死封装,要想无缝支持,需要用到vue的v-bind和v-on特性,vue的v-bind和v-on支持赋值为对象类型,vue会自动遍历对象里的属性依次绑定,v2.4.0+支持。
5、参数配置项解释
(1)示例:
/** * 表单配置 * 示例: * [{ * label: '用户名', // label文字 * prop: 'username', // 字段名 * type: 'input', // 指定antd组件 * defaultValue: '阿黄', // 字段初始值 * placeholder: '请输入用户名', // antd组件属性 * rules: [{ required: true, message: '必填项', trigger: 'blur' }], // antd组件属性 * events: { // antd组件方法 * input (val) { * console.log(val) * }, * ...... // 可添加任意antd组件支持的方法 * } * ...... // 可添加任意antd组件支持的属性 * }] */
具体扩展属性可参考antd官方文档(antd官方文档)
(2)参数传递解析的流程:
- 首先,searchForm.vue组件里通过props接收参数:
formOptions: { type: Array, required: true, default () { return [] } /** * 请求所需参数 * 示例: * needParams: { * // 参数名 * // 参数值 * token: "123" * }, */ needParams: { type: Object, default() { return {}; } }, },
- created生命周期里处理初始值:
// 添加初始值 addInitValue () { const obj = {} this.formOptions.forEach(v => { if (v.initValue !== undefined) { obj[v.prop] = v.initValue } }) this.formData = obj }
- 一部分配置项绑定在a-form-model-item上,一部分传递给formItem表单项组件再绑定:
<a-form-model :model="formData" ref="formRef" layout="horizontal" :label-col="labelCol" :wrapper-col="wrapperCol" > <a-col :span="6" v-for="(item, index) in formOptions" :key="newKeys[index]"> <a-form-model-item :prop="item.prop" :label="item.label ? item.label + ':' : ''" :rules="item.rules" > <formItem v-model="formData[item.prop]" :itemOptions="item" :needParams="needParams" /> </a-form-model-item> </a-col> <!-- 自定义插槽,可用于特殊表单块 --> <slot></slot> </a-form-model>
- formItem.vue表单项组件里props接受传参:
itemOptions: { type: Object, default () { return {} } }
- computed里处理接收的参数itemOptions,生成要绑定的所有属性对象bindProps:
// 绑定属性 bindProps() { let obj = { ...this.itemOptions }; // 移除冗余属性 delete obj.label; delete obj.prop; delete obj.type; delete obj.defaultValue; delete obj.rules; delete obj.events; if (obj.type === "select") { delete obj.options; } return obj; },
- computed里生成要绑定的所有方法对象bindEvents:
// 绑定方法 bindEvents() { return this.itemOptions.events || {}; },
- 处理请求所需参数
// 获取请求参数 requestParams() { let params = { ...this.needParams }; let results = {}; if (Array.isArray(this.itemOptions.dicParamsList)) { this.itemOptions.dicParamsList.forEach(item => { if(item.value){ results[item.key] = item.value; }else { results[item.key] = params[item.key]; } }); } return results; },
- 最后组件上使用
<a-input v-if="isInput" v-model="currentVal" v-bind="bindProps" v-on="bindEvents" allowClear />
(3)特殊情况的处理
由于antd的a-select里是通过a-select-option遍历实现的,而遍历数组options按antd官方不是绑定在a-select上的,所以针对a-select的配置项再加一个options属性,即select选择项的数据数组。
<a-select v-if="isSelect" v-model="currentVal" v-bind="bindProps" v-on="bindEvents" allowClear > <a-select-option v-for="item in itemOptions.options" :key="item.value" :label="item.label" :value="item.value" ></a-select-option> </a-select>
created中请求数据
created() { // select 动态添加options if (this.itemOptions.url) { axios({ url: this.itemOptions.url, params: this.requestParams }).then(res => { this.itemOptions.options = res.data && res.data[this.itemOptions.propsHttpRes]; }) } },
6、按钮组
目前通过prop传入,三个按钮
// 提交按钮项,多个用逗号分隔(search搜索, export导出, reset重置) btnItems: { type: String, default () { return 'search' } }
7、使用方式示例
template
<searchForm :formOptions="formOptions" :needParams="needParams" btnItems="search, export, reset" />
data
formOptions: [ { label: "用户名很长是是是", // label文字 prop: "username", // 字段名 type: "input", // 指定antd组件 defaultValue: "阿黄", // 字段初始值 placeholder: "请输入用户名", // antd组件属性 rules: [{ required: true, message: "必填项", trigger: "blur" }], // antd组件属性 events: { // antd组件方法 input(val) { console.log(val); }, }, }, { label: "年龄", // label文字 prop: "age", // 字段名 type: "number", // 指定antd组件 defaultValue: 18, // 字段初始值 placeholder: "请输入年龄", // antd组件属性 rules: [{ required: true, message: "必填项", trigger: "blur" }], // antd组件属性 events: { // antd组件方法 input(val) { console.log(val); }, }, }, { label: "性别", // label文字 prop: "sex", // 字段名 type: "select", // 指定antd组件 defaultValue: "", // 字段初始值 placeholder: "请选择性别", // antd组件属性 options: [ { label: "男", value: "1", }, { label: "女", value: "2", }, ], events: { // antd组件方法 change(val) { console.log(val); }, }, }, { label: "下拉框", // label文字 prop: "address", // 字段名 type: "select", // 指定antd组件 defaultValue: "", // 字段初始值 placeholder: "请选择性别", // antd组件属性 options: [], url: 'http://rap2api.taobao.org/app/mock/270426/city', methods: 'get', dicParamsList: [ { key: 'token' }, { key: 'bussinessId', value: '324' } ], filedName: { label: 'name', value: 'id' }, propsHttpRes: 'data', events: { // antd组件方法 change(val) { console.log(val); }, }, }, { label: "项目地址", // label文字 prop: "project", // 字段名 type: "cascader", // 指定antd组件 defaultValue: [], // 字段初始值 placeholder: "请选择性别", // antd组件属性 options: [], url: 'http://rap2api.taobao.org/app/mock/270426/getCascaderList', methods: 'get', dicParamsList: [ { key: 'token' }, { key: 'bussinessId', value: '324' } ], filedName: { label: 'name', value: 'id' }, propsHttpRes: 'data', events: { // antd组件方法 change(val) { console.log(val); }, }, }, { label: "到货日期", // label文字 prop: "arrialDate", // 字段名 type: "range-picker", // 指定antd组件 defaultValue: [], // 字段初始值 placeholder: "请选择", // antd组件属性 events: { // antd组件方法 change(val) { console.log(val); }, }, }, { label: "发货日期", // label文字 prop: "delieverDate", // 字段名 type: "date-picker", // 指定antd组件 defaultValue: '', // 字段初始值 placeholder: "请选择", // antd组件属性 events: { // antd组件方法 change(val) { console.log(val); }, }, }, ],
8、完整代码
(1)searchForm.vue
<template> <div class="search-form-box"> <a-row> <a-form-model :model="formData" ref="formRef" layout="horizontal" :label-col="labelCol" :wrapper-col="wrapperCol" > <a-col :span="6" v-for="(item, index) in formOptions" :key="newKeys[index]"> <a-form-model-item :prop="item.prop" :label="item.label ? item.label + ':' : ''" :rules="item.rules" > <formItem v-model="formData[item.prop]" :itemOptions="item" :needParams="needParams" /> </a-form-model-item> </a-col> <!-- 自定义插槽,可用于特殊表单块 --> <slot></slot> </a-form-model> </a-row> <a-row type="flex" justify="end"> <!-- 提交按钮 --> <div class="btn-box"> <a-button v-if="btnItems.includes('search')" type="primary" class="btn-search" @click="onSearch" >搜索</a-button > <a-button v-if="btnItems.includes('export')" type="primary" class="btn-export" @click="onExport" >导出</a-button > <a-button v-if="btnItems.includes('reset')" type="default" class="btn-reset" @click="onReset" >重置</a-button > </div> </a-row> </div> </template> <script> import formItem from "./formItem"; import tools from '@/utils/tools' export default { name: 'searchForm', props: { /** * 表单配置 * 示例: * [{ * label: '用户名', // label文字 * prop: 'username', // 字段名 * type: 'input', // 指定antd组件 * defaultValue: '阿黄', // 字段初始值 * placeholder: '请输入用户名', // antd组件属性 * rules: [{ required: true, message: '必填项', trigger: 'blur' }], // antd组件属性 * events: { // antd组件方法 * input (val) { * console.log(val) * }, * ...... // 可添加任意antd组件支持的方法 * } * ...... // 可添加任意antd组件支持的属性 * }] */ formOptions: { type: Array, required: true, default() { return []; } }, /** * 请求所需参数 * 示例: * needParams: { * // 参数名 * // 参数值 * token: "123" * }, */ needParams: { type: Object, default() { return {}; } }, // 提交按钮项,多个用逗号分隔(search, export, reset) btnItems: { type: String, default() { return "search"; } } }, data() { return { labelCol: { xs: { span: 24 }, sm: { span: 9 }, md: { span: 11 }, }, wrapperCol: { xs: { span: 24 }, sm: { span: 15 }, md: { span: 13 }, }, formData: {} }; }, computed: { // 生成新的key newKeys() { return this.formOptions.map(() => { return tools.createUniqueString() }); } }, created() { this.addDefaultValue(); }, methods: { // 校验 onValidate(callback) { this.$refs.formRef.validate(valid => { if (valid) { console.log("提交成功"); console.log(this.formData); callback(); } }); }, // 搜索 onSearch() { this.onValidate(() => { this.$emit("onSearch", this.formData); }); }, // 导出 onExport() { this.onValidate(() => { this.$emit("onExport", this.formData); }); }, onReset() { this.$refs.formRef.resetFields(); }, // 添加初始值 addDefaultValue() { const obj = {}; this.formOptions.forEach(v => { if (v.defaultValue !== undefined) { obj[v.prop] = v.defaultValue; } }); this.formData = obj; } }, components: { formItem } }; </script> <style lang="scss"> .search-form-box { margin-bottom: 15px; .btn-box { padding-top: 5px; display: flex; button { height: 28px; margin: 0 5px; } } .ant-input-number { width: 100% ; } .ant-form-item { margin-bottom: 10px; } } </style>
(2)formItem.vue
<template> <div class="form-item"> <a-input v-if="isInput" v-model="currentVal" v-bind="bindProps" v-on="bindEvents" allowClear /> <a-input-number v-if="isInputNumber" v-model="currentVal" v-bind="bindProps" v-on="bindEvents" allowClear /> <a-select v-if="isSelect" v-model="currentVal" v-bind="bindProps" v-on="bindEvents" allowClear > <a-select-option v-for="item in itemOptions.options" :key="item.value" :label="item.label" :value="item.value" ></a-select-option> </a-select> <!-- datetimerange/daterange --> <a-range-picker v-if="isDatePickerDateRange" v-model="currentVal" v-bind="bindProps" v-on="bindEvents" allowClear :placeholder="['开始日期', '结束日期']" separator="至" :default-time="['00:00:00', '23:59:59']" valueFormat="YYYY-MM-DD" /> <!-- monthrange --> <a-date-picker v-if="isDatePickerMonthRange" v-model="currentVal" v-bind="bindProps" v-on="bindEvents" type="monthrange" allowClear :placeholder="['开始日期', '结束日期']" range-separator="至" valueFormat="YYYY-MM" ></a-date-picker> <!-- others --> <a-date-picker v-if="isDatePickerOthers" v-model="currentVal" v-bind="bindProps" v-on="bindEvents" allowClear placeholder="请选择日期" valueFormat="YYYY-MM-DD" ></a-date-picker> <a-cascader v-if="isCascader" v-model="currentVal" v-bind="bindProps" v-on="bindEvents" :options="itemOptions.options" allowClear ></a-cascader> </div> </template> <script> import axios from "axios"; export default { inheritAttrs: false, props: { value: {}, itemOptions: { type: Object, default() { return {}; } }, needParams: { type: Object, default: () => ({}) } }, computed: { // 双向绑定数据值 currentVal: { get() { return this.value; }, set(val) { this.$emit("input", val); } }, // 绑定属性 bindProps() { let obj = { ...this.itemOptions }; // 移除冗余属性 delete obj.label; delete obj.prop; delete obj.type; delete obj.defaultValue; delete obj.rules; delete obj.events; if (obj.type === "select") { delete obj.options; } return obj; }, // 绑定方法 bindEvents() { return this.itemOptions.events || {}; }, // 获取请求参数 requestParams() { let params = { ...this.needParams }; let results = {}; if (Array.isArray(this.itemOptions.dicParamsList)) { this.itemOptions.dicParamsList.forEach(item => { if(item.value){ results[item.key] = item.value; }else { results[item.key] = params[item.key]; } }); } return results; }, // a-input isInput() { return this.itemOptions.type === "input"; }, // a-input-number isInputNumber() { return this.itemOptions.type === "number"; }, // a-select isSelect() { return this.itemOptions.type === "select"; }, // a-date-picker (type: datetimerange/daterange) isDatePickerDateRange() { return this.itemOptions.type === "range-picker"; }, // a-date-picker (type: monthrange) isDatePickerMonthRange() { return this.itemOptions.type === "month-range"; }, // a-date-picker (type: other) isDatePickerOthers() { return this.itemOptions.type === "date-picker"; }, // a-cascader isCascader() { return this.itemOptions.type === "cascader"; }, }, created() { // select 动态添加options if (this.itemOptions.url) { axios({ url: this.itemOptions.url, params: this.requestParams }).then(res => { this.itemOptions.options = res.data && res.data[this.itemOptions.propsHttpRes]; }) } }, methods: {}, components: {}, }; </script> <style lang="less" scoped></style>
依赖引入的一些函数方法 tools.js
/** * 创建唯一的字符串 * @return {string} ojgdvbvaua40 */ function createUniqueString() { const timestamp = +new Date() + ""; const randomNum = parseInt((1 + Math.random()) * 65536) + ""; return (+(randomNum + timestamp)).toString(32); } export default { createUniqueString, };
更多相关内容 -
Vue Element table表格实现表头自定义多类型动态筛选 , 目前10种筛选类型,复制即用【不定时更新最新效果图...
2022-03-24 17:28:49实现图中的表格,特定的两个要求,筛选条件的接口(返回多种类型及字段标识),列表接口统一为一个,靠mark参数传输与后台商定好的标识,当然,如果你们的后端能够即能返回列表数据又能返回筛选条件的各种类型的标识也...一、效果图
目前10种筛选类型
看看是否是你需要的,本文可能有点长 ,我尽可能的给讲清楚,包括源码附上二、不定时更新最新效果图
2022.4.9 21:09 更新
- 点击当前行跳转
- 部分数据后缀追加图标
- 某列数据根据状态增加颜色标识
三、前言
-
实现图中的表格,特定的两个要求,筛选条件的接口(返回多种类型及字段标识),列表接口统一为一个,靠mark参数传输与后台商定好的标识,当然,如果你们的后端能够即能返回列表数据又能返回筛选条件的各种类型的标识也是极好的 。
-
表格中涉及到返回的数据是value(数字、id等类似select option里绑定的value)形式的,可能需要后端处理成label的形式返给前端,如同上图里的地区,下拉选择等列
-
表格中如有当前行的跳转、当前行的数据状态亦或者尾部的操作列存在时,需小心斟酌封装
-
目前筛选tag删除掉已选的条件没有支持到筛选popover里面清空数据,类似重置功能
-
表格上面的搜索组件,是个针对表格数据的全局搜索
-
vuedraggable是拖拽插件,上图中字段管理里的拖拽效果 ,需要的话请自行npm install
-
scss样式变量需自行更改,谢谢!!
-
后续发现的bug我会陆续修复并修改此文
四、组件层级
五、文件目录
六、index.vue(最父级,调用入口)
附:组件zn-开头命名是以公司缩写来命名的,目的也是为了区分全局组件 ,个人组件命名随意
<template> <div class="allCustomer-container"> <zn-query-form> <zn-query-form-top-panel> <!-- 字段管理 --> <field-management @setColumns="getColumns" /> <!-- 搜索 --> <Search @up-Search="upSearch" /> <!-- 筛选条件 --> <filter-tag :tagList="conditionList" @up-table="tableUpdate" /> </zn-query-form-top-panel> </zn-query-form> <zn-filter-table ref="filterTable" :multiple="true" :tableData="tableList" :finallyColumns="finallyColumns" :deatilsPath="deatilsPath" @selectList="getSelect" @fetch-data="fetchData" /> <zn-pagination v-show="total > 0" :page.sync="queryForm.page" :limit.sync="queryForm.listRows" @pagination="fetchData" :total="total" :algin="'right'" /> </div> </template> <script> import Search from '@/components/Search' import FieldManagement from '@/components/ZnFilterTable/components/fieldManagement' import FilterTag from '@/components/ZnFilterTable/components/filterTag' import ZnFilterTable from '@/components/ZnFilterTable' import { getAllList } from '@/api/customer' export default { name: 'allCustomer', components: { Search, FieldManagement, FilterTag, ZnFilterTable, }, provide() { return { mark: 'Member', //特定标识,根据业务模块不同,传输的标识也不同,标识由后端定义(或者字典维护) } }, data() { return { total: 0, tableList: [], listLoading: false, queryForm: { page: 1, listRows: 10, keywords: '', _filter: {}, //头部筛选 }, deatilsPath: '/customer/deatils', //表格当前行跳转路径 options: [], conditionList: [], //自定义筛选条件 columns: [], //筛选条件中的数据 checkList: [], //筛选条件中选中的数据 multipleList: [], //表格复选多选中的数据 } }, computed: { finallyColumns() { return this.columns.filter((item) => this.checkList.includes(item.label)) }, }, watch: {}, created() { this.fetchData() }, mounted() {}, methods: { // 请求table数据 async fetchData(arr) { this.listLoading = true //根据后端要求格式特殊处理 if (Array.isArray(arr) && arr.length > 0) { this.conditionList = arr //筛选tag赋值 this.queryForm._filter = {} //每次进来置空 arr.forEach((item) => { this.queryForm._filter[item['fieldName']] = item['value'] }) } const { data: { data, total }, } = await getAllList(this.queryForm) this.tableList = data this.total = total this.listLoading = false }, // 更新搜索字段,更新table数据 upSearch(val) { this.queryForm.keywords = val this.fetchData() }, // 获取多选选中的table数据(需求未出,功能暂留) getSelect(list) { this.multipleList = list console.log('list', list) }, // 子组件筛选条件返回 getColumns(columns, checkList) { this.columns = columns this.checkList = checkList }, // 自定义检索(popover组件(data为对象)和tag组件(data为数组))发射出来的事件 tableUpdate(data) { this.$refs.filterTable.tableUpdate(data) }, }, } </script> <style lang="scss" scoped> .el-button { border: none; margin-bottom: 0; } ::v-deep.pop-li { .el-button { color: black !important; } &:hover { background-color: $base-color-public; .el-button { color: $base-main-tone !important; } } } </style>
七、ZnFilterTable组件
<template> <div class="filter-table pt-10 pb-10"> <!-- 表格 --> <el-table ref="table" :data="tableData" style="width: 100%" border stripe @selection-change="handleSelectionChange" @row-click="toDeatils" > <el-table-column type="selection" width="55" v-if="multiple" ></el-table-column> <el-table-column v-for="(item, index) in finallyColumns" :key="item.id" :label="item.label" align="center" :prop="item.name" min-width="130" show-overflow-tooltip > <template slot="header"> <type-popover :columnIndex="index" :column="item" :filterOptions="item.param" @tableUpdate="tableUpdate" ></type-popover> </template> <template #default="{ row }"> <!-- 每一列涉及value值判断显示label ,以及状态颜色 --> <span v-if="item.extra.columnStyle == 'labelTags'" :class="item.extra.labelTags[row.type.value]" > {{ row.type.label }} </span> <!-- 电话后面有电话图标 --> <span v-else-if=" item.extra.columnStyle == 'labelCall' && row[item.name] != '' " > {{ row[item.name] }} <zn-icon class="column-label-call" icon="phone-fill" @click.stop="makeACall"/> </span> <!-- 性别、街道、居委、数据是对象{value:"",label:""} --> <span v-else-if="['sex','street','committee','source'].includes(item.name)"> {{ row[item.name].label }} </span> <span v-else>{{ row[item.name] }}</span> </template> </el-table-column> <!-- 如有操作列 ↓--> </el-table> </div> </template> <script> import TypePopover from './components/typePopover' export default { name: 'ZnFilterTable', components: { TypePopover }, props: { tableData: { type: Array, // table数据 default: () => [], }, finallyColumns: { type: Array, // table数据 default: () => [], }, multiple: { type: Boolean, // table数据 default: () => false, }, deatilsPath: { type: String, // table数据 default: () => '', }, }, data() { return { conditionList: [], multipleSelection: [], //table多选数据 } }, computed: {}, mounted() {}, methods: { handleSelectionChange(val) { this.multipleSelection = val this.$emit('selectList', this.multipleSelection) }, // 自定义检索(popover组件(data为对象)和tag组件(data为数组))发射出来的事件 tableUpdate(data) { let flag = true // 筛选条件如果已经存在,就更新,注意判别传递过来的数据类型 // arr.constructor === Array if (Array.isArray(data)) { this.conditionList = JSON.parse(JSON.stringify(data)) this.conditionList.forEach((item, index) => { if (item.fieldName == data.fieldName) { item.value = data.value flag = false } }) } else if (data.fieldName) { this.conditionList.push(data) // 如果没有就添加 } this.$parent.fetchData(this.conditionList) }, toDeatils(row) { if (this.deatilsPath) { this.$router.push({ path: this.deatilsPath, query: row.id }) } else { this.$baseMessage( '请配置所需要跳转的路径', 'warning', 'zn-hey-message-warning' ) } }, // 拨打电话 makeACall(){ }, }, } </script> <style scoped lang='scss'> // 占位,解决点击自己写的自定义筛选 会冒泡到排序 ::v-deep .el-table__column-filter-trigger { display: none !important; } ::v-deep.filter-table { // table状态标签颜色定义 [class*='table-status'] { display: inline-block; border-radius: 2px; padding: 0px 12px; } // 蓝色 [class*='table-status-blue'] { background: #e6effb; color: #005bd9; } // 棕色 [class*='table-status-brown'] { background: #fff6ec; color: #ffa336; } // 绿色 [class*='table-status-green'] { background: #e8fff0; color: #00b47e; } // 黄色 [class*='table-status-yellow'] { background: #fffae8; color: #f9c200; } // 粉色 [class*='table-status-pink'] { background: #ffece8; color: #f53f3f; } // 白色 [class*='table-status-white'] { background: #f2f3f5; color: #1d2129; } } </style>
八、ZnPagination组件
<template> <div :class="{ hidden: hidden }" class="pagination-container"> <el-pagination :style="{'text-align':algin}" :background="background" :current-page.sync="currentPage" :page-size.sync="pageSize" :layout="layout" :page-sizes="pageSizes" :total="total" v-bind="$attrs" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> </div> </template> <script> import { scrollTo } from '@/utils/scroll-to' export default { name: 'ZnPagination', props: { total: { required: true, type: Number, }, page: { type: Number, default: 1, }, limit: { type: Number, default: 10, }, pageSizes: { type: Array, default() { return [10, 20, 30, 50] }, }, layout: { type: String, default: 'total, sizes, prev, pager, next, jumper', }, background: { type: Boolean, default: true, }, autoScroll: { type: Boolean, default: true, }, hidden: { type: Boolean, default: false, }, algin: { type: String, default: ()=>'center', }, }, computed: { currentPage: { get() { return this.page }, set(val) { this.$emit('update:page', val) }, }, pageSize: { get() { return this.limit }, set(val) { this.$emit('update:limit', val) }, }, }, methods: { handleSizeChange(val) { this.$emit('pagination', { page: this.currentPage, limit: val }) if (this.autoScroll) { scrollTo(0, 800) } }, handleCurrentChange(val) { this.$emit('pagination', { page: val, limit: this.pageSize }) if (this.autoScroll) { scrollTo(0, 800) } }, }, } </script> <style lang="scss" scoped> .pagination-container { background: #fff; } .pagination-container.hidden { display: none; } </style>
九、zn-query-form组件
<template> <el-row class="zn-query-form" :gutter="0"> <slot /> </el-row> </template> <script> export default { name: 'ZnQueryForm', } </script> <style lang="scss" scoped> @mixin panel { display: flex; flex-wrap: wrap; align-content: center; align-items: center; justify-content: flex-start; min-height: $base-input-height; margin: 0 0 #{math.div($base-margin, 2)} 0; > .el-button { margin: 0 10px #{math.div($base-margin, 2)} 0 !important; } } .zn-query-form { ::v-deep { .el-form-item:first-child { margin: 0 0 #{math.div($base-margin, 2)} 0 !important; } .el-form-item + .el-form-item { margin: 0 0 #{math.div($base-margin, 2)} 0 !important; .el-button { margin: 0 0 0 10px !important; } } .top-panel { @include panel; } .bottom-panel { @include panel; border-top: 1px solid #dcdfe6; } .left-panel { @include panel; } .right-panel { @include panel; justify-content: flex-end; } } } </style>
十、zn-query-form-top-panel组件
<template> <el-col :span="24"> <div class="top-panel"> <slot /> </div> </el-col> </template> <script> export default { name: 'ZnQueryFormTopPanel', } </script>
十一、field-management组件
<template> <el-dropdown class="ml-10 mr-10" trigger="hover"> <el-button type="text" size="medium" icon="el-icon-tickets"> 字段管理 </el-button> <el-dropdown-menu slot="dropdown"> <el-row :gutter="10" type="flex" class="row-flex"> <el-col :span="6"> <el-checkbox :indeterminate="isIndeterminate" v-model="checkAll" @change="handleCheckAllChange" > 勾选您要选择的字段 </el-checkbox> </el-col> <el-divider /> <el-checkbox-group v-if="options.length > 0" v-model="checkList" @change="handleCheckedChange" > <zn-draggable v-bind="dragOptions" :list="options" class="zn-draggable" > <el-col :span="24" class="checkbox-group-col" v-for="item in options" :key="item.id" > <!-- <zn-icon icon="drag-drop-line" /> --> <el-checkbox :label="item.label"> {{ item.label }} </el-checkbox> </el-col> </zn-draggable> </el-checkbox-group> </el-row> </el-dropdown-menu> </el-dropdown> </template> <script> import ZnDraggable from 'vuedraggable' import { getTableHeader } from '@/api/index' export default { name: 'fieldManagement', components: { ZnDraggable, }, inject: ['mark'], data() { return { checkAll: false, checkList: [], options: [], isIndeterminate: true, } }, computed: { dragOptions() { return { animation: 600, group: 'description', } }, }, watch: {}, created() { this.getHeader() }, mounted() {}, methods: { // 字段管理的接口是统一的 , 只有业务模块的mark标识不同 , 所以请求就写在了组件里 async getHeader() { if (this.mark != '') { getTableHeader({ mark: this.mark, }).then((res) => { this.options = res.data this.options.map((item) => { if (item.isShow == true) { this.checkList.push(item.label) } }) this.$emit('setColumns', this.options, this.checkList) }) } }, // 操纵全选 handleCheckAllChange(val) { if (val) { this.options.map((item) => { this.checkList.push(item.label) }) } else { this.checkList = [] } this.isIndeterminate = false // 向父组件发送数据 this.$emit('setColumns', this.options, this.checkList) }, // 单个数据选中 handleCheckedChange(value) { let checkedCount = value.length this.checkAll = checkedCount === this.options.length this.isIndeterminate = checkedCount > 0 && checkedCount < this.options.length // 向父组件发送数据 this.$emit('setColumns', this.options, this.checkList) }, }, } </script> <style lang="scss" scoped> .row-flex { padding: 15px; display: flex; flex-direction: column; } .zn-draggable { display: flex; flex-direction: column; justify-content: flex-start; } .checkbox-group-col { max-height: 100px; overflow-y: auto; .el-checkbox { padding: 3px 0; width: 100%; margin-right: 0; &:hover { background-color: $base-color-public; } } } </style>
十二、Search组件
<template> <div class="public-search" :class="{ isActive: isActive }" @click.stop="handleSearch" > <el-input v-model="searchText" class="search" ref="search" clearable prefix-icon="el-icon-search" placeholder="搜索" @input="searchHandler" ></el-input> </div> </template> <script> export default { name: 'Search', components: {}, props: {}, data() { return { isActive: false, searchText: '', } }, computed: {}, watch: {}, created() {}, mounted() {}, methods: { handleSearch() { let _this = this this.isActive = true this.$refs.search.focus() function otherClick() { if (_this.searchText == '') { _this.isActive = false document.body.removeEventListener('click', otherClick) } } document.body.addEventListener('click', otherClick) }, searchHandler() { this.$emit('up-Search',this.searchText) //改变搜索字段的value }, }, } </script> <style lang="scss" scoped> ::v-deep.el-input, .search { height: 100%; line-height: 34px; border: none; color: $base-color-black; padding: 0; pointer-events: none; transition: all 0.3s ease-in-out; .el-input__inner { cursor: pointer; border: none; } .el-input__inner::placeholder { color: black !important; } } .public-search { display: inline-block; overflow: hidden; cursor: pointer; border: 1px solid white; } ::v-deep.isActive { border: 1px solid $base-main-tone; transition: all 0.3s ease-in-out; } </style>
十三、filter-tag组件
<template> <!-- 条件tag --> <div class="filter-tag ml-10 mr-10"> <!-- <div class="filter-tag-title pt-10 pb-10">筛选条件:</div> --> <el-tag @close="conditionClose(index)" style="margin-left: 10px" v-for="(tag, index) in list" :key="index" closable :type="tag.prop" > <span>{{ tag.tagLabel }} :</span> <span style="color: red">{{ tag.tagValue }}</span> </el-tag> </div> </template> <script> export default { name: 'filterTag', components: {}, props: { list: { type: Array, default: () => [], }, }, data() { return {} }, computed: {}, watch: {}, created() {}, mounted() {}, methods: { conditionClose(index) { this.list.splice(index, 1) // 关闭条件tag-发射给父组件 删除已选中的 this.$emit('up-table', this.list) // 传递的是数组 并更新列表数据 }, }, } </script> <style lang="scss" scoped> .filter-tag { display: inline-block; .filter-tag-title { @extend .filter-tag; } } </style>
十四、typePopover组件
<template> <!-- 每个table表头的popover --> <!-- 注意:逻辑部分尽量不好写到这个组件内,因为这个组件是根据外面table循环创建的,在这里写逻辑会非常影响性能 --> <div class="customHeader" style="display: inline-block"> <el-popover placement="bottom" trigger="click" :ref="`popover-${columnIndex}`" > <!-- table表头文字显示--> <span slot="reference" class="label"> <zn-icon :icon="column.extra.icon" /> {{ column.label }} <i class="el-icon-arrow-down"></i> </span> <!-- text 文本 --> <div v-if="column.type == 'text'"> <el-input clearable v-model.trim="filterForm.value" placeholder="请输入查询内容" @keyup.native.enter="confirm()" ></el-input> </div> <!-- number 数字框 --> <div v-else-if="column.type == 'number'"> <el-input clearable oninput="value=value.replace(/[^0-9.]/g,'')" v-model.trim="filterForm.value" placeholder="请输入数字" @keyup.native.enter="confirm()" ></el-input> </div> <!-- number_range 数字范围--> <div v-else-if="column.type == 'number_range'"> <el-input style="width: 120px" clearable oninput="value=value.replace(/[^0-9.]/g,'')" v-model.trim="filterForm.value" placeholder="请输入数字" ></el-input> - <el-input style="width: 120px" clearable oninput="value=value.replace(/[^0-9.]/g,'')" v-model.trim="spareValue" placeholder="请输入数字" ></el-input> </div> <!-- date 单个日期--> <div v-else-if="column.type == 'date'"> <el-date-picker v-model="filterForm.value" type="date" clearable placeholder="选择日期" value-format="yyyy-MM-dd" /> </div> <!-- datetime 日期时间--> <div v-else-if="column.type == 'datetime'"> <el-date-picker v-model="filterForm.value" type="datetime" placeholder="选择日期时间" value-format="yyyy-MM-dd HH:mm:ss" ></el-date-picker> </div> <!-- date_range 日期范围--> <div v-else-if="column.type == 'date_range'"> <el-date-picker v-model="filterForm.value" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd" ></el-date-picker> </div> <!-- datetime_range 日期时分秒范围--> <div v-else-if="column.type == 'datetime_range'"> <el-date-picker v-model="filterForm.value" clearable type="datetimerange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd HH:mm:ss" ></el-date-picker> </div> <!-- select 下拉选择--> <div v-else-if="column.type == 'select'"> <el-select v-model="filterForm.value" placeholder="请选择" style="width: 100%" clearable > <el-option v-for="(item, index) in filterOptions" :key="index" :label="item.label" :value="item.value" ></el-option> </el-select> </div> <!-- radio 单选--> <div v-else-if="column.type == 'radio'"> <el-radio-group v-model="filterForm.value"> <el-radio v-for="(item, index) in filterOptions" :key="index" :label="item.value" :value="item.value" > {{ item.label }} </el-radio> </el-radio-group> </div> <!-- checkBox 多选--> <div v-else-if="column.type == 'checkBox'"> <el-checkbox-group v-model="checkboxList"> <el-checkbox v-for="(item, index) in filterOptions" :key="index" :label="item.value" :value="item.value" > {{ item.label }} </el-checkbox> </el-checkbox-group> </div> <!-- confirm 确定框--> <div style="text-align: right"> <el-button @click="confirm()" type="primary" size="mini" class="confirm" > 确定 </el-button> </div> </el-popover> </div> </template> <script> export default { name: 'typePopover', // column 当前列数据,filterOptions 多选/单选/下拉/数据 props: ['column', 'filterOptions', 'columnIndex'], data() { return { filterForm: { tagLabel: this.column.label, //筛选tag label(tag用) tagValue: '', //筛选tag value(tag用) value: '', //所筛选的数据(后端接收用) fieldName: this.column.name, //当前表头字段(后端接收用) }, spareValue: '', //备用Value popover里如是两个值的话需要用此来拼接 checkboxList: [], } }, created() {}, methods: { confirm() { let minValue = this.filterForm.value //数值双向绑定 做个闭环赋值 let type = this.column.type // 跟后端商定 , 多个值存在时进行判断 , 以filterForm.value一个值为字符串的形式传递 // 根据需求做了处理 // checkBox和radio和select由于value值的原因需要处理 if (type == 'checkBox' || type == 'radio' || type == 'select') { if (type == 'checkBox') { this.filterForm.value = this.checkboxList.join(',') } if (this.column.param && this.column.param.length > 0) { let str = '' this.column.param.forEach((i, t) => { if (type == 'checkBox' && i.value == Number(this.checkboxList[t])) { str = str + i.label } if (type == 'radio' && i.value == Number(this.filterForm.value)) { str = str + i.label } if (type == 'select' && i.value == Number(this.filterForm.value)) { str = str + i.label } }) this.filterForm.tagValue = str } } // 数字范围 else if (type == 'number_range') { this.filterForm.tagValue = this.filterForm.value + ' - ' + this.spareValue this.filterForm.value = this.filterForm.value + ',' + this.spareValue } else if (this.filterForm.value == '' && !this.spareValue) { return this.$message.warning('请输入或选择筛选条件') } else { this.filterForm.tagValue = this.filterForm.value //其他类型的赋值给tag用 } this.$emit('tableUpdate', this.filterForm) //传递的是对象 this.filterForm.value = minValue //数值双向绑定 做个闭环赋值 ,俗称瞒天过海 this.$refs['popover-' + this.columnIndex].doClose() // 关闭popover }, }, } </script> <style scoped> .confirm { margin-top: 10px; } /* 禁止双击选中文字 */ .label { -moz-user-select: none; /*火狐*/ -webkit-user-select: none !important; /*webkit浏览器*/ -ms-user-select: none; /*IE10*/ -khtml-user-select: none; /*早期浏览器*/ user-select: none; } .labelColor { color: #409eff; } .el-icon-arrow-down { cursor: pointer; } .el-checkbox-group { display: flex; justify-content: flex-start; flex-direction: column; max-height: 150px; overflow-y: auto; } </style>
十五、接口示例数据
- getHeader 字段管理数据格式
this.getHeader() async getHeader(){ return [ { id: 0, name: 'name', label: '姓名', type: 'text', param: '', isShow: true, exp: '', extra: { icon: 'file-list-line', }, }, { id: 1, name: 'age', label: '年龄', type: 'number_range', param: '', isShow: true, exp: 'age', extra: { icon: 'file-list-line', }, }, { id: 2, name: 'phone_main', label: '主要电话', type: 'text', param: '', isShow: true, exp: '', extra: { icon: 'file-list-line', columnStyle: 'labelCall', }, }, { id: 3, name: 'phone_backup', label: '备用电话', type: '', param: '', isShow: true, exp: '', extra: { icon: 'file-list-line', }, }, { id: 4, name: 'id_card', label: '身份证号', type: '', param: '', isShow: true, exp: '', extra: { icon: 'file-list-line', }, }, { id: 5, name: 'birth_day', label: '生日', type: 'date_range', param: '', isShow: true, exp: '', extra: { icon: 'file-list-line', }, }, { id: 6, name: 'sex', label: '性别', type: 'select', param: [ { label: '未知', value: 0, }, { label: '男', value: 1, }, { label: '女', value: 2, }, ], isShow: true, exp: '', extra: { icon: 'file-list-line', }, }, { id: 7, name: 'type', label: '老人类型', type: 'select', param: [ { value: 101, label: '独居', }, { value: 102, label: '孤老', }, { value: 103, label: '失独', }, { value: 104, label: '其他', }, ], isShow: true, exp: '', extra: { icon: 'file-list-line', columnStyle: 'labelTags', labelTags: { 101: 'table-status-blue', 102: 'table-status-brown', 103: 'table-status-green', 104: 'table-status-yellow', }, }, }, { id: 8, name: 'street', label: '街道', type: 'text', param: '', isShow: true, exp: 'street', extra: { icon: 'file-list-line', }, }, { id: 9, name: 'committee', label: '居委', type: 'text', param: '', isShow: true, exp: '', extra: { icon: 'file-list-line', }, }, { id: 10, name: 'address', label: '详细地址', type: 'text', param: '', isShow: true, exp: '', extra: { icon: 'file-list-line', }, }, { id: 11, name: 'reg_address', label: '户籍地址', type: 'text', param: '', isShow: true, exp: '', extra: { icon: 'file-list-line', }, }, { id: 12, name: 'source', label: '来源', type: 'select', param: [], isShow: true, exp: '', extra: { icon: 'file-list-line', }, }, { id: 13, name: 'create_time', label: '创建时间', type: 'datetime_range', param: '', isShow: true, exp: '', extra: { icon: 'file-list-line', }, }, { id: 14, name: 'update_time', label: '更新时间', type: 'datetime_range', param: '', isShow: true, exp: '', extra: { icon: 'file-list-line', }, }, { id: 15, name: 'create_user_id', label: '创建人', type: '', param: '', isShow: true, exp: '', extra: { icon: 'file-list-line', }, }, { id: 16, name: 'update_user_id', label: '更新人', type: '', param: '', isShow: true, exp: '', extra: { icon: 'file-list-line', }, }, ] }
- fetchData 列表数据格式
async fetchData() { return [ { id: 1, type: { value: 101, label: '独居', }, name: '柳倩倩', marital_status: { value: 0, label: '--', }, phone_main: '0215567252', phone_backup: '18221274181', id_card: '350583199202224336', birth_day: '1976-03-02', tags: ['123124'], sex: { value: 1, label: '男', }, province: { value: '31', label: '上海市', }, city: { value: '3101', label: '市辖区', }, area: { value: '310117', label: '松江区', }, street: { value: '310117501', label: '松江工业区', }, committee: { value: '310117501498', label: '松江工业区虚拟社区', }, address: '泰晤士小镇', reg_address: '户籍', remark: '', status: 1, general_info: '', medical_card: null, blood_type: { value: 'A', label: 'A', }, health_info: '', physical_condition: null, basic_info: null, allergic_drugs: null, medical_records: null, is_contract: 0, contract_imgs: [], source: { value: '', label: ' -- ', }, create_time: '2022-01-18 10:52:45', update_time: '2022-04-06 18:28:28', create_user_id: 0, update_user_id: 0, contract: { value: 1, label: '幸福久久', }, category: [], frequency: { value: '', label: ' -- ', }, service_start: null, service_end: null, service_status: { value: 1, label: '正常', }, service_cancel_reason: { value: 0, label: '--', }, agent_id: { value: 1, label: 'admin', }, age: 46, }, { id: 2, type: { value: 102, label: '孤老', }, name: '潘宗磊', marital_status: { value: 0, label: '--', }, phone_main: '13822223333', phone_backup: '18817673315', id_card: '34240119881214323X', birth_day: '1988-12-14', tags: ['老人', '身体不便', '高血压'], sex: { value: 2, label: '女', }, province: { value: '34', label: '安徽省', }, city: { value: '3415', label: '六安市', }, area: { value: '341503', label: '裕安区', }, street: { value: '310117006', label: '九里亭街道', }, committee: { value: '341503105205', label: '泉水村委会', }, address: '泉水冲', reg_address: '上海市松江区泰晤士小镇', remark: '备注111', status: 1, general_info: '', medical_card: '123123', blood_type: { value: 'AB', label: 'AB', }, health_info: '', physical_condition: '<p><font color="#c24f4a"><b><i>正常</i></b></font><span style="font-size: 14px;"></span></p>', basic_info: '<p>11</p>', allergic_drugs: '<p>22</p>', medical_records: '<p>33</p>', is_contract: 0, contract_imgs: [], source: { value: '', label: ' -- ', }, create_time: '2022-01-18 11:01:06', update_time: '2022-04-09 15:38:23', create_user_id: 0, update_user_id: 0, contract: { value: 1, label: '幸福久久', }, category: ['2', '3'], frequency: { value: 1, label: '一周两次', }, service_start: '2022-04-20', service_end: '2022-04-30', service_status: { value: 2, label: '取消', }, service_cancel_reason: { value: 3, label: '搬迁', }, agent_id: { value: 1, label: 'admin', }, age: 33, }, { id: 3, type: { value: 103, label: '失独', }, name: '宋岩', marital_status: { value: 0, label: '--', }, phone_main: '', phone_backup: '13917303249', id_card: '350583199202224336', birth_day: '2022-01-13', tags: [], sex: { value: 1, label: '男', }, province: { value: '31', label: '上海市', }, city: { value: '3101', label: '市辖区', }, area: { value: '310117', label: '松江区', }, street: { value: '310117001', label: '岳阳街道', }, committee: { value: '310117001002', label: '醉白池社区居委会', }, address: '新凯城', reg_address: null, remark: '', status: 1, general_info: '', medical_card: null, blood_type: { value: 'A', label: 'A', }, health_info: '', physical_condition: null, basic_info: null, allergic_drugs: null, medical_records: null, is_contract: 0, contract_imgs: [], source: { value: '', label: ' -- ', }, create_time: '2022-01-18 11:42:19', update_time: '2022-02-15 15:07:14', create_user_id: 0, update_user_id: 0, contract: { value: 1, label: '幸福久久', }, category: [], frequency: { value: '', label: ' -- ', }, service_start: null, service_end: null, service_status: { value: 1, label: '正常', }, service_cancel_reason: { value: 0, label: '--', }, agent_id: { value: 1, label: 'admin', }, age: 0, }, { id: 4, type: { value: 104, label: '其他', }, name: '李三', marital_status: { value: 0, label: '--', }, phone_main: '', phone_backup: '13917303249', id_card: '350583199202224336', birth_day: '2022-01-04', tags: [], sex: { value: 1, label: '男', }, province: { value: '12', label: '天津市', }, city: { value: '1201', label: '市辖区', }, area: { value: '120103', label: '河西区', }, street: { value: '120103002', label: '下瓦房街道', }, committee: { value: '120103002003', label: '台北路社区居委会', }, address: '发达', reg_address: null, remark: '', status: 1, general_info: '', medical_card: null, blood_type: { value: '', label: ' -- ', }, health_info: '', physical_condition: null, basic_info: null, allergic_drugs: null, medical_records: null, is_contract: 0, contract_imgs: [], source: { value: '', label: ' -- ', }, create_time: '2022-01-18 12:03:59', update_time: '2022-02-18 17:01:12', create_user_id: 0, update_user_id: 0, contract: { value: 1, label: '幸福久久', }, category: [], frequency: { value: '', label: ' -- ', }, service_start: null, service_end: null, service_status: { value: 1, label: '正常', }, service_cancel_reason: { value: 0, label: '--', }, agent_id: { value: 1, label: 'admin', }, age: 0, }, { id: 5, type: { value: 102, label: '孤老', }, name: '张三', marital_status: { value: 0, label: '--', }, phone_main: '', phone_backup: '18221274181', id_card: '51382219941101899X', birth_day: '1994-11-30', tags: [], sex: { value: 1, label: '男', }, province: { value: '31', label: '上海市', }, city: { value: '3101', label: '市辖区', }, area: { value: '310117', label: '松江区', }, street: { value: '310117004', label: '中山街道', }, committee: { value: '310117004014', label: '中山苑社区居委会', }, address: '泰晤士小镇卡纳比岛', reg_address: null, remark: '', status: 1, general_info: '', medical_card: null, blood_type: { value: 'O', label: 'O', }, health_info: '', physical_condition: null, basic_info: null, allergic_drugs: null, medical_records: null, is_contract: 0, contract_imgs: [], source: { value: '', label: ' -- ', }, create_time: '2022-01-18 12:05:45', update_time: '2022-01-25 17:34:37', create_user_id: 0, update_user_id: 0, contract: { value: 1, label: '幸福久久', }, category: [], frequency: { value: '', label: ' -- ', }, service_start: null, service_end: null, service_status: { value: 1, label: '正常', }, service_cancel_reason: { value: 0, label: '--', }, agent_id: { value: 1, label: 'admin', }, age: 27, }, { id: 6, type: { value: 102, label: '孤老', }, name: '陈情期', marital_status: { value: 0, label: '--', }, phone_main: '0215567252', phone_backup: '18817673315', id_card: '350583199202224336', birth_day: '2022-01-05', tags: [], sex: { value: 2, label: '女', }, province: { value: '31', label: '上海市', }, city: { value: '3101', label: '市辖区', }, area: { value: '310117', label: '松江区', }, street: { value: '310117501', label: '松江工业区', }, committee: { value: '310117501498', label: '松江工业区虚拟社区', }, address: '新凯城', reg_address: null, remark: '', status: 1, general_info: '', medical_card: null, blood_type: { value: 'A', label: 'A', }, health_info: '', physical_condition: null, basic_info: null, allergic_drugs: null, medical_records: null, is_contract: 0, contract_imgs: [], source: { value: '', label: ' -- ', }, create_time: '2022-01-18 14:44:58', update_time: '2022-02-17 14:39:08', create_user_id: 0, update_user_id: 0, contract: { value: 1, label: '幸福久久', }, category: [], frequency: { value: '', label: ' -- ', }, service_start: null, service_end: null, service_status: { value: 1, label: '正常', }, service_cancel_reason: { value: 0, label: '--', }, agent_id: { value: 1, label: 'admin', }, age: 0, }, { id: 7, type: { value: 102, label: '孤老', }, name: '李斯', marital_status: { value: 0, label: '--', }, phone_main: '0215567252', phone_backup: '18221274180', id_card: '51382219941101899X', birth_day: '2019-01-18', tags: [], sex: { value: 0, label: '未知', }, province: { value: '51', label: '四川省', }, city: { value: '5101', label: '成都市', }, area: { value: '510104', label: '锦江区', }, street: { value: '510104017', label: '锦官驿街道', }, committee: { value: '510104017001', label: '水井坊社区居委会', }, address: '卡纳比岛', reg_address: null, remark: '', status: 1, general_info: '', medical_card: null, blood_type: { value: 'A', label: 'A', }, health_info: '良好', physical_condition: null, basic_info: null, allergic_drugs: null, medical_records: null, is_contract: 0, contract_imgs: [], source: { value: '', label: ' -- ', }, create_time: '2022-01-18 15:14:29', update_time: '2022-02-19 17:32:08', create_user_id: 0, update_user_id: 0, contract: { value: 1, label: '幸福久久', }, category: [], frequency: { value: '', label: ' -- ', }, service_start: null, service_end: null, service_status: { value: 1, label: '正常', }, service_cancel_reason: { value: 0, label: '--', }, agent_id: { value: 1, label: 'admin', }, age: 3, }, { id: 8, type: { value: 102, label: '孤老', }, name: '李请', marital_status: { value: 0, label: '--', }, phone_main: '0215567252', phone_backup: '18221274181', id_card: '350583199202224336', birth_day: '2022-01-04', tags: [], sex: { value: 1, label: '男', }, province: { value: '31', label: '上海市', }, city: { value: '3101', label: '市辖区', }, area: { value: '310117', label: '松江区', }, street: { value: '310117501', label: '松江工业区', }, committee: { value: '310117501498', label: '松江工业区虚拟社区', }, address: '泰晤士小镇', reg_address: null, remark: '', status: 1, general_info: '', medical_card: null, blood_type: { value: 'A', label: 'A', }, health_info: '', physical_condition: null, basic_info: null, allergic_drugs: null, medical_records: null, is_contract: 0, contract_imgs: [], source: { value: '', label: ' -- ', }, create_time: '2022-01-18 15:42:02', update_time: '2022-01-25 17:33:23', create_user_id: 0, update_user_id: 0, contract: { value: 1, label: '幸福久久', }, category: [], frequency: { value: '', label: ' -- ', }, service_start: null, service_end: null, service_status: { value: 1, label: '正常', }, service_cancel_reason: { value: 0, label: '--', }, agent_id: { value: 1, label: 'admin', }, age: 0, }, { id: 9, type: { value: 102, label: '孤老', }, name: '柳慢慢', marital_status: { value: 0, label: '--', }, phone_main: '0215567252', phone_backup: '18221274182', id_card: '350583199202224336', birth_day: '1945-06-05', tags: [], sex: { value: 1, label: '男', }, province: { value: '31', label: '上海市', }, city: { value: '3101', label: '市辖区', }, area: { value: '310117', label: '松江区', }, street: { value: '310117001', label: '岳阳街道', }, committee: { value: '310117001001', label: '龙潭社区居委会', }, address: '新凯城', reg_address: null, remark: '', status: 1, general_info: '', medical_card: null, blood_type: { value: 'A', label: 'A', }, health_info: '', physical_condition: null, basic_info: null, allergic_drugs: null, medical_records: null, is_contract: 0, contract_imgs: [], source: { value: '', label: ' -- ', }, create_time: '2022-01-18 19:58:02', update_time: '2022-02-16 11:18:17', create_user_id: 0, update_user_id: 0, contract: { value: 1, label: '幸福久久', }, category: [], frequency: { value: '', label: ' -- ', }, service_start: null, service_end: null, service_status: { value: 1, label: '正常', }, service_cancel_reason: { value: 0, label: '--', }, agent_id: { value: 1, label: 'admin', }, age: 76, }, { id: 10, type: { value: 102, label: '孤老', }, name: '小千', marital_status: { value: 0, label: '--', }, phone_main: '', phone_backup: '18221274181', id_card: '350583199202224336', birth_day: '2022-01-03', tags: [], sex: { value: 1, label: '男', }, province: { value: '31', label: '上海市', }, city: { value: '3101', label: '市辖区', }, area: { value: '310117', label: '松江区', }, street: { value: '310117001', label: '岳阳街道', }, committee: { value: '310117001001', label: '龙潭社区居委会', }, address: '新凯城', reg_address: null, remark: '', status: 1, general_info: '', medical_card: null, blood_type: { value: 'B', label: 'B', }, health_info: '', physical_condition: null, basic_info: null, allergic_drugs: null, medical_records: null, is_contract: 0, contract_imgs: [], source: { value: '', label: ' -- ', }, create_time: '2022-01-18 20:00:32', update_time: '2022-01-25 17:33:03', create_user_id: 0, update_user_id: 0, contract: { value: 1, label: '幸福久久', }, category: [], frequency: { value: '', label: ' -- ', }, service_start: null, service_end: null, service_status: { value: 1, label: '正常', }, service_cancel_reason: { value: 0, label: '--', }, agent_id: { value: 1, label: 'admin', }, age: 0, }, ] }
十六、完
总结,可能后续还会进行改动 , 我会尽可能的抽时间去更新它,里面的代码有写的不好的地方多多见谅,数据的处理也是被逼无奈,希望有所帮助的同学能够一键三连
-
Vue Element UI 自定义描述列表组件
2021-05-12 11:11:05} } } } </style> 使用方式 <template> <e-desc margin='0 12px' label-width='100px'> 姓名">{{info.name}}</e-desc-item> 年龄">{{ info.age }}岁</e-desc-item> 性别">{{ info.sex }}</e-desc-item> 学校">{{ ...效果图
写在前面
写后台管理经常从列表点击查看详情,展示数据信息,Element UI虽然有表格组件,但是描述组件并没有,之前团队的成员遇到这种情况都自己去写样式,写起来也麻烦,而且每个人写出来的样式也不统一,破坏了项目的整体风格。
像是Ant Design UI就有描述组件,用起来特别舒服,所以索性自己结合Element UI的el-row和el-col自己写了一个。实现哪些功能
1、每行的高度根据改行中某一列的最大高度自动撑开
2、列宽度自动补全,避免最后一列出现残缺的情况
3、支持纯文本与HTML插槽
4、支持每行几列的设置
5、支持每列宽度自定义
6、支持动态数据重绘组件设计
1、使用父子组件嵌套实现,父组件为 e-desc, 子组件为 e-desc-item 。
2、e-desc-item传递props的label 和 插槽的value
3、利用 el-row 和 el-col 来实现整体组件布局封装e-desc组件
<template> <div class="desc" :style="{margin}"> <!-- 标题 --> <h1 v-if="title" class="desc-title" v-html="title"></h1> <el-row class="desc-row" ref='elRow'> <slot/> </el-row> </div> </template> <script> export default { name: 'EDesc', // 通过provide提供给子组件 provide () { return { labelWidth: this.labelWidth, column: this.column, size: this.size } }, props: { // 标题 title: { type: String, default: '' }, // 边距 margin: { type: String, default: '0' }, // label宽度 labelWidth: { type: String, default: '120px' }, column: { // 每行显示的项目个数 type: [Number, String], default: 3 }, size: { // 大小 type: String, default: '' } }, data () { return { // 监听插槽变化 observe: new MutationObserver(this.computedSpan) } }, mounted () { this.$nextTick(() => { this.computedSpan() this.observe.observe(this.$refs.elRow.$el, { childList: true }) }) }, methods: { computedSpan () { // 筛选出子组件e-desc-item const dataSource = this.$slots.default const dataList = [] dataSource.forEach(item => { if (item.componentOptions && item.componentOptions.tag === 'e-desc-item') { dataList.push(item.componentInstance) } }) // 剩余span let leftSpan = this.column const len = dataList.length dataList.forEach((item, index) => { // 处理column与span之间的关系 // 剩余的列数小于设置的span数 const hasLeft = leftSpan <= (item.span || 1) // 当前列的下一列大于了剩余span const nextColumnSpan = (index < (len - 1)) && (dataList[index + 1].span >= leftSpan) // 是最后一行的最后一列 const isLast = index === (len - 1) if (hasLeft || nextColumnSpan || isLast) { // 满足以上条件,需要自动补全span,避免最后一列出现残缺的情况 item.selfSpan = leftSpan leftSpan = this.column } else { leftSpan -= item.span || 1 } }) } }, beforeDestroy () { this.observer.disconnect() } } </script> <style scoped lang="scss"> .desc{ .desc-title { margin-bottom: 10px; color: #333; font-weight: 700; font-size: 16px; line-height: 1.5715; } .desc-row{ display: flex; flex-wrap: wrap; border-radius: 2px; border: 1px solid #EBEEF5; border-bottom: 0; border-right: 0; width: 100%; } } </style>
封装e-desc-item组件
<template> <el-col :span="computedSpan" class="desc-item"> <div class="desc-item-content" :class="size"> <label class="desc-item-label" :style="{width: labelWidth}" v-html="label"></label> <div class="desc-item-value" v-if="$slots"> <slot/> </div> </div> </el-col> </template> <script> export default { name: 'EDescItem', inject: ['labelWidth', 'column', 'size'], props: { span: { type: [Number, String], required: false, default: 0 }, label: { type: String, required: false, default: '' } }, data () { return { // 子组件自己的span selfSpan: 0 } }, computed: { computedSpan () { // 子组件自己的span,用于父组件计算修改span if (this.selfSpan) { return 24 / this.column * this.selfSpan } else if (this.span) { // props传递的span return 24 / this.column * this.span } else { // 未传递span时,取column return 24 / this.column } } } } </script> <style scoped lang="scss"> .desc-item { border-right: 1px solid #EBEEF5; border-bottom: 1px solid #EBEEF5; .desc-item-content { display: flex; justify-content: flex-start; align-items: center; color: rgba(0,0,0,.65); font-size: 14px; line-height: 1.5; width: 100%; background-color: #fafafa; height: 100%; .desc-item-label{ border-right: 1px solid #EBEEF5; display: inline-block; padding: 12px 16px; flex-grow: 0; flex-shrink: 0; color: rgba(0, 0, 0, 0.6); font-weight: 400; font-size: 14px; line-height: 1.5; height: 100%; display: flex; align-items: center; } .desc-item-value{ background: #fff; padding: 12px 16px; flex-grow: 1; overflow: hidden; word-break: break-all; height: 100%; display: flex; align-items: center; color: #444; span{ color: #aaa; } // 空数据时展示的内容 &:empty::after { content: '--'; } } &.small { .desc-item-label, .desc-item-value { padding: 10px 14px; } } } } </style>
使用方式
<template> <e-desc margin='0 12px' label-width='100px'> <e-desc-item label="姓名">{{info.name}}</e-desc-item> <e-desc-item label="年龄">{{ info.age }}岁</e-desc-item> <e-desc-item label="性别">{{ info.sex }}</e-desc-item> <e-desc-item label="学校">{{ info.school }}</e-desc-item> <e-desc-item label="专业">{{ info.major }}</e-desc-item> <e-desc-item label="爱好">{{ info.hobby }}</e-desc-item> <e-desc-item label="手机号">{{ info.phone }}</e-desc-item> <e-desc-item label="微信">{{ info.wx }}</e-desc-item> <e-desc-item label="QQ">{{ info.qq }}</e-desc-item> <e-desc-item label="住址">{{ info.address }}</e-desc-item> <e-desc-item label="自我描述" :span='2'>{{ info.intro }}</e-desc-item> <e-desc-item label="操作" :span='3'> <template> <el-button size="small" type="primary">修改</el-button> <el-button size="small" type="danger">删除</el-button> </template> </e-desc-item> </e-desc> </template> <script> import EDesc from './e-desc' import EDescItem from './e-desc-item' export default { components: { EDesc, EDescItem }, data () { return { info: { name: 'Jerry', age: 26, sex: '男', school: '四川大学', major: '码农专业', address: '四川省成都市', hobby: '搬砖、前端、赚钱', phone: 18888888888, wx: 'Nice2cu_Hu', qq: 332983810, intro: '我是一个粉刷匠,粉刷本领强。我要把那新房子,刷得更漂亮。刷了房顶又刷墙,刷子飞舞忙。哎呀我的小鼻子,变呀变了样。我是一个粉刷匠,粉刷本领强。我要把那新房子,刷得更漂亮。刷了房顶又刷墙,刷子飞舞忙。哎呀我的小鼻子,变呀变了样。' } } } } </script>
参数说明
至此,代码就写完啦,考虑不周或者有bug的地方,还望多多留言告知我哟😁!
-
【第一个Vue上手小项目Day5】单独封装Echarts图表,后端Lambda进行数据筛选
2019-08-18 21:15:06二、Vue引入Echarts 项目路径下安装echarts(原版百度图表库)和vue-echarts(比较轻量) cnpm install echarts vue-echarts src目录下新建:echarts包,在里面新建index.js和目录myEcharts(myEcharts.js) ...目录
一、折线图+饼图
前端效果:
二、Vue挂载echarts,单独封装
- 项目路径下安装echarts(原版百度图表库)和vue-echarts(比较轻量)
cnpm install echarts vue-echarts
- src目录下新建:echarts包,在里面新建index.js和目录myEcharts(myEcharts.js)
index.js
是echarts配置,待会儿会注入到main.js全局中
这里如果是部分引入,我们以后可以这样封装配置,现在为了方便,
在后面学习中使用import echarts from 'echarts'
,忽略注释中的配置import Vue from 'vue' // import * as echarts from 'echarts/lib/echarts'; // // 引入 echarts 主模块。 // import 'echarts/lib/echarts' // // 引入柱形图 // import 'echarts/lib/chart/bar' // 引入提示框组件、标题组件、工具箱、legend组件。 // import 'echarts/lib/component/tooltip' // import 'echarts/lib/component/title' // import 'echarts/lib/component/toolbox' // import 'echarts/lib/component/legend' // // Vue.prototype.$echarts =echarts; //自定义js import myEcharts from '@/echarts/myEcharts/myEcharts.js' Vue.use(myEcharts)
main.js
,这里就可以全局使用myEcharts.js中定义的图表了import './echarts'// 引入表格
配置就算完成了!
三、静态图表
挂载两个静态图表:
因为前面在main.js中进行过配置,在vue中:- 可以
this.$chart.line1('linechart1');
直接调用,对应方法和挂载点名称就行; - 注意挂载点div必须包含容器的大小,因为echarts会自动适应容器大小,不设置就默认不显示。
XXXX.vue
挂载点:这里是两张图并列:<template> <div> <el-row :gutter="24"> <el-col :span="12"> <el-card shadow="always"> <!-- 为 ECharts 准备一个具备大小(宽高)的 DOM --> <div id="linechart1" style="float:left;width:100%;height: 300px"></div> </el-card> </el-col> <el-col :span="12"> <el-card shadow="always"> <div id="piechart2" style="float:left;width:100%;height: 300px"></div> </el-card> </el-col> </el-row> </div> </template> <script> export default { data() { return {} }, mounted() { this.$chart.line1('linechart1'); this.$chart.pie1('piechart2'); } } </script> <style scoped> </style>
具体的
myEcharts.js
静态封装:折线图&饼图/** * 各种画echarts图表的方法都封装在这里 * 注意:这里echarts没有采用按需引入的方式,只是为了方便学习 */ import echarts from 'echarts' const install = function(Vue) { Object.defineProperties(Vue.prototype, { $chart: { get() { return { //画一条简单的线 line1: function(id) { this.chart = echarts.init(document.getElementById(id)); this.chart.clear(); //释放缓存 const optionData = { title: { text: '直线:月使用率', //主标题 subtext: '纯属虚构', //副标题 x: 'left', //x轴方向对齐方式 }, xAxis: { type: 'category', data: ['Day1','Day2','Day3','Day4','Day5'],//usersName, }, yAxis: { type: 'value' }, series: [{ data: [560,720,680,760,850],//usersAge, type: 'line', smooth: true }] }; this.chart.setOption(optionData); }, //画一个饼图 pie1: function(id) { // 基于准备好的dom,初始化echarts实例 var myChart = echarts.init(document.getElementById(id)); // 绘制图表 myChart.setOption({ title: { text: '饼图:某站点用户访问来源', //主标题 subtext: '纯属虚构', //副标题 x: 'left', //x轴方向对齐方式 }, tooltip: { trigger: 'item', formatter: "{a} <br/>{b} : {c} ({d}%)" }, legend: { orient: 'vertical', bottom: 'bottom', data: ['直接访问','邮件营销','联盟广告','视频广告','搜索引擎'] }, series: [{ name: '访问来源', type: 'pie', radius: '55%', center: ['50%', '60%'], data: [{ value: 335, name: '直接访问' }, { value: 310, name: '邮件营销' }, { value: 234, name: '联盟广告' }, { value: 135, name: '视频广告' }, { value: 1548, name: '搜索引擎' } ], itemStyle: { emphasis: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } } }] }); }, } } } }) } export default { install }
静态数据封装就算完成了!!还是比较简单,接下来难点是如何提取关键信息进行动态演示!
四、动态图表
4.1 思路
后端数据往往不可能是前面静态图表数据标准的格式:一个name对应一个value;
【表格】中所需要的内容:
- 标签 -》data: [‘Day1’,‘Day2’,‘Day3’,‘Day4’,‘Day5’]
- 数据统计结果 -》data: [560,720,680,760,850],
【实际】请求结果:通过jpa查询返回数据库中所有数据集
【需求】如果我们只需要age分布图或者地域分布图,那么我们有2种方式:-
在前端封装:
ajax获取的依然是上述完整数据,单独封装每个列:const usersName = []; const usersAge = []; const usersAddress = []; this.$ajax.get('/user/aop/list').then((res) => { //axios发送get请求 const users = res.data.data; if (users != null) { for (var i = 0; i < users.length; i++) { usersName[i] = users[i].name; usersAge[i] = users[i].age; usersAddress[i] = users[i].address; } console.log(usersName); //再分类统计。。。 } else { this.$message({ type: 'error', message: '查询失败', showClose: true }) } })
假如
封装的是name姓名
,效果如下,然后在前端js中进行分类
,再把数据写入echarts的option中
。
这样的处理是可行的,但不建议在前端做过多的数据处理:
- 实际项目中,“前端” 可能不会、或者很难实现数据分类统计处理;
- 既然我们要做到前后端分离,当然也要实现功能分离。
因此,我建议在后端做好分离接口!参考方式2
-
在后端封装:
用循环方式写入新的集合中:@RequestMapping(value = "/aop/addressList", method = RequestMethod.GET) public Result aopAddressList(Exception e) { List<UserEntity> userList = userJPA.findAll(); List<String> AddressList = new ArrayList<>(); if(userList!=null){ for(UserEntity m:userList){ AddressList.add(m.getAddress()); } return ResultGenerator.genSuccessResult(AddressList); }else { return ResultGenerator.genFailResult(e.getMessage()); } }
浏览器请求:
http://localhost:8090/user/aop/addressList
,返回接口结果:{“code”:200,“data”:[“重庆”,“重庆”,“北京”,“北京”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”,“上海”],“message”:“SUCCESS”,“success”:true}
4.2 后端数据处理
现在我们请求得到了只有
地址数据集
,离我们的图表数据还差一个统计分类
:
【目标】:{“code”:200,“data”:{“address”:[“重庆”,“北京”,“上海”],“num”:[2,2,32]},“message”:“SUCCESS”,“success”:true}
需要将不同的类别进行统计分类,这里用到了
lambda表达式
(1)lambda分类原理
lambda的用法参考:通过标签分类原理将数据流分类筛选,保留符合筛选条件的结果。
@RequestMapping(value = "/aop/addressList", method = RequestMethod.GET) public Result aopAgeList(Exception e) { List<UserEntity> userList = userJPA.findAll(); List<String> AddressList = new ArrayList<>(); //List<Integer> AgeList = null; // 需要筛选的条件:从AddressList中筛选出地址为“重庆”的用户 AddressList.add("重庆"); List<UserEntity> result = null; result = userList.stream() .filter((UserEntity s) -> AddressList.contains(s.getAddress())) .collect(Collectors.toList()); // 打印原有stuList集合中的数据 System.out.println("原有userList集合中的数据"); userList.forEach((UserEntity s) -> System.out.println(s.getName() + "--->" + s.getAddress())); // 打印过滤筛选后的result结果 System.out.println("过滤筛选后的result结果"); result.forEach((UserEntity s) -> System.out.println(s.getName() + "--->" + s.getAddress())); if(userList!=null){ for(UserEntity m:userList){ AddressList.add(m.getAddress()); } return ResultGenerator.genSuccessResult(AddressList); }else { return ResultGenerator.genFailResult(e.getMessage()); } }
运行效果:通过lambda,我们可以看到,将
重庆
的结果筛选出来了,类似于关键字查询:
(2)数据筛选
先筛选地址:
@RequestMapping(value = "/aop/addressList", method = RequestMethod.GET) public Result aopAgeList(Exception e) { List<UserEntity> userList = userJPA.findAll(); List<String> AddressList = new ArrayList<>(); //List<Integer> AgeList = null; if(userList!=null){ // 需要筛选的条件: AddressList.add("重庆"); List<UserEntity> result1 = null; result1 = userList.stream() .filter((UserEntity s) -> AddressList.contains(s.getAddress())) .collect(Collectors.toList()); AddressList.clear(); AddressList.add("北京"); List<UserEntity> result2 = null; result2 = userList.stream() .filter((UserEntity s) -> AddressList.contains(s.getAddress())) .collect(Collectors.toList()); AddressList.clear(); AddressList.add("上海"); List<UserEntity> result3 = null; result3 = userList.stream() .filter((UserEntity s) -> AddressList.contains(s.getAddress())) .collect(Collectors.toList()); AddressList.clear(); if(result1.size()!=0&&result2.size()!=0&&result3.size()!=0) { AddressList.add("重庆"); AddressList.add(String.valueOf(result1.size())); AddressList.add("北京"); AddressList.add(String.valueOf(result2.size())); AddressList.add("上海"); AddressList.add(String.valueOf(result3.size())); } return ResultGenerator.genSuccessResult(AddressList); }else { return ResultGenerator.genFailResult(e.getMessage()); } }
结果如图:就是我们想要的数据啦!
{“code”:200,“data”:[“重庆”,“2”,“北京”,“2”,“上海”,“32”],“message”:“SUCCESS”,“success”:true}
(3)优化
对前面的data进行分类统计后,返回字符串格式,不美观,再对接口进行优化:
{“code”:200,“data”:{“address”:[“重庆”,“北京”,“上海”],“num”:[2,2,32]},“message”:“SUCCESS”,“success”:true}
{“code”:200,“data”:{“num”:[2,1,32,1],“age”:[19,20,21,22]},“message”:“SUCCESS”,“success”:true}最终版:
Controller完整项目地址@RequestMapping(value = "/aop/addressList", method = RequestMethod.GET) public Result aopAddressList(Exception e) { List<UserEntity> userList = userJPA.findAll(); if(userList!=null){ String[] key= {"重庆","北京","上海"};//筛选条件 ArrayList num = new ArrayList();//数据统计 for(String keyword: key){ List<String> AddressList = new ArrayList<>();//每次都会清空筛选条件 AddressList.add(keyword);// 需要筛选的条件 List<UserEntity> result = userList.stream() .filter((UserEntity s) -> AddressList.contains(s.getAddress())) .collect(Collectors.toList()); num.add(result.size());//符合条件的统计结果添加到数据统计num[]中 log.info(String.valueOf("address:"+key +"num:"+ num));//前面要加上@Slf4j标签即可使用 } JSONObject map = new JSONObject(); map.put("address", key);//存入map中 map.put("num", num); log.info(String.valueOf(map)); return ResultGenerator.genSuccessResult(map);//aop异常结果封装 }else { return ResultGenerator.genFailResult(e.getMessage()); } } @RequestMapping(value = "/aop/ageList", method = RequestMethod.GET) public Result aopAgeList(Exception e) { List<UserEntity> userList = userJPA.findAll(); if(userList!=null){ Integer[] key= {19,20,21,22}; ArrayList num = new ArrayList(); for(Integer keyword: key){ List<Integer> AgeList = new ArrayList<>(); AgeList.add(keyword);// 需要筛选的条件 List<UserEntity> result = userList.stream() .filter((UserEntity s) -> AgeList.contains(s.getAge())) .collect(Collectors.toList()); num.add(result.size()); log.info(String.valueOf("age:"+key +"num:"+ num)); } JSONObject map = new JSONObject(); map.put("age", key); map.put("num", num); log.info(String.valueOf(map)); return ResultGenerator.genSuccessResult(map); }else { return ResultGenerator.genFailResult(e.getMessage()); } }
4.3 前端调用接口
效果:
myEcharts.js
: js完整项目地址/** * 各种画echarts图表的方法都封装在这里 * 注意:这里echarts没有采用按需引入的方式,只是为了方便学习 */ import echarts from 'echarts' import axios from 'axios' const install = function(Vue) { Object.defineProperties(Vue.prototype, { $chart: { get() { return { //画一条简单的线 line1: function(id) { this.chart = echarts.init(document.getElementById(id)); this.chart.clear(); //释放缓存 var value = []; var age = []; axios.get('/user/aop/ageList').then((res) => { //axios发送get请求 if (res.data != null) { var data = res.data.data; for (var i = 0; i < data.age.length; i++) { age[i] = data.age[i]; value[i] = data.num[i]; } //console.log("age:"+age+"\t value:"+value); //绘图 var optionData = { title: { text: '直线:年龄分段', //主标题 subtext: '后台数据', //副标题 x: 'left', //x轴方向对齐方式 }, xAxis: { type: 'category', data: age, }, yAxis: { type: 'value' }, series: [{ data: value, type: 'line', smooth: true }] }; this.chart.setOption(optionData); } else { this.$message({ type: 'error', message: '查询失败', showClose: true }) } }) }, //画一个饼图 pie1: function(id) { // 基于准备好的dom,初始化echarts实例 var myChart = echarts.init(document.getElementById(id)); var value = []; var myaddress = []; var addressAndvalue =[]; axios.get('/user/aop/addressList').then((res) => { //axios发送get请求 if (res.data != null) { //console.log(res.data); var data = res.data.data; for (var i = 0; i < data.num.length; i++) { myaddress[i] = data.address[i]; value[i] = data.num[i]; addressAndvalue[i] = {"name":data.address[i],"value":data.num[i]}; } console.log("address:"+myaddress+"\t value:"+value); console.log(addressAndvalue); // 绘制图表 myChart.setOption({ title: { text: '饼图:地域分布', //主标题 subtext: '结合后台', //副标题 x: 'left', //x轴方向对齐方式 }, tooltip: { trigger: 'item', formatter: "{a} <br/>{b} : {c} ({d}%)" }, legend: { orient: 'vertical', bottom: 'bottom', data: myaddress //['直接访问','邮件营销','联盟广告','视频广告','搜索引擎'] }, series: [{ name: '地址分布', type: 'pie', radius: '55%', center: ['50%', '60%'], data: addressAndvalue, itemStyle: { emphasis: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } } }] }); } else { this.$message({ type: 'error', message: '查询失败', showClose: true }) } })//ajax }, } } } }) } export default { install }
五、总结&源码
-
更多echarts模板
参考: echarts官方示例
-
本次遇到一个低级错误
在后端java循环时:-
for(
int i = 0; i < key.length; i++
){}
怎么都跳不出循环,我很苦恼,报错400,返回结果一直为null -
for(
String keyword: key
){}
改用之后,错误解决!
-
-
源码Github
- 项目路径下安装echarts(原版百度图表库)和vue-echarts(比较轻量)
-
Vue基础:常用12个内置指令、自定义指令、过滤器
2022-05-13 13:23:111.7.2 列表过滤 从列表中筛选人员(如查找同姓的人员) 人员列表 {{p.name}}-{{p.age}}-{{p.sex}} vue监测数据代理,数据监测,原理都是用getter和setter 1.7.4 vue数据监视总结 splice(序号,长度,... -
为什么要学Vue,Vue.js是什么,开始学Vue,Vue的基础指令,自定义指令
2020-04-24 17:01:26Vue.js -
vue学习笔记之:以对学生数据的处理作为示例实现数据过滤筛选等常规处理
2019-10-16 14:34:10示例:现有一组学生数据,通过对其中的数据筛选,获取其中的年龄最大、年龄最小、和年龄在指定区间内的学生数据 学生数据: [{'name': '张三', 'sex': '男', 'age': 15}, {'name': '刘丽丽', 'sex': '女', 'age... -
基于ElementUI封装的表头带多种筛选器的表格组件
2019-01-03 15:54:101.通过vue-cli搭建项目 vue create demo-dg-table 搭建完毕后 这里推荐使用 vscode 打开工程目录,截图如下: vue-cli3.0的目录较2.0的简洁很多。vue cli3.0更多内容点击这里 2.依赖包的安装 dg-table是基于E... -
Vue高级
2021-09-12 16:18:15Vue高级 组件化 什么叫做组件化 什么叫做组件化,为什么要组件化?接下来我就针对这两个问题一一解答,所谓组件化,就是把页面拆分成多个组件,每个组件依赖的 CSS、JS、模板、图片等资源放在一起开发和维护。 因为... -
vue的基础总结(vue的非脚手架总结)
2021-11-10 18:55:08Vue详细总结1一、Vue的基础用法(不用脚手架)1.vue的data属性在html中引入vue,初始化vue并添加data,data为json数据 key:value2.vue中的插值语法和指令语法插值语法:写法:{{xxx}},xxx是js表达式,且可以直接... -
vxe-grid筛选渲染
2021-12-29 13:46:42vxe-grid筛选渲染 表格数据过多时,需要过滤数据 代码: // index.vue <template> <div style="height: 400px"> <vxe-grid ref="xGrid" v-bind="gridOptions"></vxe-grid> </div&... -
vue中的一些知识
2022-01-26 20:43:32MVVM和MVC MVVM模式: MVVM的优点: 三者的关系: MVC模式: 特点: vue中的命令符 常用的修饰符: v-model的修饰符: 键盘事件中的修饰符: 键盘修饰符 element的修饰符 (面试回答加分) vue自定义组件 vue自定义指令 ... -
Vue之store
2021-03-11 09:51:21vuex是基于vue框架的一个状态管理库。可以管理复杂应用的数据状态,比如兄弟组件的通信、多层嵌套的组件的传值等等。vuex有这么几个核心概念——State、Getter、Mutation、Action、Module 2.Store尝试 2.1 添加... -
2w多字总结的VUE学习文档
2021-11-04 17:45:59VUE学习文档 文章目录VUE学习文档回顾:总结0 目标1.前言2.认识Vue3.快速入门3.1.创建工程3.2.安装vue3.2.1.下载安装3.2.2.使用CDN3.3.vue入门案例3.3.1.HTML模板3.3.2.vue渲染3.3.3.双向绑定3.3.4.事件处理4.Vue... -
前端VUE学习总结(一)
2021-07-19 19:37:52如果在一个挂载的vue中进行声明,那么该自定义控件只能在挂载了该vue的控件内使用。 6.3父组件和子组件 包含关系,在组件中可以包含组件。包含组件的叫父组件,被包含的叫子组件。 6.3.1 父组件与子组件的数据通信 ... -
Vue.js(十) element-ui PC端组件库
2019-01-17 21:20:56另一部分组件库是原生HTML标签元素没有的,是一些比较常用的独立的功能(如:分页、进度条、加载中、树形控件等),将这些独立的常用的功能封装成一个新的组件库(自定义标签元素)。 Element UI: 基于Vue PC端的UI框架... -
Vue.js高效前端开发 • 【Ant Design of Vue框架基础】
2021-07-13 21:10:52文章目录一、Ant Design of Vue框架1、Ant Design介绍2、Ant Design of Vue安装 一、Ant Design of Vue框架 1、Ant Design介绍 随着商业化的趋势,越来越多的企业级产品对更好的用户体验有了进一步的要求。带着这样... -
初始Vue
2022-04-12 21:10:34初始Vue.js 1.1. 什么是Vue.js Vue.js 是目前最火的一个前端框架,React是最流行的一个前端框架(React除了开发网站,还可以开发手机App, Vue语法也是可以用于进行手机App开发的,需要借助于Weex) Vue.js 是前端... -
七、Vue 学习笔记
2022-04-23 21:27:35文章目录系列文章目录前言一、安装和引入Vue.js1. Vue概述2. 第一个Vue项目3. 绑定属性4. 绑定事件二、创建Vue项目1. 使用vue/cli创建Vue项目2. 项目目录结构3. 组件化开发概述4. 改写App.vue文件三、模板语法1. ... -
Vue 全套教程(二),入门 Vue 必知必会
2022-06-17 17:02:50Vue 全套教程的第二部分,本部分主要讲述监视属性、绑定样式、条件渲染(v-if 和 v-show)、列表过滤、列表排序等内容 -
Vue学习笔记
2020-03-05 18:29:03Vue.js - Day1 课程介绍 前5天: 都在学习Vue基本的语法和概念;打包工具 Webpack , Gulp 后5天: 以项目驱动教学; 什么是Vue.js Vue.js 是目前最火的一个前端框架,React是最流行的一个前端框架(React除了开发... -
vue2知识点:列表渲染(包含:v-for、key、取值范围、列表过滤、列表排序、vue监视对象或数组的数据改变...
2022-05-20 16:39:50vue2知识点:列表渲染(包含:v-for、key、取值范围、列表过滤、列表排序、vue监视对象或数组的数据改变原理、总结vue数据检测) -
基于Vue+Java实现的在线聊天APP系统设计与实现
2022-03-10 10:07:33技术栈 (1)前端 ①Vue 作为前端框架 ②vue-router 进行前端路由管理 ③webpack 开发 SPA(单页面应用) ④mint-UI 作为 UI 框架 ⑤STOMP 实现 Socket 通信的框架 ⑥axios 发送请求 ⑦sass(css 预处理器,进行 CSS ... -
vue 官方推荐的好用的三方库
2020-12-31 21:47:13很棒的Vue.js 太棒了 与Vue.js相关的精彩内容精选清单 资源资源 官方资源 外部资源 工作门户 社区 会议活动 播客 YouTube频道 官方例子 讲解 例子 图书 博客文章 培训班 纪录片 使用Vue.js的项目 开源的 商业产品 ... -
elementUI参考学习 vue运行问题 以及组件style样式设置的区别
2022-05-14 08:28:00} </style> id来找下标,防止待办事件的下标bug (点击未生效) filter 筛选出不一样的并返回需要的 分割组件vuex 在index.js导入组件(todos.js (为导出组件)) index.js import axios from 'axios' import Vue from '... -
Vue2 面试题
2022-01-13 19:31:46一、Vue 是什么? 1. Web 发展历史 ⑴. 静态页 最早的网页是没有数据库的,可以理解成就是一张可以在网络上浏览的报纸,直到CGI技术的出现通过 CGI Perl 运行一小段代码与数据库或文件系统进行交互 ⑵. 服务端... -
前端与移动开发----Vue----Vue快速入门
2021-01-20 21:19:11Vue快速入门 -
Java前端Vue-2(快速入门)
2020-09-11 18:49:52-- 1 必须要有父容器,所有内容只能写在这个父容器中,父容器一定要有id属性,id的值自定义,但是一般叫 app --> <div id="app"> <!-- 此处需要获取Vue对象中的name,{{}}两个大括号获取data中的属性的值--> <h2>{{... -
Vue学习记录
2022-05-18 09:37:44文章目录Vue学习记录初识VUE模板语法数据绑定el与data的两种写法el的写法data的两种写法理解MVVM数据代理Object.defineProperty()方法数据代理vue中的数据代理事件处理事件基本处理事件修饰符键盘事件计算属性插值...