chuangte_bike_newxcx/page_user/verify/components/AreaSelectPopup.vue
2026-02-26 18:05:57 +08:00

412 lines
9.7 KiB
Vue

<template>
<u-popup v-model="visible" mode="bottom" :border-radius="24" @open="onOpen" @close="onClose">
<view class="area-popup">
<view class="popup-header">
<text class="title">选择运营区</text>
<view class="close-btn" @click="close">
<u-icon name="close" size="36" color="#666"></u-icon>
</view>
</view>
<!-- 地图区域 -->
<view class="map-section">
<map
id="verifyAreaMap"
class="map"
:latitude="mapCenter.lat"
:longitude="mapCenter.lng"
:markers="markers"
:show-location="true"
scale="14"
@regionchange="onRegionChange"
></map>
<!-- 地图中心标识,提示用户可拖动查询 -->
<view class="center-pin">
<view class="pin-dot"></view>
<view class="pin-stem"></view>
</view>
<view class="map-tip" @click="onRelocate">
<u-icon name="map" size="28" color="#fff"></u-icon>
<text>重新定位</text>
</view>
</view>
<!-- 运营区列表 -->
<view class="area-list-section">
<view class="list-title">
<u-icon name="location" size="28" color="#3996FD"></u-icon>
<text>已开通运营区</text>
</view>
<scroll-view scroll-y class="area-list" :style="{ height: listHeight }">
<view
v-for="(item, idx) in areaList"
:key="item.id"
class="area-item"
:class="{ active: selectedArea && selectedArea.id === item.id }"
@click="onSelectArea(item)"
>
<view class="area-index">{{ idx + 1 }}</view>
<view class="area-main">
<view class="area-name">{{ item.name }}</view>
<view class="area-region">{{ item.regionMergerName || item.regionName || '--' }}</view>
</view>
<u-icon name="arrow-right" size="28" color="#c8c9cc"></u-icon>
</view>
<view v-if="loading" class="loading-wrap">
<u-loading mode="circle" size="40"></u-loading>
<text>加载中...</text>
</view>
<view v-else-if="!loading && areaList.length === 0" class="empty-wrap">
<u-empty text="暂无开通核销的运营区" mode="list"></u-empty>
</view>
</scroll-view>
</view>
</view>
</u-popup>
</template>
<script>
/**
* 运营区选择弹窗组件
* - 顶部地图,支持重新定位
* - 底部运营区列表(仅开通核销功能的运营区,按距离排序)
*/
export default {
name: 'AreaSelectPopup',
props: {
value: {
type: Boolean,
default: false
},
selectedArea: {
type: Object,
default: null
},
appId: {
type: [String, Number],
default: '1'
}
},
data() {
return {
visible: false,
mapCenter: { lat: 0, lng: 0 },
areaList: [],
loading: false,
listHeight: '400rpx',
markers: [],
regionChangeTimer: null,
mapContext: null
}
},
watch: {
value: {
immediate: true,
handler(val) {
this.visible = !!val
}
},
visible(val) {
this.$emit('input', val)
}
},
methods: {
onOpen() {
this.fetchLocationAndAreas()
},
onClose() {
if (this.regionChangeTimer) {
clearTimeout(this.regionChangeTimer)
this.regionChangeTimer = null
}
this.$emit('close')
},
close() {
this.visible = false
},
/** 获取定位并拉取运营区列表 */
async fetchLocationAndAreas() {
this.loading = true
this.areaList = []
try {
const loc = await this.getLocation()
if (loc) {
this.mapCenter = { lat: loc.latitude, lng: loc.longitude }
this.markers = [{
id: 1,
latitude: loc.latitude,
longitude: loc.longitude,
iconPath: '/static/image/icon8.png',
width: 30,
height: 30
}]
setTimeout(() => this.initMapContext(), 300)
await this.fetchAreaList(loc.longitude, loc.latitude)
} else {
this.$u.toast('获取位置失败,请检查定位权限')
}
} catch (e) {
this.$u.toast(e.message || '加载失败')
} finally {
this.loading = false
}
},
/** 获取定位 */
getLocation() {
return new Promise((resolve) => {
uni.getLocation({
type: 'gcj02',
success: (res) => resolve(res),
fail: () => resolve(null)
})
})
},
/** 初始化地图上下文 */
initMapContext() {
this.mapContext = uni.createMapContext('verifyAreaMap', this)
},
/** 拉取附近开通核销的运营区(根据地图中心) */
async fetchAreaList(lng, lat) {
this.loading = true
try {
const url = `/app/area/nearbyVerifyList?appId=${this.appId}&center=${lng}&center=${lat}`
const res = await this.$u.get(url)
if (res.code === 200 && Array.isArray(res.data)) {
this.areaList = res.data
} else {
this.areaList = []
}
} finally {
this.loading = false
}
},
/** 地图区域变化(拖动/缩放结束时查询中心点附近运营区) */
onRegionChange(e) {
if (e.type !== 'end' || e.causedBy !== 'drag') return
if (this.regionChangeTimer) clearTimeout(this.regionChangeTimer)
this.regionChangeTimer = setTimeout(() => {
this.getMapCenterAndFetch()
}, 200)
},
/** 获取地图中心点并拉取运营区列表 */
getMapCenterAndFetch() {
if (!this.mapContext) this.initMapContext()
if (!this.mapContext) return
this.mapContext.getCenterLocation({
type: 'gcj02',
success: (res) => {
const lng = res.longitude
const lat = res.latitude
this.mapCenter = { lat, lng }
this.fetchAreaList(lng, lat)
}
})
},
/** 重新定位 */
onRelocate() {
this.fetchLocationAndAreas()
},
/** 选择运营区 */
onSelectArea(area) {
this.$emit('select', area)
this.close()
}
}
}
</script>
<style lang="scss" scoped>
.area-popup {
padding-bottom: env(safe-area-inset-bottom);
background: linear-gradient(180deg, #f8fbff 0%, #fff 120rpx);
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 32rpx 24rpx;
.title {
font-size: 36rpx;
font-weight: 600;
color: #1a1a1a;
letter-spacing: 0.5rpx;
}
.close-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #f5f6f8;
&:active {
background: #ebedf0;
}
}
}
.map-section {
position: relative;
height: 340rpx;
margin: 0 24rpx 24rpx;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 8rpx 32rpx rgba(57, 150, 253, 0.08);
.map {
width: 100%;
height: 100%;
border-radius: 20rpx;
}
/* 地图中心标识,固定于视口中心,提示用户可拖动地图查询 */
.center-pin {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -100%);
pointer-events: none;
z-index: 2;
}
.pin-dot {
width: 24rpx;
height: 24rpx;
background: #3996FD;
border: 4rpx solid #fff;
border-radius: 50%;
margin: 0 auto 0;
box-shadow: 0 2rpx 12rpx rgba(57, 150, 253, 0.4);
}
.pin-stem {
width: 4rpx;
height: 36rpx;
background: #3996FD;
margin: -4rpx auto 0;
border-radius: 2rpx;
box-shadow: 0 2rpx 8rpx rgba(57, 150, 253, 0.3);
}
.map-tip {
position: absolute;
bottom: 24rpx;
right: 24rpx;
display: flex;
align-items: center;
padding: 16rpx 28rpx;
background: linear-gradient(135deg, #3996FD 0%, #5eb3ff 100%);
border-radius: 40rpx;
font-size: 26rpx;
color: #fff;
box-shadow: 0 6rpx 20rpx rgba(57, 150, 253, 0.35);
text {
margin-left: 10rpx;
}
&:active {
opacity: 0.9;
}
}
}
.area-list-section {
padding: 0 24rpx 32rpx;
.list-title {
display: flex;
align-items: center;
font-size: 28rpx;
color: #646566;
margin-bottom: 20rpx;
font-weight: 500;
text {
margin-left: 10rpx;
}
}
.area-list {
border-radius: 20rpx;
background: #fff;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.04);
border: 1rpx solid #f0f1f3;
}
.area-item {
display: flex;
align-items: center;
padding: 28rpx 28rpx;
border-bottom: 1rpx solid #f5f6f8;
transition: background 0.2s;
&:last-child {
border-bottom: none;
}
&:first-child {
border-radius: 20rpx 20rpx 0 0;
}
&:active {
background: #fafbfc;
}
&.active {
background: linear-gradient(90deg, rgba(57, 150, 253, 0.06) 0%, rgba(57, 150, 253, 0.02) 100%);
}
.area-index {
width: 44rpx;
height: 44rpx;
line-height: 44rpx;
text-align: center;
font-size: 24rpx;
font-weight: 600;
color: #3996FD;
background: rgba(57, 150, 253, 0.1);
border-radius: 12rpx;
margin-right: 20rpx;
flex-shrink: 0;
}
.area-main {
flex: 1;
min-width: 0;
}
.area-name {
font-size: 32rpx;
color: #1a1a1a;
font-weight: 500;
margin-bottom: 6rpx;
line-height: 1.4;
}
.area-region {
font-size: 24rpx;
color: #969799;
line-height: 1.35;
}
}
.loading-wrap,
.empty-wrap {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx;
color: #969799;
font-size: 28rpx;
text {
margin-top: 20rpx;
}
}
}
</style>