Files
CosScene/clients/pages/search/index.vue
T
2026-05-09 16:40:29 +08:00

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>