Files
ctms-client/pages/uni-starter/news/search/search.nvue
fm453 c62d15b288 首次完整推送,
V:1.20240808.006
2024-08-13 18:32:37 +08:00

739 lines
19 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="container">
<view class="search-container">
<!-- 搜索框 -->
<view class="search-container-bar">
<!-- #ifdef APP-PLUS -->
<uni-icons class="search-icons" :color="iconColor" size="22" type="mic-filled" @click="speech" />
<!-- #endif -->
<!-- :cancelText="keyBoardPopup ? '取消' : '搜索'" -->
<uni-search-bar ref="searchBar" style="flex:1;" radius="100" v-model="searchText" :focus="focus"
:placeholder="hotWorld" clearButton="auto" cancelButton="none" @clear="clear" @confirm="confirm"
@cancel="cancel" />
<uni-icons class="scan-icons" :color="iconColor" size="22" type="scan" @click="scanEvent"></uni-icons>
</view>
</view>
<view class="search-body">
<unicloud-db ref='listUdb' v-slot:default="{ data, pagination, hasMore, loading, error, options }"
@error="onqueryerror" :collection="colList" :page-size="10" orderby="publish_date desc" @load="onDbLoad"
loadtime="manual">
<template v-if="!isLoadData">
<!-- 搜索历史 -->
<view class="word-container" v-if="localSearchList.length">
<view class="word-container_header">
<text class="word-container_header-text">搜索历史</text>
<uni-icons v-if="!localSearchListDel" @click="localSearchListDel = true" class="search-icons"
style="padding-right: 0;" :color="iconColor" size="18" type="trash"></uni-icons>
<view v-else class="flex-center flex-row"
style="font-weight: 500;justify-content: space-between;">
<text
style="font-size: 22rpx;color: #666;padding-top:4rpx;padding-bottom:4rpx;padding-right:20rpx;"
@click="LocalSearchListClear">全部删除</text>
<text
style="font-size: 22rpx;color: #c0402b;padding-top:4rpx;padding-bottom:4rpx;padding-left:20rpx;"
@click="localSearchListDel = false">完成</text>
</view>
</view>
<view class="word-container_body">
<view class="flex-center flex-row word-container_body-text"
v-for="(word, index) in localSearchList" :key="index"
@click="LocalSearchlistItemClick(word, index)">
<text class="word-display" :key="word">{{ word }}</text>
<uni-icons v-if="localSearchListDel" size="12" type="closeempty" />
</view>
</view>
</view>
<!-- 搜索发现 -->
<view class="word-container">
<view class="word-container_header">
<view class="flex-center flex-row">
<text class="word-container_header-text">搜索发现</text>
<uni-icons v-if="!netHotListIsHide" class="search-icons" :color="iconColor" size="14"
type="reload" @click="searchHotRefresh"></uni-icons>
</view>
<uni-icons class="search-icons" style="padding-right: 0;" :color="iconColor" size="18"
:type="netHotListIsHide ? 'eye-slash' : 'eye'"
@click="netHotListIsHide = !netHotListIsHide"></uni-icons>
</view>
<unicloud-db ref="udb" #default="{ data, loading, error, options }" field="content"
collection="opendb-search-hot" orderby="create_date desc,count desc" page-data="replace"
:page-size="10">
<text v-if="loading && !netHotListIsHide" class="word-container_body-info">正在加载...</text>
<view v-else class="word-container_body">
<template v-if="!netHotListIsHide">
<text v-if="error" class="word-container_body-info">{{ error.message }}</text>
<template v-else>
<text v-for="(word, index) in data" class="word-container_body-text" :key="index"
@click="search(word.content)">{{ word.content }}</text>
</template>
</template>
<view v-else style="flex:1;">
<text class="word-container_body-info">当前搜索发现已隐藏</text>
</view>
</view>
</unicloud-db>
</view>
</template>
<uni-list v-else class="uni-list" :border="false" :style="{ height: listHeight }">
<!-- 列表渲染 -->
<uni-list-item :to="'/uni_modules/uni-cms-article/pages/detail/detail?id=' + item._id"
v-for="(item, index) in data" :key="index">
<!-- 通过header插槽定义列表左侧图片 -->
<template v-slot:header>
<image class="thumbnail" :src="typeof item.thumbnail === 'string' ? item.thumbnail: item.thumbnail[0]" mode="aspectFill"></image>
</template>
<!-- 通过body插槽定义布局 -->
<template v-slot:body>
<view class="main">
<text class="title">{{ item.title }}</text>
<view class="info">
<text class="author">{{ item.user_id[0] ? item.user_id[0].nickname : '' }}</text>
<text class="publish_date">{{ publishTime(item.publish_date) }}</text>
<!-- -->
<!-- <uni-dateformat class="publish_date" :date="item.publish_date"-->
<!-- format="yyyy-MM-dd" :threshold="[60000, 2592000000]"/>-->
</view>
</view>
</template>
</uni-list-item>
<!-- 加载状态:上拉加载更多,加载中,没有更多数据了,加载错误 -->
<!-- #ifdef APP-PLUS -->
<uni-list-item>
<template v-slot:body>
<!-- #endif -->
<uni-load-state @networkResume="refresh" :state="{ data, pagination, hasMore, loading, error }"
@loadMore="loadMore">
</uni-load-state>
<!-- #ifdef APP-PLUS -->
</template>
</uni-list-item>
<!-- #endif -->
</uni-list>
</unicloud-db>
</view>
<!-- 搜索联想 -->
<view class="search-associative" v-if="associativeShow">
<uni-list>
<uni-list-item v-for="(item, index) in associativeList" :key="item._id" :ellipsis="1" :title="item.title"
@click="associativeClick(item)" show-extra-icon clickable
:extra-icon="{ size: 18, color: iconColor, type: 'search' }">
</uni-list-item>
</uni-list>
</view>
</view>
</template>
<script>
/**
* 云端一体搜索模板
* @description uniCloud云端一体搜索模板自带下拉候选、历史搜索、热搜。无需再开发服务器代码
*/
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
import parseScanResult from "@/uni_modules/uni-cms-article/common/parse-scan-result";
const searchLogDbName = 'opendb-search-log'; // 搜索记录数据库
const articleDbName = 'uni-cms-articles'; // 文章数据库
const associativeSearchField = 'title'; // 联想时,搜索框值检索数据库字段名
const associativeField = '_id,title'; // 联想列表每一项携带的字段
const localSearchListKey = '__local_search_history'; // 本地历史存储字段名
const db = uniCloud.database();
const articleDBName = 'uni-cms-articles'
const userDBName = 'uni-id-users'
// 数组去重
const arrUnique = arr => {
for (let i = arr.length - 1; i >= 0; i--) {
const curIndex = arr.indexOf(arr[i]);
const lastIndex = arr.lastIndexOf(arr[i])
curIndex != lastIndex && arr.splice(lastIndex, 1)
}
return arr
} // 节流
// 防抖
function debounce(fn, interval, isFirstAutoRun) {
/**
*
* @param {要执行的函数} fn
* @param {在操作多长时间后可再执行,第一次立即执行} interval
*/
var _self = fn;
var timer = null;
var first = true;
if (isFirstAutoRun) {
_self();
}
return function () {
var args = arguments;
var _me = this;
if (first) {
first = false;
_self.apply(_me, args);
}
if (timer) {
clearTimeout(timer)
// return false;
}
timer = setTimeout(function () {
clearTimeout(timer);
timer = null;
_self.apply(_me, args);
}, interval || 200);
}
}
export default {
// 组件数据
data() {
return {
// 文章数据库名称
articleDbName,
// 搜索记录数据库名称
searchLogDbName,
// 状态栏高度
statusBarHeight: '0px',
// 本地搜索列表
localSearchList: uni.getStorageSync(localSearchListKey),
// 是否删除本地搜索列表
localSearchListDel: false,
// 是否隐藏网络热搜列表
netHotListIsHide: false,
// 搜索文本
searchText: '',
// 图标颜色
iconColor: '#999999',
// 联想列表
associativeList: [],
// 是否弹出键盘
keyBoardPopup: false,
// 搜索热词
hotWorld: 'DCloud', // 搜索热词,如果没有输入即回车,则搜索热词,但是不会加入搜索记录
// 是否自动聚焦
focus: true,
// 语音识别引擎
speechEngine: 'iFly', // 语音识别引擎 iFly 讯飞 baidu 百度
// 是否正在加载数据
isLoadData: false,
// 数据库查询条件
where: '"article_status" == 1',
// 列表高度
listHeight: 0,
// 是否显示联想列表
associativeShow: false,
// 是否显示无联想列表
noAssociativeShow: false
}
},
// 组件创建时执行
created() {
// 初始化数据库
this.db = uniCloud.database();
this.searchLogDb = this.db.collection(this.searchLogDbName);
this.articleDbName = this.db.collection(this.articleDbName);
// #ifndef H5
// 监听键盘高度变化
uni.onKeyboardHeightChange((res) => {
this.keyBoardPopup = res.height !== 0;
})
// #endif
},
// 计算属性
computed: {
colList() {
// 返回文章和用户列表
return [
db.collection(articleDBName).where(this.where).field('thumbnail,title,publish_date,user_id').getTemp(),
db.collection(userDBName).field('_id,nickname').getTemp()
]
}
},
// 页面初次渲染完成时执行
onReady() {
// #ifdef APP-NVUE
/* 可用窗口高度 - 搜索框高 - 状态栏高 */
this.listHeight = uni.getSystemInfoSync().windowHeight + 'px';
// #endif
// #ifndef APP-NVUE
this.listHeight = 'auto'
// #endif
},
// 页面加载时执行
onLoad() {
//#ifdef APP-PLUS
// 获取状态栏高度
this.statusBarHeight = `${uni.getSystemInfoSync().statusBarHeight}px`;
//#endif
},
// 组件方法
methods: {
// 清空搜索框
clear(res) {
console.log("res: ", res);
},
// 确认搜索
confirm(res) {
// 键盘确认
this.search(res.value);
},
// 取消搜索
cancel(res) {
uni.hideKeyboard();
this.searchText = '';
this.isLoadData = false
this.associativeShow = false
// this.loadList();
},
// 执行搜索
search(value) {
if (!value && !this.hotWorld) {
return;
}
if (value) {
if (this.searchText !== value) {
this.searchText = value
}
this.localSearchListManage(value);
this.searchLogDbAdd(value)
} else if (this.hotWorld) {
this.searchText = this.hotWorld
}
uni.hideKeyboard();
this.loadList(this.searchText);
},
// 管理本地搜索列表
localSearchListManage(word) {
let list = uni.getStorageSync(localSearchListKey);
if (list.length) {
this.localSearchList.unshift(word);
arrUnique(this.localSearchList);
if (this.localSearchList.length > 10) {
this.localSearchList.pop();
}
} else {
this.localSearchList = [word];
}
uni.setStorageSync(localSearchListKey, this.localSearchList);
},
// 清空本地搜索列表
LocalSearchListClear() {
uni.showModal({
content: "确认清空搜索历史吗",
confirmText: "删除",
confirmColor: 'red',
cancelColor: '#808080',
success: res => {
if (res.confirm) {
this.localSearchListDel = false;
this.localSearchList = [];
uni.removeStorageSync(localSearchListKey)
}
}
});
},
// 点击本地搜索列表项
LocalSearchlistItemClick(word, index) {
if (this.localSearchListDel) {
this.localSearchList.splice(index, 1);
uni.setStorageSync(localSearchListKey, this.localSearchList);
if (!this.localSearchList.length) {
this.localSearchListDel = false;
}
return;
}
this.noAssociativeShow = true;
this.search(word);
},
// 刷新搜索热词
searchHotRefresh() {
this.$refs.udb.refresh();
},
// 语音搜索
speech() {
// #ifdef APP-PLUS
plus.speech.startRecognize({
engine: this.speechEngine,
punctuation: false, // 标点符号
timeout: 10000
}, word => {
word = word instanceof Array ? word[0] : word;
this.search(word)
}, err => {
console.error("语音识别错误: ", err);
});
// #endif
},
// 添加搜索记录
searchLogDbAdd(value) {
/*
在此处存搜索记录,如果登录则需要存 user_id若未登录则存device_id
*/
this.getDeviceId().then(device_id => {
this.searchLogDb.add({
// user_id: device_id,
device_id,
content: value,
create_date: Date.now()
})
})
},
// 获取设备ID
getDeviceId() {
return new Promise((resolve, reject) => {
// 从本地缓存中获取uni_id
const uniId = uni.getStorageSync('uni_id');
// 如果uni_id不存在则获取设备信息
if (!uniId) {
// #ifdef APP-PLUS
plus.device.getInfo({
success: (deviceInfo) => {
resolve(deviceInfo.uuid)
},
fail: () => {
// 如果获取设备信息失败,则返回一个随机字符串
resolve(uni.getSystemInfoSync().system + '_' + Math.random().toString(36).substr(2))
}
});
// #endif
// #ifndef APP-PLUS
// 如果不是APP-PLUS则返回一个随机字符串
resolve(uni.getSystemInfoSync().system + '_' + Math.random().toString(36).substr(2))
// #endif
} else {
// 如果uni_id存在则直接返回uni_id
resolve(uniId)
}
})
},
// 点击联想词
associativeClick(item) {
/**
* 注意:这里用户根据自己的业务需要,选择跳转的页面即可
*/
console.log("associativeClick: ", item, item.title);
// 隐藏联想词
this.noAssociativeShow = true;
// 将搜索框的文本设置为联想词的标题
this.searchText = item.title;
// 加载列表
this.loadList(item.title);
},
// 加载列表
loadList(text = '') {
// 设置查询条件
let where = '"article_status" == 1 '
if (text) {
this.where = where + `&& /${text}/.test(title)`;
} else {
this.where = where;
}
// 隐藏联想词
this.associativeList = [];
this.associativeShow = false;
// 延迟0ms后加载数据
setTimeout(() => {
this.$refs.listUdb.loadData({
clear: true
})
}, 0)
},
// 数据库加载完成
onDbLoad() {
console.log('onDbLoad')
// 设置数据已加载标志
this.isLoadData = true
// 显示联想词
this.noAssociativeShow = false;
},
// 查询错误
onqueryerror(e) {
console.error(e);
},
// 刷新
refresh() {
// 刷新数据
this.$refs.listUdb.loadData({
clear: true
}, () => {
// 停止下拉刷新
uni.stopPullDownRefresh()
// #ifdef APP-NVUE
// 隐藏刷新按钮
this.showRefresh = false
// #endif
})
},
// 加载更多
loadMore() {
// 加载更多数据
this.$refs.listUdb.loadMore()
},
// 格式化发布时间
publishTime(timestamp) {
return translatePublishTime(timestamp)
},
scanEvent () {
uni.scanCode({
onlyFromCamera: true,
scanType: ["qrCode"],
success: (e) => parseScanResult(e.result),
fail: (e) => {
console.error(e)
}
})
}
},
onReachBottom() {
// 当滚动到底部时,加载更多数据
this.loadMore()
},
watch: {
searchText: debounce(function (value, oldValue) {
// 当搜索框的文本发生变化时,执行以下操作
if (value === oldValue) return
if (this.noAssociativeShow) return
if (value) {
// 根据搜索框的文本,查询联想词
this.articleDbName.where({
[associativeSearchField]: new RegExp(value, 'gi'),
}).field(associativeField).get().then(res => {
// 将查询结果赋值给联想词列表,并显示联想词
this.associativeList = res.result.data;
this.associativeShow = true
})
} else {
// 如果搜索框的文本为空,则清空联想词列表
this.associativeList = [];
}
}, 100)
}
}
</script>
<style>
/* #ifndef APP-NVUE */
page {
height: 100%;
flex: 1;
}
/* #endif */
</style>
<style lang="scss" scoped>
$search-bar-height: 52px;
$word-container_header-height: 72rpx;
.status-bar {
background-color: #fff;
}
.container {
/* #ifndef APP-NVUE */
height: 100%;
/* #endif */
flex: 1;
background-color: #f7f7f7;
}
.search-body {
background-color: #fff;
border-bottom-right-radius: 10px;
border-bottom-left-radius: 10px;
}
@mixin uni-flex {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
}
@mixin words-display {
font-size: 26rpx;
color: #666;
}
.flex-center {
@include uni-flex;
justify-content: center;
align-items: center;
}
.flex-row {
@include uni-flex;
flex-direction: row;
}
/* #ifdef APP-PLUS */
/* #ifndef APP-NVUE || VUE3*/
::v-deep
/* #endif */
.uni-searchbar {
padding-left: 0;
}
/* #endif */
/* #ifndef APP-NVUE || VUE3*/
::v-deep
/* #endif */
.uni-searchbar__box {
border-width: 0;
}
/* #ifndef APP-NVUE || VUE3 */
::v-deep
/* #endif */
.uni-input-placeholder {
font-size: 28rpx;
}
.search-container {
height: $search-bar-height;
@include uni-flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
background-color: #fff;
@at-root {
#{&}-bar {
@include uni-flex;
flex-direction: row;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
}
}
}
.search-associative {
/* #ifndef APP-NVUE */
overflow-y: auto;
/* #endif */
position: absolute;
top: $search-bar-height;
left: 0;
right: 0;
bottom: 0;
background-color: #fff;
margin-top: 10rpx;
padding-left: 10rpx;
padding-right: 10rpx;
}
.search-icons, .scan-icons {
padding: 16rpx;
}
.scan-icons {
padding-left: 0;
}
.word-display {
@include words-display;
}
.word-container {
padding: 20rpx;
@at-root {
#{&}_header {
@include uni-flex;
height: $word-container_header-height;
line-height: $word-container_header-height;
flex-direction: row;
justify-content: space-between;
align-items: center;
@at-root {
#{&}-text {
color: #3e3e3e;
font-size: 30rpx;
font-weight: bold;
}
}
}
#{&}_body {
@include uni-flex;
flex-wrap: wrap;
flex-direction: row;
@at-root {
#{&}-text {
@include uni-flex;
@include words-display;
justify-content: center;
align-items: center;
background-color: #f6f6f6;
padding: 10rpx 20rpx;
margin: 20rpx 30rpx 0 0;
border-radius: 30rpx;
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
text-align: center;
}
#{&}-info {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
flex: 1;
text-align: center;
font-size: 26rpx;
color: #808080;
margin-top: 20rpx;
}
}
}
}
}
.thumbnail {
width: 240rpx;
height: 160rpx;
margin-right: 20rpx;
border-radius: 8rpx;
}
.main {
justify-content: space-between;
flex: 1;
}
.title {
font-size: 32rpx;
}
.info {
flex-direction: row;
justify-content: space-between;
}
.author,
.publish_date {
font-size: 28rpx;
color: #999999;
}
</style>