123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752 |
- <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="cancel" @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="{ 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 searchList" :key="index">
- <!-- 通过header插槽定义列表左侧图片 -->
- <template v-slot:header>
- <image class="thumbnail" :src="item.thumbnail" 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: searchList, 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";
- import {parseImageUrl} from "@/uni_modules/uni-cms-article/common/parse-image-url";
- 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,
- // 搜索结果列表
- searchList: []
- }
- },
- // 组件创建时执行
- 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;
- this.searchList = []
- this.isLoadData = true
- // 延迟0ms后加载数据
- setTimeout(() => {
- this.$refs.listUdb.loadData({
- clear: true
- })
- }, 0)
- },
- // 数据库加载完成
- async onDbLoad(data) {
- console.log('onDbLoad')
- // 设置数据已加载标志
- this.isLoadData = true
- // 显示联想词
- this.noAssociativeShow = false;
- for (const article of data) {
- const parseImages = await parseImageUrl(article.thumbnail)
- article.thumbnail = parseImages ? parseImages.map(image => image.src): []
- }
- this.searchList = data
- },
- // 查询错误
- 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>
|