Vue3搜索框(InputSearch)

简介: 这篇文章介绍了如何在Vue 3中创建一个具有搜索、清除、加载状态等多功能的搜索框组件(InputSearch),并提供了组件的配置选项、事件处理和使用示例。

效果如下图:在线预览

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

APIs

InputSearch

参数 说明 类型 默认值
width 搜索框宽度,单位 px string / number ‘100%’
icon 搜索图标 boolean / slot true
search 搜索按钮,默认时为搜索图标 string / slot undefined
searchProps 设置搜索按钮的属性,参考 Button Props object {}
size 搜索框大小 ‘small’ / ‘middle’ / ‘large’ ‘middle’
addonBefore 设置前置标签 string / slot undefined
prefix 前缀图标 string undefined
suffix 后缀图标 string undefined
allowClear 可以点击清除图标删除搜索框内容 boolean false
loading 是否搜索中 boolean false
disabled 是否禁用 boolean false
maxlength 文本最大长度 number undefined
showCount 是否展示字数 boolean false
value v-model 搜索框内容 string undefined

Events

名称 说明 类型
change 搜索框内容变化时的回调 (e: Event) => void
enter 点击搜索或按下回车键时的回调 (value: string) => void

创建搜索框组件InputSearch.vue

其中引入使用了以下组件和工具函数:

<script setup lang="ts">
defineOptions({
  inheritAttrs: false
})
import { ref, computed, nextTick } from 'vue'
import Button from '../button'
import { useSlotsExist } from '../utils'
interface Props {
  width?: string | number // 搜索框宽度,单位 px
  icon?: boolean // 搜索图标 boolean | slot
  search?: string // 搜索按钮,默认时为搜索图标 string | slot
  searchProps?: object // 设置搜索按钮的属性,参考 Button Props
  size?: 'small' | 'middle' | 'large' // 搜索框大小
  allowClear?: boolean // 可以点击清除图标删除搜索框内容
  addonBefore?: string // 设置前置标签 string | slot
  prefix?: string // 前缀图标 string | slot
  suffix?: string // 后缀图标 string | slot
  loading?: boolean // 是否搜索中
  disabled?: boolean // 是否禁用
  maxlength?: number // 文本最大长度
  showCount?: boolean // 是否展示字数
  value?: string // (v-model) 搜索框内容
  valueModifiers?: object // 用于访问组件的 v-model 上添加的修饰符
}
const props = withDefaults(defineProps<Props>(), {
  width: '100%',
  icon: true,
  search: undefined,
  searchProps: () => ({}),
  size: 'middle',
  addonBefore: undefined,
  prefix: undefined,
  suffix: undefined,
  allowClear: false,
  loading: false,
  disabled: false,
  maxlength: undefined,
  showCount: false,
  value: undefined,
  valueModifiers: () => ({})
})
const inputSearchWidth = computed(() => {
  if (typeof props.width === 'number') {
    return props.width + 'px'
  }
  return props.width
})
const showClear = computed(() => {
  return !props.disabled && props.allowClear
})
const showCountNum = computed(() => {
  if (props.maxlength) {
    return (props.value ? props.value.length : 0) + ' / ' + props.maxlength
  }
  return props.value ? props.value.length : 0
})
const slotsExist = useSlotsExist(['prefix', 'suffix', 'addonBefore'])
const showPrefix = computed(() => {
  return slotsExist.prefix || props.prefix
})
const showSuffix = computed(() => {
  return slotsExist.suffix || props.suffix
})
const showInputSuffix = computed(() => {
  return showClear.value || props.showCount || showSuffix.value
})
const showBefore = computed(() => {
  return slotsExist.addonBefore || props.addonBefore
})
const lazyInput = computed(() => {
  return 'lazy' in props.valueModifiers
})
const emits = defineEmits(['update:value', 'change', 'search'])
function onInput(e: InputEvent) {
  if (!lazyInput.value) {
    emits('update:value', (e.target as HTMLInputElement).value)
    emits('change', e)
  }
}
function onChange(e: InputEvent) {
  if (lazyInput.value) {
    emits('update:value', (e.target as HTMLInputElement).value)
    emits('change', e)
  }
}
const input = ref()
function onClear() {
  emits('update:value', '')
  input.value.focus()
}
async function onInputSearch(e: KeyboardEvent) {
  if (!lazyInput.value) {
    onSearch()
  } else {
    if (lazyInput.value) {
      input.value.blur()
      await nextTick()
      input.value.focus()
    }
    emits('search', props.value)
  }
}
function onSearch() {
  emits('search', props.value)
}
</script>
<template>
  <div class="m-input-search-wrap" :style="`width: ${inputSearchWidth};`">
    <span class="m-addon-before" :class="`addon-before-${size}`" v-if="showBefore">
      <slot name="addonBefore">{
  { addonBefore }}</slot>
    </span>
    <div
      tabindex="1"
      class="m-input-search"
      :class="[
        `input-search-${size}`,
        {
          'input-search-before': showBefore,
          'input-search-disabled': disabled
        }
      ]"
    >
      <span class="m-prefix" v-if="showPrefix">
        <slot name="prefix">{
  { prefix }}</slot>
      </span>
      <input
        ref="input"
        class="input-search"
        type="text"
        :value="value"
        :maxlength="maxlength"
        :disabled="disabled"
        @input="onInput"
        @change="onChange"
        @keydown.enter.prevent="onInputSearch"
        v-bind="$attrs"
      />
      <span v-if="showInputSuffix" class="input-search-suffix">
        <span v-if="showClear" class="m-clear" :class="{ 'clear-hidden': !value }" @click="onClear">
          <svg
            class="clear-svg"
            focusable="false"
            data-icon="close-circle"
            width="1em"
            height="1em"
            fill="currentColor"
            aria-hidden="true"
            viewBox="64 64 896 896"
          >
            <path
              d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
            ></path>
          </svg>
        </span>
        <span v-if="showCount" class="input-search-count">{
  { showCountNum }}</span>
        <slot v-if="showSuffix" name="suffix">{
  { suffix }}</slot>
      </span>
    </div>
    <span class="m-search-button" @click="onSearch" @keydown.enter.prevent="onSearch">
      <slot name="search">
        <Button class="search-btn" :size="size" :disabled="disabled" :loading="loading" v-bind="searchProps">
          <template v-if="icon" #icon>
            <svg
              focusable="false"
              data-icon="search"
              width="1em"
              height="1em"
              fill="currentColor"
              aria-hidden="true"
              viewBox="64 64 896 896"
            >
              <path
                d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
              ></path>
            </svg>
          </template>
          {
  { search }}
        </Button>
      </slot>
    </span>
  </div>
</template>
<style lang="less" scoped>
.m-input-search-wrap {
  width: 100%;
  position: relative;
  display: inline-flex;
  align-items: center;
  .m-addon-before {
    display: inline-flex;
    justify-content: center;
    align-items: center;
    position: relative;
    padding: 0 11px;
    color: rgba(0, 0, 0, 0.88);
    font-weight: normal;
    font-size: 14px;
    line-height: 1.5714285714285714;
    text-align: center;
    background-color: rgba(0, 0, 0, 0.02);
    border: 1px solid #d9d9d9;
    border-radius: 6px;
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
    border-right: 0;
    transition: all 0.3s;
    :deep(svg) {
      fill: rgba(0, 0, 0, 0.88);
    }
  }
  .addon-before-small {
    height: 24px;
  }
  .addon-before-middle {
    height: 32px;
  }
  .addon-before-small {
    height: 40px;
  }
  .m-input-search {
    font-size: 14px;
    color: rgba(0, 0, 0, 0.88);
    line-height: 1.5714285714285714;
    position: relative;
    display: inline-flex;
    width: 100%;
    min-width: 0;
    background-color: #ffffff;
    border: 1px solid #d9d9d9;
    transition: all 0.2s;
    &:hover {
      border-color: #4096ff;
      border-right-width: 1px;
      z-index: 1;
    }
    &:focus-within {
      border-color: #4096ff;
      box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
      border-right-width: 1px;
      outline: 0;
      z-index: 1;
    }
    .m-prefix {
      margin-right: 4px;
      display: flex;
      flex: none;
      align-items: center;
      :deep(svg) {
        fill: rgba(0, 0, 0, 0.88);
      }
    }
    .input-search {
      font-size: 14px;
      color: inherit;
      line-height: 1.5714285714285714;
      position: relative;
      display: inline-block;
      width: 100%;
      min-width: 0;
      background-color: #ffffff;
      border: none;
      outline: none;
      text-overflow: ellipsis;
      transition: all 0.2s;
    }
    input::-webkit-input-placeholder {
      color: rgba(0, 0, 0, 0.25);
    }
    input:-moz-placeholder {
      color: rgba(0, 0, 0, 0.25);
    }
    input::-moz-placeholder {
      color: rgba(0, 0, 0, 0.25);
    }
    input:-ms-input-placeholder {
      color: rgba(0, 0, 0, 0.25);
    }
    .input-search-suffix {
      margin-left: 4px;
      display: flex;
      flex: none;
      gap: 8px;
      align-items: center;
      .m-clear {
        cursor: pointer;
        .clear-svg {
          font-size: 12px;
          display: inline-block;
          fill: rgba(0, 0, 0, 0.25);
          text-align: center;
          line-height: 0;
          vertical-align: -0.08em;
          transition: fill 0.3s;
          &:hover {
            fill: rgba(0, 0, 0, 0.45);
          }
        }
      }
      .clear-hidden {
        visibility: hidden;
      }
      .input-search-count {
        color: rgba(0, 0, 0, 0.45);
      }
    }
  }
  .input-search-small {
    height: 24px;
    padding: 0 7px;
    border-radius: 4px;
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
  }
  .input-search-middle {
    height: 32px;
    padding: 4px 11px;
    border-radius: 6px;
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
  }
  .input-search-large {
    height: 40px;
    padding: 7px 11px;
    font-size: 16px;
    line-height: 1.5;
    border-radius: 8px;
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
    .input-search {
      font-size: 16px;
      line-height: 1.5;
    }
  }
  .input-search-before {
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
  }
  .input-search-disabled {
    color: rgba(0, 0, 0, 0.25);
    background-color: rgba(0, 0, 0, 0.04);
    cursor: not-allowed;
    &:hover {
      border-color: #d9d9d9;
    }
    &:focus-within {
      border-color: #d9d9d9;
      box-shadow: none;
    }
    .input-search {
      color: rgba(0, 0, 0, 0.25);
      background-color: transparent;
      cursor: not-allowed;
    }
  }
  .m-search-button {
    position: relative;
    left: -1px;
    border-left: 0;
    color: rgba(0, 0, 0, 0.88);
    font-weight: normal;
    font-size: 14px;
    text-align: center;
    background-color: rgba(0, 0, 0, 0.02);
    border-top-left-radius: 0;
    border-top-right-radius: 6px;
    border-bottom-right-radius: 6px;
    border-bottom-left-radius: 0;
    transition: all 0.3s;
    line-height: 1;
    :deep(.m-btn) {
      padding-top: 0;
      padding-bottom: 0;
      border-top-left-radius: 0;
      border-top-right-radius: 6px;
      border-bottom-right-radius: 6px;
      border-bottom-left-radius: 0;
      &:not(.btn-primary):not(.btn-danger):not(.btn-link):not(.btn-disabled) {
        color: rgba(0, 0, 0, 0.45);
        .btn-icon {
          svg {
            fill: rgba(0, 0, 0, 0.45);
          }
        }
      }
    }
    :deep(.search-btn):not(.btn-primary):not(.btn-danger):not(.btn-link):not(.btn-disabled) {
      color: rgba(0, 0, 0, 0.45);
      &:hover {
        .btn-icon {
          svg {
            fill: #4096ff;
          }
        }
      }
      &:active {
        .btn-icon {
          svg {
            fill: #0958d9;
          }
        }
      }
      .btn-icon {
        svg {
          fill: rgba(0, 0, 0, 0.45);
        }
      }
    }
  }
}
</style>

在要使用的页面引入

其中引入使用了以下组件:

<script setup lang="ts">
import InputSearch from './InputSearch.vue'
import { ref, watchEffect } from 'vue'
import { SearchOutlined, CompassOutlined, EnvironmentOutlined, InfoCircleOutlined } from '@ant-design/icons-vue'
const value = ref('')
const lazyValue = ref('')
const sizeOptions = [
  {
    label: 'small',
    value: 'small'
  },
  {
    label: 'middle',
    value: 'middle'
  },
  {
    label: 'large',
    value: 'large'
  }
]
const size = ref('middle')
const loading = ref(true)
const disabled = ref(true)
watchEffect(() => {
  console.log('value:', value.value)
})
watchEffect(() => {
  console.log('lazyValue:', lazyValue.value)
})
function onChange(e: Event) {
  console.log('change', e)
}
function onSearch(searchValue: string) {
  console.log('searchValue', searchValue)
}
</script>
<template>
  <div>
    <h1>{
  { $route.name }} {
  { $route.meta.title }}</h1>
    <h2 class="mt30 mb10">基本使用</h2>
    <Space gap="small" vertical>
      <Alert>
        <template #message>
          .lazy:
          <br />
          默认情况下,v-model 会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。
          <br />
          你可以添加 lazy 修饰符来改为在每次 change 事件后更新数据:
          <br />
          {
  { '<InputSearch v-model:value.lazy="msg" />' }}
        </template>
      </Alert>
      <InputSearch
        :width="200"
        v-model:value="value"
        placeholder="Basic search usage"
        @change="onChange"
        @search="onSearch"
      />
      <InputSearch
        :width="200"
        v-model:value.lazy="lazyValue"
        placeholder="Lazy search usage"
        @change="onChange"
        @search="onSearch"
      />
    </Space>
    <h2 class="mt30 mb10">自定义搜索按钮</h2>
    <Space vertical>
      <InputSearch
        v-model:value="value"
        :search-props="{ type: 'primary' }"
        placeholder="input search text"
        @search="onSearch"
      />
      <InputSearch
        v-model:value="value"
        placeholder="input search text"
        :icon="false"
        search="Search"
        :search-props="{ type: 'primary' }"
        @search="onSearch"
      />
      <InputSearch v-model:value="value" placeholder="input search text" @search="onSearch">
        <template #search>
          <Button type="primary">
            <template #icon>
              <SearchOutlined />
            </template>
            Search
          </Button>
        </template>
      </InputSearch>
      <InputSearch
        v-model:value="value"
        placeholder="input search text"
        search="Search"
        :search-props="{ type: 'primary', ghost: true }"
        @search="onSearch"
      >
        <template #icon>
          <CompassOutlined />
        </template>
      </InputSearch>
      <InputSearch v-model:value="value" placeholder="input search text" @search="onSearch">
        <template #search>
          <Button>
            <template #icon>
              <CompassOutlined />
            </template>
            Custom
          </Button>
        </template>
      </InputSearch>
    </Space>
    <h2 class="mt30 mb10">三种大小</h2>
    <Space vertical>
      <Radio :options="sizeOptions" v-model:value="size" button button-style="solid" />
      <InputSearch
        v-model:value="value"
        :size="size"
        :search-props="{ type: 'primary' }"
        placeholder="input search text"
        @search="onSearch"
      />
      <InputSearch
        v-model:value="value"
        :size="size"
        placeholder="input search text"
        :icon="false"
        search="Search"
        :search-props="{ type: 'primary' }"
        @search="onSearch"
      />
      <InputSearch v-model:value="value" :size="size" placeholder="input search text" @search="onSearch">
        <template #search>
          <Button type="primary" :size="size">
            <template #icon>
              <SearchOutlined />
            </template>
            Search
          </Button>
        </template>
      </InputSearch>
    </Space>
    <h2 class="mt30 mb10">带清除图标</h2>
    <Space>
      <InputSearch
        v-model:value="value"
        allow-clear
        placeholder="input search text"
        @search="onSearch"
      />
    </Space>
    <h2 class="mt30 mb10">带字数提示</h2>
    <Space :width="300">
      <InputSearch v-model:value="value" show-count placeholder="input search text" @search="onSearch" />
      <InputSearch
        v-model:value="value"
        allow-clear
        show-count
        :maxlength="20"
        placeholder="input search text"
        @search="onSearch"
      />
    </Space>
    <h2 class="mt30 mb10">前置标签</h2>
    <Space :width="300">
      <InputSearch v-model:value="value" addon-before="Please" placeholder="input search text" @search="onSearch" />
      <InputSearch
        v-model:value="value"
        :search-props="{ type: 'primary' }"
        placeholder="input search text"
        @search="onSearch"
      >
        <template #addonBefore>
          <CompassOutlined />
        </template>
      </InputSearch>
    </Space>
    <h2 class="mt30 mb10">前缀和后缀</h2>
    <Space :width="300">
      <InputSearch v-model:value="value" prefix="¥" suffix="RMB" placeholder="input search text" @search="onSearch" />
      <InputSearch v-model:value="value" placeholder="input search text" @search="onSearch">
        <template #prefix>
          <EnvironmentOutlined />
        </template>
        <template #suffix>
          <Tooltip :max-width="150" tooltip="Extra information">
            <InfoCircleOutlined />
          </Tooltip>
        </template>
      </InputSearch>
    </Space>
    <h2 class="mt30 mb10">搜索中</h2>
    <Space vertical>
      <Space align="center"> Loading state:<Switch v-model="loading" /> </Space>
      <InputSearch
        v-model:value="value"
        :loading="loading"
        :search-props="{ type: 'primary' }"
        placeholder="input search text"
        @search="onSearch"
      />
      <InputSearch
        v-model:value="value"
        :loading="loading"
        placeholder="input search text"
        :icon="false"
        search="Search"
        :search-props="{ type: 'primary' }"
        @search="onSearch"
      />
      <InputSearch v-model:value="value" placeholder="input search text" @search="onSearch">
        <template #search>
          <Button type="primary" :loading="loading">
            <template #icon>
              <SearchOutlined />
            </template>
            Search
          </Button>
        </template>
      </InputSearch>
    </Space>
    <h2 class="mt30 mb10">禁用</h2>
    <Space vertical>
      <Space align="center"> Disabled state:<Switch v-model="disabled" /> </Space>
      <InputSearch v-model:value="value" :disabled="disabled" placeholder="input search text" @search="onSearch" />
      <InputSearch
        v-model:value="value"
        :disabled="disabled"
        :search-props="{ type: 'primary' }"
        placeholder="input search text"
        @search="onSearch"
      />
      <InputSearch
        v-model:value="value"
        :disabled="disabled"
        placeholder="input search text"
        :icon="false"
        search="Search"
        :search-props="{ type: 'primary' }"
        @search="onSearch"
      />
      <InputSearch v-model:value="value" :disabled="disabled" placeholder="input search text" @search="onSearch">
        <template #search>
          <Button type="primary" :disabled="disabled">
            <template #icon>
              <SearchOutlined />
            </template>
            Search
          </Button>
        </template>
      </InputSearch>
    </Space>
  </div>
</template>
相关文章
|
2月前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
143 64
|
2月前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
115 60
|
10天前
|
JavaScript API 数据处理
vue3使用pinia中的actions,需要调用接口的话
通过上述步骤,您可以在Vue 3中使用Pinia和actions来管理状态并调用API接口。Pinia的简洁设计使得状态管理和异步操作更加直观和易于维护。无论是安装配置、创建Store还是在组件中使用Store,都能轻松实现高效的状态管理和数据处理。
39 3
|
2月前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
39 8
|
2月前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
33 1
|
2月前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
42 1
|
2月前
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
|
2月前
|
JavaScript 索引
Vue 3.x 版本中双向数据绑定的底层实现有哪些变化
从Vue 2.x的`Object.defineProperty`到Vue 3.x的`Proxy`,实现了更高效的数据劫持与响应式处理。`Proxy`不仅能够代理整个对象,动态响应属性的增删,还优化了嵌套对象的处理和依赖追踪,减少了不必要的视图更新,提升了性能。同时,Vue 3.x对数组的响应式处理也更加灵活,简化了开发流程。
|
2月前
|
JavaScript 前端开发 API
从Vue 2到Vue 3的演进
从Vue 2到Vue 3的演进
44 0
|
2月前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
65 0

热门文章

最新文章