| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- <template>
- <view class="form-item">
- <!-- label -->
- <view v-if="label" class="form-label">
- {{ label }}
- </view>
- <!-- input 行 -->
- <view class="form-input">
- <input
- class="form-input-inner"
- v-model="modelValue"
- :type="inputType"
- :password="passwordToggle && isPassword"
- :maxlength="maxlength"
- :placeholder="placeholder"
- :disabled="disabled"
- @focus="onFocus"
- @blur="onBlur"
- />
- <!-- 清除 -->
- <view
- v-if="showClear"
- class="clear"
- :class="{ visible: isFocused }"
- @touchstart.stop.prevent="clear"
- >
- <wd-icon name="error-fill" size="28rpx" :color="CLEAR_ICON_COLOR" />
- </view>
- <!-- 密码显隐 -->
- <view v-if="passwordToggle" class="input-type-toggle" @click="togglePassword">
- <wd-icon :name="isPassword ? 'view' : 'eye-close'" size="28rpx" :color="CLEAR_ICON_COLOR" />
- </view>
- <!-- 右侧插槽(验证码按钮等) -->
- <view class="suffix">
- <slot name="suffix" />
- </view>
- </view>
- <!-- 错误提示 -->
- <text class="error-text" :class="{ visible: !!error }">
- {{ error }}
- </text>
- </view>
- </template>
- <script setup lang="ts">
- import { computed, ref, watch } from 'vue'
- /* ---------- props ---------- */
- interface Props {
- label?: string
- placeholder?: string
- type?: 'text' | 'number' | 'password'
- maxlength?: number
- error?: string
- passwordToggle?: boolean
- disabled?: boolean
- }
- const props = withDefaults(defineProps<Props>(), {
- type: 'text',
- placeholder: '',
- maxlength: 50,
- error: '',
- passwordToggle: false,
- disabled: false,
- })
- /* ---------- emits ---------- */
- const emit = defineEmits<{
- (e: 'clear'): void
- (e: 'validate'): void
- }>()
- /* ---------- v-model ---------- */
- const modelValue = defineModel<string>({
- default: '',
- })
- /* ---------- state ---------- */
- const CLEAR_ICON_COLOR = '#9CA3AF'
- const isFocused = ref(false)
- const isPassword = ref(true)
- /* ---------- 清除检测 ---------- */
- const hadValue = ref(false)
- watch(
- () => modelValue.value,
- (val) => {
- const hasValue = !!val
- if (hadValue.value && !hasValue) {
- emit('clear')
- }
- hadValue.value = hasValue
- },
- { immediate: true }
- )
- /* ---------- computed ---------- */
- const inputType = computed(() => {
- if (props.passwordToggle) {
- return isPassword.value ? 'password' : 'text'
- }
- return props.type
- })
- const showClear = computed(() => !!modelValue.value)
- /* ---------- handlers ---------- */
- const onFocus = () => {
- isFocused.value = true
- }
- const onBlur = () => {
- isFocused.value = false
- if (modelValue.value) {
- emit('validate')
- }
- }
- const clear = () => {
- modelValue.value = ''
- }
- const togglePassword = () => {
- isPassword.value = !isPassword.value
- }
- </script>
- <style scoped lang="scss">
- .form-item {
- display: flex;
- flex-direction: column;
- padding: 20rpx 0;
- border-bottom: 1rpx solid #e4e7ed;
- position: relative;
- .form-label {
- margin-bottom: 8rpx;
- color: var(--font-color-333);
- font-size: 16px;
- line-height: 40rpx;
- }
- .form-input {
- display: flex;
- align-items: center;
- min-width: 0; /* ⭐ 必须 */
- .form-input-inner {
- flex: 1;
- min-width: 0; /* ⭐ 小程序必需 */
- height: 70rpx;
- line-height: 70rpx;
- font-size: 28rpx;
- }
- .clear {
- margin-left: 20rpx;
- opacity: 0;
- pointer-events: none;
- }
- .clear.visible {
- opacity: 1;
- pointer-events: auto;
- }
- .input-type-toggle {
- flex-shrink: 0;
- margin-left: 20rpx;
- }
- .suffix {
- flex-shrink: 0;
- margin-left: 20rpx;
- }
- }
- .error-text {
- position: absolute;
- left: 0;
- bottom: 6rpx;
- font-size: 20rpx;
- line-height: 20rpx;
- color: #ff4d4f;
- opacity: 0;
- pointer-events: none; // 不影响点击
- }
- .error-text.visible {
- opacity: 1;
- }
- }
- </style>
|