SearchBar.vue 4.96 KB
<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>