hookehuyr

feat(material): 优化搜索功能体验

...@@ -27,7 +27,7 @@ declare module 'vue' { ...@@ -27,7 +27,7 @@ declare module 'vue' {
27 OfficeViewer: typeof import('./src/components/OfficeViewer.vue')['default'] 27 OfficeViewer: typeof import('./src/components/OfficeViewer.vue')['default']
28 PdfPreview: typeof import('./src/components/PdfPreview.vue')['default'] 28 PdfPreview: typeof import('./src/components/PdfPreview.vue')['default']
29 Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] 29 Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
30 - PlanPopup: typeof import('./src/components/PlanPopup/index.vue')['default'] 30 + PlanPopup: typeof import('./src/components/PlanSchemes/PlanPopup.vue')['default']
31 PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] 31 PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
32 QrCode: typeof import('./src/components/qrCode.vue')['default'] 32 QrCode: typeof import('./src/components/qrCode.vue')['default']
33 QrCodeSearch: typeof import('./src/components/qrCodeSearch.vue')['default'] 33 QrCodeSearch: typeof import('./src/components/qrCodeSearch.vue')['default']
......
1 # Changelog 1 # Changelog
2 2
3 -> 本文档记录 Manulife WeApp 项目的所有重要变更。 3 +> 本文档记录 Manulife WeApp项目的所有重要变更。
4 > 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 4 > 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
5 5
6 --- 6 ---
7 7
8 +## [2026-02-05] - 优化搜索功能体验
9 +
10 +### 新增
11 +- 帮助中心页面实现实时搜索功能(模糊匹配问题标题)
12 +- 新增防抖工具函数 (src/utils/debounce.js)
13 +- 资料列表页面改造为实时搜索(带 500ms 防抖)
14 +
15 +### 优化
16 +- SearchBar 组件修复 blur 事件传递参数
17 +- 移除资料列表页面冗余的 blur 事件监听
18 +- 页面空状态 UI 优化(category-list, message)
19 +
20 +### 技术实现
21 +- help-center: 使用 v-model + computed 实现纯前端过滤
22 +- material-list: 使用 watch + debounce 实现防抖接口调用
23 +- SearchBar: blur 事件现在传递当前输入值
24 +
25 +---
26 +
27 +**详细信息**
28 +- **影响文件**:
29 + - src/components/SearchBar.vue
30 + - src/utils/debounce.js (新增)
31 + - src/pages/help-center/index.vue
32 + - src/pages/material-list/index.vue
33 + - src/pages/category-list/index.vue
34 + - src/pages/knowledge-base/index.vue
35 + - src/pages/message/index.vue
36 +- **技术栈**: Vue 3, Taro 4, Watch API
37 +- **测试状态**: 已通过本地测试
38 +- **备注**: 所有搜索功能已统一为实时搜索模式
39 +
40 +---
41 +
8 ## [2026-02-05] - 修复知识库搜索栏高度变化 42 ## [2026-02-05] - 修复知识库搜索栏高度变化
9 43
10 ### 修复 44 ### 修复
......
...@@ -168,8 +168,9 @@ const emit = defineEmits({ ...@@ -168,8 +168,9 @@ const emit = defineEmits({
168 /** 168 /**
169 * 失去焦点 169 * 失去焦点
170 * @event blur 170 * @event blur
171 + * @param {string} value - 当前输入值
171 */ 172 */
172 - blur: () => true, 173 + blur: (value) => true,
173 /** 174 /**
174 * 清除 175 * 清除
175 * @event clear 176 * @event clear
...@@ -219,7 +220,7 @@ function handleFocus() { ...@@ -219,7 +220,7 @@ function handleFocus() {
219 */ 220 */
220 function handleBlur() { 221 function handleBlur() {
221 console.log('[SearchBar Component] 失去焦点') 222 console.log('[SearchBar Component] 失去焦点')
222 - emit('blur') 223 + emit('blur', internalValue.value)
223 } 224 }
224 225
225 /** 226 /**
......
...@@ -21,7 +21,8 @@ ...@@ -21,7 +21,8 @@
21 21
22 <!-- Empty State --> 22 <!-- Empty State -->
23 <div v-else class="px-[40rpx] mt-[80rpx] flex items-center justify-center"> 23 <div v-else class="px-[40rpx] mt-[80rpx] flex items-center justify-center">
24 - <text class="text-[#9CA3AF] text-[28rpx]">暂无分类</text> 24 + <!-- <text class="text-[#9CA3AF] text-[28rpx]">暂无分类</text> -->
25 + <nut-empty description="暂无分类" image="empty" />
25 </div> 26 </div>
26 </div> 27 </div>
27 </template> 28 </template>
......
...@@ -7,8 +7,10 @@ ...@@ -7,8 +7,10 @@
7 <!-- Search Bar --> 7 <!-- Search Bar -->
8 <view class="mb-[32rpx]"> 8 <view class="mb-[32rpx]">
9 <SearchBar 9 <SearchBar
10 + v-model="searchKeyword"
10 placeholder="搜索问题或关键词" 11 placeholder="搜索问题或关键词"
11 variant="rounded" 12 variant="rounded"
13 + :show-clear="true"
12 /> 14 />
13 </view> 15 </view>
14 16
...@@ -32,14 +34,19 @@ ...@@ -32,14 +34,19 @@
32 <view> 34 <view>
33 <text class="block text-[32rpx] text-gray-900 font-bold mb-[24rpx]">重点问题</text> 35 <text class="block text-[32rpx] text-gray-900 font-bold mb-[24rpx]">重点问题</text>
34 <view class="bg-white rounded-[24rpx] shadow-sm overflow-hidden"> 36 <view class="bg-white rounded-[24rpx] shadow-sm overflow-hidden">
35 - <view v-for="(item, index) in questions" :key="index" 37 + <view v-if="filteredQuestions.length > 0">
36 - class="flex items-center justify-between p-[32rpx] border-b border-gray-100 last:border-b-0 active:bg-gray-50 transition-colors" 38 + <view v-for="(item, index) in filteredQuestions" :key="index"
37 - @tap="handleQuestionClick(item)"> 39 + class="flex items-center justify-between p-[32rpx] border-b border-gray-100 last:border-b-0 active:bg-gray-50 transition-colors"
38 - <view class="flex items-center"> 40 + @tap="handleQuestionClick(item)">
39 - <view class="w-[12rpx] h-[12rpx] rounded-full bg-blue-600 mr-[24rpx]"></view> 41 + <view class="flex items-center">
40 - <text class="text-[28rpx] text-gray-800">{{ item.title }}</text> 42 + <view class="w-[12rpx] h-[12rpx] rounded-full bg-blue-600 mr-[24rpx]"></view>
43 + <text class="text-[28rpx] text-gray-800">{{ item.title }}</text>
44 + </view>
45 + <IconFont name="rectRight" class="text-gray-400" size="16" />
41 </view> 46 </view>
42 - <IconFont name="rectRight" class="text-gray-400" size="16" /> 47 + </view>
48 + <view v-else class="p-[64rpx] text-center">
49 + <text class="text-[28rpx] text-gray-400">未找到相关问题</text>
43 </view> 50 </view>
44 </view> 51 </view>
45 </view> 52 </view>
...@@ -118,7 +125,7 @@ ...@@ -118,7 +125,7 @@
118 </template> 125 </template>
119 126
120 <script setup> 127 <script setup>
121 -import { ref } from 'vue' 128 +import { ref, computed } from 'vue'
122 import NavHeader from '@/components/NavHeader.vue' 129 import NavHeader from '@/components/NavHeader.vue'
123 import TabBar from '@/components/TabBar.vue' 130 import TabBar from '@/components/TabBar.vue'
124 import IconFont from '@/components/IconFont.vue' 131 import IconFont from '@/components/IconFont.vue'
...@@ -129,6 +136,9 @@ const showContactPopup = ref(false) ...@@ -129,6 +136,9 @@ const showContactPopup = ref(false)
129 const showQuestionPopup = ref(false) 136 const showQuestionPopup = ref(false)
130 const currentQuestion = ref(null) 137 const currentQuestion = ref(null)
131 138
139 +// 搜索关键字
140 +const searchKeyword = ref('')
141 +
132 // Contact methods data 142 // Contact methods data
133 const contactMethods = [ 143 const contactMethods = [
134 { 144 {
...@@ -225,6 +235,17 @@ const questions = ref([ ...@@ -225,6 +235,17 @@ const questions = ref([
225 } 235 }
226 ]) 236 ])
227 237
238 +// 模糊匹配过滤问题列表
239 +const filteredQuestions = computed(() => {
240 + if (!searchKeyword.value) {
241 + return questions.value
242 + }
243 + const keyword = searchKeyword.value.toLowerCase()
244 + return questions.value.filter(q =>
245 + q.title.toLowerCase().includes(keyword)
246 + )
247 +})
248 +
228 const handleQuestionClick = (item) => { 249 const handleQuestionClick = (item) => {
229 currentQuestion.value = item 250 currentQuestion.value = item
230 showQuestionPopup.value = true 251 showQuestionPopup.value = true
......
...@@ -103,7 +103,7 @@ ...@@ -103,7 +103,7 @@
103 103
104 <!-- 没有更多数据 --> 104 <!-- 没有更多数据 -->
105 <view v-if="!hasMore && products.length > 0" class="flex justify-center items-center py-[40rpx]"> 105 <view v-if="!hasMore && products.length > 0" class="flex justify-center items-center py-[40rpx]">
106 - <text class="text-gray-400 text-[28rpx]">没有更多了</text> 106 + <text class="text-gray-400 text-[24rpx]">没有更多了</text>
107 </view> 107 </view>
108 108
109 <!-- 空状态 --> 109 <!-- 空状态 -->
......
...@@ -12,7 +12,6 @@ ...@@ -12,7 +12,6 @@
12 v-model="searchValue" 12 v-model="searchValue"
13 placeholder="搜索资料..." 13 placeholder="搜索资料..."
14 @search="onSearch" 14 @search="onSearch"
15 - @blur="onSearch"
16 @clear="onClear" 15 @clear="onClear"
17 variant="rounded" 16 variant="rounded"
18 :show-border="true" 17 :show-border="true"
...@@ -117,13 +116,14 @@ ...@@ -117,13 +116,14 @@
117 </template> 116 </template>
118 117
119 <script setup> 118 <script setup>
120 -import { ref, computed, nextTick } from 'vue' 119 +import { ref, computed, nextTick, watch } from 'vue'
121 import { useLoad, useReachBottom } from '@tarojs/taro' 120 import { useLoad, useReachBottom } from '@tarojs/taro'
122 import NavHeader from '@/components/NavHeader.vue' 121 import NavHeader from '@/components/NavHeader.vue'
123 import SearchBar from '@/components/SearchBar.vue' 122 import SearchBar from '@/components/SearchBar.vue'
124 import ListItemActions from '@/components/ListItemActions/index.vue' 123 import ListItemActions from '@/components/ListItemActions/index.vue'
125 import { useListItemClick, ListType } from '@/composables/useListItemClick' 124 import { useListItemClick, ListType } from '@/composables/useListItemClick'
126 import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons' 125 import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons'
126 +import { debounce } from '@/utils/debounce'
127 import { fileListAPI } from '@/api/file' 127 import { fileListAPI } from '@/api/file'
128 import { useCollectOperation } from '@/composables/useCollectOperation' 128 import { useCollectOperation } from '@/composables/useCollectOperation'
129 import Taro from '@tarojs/taro' 129 import Taro from '@tarojs/taro'
...@@ -134,6 +134,15 @@ const listVisible = ref(true) ...@@ -134,6 +134,15 @@ const listVisible = ref(true)
134 const listRenderKey = ref(0) 134 const listRenderKey = ref(0)
135 135
136 /** 136 /**
137 + * 防抖搜索函数
138 + * @description 使用防抖优化搜索性能,避免频繁请求接口
139 + */
140 +const debouncedSearch = debounce(async () => {
141 + console.log('[Material List] 防抖搜索触发')
142 + await onSearch()
143 +}, 500)
144 +
145 +/**
137 * 加载状态 146 * 加载状态
138 */ 147 */
139 const loading = ref(false) 148 const loading = ref(false)
...@@ -601,6 +610,25 @@ const onSearch = async () => { ...@@ -601,6 +610,25 @@ const onSearch = async () => {
601 } 610 }
602 611
603 /** 612 /**
613 + * 监听搜索关键字变化
614 + * @description 实现实时搜索:用户输入时自动触发搜索(带防抖)
615 + */
616 +watch(searchValue, (newValue, oldValue) => {
617 + console.log('[Material List] searchValue 变化:', oldValue, '->', newValue)
618 +
619 + // 如果搜索关键字为空,立即清除搜索(不需要防抖)
620 + if (!newValue?.trim()) {
621 + console.log('[Material List] 搜索关键字为空,立即清除')
622 + onClear()
623 + return
624 + }
625 +
626 + // 有搜索关键字,使用防抖搜索
627 + console.log('[Material List] 触发防抖搜索')
628 + debouncedSearch()
629 +})
630 +
631 +/**
604 * 清除搜索关键词 632 * 清除搜索关键词
605 * @description 用户点击搜索框右侧的删除按钮时触发,重新请求当前分类的最新数据 633 * @description 用户点击搜索框右侧的删除按钮时触发,重新请求当前分类的最新数据
606 * 634 *
......
...@@ -21,14 +21,14 @@ ...@@ -21,14 +21,14 @@
21 {{ item.create_time }} 21 {{ item.create_time }}
22 </text> 22 </text>
23 </view> 23 </view>
24 - 24 +
25 <view class="text-sm text-gray-600 line-clamp-2 leading-relaxed"> 25 <view class="text-sm text-gray-600 line-clamp-2 leading-relaxed">
26 {{ item.intro || item.content || '暂无简介' }} 26 {{ item.intro || item.content || '暂无简介' }}
27 </view> 27 </view>
28 </view> 28 </view>
29 29
30 <!-- 加载更多/没有更多 --> 30 <!-- 加载更多/没有更多 -->
31 - <view class="py-4 text-center text-xs text-gray-400"> 31 + <view class="py-4 text-center text-[24rpx] text-gray-400">
32 <text v-if="loading">加载中...</text> 32 <text v-if="loading">加载中...</text>
33 <text v-else-if="!hasMore">没有更多了</text> 33 <text v-else-if="!hasMore">没有更多了</text>
34 <text v-else>上拉加载更多</text> 34 <text v-else>上拉加载更多</text>
...@@ -66,7 +66,7 @@ const loading = ref(false) ...@@ -66,7 +66,7 @@ const loading = ref(false)
66 */ 66 */
67 const fetchMessageList = async (refresh = false) => { 67 const fetchMessageList = async (refresh = false) => {
68 if (loading.value) return 68 if (loading.value) return
69 - 69 +
70 if (refresh) { 70 if (refresh) {
71 page.value = 1 71 page.value = 1
72 hasMore.value = true 72 hasMore.value = true
...@@ -75,7 +75,7 @@ const fetchMessageList = async (refresh = false) => { ...@@ -75,7 +75,7 @@ const fetchMessageList = async (refresh = false) => {
75 } 75 }
76 76
77 loading.value = true 77 loading.value = true
78 - 78 +
79 try { 79 try {
80 const res = await myListAPI({ 80 const res = await myListAPI({
81 page: page.value, 81 page: page.value,
...@@ -84,7 +84,7 @@ const fetchMessageList = async (refresh = false) => { ...@@ -84,7 +84,7 @@ const fetchMessageList = async (refresh = false) => {
84 84
85 if (res.code === 1) { 85 if (res.code === 1) {
86 const list = res.data?.list || [] 86 const list = res.data?.list || []
87 - 87 +
88 if (refresh) { 88 if (refresh) {
89 messageList.value = list 89 messageList.value = list
90 } else { 90 } else {
......
1 +/**
2 + * 防抖工具函数
3 + *
4 + * @description 创建防抖函数,延迟执行 func
5 + * @param {Function} func - 需要防抖的函数
6 + * @param {number} delay - 延迟时间(毫秒)
7 + * @returns {Function} 防抖后的函数
8 + *
9 + * @example
10 + * const debouncedSearch = debounce(() => {
11 + * console.log('搜索')
12 + * }, 500)
13 + *
14 + * debouncedSearch() // 等待 500ms 后执行
15 + * debouncedSearch() // 取消上一次,重新计时
16 + */
17 +export function debounce(func, delay = 500) {
18 + let timer = null
19 +
20 + return function (...args) {
21 + // 清除之前的定时器
22 + if (timer) {
23 + clearTimeout(timer)
24 + }
25 +
26 + // 设置新的定时器
27 + timer = setTimeout(() => {
28 + func.apply(this, args)
29 + }, delay)
30 + }
31 +}
32 +
33 +/**
34 + * 防抖 Hook
35 + *
36 + * @description 创建响应式防抖函数
37 + * @param {Function} func - 需要防抖的函数
38 + * @param {number} delay - 延迟时间(毫秒)
39 + * @returns {Function} 防抖后的函数
40 + *
41 + * @example
42 + * import { debounceFn } from '@/utils/debounce'
43 + *
44 + * const debouncedSearch = debounceFn(async () => {
45 + * await fetchData()
46 + * }, 500)
47 + */
48 +export function debounceFn(func, delay = 500) {
49 + return debounce(func, delay)
50 +}