283 lines
5.7 KiB
Vue
283 lines
5.7 KiB
Vue
<template>
|
||
<view
|
||
class="clay-input-wrapper"
|
||
:class="[
|
||
`clay-input-${size}`,
|
||
{ 'clay-input-focused': isFocused },
|
||
{ 'clay-input-error': error },
|
||
{ 'clay-input-disabled': disabled },
|
||
customClass
|
||
]"
|
||
>
|
||
<!-- 前缀图标 -->
|
||
<view v-if="prefixIcon" class="clay-prefix-icon">
|
||
<text>{{ prefixIcon }}</text>
|
||
</view>
|
||
|
||
<!-- 输入框 -->
|
||
<input
|
||
class="clay-input-field"
|
||
:type="type"
|
||
:value="modelValue"
|
||
:placeholder="placeholder"
|
||
:disabled="disabled"
|
||
:maxlength="maxlength"
|
||
@input="handleInput"
|
||
@focus="handleFocus"
|
||
@blur="handleBlur"
|
||
@confirm="handleConfirm"
|
||
/>
|
||
|
||
<!-- 后缀图标/按钮 -->
|
||
<view v-if="suffixIcon || clearable" class="clay-suffix-icon" @tap="handleSuffixTap">
|
||
<text v-if="showClearButton" class="clear-button">×</text>
|
||
<text v-else-if="suffixIcon">{{ suffixIcon }}</text>
|
||
</view>
|
||
|
||
<!-- 错误提示 -->
|
||
<view v-if="error && errorText" class="clay-error-text">
|
||
<text>{{ errorText }}</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'ClayInput',
|
||
props: {
|
||
// v-model 绑定值
|
||
modelValue: {
|
||
type: [String, Number],
|
||
default: ''
|
||
},
|
||
// 输入框类型
|
||
type: {
|
||
type: String,
|
||
default: 'text'
|
||
},
|
||
// 占位符
|
||
placeholder: {
|
||
type: String,
|
||
default: '请输入'
|
||
},
|
||
// 尺寸:sm, md, lg
|
||
size: {
|
||
type: String,
|
||
default: 'md'
|
||
},
|
||
// 是否禁用
|
||
disabled: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// 是否显示错误状态
|
||
error: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// 错误提示文字
|
||
errorText: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
// 最大长度
|
||
maxlength: {
|
||
type: [String, Number],
|
||
default: 140
|
||
},
|
||
// 是否可清空
|
||
clearable: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// 前缀图标(emoji)
|
||
prefixIcon: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
// 后缀图标(emoji)
|
||
suffixIcon: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
// 自定义类名
|
||
customClass: {
|
||
type: String,
|
||
default: ''
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
isFocused: false
|
||
}
|
||
},
|
||
computed: {
|
||
showClearButton() {
|
||
return this.clearable && this.modelValue && !this.disabled
|
||
}
|
||
},
|
||
methods: {
|
||
handleInput(e) {
|
||
this.$emit('update:modelValue', e.detail.value)
|
||
this.$emit('input', e.detail.value)
|
||
},
|
||
handleFocus(e) {
|
||
this.isFocused = true
|
||
this.$emit('focus', e)
|
||
},
|
||
handleBlur(e) {
|
||
this.isFocused = false
|
||
this.$emit('blur', e)
|
||
},
|
||
handleConfirm(e) {
|
||
this.$emit('confirm', e)
|
||
},
|
||
handleSuffixTap() {
|
||
if (this.showClearButton) {
|
||
this.$emit('update:modelValue', '')
|
||
this.$emit('clear')
|
||
} else if (this.suffixIcon) {
|
||
this.$emit('suffix-tap')
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* ============================================
|
||
Claymorphism 输入框组件
|
||
使用示例:
|
||
<ClayInput v-model="value" placeholder="请输入" prefixIcon="🔍" />
|
||
============================================ */
|
||
|
||
.clay-input-wrapper {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
background: linear-gradient(145deg, #ffffff, #f5f5f5);
|
||
border-radius: 24rpx;
|
||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||
overflow: hidden;
|
||
|
||
/* Claymorphism 双阴影 */
|
||
box-shadow:
|
||
inset 4rpx 4rpx 8rpx rgba(0, 0, 0, 0.06),
|
||
inset -4rpx -4rpx 8rpx rgba(255, 255, 255, 0.9),
|
||
4rpx 4rpx 8rpx rgba(0, 0, 0, 0.04),
|
||
-4rpx -4rpx 8rpx rgba(255, 255, 255, 0.7);
|
||
|
||
&.clay-input-sm {
|
||
border-radius: 20rpx;
|
||
padding: 16rpx 24rpx;
|
||
|
||
.clay-input-field {
|
||
font-size: 24rpx;
|
||
}
|
||
}
|
||
|
||
&.clay-input-md {
|
||
border-radius: 24rpx;
|
||
padding: 24rpx 28rpx;
|
||
|
||
.clay-input-field {
|
||
font-size: 28rpx;
|
||
}
|
||
}
|
||
|
||
&.clay-input-lg {
|
||
border-radius: 28rpx;
|
||
padding: 28rpx 32rpx;
|
||
|
||
.clay-input-field {
|
||
font-size: 32rpx;
|
||
}
|
||
}
|
||
|
||
/* 聚焦状态 */
|
||
&.clay-input-focused {
|
||
box-shadow:
|
||
inset 6rpx 6rpx 12rpx rgba(255, 107, 0, 0.08),
|
||
inset -6rpx -6rpx 12rpx rgba(255, 255, 255, 0.85),
|
||
6rpx 6rpx 12rpx rgba(255, 107, 0, 0.1),
|
||
-6rpx -6rpx 12rpx rgba(255, 255, 255, 0.6);
|
||
}
|
||
|
||
/* 错误状态 */
|
||
&.clay-input-error {
|
||
box-shadow:
|
||
inset 4rpx 4rpx 8rpx rgba(255, 59, 48, 0.1),
|
||
inset -4rpx -4rpx 8rpx rgba(255, 255, 255, 0.9),
|
||
4rpx 4rpx 8rpx rgba(255, 59, 48, 0.1),
|
||
-4rpx -4rpx 8rpx rgba(255, 255, 255, 0.7);
|
||
}
|
||
|
||
/* 禁用状态 */
|
||
&.clay-input-disabled {
|
||
opacity: 0.5;
|
||
background: linear-gradient(145deg, #f0f0f0, #e8e8e8);
|
||
}
|
||
}
|
||
|
||
.clay-input-field {
|
||
flex: 1;
|
||
border: none;
|
||
background: transparent;
|
||
color: #1D1D1F;
|
||
outline: none;
|
||
line-height: 1.5;
|
||
|
||
&::placeholder {
|
||
color: #C7C7CC;
|
||
}
|
||
}
|
||
|
||
/* 前缀图标 */
|
||
.clay-prefix-icon {
|
||
margin-right: 16rpx;
|
||
font-size: 32rpx;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
/* 后缀图标 */
|
||
.clay-suffix-icon {
|
||
margin-left: 16rpx;
|
||
font-size: 32rpx;
|
||
opacity: 0.6;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
|
||
&:active {
|
||
opacity: 1;
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.clear-button {
|
||
display: inline-block;
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
line-height: 36rpx;
|
||
text-align: center;
|
||
font-size: 40rpx;
|
||
color: #86868B;
|
||
background: linear-gradient(145deg, #e8e8e8, #f8f8f8);
|
||
border-radius: 50%;
|
||
|
||
/* Claymorphism 凹陷效果 */
|
||
box-shadow:
|
||
inset 2rpx 2rpx 4rpx rgba(0, 0, 0, 0.1),
|
||
inset -2rpx -2rpx 4rpx rgba(255, 255, 255, 0.9);
|
||
}
|
||
}
|
||
|
||
/* 错误提示 */
|
||
.clay-error-text {
|
||
position: absolute;
|
||
bottom: -40rpx;
|
||
left: 0;
|
||
font-size: 22rpx;
|
||
color: #FF3B30;
|
||
white-space: nowrap;
|
||
}
|
||
</style>
|