style:根据UI设计图跳转页面样式
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<view class="location-popup" v-if="show">
|
||||
<view class="popup-mask" @click="$emit('close')"></view>
|
||||
<view class="location-sheet" :class="{ 'expanded': expanded }">
|
||||
<view class="sheet-header">
|
||||
<text class="sheet-title">{{ title }} ({{ positions.length }})</text>
|
||||
<view class="close-btn" @click="$emit('close')">
|
||||
<uv-icon name="close"></uv-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="sheet-content">
|
||||
<view class="position-item" v-for="(item, index) in positions" :key="item.positionId || index"
|
||||
@click="$emit('select', item)">
|
||||
<view class="position-info">
|
||||
<view class="position-name">{{ item.name }}</view>
|
||||
<view class="tag-row">
|
||||
<view class="status-tag rent" v-if="isRentable(item)"><text>可租借</text></view>
|
||||
<view class="status-tag return" v-if="isReturnable(item)"><text>可归还</text></view>
|
||||
</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 }}</text></view>
|
||||
<view class="nav-btn" @click.stop="$emit('navigate', item)"><text>导航</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty-state" v-if="!isLoading && (!positions || positions.length === 0)">
|
||||
<image class="empty-icon" src="/static/scan-icon.png" mode="aspectFit" />
|
||||
<text class="empty-text">附近暂无设备</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, default: false },
|
||||
expanded: { type: Boolean, default: false },
|
||||
positions: { type: Array, default: () => [] },
|
||||
isLoading: { type: Boolean, default: false },
|
||||
title: { type: String, default: '附近设备场地' }
|
||||
})
|
||||
|
||||
const isRentable = (item) => {
|
||||
// 兼容多种标识字段
|
||||
if (typeof item?.canRent !== 'undefined') return !!item.canRent
|
||||
return String(item?.status || '').toLowerCase() === 'online'
|
||||
}
|
||||
|
||||
const isReturnable = (item) => {
|
||||
if (typeof item?.canReturn !== 'undefined') return !!item.canReturn
|
||||
return String(item?.status || '').toLowerCase() === 'online'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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: 78vh;
|
||||
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: 88vh;
|
||||
}
|
||||
|
||||
.sheet-header {
|
||||
padding: 20rpx 30rpx;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sheet-content {
|
||||
padding: 12rpx 0;
|
||||
height: 64vh;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.status-tag {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 22rpx;
|
||||
|
||||
&.rent {
|
||||
background: #e8f5e8;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
&.return {
|
||||
background: #e8f2ff;
|
||||
color: #3578e5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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: 60rpx 0;
|
||||
|
||||
.empty-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-bottom: 24rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+169
-215
@@ -3,11 +3,13 @@
|
||||
<!-- 地图容器 -->
|
||||
<view class="map-wrapper">
|
||||
<!-- 使用小程序原生地图组件 -->
|
||||
<map id="map" class="native-map" :longitude="mapCenter.longitude" :latitude="mapCenter.latitude"
|
||||
<map id="map" class="native-map" :longitude="mapCenter.longitude" :latitude="mapCenter.latitude"
|
||||
:markers="mapMarkers" :scale="mapZoom" :show-location="true" @regionchange="onMapRegionChange"
|
||||
@markertap="onMapMarkerTap" @callouttap="onCalloutTap" @updated="onMapUpdated"
|
||||
@error="onMapError">
|
||||
@markertap="onMapMarkerTap" @callouttap="onCalloutTap" @updated="onMapUpdated" @error="onMapError">
|
||||
<!-- 覆盖在地图上的可点击控件(使用 cover-view 以兼容小程序原生组件层级) -->
|
||||
<cover-view class="index-swiper">
|
||||
<image src="/static/index_swiper.png" class="index-swiper" mode="aspectFit"></image>
|
||||
</cover-view>
|
||||
<cover-view class="map-side-controls">
|
||||
<cover-view class="side-btn service" @tap="handleService">
|
||||
<cover-image class="side-icon" src="/static/customer-service.png"></cover-image>
|
||||
@@ -22,11 +24,9 @@
|
||||
<!-- <cover-view class="side-text">定位</cover-view> -->
|
||||
</cover-view>
|
||||
</cover-view>
|
||||
<!-- 使用原生 marker 方案渲染中心指示,随 regionchange 同步到地图中心 -->
|
||||
<!-- 使用原生 marker 方案渲染中心指示,随 regionchange 同步到地图中心 -->
|
||||
</map>
|
||||
|
||||
|
||||
|
||||
<!-- 地图加载状态 -->
|
||||
<view class="map-loading" v-if="isLoading">
|
||||
<view class="loading-content">
|
||||
@@ -39,15 +39,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
watch,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
nextTick,
|
||||
getCurrentInstance
|
||||
} from 'vue'
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
watch,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
nextTick,
|
||||
getCurrentInstance
|
||||
} from 'vue'
|
||||
// 导入地图工具函数
|
||||
import {
|
||||
calculateDistanceSync
|
||||
@@ -76,7 +76,7 @@ import {
|
||||
])
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
const props = defineProps({
|
||||
userLocation: {
|
||||
type: Object,
|
||||
default: null
|
||||
@@ -92,15 +92,15 @@ import {
|
||||
searchKeyword: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
noticeText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
enableMarkers: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
noticeText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
enableMarkers: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
@@ -123,73 +123,79 @@ import {
|
||||
const mapContext = ref(null) // 地图上下文
|
||||
|
||||
// 方法
|
||||
const updateMapMarkers = () => {
|
||||
const markers = []
|
||||
// 中心 marker(始终存在,使用传入中心坐标或 userLocation)
|
||||
const centerLng = Number(mapCenter.value.longitude || (props.userLocation && props.userLocation.longitude))
|
||||
const centerLat = Number(mapCenter.value.latitude || (props.userLocation && props.userLocation.latitude))
|
||||
if (!isNaN(centerLng) && !isNaN(centerLat)) {
|
||||
markers.push({
|
||||
id: 999999, // 固定 id 作为中心点
|
||||
latitude: centerLat,
|
||||
longitude: centerLng,
|
||||
iconPath: '/static/location-icon.png',
|
||||
width: 30,
|
||||
height: 40,
|
||||
anchor: { x: 0.5, y: 1 } // 图钉尖端对准坐标
|
||||
})
|
||||
}
|
||||
const updateMapMarkers = () => {
|
||||
const markers = []
|
||||
// 中心 marker(始终存在,使用传入中心坐标或 userLocation)
|
||||
const centerLng = Number(mapCenter.value.longitude || (props.userLocation && props.userLocation.longitude))
|
||||
const centerLat = Number(mapCenter.value.latitude || (props.userLocation && props.userLocation.latitude))
|
||||
if (!isNaN(centerLng) && !isNaN(centerLat)) {
|
||||
markers.push({
|
||||
id: 999999, // 固定 id 作为中心点
|
||||
latitude: centerLat,
|
||||
longitude: centerLng,
|
||||
iconPath: '/static/location-icon.png',
|
||||
width: 30,
|
||||
height: 40,
|
||||
anchor: {
|
||||
x: 0.5,
|
||||
y: 1
|
||||
} // 图钉尖端对准坐标
|
||||
})
|
||||
}
|
||||
|
||||
// 可选:周边位置点
|
||||
if (props.enableMarkers && props.filteredPositions && props.filteredPositions.length > 0) {
|
||||
props.filteredPositions.forEach((pos, index) => {
|
||||
if (pos.longitude && pos.latitude) {
|
||||
const lat = parseFloat(pos.latitude)
|
||||
const lng = parseFloat(pos.longitude)
|
||||
if (lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
|
||||
markers.push({
|
||||
id: index + 1,
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
width: 24,
|
||||
height: 24,
|
||||
title: pos.name
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// 可选:周边位置点
|
||||
if (props.enableMarkers && props.filteredPositions && props.filteredPositions.length > 0) {
|
||||
props.filteredPositions.forEach((pos, index) => {
|
||||
if (pos.longitude && pos.latitude) {
|
||||
const lat = parseFloat(pos.latitude)
|
||||
const lng = parseFloat(pos.longitude)
|
||||
if (lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
|
||||
markers.push({
|
||||
id: index + 1,
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
width: 24,
|
||||
height: 24,
|
||||
title: pos.name
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
mapMarkers.value = markers
|
||||
isLoading.value = false
|
||||
}
|
||||
mapMarkers.value = markers
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
// 移动地图到指定位置(使用 includePoints 以避免 mapid 错误,并兼容不同基础库)
|
||||
const moveToLocation = (location) => {
|
||||
if (!location || !location.longitude || !location.latitude) return
|
||||
if (!mapContext.value) return
|
||||
// 移动地图到指定位置(使用 includePoints 以避免 mapid 错误,并兼容不同基础库)
|
||||
const moveToLocation = (location) => {
|
||||
if (!location || !location.longitude || !location.latitude) return
|
||||
if (!mapContext.value) return
|
||||
|
||||
try {
|
||||
mapContext.value.includePoints({
|
||||
points: [{ longitude: Number(location.longitude), latitude: Number(location.latitude) }],
|
||||
padding: [60, 60, 60, 60],
|
||||
success: () => {
|
||||
console.log('地图已移动到指定位置(includePoints)')
|
||||
},
|
||||
fail: (err) => {
|
||||
console.warn('includePoints 失败,尝试 moveToLocation():', err)
|
||||
// 回退尝试(不传参,部分基础库仅支持移动到用户当前位置)
|
||||
try {
|
||||
mapContext.value.moveToLocation()
|
||||
} catch (e) {
|
||||
console.error('moveToLocation 回退失败:', e)
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('移动地图异常:', e)
|
||||
}
|
||||
}
|
||||
try {
|
||||
mapContext.value.includePoints({
|
||||
points: [{
|
||||
longitude: Number(location.longitude),
|
||||
latitude: Number(location.latitude)
|
||||
}],
|
||||
padding: [60, 60, 60, 60],
|
||||
success: () => {
|
||||
console.log('地图已移动到指定位置(includePoints)')
|
||||
},
|
||||
fail: (err) => {
|
||||
console.warn('includePoints 失败,尝试 moveToLocation():', err)
|
||||
// 回退尝试(不传参,部分基础库仅支持移动到用户当前位置)
|
||||
try {
|
||||
mapContext.value.moveToLocation()
|
||||
} catch (e) {
|
||||
console.error('moveToLocation 回退失败:', e)
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('移动地图异常:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听用户位置变化
|
||||
watch(() => props.userLocation, (newLocation) => {
|
||||
@@ -219,26 +225,26 @@ const moveToLocation = (location) => {
|
||||
}
|
||||
|
||||
// 地图区域变化事件
|
||||
const onMapRegionChange = (e) => {
|
||||
// 在手势或缩放结束时更新中心坐标
|
||||
if (e && e.type === 'end') {
|
||||
if (mapContext.value) {
|
||||
mapContext.value.getCenterLocation({
|
||||
success: (res) => {
|
||||
if (res && res.longitude && res.latitude) {
|
||||
mapCenter.value = {
|
||||
longitude: res.longitude,
|
||||
latitude: res.latitude
|
||||
}
|
||||
// 更新中心 marker 位置
|
||||
updateMapMarkers()
|
||||
emit('mapCenterChange', mapCenter.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
const onMapRegionChange = (e) => {
|
||||
// 在手势或缩放结束时更新中心坐标
|
||||
if (e && e.type === 'end') {
|
||||
if (mapContext.value) {
|
||||
mapContext.value.getCenterLocation({
|
||||
success: (res) => {
|
||||
if (res && res.longitude && res.latitude) {
|
||||
mapCenter.value = {
|
||||
longitude: res.longitude,
|
||||
latitude: res.latitude
|
||||
}
|
||||
// 更新中心 marker 位置
|
||||
updateMapMarkers()
|
||||
emit('mapCenterChange', mapCenter.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 标记点点击事件
|
||||
const onMapMarkerTap = (e) => {
|
||||
@@ -276,55 +282,23 @@ const moveToLocation = (location) => {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const handleRelocate = () => {
|
||||
// 直接委托父级处理定位并移动地图,避免内部重复弹 loading
|
||||
try {
|
||||
emit('relocate')
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
// 选择位置后移动地图并通知父组件刷新附近列表(增强容错与平台兼容)
|
||||
const onSuccess = (res) => {
|
||||
try {
|
||||
const lng = Number(res.longitude)
|
||||
const lat = Number(res.latitude)
|
||||
if (!isNaN(lng) && !isNaN(lat)) {
|
||||
const center = { longitude: lng, latitude: lat }
|
||||
moveToLocation(center)
|
||||
emit('mapCenterChange', center)
|
||||
} else {
|
||||
uni.showToast({ title: '无效的坐标', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('处理选择位置结果异常:', e)
|
||||
}
|
||||
}
|
||||
const onFail = (err) => {
|
||||
console.error('chooseLocation 失败:', err)
|
||||
// 授权被拒时引导开启
|
||||
if (err && (String(err.errMsg || '').includes('auth') || String(err.errMsg || '').includes('deny'))) {
|
||||
// #ifdef MP-WEIXIN
|
||||
wx.openSetting && wx.openSetting({})
|
||||
// #endif
|
||||
}
|
||||
// uni.showToast({ title: '无法打开选择位置', icon: 'none' })
|
||||
}
|
||||
// #ifdef MP-WEIXIN
|
||||
if (typeof wx !== 'undefined' && wx.chooseLocation) {
|
||||
wx.chooseLocation({ success: onSuccess, fail: onFail })
|
||||
return
|
||||
}
|
||||
// #endif
|
||||
if (uni && typeof uni.chooseLocation === 'function') {
|
||||
uni.chooseLocation({ success: onSuccess, fail: onFail })
|
||||
} else {
|
||||
uni.showToast({ title: '当前环境不支持选择位置', icon: 'none' })
|
||||
}
|
||||
const handleRelocate = () => {
|
||||
// 直接委托父级处理定位并移动地图,避免内部重复弹 loading
|
||||
try {
|
||||
emit('relocate')
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
try {
|
||||
uni.navigateTo({ url: '/pages/search/index' })
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const handleService = () => {
|
||||
uni.navigateTo({ url: '/pages/help/index' })
|
||||
uni.navigateTo({
|
||||
url: '/pages/help/index'
|
||||
})
|
||||
}
|
||||
|
||||
const handleScan = () => {
|
||||
@@ -336,26 +310,26 @@ const handleRelocate = () => {
|
||||
}
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
// 初始化地图上下文
|
||||
nextTick(() => {
|
||||
// 需要使用nextTick确保地图组件已经渲染
|
||||
const inst = getCurrentInstance()
|
||||
const vm = (inst && (inst.proxy || inst)) || undefined
|
||||
try {
|
||||
mapContext.value = uni.createMapContext('map', vm)
|
||||
} catch (e) {
|
||||
// 兼容:如果第二参不被支持,退回单参
|
||||
mapContext.value = uni.createMapContext('map')
|
||||
}
|
||||
updateMapMarkers()
|
||||
onMounted(() => {
|
||||
// 初始化地图上下文
|
||||
nextTick(() => {
|
||||
// 需要使用nextTick确保地图组件已经渲染
|
||||
const inst = getCurrentInstance()
|
||||
const vm = (inst && (inst.proxy || inst)) || undefined
|
||||
try {
|
||||
mapContext.value = uni.createMapContext('map', vm)
|
||||
} catch (e) {
|
||||
// 兼容:如果第二参不被支持,退回单参
|
||||
mapContext.value = uni.createMapContext('map')
|
||||
}
|
||||
updateMapMarkers()
|
||||
|
||||
// 初始化折叠面板
|
||||
if (collapseRef.value) {
|
||||
collapseRef.value.init()
|
||||
}
|
||||
})
|
||||
})
|
||||
// 初始化折叠面板
|
||||
if (collapseRef.value) {
|
||||
collapseRef.value.init()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理工作
|
||||
@@ -447,7 +421,7 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 地图中心图钉(cover-view,避免使用 transform 以兼容小程序) */
|
||||
.center-pin {
|
||||
position: absolute;
|
||||
@@ -479,7 +453,7 @@ onMounted(() => {
|
||||
|
||||
.map-side-controls {
|
||||
position: absolute;
|
||||
left: 20rpx;
|
||||
right: 20rpx;
|
||||
bottom: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -492,7 +466,7 @@ onMounted(() => {
|
||||
.side-btn {
|
||||
// min-width: 160rpx;
|
||||
margin: auto;
|
||||
height: 72rpx;
|
||||
// height: 72rpx;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border-radius: 36rpx;
|
||||
display: inline-flex;
|
||||
@@ -500,7 +474,7 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.12);
|
||||
padding: 0 18rpx;
|
||||
padding: 20rpx;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
|
||||
&:active {
|
||||
@@ -514,40 +488,9 @@ onMounted(() => {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.search {
|
||||
border-color: #07c160;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 展示用图标列(与 cover-view 对齐) */
|
||||
.map-side-icons {
|
||||
position: absolute;
|
||||
left: 20rpx;
|
||||
bottom: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
|
||||
.side-btn {
|
||||
min-width: 160rpx;
|
||||
height: 72rpx;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border-radius: 36rpx;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.12);
|
||||
padding: 0 18rpx;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
|
||||
.label {
|
||||
margin-left: 8rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
// &.search {
|
||||
// border-color: #07c160;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -562,9 +505,20 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.side-icon{
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
.side-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.index-swiper {
|
||||
width: 92vw;
|
||||
height: 180rpx;
|
||||
border-radius: 20rpx;
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
right: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,364 @@
|
||||
<template>
|
||||
<view class="order-item" >
|
||||
<!-- 订单头部信息 -->
|
||||
<view class="order-header">
|
||||
<view class="header-left">
|
||||
<view class="status-chip" :class="statusChipClass"><text class="chip-text">{{ statusText }}</text></view>
|
||||
<view class="title">{{ titleText }}</view>
|
||||
</view>
|
||||
<view class="header-right">
|
||||
<!-- 支付方式标识(移到头部右侧) -->
|
||||
<view class="payment-badge wx-score" v-if="order.payWay == 'wx_score_pay'">
|
||||
<image src="/static/images/wxpayflag.png" mode="aspectFit" class="badge-icon"></image>
|
||||
<view class="badge-text">
|
||||
<text>微信支付分</text>
|
||||
<text class="divider">|</text>
|
||||
<text class="highlight">免押租借</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="payment-badge member" v-else-if="order.payWay == 'wx_member_pay'">
|
||||
<text class="badge-text">会员订单</text>
|
||||
</view>
|
||||
<view class="payment-badge deposit" v-else>
|
||||
<text class="badge-text">微信支付</text>
|
||||
<text class="divider">|</text>
|
||||
<text class="badge-text">押金租借</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单内容 -->
|
||||
<view class="order-body" @click="onDetails">
|
||||
<!-- <view class="device-info">
|
||||
<view class="device-left">
|
||||
<view class="device-name">{{ titleText }}</view>
|
||||
<view class="device-id">设备号:{{ order.deviceId }}</view>
|
||||
</view>
|
||||
</view> -->
|
||||
|
||||
<!-- 订单时间信息 -->
|
||||
<view class="order-times">
|
||||
<view class="time-row">
|
||||
<text class="time-label">租借地点:</text>
|
||||
<text class="time-value">{{ order.deviceName || order.positionName }}</text>
|
||||
</view>
|
||||
<view class="time-row">
|
||||
<text class="time-label">租借时间:</text>
|
||||
<text class="time-value">{{ order.startTime }}</text>
|
||||
</view>
|
||||
<view class="arrow" @click="onDetails">
|
||||
<uv-icon name="arrow-right" size="24rpx" color="#999"></uv-icon>
|
||||
</view>
|
||||
<!-- <view class="time-row">
|
||||
<text class="time-label">结束时间:</text>
|
||||
<text class="time-value">{{ order.endTime || '-' }}</text>
|
||||
</view> -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单底部 -->
|
||||
<view class="order-footer">
|
||||
<view class="footer-left">
|
||||
<view v-if="isInUse" class="renting"><text class="dot"></text>租借中</view>
|
||||
<view v-else-if="isFinished" class="meta">
|
||||
<view class="meta-item"><text class="dot"></text>{{ usedDurationText }}</view>
|
||||
<view class="meta-item"><text class="currency">¥</text>{{ displayAmount }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="actions">
|
||||
<!-- 待支付状态显示支付和取消按钮 -->
|
||||
<view v-if="isWaitingForPayment" class="action-item primary" @click="onPay">立即支付</view>
|
||||
<view v-if="isWaitingForPayment" class="action-item secondary" @click="onCancel">取消订单</view>
|
||||
<!-- 使用中状态显示归还设备按钮 -->
|
||||
<view v-if="isInUse" class="action-item primary" @click="onReturn">快速归还</view>
|
||||
<!-- 查看详情按钮对所有订单都显示 -->
|
||||
<!-- <view class="action-item secondary" >查看详情</view> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
order: { type: Object, required: true },
|
||||
orderStatusMap: { type: Object, required: true }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['pay', 'cancel', 'return-device', 'details']);
|
||||
|
||||
const rawStatus = computed(() => props.order.orderStatus ?? props.order.status);
|
||||
const normalizedStatus = computed(() => {
|
||||
const s = rawStatus.value;
|
||||
switch (s) {
|
||||
case 0:
|
||||
case '0':
|
||||
return 'waiting_for_payment';
|
||||
case 1:
|
||||
case '1':
|
||||
return 'in_used';
|
||||
case 2:
|
||||
case '2':
|
||||
return 'used_done';
|
||||
case 3:
|
||||
case '3':
|
||||
return 'order_cancelled';
|
||||
default:
|
||||
return s || '';
|
||||
}
|
||||
});
|
||||
|
||||
const statusDef = computed(() => props.orderStatusMap?.[rawStatus.value] || props.orderStatusMap?.[normalizedStatus.value] || {});
|
||||
const statusText = computed(() => statusDef.value.text || '');
|
||||
const statusChipClass = computed(() => {
|
||||
const cls = statusDef.value.class || '';
|
||||
if (cls.includes('status-using')) return 'chip-using';
|
||||
if (cls.includes('status-waiting')) return 'chip-waiting';
|
||||
if (cls.includes('status-finished')) return 'chip-finished';
|
||||
if (cls.includes('status-cancelled')) return 'chip-cancelled';
|
||||
return 'chip-default';
|
||||
});
|
||||
|
||||
const isWaitingForPayment = computed(() => normalizedStatus.value === 'waiting_for_payment');
|
||||
const isInUse = computed(() => normalizedStatus.value === 'in_used');
|
||||
const isFinished = computed(() => normalizedStatus.value === 'used_done');
|
||||
|
||||
const titleText = computed(() => props.order.deviceName ? '租借风扇' : '租借风扇');
|
||||
|
||||
// 显示金额(优先后端给定字段)
|
||||
const displayAmount = computed(() => props.order.amount || props.order.payAmount || props.order.actualDeviceAmount || props.order.currentFee || '0.00');
|
||||
|
||||
// 使用时长
|
||||
const usedDurationText = computed(() => {
|
||||
const start = parseDate(props.order.startTime);
|
||||
const end = parseDate(props.order.endTime) || new Date();
|
||||
if (!start) return '';
|
||||
const diffMs = Math.max(0, end.getTime() - start.getTime());
|
||||
const minutes = Math.floor(diffMs / 60000);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
if (hours > 0) return `${hours}小时${mins}分钟`;
|
||||
return `${mins}分钟`;
|
||||
});
|
||||
|
||||
function parseDate(str) {
|
||||
if (!str) return null;
|
||||
try {
|
||||
return new Date(String(str).replace(/-/g, '/'));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const onPay = () => emit('pay', props.order);
|
||||
const onCancel = () => emit('cancel', props.order);
|
||||
const onReturn = () => emit('return-device', props.order);
|
||||
const onDetails = () => emit('details', props.order);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.order-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
// 订单头部
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
// border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
.header-left { display: flex; align-items: center; }
|
||||
.title { font-size: 30rpx; color: #222; margin-left: 16rpx; font-weight: 600; }
|
||||
.status-chip {
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
background: #f5f5f5;
|
||||
transform: skewX(-15deg);
|
||||
overflow: hidden;
|
||||
.chip-text { display: inline-block; transform: skewX(15deg); }
|
||||
&.chip-using { background: rgba(7,193,96,0.12); color: #07c160; }
|
||||
&.chip-waiting { background: rgba(255,152,0,0.12); color: #FF9800; }
|
||||
&.chip-finished { background: rgba(76,175,80,0.12); color: #4CAF50; }
|
||||
&.chip-cancelled { background: rgba(158,158,158,0.12); color: #9E9E9E; }
|
||||
}
|
||||
|
||||
.header-right { display: flex; align-items: center; }
|
||||
// 支付标识:仅文字与图标,无底色
|
||||
.payment-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
white-space: nowrap;
|
||||
|
||||
&.wx-score { }
|
||||
&.member { }
|
||||
&.deposit { }
|
||||
|
||||
.badge-icon { width: 32rpx; height: 26rpx; margin-right: 8rpx; }
|
||||
.badge-text { font-size: 22rpx; color: #07c160; font-weight: 500; background: transparent; }
|
||||
.divider { margin: 0 6rpx; color: #07c160; }
|
||||
}
|
||||
|
||||
// 不同支付方式的文字颜色
|
||||
.payment-badge.member {
|
||||
.badge-text, .divider { color: #1976D2; }
|
||||
}
|
||||
.payment-badge.deposit {
|
||||
.badge-text, .divider { color: #666; }
|
||||
}
|
||||
}
|
||||
|
||||
// 订单内容
|
||||
.order-body {
|
||||
padding: 24rpx;
|
||||
|
||||
.device-info {
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
.device-left {
|
||||
flex: 1;
|
||||
margin-right: 20rpx;
|
||||
|
||||
.device-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.device-id {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.device-right {
|
||||
// 支付分标识
|
||||
.payment-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
white-space: nowrap;
|
||||
|
||||
&.wx-score {
|
||||
background: rgba(7, 193, 96, 0.08);
|
||||
|
||||
.badge-icon {
|
||||
width: 32rpx;
|
||||
height: 26rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
font-size: 22rpx;
|
||||
color: #07c160;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.divider { margin: 0 6rpx; }
|
||||
.highlight { font-weight: 500; }
|
||||
}
|
||||
}
|
||||
|
||||
&.member {
|
||||
background: rgba(25, 118, 210, 0.08);
|
||||
|
||||
.badge-text {
|
||||
font-size: 22rpx;
|
||||
color: #1976D2;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
&.deposit {
|
||||
background: #f5f5f5;
|
||||
|
||||
.badge-text {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order-times {
|
||||
position: relative;
|
||||
.time-row {
|
||||
display: flex;
|
||||
font-size: 26rpx;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.time-label { color: #999; width: 140rpx; }
|
||||
.time-value { color: #333; flex: 1; }
|
||||
}
|
||||
.arrow {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 订单底部
|
||||
.order-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
background: #fafafa;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
|
||||
.footer-left { display: flex; align-items: center; }
|
||||
.renting { font-size: 26rpx; color: #333; display: flex; align-items: center; }
|
||||
.meta { display: flex; align-items: center; }
|
||||
.meta-item { font-size: 26rpx; color: #333; display: flex; align-items: center; margin-right: 28rpx; }
|
||||
.dot { width: 16rpx; height: 16rpx; background: #000; border-radius: 50%; margin-right: 12rpx; display: inline-block; }
|
||||
.currency { margin-right: 4rpx; color: #333; }
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
|
||||
.action-item {
|
||||
font-size: 26rpx;
|
||||
padding: 10rpx 30rpx;
|
||||
border-radius: 30rpx;
|
||||
margin-left: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10rpx;
|
||||
|
||||
&.primary { background: #07c160; color: #fff; }
|
||||
&.secondary { background: #f5f5f5; color: #666; border: 1rpx solid #e0e0e0; }
|
||||
&:active { opacity: 0.8; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user