可配置列表----移动端

435 阅读3分钟

可配置列表----移动端

最近项目界面上基本上都是差不多的,也不想做页面仔,所以就封装了一个列表(基于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.....新人多多担待