init
This commit is contained in:
443
uni_modules/nutui-uni/components/address/address.vue
Normal file
443
uni_modules/nutui-uni/components/address/address.vue
Normal file
@@ -0,0 +1,443 @@
|
||||
<script setup lang="ts">
|
||||
import type { ScrollViewOnScrollEvent } from '@uni-helper/uni-app-types'
|
||||
import { computed, defineComponent, reactive, ref, watch } from 'vue'
|
||||
import { CHANGE_EVENT, CLOSE_EVENT, PREFIX, SELECTED_EVENT, UPDATE_MODEL_EVENT, UPDATE_VISIBLE_EVENT } from '../_constants'
|
||||
import { getMainClass } from '../_utils'
|
||||
import requestAniFrame from '../_utils/raf'
|
||||
import { useTranslate } from '../../locale'
|
||||
import NutElevator from '../elevator/elevator.vue'
|
||||
import NutIcon from '../icon/icon.vue'
|
||||
import NutPopup from '../popup/popup.vue'
|
||||
import { addressEmits, addressProps } from './address'
|
||||
import type { AddressExistRegionData, AddressRegionData, CustomRegionData } from './type'
|
||||
|
||||
const props = defineProps(addressProps)
|
||||
const emit = defineEmits(addressEmits)
|
||||
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName)
|
||||
})
|
||||
|
||||
const showPopup = ref(props.visible)
|
||||
const privateType = ref(props.type)
|
||||
const tabIndex = ref(0)
|
||||
const prevTabIndex = ref(0)
|
||||
const tabName = ref(['province', 'city', 'country', 'town'])
|
||||
const scrollDis = ref([0, 0, 0, 0])
|
||||
const scrollTop = ref(0)
|
||||
const regionData = reactive<Array<AddressRegionData[]>>([])
|
||||
|
||||
const regionList = computed(() => {
|
||||
switch (tabIndex.value) {
|
||||
case 0:
|
||||
return props.province
|
||||
case 1:
|
||||
return props.city
|
||||
case 2:
|
||||
return props.country
|
||||
default:
|
||||
return props.town
|
||||
}
|
||||
})
|
||||
|
||||
function transformData(data: AddressRegionData[]) {
|
||||
if (!Array.isArray(data))
|
||||
throw new TypeError('params muse be array.')
|
||||
|
||||
if (!data.length)
|
||||
return []
|
||||
|
||||
data.forEach((item: AddressRegionData) => {
|
||||
if (!item.title)
|
||||
console.warn('[NutUI] <Address> 请检查数组选项的 title 值是否有设置 ,title 为必填项 .')
|
||||
})
|
||||
|
||||
const newData: CustomRegionData[] = []
|
||||
|
||||
data = data.sort((a: AddressRegionData, b: AddressRegionData) => {
|
||||
return a.title.localeCompare(b.title)
|
||||
})
|
||||
|
||||
data.forEach((item: AddressRegionData) => {
|
||||
const index = newData.findIndex((value: CustomRegionData) => value.title === item.title)
|
||||
if (index <= -1) {
|
||||
newData.push({
|
||||
title: item.title,
|
||||
list: ([] as any).concat(item),
|
||||
})
|
||||
}
|
||||
else {
|
||||
newData[index].list.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
return newData
|
||||
}
|
||||
|
||||
const selectedRegion = ref<AddressRegionData[]>([])
|
||||
|
||||
let selectedExistAddress = reactive({}) // 当前选择的地址
|
||||
|
||||
const closeWay = ref<'self' | 'mask' | 'cross'>('self')
|
||||
|
||||
// 设置选中省市县
|
||||
function initCustomSelected() {
|
||||
regionData[0] = props.province || []
|
||||
regionData[1] = props.city || []
|
||||
regionData[2] = props.country || []
|
||||
regionData[3] = props.town || []
|
||||
|
||||
const defaultValue = props.modelValue
|
||||
const num = defaultValue.length
|
||||
if (num > 0) {
|
||||
tabIndex.value = num - 1
|
||||
if (regionList.value.length === 0) {
|
||||
tabIndex.value = 0
|
||||
return
|
||||
}
|
||||
for (let index = 0; index < num; index++) {
|
||||
const arr: AddressRegionData[] = regionData[index]
|
||||
selectedRegion.value[index] = arr.filter((item: AddressRegionData) => item.id === defaultValue[index])[0]
|
||||
}
|
||||
scrollTo()
|
||||
}
|
||||
}
|
||||
|
||||
function getTabName(item: AddressRegionData | null, index: number) {
|
||||
if (item && item.name)
|
||||
return item.name
|
||||
if (tabIndex.value < index && item)
|
||||
return item.name
|
||||
|
||||
else
|
||||
return props.columnsPlaceholder[index] || translate('select')
|
||||
}
|
||||
|
||||
// 手动关闭 点击叉号(cross),或者蒙层(mask)
|
||||
function handClose(type = 'self') {
|
||||
closeWay.value = type === 'cross' ? 'cross' : 'self'
|
||||
|
||||
showPopup.value = false
|
||||
}
|
||||
|
||||
// 点击遮罩层关闭
|
||||
function clickOverlay() {
|
||||
closeWay.value = 'mask'
|
||||
}
|
||||
|
||||
// 切换下一级列表
|
||||
function nextAreaList(item: AddressRegionData) {
|
||||
const tab = tabIndex.value
|
||||
prevTabIndex.value = tabIndex.value
|
||||
const callBackParams: {
|
||||
next?: string
|
||||
value?: AddressRegionData
|
||||
custom: string
|
||||
} = {
|
||||
custom: tabName.value[tab],
|
||||
}
|
||||
|
||||
selectedRegion.value[tab] = item
|
||||
|
||||
// 删除右边已选择数据
|
||||
selectedRegion.value.splice(tab + 1, selectedRegion.value.length - (tab + 1))
|
||||
|
||||
callBackParams.value = item
|
||||
|
||||
if (regionData[tab + 1]?.length > 0) {
|
||||
tabIndex.value = tab + 1
|
||||
|
||||
callBackParams.next = tabName.value[tabIndex.value]
|
||||
|
||||
scrollToTop()
|
||||
}
|
||||
else {
|
||||
handClose()
|
||||
emit(UPDATE_MODEL_EVENT)
|
||||
}
|
||||
emit(CHANGE_EVENT, callBackParams)
|
||||
}
|
||||
// 切换地区Tab
|
||||
function changeRegionTab(item: AddressRegionData, index: number) {
|
||||
prevTabIndex.value = tabIndex.value
|
||||
if (getTabName(item, index)) {
|
||||
tabIndex.value = index
|
||||
scrollTo()
|
||||
}
|
||||
}
|
||||
|
||||
function scrollChange(e: ScrollViewOnScrollEvent) {
|
||||
scrollDis.value[tabIndex.value] = e.detail.scrollTop
|
||||
}
|
||||
|
||||
function scrollToTop() {
|
||||
// scrollTop 不会实时变更。当再次赋值时,scrollTop无变化时,不会触发滚动
|
||||
scrollTop.value += 1
|
||||
requestAniFrame(() => {
|
||||
setTimeout(() => {
|
||||
// 直接设置为0无效
|
||||
scrollTop.value = 0.01
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
function scrollTo() {
|
||||
// scrollTop 不会实时变更。当再次赋值时,scrollTop无变化时,不会触发滚动
|
||||
scrollTop.value += 1
|
||||
requestAniFrame(() => {
|
||||
setTimeout(() => {
|
||||
scrollTop.value = scrollDis.value[tabIndex.value]
|
||||
}, 10)
|
||||
})
|
||||
}
|
||||
|
||||
// 选择现有地址
|
||||
function selectedExist(item: AddressExistRegionData) {
|
||||
const copyExistAdd = props.existAddress
|
||||
let prevExistAdd: AddressExistRegionData = {} as AddressExistRegionData
|
||||
|
||||
copyExistAdd.forEach((list: AddressExistRegionData) => {
|
||||
if (list && list.selectedAddress)
|
||||
prevExistAdd = list
|
||||
list.selectedAddress = false
|
||||
})
|
||||
|
||||
item.selectedAddress = true
|
||||
|
||||
selectedExistAddress = item
|
||||
|
||||
emit(SELECTED_EVENT, prevExistAdd, item, copyExistAdd)
|
||||
|
||||
handClose()
|
||||
}
|
||||
// 初始化
|
||||
function initAddress() {
|
||||
selectedRegion.value = []
|
||||
tabIndex.value = 0
|
||||
scrollTo()
|
||||
}
|
||||
|
||||
// 关闭
|
||||
function close() {
|
||||
const data = {
|
||||
addressIdStr: '',
|
||||
addressStr: '',
|
||||
province: selectedRegion.value[0],
|
||||
city: selectedRegion.value[1],
|
||||
country: selectedRegion.value[2],
|
||||
town: selectedRegion.value[3],
|
||||
}
|
||||
|
||||
const callBackParams = {
|
||||
data: {},
|
||||
type: privateType.value,
|
||||
}
|
||||
|
||||
if (['custom', 'custom2'].includes(privateType.value)) {
|
||||
[0, 1, 2, 3].forEach((i) => {
|
||||
const item = selectedRegion.value[i]
|
||||
data.addressIdStr += `${i ? '_' : ''}${(item && item.id) || 0}`
|
||||
data.addressStr += (item && item.name) || ''
|
||||
})
|
||||
|
||||
callBackParams.data = data
|
||||
}
|
||||
else {
|
||||
callBackParams.data = selectedExistAddress
|
||||
}
|
||||
|
||||
initAddress()
|
||||
|
||||
if (closeWay.value === 'self')
|
||||
emit(CLOSE_EVENT, callBackParams)
|
||||
else
|
||||
emit('closeMask', { closeWay: closeWay.value })
|
||||
|
||||
emit(UPDATE_VISIBLE_EVENT, false)
|
||||
}
|
||||
|
||||
// 选择其他地址
|
||||
function switchModule() {
|
||||
const type = privateType.value
|
||||
privateType.value = type === 'exist' ? 'custom' : 'exist'
|
||||
initAddress()
|
||||
emit('switchModule', { type: privateType.value })
|
||||
}
|
||||
|
||||
function handleElevatorItem(key: string, item: AddressRegionData) {
|
||||
nextAreaList(item)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(value) => {
|
||||
showPopup.value = value
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => showPopup.value,
|
||||
(value) => {
|
||||
if (value)
|
||||
initCustomSelected()
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-address`
|
||||
const { translate } = useTranslate(componentName)
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NutPopup
|
||||
v-model:visible="showPopup"
|
||||
:z-index="zIndex"
|
||||
position="bottom"
|
||||
:lock-scroll="lockScroll"
|
||||
:round="round"
|
||||
@close="close"
|
||||
@click-overlay="clickOverlay"
|
||||
@open="closeWay = 'self'"
|
||||
>
|
||||
<view :class="classes" :style="customStyle">
|
||||
<view class="nut-address__header">
|
||||
<view class="nut-address__header-back" @click="switchModule">
|
||||
<slot v-if="type === 'exist' && privateType === 'custom'" name="backIcon">
|
||||
<NutIcon name="left" size="14px" />
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<view class="nut-address__header__title">
|
||||
{{
|
||||
privateType === 'custom'
|
||||
? customAddressTitle || translate('selectRegion')
|
||||
: existAddressTitle || translate('deliveryTo')
|
||||
}}
|
||||
</view>
|
||||
|
||||
<view class="nut-address__header-close" @click="handClose('cross')">
|
||||
<slot name="closeIcon">
|
||||
<NutIcon name="close" custom-color="#cccccc" size="14px" />
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 请选择 -->
|
||||
<view v-if="['custom', 'custom2'].includes(privateType)" class="nut-address__custom">
|
||||
<view class="nut-address__region">
|
||||
<view
|
||||
v-for="(item, index) in selectedRegion"
|
||||
:key="index"
|
||||
class="nut-address__region-item "
|
||||
:class="[index === tabIndex ? 'active' : '']"
|
||||
@click="changeRegionTab(item, index)"
|
||||
>
|
||||
<view>{{ getTabName(item, index) }} </view>
|
||||
<view class="nut-address__region-line--mini" :class="{ active: index === tabIndex }" />
|
||||
</view>
|
||||
<view v-if="tabIndex === selectedRegion.length" class="active nut-address__region-item">
|
||||
<view>{{ getTabName(null, selectedRegion.length) }} </view>
|
||||
<view class="nut-address__region-line--mini active" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="privateType === 'custom'" class="nut-address__detail">
|
||||
<div class="nut-address__detail-list">
|
||||
<scroll-view
|
||||
:scroll-y="true"
|
||||
:style="{ height: '100%' }"
|
||||
:scroll-top="scrollTop"
|
||||
@scroll="scrollChange"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in regionList"
|
||||
:key="index"
|
||||
class="nut-address__detail-item"
|
||||
:class="[selectedRegion[tabIndex]?.id === item.id ? 'active' : '']"
|
||||
@click="nextAreaList(item)"
|
||||
>
|
||||
<view>
|
||||
<slot v-if="selectedRegion[tabIndex]?.id === item.id" name="icon">
|
||||
<NutIcon name="Check" custom-class="nut-address-select-icon" width="13px" />
|
||||
</slot>{{ item.name }}
|
||||
</view>
|
||||
</div>
|
||||
</scroll-view>
|
||||
</div>
|
||||
</view>
|
||||
|
||||
<view v-else class="nut-address__elevator-group">
|
||||
<NutElevator
|
||||
:height="height"
|
||||
:index-list="transformData(regionList)"
|
||||
@click-item="handleElevatorItem"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 配送至 -->
|
||||
<view v-else-if="privateType === 'exist'" class="nut-address__exist">
|
||||
<div class="nut-address__exist-group">
|
||||
<ul class="nut-address__exist-group-list">
|
||||
<li
|
||||
v-for="(item, index) in existAddress"
|
||||
:key="index"
|
||||
class="nut-address__exist-group-item"
|
||||
:class="[item.selectedAddress ? 'active' : '']"
|
||||
@click="selectedExist(item)"
|
||||
>
|
||||
<slot v-if="!item.selectedAddress" name="unselectedIcon">
|
||||
<NutIcon name="location2" custom-class="nut-address-select-icon" width="13px" />
|
||||
</slot>
|
||||
|
||||
<slot v-if="item.selectedAddress" name="icon">
|
||||
<NutIcon name="Check" custom-class="nut-address-select-icon" width="13px" />
|
||||
</slot>
|
||||
|
||||
<div class="nut-address__exist-item-info">
|
||||
<div v-if="item.name && item.phone" class="nut-address__exist-item-info-top">
|
||||
<div class="nut-address__exist-item-info-name">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div class="nut-address__exist-item-info-phone">
|
||||
{{ item.phone }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nut-address__exist-item-info-bottom">
|
||||
<view>
|
||||
{{ item.provinceName + item.cityName + item.countyName + item.townName + item.addressDetail }}
|
||||
</view>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="isShowCustomAddress" class="nut-address__exist-choose" @click="switchModule">
|
||||
<div class="nut-address__exist-choose-btn">
|
||||
{{
|
||||
customAndExistTitle || translate('chooseAnotherAddress')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="!isShowCustomAddress">
|
||||
<slot name="bottom" />
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</NutPopup>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
Reference in New Issue
Block a user