可配置列表----移动端
最近项目界面上基本上都是差不多的,也不想做页面仔,所以就封装了一个列表(基于vant ui),只需要传入对应的 JSON 数据即可实现列表的增删改查 大致界面如下(样式比较丑,大家自行调整即可,效果图在最后面)
<comList
ref="comList"
:list-config="comListConfig"
:showType="showType"
:showTextFormat="showTextFormat"
:showHead="showHead"
:deleteFormat="deleteFormat"
:popupList="popupList"
:changePopupList="changePopupList"
showDetail
:headerImgHeight="0.3"
:footerBottom="50"
/>
// JSON 配置如下
comListConfig: { // 列表配置
selectTitle: '前端语言', // 搜索框搜索的值
selectKey: 'lang', // 搜索框搜索的值对应的接口key
reqestType: 'POST', // 列表数据请求的方式
api: 'http://localhost:8099/testList', // 接口地址
params: { // 接口默认参数
name: 'jen'
},
icon: undefined, // 先后图标,
insertPopupHeight: '60%', // 新增 修改 弹窗高度 百分百
insertRequestFn: this.insertRequestFn, // 新增接口
changeRequestFn: this.changeRequestFn // 修改接口
},
showType: { // 是否需要复选框,单选或者多选 { listStyleType: 'checkout', astrict(约束): 'MultipleChoice(单选) / multipleChoice(多选)' }
listStyleType: 'default',
astrict: 'multipleChoice'
},
showTextFormat: [ // 可以是数组 ['name','position','hireDate', 'status ]
{
key: 'name',
title: '姓名: '
},
{
transformValue: true, // 是否需要将数子转换为中文
key: 'position',
title: '职位: ',
1: '前端程序员',
2: '后端程序员',
3: '技术总监'
},
{
key: 'hireDate',
title: '入职时间: '
},
{
transformValue: true, // 是否需要将数子转换为中文
key: 'status',
title: '在职状态: ',
0: '离职',
1: '在职'
}
],
showHead: { // 列表数据头部按钮
showAdd: true,
showDel: true
},
deleteFormat: { // 删除格式
requestFn: this.httpDeleteListById, // 接口函数
state: true, // 刷新动画
deleteKey: 'id', // 刷新的key
apiType: 'formData', // 删除的传参类型
unselectedPlaceholder: '请先选择',
successPlaceholder: '操作成功',
errorPlaceholder: '操作失败'
},
popupList: [ // 新增弹窗元素
{
key: 'name', // key
required: true, // 是否必填
label: '姓名', // label
placeholder: '请输入姓名', // 提示语
comType: 'field' // 元素类型
},
{
key: 'position',
required: true, // 是否必填
label: '职位',
comType: 'comSelectBox',
refName: 'selectPosition',
interfaceObj: null, // 可以传入对应接口
selectList: [ // 简单模拟一下后端数据
{
value: 1,
text: '前端程序员'
},
{
value: 2,
text: '后端程序员'
},
{
value: 3,
text: '技术总监'
}
]
},
{
key: 'hireDate',
required: true, // 是否必填
label: '入职时间',
comType: 'comSelectTime',
refName: 'selectHireDate'
},
{
key: 'status',
required: true, // 是否必填
label: '状态',
comType: 'comSelectBox',
refName: 'selectStatus',
interfaceObj: null, // 可以传入对应接口
defaultOption: 1,
selectList: [ // 简单模拟一下后端数据
{
value: 0,
text: '离职'
},
{
value: 1,
text: '在职'
}
]
}
],
changePopupList: [ // 新增弹窗元素
{
key: 'name', // key
required: true, // 是否必填
label: '姓名', // label
placeholder: '请输入姓名', // 提示语
comType: 'field' // 元素类型
},
{
key: 'position',
required: true, // 是否必填
label: '职位',
comType: 'comSelectBox',
refName: 'selectPosition',
interfaceObj: null, // 可以传入对应接口
selectList: [ // 简单模拟一下后端数据
{
value: 1,
text: '前端程序员'
},
{
value: 2,
text: '后端程序员'
},
{
value: 3,
text: '技术总监'
}
]
},
{
key: 'hireDate',
required: true, // 是否必填
label: '入职时间',
comType: 'comSelectTime',
refName: 'selectHireDate'
},
{
key: 'status',
required: true, // 是否必填
label: '状态',
comType: 'comSelectBox',
refName: 'selectStatus',
interfaceObj: null, // 可以传入对应接口
defaultOption: 1,
selectList: [ // 简单模拟一下后端数据
{
value: 0,
text: '离职'
},
{
value: 1,
text: '在职'
}
]
}
]
接下来是具体实现过程
可配置头部 comHeader
<template>
<div class="comHeader">
<!-- 返回箭头 -->
<div v-if="routerMeta.showGoBack" class="goBackIcon" @click="goBack">
<van-icon style="font-size: 20px" name="arrow-left"/>
</div>
<div v-else></div>
<!-- 中间标题 -->
<div style="margin: 0 auto">{{ routerMeta.title }}</div>
<!-- 右边返回主页图标 -->
<div v-if="!path.includes($route.path)" style="position:absolute; right: 10px" @click="goHome">
<img src="../../assets/icon/home.png" width="25" height="25" alt="">
</div>
</div>
</template>
<script>
export default {
name: 'comHeader',
data () {
return {
path: [ // 需要过滤不需要返回主页图标的页面路由
'/login'
]
}
},
props: {
routerMeta: {
type: Object,
require: false,
default: () => {}
}
},
methods: {
goBack () {
if (this.routerMeta.showGoBack) {
if (this.routerMeta.goUrl) {
this.$router.push({
path: this.routerMeta.goUrl,
query: this.$route.query
})
} else {
this.$router.go(-1)
}
}
},
goHome () {
this.$router.push({
path: '/home'
})
}
}
}
</script>
<style lang="less" scoped>
.comHeader {
display: flex;
justify-content: space-between;
width: 100%;
height: 0.5rem;
line-height: 0.5rem;
font-size: 18px;
text-align: center;
.goBackIcon {
position: absolute;
left: 8px;
top: 7px;
}
}
</style>
可配置底部 comTabbar
<template>
<van-tabbar v-model="active">
<van-tabbar-item v-for="(item) in routerList" :to="item.path" :key="item.path">
<span>{{ item.title }}</span>
<template #icon="props">
<img :src="require(`../../assets/icon/${props.active ? item.iconList.active : item.iconList.inactive}.png`)" alt="">
</template>
</van-tabbar-item>
</van-tabbar>
</template>
<script>
export default {
name: 'comTabbar',
props: {
routerList: {
type: Array,
require: false,
default: () => {}
}
},
watch: {
'$route' (newV) {
this.active = newV.query.active + 1 > 0 ? newV.query.active : newV.meta.query.active // 排除 newV.query.active 是0的情况
}
},
created () {
this.active = this.$route.meta.query.active
},
data () {
return {
active: 0
}
}
}
</script>
路由配置
{
path: '/comListTest',
component: () => import('../views/text/comListTest'),
meta: {
showHeader: true, // 是否需要显示顶部
title: '测试列表', // 标题
showGoBack: true, // 显示返回图标
showTabbar: true, // 显示底部tabs
query: { active: 0 }, // 底部tabs 激活的位置
keepAlive: false, // 是否需要keepAlive 缓存
requireAuth: false // 是否需要鉴权
}
}
// 鉴权函数
router.beforeEach((to, from, next) => {
if (to.matched.some(r => r.meta.requireAuth)) {
if (localStorage.getItem('loginToken')) {
next()
} else {
next({
path: '/login'
})
}
} else {
if (to.path === '/') {
localStorage.clear()
next({
path: '/login'
})
}
next()
}
})
app.vue 文件配置
<template>
<div id="app">
<!-- 头部 -->
<comHeader v-if="routerMeta.showHeader" :routerMeta="routerMeta"/>
<!-- 中间部分 -->
<router-view
v-if="!$route.meta.keepAlive"
:class="{ showHeader: $route.meta.showHeader, hiddenHeader: !$route.meta.showHeader }"
/>
<keep-alive>
<router-view
v-if="$route.meta.keepAlive"
:class="{ showHeader: $route.meta.showHeader, hiddenHeader: !$route.meta.showHeader }"
/>
</keep-alive>
<!-- 底部 -->
<comTabBar v-if="routerMeta.showTabbar" :routerList="usersTabList[type]"></comTabBar>
</div>
</template>
<script>
import comHeader from './components/comHeader'
import comTabBar from './components/comTabbar'
export default {
name: 'App',
data () {
return {
type: 'test',
routerMeta: {},
usersTabList: {
test: [
{
path: '/test1',
title: '测试1',
iconList: {
active: 'classSchedule-active',
inactive: 'classSchedule-NoActive'
}
},
{
path: '/test2',
title: '测试2',
iconList: {
active: 'activate-list',
inactive: 'inactive-list'
}
},
{
path: '/test3',
title: '测试3',
iconList: {
active: 'activate-user',
inactive: 'inactive-user'
}
}
]
}
}
},
components: {
comHeader,
comTabBar
},
watch: {
'$route' (to) {
this.routerMeta = to.meta
}
}
}
</script>
<style lang="less">
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
width: 100% !important;
height: 100%;
overflow: hidden;
font-size: 16px;
background: #fff;
box-sizing: border-box;
text-align: center;
}
.showHeader {
height: calc(100% - 0.5rem);
}
.hiddenHeader {
height: 100%;
}
</style>
接下来是中间部分的实现
comSelectBox 组件
<template>
<div class="comSelectBox">
<div class="from-sex">
<van-field :border="false" :required="required" style="width: 1.1rem; font-size: 14px" :label="label">
</van-field>
<van-dropdown-menu>
<van-dropdown-item
v-model="selectValue"
@open="handleClick"
@change="handelSelect"
:options="selectList"
/>
</van-dropdown-menu>
<div v-if="showReset" class="clear" @click="reset">重置</div>
</div>
</div>
</template>
<script>
import { post } from '../../utils/axios/index'
export default {
name: 'comSelectBox',
props: {
comSelectBoxConfig: {
type: Object,
require: false,
default: () => {}
},
params: { // 参数
type: Object,
require: false,
default: () => {}
},
label: {
type: String,
require: false,
default: ''
},
defaultOption: { // 默认选择的值
type: Boolean,
require: false,
default: false
},
required: { // 必填
type: Boolean,
require: false,
default: false
},
showReset: { // 显示重置按钮
type: Boolean,
require: false,
default: false
},
selectKey: { // 需要绑定的key
type: String,
require: false,
default: ''
}
},
data () {
return {
selectValue: undefined,
selectList: [],
selectData: []
}
},
created () {
this.refresh({})
},
methods: {
handleClick () {
this.$emit('handleClickField')
},
handelSelect (id) {
// eslint-disable-next-line no-unused-vars
let data = {}
for (let i = 0, len = this.selectData.length; i < len; i++) {
if (id === this.selectData[i][this.selectKey || 'id']) {
data = this.selectData[i]
}
}
this.$emit('selectItem', data)
},
getList (params = {}) {
if (!this.comSelectBoxConfig) return
const ApiParams = Object.assign(this.comSelectBoxConfig.params || {}, this.params, params)
post(this.comSelectBoxConfig.api, ApiParams).then((res) => {
if (res.stateCode === 200 && res.result) {
this.selectData = res.data.list
if (this.selectData[0]?.serviceId) {
this.selectData = this.removeArr(this.selectData, 'serviceId')
}
for (let i = 0, len = this.selectData.length; i < len; i++) {
this.selectList.push({
text: this.selectData[i]?.[this.comSelectBoxConfig.textKey || 'name'],
value: this.selectData[i][this.selectKey || 'id']
})
}
if (this.defaultOption) { // 默认值
this.$emit('selectItem', this.selectData[0])
this.selectValue = this.selectData[0]?.[this.selectKey || 'id']
}
}
})
},
removeArr (list, key) {
const newObj = {}
const newList = list.reduce((preVal, curVal) => {
if (!newObj[curVal[key]]) {
newObj[curVal[key]] = preVal.push(curVal)
}
return preVal
}, [])
return newList
},
getItemData () {
return this.selectData.filter((item) => item.id === this.selectValue)
},
refresh (params) {
this.selectList = []
this.getList(params)
},
reset () {
this.selectValue = null
this.$emit('resetSelection')
}
}
}
</script>
<style scoped>
.from-sex {
display: flex;
}
.clear {
margin-left: 20px;
line-height: 40px;
}
>>>.van-dropdown-menu__item {
display: -webkit-box;
display: -webkit-flex;
display: flex;
-webkit-box-flex: 1;
-webkit-flex: 1;
flex: 1;
-webkit-box-align: center;
-webkit-align-items: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
justify-content: center;
min-width: 2rem;
width: 2rem;
cursor: pointer;
}
>>>.van-dropdown-menu__bar {
position: relative;
display: -webkit-box;
display: -webkit-flex;
display: flex;
height: 0.48rem;
background-color: #fff;
box-shadow: none;
text-align: left;
}
>>>.van-dropdown-menu__title {
position: absolute;
left: 0;
box-sizing: border-box;
max-width: 100%;
padding: 0 0.08rem;
color: #323233;
font-size: 0.15rem;
line-height: 0.22rem;
}
</style>
comSelectTime 组件
<template>
<div class="comSelectTime">
<van-field
v-model="selectValue"
:name="label"
:label="label"
:required='required'
@click="showSelectTime = true"
:placeholder="'请选择' + label"
style="font-size: 14px"
readonly
/>
<!-- 弹出层 -->
<van-popup v-model="showSelectTime" closeable position="bottom" :style="{ height: '60%' }">
<van-datetime-picker
v-if="type !== 'time'"
v-model="currentDate"
:type="type"
:title="label"
:minDate='minDate'
@confirm='confirm'
@cancel='showSelectTime = false'
/>
<van-datetime-picker
v-else
v-model="currentDate"
:type="type"
:title="label"
@confirm='confirm'
@cancel='showSelectTime = false'
/>
</van-popup>
</div>
</template>
<script>
export default {
name: 'comSelectTime',
props: {
label: {
type: String,
required: false,
default: '选择时间'
},
type: {
type: String,
required: false,
default: 'datetime'
},
required: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
selectValue: '',
showSelectTime: false,
currentDate: '',
minDate: new Date(new Date().getFullYear(), new Date().getMonth(), 1)
}
},
methods: {
confirm (value) {
if (this.type === 'time') this.selectValue = value + ':00'
if (this.type === 'datetime') this.selectValue = this.util.TimeFormat(value)
this.showSelectTime = false
}
}
}
</script>
<style scoped>
>>>.van-picker__toolbar {
display: -webkit-box;
display: -webkit-flex;
display: flex;
-webkit-box-align: center;
-webkit-align-items: center;
align-items: center;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
justify-content: space-between;
height: 0.44rem;
margin-top: 40px;
}
</style>
批量删除方法
const requestApi = (require, id) => {
return new Promise((resolve, reject) => {
require(id).then((res) => {
if (res.stateCode === 200 && res.result) {
resolve(res)
} else {
// eslint-disable-next-line prefer-promise-reject-errors
reject(false)
}
}, err => {
reject(err)
})
})
}
export const deleteList = (list, requireApi, successCallback, errCallback, deleteKey, apiType) => {
const allPromise = []
const key = deleteKey || 'id'
const formData = new FormData()
list.forEach((item) => {
if (apiType === 'formData') {
formData.append(deleteKey, item[key])
} else {
allPromise.push(requestApi(requireApi, item[key]))
}
})
if (apiType === 'formData') {
allPromise.push(requestApi(requireApi, formData))
}
return Promise.all(allPromise).then(res => {
successCallback && successCallback()
}).catch(() => {
errCallback && errCallback()
})
}
axios 文件夹 中 axios.js
import axios from 'axios'
import vm from '../../main'
import { baseApi } from '../../config' // 接口地址
import router from '../../router'
import { Toast } from 'vant'
// 可以封装错误日志上传 这次忽略
class HttpRequest {
constructor () {
this.baseUrl = baseApi // 接口地址
this.queue = {}
}
getInsideConfig () {
const config = {
baseURL: this.baseUrl,
headers: {
Authorization: this.getToken()
}
}
return config
}
getToken = () => {
return 'Basic ' + require('js-base64').Base64.encode(':' + sessionStorage.getItem('loginToken'))
}
destroy (url) {
delete this.queue[url]
if (!Object.keys(this.queue).length) vm.$loading.hide()
}
interceptors (instance, url) {
// 请求拦截
instance.interceptors.request.use(config => {
// 全局添加loading 判断header里面是否有 showLoading 属性
if (config.headers.showLoading) {
vm.$loading.show()
}
delete config.headers.showLoading
this.queue[url] = true
return config
}, error => {
return Promise.reject(error)
})
// 响应拦截
instance.interceptors.response.use(res => {
return res.data
}, error => { // 看是否需要错误日志上传
this.destroy(url)
// 登录失效
if (error.response.status) {
switch (error.response.status) {
// 401: 未登录
// 未登录则跳转登录页面,并携带当前页面的路径
// 在登录成功后返回当前页面,这一步需要在登录页操作。
case 401:
Toast.fail(error.response.data.message)
router.replace({
path: '/login'
})
break
// 403 token过期
// 登录过期对用户进行提示
// 清除本地token和清空vuex中token对象
// 跳转登录页面
case 403:
Toast.fail('登录过期,请重新登录')
// 清除token
localStorage.clear()
// 跳转登录页面,并将要浏览的页面fullPath传过去,登录成功后跳转需要访问的页面
setTimeout(() => {
router.replace({
path: '/login'
})
}, 1000)
break
// 404请求不存在
case 404:
Toast.fail('网络请求不存在')
break
// 其他错误,直接抛出错误提示
default:
Toast.fail(error.response.data.message)
}
}
})
}
request (option) {
const instance = axios.create()
option = Object.assign(this.getInsideConfig(), option)
this.interceptors(instance, option.url)
return instance(option)
}
}
export default new HttpRequest()
axios 文件夹 中 index.js
import HttpRequest from './ajax'
// post 请求
export const get = (url, params, showLoading = false) => {
return new Promise((resolve, reject) => {
HttpRequest.request({
url: url,
method: 'GET',
data: params,
contentType: 'application/json; charset=utf-8'
}).then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
}
export const post = (url, data, showLoading = false) => {
return new Promise((resolve, reject) => {
HttpRequest.request({
showLoading,
url: url,
method: 'POST',
data: data,
contentType: 'application/json; charset=utf-8'
}).then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
}
export const getBlob = (url, params) => {
return new Promise((resolve, reject) => {
HttpRequest.request({
url: url,
method: 'GET',
data: params,
contentType: 'application/json; charset=utf-8',
responseType: 'blob'
}).then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
}
export const getPostBlob = (url, params) => {
return new Promise((resolve, reject) => {
HttpRequest.request({
url: url,
method: 'POST',
data: params,
contentType: 'application/json; charset=utf-8',
responseType: 'blob'
}).then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
}
export const getArrayBuffer = (url, params) => {
return new Promise((resolve, reject) => {
HttpRequest.request({
url: url,
method: 'get',
data: params,
contentType: 'application/json; charset=utf-8',
responseType: 'arraybuffer'
}).then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
}
核心组件 comList
<template>
<div class="comPullRefresh" :style="{ height: pullRefreshHeight + 'rem'}">
<van-search
v-model="selectValue"
show-action
:placeholder="'请输入' + (listConfig.selectTitle || '关键字') "
@search="onSearch"
>
<template #action>
<div @click="onSearch">搜索</div>
</template>
</van-search>
<div class="comPullRefresh_head" v-if="showHead">
<div
v-if="showHead.showAdd"
@click="handleAdd"
>
新增
<img
style="position: relative; top: -1px"
src="../../assets/icon/addBtn.png"
@click="handleAdd"
width="15"
> </div>
<div v-else></div>
<div v-if="showHead.showDel" @click="schemaTranslation">管理</div>
<div v-else></div>
</div>
<div class="comPullRefresh_List">
<van-pull-refresh
v-model="isLoading"
:style="{ height: pullRefreshHeight + 'rem', overflowY: 'auto' }"
:head-height="40"
@refresh="onRefresh"
>
<van-list
v-model="isLoading"
:finished="finished"
:offset="1"
:immediate-check="false"
:error.sync="error"
finished-text="已全部加载完成"
error-text="请求失败,点击重新加载"
@load="onLoadList"
v-if="RefreshState"
>
<!-- 单选或者多选操作列表 -->
<van-checkbox-group
v-if="showType.listStyleType === 'checkout'"
@change="changeCheckoutItem"
v-model="result"
:max="maxSelect"
class="checkoutList"
ref="checkboxGroup"
>
<van-checkbox
:name="index" v-for="(item, index) in pullRefreshList"
:key="item.id"
style="padding: 0 10px"
>
<div class="list-item">
<div class="img-box" v-if="showHeaderImg">
<img
v-if="showHeaderImg.key"
:src="item[showHeaderImg.key]"
:style="{ top: headerImgHeight + 'rem' }"
class="item-img"
alt=""
>
<img
v-else :src="require(`../../assets/icon/${listConfig.icon || 'sericveIcon'}.png`)"
:style="{ top: headerImgHeight + 'rem' }"
class="item-img"
alt=""
>
</div>
<div @click.stop="clickCheckoutListItem(item)" style="position:relative; left: 10px; padding: 0.1rem 0; text-align: left">
<div
class="hiddenText"
:style="{ width: (showHeaderImg && showDetail) ? '2.1rem' : '2.4rem' }"
v-for="(formatItem) in showTextFormat" :key="formatItem.key"
>
{{ formatItem.title }}
<span
v-if="
Object.prototype.toString.call(formatItem) === '[object Object]' &&
(formatItem.key !== 'classesName' &&
!formatItem.transformValue)"
>
{{ formatItem.interior ? (item[formatItem.key] ? item[formatItem.key][formatItem.interior] : item[formatItem.key]) : item[formatItem.key] }}
</span>
<span v-else-if="formatItem.key === 'classesName'"> <!-- 假设需要显示一个数组里面的内容 这里可以做进一步可以配置化 -->
{{ item['countDownWithClass'].length ?
item['countDownWithClass'].reduce((pre, cur) => { return pre += cur.classesName + ' / ' }, '') :
item[formatItem.key]
}}
</span>
<span v-else-if="formatItem.transformValue">
{{ formatItem[item[formatItem.key]] }}
</span>
<span v-else>{{ item[formatItem] }} </span>
</div>
</div>
<van-divider />
<div v-if="showDetail" style="width: 0.8rem; font-size: 14px; padding-top: 15px">
<span @click.stop="handleClickDetails(item)">
详情
<van-icon name="arrow" style="position: relative; top: 2px" />
</span>
</div>
</div>
</van-checkbox>
</van-checkbox-group>
<!-- 默认 -->
<div v-if="showType.listStyleType === 'default'" class="defaultList">
<div v-for="(item, index) in pullRefreshList"
@click="defaultChangeItem(item)"
style="padding: 0 10px"
class="list-item"
:key="index"
>
<div class="img-box" v-if="showHeaderImg">
<img
v-if="showHeaderImg.key"
:src="item[showHeaderImg.key]"
:style="{ top: headerImgHeight + 'rem' }"
class="item-img"
alt=""
>
<img
v-else :src="require(`../../assets/icon/${listConfig.icon || 'sericveIcon'}.png`)"
:style="{ top: headerImgHeight + 'rem' }"
class="item-img"
alt=""
>
</div>
<div style="position:relative; left: 4px; padding: 0.1rem 0; text-align: left">
<div v-if="Object.prototype.toString.call(showTextFormat[0]) === '[object Object]'">
<p
class="hiddenText"
:style="{ width: (showHeaderImg && showDetail) ? '2.1rem' : '2.6rem' }"
v-for="(formatItem) in showTextFormat"
:key="formatItem.key"
>
{{ formatItem.title }}
<span v-if="!formatItem.transformValue && formatItem.key !== 'classesName'">
{{
formatItem.interior ?
(item[formatItem.key] ? item[formatItem.key][formatItem.interior] : item[formatItem.key]) :
item[formatItem.key]
}}
</span>
<span v-if="formatItem.key === 'classesName'">
{{ item['countDownWithClass'].length ?
item['countDownWithClass'].reduce((pre, cur) => { return pre += cur.classesName + ' / ' }, '') :
item[formatItem.key]
}}
</span>
<span v-if="formatItem.transformValue">
{{ formatItem[item[formatItem.key]] }}
</span>
<span v-else>{{ item[formatItem] }} </span>
</p>
</div>
<div v-else class="hiddenText" v-for="(key) in showTextFormat" :key="key">{{ item[key] }}</div>
</div>
<van-divider />
<div v-if="showDetail" style="width: 0.8rem; font-size: 14px; padding-top: 15px">
<span @click.stop="handleClickDetails(item)">
详情
<van-icon name="arrow" style="position: relative; top: 2px" />
</span>
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
<div
v-if="showHead.showDel"
v-show="showType.listStyleType === 'checkout'"
class="comPullRefresh_bottom"
:style="{ bottom: footerBottom + 'px' }"
>
<van-radio-group v-model="isCheckAll" style="margin-top: 11px">
<van-radio v-if="!isCheckAll" :name="true">全选</van-radio>
<van-radio v-else :name="false">取消全选</van-radio>
</van-radio-group>
<slot name="button"> <van-button round @click="selectDelete" type="danger">删除</van-button> </slot>
</div>
<!-- 新增修改弹窗 -->
<van-popup
v-model="showPopup"
closeable
@close="clearData('insert')"
position="bottom"
:style="{ height: listConfig.insertPopupHeight }"
>
<div v-for="(item, index) in popupList" :key="index">
<comSelectBox
v-if="item.comType === 'comSelectBox' && item.interfaceObj"
:required="item.required"
:label="item.label"
:ref="item.refName"
:interface-obj="item.interfaceObj"
:key="item.refName"
/>
<comSelectBox
v-else-if="item.comType === 'comSelectBox' && item.selectList"
:required="item.required"
:label="item.label"
:ref="item.refName"
:key="item.refName"
/>
<comSelectTime
v-else-if="item.comType === 'comSelectTime'"
:type="item.type"
:required="item.required"
:ref="item.refName"
:key="item.refName"
/>
<van-field
v-else-if="item.comType === 'field'"
v-model="fieldParams[item.key]"
:required="item.required"
:name="item.label"
:label="item.label"
:placeholder="item.placeholder"
:key="item.key"
/>
<van-field
v-else-if="item.comType === 'textarea'"
v-model="fieldParams[item.key]"
rows="2"
autosize
:label="item.label"
type="textarea"
:placeholder="item.placeholder"
style="font-size: 14px"
:required="item.required"
:key="item.key"
/>
<div style="position: fixed; bottom: 10px;padding: 0 1rem; width: 100%;">
<van-button style="width: 100%" @click="insertData" round type="info">新增</van-button>
</div>
</div>
</van-popup>
<!-- 修改弹窗 -->
<van-popup
v-model="showUpdatePopup"
closeable
position="bottom"
@close="clearData('change')"
:style="{ height: listConfig.insertPopupHeight }"
>
<div v-for="(item, index) in changePopupList" :key="index">
<comSelectBox
v-if="item.comType === 'comSelectBox' && item.interfaceObj"
:required="item.required"
:label="item.label"
:ref="'change' + item.refName"
:comSelectBoxConfig="item.interfaceObj"
:params="item.params"
:selectKey="item.selectKey"
:key="'change' + item.refName"
/>
<comSelectBox
v-else-if="item.comType === 'comSelectBox' && item.selectList"
:required="item.required"
:label="item.label"
:ref="'change' + item.refName"
:params="item.params"
:key="'change' + item.refName"
/>
<comSelectTime
v-else-if="item.comType === 'comSelectTime'"
:type="item.type"
:required="item.required"
:ref="'change' + item.refName"
:key="'change' + item.refName"
/>
<van-field
v-else-if="item.comType === 'field'"
v-model="changeFieldParams[item.key]"
:required="item.required"
:name="item.label"
:label="item.label"
:placeholder="item.placeholder"
:key="'change' + item.key"
/>
<van-field
v-else-if="item.comType === 'textarea'"
v-model="changeFieldParams[item.key]"
rows="2"
autosize
:label="item.label"
type="textarea"
:placeholder="item.placeholder"
style="font-size: 14px"
:required="item.required"
:key="'change' + item.key"
/>
<div style="position: fixed; bottom: 10px;padding: 0 1rem; width: 100%;">
<van-button style="width: 100%" @click="changeData" round type="info">修改</van-button>
</div>
</div>
</van-popup>
</div>
</template>
<script>
import { post, get } from '../../utils/axios'
import config from '../../config/index'
import { deleteList as batchDel } from '../../utils/batchRemove'
import comSelectBox from '../comSelectBox/comSelectBox'
import comSelectTime from '../comSelectTime/comSelectTime'
export default {
name: 'comList',
components: {
comSelectBox,
comSelectTime
},
props: {
listConfig: { // 接口参数 api 接口地址 请求类型 (GET POST) params 接口参数 interfaceObj
type: Object,
require: false,
default: () => {}
},
popupList: {
type: Array,
require: true
},
changePopupList: {
type: Array,
require: true
},
deleteFormat: { // 删除的格式
type: Object,
require: false,
default: () => {}
},
params: { // 参数
type: Object,
require: false,
default: () => {}
},
showType: { // 是否需要复选框,单选或者多选 { type: 'checkout', type: 'MultipleChoice(单选) / multipleChoice(多选)' }
type: Object,
require: false,
default: () => {}
},
showTextFormat: { // 渲染的数据key ['keyName1', 'keyName2, ...] 也可以传对象
type: Array,
require: false,
default: () => []
},
headerImgHeight: {
type: Number,
require: false,
default: 0.2
},
showHeaderImg: {
type: Boolean,
require: false,
default: true
},
showDetail: {
type: Boolean,
require: false,
default: false
},
showHead: {
type: Object,
require: false,
default: () => {}
},
pullRefreshHeight: {
type: Number,
require: false,
default: 5.5
},
footerBottom: {
type: Number,
require: false,
default: 0
}
},
watch: {
isCheckAll (newV) {
if (newV) {
this.unCheckAll()
} else {
this.checkAll()
}
}
},
data () {
return {
baseUrl: config.baseUrl,
pullRefreshList: [],
selectData: [],
delList: [],
fieldParams: {},
changeFieldParams: {},
showUpdatePopup: false,
showPopup: false,
isLoading: false,
finished: false,
error: false,
RefreshState: true,
result: [],
pageSize: 10,
pageNum: 1,
maxSelect: 0,
isCheckAll: false,
selectValue: ''
}
},
methods: {
insertData () {
for (let i = 0, len = this.popupList.length; i < len; i++) {
const item = this.popupList[i]
if (item.required) { // 校验是否必填选项
if (item.comType !== 'field' && item.comType !== 'textarea') { // 区分是选择器组件还是输入框类型
if (this.$refs[item.refName][0].selectValue === undefined) return this.$toast.fail(item.errPlaceholder || '请选择' + item.label)
} else {
if (!this.fieldParams[item.key]?.trim()) return this.$toast.fail(item.errPlaceholder || item.placeholder)
}
}
}
// 处理参数
const params = {
...this.fieldParams
}
this.popupList.forEach((item) => {
if (item.comType !== 'field' && item.comType !== 'textarea') {
params[item.key] = this.$refs[item.refName][0].selectValue
}
})
this.listConfig.insertRequestFn(params).then((res) => {
if (res.stateCode === 200 && res.result) {
this.$toast.success('操作成功')
this.onRefresh() // 刷新列表
// 清除数据
this.clearData('insert')
} else {
this.$toast.fail(res.message)
}
})
},
changeData () {
for (let i = 0, len = this.changePopupList.length; i < len; i++) {
const item = this.changePopupList[i]
if (item.required) { // 校验是否必填选项
if (item.comType !== 'field' && item.comType !== 'textarea') { // 区分是选择器组件还是输入框类型
if (!this.$refs['change' + item.refName][0].selectValue === undefined) return this.$toast.fail(item.errPlaceholder || '请选择' + item.label)
} else {
if (!this.changeFieldParams[item.key]?.trim()) return this.$toast.fail(item.errPlaceholder || item.placeholder)
}
}
}
// 处理参数
const params = {
...this.changeFieldParams
}
this.changePopupList.forEach((item) => {
if (item.comType !== 'field' && item.comType !== 'textarea') {
params[item.key] = this.$refs['change' + item.refName][0].selectValue
}
})
this.listConfig.changeRequestFn(params).then((res) => {
if (res.stateCode === 200 && res.result) {
this.$toast.success('操作成功')
this.onRefresh() // 刷新列表
// 清除数据
this.clearData('change')
} else {
this.$toast.fail(res.message)
}
})
},
clearData (type) {
// 清除数据
if (type === 'insert') {
for (const key in this.fieldParams) this.fieldParams[key] = ''
this.popupList.forEach((item) => {
if (item.comType !== 'field' && item.comType !== 'textarea') {
this.$refs[item.refName][0].selectValue = undefined
}
})
} else {
for (const key in this.changeFieldParams) this.changeFieldParams[key] = ''
this.changePopupList.forEach((item) => {
if (item.comType !== 'field' && item.comType !== 'textarea') {
this.$refs['change' + item.refName][0].selectValue = undefined
}
})
}
},
onLoadList () {
this.pageNum++
this.getList(false)
this.isLoading = false
},
onRefresh (params) {
this.result = []
this.pageNum = 1
this.getList(true, params)
this.isLoading = false
},
onSearch () {
if (this.selectValue) this.onRefresh({ [this.listConfig.selectKey]: this.selectValue })
else this.onRefresh()
},
getList (isRefresh, params) {
this.$loading.show()
new Promise((resolve, reject) => {
if (this.listConfig.reqestType === 'POST') {
resolve(post)
} else {
resolve(get)
}
}).then((request) => {
request(this.listConfig.api, {
...this.listConfig.params, // 请求参数
...params,
pageSize: this.pageSize,
pageNum: this.pageNum
}).then((res) => {
if (isRefresh) {
this.pullRefreshList = res.data.list
} else {
this.pullRefreshList = this.pullRefreshList.concat(res.data.list)
}
// 判断是否单选
if (this.showType.astrict === 'pluralChoice') { // 多选
this.maxSelect = 0 // 允许多选
} else {
this.maxSelect = 1 // 单选
}
if (res.data.list.length < this.pageSize) {
this.finished = true
}
})
})
this.$loading.hide()
},
changeCheckoutItem (indexArr) {
this.delList = []
indexArr.forEach((index) => {
this.delList.push(this.pullRefreshList[index])
})
// this.$emit('changeCheckoutItem', dataArr)
},
defaultChangeItem (data) {
this.$emit('defaultChangeItem', data)
},
clickCheckoutListItem (data) {
this.selectData = data
this.$emit('clickCheckoutListItem', data)
},
handleClickDetails (data) { // 点击详情修改信息
if (this.changePopupList.length) {
this.showUpdatePopup = true
this.$nextTick(() => {
this.changePopupList.forEach((item) => {
if (item.comType === 'comSelectBox') {
if (!this.$refs['change' + item.refName][0].selectList.length) this.$refs['change' + item.refName][0].selectList = item.selectList
this.$refs['change' + item.refName][0].selectValue = data[item.key]
} else if (item.comType === 'comSelectTime') {
this.$refs['change' + item.refName][0].selectValue = data[item.key]
} else {
this.$set(this.changeFieldParams, [item.key], data[item.key])
}
})
})
} else {
this.$emit('clickListItemDetail', data)
}
},
handleAdd () {
if (this.popupList.length) {
this.showPopup = true
this.$nextTick(() => {
this.popupList.forEach((item) => {
if (item.comType === 'comSelectBox' && !item.interfaceObj && !this.$refs[item.refName]?.selectList?.length) {
this.$refs[item.refName][0].selectList = item.selectList
// ?? 只会判断 null undefined
if (item.selectDefaultValue ?? undefined) this.$refs[item.refName][0].selectValue = item
}
})
})
} else { // 总会有些特殊的界面,所以说还是需要说提供别的解决方案
this.$emit('handleAdd')
}
},
schemaTranslation () { // 转换模式
if (this.showType.listStyleType === 'checkout') {
this.$refs.checkboxGroup.toggleAll(false)
this.showType.listStyleType = 'default'
} else {
this.showType.listStyleType = 'checkout'
}
// this.$emit('clickMore')
},
selectDelete () {
const defaultDelFormat = {
requestFn: () => {},
state: true, // 刷新动画
deleteKey: 'id',
apiType: undefined,
unselectedPlaceholder: '请先选择',
successPlaceholder: '操作成功',
errorPlaceholder: '操作失败'
}
if (this.delList.length === 0) return this.$toast.fail(this.deleteFormat.unselectedPlaceholder || defaultDelFormat.unselectedPlaceholder)
batchDel(
this.delList,
this.deleteFormat.requestFn,
() => { this.$toast.success(this.deleteFormat.successPlaceholder || defaultDelFormat.successPlaceholder); this.onRefresh(this.deleteFormat?.state || true) },
() => { this.$toast.fail(this.deleteFormat.errorPlaceholder || defaultDelFormat.errorPlaceholder) },
this.deleteFormat.deleteKey || defaultDelFormat.deleteKey,
this.deleteFormat.apiType || defaultDelFormat.apiType
)
},
checkAll () { // 全选
this.$refs.checkboxGroup.toggleAll(false)
},
unCheckAll () { // 取消全选
this.$refs.checkboxGroup.toggleAll()
}
}
}
</script>
<style lang="less" scoped>
.comPullRefresh {
width: 100%;
.comPullRefresh_head {
padding: 5px 12px 5px 12px;
display: flex;
justify-content: space-between;
}
.comPullRefresh_List {
.checkoutList {
.list-item {
display: flex;
margin-bottom: 0.03rem;
background: #FFFFFF;
.img-box {
position: relative;
width: 0.4rem;
.item-img {
display: block;
position: absolute;
border-radius: 50%;
top: 0.2rem;
width: 0.4rem;
height: 0.4rem;
}
}
.hiddenText {
width: 2.6rem;
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
height: 0.2rem;
color: #333333;
}
}
}
}
.defaultList {
.list-item {
display: flex;
margin-bottom: 0.03rem;
min-height: 0.8rem;
background: #FFFFFF;
.img-box {
position: relative;
width: 0.7rem;
.item-img {
display: block;
position: absolute;
top: 0.2rem;
left: 16px;
width: 0.4rem;
height: 0.4rem;
}
}
}
}
.comPullRefresh_bottom {
position: fixed;
bottom: 0px;
padding: 5px 12px;
width: 100%;
display: flex;
justify-content: space-between;
background: #FFFFFF;
}
.hiddenText {
width: 2.6rem;
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
height: 0.2rem;
color: #333333;
}
}
/deep/ .van-list {
padding-bottom: 100px;
}
</style>
可配置组件使用方式 comListTest
<template>
<comList
ref="comList"
:list-config="comListConfig"
:showType="showType"
:showTextFormat="showTextFormat"
:showHead="showHead"
:deleteFormat="deleteFormat"
:popupList="popupList"
:changePopupList="changePopupList"
showDetail
:headerImgHeight="0.3"
:footerBottom="50"
/>
</template>
<script>
import comList from '../../components/comPullRefresh/comList'
import { post } from '../../utils/axios'
export default {
name: 'comListTest',
components: {
comList
},
data () {
return {
comListConfig: { // 列表配置
selectTitle: '前端语言', // 搜索框搜索的值
selectKey: 'lang', // 搜索框搜索的值对应的接口key
reqestType: 'POST', // 列表数据请求的方式
api: 'http://localhost:8099/testList', // 接口地址
params: { // 接口默认参数
name: 'jen'
},
icon: undefined, // 先后图标,
insertPopupHeight: '60%', // 新增 修改 弹窗高度 百分百
insertRequestFn: this.insertRequestFn, // 新增接口
changeRequestFn: this.changeRequestFn // 修改接口
},
showType: { // 是否需要复选框,单选或者多选 { listStyleType: 'checkout', astrict(约束): 'MultipleChoice(单选) / multipleChoice(多选)' }
listStyleType: 'default',
astrict: 'multipleChoice'
},
showTextFormat: [ // 可以是数组 ['name','position','hireDate', 'status ]
{
key: 'name',
title: '姓名: '
},
{
transformValue: true, // 是否需要将数子转换为中文
key: 'position',
title: '职位: ',
1: '前端程序员',
2: '后端程序员',
3: '技术总监'
},
{
key: 'hireDate',
title: '入职时间: '
},
{
transformValue: true, // 是否需要将数子转换为中文
key: 'status',
title: '在职状态: ',
0: '离职',
1: '在职'
}
],
showHead: { // 列表数据头部按钮
showAdd: true,
showDel: true
},
deleteFormat: { // 删除格式
requestFn: this.httpDeleteListById, // 接口函数
state: true, // 刷新动画
deleteKey: 'id', // 刷新的key
apiType: 'formData', // 删除的传参类型
unselectedPlaceholder: '请先选择',
successPlaceholder: '操作成功',
errorPlaceholder: '操作失败'
},
popupList: [ // 新增弹窗元素
{
key: 'name', // key
required: true, // 是否必填
label: '姓名', // label
placeholder: '请输入姓名', // 提示语
comType: 'field' // 元素类型
},
{
key: 'position',
required: true, // 是否必填
label: '职位',
comType: 'comSelectBox',
refName: 'selectPosition',
interfaceObj: null, // 可以传入对应接口
selectList: [ // 简单模拟一下后端数据
{
value: 1,
text: '前端程序员'
},
{
value: 2,
text: '后端程序员'
},
{
value: 3,
text: '技术总监'
}
]
},
{
key: 'hireDate',
required: true, // 是否必填
label: '入职时间',
comType: 'comSelectTime',
refName: 'selectHireDate'
},
{
key: 'status',
required: true, // 是否必填
label: '状态',
comType: 'comSelectBox',
refName: 'selectStatus',
interfaceObj: null, // 可以传入对应接口
defaultOption: 1,
selectList: [ // 简单模拟一下后端数据
{
value: 0,
text: '离职'
},
{
value: 1,
text: '在职'
}
]
}
],
changePopupList: [ // 新增弹窗元素
{
key: 'name', // key
required: true, // 是否必填
label: '姓名', // label
placeholder: '请输入姓名', // 提示语
comType: 'field' // 元素类型
},
{
key: 'position',
required: true, // 是否必填
label: '职位',
comType: 'comSelectBox',
refName: 'selectPosition',
interfaceObj: null, // 可以传入对应接口
selectList: [ // 简单模拟一下后端数据
{
value: 1,
text: '前端程序员'
},
{
value: 2,
text: '后端程序员'
},
{
value: 3,
text: '技术总监'
}
]
},
{
key: 'hireDate',
required: true, // 是否必填
label: '入职时间',
comType: 'comSelectTime',
refName: 'selectHireDate'
},
{
key: 'status',
required: true, // 是否必填
label: '状态',
comType: 'comSelectBox',
refName: 'selectStatus',
interfaceObj: null, // 可以传入对应接口
defaultOption: 1,
selectList: [ // 简单模拟一下后端数据
{
value: 0,
text: '离职'
},
{
value: 1,
text: '在职'
}
]
}
]
}
},
mounted () {
this.$refs.comList.onRefresh() // 用的是本地接口的, 所以是没数据的,所以这里收到添加几条数据方便测试
this.$refs.comList.pullRefreshList = [
{
id: 1,
name: 'jen',
position: 1,
hireDate: '2020-10-26',
status: 0 // 0: 已离职 1: 在职
},
{
id: 2,
name: '张三',
position: 1,
hireDate: '2020-10-27',
status: 1 // 0: 已离职 1: 在职
},
{
id: 3,
name: '李四',
position: 2,
hireDate: '2020-10-27',
status: 1 // 0: 已离职 1: 在职
},
{
id: 4,
name: '王五',
position: 2,
hireDate: '2020-10-27',
status: 1 // 0: 已离职 1: 在职
}
]
},
methods: {
httpDeleteListById (params) {
const result = post('http://localhost:8099/comListDel/byId', params)
return result
},
insertRequestFn (params) {
const result = post('http://localhost:8099/comListDel/insertItem', params)
return result
},
changeRequestFn (params) {
const result = post('http://localhost:8099/comListDel/updataItem', params)
return result
}
}
}
</script>
<style scoped>
</style>
效果展示
删除
增加
修改
可能有些地方有点bug.....新人多多担待