文件
fenleixinxi/uniapp/pages/search/index.vue

488 行
11 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<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>