Files
cmgd-mini-app/uni_modules/nutui-uni/components/address/address.vue
2026-01-05 12:47:14 +08:00

444 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>