获取位置组件封装

This commit is contained in:
WindowBird 2025-08-19 09:51:32 +08:00
parent eb5800c22f
commit 879190c956
3 changed files with 639 additions and 391 deletions

View File

@ -0,0 +1,182 @@
# MapLocation 地图定位组件
一个可复用的地图定位组件,集成了地址输入、位置获取、地图打开等功能。
## 功能特性
- 📍 自动获取当前位置
- 🗺️ 一键打开系统地图
- 📝 支持手动输入地址
- 📱 跨平台支持APP、微信小程序、H5
- 🎨 美观的UI设计
- 🔧 高度可配置
## 使用方法
### 基础用法
```vue
<template>
<map-location
v-model="address"
label="地址"
placeholder="请输入或选择收货地址"
/>
</template>
<script>
import MapLocation from '@/components/map-location/map-location.vue'
export default {
components: {
MapLocation
},
data() {
return {
address: ''
}
}
}
</script>
```
### 完整用法
```vue
<template>
<map-location
v-model="formData.address"
label="收货地址"
placeholder="请输入或选择收货地址"
@location-success="onLocationSuccess"
@location-error="onLocationError"
@use-location="onUseLocation"
@map-opened="onMapOpened"
@address-focus="onAddressFocus"
@address-input="onAddressInput"
/>
</template>
<script>
import MapLocation from '@/components/map-location/map-location.vue'
export default {
components: {
MapLocation
},
data() {
return {
formData: {
address: ''
}
}
},
methods: {
// 位置获取成功
onLocationSuccess(location) {
console.log('位置获取成功:', location)
// location 包含: { address, latitude, longitude }
},
// 位置获取失败
onLocationError(error) {
console.error('位置获取失败:', error)
},
// 使用位置
onUseLocation(location) {
console.log('使用位置:', location)
},
// 地图打开成功
onMapOpened() {
console.log('地图已打开')
},
// 地址输入框获得焦点
onAddressFocus() {
console.log('地址输入框获得焦点')
},
// 地址输入
onAddressInput(value) {
console.log('地址输入:', value)
}
}
}
</script>
```
## Props 属性
| 属性名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| label | String | '地址' | 表单标签 |
| placeholder | String | '请输入或选择收货地址' | 输入框占位符 |
| value | String | '' | 地址值支持v-model |
| showLocationCard | Boolean | true | 是否显示位置建议卡片 |
## Events 事件
| 事件名 | 参数 | 说明 |
|--------|------|------|
| input | String | 地址值变化时触发 |
| location-success | Object | 位置获取成功时触发 |
| location-error | Error | 位置获取失败时触发 |
| use-location | Object | 点击使用位置时触发 |
| map-opened | - | 地图打开成功时触发 |
| address-focus | - | 地址输入框获得焦点时触发 |
| address-input | String | 地址输入时触发 |
## 位置对象结构
```javascript
{
address: "纬度: 39.123456, 经度: 116.123456",
latitude: 39.123456,
longitude: 116.123456
}
```
## 方法
组件提供了以下方法供外部调用:
### setLocation(location)
手动设置位置信息
```javascript
// 在父组件中调用
this.$refs.mapLocation.setLocation({
address: '北京市朝阳区',
latitude: 39.123456,
longitude: 116.123456
})
```
### clearLocation()
清空位置信息
```javascript
// 在父组件中调用
this.$refs.mapLocation.clearLocation()
```
## 平台支持
- ✅ APP-PLUS使用系统地图应用
- ✅ MP-WEIXIN使用微信小程序地图
- ✅ H5使用高德地图在线版
## 依赖
- `@/utils/permission.js`:权限处理工具
- uni-app 定位API
- uni-app 地图API
## 注意事项
1. 首次使用需要用户授权定位权限
2. 不同平台的权限处理可能略有差异
3. H5环境下需要网络连接才能使用地图功能
4. 建议在真机上测试定位功能

View File

@ -0,0 +1,431 @@
<template>
<view class="map-location-component">
<!-- 地址输入框 -->
<view class="form-item address-form-item">
<text class="field-label">{{ label || '地址' }}</text>
<view class="address-input-wrapper">
<input
v-model="addressValue"
class="address-input"
:placeholder="placeholder || '请输入或选择收货地址'"
@focus="onAddressFocus"
@input="onAddressInput"
/>
<view
class="map-icon-wrapper"
title="点击获取定位并打开地图"
@click="openMapWithLocation"
>
<text class="map-icon">🗺</text>
</view>
</view>
</view>
<!-- 当前定位 -->
<view class="location-suggestion">
<view v-if="currentLocation" class="location-card">
<view class="location-content">
<view class="location-header">
<text class="location-title">当前定位</text>
</view>
<text class="location-address">{{ currentLocation.address }}</text>
</view>
<button class="use-location-btn" @click="useCurrentLocation">使用</button>
</view>
<view v-else class="location-card">
<view class="location-content">
<view class="location-header">
<text class="location-title">获取位置</text>
<text class="location-company">点击获取当前位置</text>
</view>
<text class="location-address">需要定位权限</text>
</view>
<button class="get-location-btn" @click="getCurrentLocation">获取</button>
</view>
</view>
</view>
</template>
<script>
import { getLocationWithPermission, handleLocationError } from '@/utils/permission.js'
export default {
name: 'MapLocation',
props: {
//
label: {
type: String,
default: '地址'
},
//
placeholder: {
type: String,
default: '请输入或选择收货地址'
},
//
value: {
type: String,
default: ''
},
//
showLocationCard: {
type: Boolean,
default: true
}
},
data() {
return {
currentLocation: null,
isMapTriggered: false,
addressValue: this.value
}
},
watch: {
value(newVal) {
this.addressValue = newVal
},
addressValue(newVal) {
this.$emit('input', newVal)
}
},
methods: {
//
async getCurrentLocation() {
try {
uni.showLoading({
title: '获取位置中...',
})
const location = await getLocationWithPermission()
console.log('位置信息:', location)
// 使
this.currentLocation = {
address: `纬度: ${location.latitude.toFixed(6)}, 经度: ${location.longitude.toFixed(6)}`,
latitude: location.latitude,
longitude: location.longitude,
}
uni.hideLoading()
//
if (!this.isMapTriggered) {
uni.showToast({
title: '位置获取成功,点击地图图标查看',
icon: 'success',
duration: 3000,
})
}
//
this.$emit('location-success', this.currentLocation)
return this.currentLocation
} catch (err) {
uni.hideLoading()
handleLocationError(err)
this.$emit('location-error', err)
throw err
}
},
//
onAddressFocus() {
this.$emit('address-focus')
},
//
onAddressInput(e) {
this.$emit('address-input', e.detail.value)
},
//
async openMapWithLocation() {
try {
this.isMapTriggered = true
//
await this.getCurrentLocation()
//
this.openMap()
} catch (err) {
console.error('获取位置失败:', err)
uni.showToast({
title: '获取位置失败',
icon: 'error',
})
} finally {
this.isMapTriggered = false
}
},
//
openMap() {
if (!this.currentLocation) {
uni.showToast({
title: '请先获取位置信息',
icon: 'none',
})
return
}
const { latitude, longitude } = this.currentLocation
//
uni.showLoading({
title: '打开地图中...',
})
//
// #ifdef APP-PLUS
uni.openLocation({
latitude: latitude,
longitude: longitude,
name: '当前位置',
address: this.currentLocation.address || '未知地址',
scale: 18,
success: () => {
uni.hideLoading()
console.log('地图打开成功')
this.$emit('map-opened')
},
fail: err => {
uni.hideLoading()
console.error('打开地图失败:', err)
this.showMapInApp()
},
})
// #endif
// #ifdef MP-WEIXIN
uni.openLocation({
latitude: latitude,
longitude: longitude,
name: '当前位置',
address: this.currentLocation.address || '未知地址',
scale: 18,
success: () => {
uni.hideLoading()
this.$emit('map-opened')
},
fail: err => {
uni.hideLoading()
console.error('打开地图失败:', err)
this.showMapInApp()
},
})
// #endif
// #ifdef H5
// H5使线
uni.hideLoading()
const mapUrl = `https://uri.amap.com/marker?position=${longitude},${latitude}&name=${encodeURIComponent('当前位置')}&address=${encodeURIComponent(this.currentLocation.address || '未知地址')}`
window.open(mapUrl, '_blank')
this.$emit('map-opened')
// #endif
},
//
showMapInApp() {
uni.showModal({
title: '地图功能',
content: `${this.currentLocation.address}\n\n经纬度${this.currentLocation.latitude.toFixed(6)}, ${this.currentLocation.longitude.toFixed(6)}`,
showCancel: false,
confirmText: '确定',
})
},
// 使
useCurrentLocation() {
if (!this.currentLocation) {
this.getCurrentLocation()
return
}
this.addressValue = this.currentLocation.address
this.$emit('use-location', this.currentLocation)
uni.showToast({
title: '已使用当前定位',
icon: 'success',
})
},
//
setLocation(location) {
this.currentLocation = location
},
//
clearLocation() {
this.currentLocation = null
}
}
}
</script>
<style lang="scss" scoped>
.map-location-component {
width: 100%;
}
//
.address-form-item {
margin-bottom: 20rpx;
display: flex;
align-items: center;
border-bottom: 1rpx solid #d8d8d8;
.field-label {
display: block;
font-size: 28rpx;
color: #333;
font-weight: 400;
flex: 1;
}
}
//
.address-input-wrapper {
flex: 3;
display: flex;
align-items: center;
height: 80rpx;
padding: 0 20rpx;
border-radius: 12rpx;
background: #fff;
transition: all 0.3s ease;
&:focus-within {
border-color: #f15a04;
box-shadow: 0 0 0 2rpx rgba(241, 90, 4, 0.1);
}
.address-input {
flex: 1;
height: 100%;
font-size: 28rpx;
color: #333;
border: none;
outline: none;
background: transparent;
&::placeholder {
color: #999;
}
}
.map-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 48rpx;
height: 48rpx;
background: #f15a04;
border-radius: 50%;
transition: all 0.3s ease;
margin-left: 16rpx;
&:active {
transform: scale(0.9);
box-shadow: 0 2rpx 8rpx rgba(241, 90, 4, 0.4);
}
.map-icon {
font-size: 28rpx;
color: white;
}
}
}
//
.location-suggestion {
margin-bottom: 40rpx;
.location-card {
display: flex;
align-items: center;
justify-content: space-between;
background: white;
border: 2rpx solid #e8f4fd;
border-radius: 16rpx;
padding: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
position: relative;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6rpx;
background: #f3f5f6;
border-radius: 6rpx 0 0 6rpx;
}
.location-content {
flex: 1;
margin-right: 20rpx;
.location-header {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.location-title {
font-size: 24rpx;
color: #f15a04;
font-weight: 500;
margin-right: 12rpx;
}
.location-company {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
}
.location-address {
font-size: 24rpx;
color: #666;
line-height: 1.4;
}
}
.use-location-btn {
background: #f15a04;
color: #000000;
border: none;
border-radius: 24rpx;
padding: 12rpx 24rpx;
font-size: 24rpx;
font-weight: 500;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 107, 0.3);
transition: all 0.3s ease;
&:active {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(255, 107, 107, 0.4);
}
}
.get-location-btn {
background: #f15a04;
color: #ffffff;
border: none;
border-radius: 24rpx;
padding: 12rpx 24rpx;
font-size: 24rpx;
font-weight: 500;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 107, 0.3);
transition: all 0.3s ease;
&:active {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(255, 107, 107, 0.4);
}
}
}
}
</style>

View File

@ -38,47 +38,15 @@
</view>
<!-- 地址 -->
<view class="form-item address-form-item">
<text class="field-label">地址</text>
<view class="address-input-wrapper">
<input
v-model="formData.address"
class="address-input"
placeholder="请输入或选择收货地址"
@focus="onAddressFocus"
/>
<view
class="map-icon-wrapper"
title="点击获取定位并打开地图"
@click="openMapWithLocation"
>
<text class="map-icon">🗺</text>
</view>
</view>
</view>
<!-- 当前定位 -->
<view class="location-suggestion">
<view v-if="currentLocation" class="location-card">
<view class="location-content">
<view class="location-header">
<text class="location-title">当前定位</text>
</view>
<text class="location-address">{{ currentLocation.address }}</text>
</view>
<button class="use-location-btn" @click="useCurrentLocation">使用</button>
</view>
<view v-else class="location-card">
<view class="location-content">
<view class="location-header">
<text class="location-title">获取位置</text>
<text class="location-company">点击获取当前位置</text>
</view>
<text class="location-address">需要定位权限</text>
</view>
<button class="get-location-btn" @click="getCurrentLocation">获取</button>
</view>
</view>
<map-location
v-model="formData.address"
label="地址"
placeholder="请输入或选择收货地址"
@location-success="onLocationSuccess"
@location-error="onLocationError"
@use-location="onUseLocation"
@map-opened="onMapOpened"
/>
<!-- 详细位置 -->
<view class="form-item">
@ -134,10 +102,13 @@
<script>
import commonEnum from '../../enum/commonEnum'
import { getLocationWithPermission, handleLocationError } from '@/utils/permission.js'
import MapLocation from '@/components/map-location/map-location.vue'
export default {
name: 'LeasePage',
components: {
MapLocation
},
computed: {
commonEnum() {
return commonEnum
@ -157,168 +128,30 @@ export default {
equipment: '',
period: '1年',
},
currentLocation: null,
showDetails: false,
totalAmount: '100.10',
isMapTriggered: false,
}
},
methods: {
//
async getCurrentLocation() {
try {
uni.showLoading({
title: '获取位置中...',
})
const location = await getLocationWithPermission()
console.log('位置信息:', location)
// 使
this.currentLocation = {
address: `纬度: ${location.latitude.toFixed(6)}, 经度: ${location.longitude.toFixed(6)}`,
latitude: location.latitude,
longitude: location.longitude,
}
uni.hideLoading()
//
if (!this.isMapTriggered) {
uni.showToast({
title: '位置获取成功,点击地图图标查看',
icon: 'success',
duration: 3000,
})
}
return this.currentLocation
} catch (err) {
uni.hideLoading()
handleLocationError(err)
throw err
}
//
onLocationSuccess(location) {
console.log('位置获取成功:', location)
//
},
//
onAddressFocus() {
//
console.log('地址输入框获得焦点')
onLocationError(error) {
console.error('位置获取失败:', error)
//
},
//
async openMapWithLocation() {
try {
this.isMapTriggered = true
//
await this.getCurrentLocation()
//
this.openMap()
} catch (err) {
console.error('获取位置失败:', err)
uni.showToast({
title: '获取位置失败',
icon: 'error',
})
} finally {
this.isMapTriggered = false
}
onUseLocation(location) {
console.log('使用位置:', location)
// 使
},
selectAddress() {
//
if (this.currentLocation && this.currentLocation.latitude && this.currentLocation.longitude) {
this.openMap()
} else {
//
uni.showModal({
title: '提示',
content: '需要先获取当前位置才能打开地图,是否现在获取?',
success: res => {
if (res.confirm) {
this.getCurrentLocation()
}
},
})
}
},
//
openMap() {
const { latitude, longitude } = this.currentLocation
//
uni.showLoading({
title: '打开地图中...',
})
//
// #ifdef APP-PLUS
uni.openLocation({
latitude: latitude,
longitude: longitude,
name: '当前位置',
address: this.currentLocation.address || '未知地址',
scale: 18,
success: () => {
uni.hideLoading()
console.log('地图打开成功')
},
fail: err => {
uni.hideLoading()
console.error('打开地图失败:', err)
this.showMapInApp()
},
})
// #endif
// #ifdef MP-WEIXIN
uni.openLocation({
latitude: latitude,
longitude: longitude,
name: '当前位置',
address: this.currentLocation.address || '未知地址',
scale: 18,
success: () => {
uni.hideLoading()
},
fail: err => {
uni.hideLoading()
console.error('打开地图失败:', err)
this.showMapInApp()
},
})
// #endif
// #ifdef H5
// H5使线
uni.hideLoading()
const mapUrl = `https://uri.amap.com/marker?position=${longitude},${latitude}&name=${encodeURIComponent('当前位置')}&address=${encodeURIComponent(this.currentLocation.address || '未知地址')}`
window.open(mapUrl, '_blank')
// #endif
},
//
showMapInApp() {
uni.showModal({
title: '地图功能',
content: `${this.currentLocation.address}\n\n经纬度${this.currentLocation.latitude.toFixed(6)}, ${this.currentLocation.longitude.toFixed(6)}`,
showCancel: false,
confirmText: '确定',
})
},
useCurrentLocation() {
if (!this.currentLocation) {
this.getCurrentLocation()
return
}
this.formData.address = this.currentLocation.address
uni.showToast({
title: '已使用当前定位',
icon: 'success',
})
onMapOpened() {
console.log('地图已打开')
//
},
selectEquipment() {
//
@ -465,205 +298,7 @@ export default {
}
}
//
.address-form-item {
margin-bottom: 20rpx;
}
//
.address-input-wrapper {
flex: 3;
display: flex;
align-items: center;
height: 80rpx;
padding: 0 20rpx;
border-radius: 12rpx;
background: #fff;
//border: 1rpx solid #e0e0e0;
transition: all 0.3s ease;
&:focus-within {
border-color: #f15a04;
box-shadow: 0 0 0 2rpx rgba(241, 90, 4, 0.1);
}
.address-input {
flex: 1;
height: 100%;
font-size: 28rpx;
color: #333;
border: none;
outline: none;
background: transparent;
&::placeholder {
color: #999;
}
}
.map-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 48rpx;
height: 48rpx;
background: #f15a04;
border-radius: 50%;
transition: all 0.3s ease;
//box-shadow: 0 4rpx 12rpx rgba(241, 90, 4, 0.3);
margin-left: 16rpx;
&:active {
transform: scale(0.9);
box-shadow: 0 2rpx 8rpx rgba(241, 90, 4, 0.4);
}
.map-icon {
font-size: 28rpx;
color: white;
}
}
}
// 使
.address-selector {
flex: 3;
display: flex;
align-items: center;
justify-content: space-between;
height: 80rpx;
padding: 0 20rpx;
border-radius: 12rpx;
transition: all 0.3s ease;
&:active {
background: #f0f0f0;
border-color: #ff6b6b;
}
.address-text {
flex: 1;
font-size: 28rpx;
color: #666;
margin-right: 20rpx;
}
.map-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 48rpx;
height: 48rpx;
background: #f15a04;
border-radius: 50%;
transition: all 0.3s ease;
box-shadow: 0 4rpx 12rpx rgba(241, 90, 4, 0.3);
&:active {
transform: scale(0.9);
box-shadow: 0 2rpx 8rpx rgba(241, 90, 4, 0.4);
}
.map-icon {
font-size: 28rpx;
color: white;
}
}
}
//
.location-suggestion {
margin-bottom: 40rpx;
.location-card {
display: flex;
align-items: center;
justify-content: space-between;
background: white;
border: 2rpx solid #e8f4fd;
border-radius: 16rpx;
padding: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
position: relative;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6rpx;
background: #f3f5f6;
border-radius: 6rpx 0 0 6rpx;
}
.location-content {
flex: 1;
margin-right: 20rpx;
.location-header {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.location-title {
font-size: 24rpx;
color: #f15a04;
font-weight: 500;
margin-right: 12rpx;
}
.location-company {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
}
.location-address {
font-size: 24rpx;
color: #666;
line-height: 1.4;
}
}
.use-location-btn {
background: #f15a04;
color: #000000;
border: none;
border-radius: 24rpx;
padding: 12rpx 24rpx;
font-size: 24rpx;
font-weight: 500;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 107, 0.3);
transition: all 0.3s ease;
&:active {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(255, 107, 107, 0.4);
}
}
.get-location-btn {
background: #f15a04;
color: #ffffff;
border: none;
border-radius: 24rpx;
padding: 12rpx 24rpx;
font-size: 24rpx;
font-weight: 500;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 107, 0.3);
transition: all 0.3s ease;
&:active {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(255, 107, 107, 0.4);
}
}
}
}
//
.selector {