Files
uni-fans-score/pages/index/index.vue
T

944 lines
22 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.
<template>
<view class="container">
<!-- 顶部搜索栏 -->
<!-- <view class="header-search">
<view class="search-box">
<image class="search-icon" src="@/static/scan-icon.png" mode="aspectFit" />
<input
class="search-input"
:placeholder="searchPlaceholder"
v-model="searchKeyword"
@input="onSearchInput"
/>
<view class="location-btn" @click="handleRelocate">
<image class="location-icon" src="@/static/scan-icon.png" mode="aspectFit" />
</view>
</view>
</view> -->
<!-- 地图组件 -->
<MapComponent
v-if="!isLoading && userLocation"
ref="mapRef"
:userLocation="userLocation"
:positionList="positionList"
:filteredPositions="filteredPositions"
:searchKeyword="searchKeyword"
@relocate="handleRelocate"
@scan="handleScan"
@showList="showLocationList"
@markerTap="selectPosition"
@mapCenterChange="onMapCenterChange"
/>
<!-- 地图加载状态 -->
<view v-if="isLoading || !userLocation" class="map-loading-placeholder">
<view class="loading-content">
<view class="loading-spinner"></view>
<text>正在获取位置信息...</text>
</view>
</view>
<!-- 场地列表弹窗 -->
<view class="location-popup" v-if="showLocationPopup">
<view class="popup-mask" @click="hideLocationList"></view>
<view class="location-sheet" :class="{ 'expanded': isExpanded }">
<view class="sheet-handle" @click="toggleSheet">
<view class="handle-bar"></view>
</view>
<view class="sheet-header">
<text class="sheet-title">附近场地 ({{ filteredPositions.length }})</text>
<view class="close-btn" @click="hideLocationList">
<image class="close-icon" src="@/static/scan-icon.png" mode="aspectFit" />
</view>
</view>
<scroll-view class="sheet-content" scroll-y="true">
<view
class="position-item"
v-for="(item, index) in filteredPositions"
:key="item.positionId"
@click="selectPositionFromPopup(item)"
>
<view class="position-info">
<view class="position-name">{{ item.name }}</view>
<view class="position-desc">{{ item.describe }}</view>
<view class="position-location">
<image class="location-icon-small" src="@/static/scan-icon.png" mode="aspectFit" />
<text>{{ item.location }}</text>
</view>
<view class="position-time" v-if="item.workTime && item.workTime !== '0'">
<text>营业时间{{ item.workTime }}</text>
</view>
</view>
<view class="position-actions">
<view class="distance-info" v-if="item.distance">
<text>{{ item.distance }}km</text>
</view>
<view class="status-tag" :class="item.status">
<text>{{ item.status === 'online' ? '营业中' : '暂停服务' }}</text>
</view>
<view class="nav-btn" @click.stop="navigateToPosition(item)">
<text>导航</text>
</view>
</view>
</view>
<view class="empty-state" v-if="filteredPositions.length === 0 && !isLoading">
<image class="empty-icon" src="@/static/scan-icon.png" mode="aspectFit" />
<text class="empty-text">暂无附近场地</text>
</view>
</scroll-view>
</view>
</view>
<!-- 加载状态 -->
<view class="loading-overlay" v-if="isLoading">
<view class="loading-content">
<view class="loading-spinner"></view>
<text>正在获取场地信息...</text>
</view>
</view>
<!-- 手机号授权弹窗 -->
<view class="phone-auth-popup" v-if="showPhoneAuthPopup">
<view class="popup-mask" @click.stop="showPhoneAuthPopup = false"></view>
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">授权获取手机号</text>
</view>
<view class="popup-body">
<view class="auth-desc">
<text>为了提供更好的服务和紧急联系需要授权获取您的手机号</text>
</view>
<button class="auth-btn" open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber">
<text>一键获取手机号</text>
</button>
<view class="auth-cancel" @click="showPhoneAuthPopup = false">
<text>暂不授权</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { getQueryString, wxLogin } from '@/util/index.js'
import { URL } from "@/config/url.js"
import { getDeviceInfo } from '@/config/user.js'
import AmapUtil from '@/utils/amap.js'
import MapComponent from '@/components/MapComponent.vue'
// 响应式数据
const searchKeyword = ref('')
const userLocation = ref(null)
const positionList = ref([])
const filteredPositions = ref([])
const isExpanded = ref(false)
const isLoading = ref(false)
const showPhoneAuthPopup = ref(false)
const isLocationInitialized = ref(false)
const showLocationPopup = ref(false)
// 组件引用
const mapRef = ref(null)
// 计算属性
const searchPlaceholder = computed(() => {
if (userLocation.value && userLocation.value.address) {
return `${userLocation.value.district || '当前位置'} - 搜索附近场地`
}
return '搜索附近场地'
})
// 生命周期
onMounted(() => {
init()
})
onUnmounted(() => {
// 清理工作在子组件中处理
})
// 方法
const init = async () => {
isLoading.value = true
try {
// 1. 先获取用户位置
await getUserLocation()
// 2. 加载场地列表
await loadPositions()
} catch (error) {
console.error('初始化失败:', error)
// 即使失败也要加载场地列表
await loadPositions()
} finally {
isLoading.value = false
}
}
const getUserLocation = async () => {
try {
const location = await new Promise((resolve, reject) => {
uni.getLocation({
type: 'gcj02',
success: resolve,
fail: reject
})
})
// 保存用户位置
userLocation.value = {
longitude: location.longitude,
latitude: location.latitude
}
// 只在首次初始化时设置标记
if (!isLocationInitialized.value) {
isLocationInitialized.value = true
}
// 获取详细地址信息
try {
const addressResult = await AmapUtil.regeocode(location.longitude, location.latitude)
if (addressResult.success) {
const addressInfo = addressResult.data
userLocation.value.address = addressInfo.formatted_address
userLocation.value.city = addressInfo.addressComponent.city
userLocation.value.district = addressInfo.addressComponent.district
}
} catch (error) {
// 忽略地址信息错误,使用基础定位信息
}
// 等待地图组件响应位置变化后,重新加载场地列表
setTimeout(async () => {
await loadPositions()
uni.hideLoading()
uni.showToast({
title: '定位成功',
icon: 'success'
})
}, 800)
} catch (error) {
console.error('获取位置失败:', error)
uni.showToast({
title: '获取位置失败,显示默认地图',
icon: 'none'
})
}
}
const loadPositions = async () => {
try {
if (!uni.getStorageSync('token')) {
await wxLogin()
}
const res = await uni.request({
url: `${URL}/device/position/list`,
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
}
})
if (res.statusCode === 200 && res.data.code === 200) {
positionList.value = res.data.rows || []
calculateDistances()
filteredPositions.value = [...positionList.value]
} else {
console.error('获取场地列表失败:', res.data.msg)
}
} catch (error) {
console.error('获取场地列表异常:', error)
}
}
const calculateDistances = (centerPoint = null) => {
// 如果没有指定中心点,优先使用用户位置,其次使用地图中心
const center = centerPoint || userLocation.value || (mapRef.value?.mapCenter)
// 严格检查center对象的有效性
if (!center || typeof center.longitude === 'undefined' || typeof center.latitude === 'undefined') {
return
}
positionList.value.forEach(item => {
if (item.longitude && item.latitude) {
try {
const distance = AmapUtil.calculateDistance(
center.latitude,
center.longitude,
parseFloat(item.latitude),
parseFloat(item.longitude)
)
item.distance = distance.toFixed(1)
} catch (error) {
console.error('计算距离异常:', error, item)
item.distance = '999.0' // 设置一个默认距离
}
}
})
// 按距离排序
positionList.value.sort((a, b) => {
return (parseFloat(a.distance) || 999) - (parseFloat(b.distance) || 999)
})
}
const loadPositionsByCenter = async (center) => {
try {
if (!uni.getStorageSync('token')) {
await wxLogin()
}
// 使用原有接口获取所有场地
const res = await uni.request({
url: `${URL}/device/position/list`,
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
}
})
if (res.statusCode === 200 && res.data.code === 200) {
positionList.value = res.data.rows || []
// 基于地图中心计算距离
calculateDistances(center)
// 可以选择性过滤距离过远的场地(比如超过10km的)
const maxDistance = 10 // 最大显示距离,单位km
filteredPositions.value = positionList.value.filter(item => {
return !item.distance || parseFloat(item.distance) <= maxDistance
})
} else {
console.error('根据地图中心加载场地失败:', res.data.msg)
positionList.value = []
filteredPositions.value = []
}
} catch (error) {
console.error('根据地图中心加载场地异常:', error)
// 如果请求失败,保持当前的场地列表不变
}
}
const handleRelocate = async () => {
uni.showLoading({ title: '定位中...' })
// 直接重新加载当前页面
uni.reLaunch({
url: '/pages/index/index'
})
}
const onMapCenterChange = (center) => {
loadPositionsByCenter(center)
}
const selectPosition = (position) => {
uni.showActionSheet({
itemList: ['扫码使用', '导航前往'],
success: (res) => {
switch (res.tapIndex) {
case 0:
handleScan()
break
case 1:
navigateToPosition(position)
break
}
}
})
}
const selectPositionFromPopup = (position) => {
// 先关闭弹窗
hideLocationList()
// 延迟一点时间再显示选择菜单,让弹窗关闭动画完成
setTimeout(() => {
selectPosition(position)
}, 200)
}
const navigateToPosition = (position) => {
const latitude = parseFloat(position.latitude)
const longitude = parseFloat(position.longitude)
uni.openLocation({
latitude,
longitude,
name: position.name,
address: position.location
})
}
const toggleSheet = () => {
isExpanded.value = !isExpanded.value
}
const onSearchInput = () => {
if (!searchKeyword.value.trim()) {
filteredPositions.value = [...positionList.value]
} else {
filteredPositions.value = positionList.value.filter(item => {
return item.name.includes(searchKeyword.value) ||
item.describe.includes(searchKeyword.value) ||
item.location.includes(searchKeyword.value)
})
}
}
const handleScan = async () => {
try {
const scanResult = await new Promise((resolve, reject) => {
uni.scanCode({
success: resolve,
fail: reject
})
})
let deviceNo = getQueryString(scanResult.path, 'deviceNo')
if (!deviceNo) {
uni.showToast({
title: '无效的设备二维码',
icon: 'none'
})
return
}
if (!uni.getStorageSync('token')) {
await wxLogin()
}
// 检查是否有使用中的订单
const inUseRes = await uni.request({
url: `${URL}/app/order/inUse`,
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
}
})
if (inUseRes.statusCode == 200 && inUseRes.data.code == 200 && inUseRes.data.data) {
const inUseOrder = inUseRes.data.data
uni.reLaunch({
url: `/pages/return/index?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}`
})
return
}
// 检查是否有待支付订单
const orderRes = await uni.request({
url: `${URL}/app/order/unpaid`,
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
}
})
if (orderRes.statusCode == 200 && orderRes.data.code == 200 && orderRes.data.data) {
const unpaidOrder = orderRes.data.data
uni.navigateTo({
url: `/pages/order/payment?orderId=${unpaidOrder.orderId}`
})
} else {
try {
const deviceInfoRes = await getDeviceInfo(deviceNo)
if (deviceInfoRes.code == 200 && deviceInfoRes.data && deviceInfoRes.data.device) {
const deviceInfo = deviceInfoRes.data.device
if (deviceInfo.feeConfig) {
try {
const feeConfig = JSON.parse(deviceInfo.feeConfig)
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}&feeConfig=${encodeURIComponent(deviceInfo.feeConfig)}`
})
} catch (e) {
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
}
} else {
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
}
} else {
uni.showToast({
title: '获取设备信息失败',
icon: 'none'
})
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
}
} catch (error) {
console.error('获取设备信息异常:', error)
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
}
}
} catch (error) {
console.error('扫码处理失败:', error)
uni.showToast({
title: '扫码失败',
icon: 'none'
})
}
}
const showLocationList = () => {
showLocationPopup.value = true
}
const hideLocationList = () => {
showLocationPopup.value = false
}
const onGetPhoneNumber = (e) => {
if (e.detail.errMsg === 'getPhoneNumber:ok') {
showPhoneAuthPopup.value = false
}
}
</script>
<style lang="scss" scoped>
.container {
height: 100%;
width: 100%;
background-color: #f6f7fb;
display: flex;
flex-direction: column;
}
/* 顶部搜索栏 */
.header-search {
padding: 20rpx 30rpx;
background: #ffffff;
border-bottom: 1px solid #f0f0f0;
z-index: 10;
.search-box {
display: flex;
align-items: center;
background: #f8f9fa;
border-radius: 50rpx;
padding: 0 20rpx;
height: 80rpx;
.search-icon {
width: 32rpx;
height: 32rpx;
margin-right: 16rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: #333;
}
.location-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background: #2196F3;
border-radius: 50%;
margin-left: 16rpx;
.location-icon {
width: 24rpx;
height: 24rpx;
}
}
}
}
/* 场地列表弹窗 */
.location-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2000;
display: flex;
align-items: flex-end;
justify-content: center;
.popup-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
}
.location-sheet {
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
max-height: 70vh;
transition: all 0.3s ease;
z-index: 1;
position: relative;
width: 100%;
display: flex;
flex-direction: column;
animation: slideUp 0.3s ease-out;
&.expanded {
max-height: 85vh;
}
.sheet-handle {
display: flex;
justify-content: center;
padding: 20rpx 0;
cursor: pointer;
.handle-bar {
width: 80rpx;
height: 8rpx;
background: #e0e0e0;
border-radius: 4rpx;
}
}
.sheet-header {
padding: 0 30rpx 20rpx;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
.sheet-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.close-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f0f0f0;
border-radius: 50%;
transition: all 0.2s ease;
&:active {
background: #e0e0e0;
transform: scale(0.95);
}
.close-icon {
width: 24rpx;
height: 24rpx;
}
}
}
.sheet-content {
flex: 1;
padding: 20rpx 0;
overflow: hidden;
}
}
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
/* 场地列表项 */
.position-item {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
border-bottom: 1px solid #f8f9fa;
.position-info {
flex: 1;
.position-name {
font-size: 32rpx;
font-weight: 500;
color: #333;
margin-bottom: 8rpx;
}
.position-desc {
font-size: 26rpx;
color: #666;
margin-bottom: 8rpx;
}
.position-location {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.location-icon-small {
width: 24rpx;
height: 24rpx;
margin-right: 8rpx;
}
text {
font-size: 24rpx;
color: #999;
}
}
.position-time {
font-size: 24rpx;
color: #999;
}
}
.position-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8rpx;
.distance-info {
font-size: 24rpx;
color: #2196F3;
font-weight: 500;
}
.status-tag {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
&.online {
background: #e8f5e8;
color: #4caf50;
}
&.offline {
background: #ffeaea;
color: #f44336;
}
}
.nav-btn {
padding: 12rpx 20rpx;
background: #2196F3;
border-radius: 20rpx;
font-size: 24rpx;
color: #ffffff;
}
}
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 0;
.empty-icon {
width: 120rpx;
height: 120rpx;
margin-bottom: 24rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
}
/* 加载状态 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
.loading-content {
background: #ffffff;
border-radius: 16rpx;
padding: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #f0f0f0;
border-top: 4rpx solid #2196F3;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 24rpx;
}
text {
font-size: 28rpx;
color: #666;
}
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 地图加载状态 */
.map-loading-placeholder {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #f6f7fb;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
.loading-content {
background: #ffffff;
border-radius: 16rpx;
padding: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #f0f0f0;
border-top: 4rpx solid #2196F3;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 24rpx;
}
text {
font-size: 28rpx;
color: #666;
}
}
}
/* 手机号授权弹窗 */
.phone-auth-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
.popup-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
}
.popup-content {
background: #ffffff;
border-radius: 24rpx;
margin: 0 60rpx;
padding: 40rpx;
position: relative;
z-index: 1;
.popup-header {
text-align: center;
margin-bottom: 30rpx;
.popup-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
}
.popup-body {
.auth-desc {
text-align: center;
margin-bottom: 40rpx;
text {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
}
.auth-btn {
width: 100%;
height: 88rpx;
background: #2196F3;
border-radius: 44rpx;
border: none;
color: #ffffff;
font-size: 32rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
}
.auth-cancel {
text-align: center;
padding: 20rpx;
text {
font-size: 28rpx;
color: #999;
}
}
}
}
}
</style>