<template>
|
<view class="scenic-page">
|
<view class="top-panel">
|
<view class="nav" :style="navStyle">
|
<view class="nav__bar" :style="navBarStyle">
|
<view class="nav__back" @tap="handleBack">
|
<image class="nav__back-icon" src="/static/ui-icons/back-black.svg" mode="aspectFit" />
|
</view>
|
<text class="nav__title">名导精讲</text>
|
<view class="nav__right-spacer" :style="capsuleSpacerStyle" />
|
</view>
|
</view>
|
|
<view class="search-row">
|
<view class="search-input-wrap">
|
<image class="search-input__icon" src="/static/ui-icons/search-gray.svg" mode="aspectFit" />
|
<input
|
class="search-input"
|
:value="keyword"
|
placeholder="请输入景区名称"
|
placeholder-class="search-input__placeholder"
|
confirm-type="search"
|
@input="handleKeywordInput"
|
@confirm="handleSearch"
|
/>
|
<view v-if="keyword" class="search-clear" @tap="clearKeyword">
|
<image class="search-clear__icon" src="/static/ui-icons/clear-gray.svg" mode="aspectFit" />
|
</view>
|
</view>
|
<view class="search-button" @tap="handleSearch">搜索</view>
|
</view>
|
</view>
|
|
<scroll-view class="search-panel" scroll-y="true">
|
<view v-if="!hasKeyword" class="default-content">
|
<view v-if="hasSearchHistory" class="history-section">
|
<view class="section-title-row">
|
<text class="section-title">搜索历史</text>
|
<view class="history-clear" @tap="clearHistory">
|
<image class="history-clear__icon" src="/static/ui-icons/trash-gray.svg" mode="aspectFit" />
|
</view>
|
</view>
|
<view class="history-tags">
|
<view
|
v-for="item in searchHistory"
|
:key="item"
|
class="history-tag"
|
@tap="useHistory(item)"
|
>
|
{{ item }}
|
</view>
|
</view>
|
</view>
|
|
<view v-if="hasSearchHistory" class="divider" />
|
|
<view class="current-row">
|
<text class="current-label">当前景区:</text>
|
<text class="current-name">{{ currentScenic.name }}</text>
|
<view class="relocate" @tap="handleRelocate">
|
<text>重新定位</text>
|
<image class="relocate__icon" src="/static/ui-icons/location-blue.svg" mode="aspectFit" />
|
</view>
|
</view>
|
|
<view class="divider" />
|
|
<view class="nearby-section">
|
<text class="section-title">距离您最近</text>
|
<view class="scenic-list">
|
<view
|
v-for="item in nearbyScenics"
|
:key="item.id"
|
class="scenic-item"
|
@tap="selectScenic(item)"
|
>
|
<view class="scenic-item__main">
|
<view class="scenic-item__title-row">
|
<text class="scenic-item__name">{{ item.name }}</text>
|
<text class="scenic-item__grade">{{ item.grade }}</text>
|
</view>
|
<view class="scenic-item__meta">
|
<text>{{ item.city }} {{ item.address }}</text>
|
<text>{{ item.distance }}</text>
|
</view>
|
</view>
|
<image class="scenic-item__arrow" src="/static/ui-icons/chevron-right-gray.svg" mode="aspectFit" />
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<view v-else class="result-content">
|
<view v-if="filteredScenics.length" class="scenic-list scenic-list--result">
|
<view
|
v-for="item in filteredScenics"
|
:key="item.id"
|
class="scenic-item"
|
@tap="selectScenic(item)"
|
>
|
<view class="scenic-item__main">
|
<view class="scenic-item__title-row">
|
<text class="scenic-item__name">{{ item.name }}</text>
|
<text class="scenic-item__grade">{{ item.grade }}</text>
|
</view>
|
<view class="scenic-item__meta">
|
<text>{{ item.city }} {{ item.address }}</text>
|
<text>{{ item.distance }}</text>
|
</view>
|
</view>
|
<image class="scenic-item__arrow" src="/static/ui-icons/chevron-right-gray.svg" mode="aspectFit" />
|
</view>
|
</view>
|
<view v-else class="empty-state">暂无相关景区</view>
|
</view>
|
</scroll-view>
|
</view>
|
</template>
|
|
<script setup>
|
import { computed, ref } from 'vue'
|
import { getNavMetrics } from '@/utils/system-info'
|
import { onLoad, onShow } from '@dcloudio/uni-app'
|
import { getCurrentCoordinates } from '@/services/amap'
|
import { getScenicList } from '@/services/scenic'
|
import {
|
SCENIC_DETAIL_STORAGE_KEY,
|
SCENIC_MANUAL_SELECTED_KEY,
|
SCENIC_STORAGE_KEY,
|
scenicOptions
|
} from '@/constants/scenics'
|
|
const HISTORY_STORAGE_KEY = 'scenicSearchHistory'
|
const keyword = ref('')
|
const navMetrics = ref(getNavMetrics())
|
const searchHistory = ref(getStoredHistory())
|
const scenics = ref(scenicOptions)
|
const currentScenic = ref(getStoredScenic())
|
const coordinates = ref(null)
|
|
const navStyle = computed(() => ({
|
height: `${navMetrics.value.navHeight}px`,
|
paddingTop: `${navMetrics.value.statusBarHeight}px`
|
}))
|
|
const navBarStyle = computed(() => ({
|
height: `${navMetrics.value.navBarHeight}px`
|
}))
|
|
const capsuleSpacerStyle = computed(() => ({
|
width: `${navMetrics.value.capsuleWidth}px`
|
}))
|
|
const hasKeyword = computed(() => keyword.value.trim().length > 0)
|
const hasSearchHistory = computed(() => searchHistory.value.length > 0)
|
|
const filteredScenics = computed(() => scenics.value)
|
|
const nearbyScenics = computed(() => {
|
return scenics.value.filter((item) => item.id !== currentScenic.value.id)
|
})
|
|
onLoad(() => {
|
loadPageData()
|
})
|
|
onShow(() => {
|
currentScenic.value = getStoredScenic()
|
})
|
|
function getStoredScenic() {
|
try {
|
const scenicId = uni.getStorageSync(SCENIC_STORAGE_KEY)
|
const storedScenic = uni.getStorageSync(SCENIC_DETAIL_STORAGE_KEY)
|
return scenics.value.find((item) => item.id === String(scenicId))
|
|| (storedScenic && storedScenic.id === String(scenicId) ? storedScenic : null)
|
|| scenics.value[0]
|
|| scenicOptions[0]
|
} catch (error) {
|
return scenics.value[0] || scenicOptions[0]
|
}
|
}
|
|
async function loadPageData() {
|
await updateCoordinates(false)
|
await loadScenics()
|
currentScenic.value = getStoredScenic()
|
}
|
|
async function updateCoordinates(showError) {
|
try {
|
coordinates.value = await getCurrentCoordinates()
|
return true
|
} catch (error) {
|
console.warn('当前位置获取失败', error)
|
if (showError) {
|
uni.showToast({
|
title: '定位失败,请检查授权',
|
icon: 'none'
|
})
|
}
|
return false
|
}
|
}
|
|
async function loadScenics(searchKeyword = keyword.value.trim()) {
|
try {
|
const list = await getScenicList({
|
...(searchKeyword ? { keyword: searchKeyword } : {}),
|
...(coordinates.value || {})
|
})
|
const normalized = normalizeScenics(list)
|
scenics.value = normalized
|
currentScenic.value = getStoredScenic()
|
} catch (error) {
|
console.warn('景区接口加载失败,使用本地数据', error)
|
}
|
}
|
|
function normalizeScenics(items = []) {
|
return items.map((item) => {
|
const fallback = scenicOptions.find((option) => (
|
option.name === item.name || String(option.id) === String(item.scenicCode)
|
))
|
return {
|
id: String(item.scenicId || item.scenicCode || item.name),
|
scenicId: item.scenicId,
|
scenicCode: item.scenicCode,
|
name: item.name,
|
grade: item.grade || (fallback && fallback.grade) || '',
|
city: item.city || '',
|
address: item.address || '',
|
distanceMeters: item.distanceMeters,
|
distance: item.distance || '距离未知',
|
meetingPointName: item.meetingPointName,
|
location: item.longitude != null && item.latitude != null ? `${item.longitude},${item.latitude}` : '',
|
zoom: item.mapZoom || 16,
|
fenceRadius: item.fenceRadius || 260
|
}
|
}).filter((item) => item.id && item.name)
|
}
|
|
function getStoredHistory() {
|
try {
|
const history = uni.getStorageSync(HISTORY_STORAGE_KEY)
|
return Array.isArray(history) ? history : []
|
} catch (error) {
|
return []
|
}
|
}
|
|
function saveHistory(nextHistory) {
|
searchHistory.value = nextHistory
|
uni.setStorageSync(HISTORY_STORAGE_KEY, nextHistory)
|
}
|
|
function addHistory(value) {
|
const text = value.trim()
|
if (!text) return
|
|
saveHistory([text, ...searchHistory.value.filter((item) => item !== text)].slice(0, 6))
|
}
|
|
function handleKeywordInput(event) {
|
keyword.value = event.detail.value
|
}
|
|
async function handleSearch() {
|
addHistory(keyword.value)
|
await loadScenics()
|
}
|
|
async function clearKeyword() {
|
keyword.value = ''
|
await loadScenics('')
|
}
|
|
function clearHistory() {
|
saveHistory([])
|
}
|
|
async function useHistory(value) {
|
keyword.value = value
|
addHistory(value)
|
await loadScenics(value)
|
}
|
|
async function handleRelocate() {
|
uni.showLoading({ title: '定位中' })
|
const located = await updateCoordinates(true)
|
if (located) {
|
await loadScenics()
|
uni.showToast({
|
title: '已按距离重新排序',
|
icon: 'none'
|
})
|
}
|
uni.hideLoading()
|
}
|
|
function selectScenic(item) {
|
uni.setStorageSync(SCENIC_STORAGE_KEY, item.id)
|
uni.setStorageSync(SCENIC_DETAIL_STORAGE_KEY, item)
|
uni.setStorageSync(SCENIC_MANUAL_SELECTED_KEY, true)
|
addHistory(item.name)
|
uni.navigateBack()
|
}
|
|
function handleBack() {
|
uni.navigateBack()
|
}
|
</script>
|
|
<style lang="scss" scoped>
|
.scenic-page {
|
height: 100vh;
|
display: flex;
|
flex-direction: column;
|
background: #f4f6fa;
|
overflow: hidden;
|
}
|
|
.top-panel {
|
position: relative;
|
z-index: 10;
|
flex: 0 0 auto;
|
overflow: hidden;
|
border-radius: 0 0 32rpx 32rpx;
|
background: rgba(255, 255, 255, 0.98);
|
box-shadow: 0 6rpx 18rpx rgba(17, 24, 39, 0.05);
|
}
|
|
.nav {
|
position: relative;
|
width: 100%;
|
}
|
|
.nav__bar {
|
position: relative;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
padding: 0 28rpx;
|
}
|
|
.nav__back {
|
position: absolute;
|
left: 28rpx;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
width: 68rpx;
|
height: 68rpx;
|
border-radius: 50%;
|
}
|
|
.nav__back:active {
|
opacity: 0.68;
|
}
|
|
.nav__back-icon {
|
width: 34rpx;
|
height: 34rpx;
|
}
|
|
.nav__title {
|
color: #111827;
|
font-size: 32rpx;
|
font-weight: 700;
|
line-height: 1;
|
}
|
|
.nav__right-spacer {
|
position: absolute;
|
right: 28rpx;
|
height: 60rpx;
|
}
|
|
.search-panel {
|
flex: 1;
|
min-height: 0;
|
background: #f4f6fa;
|
box-sizing: border-box;
|
}
|
|
.search-row {
|
display: flex;
|
align-items: center;
|
gap: 20rpx;
|
padding: 8rpx 28rpx 28rpx;
|
background: transparent;
|
}
|
|
.search-input-wrap {
|
position: relative;
|
flex: 1;
|
min-width: 0;
|
height: 72rpx;
|
border: 2rpx solid #e4e9f2;
|
border-radius: 18rpx;
|
background: #f8fafc;
|
}
|
|
.search-input__icon {
|
position: absolute;
|
left: 22rpx;
|
top: 50%;
|
width: 34rpx;
|
height: 34rpx;
|
transform: translateY(-50%);
|
}
|
|
.search-input {
|
height: 72rpx;
|
padding: 0 64rpx 0 62rpx;
|
color: #111827;
|
font-size: 26rpx;
|
font-weight: 600;
|
line-height: 72rpx;
|
}
|
|
.search-input__placeholder {
|
color: #7b7f88;
|
}
|
|
.search-clear {
|
position: absolute;
|
top: 0;
|
right: 0;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
width: 64rpx;
|
height: 72rpx;
|
}
|
|
.search-clear__icon {
|
width: 34rpx;
|
height: 34rpx;
|
}
|
|
.search-button {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
flex: 0 0 auto;
|
width: 136rpx;
|
height: 72rpx;
|
border-radius: 36rpx;
|
background: #278bff;
|
color: #ffffff;
|
font-size: 26rpx;
|
font-weight: 800;
|
box-shadow: 0 8rpx 18rpx rgba(39, 139, 255, 0.16);
|
}
|
|
.default-content,
|
.result-content {
|
padding: 24rpx 28rpx 40rpx;
|
}
|
|
.history-section {
|
padding: 32rpx;
|
border-radius: 28rpx;
|
background: #ffffff;
|
box-shadow: 0 6rpx 18rpx rgba(17, 24, 39, 0.04);
|
}
|
|
.section-title-row {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
}
|
|
.section-title {
|
color: #111827;
|
font-size: 30rpx;
|
font-weight: 800;
|
line-height: 1;
|
}
|
|
.history-clear {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
width: 48rpx;
|
height: 48rpx;
|
border-radius: 50%;
|
}
|
|
.history-clear:active {
|
opacity: 0.68;
|
}
|
|
.history-clear__icon {
|
width: 32rpx;
|
height: 32rpx;
|
}
|
|
.history-tags {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 18rpx;
|
margin-top: 28rpx;
|
}
|
|
.history-tag {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
min-width: 112rpx;
|
height: 56rpx;
|
padding: 0 24rpx;
|
border-radius: 28rpx;
|
background: #eff6ff;
|
color: #278bff;
|
font-size: 24rpx;
|
font-weight: 800;
|
}
|
|
.divider {
|
height: 24rpx;
|
background: transparent;
|
}
|
|
.current-row {
|
display: flex;
|
align-items: center;
|
min-height: 96rpx;
|
padding: 0 32rpx;
|
border-radius: 28rpx;
|
background: #ffffff;
|
color: #111827;
|
font-size: 26rpx;
|
box-shadow: 0 6rpx 18rpx rgba(17, 24, 39, 0.04);
|
}
|
|
.current-label {
|
color: #111827;
|
}
|
|
.current-name {
|
color: #278bff;
|
font-weight: 800;
|
}
|
|
.relocate {
|
display: flex;
|
align-items: center;
|
gap: 14rpx;
|
margin-left: auto;
|
color: #278bff;
|
font-size: 24rpx;
|
font-weight: 800;
|
}
|
|
.relocate__icon {
|
width: 36rpx;
|
height: 36rpx;
|
}
|
|
.nearby-section {
|
padding: 32rpx;
|
border-radius: 28rpx;
|
background: #ffffff;
|
box-shadow: 0 6rpx 18rpx rgba(17, 24, 39, 0.04);
|
}
|
|
.nearby-section > .section-title {
|
display: block;
|
padding: 0 0 8rpx;
|
}
|
|
.scenic-list {
|
padding: 0;
|
}
|
|
.scenic-list--result {
|
padding: 8rpx 32rpx 0;
|
border-radius: 28rpx;
|
background: #ffffff;
|
box-shadow: 0 6rpx 18rpx rgba(17, 24, 39, 0.04);
|
}
|
|
.scenic-item {
|
display: flex;
|
align-items: center;
|
min-height: 154rpx;
|
border-bottom: 2rpx solid #edf1f6;
|
}
|
|
.scenic-item:last-child {
|
border-bottom: 0;
|
}
|
|
.scenic-item__main {
|
flex: 1;
|
min-width: 0;
|
}
|
|
.scenic-item__title-row {
|
display: flex;
|
align-items: center;
|
gap: 18rpx;
|
}
|
|
.scenic-item__name {
|
color: #111827;
|
font-size: 30rpx;
|
font-weight: 800;
|
line-height: 1;
|
}
|
|
.scenic-item__grade {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
min-width: 58rpx;
|
height: 34rpx;
|
border-radius: 17rpx;
|
background: #eff6ff;
|
color: #278bff;
|
font-size: 22rpx;
|
font-weight: 800;
|
line-height: 1;
|
}
|
|
.scenic-item__meta {
|
display: flex;
|
align-items: center;
|
gap: 34rpx;
|
margin-top: 24rpx;
|
color: #6b7280;
|
font-size: 24rpx;
|
line-height: 1;
|
}
|
|
.scenic-item__arrow {
|
width: 34rpx;
|
height: 34rpx;
|
margin-left: 24rpx;
|
flex: 0 0 auto;
|
}
|
|
.empty-state {
|
margin: 24rpx 32rpx 0;
|
padding: 96rpx 0;
|
border-radius: 28rpx;
|
background: #ffffff;
|
color: #8a93a2;
|
font-size: 26rpx;
|
text-align: center;
|
}
|
</style>
|