Vue 折騰記 – (8) 寫一個挺靠譜的多地區選擇元件

CRPER發表於2017-08-20

前言

這個不是三級聯動的地址元件;

這是在這個基礎需求上增加多地區選擇的功能;

我也不想這麼個玩意的,但是產品需求就是有這麼個東東.

上基友社群找了下,又木有這型別的元件…只能自己動手豐衣足食了..

雖然過程遇到了許許多多的坑點,但總算是搞出來了..

依舊不廢話..看看效果圖,再扯..

  • 2017-8-22 : 修正一丟丟的邏輯BUG

效果圖

功能點:

  • 支援不限城市,不限地區(這個東西的實現..真心死磕了挺久) — 左右兩邊資料的同步
  • 地區一次性多選,若是選擇了所有地區會自動轉為不限地區
  • 資料遷移箭頭的判斷..選中資料才會有對應的按鈕可以點選
  • 已選位置的資料同步響應呼叫的地方,當然也可以外部傳入…(新增傳出,編輯依賴傳入再組合)
  • 箭頭是iconfont,支援外部傳入,預設是我這邊的cdn的啦….

!!!這是一個獨立的元件,css預處理是用的scss;

寫的過程遇到的問題:

  • 因為這個功能會多次需要切換省份城市這些,所以我一次性拉取所有資料儲存到localstorage,不然請求介面次數太多了
  • 一開始的不限城市和不限地區我想併入JSON(重組JSON),但是發現雖然能降低處理麻煩,但是資料更亂了…非常不好維護,也有其他元件呼叫這個JSON的地方(比如其他元件有個地址的三級聯動也是依賴這個JSON)
    • 還有一個就是要考慮兩邊有不限制這東東的時候,要剔除不能存在的資料,比如你不限制城市,那所有該省份的城市都要幹掉,不限制地區也是類似
  • 寫左右兩邊資料的對比是最噁心的,為什麼這麼說呢?
    • 左邊三級聯動的,每個子項都有自己的idname, 而選擇的是組合成的(看GIF圖),中間是中劃線隔開,這對於推入和推出就帶來一堆遍歷和比較
    • 我們這邊的後端大佬說不限制的id均為0(城市或者地區),所以這個需要自行組合,最後就是動態圖那格式的ID就是後臺接受的,,多地區再拼接成字串….'
      3-13-2,2-44-3,4-0-0'
      這種提交到後臺..

聯動JSON資料格式

  • regionName: 項的名稱
  • regionId: 項的ID
  • child: 是否包含有子項

本來想寫個props對映下regionName,regionId,child;
但是感覺作用不大,就沒寫了,(一般公司的地區JSON格式定下來了之後變動的可能性太低)

你能學到什麼?

1: 陣列的比對,陣列的遍歷,陣列的組合及響應判斷
2: vue一些內建指令的使用
3: 元件功能細節的考慮,不限制地區,全部這些按鈕在什麼情況下能點選
4: 清空資料之後各個狀態的恢復和重置等等


程式碼

  • manyAreaSelect.vue
<
template>
<
div class="manyAreaSelect">
<
div class="item">
<
div class="item-title">
<
span>
選擇省<
/span>
<
/div>
<
div class="item-content">
<
ul>
<
li v-for="(item,index) in chinaArea" :class="item.selected?'active':''" :key="index" @click="getCityList(item)">
{{item.regionName
}
}<
/li>
<
/ul>
<
/div>
<
div class="item-footer">
<
/div>
<
/div>
<
div class="item">
<
div class="item-title">
<
span>
選擇市<
/span>
<
/div>
<
div class="item-content">
<
ul v-show="cityList.length===0">
<
li>
<
<
請選擇省份<
/li>
<
/ul>
<
ul v-show="!notLimitButton.notLimitCity &
&
cityList.length!==0">
<
li v-for="(item,index) in cityList" :class="item.selected ? 'active':''" :key="index" @click="getDistricList(item)">
{{item.regionName
}
}<
/li>
<
/ul>
<
/div>
<
div class="item-footer">
<
button type="button" class="button" :class="notLimitButton.notLimitCity?'success':''" @click="cityNotLitmit({regionName:'不限',regionId:'0'
})" :disabled="!selectItem.province.regionName">
不限城市<
/button>
<
/div>
<
/div>
<
div class="item">
<
div class="item-title">
<
span>
選擇地區<
/span>
<
/div>
<
div class="item-content">
<
ul v-show="districList.length===0">
<
li>
<
<
請選擇城市<
/li>
<
/ul>
<
ul v-show="!notLimitButton.notLimitCity &
&
!notLimitButton.notLimitDistrict &
&
districList.length!==0">
<
li v-for="(item,index) in districList" :class="item.selected?'active':''" :key="index" @click="getAreaCombineID(item)">
{{item.regionName
}
}<
/li>
<
/ul>
<
/div>
<
div class="item-footer">
<
button type="button" class="button" :class="notLimitButton.notLimitDistrict ?'success':''" @click="districNotLitmit({regionName:'不限',regionId:'0'
})" :disabled="!selectItem.city.regionName ||!selectItem.province.regionName || notLimitButton.notLimitCity ">
不限地區<
/button>
<
/div>
<
/div>
<
div class="trangle">
<
div class="trangle-wrap">
<
div class="left">
<
button type="button" class="button" @click="transferToRight" :disabled="direactionStatusToRight">
<
i :class="this.iconDirection.right">
<
/i>
<
/button>
<
/div>
<
div class="right">
<
button type="button" class="button" @click="transferToLeft" :disabled="direactionStatusToLeft">
<
i :class="this.iconDirection.left">
<
/i>
<
/button>
<
/div>
<
/div>
<
/div>
<
div class=" item ">
<
div class="item-title ">
<
span>
已選位置<
/span>
<
/div>
<
div class="item-content ">
<
ul class="selectedContent">
<
li v-for="(item,index) in selectedList" :class="item.selected?'active':''" :key="index" @click="selectedAreaSingle(item)">
{{item.regionName
}
}<
/li>
<
/ul>
<
/div>
<
div class="item-footer">
<
button type="button" class="button" @click="selectedAllArea()" :disabled="rightDataList.length=== 0" :class="selectedAllButtonStatus?'success':''">
{{selectedAllButtonStatus?'反選':'全部'
}
}<
/button>
<
/div>
<
/div>
<
/div>
<
/template>
<
script>
import _ from 'lodash';
export default {
name: 'manyAreaSelect', data: function () {
return {
chinaArea: JSON.parse(window.localStorage.getItem('chinaArea')) || [], // 這是地區聯動的JSON notLimitButton: {
notLimitCity: false, // 城市不限 notLimitDistrict: false, // 地區不限
}, selectedAllButtonStatus: false, // 已選位置列表全部按鈕的狀態 selectItem: {
province: {
}, city: {
}, distric: {
}
}, cityList: [], // 城市列表 districList: [], // 區域列表 rightDataList: [], // 選中專案組合成的渲染列表 rightData: [], // 選中需要移除的 leftData: [], // 左邊選中的轉發
}
}, props: {
selectedData: {
type: [String, Object, Array]
}, iconDirection: {
type: Object, default: function () {
// 箭頭圖示 return {
left: 'fzicon fz-ad-you', right: 'fzicon fz-ad-right'
}
}
}
}, computed: {
selectedList () {
// 已選中列表 if (this.selectedData &
&
this.selectedData !== '') {
this.rightDataList = this.selectedData;
return this.rightDataList;

} else {
return this.rightDataList;

}
}, direactionStatusToRight () {
// 控制可以轉移的箭頭狀態 if (this.notLimitButton.notLimitCity || this.notLimitButton.notLimitDistrict) {
if (this.notLimitButton.notLimitCity) {
this.removeAllSelected(this.cityList);
this.removeAllSelected(this.districList);
return false;

} else {
if (this.notLimitButton.notLimitDistrict) {
this.removeAllSelected(this.districList);
return false;

}
} return false;

} else {
if (this.selectItem.distric.regionName) {
return false;

} return true;

}
}, direactionStatusToLeft () {
// 控制可以轉移的箭頭狀態 if (this.rightData.length === 0) {
return true
} else {
return false
}
}
}, methods: {
mapSelect (list, value, type) {
// 高亮選中 if (type) {
return list.map(pitem =>
{
if (pitem.regionId === value.regionId) {
if (value.selected &
&
value.selected === true) {
this.$delete(pitem, 'selected');

} else {
this.$set(pitem, 'selected', true)
}
}
})
} else {
return list.map(pitem =>
{
if (pitem.regionId === value.regionId) {
if (value.selected &
&
value.selected === true) {
this.$delete(pitem, 'selected');

} else {
this.$set(pitem, 'selected', true)
}
} else {
this.$delete(pitem, 'selected');

}
})
}
}, resetToDefault () {
this.leftData = [];
// 清空需要轉移的陣列 this.notLimitButton = {
// 重置按鈕狀態 notLimitCity: false, // 城市不限 notLimitDistrict: false, // 地區不限
};
this.selectItem.city = {
};
this.selectItem.distric = {
} this.removeAllSelected(this.cityList);
// 清除選中狀態 this.removeAllSelected(this.districList);
// 清除選中狀態 this.cityList = [];
this.districList = [];

}, getCityList (item) {
this.resetToDefault();
if (item) {
this.cityList = item.child;
// 獲取城市列表 this.selectItem.province = item;
// 儲存省份物件 this.mapSelect(this.chinaArea, item);
// 高亮選擇,單選
}
}, getDistricList (item) {
this.leftData = [];
// 清空需要轉移的陣列 this.notLimitButton.notLimitDistrict = false;
// 重置按鈕狀態 this.removeAllSelected(this.districList);
// 清除選中狀態 this.selectItem.distric = {
};
this.districList = [];
if (item) {
this.districList = item.child;
// 獲取區域列表 this.selectItem.city = item;
// 儲存省份物件 this.mapSelect(this.cityList, item);
// 高亮選擇,單選
}
}, getAreaCombineID (item) {
// 獲取組合ID console.log(this.leftData) if (item) {
this.selectItem.distric = item;
this.mapSelect(this.districList, item, 'manySelect');
// 區域高亮選擇,多選 this.leftData.push({
regionName: this.selectItem.province.regionName + '-' + this.selectItem.city.regionName + '-' + item.regionName, regionId: this.selectItem.province.regionId + '-' + this.selectItem.city.regionId + '-' + item.regionId
}) this.leftData = _.uniqBy(this.leftData, 'regionId');
if (this.leftData.length === this.districList.length) {
this.leftData = [];
this.selectItem.distric = {
};
this.notLimitButton.notLimitDistrict = true;
// 轉為不限制地區 this.leftData.push({
regionName: this.selectItem.province.regionName + '-' + this.selectItem.city.regionName + '-不限', regionId: this.selectItem.province.regionId + '-' + this.selectItem.city.regionId + '-0'
})
}
}
}, cityNotLitmit (item) {
// 城市不限 this.leftData = [];
// 請空陣列 this.selectItem.distric = {
};
this.removeAllSelected(this.districList);
this.notLimitButton.notLimitCity = !this.notLimitButton.notLimitCity;
// 不限按鈕狀態 this.leftData.push({
regionName: this.selectItem.province.regionName + '-不限-不限', regionId: this.selectItem.province.regionId + '-0-0'
})
}, districNotLitmit (item) {
// 區域不限 this.leftData = [];
// 請空陣列 this.notLimitButton.notLimitDistrict = !this.notLimitButton.notLimitDistrict;
// 不限按鈕狀態 this.leftData.push({
regionName: this.selectItem.province.regionName + '-' + this.selectItem.city.regionName + '-不限', regionId: this.selectItem.province.regionId + '-' + this.selectItem.city.regionId + '-0'
}) if (!this.notLimitButton.notLimitDistrict) {
this.leftData = [];

}
}, transferToRight () {
// 選中推入到已選中列表區域 if (this.leftData &
&
this.leftData.length !== 0) {
if (this.leftData.length === 1) {
// 長度只有1,那就只有不限城市或者地區了 let limitId = this.leftData[0].regionId.split('-');
// 比對比對,切割成陣列 this.rightDataList.map(item =>
{
let id = item.regionId.split('-');
if (limitId[0] === id[0]) {
if (limitId[1] === '0') {
// 不限城市 this.rightDataList = this.rightDataList.filter(ritem =>
{
let rid = ritem.regionId.split('-');
if (limitId[0] !== rid[0]) {
return ritem;

}
})
} else {
if (limitId[2] === '0') {
// 不限地區 this.rightDataList = this.rightDataList.filter(ritem =>
{
let rid = ritem.regionId.split('-');
if ((limitId[0] === rid[0] &
&
limitId[1] === rid[1])) {
if (ritem[2] === '0') {
return ritem;

}
} else {
if (limitId[0] !== rid[0] || limitId[1] !== rid[1]) {
return ritem;

}
}
})
} else {
this.rightDataList = this.rightDataList.filter(ritem =>
{
let rid = ritem.regionId.split('-');
if (limitId[0] === rid[0]) {
if (limitId[1] === rid[1]) {
if (!(rid[2] === '0')) {
return ritem;

}
} else {
if (!(rid[1] === '0')) {
return ritem
}
}
} else {
return ritem
}
})
}
}
}
})
} else {
let limitId = this.leftData[0].regionId.split('-');
// 比對比對,切割成陣列 this.rightDataList = this.rightDataList.filter(ritem =>
{
let rid = ritem.regionId.split('-');
if (limitId[0] === rid[0]) {
if (limitId[1] === rid[1]) {
if (!(rid[2] === '0')) {
return ritem;

}
} else {
if (!(rid[1] === '0')) {
return ritem
}
}
} else {
return ritem
}
})
} this.leftData.map(item =>
{
this.rightDataList.push(item);

}) this.rightDataList = _.uniqBy(this.rightDataList, 'regionId');
this.resetToDefault();

}
}, selectedAreaSingle (item) {
// 已選擇區域單個選擇 if (item) {
this.rightData = [];
this.mapSelect(this.rightDataList, item, 'manySelect');
// 區域高亮選擇,多選 this.rightDataList.map(item =>
{
if (item.selected) {
this.rightData.push(item)
}
})
}
}, selectedAllArea () {
// 已選中區域全選反選 if (this.selectedAllButtonStatus) {
this.removeAllSelected(this.rightDataList);
this.rightData = [];

} else {
this.rightDataList.map(item =>
this.$set(item, 'selected', true));
this.rightData = this.rightDataList;

} this.selectedAllButtonStatus = !this.selectedAllButtonStatus;

}, transferToLeft () {
// 從已選中列表區域退回待轉發區域 if (this.rightData &
&
this.rightData.length !== 0) {
this.rightDataList = this.rightDataList.filter(item =>
{
if (!item.selected) {
return item;

}
}) this.rightData = [];

}
}, removeAllSelected (list) {
// 清空選中狀態 list.map(item =>
this.$delete(item, 'selected'));

}
}, watch: {
'rightDataList' (newValue, oldValue) {
// 選擇列表的值變動響應外部值的變動 if (newValue.length !== this.rightData.length) {
this.selectedAllButtonStatus = false;

} else {
if (newValue.length === 0) {
this.selectedAllButtonStatus = false;

} else {
this.selectedAllButtonStatus = true;

}
} this.$emit('update:selectedData', newValue);

}
}
}<
/script>
<
style scoped lang="scss">
ul {
padding: 0;
margin: 0;
max-height: 100%;
overflow-y: auto;
li {
cursor: pointer;
text-align: center;
padding: 5px;
&
.active, &
:hover {
background: #e4e8f1;
color: #48576a;

}
}
} .manyAreaSelect {
position: relative;
z-index: 2005;
.item {
border: 1px solid #d1dbe5;
background: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
display: inline-block;
vertical-align: middle;
min-width: 180px;
box-sizing: border-box;
position: relative;
height: 100%;
min-height: 260px;
margin: 5px;

} .item-title {
height: 36px;
line-height: 36px;
background: #fbfdff;
margin: 0;
border-bottom: 1px solid #d1dbe5;
box-sizing: border-box;
color: #1f2d3d;
text-align: center;

} .trangle {
background: transparent;
display: inline-block;
vertical-align: middle;
width: 40px;
box-sizing: border-box;
height: 100%;
position: relative;
.trangle-wrap {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);

} .left, .right {
margin: 10px 5px;

} ;

} .item-content {
font-size: 13px;
height: 190px;
padding: 8px 2px;

} .item-footer {
padding: 5px 0;
height: 40px;
text-align: center;

}
} .selectedContent {
li {
text-align: left;
padding-left: 25px;

}
} .button {
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
background: #fff;
border: 1px solid #c4c4c4;
color: #1f2d3d;
margin: 0;
border-radius: 4px;
padding: 4px;
font-size: 12px;
border-radius: 4px;
-webkit-appearance: button;
outline: none;
&
.success {
background: #42d885;
border-color: #42d885;
color: #fff;

} &
:disabled {
color: #bfcbd9;
cursor: not-allowed;
background-image: none;
background-color: #eef1f6;
border-color: #d1dbe5;

}
}<
/style>
複製程式碼
  • 用法
<
!--selectedData就是響應的資料.sync是2.3迴歸的語法糖-->
<
!--可以繫結iconDirection傳入箭頭的iconfont,Object-->
<
many-area-select :selectedData.sync="manyAreaValue">
<
/many-area-select>
複製程式碼

總結

這個元件的出爐,折騰了很久..

寫的過程推倒了三版(三天三個版本),都是思路沒想對和理清..寫著寫著就寫不下去了…

這個元件目前的功能是滿足我這邊的需求的,若是有更好的實現方式可以往下面留言..

謝謝..

來源:https://juejin.im/post/59998b71f265da247a15814b

相關文章