Appearance
Select组件需求分析
类似原生的Select,不过有着更强大的功能
最基本的功能:
- 点击展开下拉选项菜单
- 点击菜单中的某一项,下拉菜单关闭
- Select获取选中状态,并且填充对应的选项
组件的本质:进阶版本的Dropdown,Input组件Tooltip组件的组合
高级功能:
- 可清空选项:当hover的时候,在组件右侧显示以后可清空的按钮,点击以后清空选中的值
- 自定义模板:可以自定义,下拉菜单的选项的格式
- 可筛选选项:Input允许输入,输入后可以根据输入字符自动过滤下拉菜单的选项
- 支持远程搜索(难点):类似自动联想,可以根据输入的字符发送请求,渲染返回的内容作为选项列表
- 扩展支持:比如键盘移动
Select.vue
html<template> <div class="vk-select" :class="{'is-disabled': disabled }" @click="toggleDropdown" > <Tooltip placement="bottom-start" ref="tooltipRef" :popperOptions="popperOptions" @click-outside="controlDropdown(false)" manual > <Input v-model="states.inputValue" :disabled="disabled" :placeholder="placeholder" ref="inputRef" readonly > <template #suffix> <Icon icon="angle-down" class="header-angle" :class="{ 'is-active': isDropdownShow }"/> </template> </Input> <template #content> <ul class="vk-select__menu"> <template v-for="(item, index) in options" :key="index"> <li class="vk-select__menu-item" :class="{'is-disabled': item.disabled, 'is-selected': states.selectedOption?.value === item.value }" :id="`select-item-${item.value}`" @click.stop="itemSelect(item)" > {{item.label}} </li> </template> </ul> </template> </Tooltip> </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import type { Ref } from 'vue' import type { SelectProps, SelectEmits, SelectOption, SelectStates } from './types' import Tooltip from '../Tooltip/Tooltip.vue' import type { TooltipInstance } from '../Tooltip/types' import Input from '../Input/Input.vue' import Icon from '../Icon/Icon.vue' import type { InputInstance } from '../Input/types' const findOption = (value: string) => { const option = props.options.find(option => option.value === value) return option ? option : null } defineOptions({ name: 'VkSelect' }) const props = defineProps<SelectProps>() const emits = defineEmits<SelectEmits>() const initialOption = findOption(props.modelValue) const tooltipRef = ref() as Ref<TooltipInstance> const inputRef = ref() as Ref<InputInstance> const states = reactive<SelectStates>({ inputValue: initialOption ? initialOption.label : '', selectedOption: initialOption }) const isDropdownShow = ref(false) const popperOptions: any = { modifiers: [ { name: 'offset', options: { offset: [0, 9], }, }, { name: "sameWidth", enabled: true, fn: ({ state }: { state: any }) => { state.styles.popper.width = `${state.rects.reference.width}px`; }, phase: "beforeWrite", requires: ["computeStyles"], } ], } const controlDropdown = (show: boolean) => { if (show) { tooltipRef.value.show() } else { tooltipRef.value.hide() } isDropdownShow.value = show emits('visible-change', show) } const toggleDropdown = () => { if (props.disabled) return if (isDropdownShow.value) { controlDropdown(false) } else { controlDropdown(true) } } const itemSelect = (e: SelectOption) => { if (e.disabled) return states.inputValue = e.label states.selectedOption = e emits('change', e.value) emits('update:modelValue', e.value) controlDropdown(false) inputRef.value.ref.focus() } </script>style.css
css.vk-select { --vk-select-item-hover-bg-color: var(--vk-fill-color-light); --vk-select-item-font-size: var(--vk-font-size-base); --vk-select-item-font-color: var(--vk-text-color-regular); --vk-select-item-selected-font-color: var(--vk-color-primary); --vk-select-item-disabled-font-color: var(--vk-text-color-placeholder); --vk-select-input-focus-border-color: var(--vk-color-primary); } .vk-select { display: inline-block; vertical-align: middle; line-height: 32px; .vk-tooltip .vk-tooltip__popper { padding: 0; } .vk-input.is-focus .vk-input__wrapper { box-shadow: 0 0 0 1px var(--vk-select-input-focus-border-color) inset!important } .vk-input { .header-angle { transition: transform var(--vk-transition-duration); &.is-active { transform: rotate(180deg); } } } .vk-input__inner { cursor: pointer; } .vk-select__menu { list-style: none; margin: 6px 0; padding: 0; box-sizing: border-box; } .vk-select__menu-item { margin: 0; font-size: var(--vk-select-item-font-size); padding: 0 32px 0 20px; position: relative; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--vk-select-item-font-color); height: 34px; line-height: 34px; box-sizing: border-box; cursor: pointer; &:hover { background-color: var(--vk-select-item-hover-bg-color); } &.is-selected { color: var(--vk-select-item-selected-font-color); font-weight: 700; } &.is-disabled { color: var(--vk-select-item-disabled-font-color); cursor: not-allowed; &:hover { background-color: transparent; } } } }types.ts
typescriptexport interface SelectOption { label: string; value: string; disabled?: boolean; } export interface SelectProps { // v-model modelValue: string; // 选项 options: SelectOption[]; // 一些基本表单属性 placeholder: string; disabled: boolean; } export interface SelectStates { inputValue: string; selectedOption: null | SelectOption; } export interface SelectEmits { (e:'change', value: string) : void; (e:'update:modelValue', value: string) : void; (e: 'visible-change', value:boolean): void; }