488 行
11 KiB
Vue
488 行
11 KiB
Vue
<template>
|
||
<view class="search fc-page">
|
||
<!-- 规范化搜索顶栏:精确避让系统状态栏与微信胶囊,杜绝刘海屏坍塌遮挡 -->
|
||
<view class="search__header">
|
||
<!-- 系统状态栏占位 -->
|
||
<view class="search__status" :style="{ height: statusBarHeight + 'px' }"></view>
|
||
|
||
<!-- 搜索内容条:右侧避让胶囊 -->
|
||
<view
|
||
class="search__bar"
|
||
:style="{
|
||
paddingRight: (capsulePadding ? capsulePadding + 'px' : '24rpx'),
|
||
}"
|
||
>
|
||
<view class="search__back" @click="onBack">
|
||
<text class="search__back-icon">‹</text>
|
||
</view>
|
||
<view class="search__box">
|
||
<image class="fc-icon fc-icon--sm" :src="iconUrl('search')" mode="aspectFit" />
|
||
<input
|
||
v-model="keyword"
|
||
class="search__input"
|
||
confirm-type="search"
|
||
:placeholder="placeholderText"
|
||
placeholder-class="search__ph"
|
||
@confirm="doSearch"
|
||
focus
|
||
/>
|
||
<view v-if="keyword" class="search__clear-btn" @click="onClearInput">
|
||
<text class="search__clear-txt">×</text>
|
||
</view>
|
||
</view>
|
||
<view class="search__btn" @click="doSearch">
|
||
<text class="search__btn-text">搜索</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 当前限定分类范围提示条(可一键切换回全站搜索) -->
|
||
<view v-if="categoryName" class="search__scope">
|
||
<view class="search__scope-badge">
|
||
<text class="search__scope-txt">当前分类:{{ categoryName }}</text>
|
||
<text class="search__scope-clear" @click="clearCategory">✕ 搜全站</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 历史记录与热门搜索 -->
|
||
<view v-if="!searched" class="search__block">
|
||
<view class="search__block-head">
|
||
<text class="search__block-title">搜索历史</text>
|
||
<text v-if="history.length" class="search__block-clear" @click="clearHistory">清空</text>
|
||
</view>
|
||
<view class="search__chips">
|
||
<text
|
||
v-for="(h, i) in history"
|
||
:key="i"
|
||
class="search__chip"
|
||
@click="pickKeyword(h)"
|
||
>
|
||
{{ h }}
|
||
</text>
|
||
<text v-if="!history.length" class="search__empty-tip">暂无搜索历史</text>
|
||
</view>
|
||
|
||
<view class="search__block-head search__block-head--mt">
|
||
<view class="search__hot-label">
|
||
<view class="search__hot-icon-badge">
|
||
<text class="search__hot-icon-txt">🔥</text>
|
||
</view>
|
||
<text class="search__block-title">热门搜索</text>
|
||
</view>
|
||
</view>
|
||
<view class="search__chips">
|
||
<text
|
||
v-for="(h, i) in hotKeywords"
|
||
:key="i"
|
||
class="search__chip search__chip--hot"
|
||
:class="{ 'search__chip--top': i < 3 }"
|
||
@click="pickKeyword(h)"
|
||
>
|
||
{{ h }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 搜索结果 -->
|
||
<view v-else class="search__result">
|
||
<view class="search__result-head">
|
||
<text class="search__result-summary">共找到 {{ total }} 条{{ searchScopeText }}「{{ lastKeyword }}」相关信息</text>
|
||
<text class="search__result-reset" @click="searched = false">重新搜索</text>
|
||
</view>
|
||
<fc-empty v-if="!list.length" text="没有找到相关信息,换个关键词试试" />
|
||
<view v-else class="search__list">
|
||
<fc-post-card v-for="p in list" :key="p.id" :post="p" @click="goDetail" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import FcPostCard from '@/components/fc-post-card/fc-post-card.vue'
|
||
import FcEmpty from '@/components/fc-empty/fc-empty.vue'
|
||
import homeApi from '@/common/api/home.js'
|
||
import storage from '@/common/utils/storage.js'
|
||
import config from '@/common/config/index.js'
|
||
import { iconUrl } from '@/common/store/ui.js'
|
||
import { getNavMetrics } from '@/common/utils/navbar.js'
|
||
|
||
export default {
|
||
components: { FcPostCard, FcEmpty },
|
||
data() {
|
||
return {
|
||
statusBarHeight: 0,
|
||
capsulePadding: 0,
|
||
categoryId: '',
|
||
categoryName: '',
|
||
lastScopeName: '',
|
||
keyword: '',
|
||
lastKeyword: '',
|
||
searched: false,
|
||
list: [],
|
||
total: 0,
|
||
history: [],
|
||
hotKeywords: [],
|
||
}
|
||
},
|
||
onLoad(query) {
|
||
if (query && query.categoryId) {
|
||
this.categoryId = String(query.categoryId)
|
||
this.categoryName = decodeURIComponent(query.categoryName || '')
|
||
}
|
||
this.initMetrics()
|
||
this.history = storage.get(config.storageKeys.history, []) || []
|
||
this.loadHot()
|
||
},
|
||
computed: {
|
||
/**
|
||
* 输入框动态占位文案(随当前分类自动联动)
|
||
*/
|
||
placeholderText() {
|
||
return this.categoryName ? ('在“' + this.categoryName + '”中搜索…') : '搜索招聘、租房、二手…'
|
||
},
|
||
/**
|
||
* 搜索结果范围文案
|
||
*/
|
||
searchScopeText() {
|
||
return this.lastScopeName ? ('【' + this.lastScopeName + '】') : ''
|
||
},
|
||
},
|
||
methods: {
|
||
/**
|
||
* 解除当前分类限制,恢复为全站搜索
|
||
*/
|
||
clearCategory() {
|
||
this.categoryId = ''
|
||
this.categoryName = ''
|
||
if (this.searched && this.lastKeyword) {
|
||
this.doSearch()
|
||
}
|
||
},
|
||
|
||
/** 暴露图标地址解析工具 */
|
||
iconUrl,
|
||
/**
|
||
* 获取设备状态栏高度与胶囊避让参数
|
||
*/
|
||
initMetrics() {
|
||
const m = getNavMetrics()
|
||
this.statusBarHeight = m.statusBarHeight
|
||
try {
|
||
if (typeof uni !== 'undefined' && uni.setNavigationBarColor) {
|
||
uni.setNavigationBarColor({ frontColor: '#000000', backgroundColor: '#eafbf1' })
|
||
}
|
||
} catch (e) {}
|
||
this.capsulePadding = m.capsulePadding
|
||
},
|
||
/**
|
||
* 返回上一页或首页
|
||
*/
|
||
onBack() {
|
||
const pages = (typeof getCurrentPages === 'function') ? getCurrentPages() : []
|
||
if (pages && pages.length > 1) {
|
||
uni.navigateBack()
|
||
} else {
|
||
// #ifdef H5
|
||
uni.redirectTo({
|
||
url: '/pages/index/index',
|
||
fail: () => uni.reLaunch({ url: '/pages/index/index' }),
|
||
})
|
||
// #endif
|
||
// #ifndef H5
|
||
uni.reLaunch({ url: '/pages/index/index' })
|
||
// #endif
|
||
}
|
||
},
|
||
/**
|
||
* 清空当前输入的关键词
|
||
*/
|
||
onClearInput() {
|
||
this.keyword = ''
|
||
},
|
||
/**
|
||
* 加载推荐热门搜索词
|
||
*/
|
||
async loadHot() {
|
||
try {
|
||
const res = await homeApi.hotKeywords()
|
||
if (Array.isArray(res) && res.length) {
|
||
this.hotKeywords = res
|
||
}
|
||
} catch (e) {}
|
||
},
|
||
/**
|
||
* 点击选择关键词并直接发起搜索
|
||
* @param {string} k 选中的关键词
|
||
*/
|
||
pickKeyword(k) {
|
||
this.keyword = k
|
||
this.doSearch()
|
||
},
|
||
/**
|
||
* 执行搜索
|
||
*/
|
||
async doSearch() {
|
||
const kw = (this.keyword || '').trim()
|
||
if (!kw) {
|
||
uni.showToast({ title: '请输入关键词', icon: 'none' })
|
||
return
|
||
}
|
||
this.lastKeyword = kw
|
||
this.saveHistory(kw)
|
||
this.lastScopeName = this.categoryName
|
||
try {
|
||
const params = { keyword: kw, page: 1, size: 20 }
|
||
if (this.categoryId) {
|
||
params.categoryId = this.categoryId
|
||
}
|
||
const res = await homeApi.feed(params)
|
||
this.list = res.list || []
|
||
this.total = res.total || 0
|
||
this.searched = true
|
||
} catch (e) {
|
||
uni.showToast({ title: '搜索失败', icon: 'none' })
|
||
}
|
||
},
|
||
/**
|
||
* 将关键词存入本地搜索历史
|
||
* @param {string} kw 搜索词
|
||
*/
|
||
saveHistory(kw) {
|
||
const list = (this.history || []).filter((x) => x !== kw)
|
||
list.unshift(kw)
|
||
this.history = list.slice(0, 10)
|
||
storage.set(config.storageKeys.history, this.history)
|
||
},
|
||
/**
|
||
* 清空搜索历史
|
||
*/
|
||
clearHistory() {
|
||
this.history = []
|
||
storage.remove(config.storageKeys.history)
|
||
uni.showToast({ title: '已清空', icon: 'none' })
|
||
},
|
||
/**
|
||
* 跳转到帖子详情
|
||
* @param {Object} p 点击的帖子项
|
||
*/
|
||
goDetail(p) {
|
||
uni.navigateTo({ url: `/pages/detail/detail?id=${p.id}` })
|
||
},
|
||
},
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.search {
|
||
min-height: 100vh;
|
||
background: #f8fafc;
|
||
}
|
||
.search__header {
|
||
background: $fc-gradient-hero;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 99;
|
||
}
|
||
.search__status {
|
||
width: 100%;
|
||
}
|
||
.search__bar {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12rpx 20rpx 14rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
.search__back {
|
||
width: 68rpx;
|
||
height: 68rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
margin-right: 8rpx;
|
||
border-radius: 50%;
|
||
transition: background-color 0.2s ease;
|
||
&:active {
|
||
background-color: rgba(0, 0, 0, 0.05);
|
||
}
|
||
}
|
||
.search__back-icon {
|
||
font-size: 52rpx;
|
||
line-height: 1;
|
||
color: #ffffff;
|
||
font-weight: 300;
|
||
margin-top: -6rpx;
|
||
}
|
||
.search__box {
|
||
flex: 1;
|
||
height: 72rpx;
|
||
border-radius: $fc-radius-pill;
|
||
background: #ffffff;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 24rpx;
|
||
min-width: 0;
|
||
border: 1rpx solid transparent;
|
||
transition: border-color 0.2s;
|
||
&:focus-within {
|
||
border-color: rgba(4, 135, 74, 0.3);
|
||
background: #ffffff;
|
||
}
|
||
}
|
||
.search__input {
|
||
flex: 1;
|
||
font-size: 28rpx;
|
||
margin-left: 12rpx;
|
||
color: $fc-text;
|
||
}
|
||
.search__ph {
|
||
color: $fc-text-placeholder;
|
||
}
|
||
.search__clear-btn {
|
||
width: 36rpx;
|
||
height: 36rpx;
|
||
border-radius: 50%;
|
||
background: rgba(0, 0, 0, 0.16);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
}
|
||
.search__clear-txt {
|
||
font-size: 24rpx;
|
||
color: #ffffff;
|
||
line-height: 1;
|
||
margin-top: -2rpx;
|
||
}
|
||
.search__btn {
|
||
margin-left: 16rpx;
|
||
padding: 10rpx 14rpx;
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
}
|
||
.search__btn-text {
|
||
font-size: 28rpx;
|
||
color: #ffffff;
|
||
font-weight: 700;
|
||
}
|
||
/* 当前限定分类胶囊条 */
|
||
.search__scope {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 24rpx 14rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
.search__scope-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
height: 48rpx;
|
||
padding: 0 16rpx 0 20rpx;
|
||
background: rgba(255, 255, 255, 0.22);
|
||
border: 1rpx solid rgba(255, 255, 255, 0.45);
|
||
border-radius: 999rpx;
|
||
gap: 12rpx;
|
||
}
|
||
.search__scope-txt {
|
||
font-size: 22rpx;
|
||
color: #ffffff;
|
||
font-weight: 600;
|
||
}
|
||
.search__scope-clear {
|
||
font-size: 20rpx;
|
||
color: #04301e;
|
||
background: #00e070;
|
||
padding: 2rpx 12rpx;
|
||
border-radius: 999rpx;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
line-height: 1.4;
|
||
transition: opacity 0.15s;
|
||
&:active {
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
.search__block {
|
||
padding: 28rpx 32rpx;
|
||
}
|
||
.search__block-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 20rpx;
|
||
&--mt {
|
||
margin-top: 36rpx;
|
||
}
|
||
}
|
||
.search__hot-label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
}
|
||
.search__hot-icon-badge {
|
||
font-size: 24rpx;
|
||
}
|
||
.search__block-title {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
color: $fc-text;
|
||
}
|
||
.search__block-clear {
|
||
font-size: 24rpx;
|
||
color: $fc-text-placeholder;
|
||
cursor: pointer;
|
||
}
|
||
.search__chips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
}
|
||
.search__chip {
|
||
padding: 12rpx 28rpx;
|
||
border-radius: 999rpx;
|
||
background: #ffffff;
|
||
border: 1rpx solid $fc-border;
|
||
font-size: 26rpx;
|
||
color: $fc-text-regular;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
&:active {
|
||
background: $fc-primary-lighter;
|
||
border-color: $fc-primary;
|
||
color: $fc-primary-text;
|
||
}
|
||
&--top {
|
||
color: $fc-primary-text;
|
||
font-weight: 600;
|
||
border-color: rgba(4, 135, 74, 0.3);
|
||
background: #f7fffb;
|
||
}
|
||
}
|
||
.search__empty-tip {
|
||
font-size: 24rpx;
|
||
color: $fc-text-placeholder;
|
||
padding: 12rpx 0;
|
||
}
|
||
.search__result-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 24rpx 32rpx 12rpx;
|
||
}
|
||
.search__result-summary {
|
||
font-size: 24rpx;
|
||
color: $fc-text-secondary;
|
||
}
|
||
.search__result-reset {
|
||
font-size: 24rpx;
|
||
color: $fc-primary-text;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
}
|
||
.search__list {
|
||
padding-top: 12rpx;
|
||
}
|
||
</style>
|