Files
uni-fans-score/pages/position/detail.vue
T
2026-01-22 10:52:58 +08:00

362 lines
8.7 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="position-detail-page">
<!-- 顶部设备柜图示 -->
<view class="device-illustration">
<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 class="info-card">
<!-- 场地名称 -->
<view class="position-name">{{ positionInfo.name || $t('common.loading') }}</view>
<!-- 地址信息 -->
<view class="info-item" v-if="positionInfo.location">
<image src="/static/device-location.png" class="item-icon" mode="aspectFit"></image>
<text class="item-text">{{ positionInfo.location }}</text>
</view>
<!-- 营业时间 -->
<view class="info-item" v-if="positionInfo.workTime && positionInfo.workTime !== '0'">
<image src="/static/device-time.png" class="item-icon" mode="aspectFit"></image>
<text class="item-text">{{ $t('location.businessHours') }}{{ positionInfo.workTime }}</text>
</view>
<!-- 计费信息 -->
<view class="info-item">
<image src="/static/device-price.png" class="item-icon" mode="aspectFit"></image>
<text class="item-text">{{ $t('device.pricing') }}{{ pricingText }}</text>
</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 style="display: flex;flex-direction: row;gap: 10rpx;">
<view class="status-btn" v-if="isRentable">{{ $t('location.rent') }}</view>
<view class="status-btn" v-if="isReturnable">{{ $t('location.return') }}</view>
</view>
<view class="nav-btn" @click.stop="navigateToPosition">{{ $t('location.navigateHere') }}</view>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="footer-actions">
<button class="action-btn btn-outline" @click="reportError">{{ $t('device.reportError') }}</button>
<button class="action-btn btn-primary" @click="scanCode">{{ $t('device.scanToUse') }}</button>
</view>
</view>
</template>
<script setup>
import {
ref,
computed,
onMounted
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app'
import {
getNearbyDevices,
transformDeviceData
} from '../../config/api/device.js'
import { useI18n } from '../../utils/i18n.js'
const { t } = useI18n()
const positionInfo = ref({})
const positionId = ref('')
const isRentable = computed(() => {
if (typeof positionInfo.value?.canRent !== 'undefined') {
return !!positionInfo.value.canRent
}
return String(positionInfo.value?.status || '').toLowerCase() === 'online'
})
const isReturnable = computed(() => {
if (typeof positionInfo.value?.canReturn !== 'undefined') {
return !!positionInfo.value.canReturn
}
return String(positionInfo.value?.status || '').toLowerCase() === 'online'
})
const pricingText = computed(() => {
// 使用设备的 remark 字段作为计费信息
if (positionInfo.value?.remark) {
return positionInfo.value.remark
}
// 如果 remark 为空,显示默认提示
return '暂无计费信息'
})
onLoad(async (options) => {
if (options.positionId) {
positionId.value = options.positionId
await loadPositionDetail()
}
})
const loadPositionDetail = async () => {
try {
uni.showLoading({
title: t('common.loading')
})
// 获取用户位置用于查询附近设备
let userLocation = null
try {
userLocation = uni.getStorageSync('userLocation')
} catch (e) {
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.code === 200) {
const devices = res.data?.records || []
// 将设备数据转换为统一格式
const positions = devices.map(transformDeviceData)
const position = positions.find(p => p.positionId === positionId.value)
if (position) {
positionInfo.value = position
} else {
uni.showToast({
title: t('location.notExist'),
icon: 'none'
})
}
}
} catch (e) {
console.error('加载设备详情失败:', e)
uni.showToast({
title: t('common.loadFailed'),
icon: 'none'
})
} finally {
uni.hideLoading()
}
}
const navigateToPosition = () => {
if (!positionInfo.value.latitude || !positionInfo.value.longitude) {
uni.showToast({
title: t('location.coordinateError'),
icon: 'none'
})
return
}
const latitude = parseFloat(positionInfo.value.latitude)
const longitude = parseFloat(positionInfo.value.longitude)
// 验证坐标有效性
if (isNaN(latitude) || isNaN(longitude) ||
latitude < -90 || latitude > 90 ||
longitude < -180 || longitude > 180 ||
(latitude === 0 && longitude === 0)) {
uni.showToast({
title: t('location.coordinateError'),
icon: 'none'
})
return
}
uni.openLocation({
latitude,
longitude,
name: positionInfo.value.name,
address: positionInfo.value.location
})
}
const reportError = () => {
uni.navigateTo({
url: '/pages/feedback/index'
})
}
const scanCode = () => {
uni.scanCode({
scanType: ['qrCode', 'barCode'],
success: (res) => {
console.log('扫码结果:', res)
// 处理扫码结果,跳转到设备详情页
if (res.result) {
// 假设二维码内容是设备号
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${res.result}`
})
}
},
fail: (err) => {
console.error('扫码失败:', err)
uni.showToast({
title: this.$t('home.scanFailed'),
icon: 'none'
})
}
})
}
</script>
<style lang="scss" scoped>
.position-detail-page {
min-height: 100vh;
background: linear-gradient(180deg, #D1FFE1 0%, #F5F9F7 100%);
padding-bottom: 200rpx;
}
.device-illustration {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 0 30rpx;
.device-img {
width: 480rpx;
height: 480rpx;
}
}
.info-card {
background: #ffffff;
margin: 0 32rpx;
border-radius: 32rpx;
padding: 44rpx 36rpx 36rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
.position-name {
font-size: 44rpx;
font-weight: 700;
color: #1A1A1A;
margin-bottom: 32rpx;
line-height: 1.3;
}
.info-item {
display: flex;
align-items: flex-start;
margin-bottom: 20rpx;
.item-icon {
width: 32rpx;
height: 32rpx;
margin-right: 12rpx;
margin-top: 2rpx;
flex-shrink: 0;
}
.item-text {
flex: 1;
font-size: 28rpx;
color: #666666;
line-height: 1.6;
}
}
.button-group {
display: flex;
gap: 12rpx;
margin-top: 36rpx;
justify-content: space-between;
flex-wrap: wrap;
.status-btn {
padding: 12rpx 28rpx;
border-radius: 40rpx;
font-size: 26rpx;
font-weight: 500;
border: 2rpx solid #3EAB64;
color: #3EAB64;
background: #ffffff;
}
.nav-btn {
padding: 12rpx 28rpx;
border-radius: 40rpx;
font-size: 26rpx;
font-weight: 500;
border: 2rpx solid #3EAB64;
color: #ffffff;
background: #3EAB64;
&:active {
opacity: 0.8;
}
}
}
}
.footer-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #ffffff;
padding: 24rpx 32rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
box-shadow: 0 -2rpx 16rpx rgba(0, 0, 0, 0.08);
display: flex;
gap: 16rpx;
.action-btn {
flex: 1;
height: 96rpx;
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 600;
border: none;
&.btn-outline {
border: 2rpx solid #3EAB64;
color: #3EAB64;
background: #ffffff;
}
&.btn-primary {
background: #3EAB64;
color: #ffffff;
}
&:active {
opacity: 0.85;
transform: scale(0.98);
}
&::after {
border: none;
}
}
}
</style>