Showing
9 changed files
with
156 additions
and
21 deletions
| ... | @@ -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 { | ... | ... |
src/utils/debounce.js
0 → 100644
| 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 | +} |
-
Please register or login to post a comment