hookehuyr

feat(components): 添加可复用的 SearchBar 组件并替换多个页面中的搜索框

将多个页面中的重复搜索框代码提取为统一的 SearchBar 组件,提高代码复用性和维护性。该组件支持多种样式变体、清除按钮、边框等配置选项,并保持与原有功能的一致性。
...@@ -36,6 +36,7 @@ declare module 'vue' { ...@@ -36,6 +36,7 @@ declare module 'vue' {
36 RouterView: typeof import('vue-router')['RouterView'] 36 RouterView: typeof import('vue-router')['RouterView']
37 SchemeA: typeof import('./src/components/PlanSchemes/SchemeA.vue')['default'] 37 SchemeA: typeof import('./src/components/PlanSchemes/SchemeA.vue')['default']
38 SchemeB: typeof import('./src/components/PlanSchemes/SchemeB.vue')['default'] 38 SchemeB: typeof import('./src/components/PlanSchemes/SchemeB.vue')['default']
39 + SearchBar: typeof import('./src/components/SearchBar.vue')['default']
39 SectionCard: typeof import('./src/components/SectionCard.vue')['default'] 40 SectionCard: typeof import('./src/components/SectionCard.vue')['default']
40 SectionItem: typeof import('./src/components/SectionItem.vue')['default'] 41 SectionItem: typeof import('./src/components/SectionItem.vue')['default']
41 TabBar: typeof import('./src/components/TabBar.vue')['default'] 42 TabBar: typeof import('./src/components/TabBar.vue')['default']
......
1 +<template>
2 + <view
3 + class="search-bar flex items-center"
4 + :class="containerClass"
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"
17 + :type="inputType"
18 + :placeholder="placeholder"
19 + :placeholder-class="placeholderClass"
20 + :class="inputClass"
21 + :disabled="disabled"
22 + @focus="handleFocus"
23 + @blur="handleBlur"
24 + @input="handleInput"
25 + @confirm="handleSearch"
26 + />
27 +
28 + <!-- Clear Button -->
29 + <IconFont
30 + v-if="showClear && internalValue"
31 + name="close"
32 + :size="clearIconSize"
33 + :color="clearIconColor"
34 + class="flex-shrink-0 ml-[16rpx]"
35 + @tap="handleClear"
36 + />
37 + </view>
38 +</template>
39 +
40 +<script setup>
41 +import { ref, watch, computed } from 'vue'
42 +import IconFont from '@/components/IconFont.vue'
43 +
44 +/**
45 + * SearchBar 组件
46 + *
47 + * @description 可复用的搜索栏组件,支持多种样式变体
48 + * @author Claude Code
49 + * @example
50 + * <SearchBar
51 + * v-model="searchValue"
52 + * placeholder="搜索资料..."
53 + * variant="rounded"
54 + * @search="handleSearch"
55 + * />
56 + */
57 +
58 +const props = defineProps({
59 + /**
60 + * 绑定值
61 + * @type {string|number}
62 + */
63 + modelValue: {
64 + type: [String, Number],
65 + default: ''
66 + },
67 + /**
68 + * 占位文本
69 + * @type {string}
70 + * @default '搜索...'
71 + */
72 + placeholder: {
73 + type: String,
74 + default: '搜索...'
75 + },
76 + /**
77 + * 样式变体
78 + * @type {'normal' | 'rounded'}
79 + * @default 'normal'
80 + */
81 + variant: {
82 + type: String,
83 + default: 'normal',
84 + validator: (value) => ['normal', 'rounded'].includes(value)
85 + },
86 + /**
87 + * 是否显示边框
88 + * @type {boolean}
89 + * @default false
90 + */
91 + showBorder: {
92 + type: Boolean,
93 + default: false
94 + },
95 + /**
96 + * 是否显示清除按钮
97 + * @type {boolean}
98 + * @default false
99 + */
100 + showClear: {
101 + type: Boolean,
102 + default: false
103 + },
104 + /**
105 + * 是否禁用
106 + * @type {boolean}
107 + * @default false
108 + */
109 + disabled: {
110 + type: Boolean,
111 + default: false
112 + },
113 + /**
114 + * 输入框类型
115 + * @type {string}
116 + * @default 'text'
117 + */
118 + inputType: {
119 + type: String,
120 + default: 'text'
121 + },
122 + /**
123 + * 图标大小
124 + * @type {number|string}
125 + * @default 18
126 + */
127 + iconSize: {
128 + type: [Number, String],
129 + default: 18
130 + },
131 + /**
132 + * 图标颜色
133 + * @type {string}
134 + * @default '#9CA3AF'
135 + */
136 + iconColor: {
137 + type: String,
138 + 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 + }
158 +})
159 +
160 +const emit = defineEmits({
161 + /**
162 + * 更新绑定值
163 + * @event update:modelValue
164 + * @param {string|number} value - 新值
165 + */
166 + 'update:modelValue': (value) => true,
167 + /**
168 + * 搜索事件(按下回车)
169 + * @event search
170 + * @param {string|number} value - 当前值
171 + */
172 + search: (value) => true,
173 + /**
174 + * 输入事件
175 + * @event input
176 + * @param {string|number} value - 当前值
177 + */
178 + input: (value) => true,
179 + /**
180 + * 获得焦点
181 + * @event focus
182 + */
183 + focus: () => true,
184 + /**
185 + * 失去焦点
186 + * @event blur
187 + */
188 + blur: () => true,
189 + /**
190 + * 清除
191 + * @event clear
192 + */
193 + clear: () => true
194 +})
195 +
196 +// 内部值
197 +const internalValue = ref(props.modelValue)
198 +
199 +// 容器样式类
200 +const containerClass = computed(() => {
201 + const base = [
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 +
214 + // 高度样式
215 + if (props.variant === 'rounded') {
216 + base.push('h-[88rpx]')
217 + } else {
218 + base.push('py-[24rpx]')
219 + }
220 +
221 + return base.join(' ')
222 +})
223 +
224 +// 占位符样式类
225 +const placeholderClass = 'text-gray-400 text-[28rpx]'
226 +
227 +// 输入框样式类
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 +})
237 +
238 +// 监听 modelValue 变化
239 +watch(() => props.modelValue, (newValue) => {
240 + internalValue.value = newValue
241 +})
242 +
243 +// 监听内部值变化,触发更新
244 +watch(internalValue, (newValue) => {
245 + emit('update:modelValue', newValue)
246 +})
247 +
248 +/**
249 + * 处理获得焦点
250 + */
251 +function handleFocus() {
252 + emit('focus')
253 +}
254 +
255 +/**
256 + * 处理失去焦点
257 + */
258 +function handleBlur() {
259 + emit('blur')
260 +}
261 +
262 +/**
263 + * 处理输入
264 + */
265 +function handleInput(e) {
266 + emit('input', internalValue.value)
267 +}
268 +
269 +/**
270 + * 处理搜索(回车)
271 + */
272 +function handleSearch() {
273 + emit('search', internalValue.value)
274 +}
275 +
276 +/**
277 + * 清除输入
278 + */
279 +function handleClear() {
280 + internalValue.value = ''
281 + emit('clear')
282 + emit('update:modelValue', '')
283 +}
284 +</script>
285 +
286 +<style lang="less" scoped>
287 +/* 样式通过 TailwindCSS 类控制 */
288 +</style>
...@@ -5,10 +5,11 @@ ...@@ -5,10 +5,11 @@
5 <!-- Content Section --> 5 <!-- Content Section -->
6 <view class="px-[32rpx] mt-[32rpx]"> 6 <view class="px-[32rpx] mt-[32rpx]">
7 <!-- Search Bar --> 7 <!-- Search Bar -->
8 - <view class="flex items-center w-full h-[88rpx] bg-white rounded-full px-[32rpx] mb-[32rpx] shadow-sm"> 8 + <view class="mb-[32rpx]">
9 - <IconFont name="search" class="text-gray-400 mr-[16rpx]" size="18" /> 9 + <SearchBar
10 - <input type="text" placeholder="搜索问题或关键词" placeholder-class="text-gray-400 text-[28rpx]" 10 + placeholder="搜索问题或关键词"
11 - class="flex-1 text-[28rpx] text-gray-800 h-full" /> 11 + variant="rounded"
12 + />
12 </view> 13 </view>
13 14
14 <!-- Contact Service --> 15 <!-- Contact Service -->
...@@ -121,6 +122,7 @@ import { ref } from 'vue' ...@@ -121,6 +122,7 @@ import { ref } from 'vue'
121 import NavHeader from '@/components/NavHeader.vue' 122 import NavHeader from '@/components/NavHeader.vue'
122 import TabBar from '@/components/TabBar.vue' 123 import TabBar from '@/components/TabBar.vue'
123 import IconFont from '@/components/IconFont.vue' 124 import IconFont from '@/components/IconFont.vue'
125 +import SearchBar from '@/components/SearchBar.vue'
124 126
125 // Popup 状态 127 // Popup 状态
126 const showContactPopup = ref(false) 128 const showContactPopup = ref(false)
......
...@@ -8,12 +8,11 @@ ...@@ -8,12 +8,11 @@
8 <NavHeader :title="pageTitle" /> 8 <NavHeader :title="pageTitle" />
9 9
10 <view class="px-[32rpx] mt-[32rpx]"> 10 <view class="px-[32rpx] mt-[32rpx]">
11 - <view class="bg-white rounded-[20rpx] flex items-center px-[32rpx] py-[24rpx] shadow-sm border border-gray-50"> 11 + <SearchBar
12 - <IconFont name="search" size="20" color="#9CA3AF" class="mr-[16rpx]" /> 12 + v-model="searchValue"
13 - <input v-model="searchValue" type="text" placeholder="搜索资料..." 13 + placeholder="搜索资料..."
14 - class="flex-1 text-[28rpx] text-[#1F2937] placeholder-gray-400 bg-transparent outline-none" 14 + @search="onSearch"
15 - @confirm="onSearch" /> 15 + />
16 - </view>
17 </view> 16 </view>
18 17
19 <view v-if="categories && categories.length > 0" class="px-[32rpx] mt-[32rpx]"> 18 <view v-if="categories && categories.length > 0" class="px-[32rpx] mt-[32rpx]">
...@@ -84,7 +83,7 @@ import { ref, computed } from 'vue' ...@@ -84,7 +83,7 @@ import { ref, computed } from 'vue'
84 import { useLoad } from '@tarojs/taro' 83 import { useLoad } from '@tarojs/taro'
85 import NavHeader from '@/components/NavHeader.vue' 84 import NavHeader from '@/components/NavHeader.vue'
86 import TabBar from '@/components/TabBar.vue' 85 import TabBar from '@/components/TabBar.vue'
87 -import IconFont from '@/components/IconFont.vue' 86 +import SearchBar from '@/components/SearchBar.vue'
88 import ListItemActions from '@/components/ListItemActions/index.vue' 87 import ListItemActions from '@/components/ListItemActions/index.vue'
89 import { useListItemClick, ListType } from '@/composables/useListItemClick' 88 import { useListItemClick, ListType } from '@/composables/useListItemClick'
90 import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons' 89 import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons'
......
...@@ -6,11 +6,12 @@ ...@@ -6,11 +6,12 @@
6 6
7 <!-- Search Bar --> 7 <!-- Search Bar -->
8 <view class="bg-white px-[24rpx] py-[16rpx]"> 8 <view class="bg-white px-[24rpx] py-[16rpx]">
9 - <nut-searchbar v-model="searchValue" placeholder="搜索计划书名称、客户姓名..." @search="onSearch" clearable> 9 + <SearchBar
10 - <template #left-in> 10 + v-model="searchValue"
11 - <IconFont name="search" size="14" /> 11 + placeholder="搜索计划书名称、客户姓名..."
12 - </template> 12 + :show-clear="true"
13 - </nut-searchbar> 13 + @search="onSearch"
14 + />
14 </view> 15 </view>
15 16
16 <!-- Tabs --> 17 <!-- Tabs -->
...@@ -71,15 +72,14 @@ ...@@ -71,15 +72,14 @@
71 72
72 <script setup> 73 <script setup>
73 import { ref, computed } from 'vue' 74 import { ref, computed } from 'vue'
74 -import { useGo } from '@/hooks/useGo'
75 import { useFileOperation } from '@/composables/useFileOperation' 75 import { useFileOperation } from '@/composables/useFileOperation'
76 import IconFont from '@/components/IconFont.vue' 76 import IconFont from '@/components/IconFont.vue'
77 import FilterTabs from '@/components/FilterTabs.vue' 77 import FilterTabs from '@/components/FilterTabs.vue'
78 import NavHeader from '@/components/NavHeader.vue' 78 import NavHeader from '@/components/NavHeader.vue'
79 import ListItemActions from '@/components/ListItemActions/index.vue' 79 import ListItemActions from '@/components/ListItemActions/index.vue'
80 +import SearchBar from '@/components/SearchBar.vue'
80 import Taro from '@tarojs/taro' 81 import Taro from '@tarojs/taro'
81 82
82 -const go = useGo()
83 const { viewFile } = useFileOperation() 83 const { viewFile } = useFileOperation()
84 84
85 const searchValue = ref('') 85 const searchValue = ref('')
......
...@@ -6,18 +6,16 @@ ...@@ -6,18 +6,16 @@
6 <!-- Content Area --> 6 <!-- Content Area -->
7 <view class="px-[40rpx] mt-[40rpx]"> 7 <view class="px-[40rpx] mt-[40rpx]">
8 <!-- Search Input --> 8 <!-- Search Input -->
9 - <view class="flex items-center w-full h-[88rpx] bg-white rounded-full px-[32rpx] border border-gray-200 mb-[40rpx]"> 9 + <view class="mb-[40rpx]">
10 - <IconFont name="search" class="text-gray-400 mr-[16rpx]" size="18" /> 10 + <SearchBar
11 - <input
12 v-model="searchKeyword" 11 v-model="searchKeyword"
13 - type="text"
14 placeholder="搜索培训资料、案例、产品..." 12 placeholder="搜索培训资料、案例、产品..."
15 - class="flex-1 text-[28rpx] text-gray-800 placeholder-gray-400" 13 + variant="rounded"
16 - @confirm="handleSearch" 14 + :show-border="true"
15 + :show-clear="true"
16 + @search="handleSearch"
17 + @clear="clearSearch"
17 /> 18 />
18 - <view v-if="searchKeyword" class="ml-[16rpx]" @tap="clearSearch">
19 - <IconFont name="close" class="text-gray-400" size="16" />
20 - </view>
21 </view> 19 </view>
22 20
23 <!-- Filter Tabs --> 21 <!-- Filter Tabs -->
...@@ -114,6 +112,7 @@ import Taro from '@tarojs/taro' ...@@ -114,6 +112,7 @@ import Taro from '@tarojs/taro'
114 import { useGo } from '@/hooks/useGo' 112 import { useGo } from '@/hooks/useGo'
115 import NavHeader from '@/components/NavHeader.vue' 113 import NavHeader from '@/components/NavHeader.vue'
116 import IconFont from '@/components/IconFont.vue' 114 import IconFont from '@/components/IconFont.vue'
115 +import SearchBar from '@/components/SearchBar.vue'
117 116
118 // Navigation 117 // Navigation
119 const go = useGo() 118 const go = useGo()
......