hookehuyr

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

将多个页面中的重复搜索框代码提取为统一的 SearchBar 组件,提高代码复用性和维护性。该组件支持多种样式变体、清除按钮、边框等配置选项,并保持与原有功能的一致性。
......@@ -36,6 +36,7 @@ declare module 'vue' {
RouterView: typeof import('vue-router')['RouterView']
SchemeA: typeof import('./src/components/PlanSchemes/SchemeA.vue')['default']
SchemeB: typeof import('./src/components/PlanSchemes/SchemeB.vue')['default']
SearchBar: typeof import('./src/components/SearchBar.vue')['default']
SectionCard: typeof import('./src/components/SectionCard.vue')['default']
SectionItem: typeof import('./src/components/SectionItem.vue')['default']
TabBar: typeof import('./src/components/TabBar.vue')['default']
......
<template>
<view
class="search-bar flex items-center"
:class="containerClass"
>
<!-- Search Icon -->
<IconFont
name="search"
:size="iconSize"
:color="iconColor"
class="flex-shrink-0 mr-[16rpx]"
/>
<!-- Input -->
<input
v-model="internalValue"
:type="inputType"
:placeholder="placeholder"
:placeholder-class="placeholderClass"
:class="inputClass"
:disabled="disabled"
@focus="handleFocus"
@blur="handleBlur"
@input="handleInput"
@confirm="handleSearch"
/>
<!-- Clear Button -->
<IconFont
v-if="showClear && internalValue"
name="close"
:size="clearIconSize"
:color="clearIconColor"
class="flex-shrink-0 ml-[16rpx]"
@tap="handleClear"
/>
</view>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
import IconFont from '@/components/IconFont.vue'
/**
* SearchBar 组件
*
* @description 可复用的搜索栏组件,支持多种样式变体
* @author Claude Code
* @example
* <SearchBar
* v-model="searchValue"
* placeholder="搜索资料..."
* variant="rounded"
* @search="handleSearch"
* />
*/
const props = defineProps({
/**
* 绑定值
* @type {string|number}
*/
modelValue: {
type: [String, Number],
default: ''
},
/**
* 占位文本
* @type {string}
* @default '搜索...'
*/
placeholder: {
type: String,
default: '搜索...'
},
/**
* 样式变体
* @type {'normal' | 'rounded'}
* @default 'normal'
*/
variant: {
type: String,
default: 'normal',
validator: (value) => ['normal', 'rounded'].includes(value)
},
/**
* 是否显示边框
* @type {boolean}
* @default false
*/
showBorder: {
type: Boolean,
default: false
},
/**
* 是否显示清除按钮
* @type {boolean}
* @default false
*/
showClear: {
type: Boolean,
default: false
},
/**
* 是否禁用
* @type {boolean}
* @default false
*/
disabled: {
type: Boolean,
default: false
},
/**
* 输入框类型
* @type {string}
* @default 'text'
*/
inputType: {
type: String,
default: 'text'
},
/**
* 图标大小
* @type {number|string}
* @default 18
*/
iconSize: {
type: [Number, String],
default: 18
},
/**
* 图标颜色
* @type {string}
* @default '#9CA3AF'
*/
iconColor: {
type: String,
default: '#9CA3AF'
},
/**
* 清除图标大小
* @type {number|string}
* @default 16
*/
clearIconSize: {
type: [Number, String],
default: 16
},
/**
* 清除图标颜色
* @type {string}
* @default '#9CA3AF'
*/
clearIconColor: {
type: String,
default: '#9CA3AF'
}
})
const emit = defineEmits({
/**
* 更新绑定值
* @event update:modelValue
* @param {string|number} value - 新值
*/
'update:modelValue': (value) => true,
/**
* 搜索事件(按下回车)
* @event search
* @param {string|number} value - 当前值
*/
search: (value) => true,
/**
* 输入事件
* @event input
* @param {string|number} value - 当前值
*/
input: (value) => true,
/**
* 获得焦点
* @event focus
*/
focus: () => true,
/**
* 失去焦点
* @event blur
*/
blur: () => true,
/**
* 清除
* @event clear
*/
clear: () => true
})
// 内部值
const internalValue = ref(props.modelValue)
// 容器样式类
const containerClass = computed(() => {
const base = [
'bg-white',
'shadow-sm',
'px-[40rpx]', // 左右 padding
props.variant === 'rounded' ? 'rounded-full' : 'rounded-[20rpx]'
]
if (props.showBorder) {
base.push('border', 'border-gray-200')
} else {
base.push('border', 'border-gray-50')
}
// 高度样式
if (props.variant === 'rounded') {
base.push('h-[88rpx]')
} else {
base.push('py-[24rpx]')
}
return base.join(' ')
})
// 占位符样式类
const placeholderClass = 'text-gray-400 text-[28rpx]'
// 输入框样式类
const inputClass = computed(() => {
return [
'flex-1',
'text-[28rpx]',
'bg-transparent',
'outline-none',
props.disabled ? 'opacity-50' : ''
].filter(Boolean).join(' ')
})
// 监听 modelValue 变化
watch(() => props.modelValue, (newValue) => {
internalValue.value = newValue
})
// 监听内部值变化,触发更新
watch(internalValue, (newValue) => {
emit('update:modelValue', newValue)
})
/**
* 处理获得焦点
*/
function handleFocus() {
emit('focus')
}
/**
* 处理失去焦点
*/
function handleBlur() {
emit('blur')
}
/**
* 处理输入
*/
function handleInput(e) {
emit('input', internalValue.value)
}
/**
* 处理搜索(回车)
*/
function handleSearch() {
emit('search', internalValue.value)
}
/**
* 清除输入
*/
function handleClear() {
internalValue.value = ''
emit('clear')
emit('update:modelValue', '')
}
</script>
<style lang="less" scoped>
/* 样式通过 TailwindCSS 类控制 */
</style>
......@@ -5,10 +5,11 @@
<!-- Content Section -->
<view class="px-[32rpx] mt-[32rpx]">
<!-- Search Bar -->
<view class="flex items-center w-full h-[88rpx] bg-white rounded-full px-[32rpx] mb-[32rpx] shadow-sm">
<IconFont name="search" class="text-gray-400 mr-[16rpx]" size="18" />
<input type="text" placeholder="搜索问题或关键词" placeholder-class="text-gray-400 text-[28rpx]"
class="flex-1 text-[28rpx] text-gray-800 h-full" />
<view class="mb-[32rpx]">
<SearchBar
placeholder="搜索问题或关键词"
variant="rounded"
/>
</view>
<!-- Contact Service -->
......@@ -121,6 +122,7 @@ import { ref } from 'vue'
import NavHeader from '@/components/NavHeader.vue'
import TabBar from '@/components/TabBar.vue'
import IconFont from '@/components/IconFont.vue'
import SearchBar from '@/components/SearchBar.vue'
// Popup 状态
const showContactPopup = ref(false)
......
......@@ -8,12 +8,11 @@
<NavHeader :title="pageTitle" />
<view class="px-[32rpx] mt-[32rpx]">
<view class="bg-white rounded-[20rpx] flex items-center px-[32rpx] py-[24rpx] shadow-sm border border-gray-50">
<IconFont name="search" size="20" color="#9CA3AF" class="mr-[16rpx]" />
<input v-model="searchValue" type="text" placeholder="搜索资料..."
class="flex-1 text-[28rpx] text-[#1F2937] placeholder-gray-400 bg-transparent outline-none"
@confirm="onSearch" />
</view>
<SearchBar
v-model="searchValue"
placeholder="搜索资料..."
@search="onSearch"
/>
</view>
<view v-if="categories && categories.length > 0" class="px-[32rpx] mt-[32rpx]">
......@@ -84,7 +83,7 @@ import { ref, computed } from 'vue'
import { useLoad } from '@tarojs/taro'
import NavHeader from '@/components/NavHeader.vue'
import TabBar from '@/components/TabBar.vue'
import IconFont from '@/components/IconFont.vue'
import SearchBar from '@/components/SearchBar.vue'
import ListItemActions from '@/components/ListItemActions/index.vue'
import { useListItemClick, ListType } from '@/composables/useListItemClick'
import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons'
......
......@@ -6,11 +6,12 @@
<!-- Search Bar -->
<view class="bg-white px-[24rpx] py-[16rpx]">
<nut-searchbar v-model="searchValue" placeholder="搜索计划书名称、客户姓名..." @search="onSearch" clearable>
<template #left-in>
<IconFont name="search" size="14" />
</template>
</nut-searchbar>
<SearchBar
v-model="searchValue"
placeholder="搜索计划书名称、客户姓名..."
:show-clear="true"
@search="onSearch"
/>
</view>
<!-- Tabs -->
......@@ -71,15 +72,14 @@
<script setup>
import { ref, computed } from 'vue'
import { useGo } from '@/hooks/useGo'
import { useFileOperation } from '@/composables/useFileOperation'
import IconFont from '@/components/IconFont.vue'
import FilterTabs from '@/components/FilterTabs.vue'
import NavHeader from '@/components/NavHeader.vue'
import ListItemActions from '@/components/ListItemActions/index.vue'
import SearchBar from '@/components/SearchBar.vue'
import Taro from '@tarojs/taro'
const go = useGo()
const { viewFile } = useFileOperation()
const searchValue = ref('')
......
......@@ -6,18 +6,16 @@
<!-- Content Area -->
<view class="px-[40rpx] mt-[40rpx]">
<!-- Search Input -->
<view class="flex items-center w-full h-[88rpx] bg-white rounded-full px-[32rpx] border border-gray-200 mb-[40rpx]">
<IconFont name="search" class="text-gray-400 mr-[16rpx]" size="18" />
<input
<view class="mb-[40rpx]">
<SearchBar
v-model="searchKeyword"
type="text"
placeholder="搜索培训资料、案例、产品..."
class="flex-1 text-[28rpx] text-gray-800 placeholder-gray-400"
@confirm="handleSearch"
variant="rounded"
:show-border="true"
:show-clear="true"
@search="handleSearch"
@clear="clearSearch"
/>
<view v-if="searchKeyword" class="ml-[16rpx]" @tap="clearSearch">
<IconFont name="close" class="text-gray-400" size="16" />
</view>
</view>
<!-- Filter Tabs -->
......@@ -114,6 +112,7 @@ import Taro from '@tarojs/taro'
import { useGo } from '@/hooks/useGo'
import NavHeader from '@/components/NavHeader.vue'
import IconFont from '@/components/IconFont.vue'
import SearchBar from '@/components/SearchBar.vue'
// Navigation
const go = useGo()
......