| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702 |
- <template>
- <view class="container">
- <!-- 顶部搜索栏 -->
- <view class="search-header">
- <view class="search-input-wrapper">
- <text class="search-icon">🔍</text>
- <input
- class="search-input"
- v-model="searchKeyword"
- placeholder="西游记"
- :focus="isFocus"
- @input="handleInput"
- @confirm="handleSearch"
- @focus="handleFocus"
- @blur="handleBlur"
- />
- </view>
- <text class="cancel-btn" @click="handleCancel">取消</text>
- </view>
-
- <!-- 搜索内容区域 -->
- <scroll-view class="search-content" scroll-y v-if="!showSearchResults">
- <!-- 搜索历史 -->
- <view class="section" v-if="searchHistory.length > 0">
- <view class="section-header">
- <text class="section-title">搜索历史</text>
- <text class="clear-btn" @click="clearHistory">清空</text>
- </view>
- <view class="tag-list">
- <view
- class="tag-item"
- v-for="(item, index) in searchHistory"
- :key="index"
- @click="searchByHistory(item)"
- >
- <text class="tag-text">{{ item }}</text>
- </view>
- </view>
- </view>
-
- <!-- 热门搜索 -->
- <view class="section">
- <view class="section-header">
- <text class="section-title">热门搜索</text>
- </view>
- <view class="tag-list">
- <view
- class="tag-item"
- v-for="(item, index) in popularSearches"
- :key="index"
- @click="searchByTag(item)"
- >
- <text class="tag-text">{{ item }}</text>
- </view>
- </view>
- </view>
- </scroll-view>
-
- <!-- 搜索结果 -->
- <scroll-view
- class="search-results"
- scroll-y
- v-if="showSearchResults"
- @scrolltolower="loadMore"
- :lower-threshold="100"
- >
- <view class="results-header">
- <text class="results-title">搜索结果</text>
- <text class="results-count" v-if="searchResults.length > 0">找到 {{ searchResults.length }} 本相关书籍</text>
- </view>
-
- <!-- 加载中 -->
- <view class="loading-container" v-if="isLoading && searchResults.length === 0">
- <text class="loading-text">搜索中...</text>
- </view>
-
- <!-- 搜索结果列表 -->
- <view class="book-list" v-else-if="searchResults.length > 0">
- <view
- class="book-item"
- v-for="(book, index) in searchResults"
- :key="book.id || index"
- @click="goToBookDetail(book)"
- >
- <image
- class="book-cover"
- :src="book.cover || book.image"
- mode="aspectFill"
- :lazy-load="true"
- @error="handleImageError(index)"
- ></image>
- <view class="book-info">
- <text class="book-title">{{ book.title }}</text>
- <text class="book-desc" v-if="book.desc">{{ book.desc }}</text>
- <text class="book-author" v-if="book.author">{{ book.author }}</text>
- </view>
- </view>
- </view>
-
- <!-- 无结果提示 -->
- <view class="no-results" v-else-if="!isLoading">
- <text class="no-results-text">暂无搜索结果</text>
- <text class="no-results-hint">试试其他关键词吧</text>
- </view>
-
- <!-- 加载更多提示 -->
- <view class="load-more" v-if="isLoading && searchResults.length > 0">
- <text class="load-more-text">加载中...</text>
- </view>
- <view class="load-more" v-else-if="!hasMore && searchResults.length > 0">
- <text class="load-more-text">没有更多了</text>
- </view>
- </scroll-view>
- </view>
- </template>
- <script>
- import { searchBooks, getPopularKeywords, searchAudiobooks, recordSearchHistory, getSearchHistory, clearSearchHistory } from '../../utils/api.js'
-
- export default {
- data() {
- return {
- searchKeyword: '',
- isFocus: false,
- showSearchResults: false,
- searchHistory: [],
- popularSearches: [],
- searchResults: [],
- isLoading: false,
- currentPage: 1,
- pageSize: 20,
- hasMore: true,
- mode: 'book', // 'book' or 'audio'
- userInfo: null
- }
- },
- onLoad(options) {
- // 模式:书籍 or 听书
- if (options.mode === 'audio') {
- this.mode = 'audio'
- }
-
- // 获取用户信息
- try {
- const userInfo = uni.getStorageSync('userInfo')
- if (userInfo && userInfo.id) {
- this.userInfo = userInfo
- }
- } catch (e) {
- console.error('获取用户信息失败', e)
- }
-
- // 从数据库加载搜索历史
- this.loadSearchHistory();
-
- // 加载热门搜索
- this.loadPopularSearches();
-
- // 如果有传入的关键词,直接搜索
- if (options.keyword) {
- this.searchKeyword = decodeURIComponent(options.keyword);
- this.performSearch(this.searchKeyword);
- }
- },
- methods: {
- handleInput(e) {
- this.searchKeyword = e.detail.value;
- },
- handleFocus() {
- this.isFocus = true;
- },
- handleBlur() {
- this.isFocus = false;
- },
- handleSearch() {
- if (this.searchKeyword.trim()) {
- this.performSearch(this.searchKeyword.trim());
- }
- },
- handleCancel() {
- if (this.showSearchResults) {
- // 如果在搜索结果页面,返回搜索首页
- this.showSearchResults = false;
- this.searchKeyword = '';
- this.searchResults = [];
- } else {
- // 否则返回上一页
- uni.navigateBack({
- delta: 1
- });
- }
- },
- searchByHistory(keyword) {
- this.searchKeyword = keyword;
- this.performSearch(keyword);
- },
- searchByTag(keyword) {
- this.searchKeyword = keyword;
- this.performSearch(keyword);
- },
- performSearch(keyword) {
- if (!keyword || !keyword.trim()) {
- return;
- }
-
- // 保存搜索历史
- this.saveToHistory(keyword);
-
- // 显示搜索结果
- this.showSearchResults = true;
-
- // 重置分页
- this.currentPage = 1;
- this.hasMore = true;
-
- // 调用搜索API
- this.searchDispatcher(keyword, 1);
- },
- async loadPopularSearches() {
- try {
- const res = await getPopularKeywords(10);
- if (res && res.code === 200 && res.data && Array.isArray(res.data)) {
- this.popularSearches = res.data;
- }
- } catch (e) {
- console.error('加载热门搜索失败:', e);
- // 如果加载失败,使用默认值
- this.popularSearches = ['西游记', '三体', '大侦探', '窗边的小豆豆'];
- }
- },
- async searchDispatcher(keyword, page = 1) {
- if (this.mode === 'audio') {
- await this.searchAudio(keyword, page)
- } else {
- await this.searchBook(keyword, page)
- }
- },
- async searchBook(keyword, page = 1) {
- if (!keyword || !keyword.trim()) {
- this.searchResults = [];
- return;
- }
-
- try {
- this.isLoading = true;
- const res = await searchBooks(keyword.trim(), page, this.pageSize);
-
- if (res && res.code === 200 && res.data) {
- const pageResult = res.data;
- const books = pageResult.list || pageResult.data || [];
-
- // 处理书籍数据
- const processed = books.map(b => ({
- id: b.id,
- title: b.title || '',
- author: b.author || '未知作者',
- desc: b.desc || b.brief || b.introduction || '',
- cover: b.cover || b.image || '',
- image: b.image || b.cover || ''
- }))
- if (page === 1) this.searchResults = processed; else this.searchResults = this.searchResults.concat(processed)
- const total = pageResult.total || 0;
- this.hasMore = this.searchResults.length < total;
- this.currentPage = page;
- } else {
- if (page === 1) this.searchResults = []
- uni.showToast({ title: res.message || '搜索失败', icon: 'none' })
- }
- } catch (e) {
- console.error('搜索书籍失败:', e);
- if (page === 1) this.searchResults = []
- uni.showToast({ title: e.message || '搜索失败,请重试', icon: 'none' })
- } finally {
- this.isLoading = false;
- }
- },
- async searchAudio(keyword, page = 1) {
- if (!keyword || !keyword.trim()) {
- this.searchResults = [];
- return;
- }
- try {
- this.isLoading = true
- const res = await searchAudiobooks({ keyword: keyword.trim(), page, size: this.pageSize })
- if (res && res.code === 200 && res.data) {
- const pageResult = res.data
- const list = pageResult.list || []
- const processed = list.map(a => ({
- id: a.id,
- title: a.title || '',
- author: a.narrator || a.author || '未知主播',
- desc: a.brief || a.desc || '',
- cover: a.image || a.cover || '',
- image: a.image || a.cover || '',
- isAudio: true
- }))
- if (page === 1) this.searchResults = processed; else this.searchResults = this.searchResults.concat(processed)
- const total = pageResult.total || 0
- this.hasMore = this.searchResults.length < total
- this.currentPage = page
- } else {
- if (page === 1) this.searchResults = []
- uni.showToast({ title: res.message || '搜索失败', icon: 'none' })
- }
- } catch (e) {
- console.error('搜索听书失败:', e)
- if (page === 1) this.searchResults = []
- uni.showToast({ title: e.message || '搜索失败,请重试', icon: 'none' })
- } finally {
- this.isLoading = false
- }
- },
- async saveToHistory(keyword) {
- if (!this.userInfo || !this.userInfo.id) {
- // 如果未登录,使用本地存储作为备用
- const index = this.searchHistory.findIndex(item =>
- (typeof item === 'string' ? item : item.keyword) === 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);
- }
- try {
- uni.setStorageSync('searchHistory', this.searchHistory);
- } catch (e) {
- console.error('保存搜索历史失败', e);
- }
- return;
- }
-
- // 保存到数据库
- try {
- await recordSearchHistory(this.userInfo.id, keyword);
- // 重新加载搜索历史
- await this.loadSearchHistory();
- } catch (e) {
- console.error('保存搜索历史失败', e);
- }
- },
- async loadSearchHistory() {
- if (!this.userInfo || !this.userInfo.id) {
- // 如果未登录,从本地存储加载
- try {
- const history = uni.getStorageSync('searchHistory');
- if (history && Array.isArray(history)) {
- this.searchHistory = history.map(item =>
- typeof item === 'string' ? item : item.keyword
- );
- }
- } catch (e) {
- console.error('加载搜索历史失败', e);
- }
- return;
- }
-
- // 从数据库加载
- try {
- const res = await getSearchHistory(this.userInfo.id, 10);
- if (res && res.code === 200 && res.data) {
- this.searchHistory = res.data.map(item => item.keyword);
- }
- } catch (e) {
- console.error('加载搜索历史失败', e);
- // 如果加载失败,尝试从本地存储加载
- try {
- const history = uni.getStorageSync('searchHistory');
- if (history && Array.isArray(history)) {
- this.searchHistory = history.map(item =>
- typeof item === 'string' ? item : item.keyword
- );
- }
- } catch (err) {
- console.error('从本地存储加载搜索历史失败', err);
- }
- }
- },
- async clearHistory() {
- uni.showModal({
- title: '提示',
- content: '确定要清空搜索历史吗?',
- success: async (res) => {
- if (res.confirm) {
- if (this.userInfo && this.userInfo.id) {
- // 清空数据库中的搜索历史
- try {
- uni.showLoading({
- title: '清空中...',
- mask: true
- });
-
- const result = await clearSearchHistory(this.userInfo.id);
- uni.hideLoading();
-
- if (result && result.code === 200) {
- this.searchHistory = [];
- uni.showToast({
- title: '已清空',
- icon: 'success'
- });
- } else {
- uni.showToast({
- title: result.message || '清空失败',
- icon: 'none'
- });
- }
- } catch (e) {
- uni.hideLoading();
- console.error('清空搜索历史失败', e);
- uni.showToast({
- title: '清空失败,请重试',
- icon: 'none'
- });
- }
- } else {
- // 清空本地存储
- this.searchHistory = [];
- try {
- uni.removeStorageSync('searchHistory');
- uni.showToast({
- title: '已清空',
- icon: 'success'
- });
- } catch (e) {
- console.error('清空搜索历史失败', e);
- }
- }
- }
- }
- });
- },
- loadMore() {
- // 加载更多搜索结果
- if (!this.isLoading && this.hasMore && this.searchKeyword.trim()) {
- this.searchDispatcher(this.searchKeyword.trim(), this.currentPage + 1);
- }
- },
- goToBookDetail(book) {
- if (!book || !book.id) {
- uni.showToast({ title: '信息不完整', icon: 'none' })
- return
- }
- if (this.mode === 'audio' || book.isAudio) {
- uni.navigateTo({ url: `/pages/listen-detail/listen-detail?audiobookId=${book.id}` })
- } else {
- uni.navigateTo({ url: `/pages/book-detail/book-detail?bookId=${book.id}` })
- }
- },
- handleImageError(index) {
- // 图片加载失败时使用备用图片
- if (this.searchResults[index]) {
- this.searchResults[index].cover = 'https://via.placeholder.com/200x300?text=No+Image';
- this.searchResults[index].image = 'https://via.placeholder.com/200x300?text=No+Image';
- }
- }
- }
- }
- </script>
- <style scoped>
- .container {
- width: 100%;
- height: 100vh;
- background-color: #FFFFFF;
- display: flex;
- flex-direction: column;
- padding-top: 50px;
- box-sizing: border-box;
- }
-
- /* 顶部搜索栏 */
- .search-header {
- display: flex;
- align-items: center;
- padding: 20rpx 30rpx;
- background-color: #FFFFFF;
- border-bottom: 1rpx solid #E5E5E5;
- }
-
- .search-input-wrapper {
- flex: 1;
- display: flex;
- align-items: center;
- background-color: #F5F5F5;
- border-radius: 40rpx;
- padding: 0 24rpx;
- height: 70rpx;
- margin-right: 20rpx;
- }
-
- .search-icon {
- font-size: 32rpx;
- color: #999999;
- margin-right: 12rpx;
- }
-
- .search-input {
- flex: 1;
- font-size: 28rpx;
- color: #333333;
- height: 70rpx;
- line-height: 70rpx;
- }
-
- .cancel-btn {
- font-size: 28rpx;
- color: #666666;
- }
-
- /* 搜索内容区域 */
- .search-content {
- flex: 1;
- width: 100%;
- height: 0;
- overflow: hidden;
- padding: 30rpx;
- background-color: #FFFFFF;
- }
-
- /* 搜索结果区域 */
- .search-results {
- flex: 1;
- width: 100%;
- height: 0;
- overflow: hidden;
- background-color: #FFFFFF;
- }
-
- /* 区块样式 */
- .section {
- margin-bottom: 50rpx;
- }
-
- .section-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 24rpx;
- }
-
- .section-title {
- font-size: 32rpx;
- font-weight: bold;
- color: #333333;
- }
-
- .clear-btn {
- font-size: 28rpx;
- color: #4FC3F7;
- }
-
- /* 标签列表 */
- .tag-list {
- display: flex;
- flex-wrap: wrap;
- gap: 20rpx;
- }
-
- .tag-item {
- padding: 16rpx 28rpx;
- background-color: #F5F5F5;
- border-radius: 40rpx;
- }
-
- .tag-text {
- font-size: 28rpx;
- color: #666666;
- }
-
- /* 搜索结果头部 */
- .results-header {
- padding: 30rpx;
- border-bottom: 1rpx solid #E5E5E5;
- }
-
- .results-title {
- font-size: 32rpx;
- font-weight: bold;
- color: #333333;
- margin-bottom: 8rpx;
- display: block;
- }
-
- .results-count {
- font-size: 24rpx;
- color: #999999;
- display: block;
- }
-
- /* 书籍列表 */
- .book-list {
- padding: 0 30rpx;
- }
-
- .book-item {
- display: flex;
- padding: 30rpx 0;
- border-bottom: 1rpx solid #F0F0F0;
- }
-
- .book-item:last-child {
- border-bottom: none;
- }
-
- /* 书籍封面 */
- .book-cover {
- width: 160rpx;
- height: 220rpx;
- border-radius: 8rpx;
- margin-right: 24rpx;
- flex-shrink: 0;
- background-color: #F5F5F5;
- }
-
- /* 书籍信息 */
- .book-info {
- flex: 1;
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- min-width: 0;
- }
-
- .book-title {
- font-size: 32rpx;
- font-weight: bold;
- color: #000000;
- margin-bottom: 16rpx;
- line-height: 1.4;
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 2;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .book-desc {
- font-size: 28rpx;
- color: #666666;
- line-height: 1.6;
- margin-bottom: 20rpx;
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 3;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .book-author {
- font-size: 26rpx;
- color: #999999;
- margin-top: auto;
- }
-
- /* 加载中 */
- .loading-container {
- display: flex;
- justify-content: center;
- align-items: center;
- padding: 100rpx 30rpx;
- }
-
- .loading-text {
- font-size: 28rpx;
- color: #999999;
- }
-
- /* 加载更多 */
- .load-more {
- display: flex;
- justify-content: center;
- align-items: center;
- padding: 30rpx;
- }
-
- .load-more-text {
- font-size: 26rpx;
- color: #999999;
- }
-
- /* 无结果提示 */
- .no-results {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 100rpx 30rpx;
- }
-
- .no-results-text {
- font-size: 32rpx;
- color: #999999;
- margin-bottom: 16rpx;
- }
-
- .no-results-hint {
- font-size: 28rpx;
- color: #CCCCCC;
- }
- </style>
|