소스 검색

feat: 组件复用及兼容性处理

zhangyuhan 1 년 전
부모
커밋
750d6f36dd

+ 71 - 0
src/components/common/SpecCard.vue

@@ -0,0 +1,71 @@
+<template>
+  <div
+    class="spec-card"
+    :class="[mark, active ? 'active' : '']"
+    @click="clickCard"
+  >
+    <slot name="default"></slot>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'spec-card-active',
+  props: {
+    active: {
+      type: Boolean,
+      default: false
+    },
+    mark: {
+      type: String, // ''/gold/deep
+      default: ''
+    }
+  },
+  methods: {
+    clickCard() {
+      this.$emit('onClick')
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+.spec-card {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px 0;
+  background-color: #f5f6f7;
+  border-radius: 8px;
+  border: 2px solid transparent;
+  cursor: pointer;
+  &.active {
+    border-color: $color_main;
+    background-color: #fff;
+    &::after {
+      content: '';
+      position: absolute;
+      right: -2px;
+      bottom: -2px;
+      width: 22px;
+      height: 22px;
+      background-image: url(../../assets/images/icon/icon-button-mark.png);
+      background-repeat: no-repeat;
+      background-size: contain;
+    }
+    &.deep {
+      &::after {
+        background-image: url(../../assets/images/icon/icon-button-mark-deep.png);
+      }
+    }
+    &.gold {
+      border-color: #f1d090;
+      &::after {
+        background-image: url(../../assets/images/icon/icon-button-mark-gold.png);
+      }
+    }
+  }
+}
+</style>

+ 271 - 0
src/components/create-order/SpecList.vue

@@ -0,0 +1,271 @@
+<template>
+  <div class="spec-list-module">
+    <div class="spec-list bg-white">
+      <div class="spec-list-hd">
+        <slot name="header">{{ title }}</slot>
+      </div>
+      <div class="spec-list-content" :class="{ mtb10: hasTip }">
+        <slot name="content">
+          <SpecCard
+            v-for="spec in list"
+            :key="spec.id"
+            :mark="spec.markType"
+            :active="calcActive(spec)"
+            @onClick="clickSpec(spec)"
+          >
+          <span
+            class="float-tip left"
+            :class="spec.tipType"
+            v-if="!!spec.tipText"
+          >
+            <slot name="float-tip">
+              <span v-if="spec.tipText">
+                <i class="j-icon icon-crown"></i>
+                {{ spec.tipText }}
+              </span>
+            </slot>
+          </span>
+            <slot name="card-content" :spec="spec">
+              <div class="spec-c-label">
+                <span>{{ spec.label }}</span>
+                <!-- spec.giftInfo && calcActive(spec) -->
+                <span class="highlight-text" v-if="spec.giftInfo"
+                >&nbsp;{{ spec.giftInfo }}</span
+                >
+                <div class="activity-badge" v-if="spec.activityInfo">
+                  <span class="j-icon icon-crown"></span>&nbsp;{{
+                    spec.activityInfo
+                  }}
+                </div>
+              </div>
+              <div class="spec-c-price">
+                <span>&yen;</span>
+                <span class="spec-c-price-text">{{ spec.price }}</span>
+              </div>
+              <div
+                class="spec-c-lineprice-box"
+                v-if="spec.lineprice > spec.price"
+              >
+                <span class="spec-c-lineprice">{{ spec.lineprice }}元</span>
+              </div>
+              <div class="spec-c-desc" v-html="spec.desc"></div>
+            </slot>
+          </SpecCard>
+        </slot>
+      </div>
+      <div class="spec-list-ft">
+        <slot name="footer"></slot>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import SpecCard from '@/components/common/SpecCard.vue'
+
+export default {
+  name: 'spec-list',
+  components: {
+    SpecCard
+  },
+  props: {
+    title: {
+      type: String,
+      default: ''
+    },
+    active: [String, Number, Boolean],
+    activeType: {
+      type: String,
+      default: 'id'
+    },
+    list: {
+      type: Array,
+      default() {
+        return [
+          // {
+          //   id: 1,
+          //   label: '1月',
+          //   value: '1个月',
+          //   price: 0,
+          //   desc: '每天仅需0元',
+          //   productionId: 1012, // 产品id,后台配置
+          //   tipText: '',
+          //   tipType: '', // gift/discount/'' 为空则显示默认的蓝色
+          //   giftInfo: '', // 赠品展示文字
+          //   giftName: '', // 赠品名称
+          //   discountId: '' // 赠品id
+          // }
+        ]
+      }
+    }
+  },
+  model: {
+    prop: 'active',
+    event: 'activeChange'
+  },
+  data() {
+    return {}
+  },
+  computed: {
+    hasTip() {
+      const index = this.list.findIndex((spec) => {
+        return !!spec.tipText
+      })
+      return index !== -1
+    },
+    activeItem() {
+      return this.list.find((spec) => spec[this.activeType] === this.active)
+    }
+  },
+  watch: {
+    active: function (newVal, oldVal) {
+      this.$emit('change', this.activeItem)
+    }
+  },
+  methods: {
+    calcActive(spec) {
+      return spec[this.activeType] === this.active
+    },
+    // v-model专用自定义事件
+    clickSpec(spec) {
+      this.$emit('activeChange', spec[this.activeType])
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+.spec-list-module {
+  .spec-list {
+    padding: 8px 16px;
+    &-hd {
+      font-size: 14px;
+      color: #171826;
+      line-height: 20px;
+    }
+    &-ft {
+    }
+  }
+  .spec-card {
+    padding: 12px 0;
+  }
+  .spec-list-content {
+    display: flex;
+    align-items: center;
+    flex-wrap: wrap;
+    &.mtb10 {
+      margin: 10px 0;
+    }
+    ::v-deep {
+      .spec-card {
+        margin: 6px 0;
+        flex: 1;
+        max-width: 158px;
+      }
+      .spec-card:not(:last-of-type) {
+        margin-right: 12px;
+      }
+    }
+  }
+
+  .spec-c {
+    &-label {
+      color: #5f5e64;
+      font-size: 13px;
+      line-height: 22px;
+      text-align: center;
+    }
+    &-price {
+      // margin: 4px 0;
+      color: #171826;
+      font-weight: 700;
+      font-size: 13px;
+      white-space: nowrap;
+      &-text {
+        margin-left: 2px;
+        font-size: 32px;
+        line-height: 36px;
+      }
+    }
+    &-desc {
+      font-size: 11px;
+      color: #171826;
+      line-height: 22px;
+      white-space: nowrap;
+    }
+  }
+  .spec-c-lineprice {
+    font-size: 11px;
+    color: #9b9ca3;
+    line-height: 22px;
+    text-decoration: line-through;
+  }
+  .spec-c-lineprice-box {
+    height: 22px;
+  }
+
+  .float-tip {
+    position: absolute;
+    top: -2px;
+    padding: 0 8px;
+    color: #fff;
+    font-size: 12px;
+    line-height: 20px;
+    background-color: $color_main;
+    z-index: 9;
+    &.left {
+      border-radius: 8px 2px 10px 2px;
+      left: -2px;
+    }
+    &.gift {
+      left: unset;
+      right: -6px;
+      top: -6px;
+      border-radius: 10px 0px 10px 0px;
+      background: linear-gradient(98deg, #ff7c32 0%, #f33838 100%);
+      &.tip-text {
+        margin-left: 4px;
+      }
+    }
+    &.discount {
+      left: -6px;
+      top: -6px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      padding: 2px 6px;
+      height: 18px;
+      color: #fff;
+      background: linear-gradient(102deg, #ffa674, #f01212 100%);
+      border-radius: 0;
+      border-top-left-radius: 4px;
+      border-bottom-left-radius: 4px;
+      &::after {
+        content: '';
+        position: absolute;
+        top: 0;
+        right: -6px;
+        width: 0;
+        height: 0;
+        border-top: 9px solid #f01212;
+        border-right: 7px solid transparent;
+        border-bottom: 9px solid #f01212;
+      }
+    }
+  }
+  .activity-badge {
+    position: absolute;
+    top: -8px;
+    right: -8px;
+    display: flex;
+    align-items: center;
+    padding: 2px 5px;
+    background: linear-gradient(98deg, #ff7c32 0%, #f33838 100%);
+    border-radius: 9px 0px;
+    text-align: center;
+    color: #fff;
+    font-size: 11px;
+    line-height: 14px;
+  }
+}
+</style>

+ 6 - 1
src/components/search/TopSearch.vue

@@ -18,6 +18,7 @@
           :maxlength="maxlength"
           :placeholder="getPlaceholder"
           :formatter="formatter"
+          :readonly="readonly"
           @blur="onBlur"
           @focus="onFocus"
           @input="$emit('input', $event)"
@@ -62,11 +63,15 @@ export default {
     },
     placeholder: {
       type: String,
-      default: '多个关键词用空格隔开'
+      default: '请输入要搜索的项目'
     },
     value: {
       type: String,
       default: ''
+    },
+    readonly: {
+      type: Boolean,
+      default: false
     }
   },
   computed: {

+ 441 - 0
src/components/selector/area-city-sidebar/index.vue

@@ -0,0 +1,441 @@
+<template>
+  <SideBar
+    ref="sidebar"
+    class="area-city-sidebar-selector"
+    :height="height"
+    :class="{
+      'disabled-city': disabledCitySelect,
+      'province-city-split': useProvinceCitySplit
+    }"
+    :whenParentLevel0Selected="whenParentLevel0Selected"
+    :sourceList="provinceList"
+    :childrenAddText="childrenAddText"
+    @onChange="onChange"
+    :beforeChildChange="beforeChildChange"
+  >
+    <template #tab-tag="{ parent }">
+      <slot name="tab-tag" :parent="parent">
+        <van-tag
+          round
+          type="primary"
+          v-if="tagTextOnlySelectedCount && parent._children_selectedCount"
+        >
+          {{ parent._children_selectedCount }}</van-tag
+        >
+      </slot>
+    </template>
+    <template #default="{ parent, child }">
+      <slot name="default" v-bind="{ parent, child }">
+        <template
+          v-if="parent.level !== 0 && child.level === 0 && useProvinceCitySplit"
+        >
+          <van-cell is-link :border="false">
+            {{ child.name }}
+            <template #right-icon>
+              <span
+                class="j-icon checkbox"
+                :class="{ checked: child._selected }"
+              ></span>
+            </template>
+          </van-cell>
+          <div class="children-title border-line-b" @click.stop>
+            <div class="c-title-text">市级</div>
+            <van-tag
+              v-if="disabledCitySelect"
+              round
+              plain
+              type="danger"
+              @click="disabledCityClick"
+              >开通</van-tag
+            >
+          </div>
+        </template>
+      </slot>
+    </template>
+  </SideBar>
+</template>
+
+<script>
+import { Cell, Tag } from 'vant'
+import SideBar from '@/ui/sidebar-selector'
+import chinaMapJSON from '@/assets/js/china_area'
+import { provinceListMapExp } from '@/data'
+import { getRandomString } from '@/utils/'
+
+export default {
+  name: 'AreaCitySidebarSelector',
+  components: {
+    [Cell.name]: Cell,
+    [Tag.name]: Tag,
+    SideBar
+  },
+  props: {
+    height: {
+      type: String,
+      default: ''
+    },
+    value: {
+      type: Object,
+      default() {
+        return {}
+      }
+    },
+    /**
+     * 省份城市布局分割
+     */
+    useProvinceCitySplit: {
+      type: Boolean,
+      default: false
+    },
+    /**
+     * 城市选择禁用
+     */
+    disabledCitySelect: {
+      type: Boolean,
+      default: false
+    },
+    /**
+     * 数据统计tag显示
+     */
+    tagTextOnlySelectedCount: {
+      type: Boolean,
+      default: false
+    },
+    /**
+     * 二级子项 "全部" 文字的替换
+     */
+    childrenAddText: String,
+    /**
+     * 更改确认函数
+     *
+     * 如果传入该函数,点击二级子项会触发,返回true会确认成功,返回false则会阻止更改
+     */
+    beforeChange: Function
+  },
+  model: {
+    prop: 'value',
+    event: 'modelChange'
+  },
+  watch: {
+    value(newVal) {
+      this.setState(newVal)
+    }
+  },
+  data() {
+    return {
+      whenParentLevel0Selected: false,
+      // 原始城市数据
+      // chinaMapJSON,
+      // 省份与字母IndexBar对照表
+      provinceListMapExp,
+      provinceListMap: {
+        // A: [
+        //     {
+        //         name: '安徽',
+        //         expanded: false,
+        //         canExpanded: true,
+        //         selectedState: '',
+        //         children: []
+        //     }
+        // ]
+      },
+      provinceList: [],
+      // indexBar数据
+      indexList: [],
+      provinceExp: {
+        name: '安徽',
+        value: '',
+        // 展开状态
+        expanded: false,
+        // 是否可以展开
+        canExpanded: false,
+        children: []
+      }
+    }
+  },
+  created() {
+    this.init(this.provinceListMapExp)
+  },
+  mounted() {
+    // 默认全国选中
+    this.setState()
+    this.setActiveTab(1)
+  },
+  methods: {
+    setActiveTab(num) {
+      const { sidebar } = this.$refs
+      sidebar.setActiveTab(num)
+    },
+    // 整理城市数据列表(并初始化indexBar数据)
+    init(provinceListMapExp) {
+      // 整理数据得到List,同时获得indexList
+      const provinceListMap = {}
+      const indexList = []
+
+      for (const key in provinceListMapExp) {
+        const areaArr = []
+        indexList.push(key)
+        provinceListMapExp[key].forEach((pName) => {
+          const provinceExp = JSON.parse(JSON.stringify(this.provinceExp))
+
+          provinceExp.name = pName
+          provinceExp.id = `ap-${getRandomString(8).toLowerCase()}`
+
+          if (pName !== '全国') {
+            const cities = this.getCitiesFromJSONMap(pName)
+            // console.log(pName, cities)
+            // 筛选掉直辖市和特别行政区(台湾省也不不需要展开)
+            if (cities.ProRemark === '省份' || cities.ProRemark === '自治区') {
+              if (cities.ProID === 32) {
+                provinceExp.children = []
+                provinceExp.canExpanded = false
+              } else {
+                cities.city.forEach((c) => {
+                  // 将市区重组成一个新的对象
+                  provinceExp.children.push({
+                    city: c.name,
+                    selected: false,
+                    canSelected: true,
+                    id: `ac-${getRandomString(8).toLowerCase()}`
+                  })
+                })
+              }
+            } else {
+              provinceExp.children = []
+              provinceExp.canExpanded = false
+            }
+          }
+
+          provinceExp.canExpanded = provinceExp.children.length !== 0
+          areaArr.push(provinceExp)
+        })
+        provinceListMap[key] = areaArr
+      }
+      this.provinceListMap = provinceListMap
+      this.indexList = indexList.filter((i) => i !== '#')
+      // 给provinceListMap赋值
+      for (const k in provinceListMap) {
+        this.$set(this.provinceListMap, k, provinceListMap[k])
+      }
+      this.initProvinceList(provinceListMap)
+    },
+    // 循环chinaMapJSON,找到对应省下面对应的市
+    getCitiesFromJSONMap(provinceName) {
+      let temp = null
+      for (let i = 0; i < chinaMapJSON.length; i++) {
+        const findThis = chinaMapJSON[i].name.indexOf(provinceName) !== -1
+        // 如果找到了,就不再循环后面的了
+        if (findThis) {
+          temp = chinaMapJSON[i]
+          break
+        }
+      }
+      return temp
+    },
+    initProvinceList(provinceListMap) {
+      const provinceList = []
+      for (const key in provinceListMap) {
+        const provinces = provinceListMap[key]
+        provinces.forEach((province) => {
+          if (province.name === '全国') {
+            province.level = 0
+          } else {
+            province.level = 1
+          }
+          const cities = []
+          if (Array.isArray(province.children)) {
+            province.children.forEach((city) => {
+              cities.push({
+                id: city.id,
+                name: city.city,
+                value: city.city,
+                level: 2
+              })
+            })
+          }
+
+          provinceList.push({
+            id: province.id,
+            name: province.name,
+            value: province.name,
+            level: province.level,
+            sourceChildrenCount: cities.length, // 子集真实数量
+            children: cities
+          })
+        })
+      }
+
+      this.provinceList = provinceList
+    },
+    disabledCityClick() {
+      this.$emit('onDisabledCityClick')
+    },
+    beforeChildChange(parent, child) {
+      if (this.beforeChange) {
+        return this.beforeChange(parent, child)
+      }
+      if (this.disabledCitySelect) {
+        if (child.level !== 0) {
+          this.disabledCityClick()
+          return false
+        } else {
+          return true
+        }
+      } else {
+        return true
+      }
+    },
+    getState() {
+      const { state } = this.getStateMore()
+      return state
+    },
+    getStateMore() {
+      const sidebar = this.$refs.sidebar
+      const renderList = sidebar.renderList
+
+      // 选中状态
+      const state = {}
+      // 选中数量统计
+      const stateCounter = {}
+
+      for (let i = 0; i < renderList.length; i++) {
+        const parent = renderList[i]
+        const children = parent.children
+
+        // 全国选中
+        if (parent.level === 0) {
+          if (parent._selected) {
+            break
+          }
+          continue
+        }
+
+        if (Array.isArray(children)) {
+          const childSelected = []
+          for (let j = 0; j < children.length; j++) {
+            const child = children[j]
+            if (parent._selected) {
+              break
+            } else {
+              if (child.level === 0) {
+                continue
+              } else {
+                if (child._selected) {
+                  childSelected.push(child.value)
+                }
+              }
+            }
+          }
+
+          // 直辖市
+          if (parent.sourceChildrenCount === 0) {
+            if (parent._selected) {
+              state[parent.name] = []
+              stateCounter[parent.name] = 1
+            }
+          } else {
+            if (parent._selected) {
+              state[parent.name] = []
+              stateCounter[parent.name] = parent.realChildren.length
+            } else {
+              if (childSelected.length > 0) {
+                state[parent.name] = childSelected
+                stateCounter[parent.name] = childSelected.length
+              }
+            }
+          }
+        }
+      }
+
+      return {
+        state,
+        stateCounter
+      }
+    },
+    /**
+     * 设置组件状态
+     * getState中获取的数据能够直接传入进行状态恢复
+     * 全部不选中传入-1
+     */
+    setState(state = {}) {
+      const sidebar = this.$refs.sidebar
+      if (state === -1) {
+        sidebar.setAllState(false)
+      } else if (!state || Object.keys(state).length === 0) {
+        // 全国选中
+        sidebar.setAllState(false)
+        sidebar.setParentLevel0State(true)
+      } else {
+        sidebar.setAllState(false)
+        this.setSidebarState(state)
+        sidebar.setParentLevel0State(false)
+      }
+      // 重新计算parent数据统计
+      sidebar.refreshAllChildrenState()
+      this.$emit('modelChange', state || {})
+    },
+    setSidebarState(state) {
+      const sidebar = this.$refs.sidebar
+      sidebar.setState((renderList) => {
+        renderList.forEach((parent) => {
+          const selectedChild = state[parent.name]
+          if (Array.isArray(selectedChild)) {
+            if (selectedChild.length === 0) {
+              sidebar.setChildrenState(parent.children, true)
+            } else {
+              parent.children.forEach((child) => {
+                if (selectedChild.includes(child.value)) {
+                  child._selected = true
+                }
+              })
+            }
+          }
+          sidebar.checkChildrenAllChecked(parent, parent.children)
+        })
+      })
+      sidebar.refreshAllChildrenState()
+    },
+    onChange({ parent, child }) {
+      const value = this.getState()
+      const payload = { parent, child, value }
+      this.$emit('modelChange', payload.value)
+      this.$emit('change', payload)
+    }
+  }
+}
+</script>
+<style lang="scss">
+.area-city-sidebar-selector {
+  &.province-city-split {
+    .content-list-item-container {
+        &::after {
+          height: 0;
+        }
+      }
+    .children-title {
+      display: flex;
+      align-items: center;
+      padding: 8px 16px;
+      font-size: 12px;
+      color: #171826;
+      line-height: 22px;
+      &.border-line-b {
+        &::after {
+          left: 16px;
+          right: 16px;
+        }
+      }
+      .c-title-text {
+        margin-right: 8px;
+      }
+    }
+  }
+
+  &.disabled-city {
+    .content-list-item-container.child {
+        opacity: 0.6;
+      }
+  }
+}
+
+</style>

+ 7 - 0
src/components/selector/area-city/index.config.js

@@ -0,0 +1,7 @@
+export default definePageConfig({
+  "navigationBarBackgroundColor": "#D7F6FB",
+  usingComponents: {
+    "van-index-bar-weapp": "@/vant/index-bar/index",
+    "van-index-anchor-weapp": "@/vant/index-anchor/index"
+  },
+})

+ 691 - 0
src/components/selector/area-city/index.vue

@@ -0,0 +1,691 @@
+<template>
+  <div class="area-city-selector-content j-container">
+    <div class="j-header">
+      <slot name="header"></slot>
+    </div>
+    <div class="j-main">
+      <van-index-bar-weapp
+        :index-list="indexList"
+        :sticky="true"
+        class="area-list"
+      >
+        <li
+          v-for="(item, key) in provinceListMap"
+          :key="key"
+          class="province-item-container"
+        >
+          <van-index-anchor-weapp :index="key" v-if="key !== '#'">
+            {{ key }}
+          </van-index-anchor-weapp>
+          <div v-for="(province, ii) in item" :key="ii * 2">
+            <div
+              class="province-item bg-white tab_content"
+              v-if="province.name == '全国'"
+            >
+              <button
+                v-if="!onlyProvince"
+                class="j-button-select"
+                :class="{ active: province.selectedState }"
+                @click="changeCityState(province, '#')"
+              >
+                {{ province.name }}
+              </button>
+            </div>
+            <div class="province-item bg-white" v-else>
+              <div class="tab" :class="province.id">
+                <div class="province" @click="clickCheckbox(province)">
+                  <div
+                    class="j-icon checkbox"
+                    :class="province.selectedState"
+                  ></div>
+                  <span>{{ province.name }}</span>
+                </div>
+                <span
+                  style="flex: 1; min-height: 15px"
+                  @click="
+                    onlyProvince
+                      ? clickCheckbox(province)
+                      : changeExpandState($event, province)
+                  "
+                ></span>
+                <span
+                  v-if="province.canExpanded && !onlyProvince"
+                  :class="{
+                    'right-action': true,
+                    rotate180: !province.expanded
+                  }"
+                  @click="changeExpandState($event, province)"
+                >
+                  <van-icon name="arrow-up" />
+                </span>
+              </div>
+              <transition
+                name="fade"
+                @after-leave="onTransitionEnd"
+                @after-enter="onTransitionEnd"
+              >
+                <div
+                  v-show="province.expanded"
+                  v-if="!onlyProvince"
+                  :class="['tab_content', province.id]"
+                >
+                  <div class="content">
+                    <button
+                      v-for="(city, iii) in province.children"
+                      :key="iii * 3"
+                      class="j-button-select"
+                      :class="{ active: city.selected, [city.id]: true }"
+                      :disabled="!city.canSelected"
+                      @click="changeCityState(province, city)"
+                    >
+                      {{ city.city }}
+                    </button>
+                  </div>
+                </div>
+              </transition>
+            </div>
+          </div>
+        </li>
+      </van-index-bar-weapp>
+    </div>
+    <slot name="footer"></slot>
+  </div>
+</template>
+<script>
+import chinaMapJSON from '@/assets/js/china_area'
+import { provinceListMapExp } from '@/data'
+import { getRandomString } from '@/utils/'
+import { Icon, IndexBar, IndexAnchor } from 'vant'
+
+export default {
+  name: 'AreaCitySelector',
+  components: {
+    [Icon.name]: Icon,
+    [IndexBar.name]: IndexBar,
+    [IndexAnchor.name]: IndexAnchor
+  },
+  props: {
+    onlyOne: {
+      type: Boolean,
+      default: false
+    },
+    /**
+     * 是否只显示省份
+     *
+     * true则隐藏城市选择,城市选择打不开
+     */
+    onlyProvince: {
+      type: Boolean,
+      default: false
+    },
+    /**
+     * 当省份全部选中后,是否全国选中,并取消其他选中
+     */
+    checkAllSelected: {
+      type: Boolean,
+      default: true
+    },
+    /**
+     * 当前选中该状态
+     * v-model绑定数据
+     */
+    value: {
+      type: Object,
+      default() {
+        return {
+          // '北京': [],
+          // '安徽': [],
+          // '广东': [
+          //     '揭阳市',
+          //     '茂名市',
+          //     '韶关市'
+          // ],
+          // '河北': [
+          //     '邯郸市',
+          //     '秦皇岛市',
+          //     '保定市'
+          // ],
+          // '福建': [
+          //     '福州市',
+          //     '厦门市',
+          //     '宁德市'
+          // ],
+          // '重庆': []
+        }
+      }
+    }
+  },
+  model: {
+    prop: 'value',
+    event: 'modelChange'
+  },
+  data() {
+    return {
+      // 原始城市数据
+      // chinaMapJSON,
+      // 省份与字母IndexBar对照表
+      provinceListMapExp,
+      provinceListMap: {
+        // A: [
+        //     {
+        //         name: '安徽',
+        //         expanded: false,
+        //         canExpanded: true,
+        //         selectedState: '',
+        //         children: []
+        //     }
+        // ]
+      },
+      // indexBar数据
+      indexList: [],
+      // indexBar是否高亮
+      indexListMap: {},
+      provinceExp: {
+        name: '安徽',
+        // 展开状态
+        expanded: false,
+        // 是否可以展开
+        canExpanded: false,
+        // 选中状态: half(半选)、checked(全选)、''(未选中)、checkeddisabled(全选不能点击)、nonedisabled(未选不能点击)
+        selectedState: '',
+        children: []
+      }
+    }
+  },
+  watch: {
+    value(newVal) {
+      this.setState(newVal)
+    }
+  },
+  created() {
+    this.initIndexBarAndAreaMap()
+    this.setState(this.value)
+  },
+  methods: {
+    setQuanGuoState(f = false) {
+      const state = f ? 'checked' : ''
+      try {
+        this.provinceListMap['#'][0].selectedState = state
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    // 整理城市数据列表(并初始化indexBar数据)
+    initIndexBarAndAreaMap() {
+      // 整理数据得到indexListMap(),同时获得indexList
+      // const indexListMap = {}
+      const provinceListMap = {}
+      const indexList = []
+
+      for (const key in this.provinceListMapExp) {
+        const areaArr = []
+        indexList.push(key)
+        this.provinceListMapExp[key].forEach((pName) => {
+          const provinceExp = JSON.parse(JSON.stringify(this.provinceExp))
+
+          provinceExp.name = pName
+          provinceExp.id = `ap-${getRandomString(8).toLowerCase()}`
+
+          if (pName !== '全国') {
+            const cities = this.getCitiesFromJSONMap(pName)
+            // console.log(pName, cities)
+            // 筛选掉直辖市和特别行政区(台湾省也不不需要展开)
+            if (cities.ProRemark === '省份' || cities.ProRemark === '自治区') {
+              if (cities.ProID === 32) {
+                provinceExp.children = []
+                provinceExp.canExpanded = false
+              } else {
+                cities.city.forEach((c) => {
+                  // 将市区重组成一个新的对象
+                  provinceExp.children.push({
+                    city: c.name,
+                    selected: false,
+                    canSelected: true,
+                    id: `ac-${getRandomString(8).toLowerCase()}`
+                  })
+                })
+              }
+            } else {
+              provinceExp.children = []
+              provinceExp.canExpanded = false
+            }
+          }
+
+          provinceExp.canExpanded = provinceExp.children.length !== 0
+          areaArr.push(provinceExp)
+        })
+        provinceListMap[key] = areaArr
+      }
+      this.provinceListMap = provinceListMap
+      this.indexList = indexList.filter((i) => i !== '#')
+      // 给provinceListMap赋值
+      for (const k in provinceListMap) {
+        this.$set(this.provinceListMap, k, provinceListMap[k])
+      }
+    },
+    // 循环chinaMapJSON,找到对应省下面对应的市
+    getCitiesFromJSONMap(provinceName) {
+      let temp = null
+      for (let i = 0; i < chinaMapJSON.length; i++) {
+        const findThis = chinaMapJSON[i].name.indexOf(provinceName) !== -1
+        // 如果找到了,就不再循环后面的了
+        if (findThis) {
+          temp = chinaMapJSON[i]
+          break
+        }
+      }
+      return temp
+    },
+    // 控制城市盒子展开和收起
+    changeExpandState(e, province) {
+      if (!province.canExpanded) return
+      province.expanded = !province.expanded
+      //
+      // const wrapper = this.$el.querySelector(`.tab_content.${province.id}`)
+      // const content = wrapper.querySelector('.content')
+      //
+      // requestAnimationFrame(() => {
+      //   const clientHeight = content.clientHeight
+      //   wrapper.style.overflow = 'hidden'
+      //   if (clientHeight) {
+      //     // 在raf中再次调用raf,则在浏览器下次重绘之前继续更新下一帧动画。
+      //     // https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame
+      //     wrapper.style.height = province.expanded ? 0 : `${clientHeight}px`
+      //     requestAnimationFrame(() => {
+      //       wrapper.style.height = province.expanded ? `${clientHeight}px` : 0
+      //     })
+      //   } else {
+      //     // if (!province.expanded) {
+      //     //     province.expanded = false;
+      //     // } else {
+      //     //     wrapper.style.height = null;
+      //     // }
+      //   }
+      // })
+    },
+    onTransitionEnd(e) {
+      e.style.height = null
+    },
+    // 城市选择按钮点击事件
+    // 根据城市的选择情况判断省份的选择情况
+    changeCityState(province, city) {
+      if (city === '#') {
+        this.setState()
+        this.onChange()
+        return
+      }
+      // 全国置为空
+      this.setQuanGuoState(false)
+      city.selected = !city.selected
+
+      // 判断省份的选择状态
+      let count = 0
+      const cityLength = province.children.length
+      if (cityLength) {
+        province.children.forEach((v) => {
+          // 前提是可点击的
+          if (v.canSelected && v.selected) count++
+        })
+      } else {
+        // 直辖市或自治区
+        province.canExpanded = false
+        province.expanded = false
+      }
+
+      // 选中状态: half(半选)、checked(全选)、''(未选中)、checkeddisabled(全选不能点击)、nonedisabled(未选不能点击)
+      if (count === 0) {
+        province.selectedState = ''
+      } else if (count < cityLength) {
+        province.selectedState = 'half'
+      } else if (count === cityLength) {
+        province.selectedState = 'checked'
+      } else {
+        province.selectedState = ''
+      }
+
+      this.onChange()
+    },
+    // 省份checkbox点击事件
+    // 省份checkbox点击事件
+    clickCheckbox(province) {
+      const state = province.selectedState
+      if (state === 'checkeddisabled' || state === 'nonedisabled') return
+
+      // 最大选中数量
+      if (this.onlyOne) {
+        this.setState()
+      }
+
+      // 全国置为空
+      this.setQuanGuoState(false)
+
+
+
+      if (state === '' || state === 'half') {
+        province.children.forEach((v) => {
+          v.selected = true
+        })
+        province.selectedState = 'checked'
+      } else {
+        province.children.forEach((v) => {
+          v.selected = false
+        })
+        province.selectedState = ''
+      }
+      this.onChange()
+    },
+    // 检查是否所有省份按钮被全选中
+    // 全部被全选->返回true
+    checkAllProvinceState() {
+      const stateArr = []
+      for (const key in this.provinceListMap) {
+        this.provinceListMap[key].forEach((item) => {
+          if (item.name !== '全国') {
+            if (item.selectedState === '') {
+              stateArr.push('checked')
+            } else if (item.selectedState === 'checked') {
+              stateArr.push('unchecked')
+            } else {
+              stateArr.push('other')
+            }
+          }
+        })
+      }
+      // 统计不同状态的个数
+      const counter = {
+        checked: 0,
+        unchecked: 0,
+        other: 0
+      }
+      for (let i = 0; i < stateArr.length; i++) {
+        const k = stateArr[i]
+        if (counter[k]) {
+          counter[k] += 1
+        } else {
+          counter[k] = 1
+        }
+      }
+      // console.log(counter)
+      return {
+        state: stateArr,
+        allSelected: counter.checked === stateArr.length,
+        noSelected: counter.unchecked === stateArr.length
+      }
+    },
+    checkAllProvinceStateAfter() {
+      const checkState = this.checkAllProvinceState()
+      if (checkState.allSelected || checkState.noSelected) {
+        this.setState()
+      }
+    },
+    setAllUnselected() {
+      this.setState()
+      this.setQuanGuoState(false)
+    },
+    // 初始化选中城市数据
+    setState(data) {
+      // 设置全国
+      if (!data || Object.keys(data).length === 0) {
+        // 其他全部设置不选中,全国设置选中
+        for (const key in this.provinceListMap) {
+          this.provinceListMap[key].forEach((item) => {
+            item.selectedState = ''
+            item.children.forEach((iitem) => {
+              iitem.selected = false
+            })
+            if (item.name === '全国') {
+              item.selectedState = 'checked'
+            }
+          })
+        }
+      } else {
+        // 先将所有城市选择取消
+        this.setState()
+        // 设置某几个省份被选中
+        for (const key in this.provinceListMap) {
+          this.provinceListMap[key].forEach((item) => {
+            const selectCityArr = data[item.name]
+            if (selectCityArr && selectCityArr instanceof Array) {
+              if (selectCityArr.length === 0) {
+                // 全省被选中
+                item.children.forEach((iitem) => {
+                  iitem.selected = true
+                })
+                item.selectedState = 'checked'
+              } else {
+                // 省份中的某些市被选中
+                item.children.forEach((iitem) => {
+                  if (selectCityArr.indexOf(iitem.city) !== -1) {
+                    iitem.selected = true
+                  }
+                })
+                item.selectedState = 'half'
+              }
+            }
+
+            if (item.name === '全国') {
+              item.selectedState = ''
+            }
+          })
+        }
+      }
+
+      this.$emit('modelChange', data || {})
+    },
+    // 获取当前选中城市数据
+    getState: function () {
+      const counter = {}
+      // 判断是否全国被选中
+      if (this.provinceListMap['#'][0].selectedState === 'checked') {
+        return counter
+      }
+
+      // 全国没有被选中,排除循环全国
+      for (const key in this.provinceListMap) {
+        if (key === '#') continue
+        this.provinceListMap[key].forEach(function (item) {
+          // 当前省份下被选中的城市数量
+          const selectedCityArr = []
+          const cityTotalCount = item.children.length
+          item.children.forEach(function (iitem) {
+            if (iitem.selected && iitem.canSelected) {
+              selectedCityArr.push(iitem.city)
+            }
+          })
+          // 计算出当前省份下的城市是否被全选了
+          if (
+            cityTotalCount === selectedCityArr.length &&
+            item.selectedState === 'checked'
+          ) {
+            // 城市被全选
+            counter[item.name] = []
+          } else {
+            if (selectedCityArr.length !== 0) {
+              counter[item.name] = selectedCityArr
+            }
+          }
+        })
+      }
+
+      return counter
+    },
+    // 统计城市分布数量
+    getCityCount(selected) {
+      const selectedCount = {
+        // 全国被选中时,country为1
+        country: 1,
+        province: 0,
+        city: {
+          // 一共选了多少个城市
+          totalCount: 0,
+          // 分布在几个省份
+          pCount: 0
+        }
+      }
+
+      if (Object.keys(selected).length === 0) {
+        // 全国
+      } else {
+        selectedCount.country = 0
+        for (const p in selected) {
+          if (selected[p].length === 0) {
+            selectedCount.province++
+          } else {
+            selectedCount.city.pCount++
+            selected[p].forEach(function () {
+              selectedCount.city.totalCount++
+            })
+          }
+        }
+      }
+
+      const tipText = {
+        p: selectedCount.province === 0 ? '' : selectedCount.province + '个省',
+        c:
+          selectedCount.city.totalCount === 0
+            ? ''
+            : selectedCount.city.totalCount + '个市',
+        s:
+          selectedCount.city.pCount === 1
+            ? ''
+            : '(分布在' + selectedCount.city.pCount + '个省内)',
+        text: ''
+      }
+
+      if (selectedCount.country === 1) {
+        tipText.text = '全国'
+      } else {
+        let dot = ''
+        if (
+          selectedCount.city.totalCount !== 0 &&
+          selectedCount.province !== 0
+        ) {
+          dot = '、'
+        }
+        if (
+          selectedCount.city.totalCount === 0 ||
+          selectedCount.city.totalCount === 1
+        ) {
+          tipText.s = ''
+        }
+        tipText.text = tipText.p + dot + tipText.c + tipText.s
+      }
+
+      return {
+        data: selectedCount,
+        zh: tipText.text
+      }
+    },
+    indexBarMounted() {
+      // const indexBar = this.$el.querySelector('.van-index-bar__sidebar')
+      // this.$el.appendChild(indexBar)
+    },
+    onChange() {
+      if (this.checkAllSelected) {
+        // 检查是否全部省份选中
+        this.checkAllProvinceStateAfter()
+      }
+
+      const selectedCity = this.getState()
+      const payload = {
+        data: selectedCity,
+        text: this.getCityCount(selectedCity)
+      }
+      this.$emit('modelChange', selectedCity)
+      this.$emit('change', payload)
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+.area-city-selector-content {
+  max-height: 100%;
+  overflow: hidden;
+  .fade-enter-active,
+  .fade-leave-active {
+    transition: height 300ms ease-in-out;
+  }
+  .fade-enter,
+  .fade-leave-to {
+    height: 0;
+  }
+
+  .van-index-bar__sidebar {
+    top: 180px;
+    right: 0;
+  }
+  // indexBar 透明度
+  .van-index-bar__index {
+      padding: 0 12px;
+      opacity: 0.5;
+    }
+
+  .popup {
+    .van-index-bar__sidebar {
+      right: 10px;
+      top: 2vh;
+      transform: none;
+    }
+    .right-action {
+        margin-right: 16px;
+      }
+  }
+  /* 设置区域css --- set_area */
+  .j-button-select {
+    margin: 5px 0 5px 5px;
+  }
+  .province-item {
+    .tab {
+      position: relative;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      color: #1d1d1d;
+      font-size: 15px;
+      padding: 15px 32px 15px 19px;
+      &::after {
+        position: absolute;
+        content: '';
+        width: calc(100% - 47px);
+        height: 0;
+        left: 47px;
+        bottom: 0;
+        border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+      }
+    }
+
+    .right-action {
+      transition: transform 0.3s ease;
+    }
+
+    .checkbox {
+      margin-right: 10px;
+    }
+
+    .province {
+      display: flex;
+      align-items: center;
+    }
+
+    .right-action {
+      .j-icon {
+        width: 16px;
+        height: 16px;
+      }
+    }
+  }
+
+  .tab_content {
+    display: flex;
+    flex-wrap: wrap;
+    font-size: 14px;
+    padding: 5px 15px;
+    .content {
+      display: flex;
+      flex-wrap: wrap;
+    }
+  }
+}
+
+</style>