297 lines
5.6 KiB
Vue
297 lines
5.6 KiB
Vue
<script setup>
|
|
import { ref } from "vue";
|
|
import { onReachBottom } from "@dcloudio/uni-app";
|
|
import { searchSpots } from "@/api/search";
|
|
import { getTags } from "@/api/tag";
|
|
import { extractList } from "@/utils/request";
|
|
import SpotCard from "@/components/spot-card/spot-card.vue";
|
|
|
|
const keyword = ref("");
|
|
const hotTags = ref([]);
|
|
const results = ref([]);
|
|
const searched = ref(false);
|
|
const loading = ref(false);
|
|
const page = ref(1);
|
|
const pageSize = 10;
|
|
const hasMore = ref(true);
|
|
|
|
const fetchHotTags = async () => {
|
|
try {
|
|
const res = await getTags({ sort: "hot" });
|
|
hotTags.value = extractList(res);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
};
|
|
|
|
const doSearch = async (reset = true) => {
|
|
const q = keyword.value.trim();
|
|
if (!q) return;
|
|
if (loading.value) return;
|
|
if (!reset && !hasMore.value) return;
|
|
loading.value = true;
|
|
if (reset) {
|
|
page.value = 1;
|
|
hasMore.value = true;
|
|
results.value = [];
|
|
}
|
|
searched.value = true;
|
|
try {
|
|
const res = await searchSpots({
|
|
q,
|
|
page: page.value,
|
|
page_size: pageSize,
|
|
});
|
|
const list = extractList(res);
|
|
if (reset) {
|
|
results.value = list;
|
|
} else {
|
|
results.value.push(...list);
|
|
}
|
|
if (list.length < pageSize) {
|
|
hasMore.value = false;
|
|
} else {
|
|
page.value++;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const onTagTap = (tag) => {
|
|
keyword.value = tag.name;
|
|
doSearch();
|
|
};
|
|
|
|
const goDetail = (id) => {
|
|
uni.navigateTo({ url: `/pages/spot/detail?id=${id}` });
|
|
};
|
|
|
|
const loadMore = () => {
|
|
doSearch(false);
|
|
};
|
|
|
|
onReachBottom(() => {
|
|
if (searched.value) loadMore();
|
|
});
|
|
|
|
fetchHotTags();
|
|
</script>
|
|
|
|
<template>
|
|
<view class="search-page">
|
|
<view class="search-bar">
|
|
<view class="search-input-wrap">
|
|
<uni-icons type="search" size="16" color="#94a3b8" class="search-icon" />
|
|
<input
|
|
class="search-input"
|
|
v-model="keyword"
|
|
placeholder="搜索取景地"
|
|
confirm-type="search"
|
|
@confirm="doSearch()"
|
|
/>
|
|
</view>
|
|
<view class="search-btn" @tap="doSearch()">
|
|
<text class="search-btn-text">搜索</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="!searched" class="pre-search">
|
|
<view v-if="hotTags.length" class="hot-section">
|
|
<text class="hot-title">热门标签</text>
|
|
<view class="hot-tags">
|
|
<view
|
|
v-for="tag in hotTags"
|
|
:key="tag.id"
|
|
class="hot-tag"
|
|
@tap="onTagTap(tag)"
|
|
>
|
|
<text class="hot-tag-text">{{ tag.name }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="empty-hint">
|
|
<uni-icons type="search" size="40" color="#94a3b8" class="empty-hint-icon" />
|
|
<text class="empty-hint-text">输入关键词搜索取景地</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-else class="results">
|
|
<view v-if="results.length > 0" class="result-list">
|
|
<SpotCard
|
|
v-for="item in results"
|
|
:key="item.id"
|
|
:spot="item"
|
|
@click="goDetail(item.id)"
|
|
/>
|
|
<view v-if="hasMore" class="load-more" @tap="loadMore">
|
|
<text>{{ loading ? "加载中..." : "加载更多" }}</text>
|
|
</view>
|
|
<view v-else class="no-more">
|
|
<text>没有更多了</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-else-if="!loading" class="no-result">
|
|
<uni-icons type="info" size="40" color="#94a3b8" class="no-result-icon" />
|
|
<text class="no-result-text">没有找到相关取景地</text>
|
|
</view>
|
|
|
|
<view v-if="loading && results.length === 0" class="loading-tip">
|
|
<text>搜索中...</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.search-page {
|
|
min-height: 100vh;
|
|
background: #f5f6fa;
|
|
}
|
|
|
|
.search-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 16rpx 24rpx;
|
|
background: #ffffff;
|
|
}
|
|
|
|
.search-input-wrap {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
background: #f5f6fa;
|
|
border-radius: 36rpx;
|
|
padding: 0 24rpx;
|
|
height: 72rpx;
|
|
}
|
|
|
|
.search-icon {
|
|
font-size: 28rpx;
|
|
margin-right: 12rpx;
|
|
}
|
|
|
|
.search-input {
|
|
flex: 1;
|
|
font-size: 28rpx;
|
|
color: #1e293b;
|
|
}
|
|
|
|
.search-btn {
|
|
margin-left: 16rpx;
|
|
background: #6366f1;
|
|
border-radius: 36rpx;
|
|
padding: 0 32rpx;
|
|
height: 72rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.search-btn-text {
|
|
color: #ffffff;
|
|
font-size: 28rpx;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.pre-search {
|
|
padding: 32rpx;
|
|
}
|
|
|
|
.hot-section {
|
|
margin-bottom: 48rpx;
|
|
}
|
|
|
|
.hot-title {
|
|
font-size: 30rpx;
|
|
font-weight: 700;
|
|
color: #1e293b;
|
|
display: block;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.hot-tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 16rpx;
|
|
}
|
|
|
|
.hot-tag {
|
|
background: #ffffff;
|
|
padding: 14rpx 28rpx;
|
|
border-radius: 32rpx;
|
|
border: 2rpx solid #e2e8f0;
|
|
}
|
|
|
|
.hot-tag-text {
|
|
font-size: 26rpx;
|
|
color: #475569;
|
|
}
|
|
|
|
.empty-hint {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 80rpx 0;
|
|
}
|
|
|
|
.empty-hint-icon {
|
|
font-size: 80rpx;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.empty-hint-text {
|
|
font-size: 28rpx;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.results {
|
|
padding: 24rpx 32rpx;
|
|
}
|
|
|
|
.result-list {
|
|
padding-bottom: 32rpx;
|
|
}
|
|
|
|
.load-more {
|
|
text-align: center;
|
|
padding: 24rpx 0;
|
|
color: #6366f1;
|
|
font-size: 26rpx;
|
|
}
|
|
|
|
.no-more {
|
|
text-align: center;
|
|
padding: 24rpx 0;
|
|
color: #94a3b8;
|
|
font-size: 26rpx;
|
|
}
|
|
|
|
.no-result {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 120rpx 0;
|
|
}
|
|
|
|
.no-result-icon {
|
|
font-size: 80rpx;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.no-result-text {
|
|
font-size: 28rpx;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.loading-tip {
|
|
text-align: center;
|
|
padding: 60rpx 0;
|
|
color: #94a3b8;
|
|
font-size: 28rpx;
|
|
}
|
|
</style>
|