فهرست منبع

feat(search): 添加搜索框输入提示功能

- 新增搜索提示 API 接口 getSearchPromptAjax
- 搜索框输入时显示关键词提示列表,支持高亮匹配词
- 添加清空输入框按钮,优化历史记录和热门搜索点击逻辑为填充关键词
- 使用防抖处理输入请求,避免频繁调用接口
ylong 20 ساعت پیش
والد
کامیت
d7dd8bc2b6
2فایلهای تغییر یافته به همراه120 افزوده شده و 33 حذف شده
  1. 3 0
      api/modules/mall.js
  2. 117 33
      pages-sell/pages/search.vue

+ 3 - 0
api/modules/mall.js

@@ -17,6 +17,9 @@ export const useMallApi = (Vue, vm) => {
 		// 用户搜索 (结果列表)
 		getSearchKeywordAjax: (params) => vm.$u.get('/token/shop/user/searchKeyword', params),
 		
+		// 用户快捷搜索提示(搜索下拉框)
+		getSearchPromptAjax: (params) => vm.$u.get('/token/shop/user/searchPrompt', params),
+		
 		// 购物车列表
 		getShopCartListAjax: (params) => vm.$u.post('/token/shop/cart/list', params),
 		

+ 117 - 33
pages-sell/pages/search.vue

@@ -8,47 +8,61 @@
                 <image src="/pages-sell/static/search/icon-scan.png" class="search-icon-left" mode="aspectFit"></image>
                 <input class="search-input" v-model="keyword" placeholder="书名 / 作者 / ISBN" confirm-type="search"
                     @confirm="onSearch" :focus="true" />
+                <u-icon name="close-circle-fill" color="#c0c4cc" size="32" v-if="keyword" @click="keyword = ''"
+                    class="clear-icon"></u-icon>
                 <view class="search-btn" @click="onSearch">
                     <text>搜索</text>
                 </view>
             </view>
         </view>
 
-        <!-- History -->
-        <view class="section history-section" v-if="historyList.length">
-            <view class="section-header">
-                <text class="title">搜索历史</text>
-                <image src="/pages-sell/static/search/icon-delete.png" class="icon-delete" mode="aspectFit"
-                    @click="clearHistory"></image>
-            </view>
-            <view class="tags-list">
-                <view class="tag-item history-tag" v-for="(item, index) in historyList" :key="index"
-                    @click="doSearch(item)">
-                    {{ item }}
+        <!-- Search Prompt -->
+        <view class="prompt-list" v-if="keyword.trim()">
+            <view class="prompt-item" v-for="(item, index) in promptList" :key="index" @click="doSearch(item)">
+                <rich-text :nodes="highlightKeyword(item, keyword)" class="prompt-text"></rich-text>
+                <view class="arrow-wrap" @click.stop="fillKeyword(item)">
+                    <u-icon name="arrow-up" color="#ccc" size="28" style="transform: rotate(45deg);"></u-icon>
                 </view>
             </view>
         </view>
 
-        <!-- Hot Search -->
-        <view class="section hot-section">
-            <view class="section-header">
-                <view class="left">
-                    <image src="/pages-sell/static/search/icon-fire.png" class="icon-fire" mode="aspectFit"></image>
-                    <text class="title">热门搜索</text>
+        <view v-else>
+            <!-- History -->
+            <view class="section history-section" v-if="historyList.length">
+                <view class="section-header">
+                    <text class="title">搜索历史</text>
+                    <image src="/pages-sell/static/search/icon-delete.png" class="icon-delete" mode="aspectFit"
+                        @click="clearHistory"></image>
+                </view>
+                <view class="tags-list">
+                    <view class="tag-item history-tag" v-for="(item, index) in historyList" :key="index"
+                        @click="fillKeyword(item)">
+                        {{ item }}
+                    </view>
                 </view>
-                <image
-                    :src="hotVisible ? '/pages-sell/static/search/icon-view.png' : '/pages-sell/static/search/icon-view.png'"
-                    class="icon-view" mode="aspectFit" @click="toggleHot" :style="{ opacity: hotVisible ? 1 : 0.5 }">
-                </image>
             </view>
-            <view class="tags-list" v-if="hotVisible">
-                <view class="tag-item" v-for="(item, index) in hotList" :key="index" @click="doSearch(item.name)"
-                    :class="item.className">
-                    <image v-if="item.tag === 'NEW'" src="/pages-sell/static/search/icon-new.png" class="tag-icon-new"
-                        mode="heightFix"></image>
-                    <image v-if="item.tag === 'HOT'" src="/pages-sell/static/search/icon-fire.png" class="tag-icon-fire"
-                        mode="heightFix"></image>
-                    <text>{{ item.name }}</text>
+
+            <!-- Hot Search -->
+            <view class="section hot-section">
+                <view class="section-header">
+                    <view class="left">
+                        <image src="/pages-sell/static/search/icon-fire.png" class="icon-fire" mode="aspectFit"></image>
+                        <text class="title">热门搜索</text>
+                    </view>
+                    <image
+                        :src="hotVisible ? '/pages-sell/static/search/icon-view.png' : '/pages-sell/static/search/icon-view.png'"
+                        class="icon-view" mode="aspectFit" @click="toggleHot" :style="{ opacity: hotVisible ? 1 : 0.5 }">
+                    </image>
+                </view>
+                <view class="tags-list" v-if="hotVisible">
+                    <view class="tag-item" v-for="(item, index) in hotList" :key="index" @click="fillKeyword(item.name)"
+                        :class="item.className">
+                        <image v-if="item.tag === 'NEW'" src="/pages-sell/static/search/icon-new.png" class="tag-icon-new"
+                            mode="heightFix"></image>
+                        <image v-if="item.tag === 'HOT'" src="/pages-sell/static/search/icon-fire.png" class="tag-icon-fire"
+                            mode="heightFix"></image>
+                        <text>{{ item.name }}</text>
+                    </view>
                 </view>
             </view>
         </view>
@@ -68,6 +82,20 @@
                 historyList: [],
                 hotVisible: true,
                 hotList: [],
+                promptList: [],
+                inputTimer: null
+            }
+        },
+        watch: {
+            keyword(val) {
+                if (!val.trim()) {
+                    this.promptList = [];
+                    return;
+                }
+                if (this.inputTimer) clearTimeout(this.inputTimer);
+                this.inputTimer = setTimeout(() => {
+                    this.getPromptData(val.trim());
+                }, 300);
             }
         },
         onShow() {
@@ -75,6 +103,28 @@
             this.getHotData();
         },
         methods: {
+            getPromptData(keyword) {
+                this.$u.api.getSearchPromptAjax({ keyword }).then(res => {
+                    if (res.code == 200) {
+                        this.promptList = res.data || [];
+                    }
+                })
+            },
+            highlightKeyword(text, keyword) {
+                if (!text) return '';
+                // 如果后端返回了 <em> 标签,直接替换样式
+                if (text.includes('<em>')) {
+                    return text
+                        .replace(/<em>/g, '<span style="color: #37C148; font-style: normal;">')
+                        .replace(/<\/em>/g, '</span>');
+                }
+
+                if (!keyword) return text;
+                // escape keyword for regex
+                const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+                const reg = new RegExp(`(${escapedKeyword})`, 'gi');
+                return text.replace(reg, '<span style="color: #37C148; font-style: normal;">$1</span>');
+            },
             getHistoryData() {
                 this.$u.api.getSearchHistoryAjax().then(res => {
                     this.historyList = res.data || [];
@@ -93,17 +143,25 @@
                 this.doSearch(this.keyword);
             },
             doSearch(key) {
-                console.log('Search:', key);
-                this.keyword = key;
+                if (!key) return;
+                // 去除可能包含的 HTML 标签 (如后端返回的 <em>)
+                const cleanKey = key.replace(/<[^>]+>/g, '');
+                console.log('Search:', cleanKey);
+                this.keyword = cleanKey;
                 // Navigate to result page or show results
                 uni.showToast({
-                    title: '搜索: ' + key,
+                    title: '搜索: ' + cleanKey,
                     icon: 'none'
                 });
                 uni.navigateTo({
-                    url: '/pages-sell/pages/search-result?keyword=' + key
+                    url: '/pages-sell/pages/search-result?keyword=' + cleanKey
                 });
             },
+            fillKeyword(key) {
+                if (!key) return;
+                const cleanKey = key.replace(/<[^>]+>/g, '');
+                this.keyword = cleanKey;
+            },
             clearHistory() {
                 uni.showModal({
                     title: '提示',
@@ -162,6 +220,10 @@
             color: #333;
         }
 
+        .clear-icon {
+            margin: 0 10rpx;
+        }
+
         .search-btn {
             width: 120rpx;
             height: 60rpx;
@@ -249,4 +311,26 @@
             }
         }
     }
+
+    .prompt-list {
+        padding: 0 30rpx;
+
+        .prompt-item {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            padding: 30rpx 0;
+            border-bottom: 1rpx solid #F5F5F5;
+
+            .prompt-text {
+                font-size: 28rpx;
+                color: #333;
+                flex: 1;
+            }
+
+            .arrow-wrap {
+                padding: 10rpx;
+            }
+        }
+    }
 </style>