This commit is contained in:
2026-01-05 12:47:14 +08:00
commit 1fc846fae3
1614 changed files with 162035 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
.nut-theme-dark {
.nut-searchbar {
background: $dark-background;
&__search-input {
background: $dark-background4;
}
&__right-search-icon,
&__left-search-icon {
color: $dark-color;
}
}
}
.nut-searchbar {
box-sizing: border-box;
display: flex;
align-items: center;
width: $searchbar-width;
padding: $searchbar-padding;
color: $searchbar-input-bar-color;
background: $searchbar-background;
&.safe-area-inset-bottom {
position: relative;
margin-bottom: constant(safe-area-inset-bottom);
margin-bottom: env(safe-area-inset-bottom);
&::after {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: constant(safe-area-inset-bottom);
height: env(safe-area-inset-bottom);
content: "";
background: $searchbar-background;
}
}
&::placeholder {
color: $searchbar-input-bar-placeholder-color;
}
&__search-input {
box-sizing: border-box;
display: flex;
flex: 1;
align-items: center;
height: $searchbar-input-height;
padding: $searchbar-input-padding;
background: $searchbar-input-background;
border-radius: $searchbar-input-border-radius;
box-shadow: $searchbar-input-box-shadow;
&.square {
border-radius: 0;
}
.nut-searchbar__input-inner {
position: relative;
display: flex;
flex: 1;
align-items: center;
overflow: hidden;
.nut-searchbar__input-form {
flex: 1;
overflow: hidden;
}
}
.nut-searchbar__input-inner-icon {
position: relative;
display: flex;
align-items: center;
padding: 0 7px;
}
.nut-searchbar__input-clear {
position: relative;
z-index: 10;
padding: 0 5px;
}
.nut-searchbar__input-inner-icon-absolute {
.nut-searchbar__input-clear {
position: absolute;
left: -20px;
}
}
.nut-searchbar__iptleft-search-icon {
width: 14px;
height: 14px;
margin-right: 6px;
}
.nut-searchbar__iptright-search-icon {
margin-left: 5px;
}
.nut-searchbar__input-bar {
flex: 1;
height: $searchbar-input-height;
padding: 0;
margin: 0;
font-size: 14px;
line-height: $searchbar-input-height;
background-color: transparent;
border-color: transparent;
outline: none;
}
.nut-searchbar__input-inner-absolute {
.nut-searchbar__input-bar {
box-sizing: border-box;
padding-right: 20px;
}
}
}
&__left-search-icon {
margin-right: 8px;
}
&__search-icon {
display: flex;
align-items: center;
justify-content: center;
}
&__right-search-icon {
margin-left: 16px;
font-size: 14px;
color: $searchbar-right-out-color;
}
}

View File

@@ -0,0 +1,2 @@
export * from './searchbar'
export * from './type'

View File

@@ -0,0 +1,104 @@
import type { InputOnBlurEvent, InputOnFocusEvent, InputOnInputEvent } from '@uni-helper/uni-app-types'
import type { CSSProperties, ExtractPropTypes } from 'vue'
import { BLUR_EVENT, CHANGE_EVENT, CLEAR_EVENT, FOCUS_EVENT, SEARCH_EVENT, UPDATE_MODEL_EVENT } from '../_constants'
import {
commonProps,
isString,
makeNumberProp,
makeNumericProp,
makeObjectProp,
makeStringProp,
nullableBooleanProp,
numericProp,
truthProp,
} from '../_utils'
import type { InputAlignType, InputConfirmType, InputType } from '../input'
import type { SearchbarShape } from './type'
export const searchbarProps = {
...commonProps,
/**
* @description 当前输入的值
*/
modelValue: makeNumericProp(''),
/**
* @description 输入框类型
*/
inputType: makeStringProp<InputType>('text'),
/**
* @description 搜索框形状,可选值为 `square` `round`
*/
shape: makeStringProp<SearchbarShape>('round'),
/**
* @description 最大输入长度
*/
maxLength: numericProp,
/**
* @description 输入框默认占位符
*/
placeholder: String,
/**
* @description 是否展示清除按钮
*/
clearable: truthProp,
/**
* @description 自定义清除按钮图标
*/
clearIcon: makeStringProp('circle-close'),
/**
* @description 输入框外部背景
*/
background: String,
/**
* @description 输入框内部背景
*/
inputBackground: String,
/**
* @description 聚焦时搜索框样式
*/
focusStyle: makeObjectProp<CSSProperties>({}),
/**
* @description 是否自动聚焦
*/
autofocus: Boolean,
/**
* @description 是否禁用输入框
*/
disabled: nullableBooleanProp,
/**
* @description 输入框只读
*/
readonly: Boolean,
/**
* @description 对齐方式,可选 `left` `center` `right`
*/
inputAlign: makeStringProp<InputAlignType>('left'),
/**
* @description 键盘右下角按钮的文字,仅在`type='text'`时生效,可选值 `send`:发送、`search`:搜索、`next`:下一个、`go`:前往、`done`:完成
*/
confirmType: makeStringProp<InputConfirmType>('done'),
/**
* @description 是否开启 iphone 系列全面屏底部安全区适配
*/
safeAreaInsetBottom: Boolean,
/**
* @description 指定的距离的最小值作为光标与键盘的距离
*/
cursorSpacing: makeNumberProp(0),
}
export type SearchbarProps = ExtractPropTypes<typeof searchbarProps>
export const searchbarEmits = {
[UPDATE_MODEL_EVENT]: (val: string, event: InputOnInputEvent) => (isString(val) || val === undefined) && event instanceof Object,
[CHANGE_EVENT]: (val: string, event: InputOnInputEvent) => (isString(val) || val === undefined) && event instanceof Object,
[BLUR_EVENT]: (val: string, event: InputOnBlurEvent) => (isString(val) || val === undefined) && event instanceof Object,
[FOCUS_EVENT]: (val: string, event: InputOnFocusEvent) => (isString(val) || val === undefined) && event instanceof Object,
[CLEAR_EVENT]: (val: string) => (isString(val) || val === undefined),
[SEARCH_EVENT]: (val: string) => (isString(val) || val === undefined),
clickInput: (val: string, event: Event) => (isString(val) || val === undefined) && event instanceof Object,
clickLeftIcon: (val: string, event: Event) => (isString(val) || val === undefined) && event instanceof Object,
clickRightIcon: (val: string, event: Event) => (isString(val) || val === undefined) && event instanceof Object,
}
export type SearchbarEmits = typeof searchbarEmits

View File

@@ -0,0 +1,217 @@
<script setup lang="ts">
import type { InputOnBlurEvent, InputOnFocusEvent, InputOnInputEvent } from '@uni-helper/uni-app-types'
import type { CSSProperties } from 'vue'
import { computed, defineComponent, reactive, toRef, useSlots } from 'vue'
import { BLUR_EVENT, CHANGE_EVENT, CLEAR_EVENT, FOCUS_EVENT, PREFIX, SEARCH_EVENT, UPDATE_MODEL_EVENT } from '../_constants'
import { getMainClass, getMainStyle } from '../_utils'
import { useTranslate } from '../../locale'
import { useFormDisabled } from '../form/form'
import NutIcon from '../icon/icon.vue'
import { searchbarEmits, searchbarProps } from './searchbar'
const props = defineProps(searchbarProps)
const emit = defineEmits(searchbarEmits)
const slots = useSlots()
function hasSlot(name: string) {
return Boolean(slots[name])
}
const formDisabled = useFormDisabled(toRef(props, 'disabled'))
const state = reactive({
active: false,
})
function stringModelValue() {
if (props.modelValue == null)
return ''
return String(props.modelValue)
}
const innerValue = computed<string>(() => {
return stringModelValue()
})
const innerMaxLength = computed(() => {
if (props.maxLength == null)
return -1
return Number(props.maxLength)
})
const classes = computed(() => {
return getMainClass(props, componentName, {
'safe-area-inset-bottom': props.safeAreaInsetBottom,
})
})
const styles = computed(() => {
return getMainStyle(props, {
background: props.background,
})
})
const inputWrapperStyles = computed(() => {
const style: CSSProperties = {
background: props.inputBackground,
}
if (state.active)
Object.assign(style, props.focusStyle)
return style
})
const inputStyles = computed(() => {
return {
textAlign: props.inputAlign,
}
})
function handleValue(value: string) {
if (innerMaxLength.value > 0 && value.length > innerMaxLength.value)
value = value.slice(0, innerMaxLength.value)
return value
}
function handleInput(event: InputOnInputEvent) {
const value = handleValue(event.detail.value)
emit(UPDATE_MODEL_EVENT, value, event)
emit(CHANGE_EVENT, value, event)
}
function handleFocus(event: InputOnFocusEvent) {
const value = handleValue(event.detail.value)
state.active = true
emit(FOCUS_EVENT, value, event)
}
function handleBlur(event: InputOnBlurEvent) {
const value = handleValue(event.detail.value)
setTimeout(() => {
state.active = false
}, 200)
emit(BLUR_EVENT, value, event)
}
function handleClear(event: any) {
emit(UPDATE_MODEL_EVENT, '', event)
emit(CHANGE_EVENT, '', event)
emit(CLEAR_EVENT, '')
}
function handleSubmit() {
emit(SEARCH_EVENT, innerValue.value)
}
function handleInputClick(event: any) {
emit('clickInput', innerValue.value, event)
}
function handleLeftIconClick(event: any) {
emit('clickLeftIcon', innerValue.value, event)
}
function handleRightIconClick(event: any) {
emit('clickRightIcon', innerValue.value, event)
}
</script>
<script lang="ts">
const componentName = `${PREFIX}-searchbar`
const { translate } = useTranslate(componentName)
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view :class="classes" :style="styles">
<view
v-if="hasSlot('leftout')"
class="nut-searchbar__search-icon nut-searchbar__left-search-icon"
@click="handleLeftIconClick"
>
<slot name="leftout" />
</view>
<view class="nut-searchbar__search-input" :class="[props.shape]" :style="inputWrapperStyles">
<view v-if="hasSlot('leftin')" class="nut-searchbar__search-icon nut-searchbar__iptleft-search-icon">
<slot name="leftin" />
</view>
<view class="nut-searchbar__input-inner" :class="{ 'nut-searchbar__input-inner-absolute': hasSlot('rightin') }">
<form
class="nut-searchbar__input-form"
action="#"
onsubmit="return false"
@submit.prevent="handleSubmit"
>
<input
class="nut-searchbar__input-bar"
:class="{ 'nut-searchbar__input-bar_clear': props.clearable }"
:style="inputStyles"
:type="props.inputType as any"
:maxlength="innerMaxLength"
:placeholder="props.placeholder || translate('placeholder')"
:value="innerValue"
:focus="props.autofocus"
:confirm-type="props.confirmType"
:disabled="formDisabled"
:readonly="props.readonly"
:cursor-spacing="props.cursorSpacing"
@click="handleInputClick"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@confirm="handleSubmit"
>
</form>
</view>
<view
class="nut-searchbar__input-inner-icon"
:class="{ 'nut-searchbar__input-inner-icon-absolute': hasSlot('rightin') }"
>
<view
v-if="props.clearable"
class="nut-searchbar__search-icon nut-searchbar__input-clear"
:class="{ 'nut-hidden': innerValue.length <= 0 }"
@click="handleClear"
>
<template v-if="hasSlot('clear-icon')">
<slot name="clear-icon" />
</template>
<NutIcon v-else :name="props.clearIcon" />
</view>
<view
v-if="hasSlot('rightin')"
class="nut-searchbar__search-icon nut-searchbar__iptright-search-icon"
@click="handleRightIconClick"
>
<slot name="rightin" />
</view>
</view>
</view>
<view v-if="hasSlot('rightout')" class="nut-searchbar__search-icon nut-searchbar__right-search-icon">
<slot name="rightout" />
</view>
</view>
</template>
<style lang="scss">
@import "./index";
</style>

View File

@@ -0,0 +1,2 @@
export const searchbarShape = ['square', 'round']
export type SearchbarShape = (typeof searchbarShape)[number]