style:根据UI设计图跳转页面样式

This commit is contained in:
2025-10-15 01:35:23 +08:00
parent 4408673438
commit 46179c5d3f
30 changed files with 4632 additions and 2459 deletions
+391
View File
@@ -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>