feat(components): 添加可复用的 SearchBar 组件并替换多个页面中的搜索框
将多个页面中的重复搜索框代码提取为统一的 SearchBar 组件,提高代码复用性和维护性。该组件支持多种样式变体、清除按钮、边框等配置选项,并保持与原有功能的一致性。
Showing
6 changed files
with
316 additions
and
27 deletions
| ... | @@ -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'] | ... | ... |
src/components/SearchBar.vue
0 → 100644
| 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() | ... | ... |
-
Please register or login to post a comment