Initial project commit
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user