style:根据UI设计图跳转页面样式
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
<template>
|
||||
<view class="search-page">
|
||||
<!-- <view class="map-wrap">
|
||||
<MapComponent :userLocation="userLocation" :filteredPositions="filteredPositions"
|
||||
@mapCenterChange="onMapCenterChange" />
|
||||
</view> -->
|
||||
<view class="list-wrap">
|
||||
<view class="panel">
|
||||
<view class="filter-tabs">
|
||||
<view class="tab" :class="{ active: activeTab === 'rent' }" @click="setTab('rent')">可租借</view>
|
||||
<view class="tab" :class="{ active: activeTab === 'return' }" @click="setTab('return')">可归还</view>
|
||||
</view>
|
||||
<scroll-view class="list-scroll" scroll-y="true">
|
||||
<view class="card" :class="{ available: isRentable(item) }"
|
||||
v-for="(item, index) in filteredPositions" :key="item.positionId || index">
|
||||
<view class="thumb"></view>
|
||||
<view class="info">
|
||||
<view class="row top">
|
||||
<view class="name">{{ item.name }}</view>
|
||||
|
||||
</view>
|
||||
<view class="row sub" v-if="item.location">
|
||||
<text class="addr">{{ item.location }}</text>
|
||||
</view>
|
||||
<view class="row meta" v-if="item.workTime && item.workTime !== '0'">
|
||||
<text class="time">营业时间:{{ item.workTime }}</text>
|
||||
</view>
|
||||
<view class="tags">
|
||||
<view class="tag rent" v-if="isRentable(item)">可租借</view>
|
||||
<view class="tag return" v-if="isReturnable(item)">可归还</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="actions">
|
||||
|
||||
<view class="nav" @click.stop="navigateToPosition(item)">
|
||||
<image src="/static/location.png" class="action-icon" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="distance" v-if="item.distance">{{ item.distance }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty-state" v-if="!isLoading && (!positionList || positionList.length === 0)">
|
||||
<image class="empty-icon" src="/static/scan-icon.png" mode="aspectFit" />
|
||||
<text class="empty-text">附近暂无设备</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
onMounted
|
||||
} from 'vue'
|
||||
import MapComponent from '../../components/MapComponent.vue'
|
||||
import {
|
||||
URL
|
||||
} from '../../config/url.js'
|
||||
import {
|
||||
getUserLocation,
|
||||
getRegeo,
|
||||
calculateDistanceSync
|
||||
} from '../../utils/mapUtils.js'
|
||||
|
||||
const userLocation = ref(null)
|
||||
const positionList = ref([])
|
||||
const filteredPositions = ref([])
|
||||
const isLoading = ref(false)
|
||||
const activeTab = ref('rent')
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
const formatDistance = (meters) => {
|
||||
if (meters < 1000) return `${Math.round(meters)}m`
|
||||
return `${(meters / 1000).toFixed(1)}km`
|
||||
}
|
||||
|
||||
const setTab = (name) => {
|
||||
activeTab.value = name
|
||||
applyFilter()
|
||||
}
|
||||
|
||||
const applyFilter = () => {
|
||||
if (activeTab.value === 'rent') {
|
||||
filteredPositions.value = positionList.value.filter(isRentable)
|
||||
} else if (activeTab.value === 'return') {
|
||||
filteredPositions.value = positionList.value.filter(isReturnable)
|
||||
} else {
|
||||
filteredPositions.value = [...positionList.value]
|
||||
}
|
||||
}
|
||||
|
||||
const calculateDistances = (center) => {
|
||||
positionList.value.forEach(item => {
|
||||
if (item.longitude && item.latitude) {
|
||||
try {
|
||||
const distanceInMeters = calculateDistanceSync(
|
||||
center.latitude,
|
||||
center.longitude,
|
||||
parseFloat(item.latitude),
|
||||
parseFloat(item.longitude)
|
||||
)
|
||||
item.distance = formatDistance(distanceInMeters)
|
||||
item.distanceInMeters = distanceInMeters
|
||||
} catch (_) {}
|
||||
}
|
||||
})
|
||||
positionList.value.sort((a, b) => (a.distanceInMeters || 999000) - (b.distanceInMeters || 999000))
|
||||
applyFilter()
|
||||
}
|
||||
|
||||
const loadPositions = async (center) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const res = await uni.request({
|
||||
url: `${URL}/device/position/app/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)
|
||||
} else if (res.statusCode === 401 || res.data?.code === 401 || res.data?.code === 40101) {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index'
|
||||
})
|
||||
} else {
|
||||
positionList.value = []
|
||||
filteredPositions.value = []
|
||||
}
|
||||
} catch (e) {
|
||||
positionList.value = []
|
||||
filteredPositions.value = []
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const loc = await getUserLocation()
|
||||
userLocation.value = {
|
||||
longitude: loc.longitude,
|
||||
latitude: loc.latitude
|
||||
}
|
||||
await loadPositions(userLocation.value)
|
||||
} catch (e) {
|
||||
await loadPositions(userLocation.value || {
|
||||
longitude: 0,
|
||||
latitude: 0
|
||||
})
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onMapCenterChange = (center) => {
|
||||
if (center && typeof center.longitude !== 'undefined' && typeof center.latitude !== 'undefined') {
|
||||
userLocation.value = {
|
||||
longitude: Number(center.longitude),
|
||||
latitude: Number(center.latitude)
|
||||
}
|
||||
loadPositions(userLocation.value)
|
||||
}
|
||||
}
|
||||
|
||||
const navigateToPosition = (position) => {
|
||||
const latitude = parseFloat(position.latitude)
|
||||
const longitude = parseFloat(position.longitude)
|
||||
uni.openLocation({
|
||||
latitude,
|
||||
longitude,
|
||||
name: position.name,
|
||||
address: position.location
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.map-wrap {
|
||||
flex: 0 0 48vh;
|
||||
padding: 20rpx 20rpx 0 20rpx;
|
||||
}
|
||||
|
||||
.list-wrap {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
margin-top: -20rpx;
|
||||
padding: 0 20rpx 20rpx;
|
||||
|
||||
.panel {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
|
||||
padding: 16rpx 16rpx 8rpx;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
background: #E8F8EF;
|
||||
padding: 8rpx;
|
||||
border-radius: 28rpx;
|
||||
gap: 8rpx;
|
||||
margin: 6rpx 6rpx 12rpx 6rpx;
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
color: #3EAB64;
|
||||
font-weight: 600;
|
||||
|
||||
&.active {
|
||||
background: #3EAB64;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-scroll {
|
||||
height: calc(52vh - 88rpx);
|
||||
padding: 6rpx 4rpx 12rpx 4rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
display: grid;
|
||||
grid-template-columns: 120rpx 1fr 72rpx;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 20rpx;
|
||||
border-radius: 20rpx;
|
||||
border: 2rpx solid #E3EDE7;
|
||||
background: #ffffff;
|
||||
margin: 12rpx 8rpx;
|
||||
|
||||
&.available {
|
||||
border-color: #BFE2CF;
|
||||
background: #F6FBF8;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #F0F2F5;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
|
||||
.row.top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #2A2A2A;
|
||||
max-width: 70%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.row.sub {
|
||||
color: #888;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.addr {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.row.meta {
|
||||
color: #999;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tag.rent {
|
||||
background: #E8F5E8;
|
||||
color: #3EAB64;
|
||||
}
|
||||
|
||||
.tag.return {
|
||||
background: #E8F2FF;
|
||||
color: #3578e5;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 25%;
|
||||
background: #E8F5EE;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #3EAB64;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
filter: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.distance {
|
||||
font-size: 24rpx;
|
||||
color: #2A8E5A;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user