AreaSelectorCard.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. <template>
  2. <selector-card
  3. class="area-selector card"
  4. @onConfirm="onConfirm"
  5. @onCancel="onCancel"
  6. >
  7. <div slot="header">选择区域</div>
  8. <div class="selector-content" v-loading="loading">
  9. <div class="search-container">
  10. <el-input v-model="searchContent" placeholder="搜索" prefix-icon="el-icon-search"></el-input>
  11. </div>
  12. <div class="select-list scrollbar" ref="selectList">
  13. <div v-for="(item, key) in provinceListMap" :key="key" class="select-group-container">
  14. <div class="index-anchor" :id="key" :data-index="key" v-if="key !== '#'">{{ key }}</div>
  15. <el-collapse-transition v-for="(province, ii) in item" :key="ii*2">
  16. <div class="select-group tab-content global" v-if="province.name == '全国'">
  17. <button
  18. class="j-button-item global"
  19. :class="{
  20. active: province.selectedState,
  21. [province.id]: true
  22. }"
  23. @click="changeCityState(province, '#')"
  24. >{{ province.name }}</button>
  25. </div>
  26. <div class="select-group" v-else>
  27. <div class="tab" :class="province.id">
  28. <div class="tab-name-container" @click="clickCheckbox(province)">
  29. <div class="j-checkbox" :class="province.selectedState"></div>
  30. <span class="tab-name">{{ province.name }}</span>
  31. </div>
  32. <span style="flex: 1; height: 100%;" @click="changeExpandState($event, province)"></span>
  33. <span
  34. v-if="province.canExpanded"
  35. @click="changeExpandState($event, province)">
  36. <i
  37. class="el-icon-arrow-down"
  38. :class="{
  39. rotate180: province.expanded
  40. }"></i>
  41. </span>
  42. </div>
  43. <el-collapse-transition>
  44. <div v-show="province.expanded" class="tab-content">
  45. <div class="content-list">
  46. <button v-for="(city, iii) in province.children" :key="iii*3"
  47. class="j-button-item"
  48. :class="{
  49. active: city.selected,
  50. [city.id]: true
  51. }"
  52. :disabled="!city.canSelected"
  53. @click="changeCityState(province, city)"
  54. >{{ city.city }}</button>
  55. </div>
  56. </div>
  57. </el-collapse-transition>
  58. </div>
  59. </el-collapse-transition>
  60. </div>
  61. </div>
  62. </div>
  63. </selector-card>
  64. </template>
  65. <script>
  66. import { Input, Icon } from 'element-ui'
  67. import 'element-ui/lib/theme-chalk/base.css'
  68. import CollapseTransition from 'element-ui/lib/transitions/collapse-transition'
  69. import SelectorCard from '@/components/selector/SelectorCard.vue'
  70. import chinaMapJSON from '@/assets/js/china_area.js'
  71. import { provinceListMapExp } from '@/assets/js/selector.js'
  72. import { debounce, getRandomString } from '@/utils/'
  73. export default {
  74. name: 'area-selector-card',
  75. components: {
  76. [Input.name]: Input,
  77. [Icon.name]: Icon,
  78. [CollapseTransition.name]: CollapseTransition,
  79. SelectorCard
  80. },
  81. props: {
  82. // 初始化城市数据
  83. // 刚进入页面需要被选中的城市数据
  84. initCityMap: {
  85. type: Object,
  86. default () {
  87. return {
  88. // '北京': [],
  89. // '安徽': [],
  90. // '广东': [
  91. // '揭阳市',
  92. // '茂名市',
  93. // '韶关市'
  94. // ],
  95. // '河北': [
  96. // '邯郸市',
  97. // '秦皇岛市',
  98. // '保定市'
  99. // ],
  100. // '福建': [
  101. // '福州市',
  102. // '厦门市',
  103. // '宁德市'
  104. // ],
  105. // '重庆': []
  106. }
  107. }
  108. }
  109. },
  110. data () {
  111. return {
  112. searchContent: '',
  113. loading: false,
  114. // 省份与字母IndexBar对照表
  115. provinceListMapExp,
  116. provinceListMap: {
  117. // A: [
  118. // {
  119. // name: '安徽',
  120. // expanded: false,
  121. // canExpanded: true,
  122. // selectedState: '',
  123. // children: []
  124. // }
  125. // ]
  126. },
  127. // indexBar数据
  128. indexList: [],
  129. provinceExp: {
  130. name: '',
  131. // 展开状态
  132. expanded: false,
  133. // 是否可以展开
  134. canExpanded: false,
  135. // 选中状态: half(半选)、checked(全选)、''(未选中)、checkeddisabled(全选不能点击)、nonedisabled(未选不能点击)
  136. selectedState: '',
  137. children: [],
  138. id: ''
  139. }
  140. }
  141. },
  142. watch: {
  143. initCityMap (newVal, oldVal) {
  144. this.setCitySelected(newVal)
  145. },
  146. searchContent: debounce(function (newVal, oldVal) {
  147. const search = newVal
  148. const { findP } = this.getProvinceWithString(search)
  149. const id = findP[0] && findP[0].id
  150. if (id) {
  151. if (findP[0].canExpanded) {
  152. findP[0].expanded = true
  153. }
  154. this.$nextTick(() => {
  155. const wrapper = document.querySelector('.area-selector.card')
  156. this.$refs.selectList.scrollTop = wrapper.querySelector(`.${id}`).offsetTop
  157. // document.querySelector(`.${id}`).scrollIntoView() // 兼容性有问题
  158. })
  159. }
  160. }, 300)
  161. },
  162. created () {
  163. this.initIndexBarAndAreaMap()
  164. this.provinceListMap['#'][0].selectedState = 'checked'
  165. this.setCitySelected(this.initCityMap)
  166. },
  167. methods: {
  168. changeLoadingState (s) {
  169. this.loading = s
  170. },
  171. // 整理城市数据列表(并初始化indexBar数据)
  172. initIndexBarAndAreaMap () {
  173. // 整理数据得到indexListMap(),同时获得indexList
  174. const provinceListMap = {}
  175. const indexList = []
  176. for (const key in this.provinceListMapExp) {
  177. const areaArr = []
  178. indexList.push(key)
  179. this.provinceListMapExp[key].forEach(pName => {
  180. const provinceExp = JSON.parse(JSON.stringify(this.provinceExp))
  181. provinceExp.name = pName
  182. provinceExp.id = `ap-${getRandomString(8).toLowerCase()}`
  183. if (pName !== '全国') {
  184. const cities = this.getCitiesFromJSONMap(pName)
  185. // console.log(pName, cities)
  186. // 筛选掉直辖市和特别行政区(台湾省也不不需要展开)
  187. if (cities.ProRemark === '省份' || cities.ProRemark === '自治区') {
  188. if (cities.ProID === 32) {
  189. provinceExp.children = []
  190. provinceExp.canExpanded = false
  191. } else {
  192. cities.city.forEach(c => {
  193. // 将市区重组成一个新的对象
  194. return provinceExp.children.push({
  195. city: c.name,
  196. selected: false,
  197. canSelected: true,
  198. id: `ac-${getRandomString(8).toLowerCase()}`
  199. })
  200. })
  201. }
  202. } else {
  203. provinceExp.children = []
  204. provinceExp.canExpanded = false
  205. }
  206. }
  207. provinceExp.canExpanded = provinceExp.children.length !== 0
  208. areaArr.push(provinceExp)
  209. })
  210. provinceListMap[key] = areaArr
  211. }
  212. this.provinceListMap = provinceListMap
  213. this.indexList = indexList
  214. // 给provinceListMap赋值
  215. for (const k in provinceListMap) {
  216. this.$set(this.provinceListMap, k, provinceListMap[k])
  217. }
  218. },
  219. // 循环chinaMapJSON,找到对应省下面对应的市
  220. getCitiesFromJSONMap (provinceName) {
  221. return chinaMapJSON.find(item => item.name.indexOf(provinceName) !== -1)
  222. },
  223. // 输入字符串,找到其所在省份
  224. getProvinceWithString (s = '') {
  225. // 找是否有省份相同的
  226. const findP = [] // 匹配到的省份数组
  227. const findC = [] // 匹配到的市数组
  228. if (s) {
  229. for (const key in this.provinceListMap) {
  230. const item = this.provinceListMap[key]
  231. for (let i = 0; i < item.length; i++) {
  232. if (item[i].name.includes(s)) {
  233. findP.push(item[i])
  234. }
  235. if (Array.isArray(item[i].children) && item[i].children.length !== 0) {
  236. item[i].children.forEach(city => {
  237. if (city.city.includes(s)) {
  238. findP.push(item[i])
  239. findC.push(city)
  240. }
  241. })
  242. }
  243. }
  244. }
  245. }
  246. return {
  247. findP,
  248. findC
  249. }
  250. },
  251. // 控制城市盒子展开和收起
  252. changeExpandState (e, province) {
  253. if (!province.canExpanded) return
  254. province.expanded = !province.expanded
  255. },
  256. // 城市选择按钮点击事件
  257. // 根据城市的选择情况判断省份的选择情况
  258. changeCityState (province, city) {
  259. if (city === '#') {
  260. return this.setCitySelected()
  261. }
  262. // 全国置为空
  263. this.provinceListMap['#'][0].selectedState = ''
  264. city.selected = !city.selected
  265. // 判断省份的选择状态
  266. let count = 0
  267. const cityLength = province.children.length
  268. if (cityLength) {
  269. province.children.forEach(v => {
  270. // 前提是可点击的
  271. if (v.canSelected && v.selected) {
  272. count++
  273. }
  274. })
  275. } else {
  276. // 直辖市或自治区
  277. province.canExpanded = false
  278. province.expanded = false
  279. }
  280. // 选中状态: half(半选)、checked(全选)、''(未选中)、checkeddisabled(全选不能点击)、nonedisabled(未选不能点击)
  281. if (count === 0) {
  282. province.selectedState = ''
  283. } else if (count < cityLength) {
  284. province.selectedState = 'half'
  285. } else if (count === cityLength) {
  286. province.selectedState = 'checked'
  287. } else {
  288. province.selectedState = ''
  289. }
  290. const pState = this.checkAllProvinceState()
  291. // 如果所有省份被全选,则取消所有选中,让全国选中
  292. if (pState.allSelected || pState.noSelected) {
  293. this.setCitySelected()
  294. }
  295. },
  296. // 省份checkbox点击事件
  297. clickCheckbox (province) {
  298. const state = province.selectedState
  299. if (state === 'checkeddisabled' || state === 'nonedisabled') return
  300. // 全国置为空
  301. this.provinceListMap['#'][0].selectedState = ''
  302. if (state === '' || state === 'half') {
  303. province.children.forEach(v => (v.selected = true))
  304. province.selectedState = 'checked'
  305. } else {
  306. province.children.forEach(v => (v.selected = false))
  307. province.selectedState = ''
  308. }
  309. const pState = this.checkAllProvinceState()
  310. // 如果所有省份被全选,则取消所有选中,让全国选中
  311. if (pState.allSelected || pState.noSelected) {
  312. this.setCitySelected()
  313. }
  314. },
  315. // 检查是否所有省份按钮被全选中
  316. // 全部被全选->返回true
  317. checkAllProvinceState () {
  318. const stateArr = []
  319. for (const key in this.provinceListMap) {
  320. this.provinceListMap[key].forEach(item => {
  321. if (item.name !== '全国') {
  322. if (item.selectedState === '') {
  323. stateArr.push('checked')
  324. } else if (item.selectedState === 'checked') {
  325. stateArr.push('unchecked')
  326. } else {
  327. stateArr.push('other')
  328. }
  329. }
  330. })
  331. }
  332. // 统计不同状态的个数
  333. const counter = {
  334. checked: 0,
  335. unchecked: 0,
  336. other: 0
  337. }
  338. for (let i = 0; i < stateArr.length; i++) {
  339. const k = stateArr[i]
  340. if (counter[k]) {
  341. counter[k] += 1
  342. } else {
  343. counter[k] = 1
  344. }
  345. }
  346. // console.log(counter)
  347. return {
  348. state: stateArr,
  349. allSelected: counter.checked === stateArr.length,
  350. noSelected: counter.unchecked === stateArr.length
  351. }
  352. },
  353. // 初始化选中城市数据
  354. setCitySelected (data) {
  355. // 设置全国
  356. if (!data || Object.keys(data).length === 0) {
  357. // 其他全部设置不选中,全国设置选中
  358. for (const key in this.provinceListMap) {
  359. this.provinceListMap[key].forEach(item => {
  360. item.selectedState = ''
  361. item.children.forEach(iitem => {
  362. iitem.selected = false
  363. })
  364. if (item.name === '全国') {
  365. item.selectedState = 'checked'
  366. }
  367. })
  368. }
  369. } else {
  370. // 先将所有城市选择取消
  371. this.setCitySelected()
  372. // 设置某几个省份被选中
  373. for (const key in this.provinceListMap) {
  374. this.provinceListMap[key].forEach(item => {
  375. const selectCityArr = data[item.name]
  376. if (Array.isArray(selectCityArr)) {
  377. if (selectCityArr.length === 0) {
  378. // 全省被选中
  379. item.children.forEach(iitem => {
  380. iitem.selected = true
  381. })
  382. item.selectedState = 'checked'
  383. } else {
  384. // 省份中的某些市被选中
  385. item.children.forEach(iitem => {
  386. if (selectCityArr.indexOf(iitem.city) !== -1) {
  387. iitem.selected = true
  388. }
  389. })
  390. item.selectedState = 'half'
  391. }
  392. }
  393. if (item.name === '全国') {
  394. item.selectedState = ''
  395. }
  396. })
  397. }
  398. }
  399. },
  400. // 获取当前选中城市数据
  401. getSelectedCity () {
  402. const counter = {}
  403. // 判断是否全国被选中
  404. if (this.provinceListMap['#'][0].selectedState === 'checked') {
  405. return counter
  406. }
  407. // 全国没有被选中,排除循环全国
  408. for (const key in this.provinceListMap) {
  409. if (key === '#') continue
  410. this.provinceListMap[key].forEach(item => {
  411. // 当前省份下被选中的城市数量
  412. const selectedCityArr = []
  413. const cityTotalCount = item.children.length
  414. item.children.forEach(iitem => {
  415. if (iitem.selected && iitem.canSelected) {
  416. selectedCityArr.push(iitem.city)
  417. }
  418. })
  419. // 计算出当前省份下的城市是否被全选了
  420. if (cityTotalCount === selectedCityArr.length && item.selectedState === 'checked') {
  421. // 城市被全选
  422. counter[item.name] = []
  423. } else {
  424. if (selectedCityArr.length !== 0) {
  425. counter[item.name] = selectedCityArr
  426. }
  427. }
  428. })
  429. }
  430. return counter
  431. },
  432. // 统计城市分布数量
  433. getCityCount () {
  434. const selectedCount = {
  435. // 全国被选中时,country为1
  436. country: 1,
  437. province: 0,
  438. city: {
  439. // 一共选了多少个城市
  440. totalCount: 0,
  441. // 分布在几个省份
  442. pCount: 0
  443. }
  444. }
  445. const selected = this.getSelectedCity()
  446. if (Object.keys(selected).length === 0) {
  447. // 全国
  448. } else {
  449. selectedCount.country = 0
  450. for (const p in selected) {
  451. if (selected[p].length === 0) {
  452. selectedCount.province++
  453. } else {
  454. selectedCount.city.pCount++
  455. selected[p].forEach(() => {
  456. selectedCount.city.totalCount++
  457. })
  458. }
  459. }
  460. }
  461. const tipText = {
  462. p: selectedCount.province === 0 ? '' : selectedCount.province + '个省',
  463. c: selectedCount.city.totalCount === 0 ? '' : selectedCount.city.totalCount + '个市',
  464. s: selectedCount.city.pCount === 1 ? '' : '(分布在' + selectedCount.city.pCount + '个省内)',
  465. text: ''
  466. }
  467. if (selectedCount.province === 1) {
  468. tipText.text = '全国'
  469. } else {
  470. let dot = ''
  471. if (selectedCount.city.totalCount !== 0 && selectedCount.province !== 0) {
  472. dot = '、'
  473. }
  474. if (selectedCount.city.totalCount === 0 || selectedCount.city.totalCount === 1) {
  475. tipText.s = ''
  476. }
  477. tipText.text = tipText.p + dot + tipText.c + tipText.s
  478. }
  479. return {
  480. data: selectedCount,
  481. zh: tipText
  482. }
  483. },
  484. onCancel () {
  485. this.$emit('onCancel')
  486. },
  487. onConfirm () {
  488. const selectedCity = this.getSelectedCity()
  489. this.$emit('onConfirm', selectedCity)
  490. }
  491. }
  492. }
  493. </script>
  494. <style lang="scss" scoped>
  495. .j-checkbox {
  496. width: 18px;
  497. height: 18px;
  498. border-radius: 50%;
  499. border: 1px solid #e0e0e0;
  500. cursor: pointer;
  501. &.checked {
  502. border: 0;
  503. background: url('~@/assets/images/icon/checked.png') no-repeat;
  504. background-size: 20px;
  505. &[disabled] {
  506. background: url('~@/assets/images/icon/checked_disabled.png') no-repeat;
  507. background-size: 100%;
  508. }
  509. }
  510. &.half {
  511. border: 0;
  512. background: url('~@/assets/images/icon/checked-half.png') no-repeat;
  513. background-size: 20px;
  514. }
  515. }
  516. .j-button-item {
  517. &.global {
  518. padding: 6px 8px;
  519. height: 24px;
  520. line-height: 24px;
  521. font-weight: 700;
  522. color: inherit;
  523. border-color: rgba(0,0,0,.05);
  524. }
  525. }
  526. [class^=el-icon-] {
  527. transition: transform 0.2s ease;
  528. }
  529. .rotate180 {
  530. transform: rotate(180deg);
  531. }
  532. .j-button-item {
  533. border-color: transparent;
  534. }
  535. .select-group {
  536. font-size: 14px;
  537. &.global {
  538. padding: 0 18px;
  539. }
  540. .tab {
  541. display: flex;
  542. align-items: center;
  543. justify-content: space-between;
  544. padding: 0 18px;
  545. height: 40px;
  546. border-bottom: 1px solid rgba(0,0,0,.05);
  547. cursor: pointer;
  548. }
  549. .tab-name-container {
  550. display: flex;
  551. align-items: center;
  552. .tab-name {
  553. margin-left: 10px;
  554. font-weight: bold;
  555. }
  556. }
  557. .tab-content {
  558. padding: 0 18px;
  559. // border-bottom: 1px solid rgba(0,0,0,.05);
  560. .content-list {
  561. display: flex;
  562. flex-wrap: wrap;
  563. padding: 2px 0;
  564. }
  565. }
  566. }
  567. </style>