refactor(components): 重构 SearchBar 组件并更新搜索页面
- 将 SearchBar 组件从原生 input 重构为 NutUI Input 实现,简化样式和逻辑 - 更新 components.d.ts 类型声明,移除未使用的 NutConfigProvider,修正 PlanPopup 导入路径 - 重构搜索页面,使用 NutTabs 实现分类切换,支持长列表测试数据 - 为搜索结果项添加动画效果,优化用户体验
Showing
3 changed files
with
317 additions
and
179 deletions
| ... | @@ -16,7 +16,6 @@ declare module 'vue' { | ... | @@ -16,7 +16,6 @@ declare module 'vue' { |
| 16 | NavHeader: typeof import('./src/components/NavHeader.vue')['default'] | 16 | NavHeader: typeof import('./src/components/NavHeader.vue')['default'] |
| 17 | NutAvatar: typeof import('@nutui/nutui-taro')['Avatar'] | 17 | NutAvatar: typeof import('@nutui/nutui-taro')['Avatar'] |
| 18 | NutButton: typeof import('@nutui/nutui-taro')['Button'] | 18 | NutButton: typeof import('@nutui/nutui-taro')['Button'] |
| 19 | - NutConfigProvider: typeof import('@nutui/nutui-taro')['ConfigProvider'] | ||
| 20 | NutInput: typeof import('@nutui/nutui-taro')['Input'] | 19 | NutInput: typeof import('@nutui/nutui-taro')['Input'] |
| 21 | NutPicker: typeof import('@nutui/nutui-taro')['Picker'] | 20 | NutPicker: typeof import('@nutui/nutui-taro')['Picker'] |
| 22 | NutPopup: typeof import('@nutui/nutui-taro')['Popup'] | 21 | NutPopup: typeof import('@nutui/nutui-taro')['Popup'] |
| ... | @@ -28,7 +27,7 @@ declare module 'vue' { | ... | @@ -28,7 +27,7 @@ declare module 'vue' { |
| 28 | OfficeViewer: typeof import('./src/components/OfficeViewer.vue')['default'] | 27 | OfficeViewer: typeof import('./src/components/OfficeViewer.vue')['default'] |
| 29 | PdfPreview: typeof import('./src/components/PdfPreview.vue')['default'] | 28 | PdfPreview: typeof import('./src/components/PdfPreview.vue')['default'] |
| 30 | Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] | 29 | Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] |
| 31 | - PlanPopup: typeof import('./src/components/PlanSchemes/PlanPopup.vue')['default'] | 30 | + PlanPopup: typeof import('./src/components/PlanPopup/index.vue')['default'] |
| 32 | PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] | 31 | PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] |
| 33 | QrCode: typeof import('./src/components/qrCode.vue')['default'] | 32 | QrCode: typeof import('./src/components/qrCode.vue')['default'] |
| 34 | QrCodeSearch: typeof import('./src/components/qrCodeSearch.vue')['default'] | 33 | QrCodeSearch: typeof import('./src/components/qrCodeSearch.vue')['default'] | ... | ... |
| 1 | <template> | 1 | <template> |
| 2 | - <view | 2 | + <view class="search-bar" :class="containerClass"> |
| 3 | - class="search-bar flex items-center" | 3 | + <!-- NutUI Input 组件 --> |
| 4 | - :class="containerClass" | 4 | + <nut-input |
| 5 | - > | ||
| 6 | - <!-- Search Icon --> | ||
| 7 | - <IconFont | ||
| 8 | - name="search" | ||
| 9 | - :size="iconSize" | ||
| 10 | - :color="iconColor" | ||
| 11 | - class="flex-shrink-0 mr-[16rpx]" | ||
| 12 | - /> | ||
| 13 | - | ||
| 14 | - <!-- Input --> | ||
| 15 | - <input | ||
| 16 | v-model="internalValue" | 5 | v-model="internalValue" |
| 17 | :type="inputType" | 6 | :type="inputType" |
| 18 | :placeholder="placeholder" | 7 | :placeholder="placeholder" |
| 19 | - :placeholder-class="placeholderClass" | ||
| 20 | - :class="inputClass" | ||
| 21 | :disabled="disabled" | 8 | :disabled="disabled" |
| 22 | - @focus="handleFocus" | 9 | + confirm-type="search" |
| 10 | + :clearable="showClear" | ||
| 11 | + class="search-input" | ||
| 12 | + @clear="handleClear" | ||
| 23 | @blur="handleBlur" | 13 | @blur="handleBlur" |
| 24 | - @input="handleInput" | 14 | + @focus="handleFocus" |
| 25 | @confirm="handleSearch" | 15 | @confirm="handleSearch" |
| 26 | - /> | 16 | + > |
| 27 | - | 17 | + <template #left> |
| 28 | - <!-- Clear Button --> | ||
| 29 | <IconFont | 18 | <IconFont |
| 30 | - v-if="showClear && internalValue" | 19 | + name="search" |
| 31 | - name="close" | 20 | + :size="iconSize" |
| 32 | - :size="clearIconSize" | 21 | + :color="iconColor" |
| 33 | - :color="clearIconColor" | 22 | + class="mr-[8rpx]" |
| 34 | - class="flex-shrink-0 ml-[16rpx]" | ||
| 35 | - @tap="handleClear" | ||
| 36 | /> | 23 | /> |
| 24 | + </template> | ||
| 25 | + </nut-input> | ||
| 37 | </view> | 26 | </view> |
| 38 | </template> | 27 | </template> |
| 39 | 28 | ||
| ... | @@ -42,9 +31,9 @@ import { ref, watch, computed } from 'vue' | ... | @@ -42,9 +31,9 @@ import { ref, watch, computed } from 'vue' |
| 42 | import IconFont from '@/components/IconFont.vue' | 31 | import IconFont from '@/components/IconFont.vue' |
| 43 | 32 | ||
| 44 | /** | 33 | /** |
| 45 | - * SearchBar 组件 | 34 | + * SearchBar 组件(基于 NutUI Input) |
| 46 | * | 35 | * |
| 47 | - * @description 可复用的搜索栏组件,支持多种样式变体 | 36 | + * @description 可复用的搜索栏组件,使用 NutUI Input 组件实现 |
| 48 | * @author Claude Code | 37 | * @author Claude Code |
| 49 | * @example | 38 | * @example |
| 50 | * <SearchBar | 39 | * <SearchBar |
| ... | @@ -136,24 +125,6 @@ const props = defineProps({ | ... | @@ -136,24 +125,6 @@ const props = defineProps({ |
| 136 | iconColor: { | 125 | iconColor: { |
| 137 | type: String, | 126 | type: String, |
| 138 | default: '#9CA3AF' | 127 | default: '#9CA3AF' |
| 139 | - }, | ||
| 140 | - /** | ||
| 141 | - * 清除图标大小 | ||
| 142 | - * @type {number|string} | ||
| 143 | - * @default 16 | ||
| 144 | - */ | ||
| 145 | - clearIconSize: { | ||
| 146 | - type: [Number, String], | ||
| 147 | - default: 16 | ||
| 148 | - }, | ||
| 149 | - /** | ||
| 150 | - * 清除图标颜色 | ||
| 151 | - * @type {string} | ||
| 152 | - * @default '#9CA3AF' | ||
| 153 | - */ | ||
| 154 | - clearIconColor: { | ||
| 155 | - type: String, | ||
| 156 | - default: '#9CA3AF' | ||
| 157 | } | 128 | } |
| 158 | }) | 129 | }) |
| 159 | 130 | ||
| ... | @@ -198,41 +169,17 @@ const internalValue = ref(props.modelValue) | ... | @@ -198,41 +169,17 @@ const internalValue = ref(props.modelValue) |
| 198 | 169 | ||
| 199 | // 容器样式类 | 170 | // 容器样式类 |
| 200 | const containerClass = computed(() => { | 171 | const containerClass = computed(() => { |
| 201 | - const base = [ | 172 | + const classes = ['search-bar'] |
| 202 | - 'bg-white', | ||
| 203 | - 'shadow-sm', | ||
| 204 | - 'px-[40rpx]', // 左右 padding | ||
| 205 | - props.variant === 'rounded' ? 'rounded-full' : 'rounded-[20rpx]' | ||
| 206 | - ] | ||
| 207 | - | ||
| 208 | - if (props.showBorder) { | ||
| 209 | - base.push('border', 'border-gray-200') | ||
| 210 | - } else { | ||
| 211 | - base.push('border', 'border-gray-50') | ||
| 212 | - } | ||
| 213 | 173 | ||
| 214 | - // 高度样式 | ||
| 215 | if (props.variant === 'rounded') { | 174 | if (props.variant === 'rounded') { |
| 216 | - base.push('h-[88rpx]') | 175 | + classes.push('search-bar-rounded') |
| 217 | - } else { | ||
| 218 | - base.push('py-[24rpx]') | ||
| 219 | } | 176 | } |
| 220 | 177 | ||
| 221 | - return base.join(' ') | 178 | + if (props.showBorder) { |
| 222 | -}) | 179 | + classes.push('search-bar-bordered') |
| 223 | - | 180 | + } |
| 224 | -// 占位符样式类 | ||
| 225 | -const placeholderClass = 'text-gray-400 text-[28rpx]' | ||
| 226 | 181 | ||
| 227 | -// 输入框样式类 | 182 | + return classes.join(' ') |
| 228 | -const inputClass = computed(() => { | ||
| 229 | - return [ | ||
| 230 | - 'flex-1', | ||
| 231 | - 'text-[28rpx]', | ||
| 232 | - 'bg-transparent', | ||
| 233 | - 'outline-none', | ||
| 234 | - props.disabled ? 'opacity-50' : '' | ||
| 235 | - ].filter(Boolean).join(' ') | ||
| 236 | }) | 183 | }) |
| 237 | 184 | ||
| 238 | // 监听 modelValue 变化 | 185 | // 监听 modelValue 变化 |
| ... | @@ -243,12 +190,14 @@ watch(() => props.modelValue, (newValue) => { | ... | @@ -243,12 +190,14 @@ watch(() => props.modelValue, (newValue) => { |
| 243 | // 监听内部值变化,触发更新 | 190 | // 监听内部值变化,触发更新 |
| 244 | watch(internalValue, (newValue) => { | 191 | watch(internalValue, (newValue) => { |
| 245 | emit('update:modelValue', newValue) | 192 | emit('update:modelValue', newValue) |
| 193 | + emit('input', newValue) | ||
| 246 | }) | 194 | }) |
| 247 | 195 | ||
| 248 | /** | 196 | /** |
| 249 | * 处理获得焦点 | 197 | * 处理获得焦点 |
| 250 | */ | 198 | */ |
| 251 | function handleFocus() { | 199 | function handleFocus() { |
| 200 | + console.log('[SearchBar Component] 获得焦点') | ||
| 252 | emit('focus') | 201 | emit('focus') |
| 253 | } | 202 | } |
| 254 | 203 | ||
| ... | @@ -256,27 +205,25 @@ function handleFocus() { | ... | @@ -256,27 +205,25 @@ function handleFocus() { |
| 256 | * 处理失去焦点 | 205 | * 处理失去焦点 |
| 257 | */ | 206 | */ |
| 258 | function handleBlur() { | 207 | function handleBlur() { |
| 208 | + console.log('[SearchBar Component] 失去焦点') | ||
| 259 | emit('blur') | 209 | emit('blur') |
| 260 | } | 210 | } |
| 261 | 211 | ||
| 262 | /** | 212 | /** |
| 263 | - * 处理输入 | ||
| 264 | - */ | ||
| 265 | -function handleInput(e) { | ||
| 266 | - emit('input', internalValue.value) | ||
| 267 | -} | ||
| 268 | - | ||
| 269 | -/** | ||
| 270 | * 处理搜索(回车) | 213 | * 处理搜索(回车) |
| 271 | */ | 214 | */ |
| 272 | function handleSearch() { | 215 | function handleSearch() { |
| 216 | + console.log('[SearchBar Component] handleSearch 被调用') | ||
| 217 | + console.log('[SearchBar Component] 当前输入值:', internalValue.value) | ||
| 273 | emit('search', internalValue.value) | 218 | emit('search', internalValue.value) |
| 219 | + console.log('[SearchBar Component] search 事件已发送') | ||
| 274 | } | 220 | } |
| 275 | 221 | ||
| 276 | /** | 222 | /** |
| 277 | * 清除输入 | 223 | * 清除输入 |
| 278 | */ | 224 | */ |
| 279 | function handleClear() { | 225 | function handleClear() { |
| 226 | + console.log('[SearchBar Component] 清除输入') | ||
| 280 | internalValue.value = '' | 227 | internalValue.value = '' |
| 281 | emit('clear') | 228 | emit('clear') |
| 282 | emit('update:modelValue', '') | 229 | emit('update:modelValue', '') |
| ... | @@ -284,5 +231,30 @@ function handleClear() { | ... | @@ -284,5 +231,30 @@ function handleClear() { |
| 284 | </script> | 231 | </script> |
| 285 | 232 | ||
| 286 | <style lang="less" scoped> | 233 | <style lang="less" scoped> |
| 287 | -/* 样式通过 TailwindCSS 类控制 */ | 234 | +.search-bar { |
| 235 | + padding: 0; | ||
| 236 | + background: white; | ||
| 237 | + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); | ||
| 238 | + | ||
| 239 | + :deep(.nut-input) { | ||
| 240 | + padding: 24rpx 32rpx; | ||
| 241 | + background-color: transparent; | ||
| 242 | + } | ||
| 243 | + | ||
| 244 | + &.search-bar-rounded { | ||
| 245 | + border-radius: 9999rpx; | ||
| 246 | + | ||
| 247 | + :deep(.nut-input) { | ||
| 248 | + border-radius: 9999rpx; | ||
| 249 | + } | ||
| 250 | + } | ||
| 251 | + | ||
| 252 | + &.search-bar-bordered { | ||
| 253 | + border: 1px solid #e5e7eb; | ||
| 254 | + } | ||
| 255 | +} | ||
| 256 | + | ||
| 257 | +.search-bar-rounded { | ||
| 258 | + height: 88rpx; | ||
| 259 | +} | ||
| 288 | </style> | 260 | </style> | ... | ... |
| 1 | +<!-- | ||
| 2 | + * @Date: 2026-01-31 | ||
| 3 | + * @Description: 搜索页面 - 已改造为 NutTabs 版本,支持长列表和分类切换测试 | ||
| 4 | +--> | ||
| 1 | <template> | 5 | <template> |
| 2 | <view class="min-h-screen bg-[#F9FAFB] pb-[calc(160rpx+env(safe-area-inset-bottom))]"> | 6 | <view class="min-h-screen bg-[#F9FAFB] pb-[calc(160rpx+env(safe-area-inset-bottom))]"> |
| 3 | <!-- Navigation Header --> | 7 | <!-- Navigation Header --> |
| ... | @@ -15,21 +19,37 @@ | ... | @@ -15,21 +19,37 @@ |
| 15 | :show-clear="true" | 19 | :show-clear="true" |
| 16 | @search="handleSearch" | 20 | @search="handleSearch" |
| 17 | @clear="clearSearch" | 21 | @clear="clearSearch" |
| 22 | + @blur="handleBlur" | ||
| 18 | /> | 23 | /> |
| 19 | </view> | 24 | </view> |
| 20 | 25 | ||
| 21 | - <!-- Filter Tabs --> | 26 | + <!-- Tabs Container --> |
| 22 | - <view class="flex overflow-x-auto no-scrollbar mb-[40rpx] space-x-[24rpx]"> | 27 | + <view class="mb-[40rpx]"> |
| 23 | - <view v-for="(tab, index) in tabs" :key="index" | 28 | + <nut-tabs v-model="activeTabId"> |
| 24 | - class="px-[32rpx] py-[16rpx] rounded-full text-[28rpx] whitespace-nowrap transition-colors" | 29 | + <!-- 自定义标签栏 --> |
| 25 | - :class="activeTab === index ? 'bg-[#2563EB] text-white' : 'bg-[#F3F4F6] text-[#6B7280]'" | 30 | + <template #titles> |
| 26 | - @tap="activeTab = index"> | 31 | + <view class="filter-tabs-wrapper"> |
| 27 | - {{ tab }} | 32 | + <view |
| 33 | + v-for="item in tabsData" | ||
| 34 | + :key="item.id" | ||
| 35 | + :class="[ | ||
| 36 | + 'filter-tab-item', | ||
| 37 | + activeTabId === item.id ? 'filter-tab-active' : 'filter-tab-inactive' | ||
| 38 | + ]" | ||
| 39 | + @tap="onTabClick(item.id)" | ||
| 40 | + > | ||
| 41 | + <text class="filter-tab-text">{{ item.name }}</text> | ||
| 28 | </view> | 42 | </view> |
| 29 | </view> | 43 | </view> |
| 44 | + </template> | ||
| 45 | + </nut-tabs> | ||
| 46 | + </view> | ||
| 30 | 47 | ||
| 31 | <!-- Search Results --> | 48 | <!-- Search Results --> |
| 32 | - <view v-if="searchResults.length > 0"> | 49 | + <view |
| 50 | + v-if="searchResults.length > 0" | ||
| 51 | + :key="listRenderKey" | ||
| 52 | + > | ||
| 33 | <!-- Result Count --> | 53 | <!-- Result Count --> |
| 34 | <view class="text-[#6B7280] text-[24rpx] mb-[24rpx]"> | 54 | <view class="text-[#6B7280] text-[24rpx] mb-[24rpx]"> |
| 35 | 找到 {{ searchResults.length }} 个相关结果 | 55 | 找到 {{ searchResults.length }} 个相关结果 |
| ... | @@ -37,11 +57,12 @@ | ... | @@ -37,11 +57,12 @@ |
| 37 | 57 | ||
| 38 | <!-- Results List --> | 58 | <!-- Results List --> |
| 39 | <view class="flex flex-col gap-[24rpx]"> | 59 | <view class="flex flex-col gap-[24rpx]"> |
| 40 | - <!-- Product Card --> | 60 | + <!-- Product/Material Card --> |
| 41 | <view | 61 | <view |
| 42 | v-for="(item, index) in searchResults" | 62 | v-for="(item, index) in searchResults" |
| 43 | :key="index" | 63 | :key="index" |
| 44 | - class="bg-white rounded-[24rpx] overflow-hidden shadow-sm" | 64 | + class="bg-white rounded-[24rpx] overflow-hidden shadow-sm search-result-item" |
| 65 | + :style="{ animationDelay: `${index * 30}ms` }" | ||
| 45 | @tap="goToDetail(item)" | 66 | @tap="goToDetail(item)" |
| 46 | > | 67 | > |
| 47 | <!-- Image + Content Layout --> | 68 | <!-- Image + Content Layout --> |
| ... | @@ -107,7 +128,7 @@ | ... | @@ -107,7 +128,7 @@ |
| 107 | </template> | 128 | </template> |
| 108 | 129 | ||
| 109 | <script setup> | 130 | <script setup> |
| 110 | -import { ref, computed } from 'vue' | 131 | +import { ref, computed, watch } from 'vue' |
| 111 | import Taro from '@tarojs/taro' | 132 | import Taro from '@tarojs/taro' |
| 112 | import { useGo } from '@/hooks/useGo' | 133 | import { useGo } from '@/hooks/useGo' |
| 113 | import NavHeader from '@/components/NavHeader.vue' | 134 | import NavHeader from '@/components/NavHeader.vue' |
| ... | @@ -119,118 +140,184 @@ const go = useGo() | ... | @@ -119,118 +140,184 @@ const go = useGo() |
| 119 | 140 | ||
| 120 | // State | 141 | // State |
| 121 | const searchKeyword = ref('') | 142 | const searchKeyword = ref('') |
| 122 | -const activeTab = ref(0) | 143 | +const activeTabId = ref('') |
| 123 | const hasSearched = ref(false) | 144 | const hasSearched = ref(false) |
| 145 | +const listRenderKey = ref(0) | ||
| 124 | 146 | ||
| 125 | -// Tabs | 147 | +/** |
| 126 | -const tabs = ['全部', '产品', '资料'] | 148 | + * Tab 数据源 |
| 149 | + * @description 包含分类信息和对应的列表 | ||
| 150 | + */ | ||
| 151 | +const tabsData = ref([ | ||
| 152 | + { id: '', name: '全部', list: [] }, | ||
| 153 | + { id: 'product', name: '产品', list: [] }, | ||
| 154 | + { id: 'material', name: '资料', list: [] }, | ||
| 155 | +]) | ||
| 127 | 156 | ||
| 128 | -// Mock data | 157 | +/** |
| 129 | -const mockData = ref([ | 158 | + * 生成大量 Mock 数据用于测试长列表 |
| 130 | - { | 159 | + * @description 生成 50 个产品 + 50 个资料,共 100 条数据 |
| 131 | - id: 1, | 160 | + */ |
| 132 | - title: '家庭财富传承保障计划(分红)', | 161 | +const generateMockData = () => { |
| 133 | - type: '产品', | 162 | + const products = [] |
| 134 | - tag: '热卖', | 163 | + const materials = [] |
| 135 | - views: 256, | 164 | + |
| 136 | - image: 'https://picsum.photos/seed/prod1/200/140', | 165 | + // 生成 50 个产品 |
| 137 | - category: 'product' | 166 | + for (let i = 1; i <= 50; i++) { |
| 138 | - }, | 167 | + products.push({ |
| 139 | - { | 168 | + id: i, |
| 140 | - id: 2, | 169 | + title: `保险产品 ${i} - ${getProductName(i)}`, |
| 141 | - title: '2024年保险市场趋势分析报告', | ||
| 142 | - type: '资料', | ||
| 143 | - views: 189, | ||
| 144 | - image: 'https://picsum.photos/seed/mat1/200/140', | ||
| 145 | - category: 'material' | ||
| 146 | - }, | ||
| 147 | - { | ||
| 148 | - id: 3, | ||
| 149 | - title: '儿童教育金储备方案(分红)', | ||
| 150 | - type: '产品', | ||
| 151 | - tag: '推荐', | ||
| 152 | - views: 342, | ||
| 153 | - image: 'https://picsum.photos/seed/prod2/200/140', | ||
| 154 | - category: 'product' | ||
| 155 | - }, | ||
| 156 | - { | ||
| 157 | - id: 4, | ||
| 158 | - title: '高净值客户需求分析与产品匹配', | ||
| 159 | - type: '资料', | ||
| 160 | - views: 142, | ||
| 161 | - image: 'https://picsum.photos/seed/mat2/200/140', | ||
| 162 | - category: 'material' | ||
| 163 | - }, | ||
| 164 | - { | ||
| 165 | - id: 5, | ||
| 166 | - title: '百万医疗保险计划', | ||
| 167 | - type: '产品', | ||
| 168 | - views: 267, | ||
| 169 | - image: 'https://picsum.photos/seed/prod3/200/140', | ||
| 170 | - category: 'product' | ||
| 171 | - }, | ||
| 172 | - { | ||
| 173 | - id: 6, | ||
| 174 | - title: '保险合同条款解读与风险提示', | ||
| 175 | - type: '资料', | ||
| 176 | - views: 198, | ||
| 177 | - image: 'https://picsum.photos/seed/mat3/200/140', | ||
| 178 | - category: 'material' | ||
| 179 | - }, | ||
| 180 | - { | ||
| 181 | - id: 7, | ||
| 182 | - title: '意外伤害保障计划', | ||
| 183 | type: '产品', | 170 | type: '产品', |
| 184 | - tag: '热卖', | 171 | + tag: i % 3 === 0 ? '热卖' : (i % 5 === 0 ? '推荐' : ''), |
| 185 | - views: 223, | 172 | + views: Math.floor(Math.random() * 500) + 50, |
| 186 | - image: 'https://picsum.photos/seed/prod4/200/140', | 173 | + image: `https://picsum.photos/seed/prod${i}/200/140`, |
| 187 | category: 'product' | 174 | category: 'product' |
| 188 | - }, | 175 | + }) |
| 189 | - { | 176 | + } |
| 190 | - id: 8, | 177 | + |
| 191 | - title: '保险销售实战技巧分享', | 178 | + // 生成 50 个资料 |
| 179 | + for (let i = 1; i <= 50; i++) { | ||
| 180 | + materials.push({ | ||
| 181 | + id: 50 + i, | ||
| 182 | + title: `培训资料 ${i} - ${getMaterialName(i)}`, | ||
| 192 | type: '资料', | 183 | type: '资料', |
| 193 | - views: 156, | 184 | + views: Math.floor(Math.random() * 300) + 30, |
| 194 | - image: 'https://picsum.photos/seed/mat4/200/140', | 185 | + image: `https://picsum.photos/seed/mat${i}/200/140`, |
| 195 | category: 'material' | 186 | category: 'material' |
| 196 | - }, | 187 | + }) |
| 197 | -]) | 188 | + } |
| 189 | + | ||
| 190 | + return [...products, ...materials] | ||
| 191 | +} | ||
| 192 | + | ||
| 193 | +/** | ||
| 194 | + * 获取产品名称 | ||
| 195 | + */ | ||
| 196 | +const getProductName = (index) => { | ||
| 197 | + const names = [ | ||
| 198 | + '终身寿险', '百万医疗', '重疾保障', '意外保险', '年金保险', | ||
| 199 | + '教育金', '养老保险', '财富传承', '投资连结', '分红保险', | ||
| 200 | + '万能险', '定期寿险', '终身医疗', '高端医疗', '团体保险' | ||
| 201 | + ] | ||
| 202 | + return names[index % names.length] | ||
| 203 | +} | ||
| 204 | + | ||
| 205 | +/** | ||
| 206 | + * 获取资料名称 | ||
| 207 | + */ | ||
| 208 | +const getMaterialName = (index) => { | ||
| 209 | + const names = [ | ||
| 210 | + '销售话术', '产品培训', '案例分析', '合规指引', '核保规则', | ||
| 211 | + '理赔流程', '客户服务', '市场分析', '竞争产品对比', '政策解读', | ||
| 212 | + '新人培训', '晋升考核', '团队管理', '活动策划', '产说会流程' | ||
| 213 | + ] | ||
| 214 | + return names[index % names.length] | ||
| 215 | +} | ||
| 216 | + | ||
| 217 | +// All mock data | ||
| 218 | +const allData = ref(generateMockData()) | ||
| 219 | + | ||
| 220 | +console.log('[Search] 数据生成完成,总数:', allData.value.length) | ||
| 221 | +console.log('[Search] 产品数量:', allData.value.filter(item => item.category === 'product').length) | ||
| 222 | +console.log('[Search] 资料数量:', allData.value.filter(item => item.category === 'material').length) | ||
| 223 | + | ||
| 224 | +/** | ||
| 225 | + * 初始化数据分布 | ||
| 226 | + * @description 根据分类规则将 allData 中的数据分配到各个 tab 中 | ||
| 227 | + */ | ||
| 228 | +const initTabsData = () => { | ||
| 229 | + tabsData.value.forEach((tab) => { | ||
| 230 | + if (tab.id === '') { | ||
| 231 | + tab.list = [...allData.value] | ||
| 232 | + } else if (tab.id === 'product') { | ||
| 233 | + tab.list = allData.value.filter(item => item.category === 'product') | ||
| 234 | + } else if (tab.id === 'material') { | ||
| 235 | + tab.list = allData.value.filter(item => item.category === 'material') | ||
| 236 | + } | ||
| 237 | + }) | ||
| 238 | + | ||
| 239 | + // 默认选中第一个 tab(全部) | ||
| 240 | + if (tabsData.value.length > 0) { | ||
| 241 | + activeTabId.value = tabsData.value[0].id | ||
| 242 | + console.log('[Search] 初始化完成,默认选中:', tabsData.value[0].name) | ||
| 243 | + console.log('[Search] 全部分类数据量:', tabsData.value[0].list.length) | ||
| 244 | + console.log('[Search] 产品分类数据量:', tabsData.value[1].list.length) | ||
| 245 | + console.log('[Search] 资料分类数据量:', tabsData.value[2].list.length) | ||
| 246 | + } | ||
| 247 | +} | ||
| 198 | 248 | ||
| 199 | // Search results | 249 | // Search results |
| 200 | const searchResults = computed(() => { | 250 | const searchResults = computed(() => { |
| 201 | if (!hasSearched.value) return [] | 251 | if (!hasSearched.value) return [] |
| 202 | 252 | ||
| 203 | - let results = mockData.value | 253 | + // 找到当前选中的 tab |
| 254 | + const currentTab = tabsData.value.find(tab => tab.id === activeTabId.value) | ||
| 255 | + console.log('[Search Results] activeTabId:', activeTabId.value) | ||
| 256 | + console.log('[Search Results] currentTab:', currentTab) | ||
| 257 | + console.log('[Search Results] currentTab.list.length:', currentTab?.list?.length || 0) | ||
| 204 | 258 | ||
| 205 | - // Filter by tab | 259 | + if (!currentTab) return [] |
| 206 | - if (activeTab.value === 1) { | 260 | + |
| 207 | - results = results.filter(item => item.category === 'product') | 261 | + let results = currentTab.list |
| 208 | - } else if (activeTab.value === 2) { | ||
| 209 | - results = results.filter(item => item.category === 'material') | ||
| 210 | - } | ||
| 211 | 262 | ||
| 212 | // Filter by keyword | 263 | // Filter by keyword |
| 213 | if (searchKeyword.value.trim()) { | 264 | if (searchKeyword.value.trim()) { |
| 214 | const keyword = searchKeyword.value.toLowerCase() | 265 | const keyword = searchKeyword.value.toLowerCase() |
| 266 | + console.log('[Search Results] 搜索关键词:', keyword) | ||
| 267 | + console.log('[Search Results] 过滤前数量:', results.length) | ||
| 268 | + | ||
| 215 | results = results.filter(item => | 269 | results = results.filter(item => |
| 216 | item.title.toLowerCase().includes(keyword) | 270 | item.title.toLowerCase().includes(keyword) |
| 217 | ) | 271 | ) |
| 272 | + | ||
| 273 | + console.log('[Search Results] 过滤后数量:', results.length) | ||
| 218 | } | 274 | } |
| 219 | 275 | ||
| 220 | return results | 276 | return results |
| 221 | }) | 277 | }) |
| 222 | 278 | ||
| 279 | +/** | ||
| 280 | + * Tab 点击处理 | ||
| 281 | + */ | ||
| 282 | +const onTabClick = (id) => { | ||
| 283 | + activeTabId.value = id | ||
| 284 | + listRenderKey.value += 1 | ||
| 285 | + // 自动触发搜索(如果已经搜索过) | ||
| 286 | + if (hasSearched.value) { | ||
| 287 | + console.log('[Search] 切换分类到:', id, '结果数量:', searchResults.value.length) | ||
| 288 | + } | ||
| 289 | +} | ||
| 290 | + | ||
| 223 | // Handle search | 291 | // Handle search |
| 224 | const handleSearch = () => { | 292 | const handleSearch = () => { |
| 293 | + console.log('[Search handleSearch] 被调用') | ||
| 294 | + console.log('[Search handleSearch] searchKeyword:', searchKeyword.value) | ||
| 295 | + | ||
| 225 | if (searchKeyword.value.trim()) { | 296 | if (searchKeyword.value.trim()) { |
| 226 | hasSearched.value = true | 297 | hasSearched.value = true |
| 298 | + console.log('[Search handleSearch] hasSearched 已设置为 true') | ||
| 299 | + console.log('[Search handleSearch] 搜索关键词:', searchKeyword.value) | ||
| 300 | + console.log('[Search handleSearch] 当前分类:', activeTabId.value) | ||
| 301 | + console.log('[Search handleSearch] 搜索结果数量:', searchResults.value.length) | ||
| 302 | + } else { | ||
| 303 | + console.log('[Search handleSearch] 搜索关键词为空,不执行搜索') | ||
| 227 | } | 304 | } |
| 228 | } | 305 | } |
| 229 | 306 | ||
| 307 | +// Handle blur | ||
| 308 | +const handleBlur = () => { | ||
| 309 | + console.log('[Search handleBlur] 搜索框失去焦点') | ||
| 310 | + // 可以在这里添加一些失去焦点时的逻辑,比如: | ||
| 311 | + // - 收起键盘 | ||
| 312 | + // - 记录搜索日志 | ||
| 313 | + // - 其他 UI 状态更新 | ||
| 314 | +} | ||
| 315 | + | ||
| 230 | // Clear search | 316 | // Clear search |
| 231 | const clearSearch = () => { | 317 | const clearSearch = () => { |
| 232 | searchKeyword.value = '' | 318 | searchKeyword.value = '' |
| 233 | hasSearched.value = false | 319 | hasSearched.value = false |
| 320 | + listRenderKey.value += 1 | ||
| 234 | } | 321 | } |
| 235 | 322 | ||
| 236 | // Go to detail | 323 | // Go to detail |
| ... | @@ -238,7 +325,7 @@ const goToDetail = (item) => { | ... | @@ -238,7 +325,7 @@ const goToDetail = (item) => { |
| 238 | if (item.category === 'product') { | 325 | if (item.category === 'product') { |
| 239 | go('/pages/knowledge-base/index') | 326 | go('/pages/knowledge-base/index') |
| 240 | } else { | 327 | } else { |
| 241 | - go('/pages/knowledge-base/index') | 328 | + go('/pages/material-list/index', { title: '搜索结果' }) |
| 242 | } | 329 | } |
| 243 | 330 | ||
| 244 | Taro.showToast({ | 331 | Taro.showToast({ |
| ... | @@ -247,15 +334,95 @@ const goToDetail = (item) => { | ... | @@ -247,15 +334,95 @@ const goToDetail = (item) => { |
| 247 | duration: 1500 | 334 | duration: 1500 |
| 248 | }) | 335 | }) |
| 249 | } | 336 | } |
| 337 | + | ||
| 338 | +// 初始化数据 | ||
| 339 | +initTabsData() | ||
| 340 | + | ||
| 341 | +/** | ||
| 342 | + * 监听搜索关键词变化,实现实时搜索 | ||
| 343 | + */ | ||
| 344 | +watch(searchKeyword, (newVal) => { | ||
| 345 | + if (newVal.trim()) { | ||
| 346 | + hasSearched.value = true | ||
| 347 | + console.log('[Search Watch] 实时搜索触发,关键词:', newVal) | ||
| 348 | + console.log('[Search Watch] 当前分类:', activeTabId.value) | ||
| 349 | + console.log('[Search Watch] 搜索结果数量:', searchResults.value.length) | ||
| 350 | + } else { | ||
| 351 | + // 清空搜索关键词时,也清空搜索状态 | ||
| 352 | + hasSearched.value = false | ||
| 353 | + } | ||
| 354 | +}) | ||
| 250 | </script> | 355 | </script> |
| 251 | 356 | ||
| 252 | -<style> | 357 | +<style lang="less"> |
| 253 | -.no-scrollbar::-webkit-scrollbar { | 358 | +@keyframes slideIn { |
| 254 | - display: none; | 359 | + from { |
| 360 | + opacity: 0; | ||
| 361 | + transform: translateY(20rpx); | ||
| 362 | + } | ||
| 363 | + | ||
| 364 | + to { | ||
| 365 | + opacity: 1; | ||
| 366 | + transform: translateY(0); | ||
| 367 | + } | ||
| 255 | } | 368 | } |
| 256 | 369 | ||
| 257 | -.no-scrollbar { | 370 | +.search-result-item { |
| 371 | + animation: slideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1) backwards; | ||
| 372 | +} | ||
| 373 | + | ||
| 374 | +// FilterTabs 风格的标签栏 | ||
| 375 | +.filter-tabs-wrapper { | ||
| 376 | + display: flex; | ||
| 377 | + overflow-x: auto; | ||
| 378 | + padding: 0; | ||
| 379 | + gap: 24rpx; | ||
| 380 | + transition: all 0.3s ease; | ||
| 381 | + | ||
| 382 | + // 隐藏滚动条 | ||
| 383 | + &::-webkit-scrollbar { | ||
| 384 | + display: none; | ||
| 385 | + width: 0; | ||
| 386 | + height: 0; | ||
| 387 | + } | ||
| 388 | + | ||
| 258 | -ms-overflow-style: none; | 389 | -ms-overflow-style: none; |
| 259 | scrollbar-width: none; | 390 | scrollbar-width: none; |
| 260 | } | 391 | } |
| 392 | + | ||
| 393 | +.filter-tab-item { | ||
| 394 | + display: flex; | ||
| 395 | + align-items: center; | ||
| 396 | + justify-content: center; | ||
| 397 | + padding: 0 32rpx; | ||
| 398 | + border-radius: 9999rpx; | ||
| 399 | + white-space: nowrap; | ||
| 400 | + transition: all 0.3s ease; | ||
| 401 | + flex-shrink: 0; | ||
| 402 | + height: 64rpx; | ||
| 403 | +} | ||
| 404 | + | ||
| 405 | +.filter-tab-active { | ||
| 406 | + background-color: #2563EB; // 蓝色背景 | ||
| 407 | + color: #fff; | ||
| 408 | +} | ||
| 409 | + | ||
| 410 | +.filter-tab-inactive { | ||
| 411 | + background-color: #F3F4F6; // 灰色背景 | ||
| 412 | + color: #6B7280; | ||
| 413 | +} | ||
| 414 | + | ||
| 415 | +.filter-tab-text { | ||
| 416 | + font-size: 28rpx; | ||
| 417 | + font-weight: 500; | ||
| 418 | +} | ||
| 419 | + | ||
| 420 | +// 覆盖 NutUI Tabs 默认样式,隐藏原有的头部和内容(因为我们使用自定义头部和外部列表) | ||
| 421 | +:deep(.nut-tabs__titles) { | ||
| 422 | + display: none; | ||
| 423 | +} | ||
| 424 | + | ||
| 425 | +:deep(.nut-tabs__content) { | ||
| 426 | + display: none; | ||
| 427 | +} | ||
| 261 | </style> | 428 | </style> | ... | ... |
-
Please register or login to post a comment