hookehuyr

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

......@@ -27,7 +27,7 @@ declare module 'vue' {
OfficeViewer: typeof import('./src/components/OfficeViewer.vue')['default']
PdfPreview: typeof import('./src/components/PdfPreview.vue')['default']
Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
PlanPopup: typeof import('./src/components/PlanPopup/index.vue')['default']
PlanPopup: typeof import('./src/components/PlanSchemes/PlanPopup.vue')['default']
PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
QrCode: typeof import('./src/components/qrCode.vue')['default']
QrCodeSearch: typeof import('./src/components/qrCodeSearch.vue')['default']
......
# Changelog
> 本文档记录 Manulife WeApp 项目的所有重要变更。
> 本文档记录 Manulife WeApp项目的所有重要变更。
> 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
---
## [2026-02-05] - 优化搜索功能体验
### 新增
- 帮助中心页面实现实时搜索功能(模糊匹配问题标题)
- 新增防抖工具函数 (src/utils/debounce.js)
- 资料列表页面改造为实时搜索(带 500ms 防抖)
### 优化
- SearchBar 组件修复 blur 事件传递参数
- 移除资料列表页面冗余的 blur 事件监听
- 页面空状态 UI 优化(category-list, message)
### 技术实现
- help-center: 使用 v-model + computed 实现纯前端过滤
- material-list: 使用 watch + debounce 实现防抖接口调用
- SearchBar: blur 事件现在传递当前输入值
---
**详细信息**
- **影响文件**:
- src/components/SearchBar.vue
- src/utils/debounce.js (新增)
- src/pages/help-center/index.vue
- src/pages/material-list/index.vue
- src/pages/category-list/index.vue
- src/pages/knowledge-base/index.vue
- src/pages/message/index.vue
- **技术栈**: Vue 3, Taro 4, Watch API
- **测试状态**: 已通过本地测试
- **备注**: 所有搜索功能已统一为实时搜索模式
---
## [2026-02-05] - 修复知识库搜索栏高度变化
### 修复
......
......@@ -168,8 +168,9 @@ const emit = defineEmits({
/**
* 失去焦点
* @event blur
* @param {string} value - 当前输入值
*/
blur: () => true,
blur: (value) => true,
/**
* 清除
* @event clear
......@@ -219,7 +220,7 @@ function handleFocus() {
*/
function handleBlur() {
console.log('[SearchBar Component] 失去焦点')
emit('blur')
emit('blur', internalValue.value)
}
/**
......
......@@ -21,7 +21,8 @@
<!-- Empty State -->
<div v-else class="px-[40rpx] mt-[80rpx] flex items-center justify-center">
<text class="text-[#9CA3AF] text-[28rpx]">暂无分类</text>
<!-- <text class="text-[#9CA3AF] text-[28rpx]">暂无分类</text> -->
<nut-empty description="暂无分类" image="empty" />
</div>
</div>
</template>
......
......@@ -7,8 +7,10 @@
<!-- Search Bar -->
<view class="mb-[32rpx]">
<SearchBar
v-model="searchKeyword"
placeholder="搜索问题或关键词"
variant="rounded"
:show-clear="true"
/>
</view>
......@@ -32,14 +34,19 @@
<view>
<text class="block text-[32rpx] text-gray-900 font-bold mb-[24rpx]">重点问题</text>
<view class="bg-white rounded-[24rpx] shadow-sm overflow-hidden">
<view v-for="(item, index) in questions" :key="index"
class="flex items-center justify-between p-[32rpx] border-b border-gray-100 last:border-b-0 active:bg-gray-50 transition-colors"
@tap="handleQuestionClick(item)">
<view class="flex items-center">
<view class="w-[12rpx] h-[12rpx] rounded-full bg-blue-600 mr-[24rpx]"></view>
<text class="text-[28rpx] text-gray-800">{{ item.title }}</text>
<view v-if="filteredQuestions.length > 0">
<view v-for="(item, index) in filteredQuestions" :key="index"
class="flex items-center justify-between p-[32rpx] border-b border-gray-100 last:border-b-0 active:bg-gray-50 transition-colors"
@tap="handleQuestionClick(item)">
<view class="flex items-center">
<view class="w-[12rpx] h-[12rpx] rounded-full bg-blue-600 mr-[24rpx]"></view>
<text class="text-[28rpx] text-gray-800">{{ item.title }}</text>
</view>
<IconFont name="rectRight" class="text-gray-400" size="16" />
</view>
<IconFont name="rectRight" class="text-gray-400" size="16" />
</view>
<view v-else class="p-[64rpx] text-center">
<text class="text-[28rpx] text-gray-400">未找到相关问题</text>
</view>
</view>
</view>
......@@ -118,7 +125,7 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
import NavHeader from '@/components/NavHeader.vue'
import TabBar from '@/components/TabBar.vue'
import IconFont from '@/components/IconFont.vue'
......@@ -129,6 +136,9 @@ const showContactPopup = ref(false)
const showQuestionPopup = ref(false)
const currentQuestion = ref(null)
// 搜索关键字
const searchKeyword = ref('')
// Contact methods data
const contactMethods = [
{
......@@ -225,6 +235,17 @@ const questions = ref([
}
])
// 模糊匹配过滤问题列表
const filteredQuestions = computed(() => {
if (!searchKeyword.value) {
return questions.value
}
const keyword = searchKeyword.value.toLowerCase()
return questions.value.filter(q =>
q.title.toLowerCase().includes(keyword)
)
})
const handleQuestionClick = (item) => {
currentQuestion.value = item
showQuestionPopup.value = true
......
......@@ -103,7 +103,7 @@
<!-- 没有更多数据 -->
<view v-if="!hasMore && products.length > 0" class="flex justify-center items-center py-[40rpx]">
<text class="text-gray-400 text-[28rpx]">没有更多了</text>
<text class="text-gray-400 text-[24rpx]">没有更多了</text>
</view>
<!-- 空状态 -->
......
......@@ -12,7 +12,6 @@
v-model="searchValue"
placeholder="搜索资料..."
@search="onSearch"
@blur="onSearch"
@clear="onClear"
variant="rounded"
:show-border="true"
......@@ -117,13 +116,14 @@
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
import { ref, computed, nextTick, watch } from 'vue'
import { useLoad, useReachBottom } from '@tarojs/taro'
import NavHeader from '@/components/NavHeader.vue'
import SearchBar from '@/components/SearchBar.vue'
import ListItemActions from '@/components/ListItemActions/index.vue'
import { useListItemClick, ListType } from '@/composables/useListItemClick'
import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons'
import { debounce } from '@/utils/debounce'
import { fileListAPI } from '@/api/file'
import { useCollectOperation } from '@/composables/useCollectOperation'
import Taro from '@tarojs/taro'
......@@ -134,6 +134,15 @@ const listVisible = ref(true)
const listRenderKey = ref(0)
/**
* 防抖搜索函数
* @description 使用防抖优化搜索性能,避免频繁请求接口
*/
const debouncedSearch = debounce(async () => {
console.log('[Material List] 防抖搜索触发')
await onSearch()
}, 500)
/**
* 加载状态
*/
const loading = ref(false)
......@@ -601,6 +610,25 @@ const onSearch = async () => {
}
/**
* 监听搜索关键字变化
* @description 实现实时搜索:用户输入时自动触发搜索(带防抖)
*/
watch(searchValue, (newValue, oldValue) => {
console.log('[Material List] searchValue 变化:', oldValue, '->', newValue)
// 如果搜索关键字为空,立即清除搜索(不需要防抖)
if (!newValue?.trim()) {
console.log('[Material List] 搜索关键字为空,立即清除')
onClear()
return
}
// 有搜索关键字,使用防抖搜索
console.log('[Material List] 触发防抖搜索')
debouncedSearch()
})
/**
* 清除搜索关键词
* @description 用户点击搜索框右侧的删除按钮时触发,重新请求当前分类的最新数据
*
......
......@@ -21,14 +21,14 @@
{{ item.create_time }}
</text>
</view>
<view class="text-sm text-gray-600 line-clamp-2 leading-relaxed">
{{ item.intro || item.content || '暂无简介' }}
</view>
</view>
<!-- 加载更多/没有更多 -->
<view class="py-4 text-center text-xs text-gray-400">
<view class="py-4 text-center text-[24rpx] text-gray-400">
<text v-if="loading">加载中...</text>
<text v-else-if="!hasMore">没有更多了</text>
<text v-else>上拉加载更多</text>
......@@ -66,7 +66,7 @@ const loading = ref(false)
*/
const fetchMessageList = async (refresh = false) => {
if (loading.value) return
if (refresh) {
page.value = 1
hasMore.value = true
......@@ -75,7 +75,7 @@ const fetchMessageList = async (refresh = false) => {
}
loading.value = true
try {
const res = await myListAPI({
page: page.value,
......@@ -84,7 +84,7 @@ const fetchMessageList = async (refresh = false) => {
if (res.code === 1) {
const list = res.data?.list || []
if (refresh) {
messageList.value = list
} else {
......
/**
* 防抖工具函数
*
* @description 创建防抖函数,延迟执行 func
* @param {Function} func - 需要防抖的函数
* @param {number} delay - 延迟时间(毫秒)
* @returns {Function} 防抖后的函数
*
* @example
* const debouncedSearch = debounce(() => {
* console.log('搜索')
* }, 500)
*
* debouncedSearch() // 等待 500ms 后执行
* debouncedSearch() // 取消上一次,重新计时
*/
export function debounce(func, delay = 500) {
let timer = null
return function (...args) {
// 清除之前的定时器
if (timer) {
clearTimeout(timer)
}
// 设置新的定时器
timer = setTimeout(() => {
func.apply(this, args)
}, delay)
}
}
/**
* 防抖 Hook
*
* @description 创建响应式防抖函数
* @param {Function} func - 需要防抖的函数
* @param {number} delay - 延迟时间(毫秒)
* @returns {Function} 防抖后的函数
*
* @example
* import { debounceFn } from '@/utils/debounce'
*
* const debouncedSearch = debounceFn(async () => {
* await fetchData()
* }, 500)
*/
export function debounceFn(func, delay = 500) {
return debounce(func, delay)
}