hookehuyr

refactor(components): 重构 SearchBar 组件并更新搜索页面

- 将 SearchBar 组件从原生 input 重构为 NutUI Input 实现,简化样式和逻辑
- 更新 components.d.ts 类型声明,移除未使用的 NutConfigProvider,修正 PlanPopup 导入路径
- 重构搜索页面,使用 NutTabs 实现分类切换,支持长列表测试数据
- 为搜索结果项添加动画效果,优化用户体验
...@@ -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>
......