feat:对接获取附近设备列表

This commit is contained in:
2025-10-29 16:35:51 +08:00
parent 3d67dc928d
commit 409da480b1
9 changed files with 266 additions and 168 deletions
+1 -1
View File
@@ -68,7 +68,7 @@
</script> </script>
<style lang="scss"> <style lang="scss">
@import "uview-ui/index.scss" @import "@climblee/uv-ui/index.scss"
/*每个页面公共css */ /*每个页面公共css */
</style> </style>
+41
View File
@@ -8,6 +8,47 @@ export const getDeviceInfo = (deviceNo) => {
}) })
} }
// 查询附近设备
export const getNearbyDevices = ({ userLatitude, userLongitude, queryType = 'rent', radiusKm = 5, pageNum = 1, pageSize = 100 }) => {
return request({
url: `/device/device/nearby?pageNum=${pageNum}&pageSize=${pageSize}`,
method: 'post',
data: {
userLatitude,
userLongitude,
queryType, // 'rent' 可租借 或 'return' 可归还
radiusKm
},
hideLoading: true // 不显示加载提示,由页面自己控制
})
}
// 转换设备数据为统一格式(兼容旧的场地数据结构)
export const transformDeviceData = (device) => {
return {
...device,
// 保持原有字段
positionId: device.deviceId, // 使用 deviceId 作为 positionId
name: device.name || device.positionName,
location: device.deviceLocation,
latitude: device.latitude,
longitude: device.longitude,
distance: device.distance ? `${device.distance}km` : '',
distanceInMeters: device.distance ? device.distance * 1000 : 999000,
// 设备特有字段
deviceNo: device.deviceNo,
deviceImg: device.deviceImg,
availablePowerBankCount: device.availablePowerBankCount,
availableEmptyGridCount: device.availableEmptyGridCount,
totalGridCount: device.totalGridCount,
remark: device.remark || '', // 计费备注信息
status: device.status || 'online',
// 添加租借和归还能力标识
canRent: (device.availablePowerBankCount || 0) > 0,
canReturn: (device.availableEmptyGridCount || 0) > 0
}
}
// 立即租借 // 立即租借
export const rentPowerBank = (deviceNo, phone) => { export const rentPowerBank = (deviceNo, phone) => {
return request({ return request({
+2 -2
View File
@@ -1,6 +1,6 @@
// export const URL = "https://my.gxfs123.com/api" //正式服务器 // export const URL = "https://my.gxfs123.com/api" //正式服务器
export const URL = "https://fansdev.gxfs123.com/api" //测试服务器 // export const URL = "https://fansdev.gxfs123.com/api" //测试服务器
// export const URL = "http://192.168.5.120:8080" //本地调试 export const URL = "http://192.168.5.120:8080" //本地调试
// export const URL = "http://127.0.0.1:8080" //本地调试 // export const URL = "http://127.0.0.1:8080" //本地调试
export const appid = "wx2165f0be356ae7a9" //小程序appid export const appid = "wx2165f0be356ae7a9" //小程序appid
+1 -1
View File
@@ -4,7 +4,7 @@ import { createSSRApp } from 'vue'
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
import zhCN from './locale/zh-CN.js' import zhCN from './locale/zh-CN.js'
import enUS from './locale/en-US.js' import enUS from './locale/en-US.js'
import uView from "uview-ui" import uView from '@climblee/uv-ui'
// 获取系统语言 // 获取系统语言
const getSystemLanguage = () => { const getSystemLanguage = () => {
+1 -2
View File
@@ -2,8 +2,7 @@
"easycom": { "easycom": {
"autoscan": true, "autoscan": true,
"custom": { "custom": {
"^uv-(.*)": "@climblee/uv-ui/components/uv-$1/uv-$1.vue", "^uv-(.*)": "@climblee/uv-ui/components/uv-$1/uv-$1.vue"
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
} }
}, },
"pages": [{ "pages": [{
+89 -109
View File
@@ -167,7 +167,9 @@
URL URL
} from "../../config/url.js" } from "../../config/url.js"
import { import {
getDeviceInfo getDeviceInfo,
getNearbyDevices,
transformDeviceData
} from '../../config/api/device.js' } from '../../config/api/device.js'
import { import {
getPotionsDetail getPotionsDetail
@@ -422,32 +424,34 @@ const noticePopup = ref(null)
const loadPositions = async () => { const loadPositions = async () => {
try { try {
const res = await uni.request({ if (!userLocation.value || !userLocation.value.latitude || !userLocation.value.longitude) {
url: `${URL}/device/position/app/list`, console.warn('用户位置信息不完整,无法查询附近设备')
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
},
data: {
latitude: userLocation.value.latitude,
longitude: userLocation.value.longitude
}
})
console.log(res);
if (res.statusCode === 401 || res.data?.code === 401 || res.data?.code === 40101) {
redirectToLogin()
return return
} else if (res.statusCode === 200 && res.data.code === 200) { }
positionList.value = res.data.rows || []
const res = await getNearbyDevices({
userLatitude: userLocation.value.latitude,
userLongitude: userLocation.value.longitude,
queryType: 'rent', // 默认查询可租借设备
radiusKm: 5,
pageNum: 1,
pageSize: 100
})
console.log('查询附近设备结果:', res)
if (res.code === 200) {
// 新接口返回的是 data.records,需要适配字段名
const devices = res.data?.records || []
// 将设备数据转换为统一格式
positionList.value = devices.map(transformDeviceData)
calculateDistances() calculateDistances()
filteredPositions.value = [...positionList.value] filteredPositions.value = [...positionList.value]
} else { } else {
console.error('获取场地列表失败:', res.data.msg) console.error('获取设备列表失败:', res.msg)
} }
} catch (error) { } catch (error) {
console.error('获取场地列表异常:', error) console.error('获取设备列表异常:', error)
} }
} }
@@ -494,50 +498,45 @@ const noticePopup = ref(null)
return return
} }
console.log('根据地图中心加载场地:', center) console.log('根据地图中心加载设备:', center)
try { try {
// 使用原有接口获取所有场地,传入中心点经纬度 // 使用新的附近设备查询接口
const res = await uni.request({ const res = await getNearbyDevices({
url: `${URL}/device/position/app/list`, userLatitude: center.latitude,
method: 'GET', userLongitude: center.longitude,
header: { queryType: 'rent', // 默认查询可租借设备
'Authorization': "Bearer " + uni.getStorageSync('token'), radiusKm: 5,
'Clientid': uni.getStorageSync('client_id') pageNum: 1,
}, pageSize: 100
data: {
latitude: center.latitude,
longitude: center.longitude
}
}) })
if (res.statusCode === 401 || res.data?.code === 401 || res.data?.code === 40101) { if (res.code === 200) {
redirectToLogin() const devices = res.data?.records || []
return console.log('加载到设备数量:', devices.length)
} else if (res.statusCode === 200 && res.data.code === 200) {
const rows = res.data.rows || [] // 将设备数据转换为统一格式
console.log('加载到场地数量:', rows.length) positionList.value = devices.map(transformDeviceData)
positionList.value = rows
// 基于地图中心计算距离 // 基于地图中心计算距离
calculateDistances(center) calculateDistances(center)
// 可以选择性过滤距离过远的场地(比如超过10km的) // 可以选择性过滤距离过远的设备(比如超过10km的)
const maxDistanceInMeters = 10000 // 最大显示距离,单位米(10km) const maxDistanceInMeters = 10000 // 最大显示距离,单位米(10km)
filteredPositions.value = positionList.value.filter(item => { filteredPositions.value = positionList.value.filter(item => {
return !item.distanceInMeters || item.distanceInMeters <= maxDistanceInMeters return !item.distanceInMeters || item.distanceInMeters <= maxDistanceInMeters
}) })
console.log('过滤后场地数量:', filteredPositions.value.length) console.log('过滤后设备数量:', filteredPositions.value.length)
} else { } else {
console.error('根据地图中心加载场地失败:', res.data.msg) console.error('根据地图中心加载设备失败:', res.msg)
positionList.value = [] positionList.value = []
filteredPositions.value = [] filteredPositions.value = []
} }
} catch (error) { } catch (error) {
console.error('根据地图中心加载场地异常:', error) console.error('根据地图中心加载设备异常:', error)
// 如果请求失败,保持当前的场地列表不变 // 如果请求失败,保持当前的设备列表不变
} }
} }
@@ -1238,37 +1237,39 @@ const closeNoticePopup = () => {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
// gap: 16rpx; gap: 16rpx;
padding: 0; padding: 0;
.action-btn { .action-btn {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
align-content: center;
justify-content: center; justify-content: center;
font-size: 26rpx; font-size: 24rpx;
font-weight: 600; font-weight: 500;
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
opacity: 0.8;
}
&.primary { &.primary {
background: #3EAB64; background: #3EAB64;
color: #fff; color: #fff;
border-radius: 64rpx; border-radius: 56rpx;
height: 100rpx; height: 112rpx;
min-width: 320rpx; flex: 1;
// box-shadow: 0 16rpx 40rpx rgba(7, 193, 96, 0.35); max-width: 400rpx;
padding: 12rpx 24rpx; padding: 0 24rpx;
// box-shadow: 0 8rpx 24rpx rgba(62, 171, 100, 0.3);
.icon-wrap { .icon-wrap {
width: 50rpx; width: 48rpx;
height: 50rpx; height: 48rpx;
border-radius: 50%;
// background: rgba(255, 255, 255, 0.25);
display: flex; display: flex;
align-items: center; align-items: center;
align-content: center;
justify-content: center; justify-content: center;
// margin-bottom: 10rpx;
} }
} }
@@ -1277,66 +1278,44 @@ const closeNoticePopup = () => {
color: #333; color: #333;
border-radius: 24rpx; border-radius: 24rpx;
height: 100rpx; height: 100rpx;
min-width: 180rpx; width: 140rpx;
// box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08); flex-shrink: 0;
padding: 12rpx 16rpx; padding: 8rpx 12rpx;
// box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
// border: 1rpx solid #f0f0f0;
.icon-wrap { .icon-wrap {
width: 40rpx; width: 36rpx;
height: 40rpx; height: 36rpx;
border-radius: 50%;
// background: #f7f8fa;
// border: 2rpx solid #eaeaea;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-bottom: 10rpx; margin-bottom: 6rpx;
} }
} }
&.small {
height: 120rpx;
min-width: 180rpx;
border-radius: 28rpx;
}
}
.btn-nearby,
.btn-my {
gap: 10rpx;
} }
.btn-scan { .btn-scan {
/* 尺寸与布局(覆盖 primary 的默认尺寸) */ /* 中间扫码按钮:横向布局 */
height: 112rpx;
min-width: 360rpx;
padding: 0 32rpx;
border-radius: 56rpx;
flex-direction: row; flex-direction: row;
// gap: 16rpx; gap: 8rpx;
align-items: center;
justify-content: center;
/* 左侧图标容器 */
.icon-wrap { .icon-wrap {
width: 64rpx; width: 48rpx;
height: 64rpx; height: 48rpx;
border-radius: 50%;
margin: 0; margin: 0;
margin-right: 12rpx;
// background: rgba(255, 255, 255, 0.18);
display: flex;
align-items: center;
justify-content: center;
box-shadow: none;
} }
/* 图标大小与反色 */
.action-icon { .action-icon {
width: 40rpx; width: 32rpx;
height: 40rpx; height: 32rpx;
filter: brightness(0) invert(1); filter: brightness(0) invert(1);
} }
.primary-label {
font-size: 28rpx;
font-weight: 600;
}
} }
} }
@@ -1506,17 +1485,18 @@ const closeNoticePopup = () => {
} }
.action-icon { .action-icon {
width: 42rpx; width: 36rpx;
height: 42rpx; height: 36rpx;
filter: none; filter: none;
box-shadow: none;
}
.btn-scan .action-icon {
filter: brightness(0) invert(1);
} }
.action-label { .action-label {
line-height: 1.2;
text-align: center;
}
.primary-label {
color: #ffffff;
line-height: 1; line-height: 1;
} }
+56 -25
View File
@@ -2,7 +2,8 @@
<view class="position-detail-page"> <view class="position-detail-page">
<!-- 顶部设备柜图示 --> <!-- 顶部设备柜图示 -->
<view class="device-illustration"> <view class="device-illustration">
<image src="/static/device-info.png" class="device-img" mode="aspectFit"></image> <image v-if="positionInfo.deviceImg" :src="positionInfo.deviceImg" class="device-img" mode="aspectFit"></image>
<image v-else src="/static/device-info.png" class="device-img" mode="aspectFit"></image>
</view> </view>
<!-- 场地信息卡片 --> <!-- 场地信息卡片 -->
@@ -28,6 +29,17 @@
<text class="item-text">{{ $t('device.pricing') }}{{ pricingText }}</text> <text class="item-text">{{ $t('device.pricing') }}{{ pricingText }}</text>
</view> </view>
<!-- 可用数量信息 -->
<view class="info-item" v-if="positionInfo.availablePowerBankCount !== undefined && positionInfo.availablePowerBankCount !== null">
<image src="/static/device-info.png" class="item-icon" mode="aspectFit"></image>
<text class="item-text">可租借风扇{{ positionInfo.availablePowerBankCount }} </text>
</view>
<view class="info-item" v-if="positionInfo.availableEmptyGridCount !== undefined && positionInfo.availableEmptyGridCount !== null">
<image src="/static/device-info.png" class="item-icon" mode="aspectFit"></image>
<text class="item-text">可归还空位{{ positionInfo.availableEmptyGridCount }} </text>
</view>
<!-- 按钮组 --> <!-- 按钮组 -->
<view class="button-group"> <view class="button-group">
<view style="display: flex;flex-direction: row;gap: 10rpx;"> <view style="display: flex;flex-direction: row;gap: 10rpx;">
@@ -48,17 +60,21 @@
</template> </template>
<script setup> <script setup>
import { import {
ref, ref,
computed, computed,
onMounted onMounted
} from 'vue' } from 'vue'
import { import {
onLoad onLoad
} from '@dcloudio/uni-app' } from '@dcloudio/uni-app'
import { import {
URL getNearbyDevices,
} from '../../config/url.js' transformDeviceData
} from '../../config/api/device.js'
import { useI18n } from '../../utils/i18n.js'
const { t: $t } = useI18n()
const positionInfo = ref({}) const positionInfo = ref({})
const positionId = ref('') const positionId = ref('')
@@ -78,9 +94,12 @@
}) })
const pricingText = computed(() => { const pricingText = computed(() => {
// 这里可以根据实际的计费规则来显示 // 使用设备的 remark 字段作为计费信息
// 默认显示一个通用的计费说明 if (positionInfo.value?.remark) {
return '5元/小时,36元/24小时,总计¥899元' return positionInfo.value.remark
}
// 如果 remark 为空,显示默认提示
return '暂无计费信息'
}) })
onLoad(async (options) => { onLoad(async (options) => {
@@ -96,33 +115,45 @@
title: $t('common.loading') title: $t('common.loading')
}) })
const res = await uni.request({ // 获取用户位置用于查询附近设备
url: `${URL}/device/position/app/list`, let userLocation = null
method: 'GET', try {
header: { userLocation = uni.getStorageSync('userLocation')
'Authorization': 'Bearer ' + uni.getStorageSync('token'), } catch (e) {
'Clientid': uni.getStorageSync('client_id') console.warn('获取用户位置失败:', e)
} }
if (!userLocation || !userLocation.latitude || !userLocation.longitude) {
// 如果没有用户位置,使用默认位置
userLocation = { latitude: 39.916527, longitude: 116.397128 }
}
const res = await getNearbyDevices({
userLatitude: userLocation.latitude,
userLongitude: userLocation.longitude,
queryType: 'rent', // 查询可租借设备
radiusKm: 50, // 扩大查询范围以确保能找到目标设备
pageNum: 1,
pageSize: 100
}) })
if (res.statusCode === 200 && res.data?.code === 200) { if (res.code === 200) {
const positions = res.data.rows || [] const devices = res.data?.records || []
// 将设备数据转换为统一格式
const positions = devices.map(transformDeviceData)
const position = positions.find(p => p.positionId === positionId.value) const position = positions.find(p => p.positionId === positionId.value)
if (position) { if (position) {
positionInfo.value = position positionInfo.value = position
} else { } else {
uni.showToast({ uni.showToast({
title: this.$t('location.notExist'), title: $t('location.notExist'),
icon: 'none' icon: 'none'
}) })
} }
} else if (res.statusCode === 401 || res.data?.code === 401 || res.data?.code === 40101) {
uni.reLaunch({
url: '/pages/login/index'
})
} }
} catch (e) { } catch (e) {
console.error('加载场地详情失败:', e) console.error('加载设备详情失败:', e)
uni.showToast({ uni.showToast({
title: $t('common.loadFailed'), title: $t('common.loadFailed'),
icon: 'none' icon: 'none'
+65 -18
View File
@@ -19,7 +19,10 @@
<view class="card" :class="{ available: isRentable(item), invalid: !isValidCoordinate(item.latitude, item.longitude) }" <view class="card" :class="{ available: isRentable(item), invalid: !isValidCoordinate(item.latitude, item.longitude) }"
v-for="(item, index) in filteredPositions" :key="item.positionId || index" v-for="(item, index) in filteredPositions" :key="item.positionId || index"
@click="goToPositionDetail(item)"> @click="goToPositionDetail(item)">
<view class="thumb"></view> <view class="thumb">
<image v-if="item.deviceImg" :src="item.deviceImg" class="thumb-img" mode="aspectFill"></image>
<image v-else src="/static/device-info.png" class="thumb-img" mode="aspectFit"></image>
</view>
<view class="info"> <view class="info">
<view class="row top"> <view class="row top">
<view class="name">{{ item.name }}</view> <view class="name">{{ item.name }}</view>
@@ -34,6 +37,15 @@
<view class="row meta" v-if="!isValidCoordinate(item.latitude, item.longitude)"> <view class="row meta" v-if="!isValidCoordinate(item.latitude, item.longitude)">
<text class="time" style="color: #ff6b6b;">{{ $t('location.coordinateError') }}</text> <text class="time" style="color: #ff6b6b;">{{ $t('location.coordinateError') }}</text>
</view> </view>
<view class="row meta" v-if="item.availablePowerBankCount !== undefined && item.availablePowerBankCount !== null">
<text class="time">可租借{{ item.availablePowerBankCount }} </text>
</view>
<view class="row meta" v-if="item.availableEmptyGridCount !== undefined && item.availableEmptyGridCount !== null">
<text class="time">可归还{{ item.availableEmptyGridCount }} 个空位</text>
</view>
<view class="row meta remark-info" v-if="item.remark">
<text class="time">💰 {{ item.remark }}</text>
</view>
<view class="tags"> <view class="tags">
<view class="tag rent" v-if="isRentable(item)">{{ $t('location.rent') }}</view> <view class="tag rent" v-if="isRentable(item)">{{ $t('location.rent') }}</view>
<view class="tag return" v-if="isReturnable(item)">{{ $t('location.return') }}</view> <view class="tag return" v-if="isReturnable(item)">{{ $t('location.return') }}</view>
@@ -68,8 +80,9 @@
} from 'vue' } from 'vue'
import MapComponent from '../../components/MapComponent.vue' import MapComponent from '../../components/MapComponent.vue'
import { import {
URL getNearbyDevices,
} from '../../config/url.js' transformDeviceData
} from '../../config/api/device.js'
import { import {
getUserLocation, getUserLocation,
getRegeo, getRegeo,
@@ -109,7 +122,10 @@
const setTab = (name) => { const setTab = (name) => {
activeTab.value = name activeTab.value = name
applyFilter() // 切换 tab 时重新查询设备列表(因为接口需要不同的 queryType)
if (userLocation.value) {
loadPositions(userLocation.value)
}
} }
const applyFilter = () => { const applyFilter = () => {
@@ -157,28 +173,43 @@
const loadPositions = async (center) => { const loadPositions = async (center) => {
try { try {
isLoading.value = true isLoading.value = true
const res = await uni.request({
url: `${URL}/device/position/app/list`, if (!center || !center.latitude || !center.longitude) {
method: 'GET', console.warn('中心点位置信息不完整,无法查询附近设备')
header: { return
'Authorization': 'Bearer ' + uni.getStorageSync('token'), }
'Clientid': uni.getStorageSync('client_id')
const res = await getNearbyDevices({
userLatitude: center.latitude,
userLongitude: center.longitude,
queryType: activeTab.value, // 使用当前选中的 tab (rent/return)
radiusKm: 5,
pageNum: 1,
pageSize: 100
})
if (res.code === 200) {
// 新接口返回的是 data.records
const devices = res.data?.records || []
// 将设备数据转换为统一格式
positionList.value = devices.map(device => {
const transformed = transformDeviceData(device)
// 根据当前 tab 设置租借和归还能力
return {
...transformed,
canRent: activeTab.value === 'rent' ? true : (device.availablePowerBankCount > 0),
canReturn: activeTab.value === 'return' ? true : (device.availableEmptyGridCount > 0)
} }
}) })
if (res.statusCode === 200 && res.data?.code === 200) {
// 过滤掉特定的 positionId
const filteredData = (res.data.rows || []).filter(item => item.positionId !== '1979008434641670145')
positionList.value = filteredData
calculateDistances(center) calculateDistances(center)
} else if (res.statusCode === 401 || res.data?.code === 401 || res.data?.code === 40101) {
uni.reLaunch({
url: '/pages/login/index'
})
} else { } else {
positionList.value = [] positionList.value = []
filteredPositions.value = [] filteredPositions.value = []
} }
} catch (e) { } catch (e) {
console.error('查询附近设备失败:', e)
positionList.value = [] positionList.value = []
filteredPositions.value = [] filteredPositions.value = []
} finally { } finally {
@@ -359,6 +390,13 @@
height: 120rpx; height: 120rpx;
border-radius: 16rpx; border-radius: 16rpx;
background: #F0F2F5; background: #F0F2F5;
overflow: hidden;
.thumb-img {
width: 100%;
height: 100%;
border-radius: 16rpx;
}
} }
.info { .info {
@@ -397,6 +435,15 @@
.row.meta { .row.meta {
color: #999; color: #999;
font-size: 22rpx; font-size: 22rpx;
&.remark-info {
color: #FF6B35;
font-weight: 500;
.time {
color: #FF6B35;
}
}
} }
.tags { .tags {
+1 -1
View File
@@ -11,7 +11,7 @@
* *
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/ */
@import 'uview-ui/theme.scss'; @import '@climblee/uv-ui/theme.scss';
/* 颜色变量 */ /* 颜色变量 */