后台登录已经成功

This commit is contained in:
2025-08-31 21:09:33 +08:00
parent c658033023
commit 5b5d65e072
10 changed files with 3984 additions and 6 deletions

View File

@@ -0,0 +1,767 @@
<template>
<view class="search-page">
<!-- 搜索头部 -->
<view class="search-header">
<view class="search-input-container">
<uni-icons type="arrow-left" size="20" color="#333" @click="goBack"></uni-icons>
<view class="search-input">
<uni-icons type="search" size="16" color="#999"></uni-icons>
<input
type="text"
v-model="keyword"
placeholder="请输入搜索关键词"
placeholder-class="placeholder"
focus
@confirm="handleSearch"
@input="handleInput"
/>
<uni-icons
v-if="keyword"
type="clear"
size="16"
color="#999"
@click="clearKeyword"
></uni-icons>
</view>
<text class="search-btn" @click="handleSearch">搜索</text>
</view>
</view>
<!-- 搜索类型切换 -->
<view class="search-type-tabs">
<view
class="tab-item"
:class="{ active: activeTab === 'all' }"
@click="changeTab('all')"
>
<text>全部</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'travel' }"
@click="changeTab('travel')"
>
<text>旅行计划</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'animal' }"
@click="changeTab('animal')"
>
<text>动物认养</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'flower' }"
@click="changeTab('flower')"
>
<text>鲜花配送</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'user' }"
@click="changeTab('user')"
>
<text>用户</text>
</view>
</view>
<!-- 搜索历史 -->
<view class="search-history" v-if="!hasSearched && searchHistory.length > 0">
<view class="section-header">
<text class="section-title">搜索历史</text>
<uni-icons type="delete" size="16" color="#999" @click="clearHistory"></uni-icons>
</view>
<view class="history-list">
<view
class="history-item"
v-for="(item, index) in searchHistory"
:key="index"
@click="selectHistory(item)"
>
<text>{{ item }}</text>
</view>
</view>
</view>
<!-- 热门搜索 -->
<view class="hot-search" v-if="!hasSearched">
<view class="section-header">
<text class="section-title">热门搜索</text>
</view>
<view class="hot-list">
<view
class="hot-item"
v-for="(item, index) in hotKeywords"
:key="index"
@click="selectHotKeyword(item)"
>
<uni-icons type="fire" size="14" color="#ff6b35" v-if="index < 3"></uni-icons>
<text>{{ item }}</text>
</view>
</view>
</view>
<!-- 搜索结果 -->
<view class="search-results" v-if="hasSearched">
<!-- 搜索结果统计 -->
<view class="result-stats">
<text>找到 {{ totalCount }} 条相关结果</text>
</view>
<!-- 旅行计划结果 -->
<view class="result-section" v-if="activeTab === 'all' || activeTab === 'travel'">
<view class="section-header">
<text class="section-title">旅行计划</text>
<text class="more" @click="viewMore('travel')" v-if="travelResults.length > 0">更多</text>
</view>
<view class="result-list">
<view
class="result-item travel-item"
v-for="item in travelResults"
:key="item.id"
@click="navigateToTravelDetail(item.id)"
>
<image :src="item.coverImage" class="item-image" mode="aspectFill"></image>
<view class="item-info">
<text class="item-title">{{ item.title }}</text>
<text class="item-destination">{{ item.destination }}</text>
<view class="item-meta">
<text class="item-date">{{ item.startDate }} - {{ item.endDate }}</text>
<text class="item-price">¥{{ item.budget }}</text>
</view>
</view>
</view>
<view class="empty-tip" v-if="travelResults.length === 0">
<text>暂无相关旅行计划</text>
</view>
</view>
</view>
<!-- 动物认养结果 -->
<view class="result-section" v-if="activeTab === 'all' || activeTab === 'animal'">
<view class="section-header">
<text class="section-title">动物认养</text>
<text class="more" @click="viewMore('animal')" v-if="animalResults.length > 0">更多</text>
</view>
<view class="result-list">
<view
class="result-item animal-item"
v-for="item in animalResults"
:key="item.id"
@click="navigateToAnimalDetail(item.id)"
>
<image :src="item.image" class="item-image" mode="aspectFill"></image>
<view class="item-info">
<text class="item-title">{{ item.name }}</text>
<text class="item-species">{{ item.species }}</text>
<view class="item-meta">
<text class="item-location">{{ item.location }}</text>
<text class="item-price">¥{{ item.price }}</text>
</view>
</view>
</view>
<view class="empty-tip" v-if="animalResults.length === 0">
<text>暂无相关动物</text>
</view>
</view>
</view>
<!-- 鲜花配送结果 -->
<view class="result-section" v-if="activeTab === 'all' || activeTab === 'flower'">
<view class="section-header">
<text class="section-title">鲜花配送</text>
<text class="more" @click="viewMore('flower')" v-if="flowerResults.length > 0">更多</text>
</view>
<view class="result-list">
<view
class="result-item flower-item"
v-for="item in flowerResults"
:key="item.id"
@click="navigateToFlowerDetail(item.id)"
>
<image :src="item.image" class="item-image" mode="aspectFill"></image>
<view class="item-info">
<text class="item-title">{{ item.name }}</text>
<text class="item-description">{{ item.description }}</text>
<view class="item-meta">
<text class="item-category">{{ item.category }}</text>
<text class="item-price">¥{{ item.price }}</text>
</view>
</view>
</view>
<view class="empty-tip" v-if="flowerResults.length === 0">
<text>暂无相关鲜花</text>
</view>
</view>
</view>
<!-- 用户结果 -->
<view class="result-section" v-if="activeTab === 'all' || activeTab === 'user'">
<view class="section-header">
<text class="section-title">用户</text>
<text class="more" @click="viewMore('user')" v-if="userResults.length > 0">更多</text>
</view>
<view class="result-list">
<view
class="result-item user-item"
v-for="item in userResults"
:key="item.id"
@click="navigateToUserProfile(item.id)"
>
<image :src="item.avatar" class="user-avatar" mode="aspectFill"></image>
<view class="user-info">
<text class="user-name">{{ item.nickname }}</text>
<text class="user-bio">{{ item.bio || '暂无简介' }}</text>
</view>
<view class="follow-btn" @click.stop="handleFollow(item)">
<text>{{ item.isFollowing ? '已关注' : '关注' }}</text>
</view>
</view>
<view class="empty-tip" v-if="userResults.length === 0">
<text>暂无相关用户</text>
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="hasMore && loading">
<text>加载中...</text>
</view>
<view class="no-more" v-if="!hasMore && hasSearched">
<text>没有更多数据了</text>
</view>
</view>
<!-- 搜索建议 -->
<view class="search-suggestions" v-if="showSuggestions && suggestions.length > 0">
<view class="suggestion-list">
<view
class="suggestion-item"
v-for="(item, index) in suggestions"
:key="index"
@click="selectSuggestion(item)"
>
<uni-icons type="search" size="16" color="#999"></uni-icons>
<text>{{ item }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { searchService } from '../../api/services.js'
export default {
data() {
return {
keyword: '',
activeTab: 'all',
hasSearched: false,
showSuggestions: false,
loading: false,
hasMore: false,
currentPage: 1,
pageSize: 10,
totalCount: 0,
// 搜索结果
travelResults: [],
animalResults: [],
flowerResults: [],
userResults: [],
// 搜索建议
suggestions: [],
// 搜索历史
searchHistory: uni.getStorageSync('searchHistory') || [],
// 热门搜索
hotKeywords: [
'西藏旅行',
'小羊驼',
'玫瑰花束',
'云南大理',
'农场体验',
'生日鲜花',
'结伴旅行',
'动物认养'
]
}
},
onLoad(options) {
if (options.keyword) {
this.keyword = options.keyword
this.handleSearch()
}
if (options.type) {
this.activeTab = options.type
}
},
methods: {
async handleSearch() {
if (!this.keyword.trim()) {
uni.showToast({
title: '请输入搜索关键词',
icon: 'none'
})
return
}
// 保存搜索历史
this.saveSearchHistory(this.keyword)
this.hasSearched = true
this.showSuggestions = false
this.currentPage = 1
this.loading = true
try {
const params = {
keyword: this.keyword,
type: this.activeTab === 'all' ? undefined : this.activeTab,
page: this.currentPage,
pageSize: this.pageSize
}
const response = await searchService.search(params)
this.processSearchResults(response)
} catch (error) {
console.error('搜索失败:', error)
uni.showToast({
title: '搜索失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
async handleInput() {
if (!this.keyword.trim()) {
this.showSuggestions = false
return
}
// 防抖处理
clearTimeout(this.suggestionTimer)
this.suggestionTimer = setTimeout(async () => {
try {
const response = await searchService.getSuggestions(this.keyword)
this.suggestions = response
this.showSuggestions = true
} catch (error) {
console.error('获取搜索建议失败:', error)
}
}, 300)
},
processSearchResults(response) {
this.totalCount = response.total || 0
if (this.activeTab === 'all') {
this.travelResults = response.travel?.list || []
this.animalResults = response.animal?.list || []
this.flowerResults = response.flower?.list || []
this.userResults = response.user?.list || []
} else {
const results = response.list || []
switch (this.activeTab) {
case 'travel':
this.travelResults = results
break
case 'animal':
this.animalResults = results
break
case 'flower':
this.flowerResults = results
break
case 'user':
this.userResults = results
break
}
}
this.hasMore = response.total > this.currentPage * this.pageSize
},
saveSearchHistory(keyword) {
// 去重
const index = this.searchHistory.indexOf(keyword)
if (index !== -1) {
this.searchHistory.splice(index, 1)
}
// 添加到开头
this.searchHistory.unshift(keyword)
// 限制历史记录数量
if (this.searchHistory.length > 10) {
this.searchHistory = this.searchHistory.slice(0, 10)
}
// 保存到本地存储
uni.setStorageSync('searchHistory', this.searchHistory)
},
clearHistory() {
uni.showModal({
title: '提示',
content: '确定要清空搜索历史吗?',
success: (res) => {
if (res.confirm) {
this.searchHistory = []
uni.removeStorageSync('searchHistory')
}
}
})
},
selectHistory(keyword) {
this.keyword = keyword
this.handleSearch()
},
selectHotKeyword(keyword) {
this.keyword = keyword
this.handleSearch()
},
selectSuggestion(keyword) {
this.keyword = keyword
this.handleSearch()
},
clearKeyword() {
this.keyword = ''
this.showSuggestions = false
},
changeTab(tab) {
if (this.activeTab !== tab) {
this.activeTab = tab
if (this.hasSearched) {
this.handleSearch()
}
}
},
goBack() {
uni.navigateBack()
},
viewMore(type) {
uni.navigateTo({
url: `/pages/search/more?keyword=${this.keyword}&type=${type}`
})
},
navigateToTravelDetail(id) {
uni.navigateTo({
url: `/pages/travel/detail?id=${id}`
})
},
navigateToAnimalDetail(id) {
uni.navigateTo({
url: `/pages/animal/detail?id=${id}`
})
},
navigateToFlowerDetail(id) {
uni.navigateTo({
url: `/pages/flower/detail?id=${id}`
})
},
navigateToUserProfile(id) {
uni.navigateTo({
url: `/pages/user/profile?id=${id}`
})
},
handleFollow(user) {
// 关注/取消关注逻辑
console.log('关注用户:', user)
}
}
}
</script>
<style scoped>
.search-page {
background-color: #f8f9fa;
min-height: 100vh;
}
.search-header {
background-color: #fff;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #eee;
}
.search-input-container {
display: flex;
align-items: center;
gap: 20rpx;
}
.search-input {
flex: 1;
display: flex;
align-items: center;
height: 60rpx;
background-color: #f0f0f0;
border-radius: 30rpx;
padding: 0 20rpx;
}
.search-input input {
flex: 1;
font-size: 28rpx;
margin: 0 10rpx;
}
.placeholder {
font-size: 28rpx;
color: #999;
}
.search-btn {
font-size: 28rpx;
color: #007aff;
font-weight: bold;
}
.search-type-tabs {
display: flex;
background-color: #fff;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #eee;
}
.tab-item {
padding: 10rpx 25rpx;
margin-right: 20rpx;
border-radius: 20rpx;
background-color: #f8f9fa;
font-size: 26rpx;
color: #666;
}
.tab-item.active {
background-color: #007aff;
color: #fff;
}
.search-history,
.hot-search {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.section-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.history-list,
.hot-list {
display: flex;
flex-wrap: wrap;
gap: 15rpx;
}
.history-item,
.hot-item {
padding: 12rpx 20rpx;
background-color: #f8f9fa;
border-radius: 20rpx;
font-size: 26rpx;
color: #666;
}
.hot-item {
display: flex;
align-items: center;
}
.hot-item uni-icons {
margin-right: 8rpx;
}
.search-results {
padding: 20rpx;
}
.result-stats {
padding: 20rpx 0;
font-size: 26rpx;
color: #999;
}
.result-section {
background-color: #fff;
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.result-list {
margin-top: 15rpx;
}
.result-item {
display: flex;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.result-item:last-child {
border-bottom: none;
}
.item-image {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
margin-right: 20rpx;
}
.item-info {
flex: 1;
}
.item-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.item-destination,
.item-species,
.item-description,
.item-location,
.item-category {
font-size: 24rpx;
color: #666;
display: block;
margin-bottom: 8rpx;
}
.item-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-date,
.item-location,
.item-category {
font-size: 22rpx;
color: #999;
}
.item-price {
font-size: 26rpx;
color: #ff6b35;
font-weight: bold;
}
.user-item {
align-items: center;
}
.user-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 20rpx;
}
.user-info {
flex: 1;
}
.user-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 5rpx;
}
.user-bio {
font-size: 24rpx;
color: #999;
}
.follow-btn {
padding: 8rpx 20rpx;
border: 1rpx solid #007aff;
border-radius: 20rpx;
color: #007aff;
font-size: 24rpx;
}
.empty-tip {
text-align: center;
padding: 40rpx;
color: #999;
font-size: 26rpx;
}
.load-more,
.no-more {
text-align: center;
padding: 30rpx;
font-size: 26rpx;
color: #999;
}
.search-suggestions {
position: absolute;
top: 120rpx;
left: 0;
right: 0;
background-color: #fff;
border-radius: 0 0 16rpx 16rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
z-index: 1000;
}
.suggestion-list {
max-height: 400rpx;
overflow-y: auto;
}
.suggestion-item {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.suggestion-item:last-child {
border-bottom: none;
}
.suggestion-item uni-icons {
margin-right: 15rpx;
}
</style>