Skip to content

字符串输入组件

📝 组件概述

StringInputComponent 是一个灵活的字符串输入组件,支持按单位分段输入,并组合成特定格式的字符串。

输出格式RMB100YUAN50JIAO5FEN8(单位+值)

核心特性

  • ✅ 灵活的单位配置(字符串数组或配置对象)
  • ✅ 完整的 v-model 双向绑定
  • ✅ 简洁的输出格式
  • ✅ 空单位支持(第一个可为空)
  • ✅ 必填和自定义校验
  • ✅ 多种输入类型(text、number、tel 等)
  • ✅ 响应式设计

⚠️ 使用限制

  • 不支持连续的多个空单位:组件不适用于连续出现多个空单位(空字符串)的场景,如 ['', '', 'unit'] 会导致解析异常
  • 建议使用场景:每个单位之间最好有明确的单位标识,或者只在第一个位置使用空单位

🚀 快速开始

基础使用

vue
<template>
  <StringInputComponent v-model="money" :units="['RMB', 'YUAN', 'JIAO', 'FEN']" />
  <p>输出: {{ money }}</p>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import StringInputComponent from '@/record/StringInputComponent.vue'

const money = ref('RMB100YUAN50JIAO5FEN8')
</script>

输出RMB100YUAN50JIAO5FEN8

空单位支持

vue
<template>
  <StringInputComponent v-model="value" :units="['', 'YUAN', 'JIAO']" />
</template>

<script setup lang="ts">
const value = ref('123YUAN50JIAO5')
</script>

输出123YUAN50JIAO5(第一个无单位标签)


📖 完整示例

示例 1:基础货币输入

带类型限制的货币输入,支持数字类型和占位符。

example

示例 1:基础货币输入

带类型限制的货币输入,支持数字类型和占位符

RMB
YUAN
JIAO
FEN
输出值:RMB100YUAN50JIAO5FEN8
vue
<template>
    <div class="example-container">
        <h3>示例 1:基础货币输入</h3>
        <p class="description">带类型限制的货币输入,支持数字类型和占位符</p>

        <StringInputComponent v-model="money" :units="['RMB', 'YUAN', 'JIAO', 'FEN']" :default-input-config="{
            type: 'number',
            placeholder: '0'
        }" />

        <div class="output">
            <strong>输出值:</strong>
            <code>{{ money || '(空)' }}</code>
        </div>
    </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import StringInputComponent from '../StringInputComponent.vue'

const money = ref('RMB100YUAN50JIAO5FEN8')
</script>

<style scoped>
.example-container {
    padding: 20px;
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    background-color: #fff;
}

h3 {
    margin-top: 0;
    margin-bottom: 10px;
    font-size: 18px;
    font-weight: 600;
    color: #1a1a1a;
}

.description {
    margin-bottom: 20px;
    font-size: 14px;
    color: #6b7280;
}

.output {
    margin-top: 20px;
    padding: 12px;
    background-color: #f9fafb;
    border-radius: 6px;
    font-size: 14px;
}

.output code {
    padding: 4px 8px;
    background-color: #1f2937;
    color: #10b981;
    font-family: 'Monaco', 'Menlo', monospace;
    border-radius: 4px;
    margin-left: 8px;
}
</style>
查看代码
vue
<template>
  <StringInputComponent
    v-model="money"
    :units="['RMB', 'YUAN', 'JIAO', 'FEN']"
    :default-input-config="{
      type: 'number',
      placeholder: '0'
    }"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
const money = ref('RMB100YUAN50JIAO5FEN8')
</script>

示例 2:必填校验

支持必填字段验证,点击校验按钮触发表单验证。

example

示例 2:必填校验

支持必填字段验证,点击校验按钮触发表单验证

RMB
YUAN
JIAO
输出值:RMB11YUAN123
vue
<template>
    <div class="example-container">
        <h3>示例 2:必填校验</h3>
        <p class="description">支持必填字段验证,点击校验按钮触发表单验证</p>

        <StringInputComponent ref="formRef" v-model="money" :units="unitConfigs" />

        <div class="button-group">
            <button @click="handleValidate" class="btn-primary">校验</button>
            <button @click="handleReset" class="btn-secondary">重置</button>
        </div>

        <div v-if="validationResult" :class="['validation-result', validationResult.type]">
            {{ validationResult.message }}
        </div>

        <div class="output">
            <strong>输出值:</strong>
            <code>{{ money || '(空)' }}</code>
        </div>
    </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import StringInputComponent from '../StringInputComponent.vue'
import type { UnitInputConfig } from '../types'

const formRef = ref()
const money = ref('RMB11YUAN123')
const validationResult = ref<{ type: string; message: string } | null>(null)

const unitConfigs: UnitInputConfig[] = [
    {
        unit: 'RMB',
        inputConfig: {
            type: 'number',
            required: true,
            placeholder: '必填'
        }
    },
    {
        unit: 'YUAN',
        inputConfig: {
            type: 'number',
            required: true,
            placeholder: '必填'
        }
    },
    {
        unit: 'JIAO',
        inputConfig: {
            type: 'number',
            placeholder: '可选'
        }
    }
]

const handleValidate = () => {
    if (formRef.value) {
        const isValid = formRef.value.validate()
        validationResult.value = {
            type: isValid ? 'success' : 'error',
            message: isValid ? '✓ 校验通过' : '✗ 校验失败,请检查必填项'
        }
    }
}

const handleReset = () => {
    if (formRef.value) {
        formRef.value.reset()
        validationResult.value = null
    }
}
</script>

<style scoped>
.example-container {
    padding: 20px;
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    background-color: #fff;
}

h3 {
    margin-top: 0;
    margin-bottom: 10px;
    font-size: 18px;
    font-weight: 600;
    color: #1a1a1a;
}

.description {
    margin-bottom: 20px;
    font-size: 14px;
    color: #6b7280;
}

.button-group {
    display: flex;
    gap: 12px;
    margin-top: 16px;
}

button {
    padding: 8px 24px;
    font-size: 14px;
    font-weight: 500;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    transition: all 0.3s;
}

.btn-primary {
    color: #fff;
    background-color: #3b82f6;
}

.btn-primary:hover {
    background-color: #2563eb;
    transform: translateY(-1px);
}

.btn-secondary {
    color: #374151;
    background-color: #f3f4f6;
}

.btn-secondary:hover {
    background-color: #e5e7eb;
}

.validation-result {
    margin-top: 12px;
    padding: 12px 16px;
    border-radius: 6px;
    font-size: 14px;
    font-weight: 500;
}

.validation-result.success {
    color: #059669;
    background-color: #d1fae5;
    border: 1px solid #a7f3d0;
}

.validation-result.error {
    color: #dc2626;
    background-color: #fee2e2;
    border: 1px solid #fecaca;
}

.output {
    margin-top: 20px;
    padding: 12px;
    background-color: #f9fafb;
    border-radius: 6px;
    font-size: 14px;
}

.output code {
    padding: 4px 8px;
    background-color: #1f2937;
    color: #10b981;
    font-family: 'Monaco', 'Menlo', monospace;
    border-radius: 4px;
    margin-left: 8px;
}
</style>
查看代码
vue
<template>
  <StringInputComponent ref="formRef" v-model="money" :units="unitConfigs" />
  <button @click="handleSubmit">校验</button>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import type { UnitInputConfig } from '@/record/types'

const formRef = ref()
const money = ref('')

const unitConfigs: UnitInputConfig[] = [
  {
    unit: 'RMB',
    inputConfig: {
      type: 'number',
      required: true,
      placeholder: '必填'
    }
  },
  {
    unit: 'YUAN',
    inputConfig: {
      type: 'number',
      required: true,
      placeholder: '必填'
    }
  },
  {
    unit: 'JIAO',
    inputConfig: {
      type: 'number',
      placeholder: '可选'
    }
  }
]

const handleSubmit = () => {
  if (formRef.value?.validate()) {
    console.log('校验通过!', money.value)
  }
}
</script>

示例 3:自定义校验规则

支持自定义校验规则,实时校验用户输入。

example

示例 3:自定义校验规则

支持自定义校验规则,实时校验用户输入

规则:RMB必须是数字且不能为负,YUAN必须小于100

RMB
YUAN
输出值:(空)
vue
<template>
    <div class="example-container">
        <h3>示例 3:自定义校验规则</h3>
        <p class="description">支持自定义校验规则,实时校验用户输入</p>
        <p class="hint">规则:RMB必须是数字且不能为负,YUAN必须小于100</p>

        <StringInputComponent v-model="money" :units="['RMB', 'YUAN']" :rules="customRules" :validate-on-input="true" />

        <div class="output">
            <strong>输出值:</strong>
            <code>{{ money || '(空)' }}</code>
        </div>
    </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import StringInputComponent from '../StringInputComponent.vue'
import type { ValidationRule } from '../types'

const money = ref('')

const customRules: Record<string, ValidationRule[]> = {
    RMB: [
        {
            validator: (value: string) => {
                if (!value) return true
                const num = parseInt(value)
                if (isNaN(num)) return 'RMB必须是数字'
                if (num < 0) return 'RMB不能为负数'
                return true
            }
        }
    ],
    YUAN: [
        {
            validator: (value: string) => {
                if (!value) return true
                const num = parseInt(value)
                if (isNaN(num)) return 'YUAN必须是数字'
                if (num >= 100) return 'YUAN必须小于100'
                return true
            }
        }
    ]
}
</script>

<style scoped>
.example-container {
    padding: 20px;
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    background-color: #fff;
}

h3 {
    margin-top: 0;
    margin-bottom: 10px;
    font-size: 18px;
    font-weight: 600;
    color: #1a1a1a;
}

.description {
    margin-bottom: 12px;
    font-size: 14px;
    color: #6b7280;
}

.hint {
    margin-bottom: 20px;
    padding: 12px;
    background-color: #fef3c7;
    border-left: 3px solid #f59e0b;
    border-radius: 4px;
    font-size: 13px;
    color: #92400e;
}

.output {
    margin-top: 20px;
    padding: 12px;
    background-color: #f9fafb;
    border-radius: 6px;
    font-size: 14px;
}

.output code {
    padding: 4px 8px;
    background-color: #1f2937;
    color: #10b981;
    font-family: 'Monaco', 'Menlo', monospace;
    border-radius: 4px;
    margin-left: 8px;
}
</style>
查看代码
vue
<template>
  <StringInputComponent v-model="money" :units="['RMB', 'YUAN']" :rules="customRules" :validate-on-input="true" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import type { ValidationRule } from '@/record/types'

const money = ref('')

const customRules: Record<string, ValidationRule[]> = {
  RMB: [
    {
      validator: (value: string) => {
        if (!value) return true
        const num = parseInt(value)
        if (isNaN(num)) return 'RMB必须是数字'
        if (num < 0) return 'RMB不能为负数'
        return true
      }
    }
  ],
  YUAN: [
    {
      validator: (value: string) => {
        if (!value) return true
        const num = parseInt(value)
        if (isNaN(num)) return 'YUAN必须是数字'
        if (num >= 100) return 'YUAN必须小于100'
        return true
      }
    }
  ]
}
</script>

示例 4:时间格式输入

支持时分秒格式,最后一个单位可以设置为禁用。

example

示例 4:时间格式输入

支持时分秒格式,最后一个单位可以设置为禁用

输出值:14时30分45秒
vue
<template>
    <div class="example-container">
        <h3>示例 4:时间格式输入</h3>
        <p class="description">支持时分秒格式,最后一个单位可以设置为禁用</p>

        <StringInputComponent v-model="time" :units="timeUnits" />

        <div class="output">
            <strong>输出值:</strong>
            <code>{{ time || '(空)' }}</code>
        </div>
    </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import StringInputComponent from '../StringInputComponent.vue'

const time = ref('14时30分45秒')

const timeUnits = [
    { unit: '', inputConfig: { type: 'number', placeholder: '时', maxlength: 2 } },
    { unit: '时', inputConfig: { type: 'number', placeholder: '分', maxlength: 2 } },
    { unit: '分', inputConfig: { type: 'number', placeholder: '秒', maxlength: 2 } },
    { unit: '秒', inputConfig: { disabled: true } }
]
</script>

<style scoped>
.example-container {
    padding: 20px;
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    background-color: #fff;
}

h3 {
    margin-top: 0;
    margin-bottom: 10px;
    font-size: 18px;
    font-weight: 600;
    color: #1a1a1a;
}

.description {
    margin-bottom: 20px;
    font-size: 14px;
    color: #6b7280;
}

.output {
    margin-top: 20px;
    padding: 12px;
    background-color: #f9fafb;
    border-radius: 6px;
    font-size: 14px;
}

.output code {
    padding: 4px 8px;
    background-color: #1f2937;
    color: #10b981;
    font-family: 'Monaco', 'Menlo', monospace;
    border-radius: 4px;
    margin-left: 8px;
}
</style>
查看代码
vue
<template>
  <StringInputComponent v-model="time" :units="timeUnits" />
</template>

<script setup lang="ts">
import { ref } from 'vue'

const time = ref('14时30分45秒')

const timeUnits = [
  { unit: '', inputConfig: { type: 'number', placeholder: '时', maxlength: 2 } },
  { unit: '时', inputConfig: { type: 'number', placeholder: '分', maxlength: 2 } },
  { unit: '分', inputConfig: { type: 'number', placeholder: '秒', maxlength: 2 } },
  { unit: '秒', inputConfig: { disabled: true } }
]
</script>

完整演示(7 个场景)

查看包含所有功能的完整演示。

example

字符串输入组件示例

示例1: 简单使用(货币单位)

RMB
YUAN
JIAO
FEN
输出值:RMB100YUAN50JIAO5FEN8

示例2: 支持空单位(第一个输入框无单位标签)

YUAN
JIAO
FEN
输出值:123YUAN50JIAO5FEN8

示例3: 配置输入框属性

金额
输出值:(空)

示例4: 必填校验

RMB
YUAN
JIAO
输出值:(空)

示例5: 自定义校验规则

规则:RMB必须是整数,YUAN必须小于100,JIAO和FEN必须是0-9的数字

RMB
YUAN
JIAO
FEN
输出值:(空)

示例6: 禁用部分输入框

RMB
YUAN
JIAO
FEN
输出值:RMB888YUAN66JIAOFEN

示例7: 时间格式

输出值:14时30分00秒
vue
<template>
  <div class="string-input-demo">
    <h2>字符串输入组件示例</h2>

    <!-- 示例1: 简单使用 -->
    <div class="demo-section">
      <h3>示例1: 简单使用(货币单位)</h3>
      <StringInputComponent v-model="value1" :units="['RMB', 'YUAN', 'JIAO', 'FEN']" />
      <div class="result">
        <strong>输出值:</strong>
        <code>{{ value1 || '(空)' }}</code>
      </div>
    </div>

    <!-- 示例2: 支持空单位 -->
    <div class="demo-section">
      <h3>示例2: 支持空单位(第一个输入框无单位标签)</h3>
      <StringInputComponent v-model="value2" :units="['', 'YUAN', 'JIAO', 'FEN']" />
      <div class="result">
        <strong>输出值:</strong>
        <code>{{ value2 || '(空)' }}</code>
      </div>
    </div>

    <!-- 示例3: 配置输入框属性 -->
    <div class="demo-section">
      <h3>示例3: 配置输入框属性</h3>
      <StringInputComponent v-model="value3" :units="['金额', '元', '角', '分']" :default-input-config="{
        type: 'number',
        placeholder: '请输入数字',
        maxlength: 10
      }" />
      <div class="result">
        <strong>输出值:</strong>
        <code>{{ value3 || '(空)' }}</code>
      </div>
    </div>

    <!-- 示例4: 必填校验 -->
    <div class="demo-section">
      <h3>示例4: 必填校验</h3>
      <StringInputComponent ref="form1" v-model="value4" :units="unitConfigs1" />
      <div class="button-group">
        <button @click="validateForm1">校验</button>
        <button @click="resetForm1">重置</button>
      </div>
      <div class="result">
        <strong>输出值:</strong>
        <code>{{ value4 || '(空)' }}</code>
      </div>
      <div v-if="validationResult1" class="validation-result"
        :class="{ success: validationResult1.isValid, error: !validationResult1.isValid }">
        {{ validationResult1.message }}
      </div>
    </div>

    <!-- 示例5: 自定义校验规则 -->
    <div class="demo-section">
      <h3>示例5: 自定义校验规则</h3>
      <p class="hint">规则:RMB必须是整数,YUAN必须小于100,JIAO和FEN必须是0-9的数字</p>
      <StringInputComponent ref="form2" v-model="value5" :units="['RMB', 'YUAN', 'JIAO', 'FEN']" :rules="customRules"
        :validate-on-input="true" />
      <div class="button-group">
        <button @click="validateForm2">校验</button>
        <button @click="clearErrors2">清除错误</button>
        <button @click="resetForm2">重置</button>
      </div>
      <div class="result">
        <strong>输出值:</strong>
        <code>{{ value5 || '(空)' }}</code>
      </div>
    </div>

    <!-- 示例6: 禁用状态 -->
    <div class="demo-section">
      <h3>示例6: 禁用部分输入框</h3>
      <StringInputComponent v-model="value6" :units="unitConfigs2" />
      <div class="result">
        <strong>输出值:</strong>
        <code>{{ value6 || '(空)' }}</code>
      </div>
    </div>

    <!-- 示例7: 时间格式 -->
    <div class="demo-section">
      <h3>示例7: 时间格式</h3>
      <StringInputComponent v-model="value7" :units="timeUnits" :rules="timeRules" />
      <div class="result">
        <strong>输出值:</strong>
        <code>{{ value7 || '(空)' }}</code>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import StringInputComponent from '../StringInputComponent.vue'
import type { UnitInputConfig, ValidationRule } from '../types'

// 示例1的值(新格式:无中括号)
const value1 = ref('RMB100YUAN50JIAO5FEN8')

// 示例2的值(新格式:无中括号)
const value2 = ref('123YUAN50JIAO5FEN8')

// 示例3的值
const value3 = ref('')

// 示例4的值和配置
const value4 = ref('')
const form1 = ref<InstanceType<typeof StringInputComponent>>()
const validationResult1 = ref<{ isValid: boolean; message: string } | null>(null)

const unitConfigs1: UnitInputConfig[] = [
  {
    unit: 'RMB',
    inputConfig: {
      type: 'number',
      placeholder: '请输入金额',
      required: true
    }
  },
  {
    unit: 'YUAN',
    inputConfig: {
      type: 'number',
      placeholder: '请输入元',
      required: true
    }
  },
  {
    unit: 'JIAO',
    inputConfig: {
      type: 'number',
      placeholder: '可选',
      required: false
    }
  }
]

const validateForm1 = () => {
  if (form1.value) {
    const isValid = form1.value.validate()
    validationResult1.value = {
      isValid,
      message: isValid ? '✓ 校验通过' : '✗ 校验失败,请检查必填项'
    }
  }
}

const resetForm1 = () => {
  if (form1.value) {
    form1.value.reset()
    validationResult1.value = null
  }
}

// 示例5的值和配置
const value5 = ref('')
const form2 = ref<InstanceType<typeof StringInputComponent>>()

const customRules: Record<string, ValidationRule[]> = {
  'RMB': [
    {
      validator: (value: string) => {
        if (!value) return true
        const num = parseInt(value)
        if (isNaN(num)) return 'RMB必须是数字'
        if (num.toString() !== value) return 'RMB必须是整数'
        return true
      }
    }
  ],
  'YUAN': [
    {
      validator: (value: string) => {
        if (!value) return true
        const num = parseInt(value)
        if (isNaN(num)) return 'YUAN必须是数字'
        if (num >= 100) return 'YUAN必须小于100'
        return true
      }
    }
  ],
  'JIAO': [
    {
      validator: (value: string) => {
        if (!value) return true
        if (!/^\d$/.test(value)) return 'JIAO必须是0-9的单个数字'
        return true
      }
    }
  ],
  'FEN': [
    {
      validator: (value: string) => {
        if (!value) return true
        if (!/^\d$/.test(value)) return 'FEN必须是0-9的单个数字'
        return true
      }
    }
  ]
}

const validateForm2 = () => {
  if (form2.value) {
    form2.value.validate()
  }
}

const clearErrors2 = () => {
  if (form2.value) {
    form2.value.clearErrors()
  }
}

const resetForm2 = () => {
  if (form2.value) {
    form2.value.reset()
  }
}

// 示例6的值和配置(新格式:无中括号)
const value6 = ref('RMB888YUAN66JIAOFEN')

const unitConfigs2: UnitInputConfig[] = [
  {
    unit: 'RMB',
    inputConfig: {
      type: 'number',
      disabled: false
    }
  },
  {
    unit: 'YUAN',
    inputConfig: {
      type: 'number',
      disabled: false
    }
  },
  {
    unit: 'JIAO',
    inputConfig: {
      type: 'number',
      disabled: true
    }
  },
  {
    unit: 'FEN',
    inputConfig: {
      type: 'number',
      disabled: true
    }
  }
]

// 示例7: 时间格式(新格式:无中括号)
const value7 = ref('14时30分00秒')
const timeUnits: UnitInputConfig[] = [
  {
    unit: '',
    inputConfig: {
      type: 'number',
      placeholder: '时',
      maxlength: 2,
      required: true
    }
  },
  {
    unit: '时',
    inputConfig: {
      type: 'number',
      placeholder: '分',
      maxlength: 2,
      required: true
    }
  },
  {
    unit: '分',
    inputConfig: {
      type: 'number',
      placeholder: '秒',
      maxlength: 2,
      required: true
    }
  },
  {
    unit: '秒',
    inputConfig: {
      type: 'text',
      placeholder: '',
      disabled: true
    }
  }
]

const timeRules: Record<string, ValidationRule[]> = {
  '': [
    {
      validator: (value: string) => {
        if (!value) return '小时不能为空'
        const num = parseInt(value)
        if (isNaN(num) || num < 0 || num > 23) return '小时必须在0-23之间'
        return true
      }
    }
  ],
  '时': [
    {
      validator: (value: string) => {
        if (!value) return '分钟不能为空'
        const num = parseInt(value)
        if (isNaN(num) || num < 0 || num > 59) return '分钟必须在0-59之间'
        return true
      }
    }
  ],
  '分': [
    {
      validator: (value: string) => {
        if (!value) return '秒不能为空'
        const num = parseInt(value)
        if (isNaN(num) || num < 0 || num > 59) return '秒必须在0-59之间'
        return true
      }
    }
  ]
}
</script>

<style scoped>
.string-input-demo {
  max-width: 1200px;
  margin: 0 auto;
  padding: 24px;
}

h2 {
  font-size: 28px;
  font-weight: 600;
  color: #1a1a1a;
  margin-bottom: 32px;
  border-bottom: 2px solid #e5e7eb;
  padding-bottom: 16px;
}

h3 {
  font-size: 18px;
  font-weight: 500;
  color: #374151;
  margin-bottom: 16px;
}

.demo-section {
  margin-bottom: 48px;
  padding: 24px;
  background-color: #fff;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.hint {
  font-size: 14px;
  color: #6b7280;
  margin-bottom: 16px;
  padding: 12px;
  background-color: #f9fafb;
  border-left: 3px solid #3b82f6;
  border-radius: 4px;
}

.result {
  margin-top: 24px;
  padding: 16px;
  background-color: #f9fafb;
  border-radius: 6px;
  border: 1px solid #e5e7eb;
}

.result strong {
  font-weight: 600;
  color: #374151;
  margin-right: 8px;
}

.result code {
  display: inline-block;
  padding: 4px 12px;
  background-color: #1f2937;
  color: #10b981;
  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
  font-size: 14px;
  border-radius: 4px;
  margin-top: 8px;
}

.button-group {
  display: flex;
  gap: 12px;
  margin-top: 16px;
}

button {
  padding: 8px 24px;
  font-size: 14px;
  font-weight: 500;
  color: #fff;
  background-color: #3b82f6;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.3s;
}

button:hover {
  background-color: #2563eb;
  transform: translateY(-1px);
  box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
}

button:active {
  transform: translateY(0);
}

.validation-result {
  margin-top: 16px;
  padding: 12px 16px;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 500;
}

.validation-result.success {
  color: #059669;
  background-color: #d1fae5;
  border: 1px solid #a7f3d0;
}

.validation-result.error {
  color: #dc2626;
  background-color: #fee2e2;
  border: 1px solid #fecaca;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .string-input-demo {
    padding: 16px;
  }

  h2 {
    font-size: 24px;
  }

  .demo-section {
    padding: 16px;
  }

  .button-group {
    flex-direction: column;
  }

  button {
    width: 100%;
  }
}
</style>
查看完整演示代码

完整代码请查看 src/record/examples/StringInputDemo.vue


📋 API 文档

Props

属性类型默认值说明
unitsstring[] | UnitInputConfig[]必填单位配置
modelValuestring''v-model 绑定值
defaultInputConfigInputConfig{ type: 'text', placeholder: '请输入' }默认输入框配置
rulesRecord<string, ValidationRule[]>-校验规则(key 为单位名称)
validateOnInputbooleanfalse是否实时校验

Events

事件名参数说明
update:modelValue(value: string)值变化时触发
validate(isValid: boolean, errors: Record<string, string>)校验时触发

Methods

通过 ref 调用:

方法返回值说明
validate()boolean校验所有字段,返回是否通过
clearErrors()void清除所有错误提示
reset()void重置所有输入并清除错误

🎯 类型定义

InputConfig

typescript
interface InputConfig {
  type?: 'text' | 'number' | 'tel' | 'email' | 'password'
  placeholder?: string
  maxlength?: number
  disabled?: boolean
  required?: boolean
}

UnitInputConfig

typescript
interface UnitInputConfig {
  unit: string
  inputConfig?: InputConfig
}

ValidationRule

typescript
interface ValidationRule {
  validator: (value: string, allValues: Record<string, string>) => boolean | string
  trigger?: 'blur' | 'change'
}

🏗️ 技术实现

解析算法

组件通过智能字符串分割实现值的提取:

typescript
const parseModelValue = (value: string): ParsedValues => {
  const result: ParsedValues = {}
  let remainingValue = value

  units.forEach((unit, index) => {
    const unitIndex = remainingValue.indexOf(unit)
    if (unitIndex !== -1) {
      // 提取单位后面到下一个单位之间的内容
      const nextUnit = units[index + 1]
      // ... 提取值
    }
  })

  return result
}

工作原理

  1. 按单位顺序遍历
  2. 找到当前单位位置
  3. 提取到下一个单位之间的内容
  4. 继续处理剩余字符串

组合算法

typescript
const composeValue = (): string => {
  const parts: string[] = []

  normalizedUnits.value.forEach(unitConfig => {
    const value = inputValues.value[unitConfig.unit] || ''
    parts.push(unitConfig.unit ? `${unitConfig.unit}${value}` : value)
  })

  return parts.join('')
}

输出格式UNIT1value1UNIT2value2...


💡 常见问题

Q1: 如何让第一个输入框不显示单位标签?

typescript
:units="['', 'YUAN', 'JIAO', 'FEN']"

输出格式:123YUAN50JIAO5FEN8

Q2: 如何禁用某些输入框?

typescript
const units: UnitInputConfig[] = [
  {
    unit: 'JIAO',
    inputConfig: { disabled: true }
  }
]

Q3: 如何修改输入框宽度?

vue
<style scoped>
.custom-width :deep(.input-field) {
  min-width: 120px;
}
</style>

Q4: 如何实现实时校验?

vue
<StringInputComponent v-model="value" :units="units" :rules="rules" :validate-on-input="true" />

Q5: 校验规则如何访问其他输入框的值?

typescript
const rules = {
  YUAN: [
    {
      validator: (value: string, allValues: Record<string, string>) => {
        const rmbValue = allValues['RMB']
        // 可以基于其他字段的值进行校验
        return true
      }
    }
  ]
}

🎨 样式定制

使用 scoped 样式

vue
<template>
  <StringInputComponent v-model="value" :units="units" class="custom-input" />
</template>

<style scoped>
.custom-input :deep(.input-field) {
  border-color: #1890ff;
  border-radius: 8px;
}

.custom-input :deep(.unit-label) {
  background-color: #e6f7ff;
  color: #1890ff;
}
</style>

📊 应用场景

场景配置示例输出格式
货币输入['RMB', 'YUAN', 'JIAO', 'FEN']RMB100YUAN50JIAO5FEN8
电话号码['+', '']+8613812345678
时间格式['', '时', '分', '秒']14时30分45秒
自定义单位['前缀:', '后缀:']前缀:abc后缀:xyz

📦 文件结构

src/record/
├── StringInputComponent.vue          # 主组件
├── types.ts                          # 类型定义
├── style.module.css                  # CSS Module
├── examples/
│   ├── StringInputDemo.vue          # 演示示例
│   └── StringInputInitTest.vue      # 测试用例
└── 字符串输入组件.md                 # 本文档

💻 完整源代码

StringInputComponent.vue

点击查看完整源代码
vue
<template>
  <div class="string-input-component">
    <div class="input-wrapper">
      <template v-for="(unitConfig, index) in normalizedUnits" :key="unitConfig.unit">
        <!-- 单位标签(如果存在) -->
        <span v-if="unitConfig.unit" class="unit-label">{{ unitConfig.unit }}</span>

        <!-- 输入框 -->
        <div class="input-container">
          <input
            :value="inputValues[unitConfig.unit]"
            :type="unitConfig.inputConfig?.type || 'text'"
            :placeholder="unitConfig.inputConfig?.placeholder || '请输入'"
            :maxlength="unitConfig.inputConfig?.maxlength"
            :disabled="unitConfig.inputConfig?.disabled"
            :class="[
              'input-field',
              { 'is-error': errors[unitConfig.unit], 'is-disabled': unitConfig.inputConfig?.disabled }
            ]"
            @input="handleInput(unitConfig.unit, $event)"
            @blur="handleBlur(unitConfig.unit)"
          />
          <!-- 错误提示 -->
          <div v-if="errors[unitConfig.unit]" class="error-message">
            {{ errors[unitConfig.unit] }}
          </div>
        </div>
      </template>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import type { StringInputProps, UnitInputConfig, InputConfig, ParsedValues, ValidationRule } from './types'

/**
 * 字符串输入组件
 * 支持按单位分段输入,并组合成特定格式的字符串
 */

// 定义Props
const props = withDefaults(defineProps<StringInputProps>(), {
  modelValue: '',
  validateOnInput: false,
  defaultInputConfig: () => ({
    type: 'text',
    placeholder: '请输入',
    required: false
  })
})

// 定义Emits
const emit = defineEmits<{
  'update:modelValue': [value: string]
  validate: [isValid: boolean, errors: Record<string, string>]
}>()

// 内部状态
const inputValues = ref<Record<string, string>>({})
const errors = ref<Record<string, string>>({})

/**
 * 标准化units配置
 */
const normalizedUnits = computed<UnitInputConfig[]>(() => {
  return props.units.map(unit => {
    if (typeof unit === 'string') {
      return {
        unit,
        inputConfig: props.defaultInputConfig
      }
    }
    return unit
  })
})

/**
 * 解析modelValue字符串为各个单位的值
 * 格式:UNITvalue(如 RMB100YUAN50)
 */
const parseModelValue = (value: string): ParsedValues => {
  if (!value) return {}

  const result: ParsedValues = {}
  const units = normalizedUnits.value.map(u => u.unit)
  let remainingValue = value

  units.forEach((unit, index) => {
    if (!unit) {
      // 空单位:提取开头到第一个单位之间的内容
      const nextUnit = units[index + 1]
      if (nextUnit) {
        const nextUnitIndex = remainingValue.indexOf(nextUnit)
        if (nextUnitIndex > 0) {
          result[unit] = remainingValue.substring(0, nextUnitIndex)
          remainingValue = remainingValue.substring(nextUnitIndex)
        } else if (nextUnitIndex === -1) {
          result[unit] = remainingValue
          remainingValue = ''
        }
      } else {
        result[unit] = remainingValue
        remainingValue = ''
      }
    } else {
      // 有单位:从当前单位开始提取值
      const unitIndex = remainingValue.indexOf(unit)
      if (unitIndex !== -1) {
        let valueStart = unitIndex + unit.length
        remainingValue = remainingValue.substring(valueStart)

        const nextUnit = units[index + 1]
        let valueEnd = remainingValue.length

        if (nextUnit) {
          const nextUnitIndex = remainingValue.indexOf(nextUnit)
          if (nextUnitIndex !== -1) {
            valueEnd = nextUnitIndex
          }
        }

        result[unit] = remainingValue.substring(0, valueEnd)
        remainingValue = remainingValue.substring(valueEnd)
      }
    }
  })

  return result
}

/**
 * 根据各个输入框的值组合成完整的字符串
 * 格式:UNITvalue(如 RMB100YUAN50)
 */
const composeValue = (): string => {
  const parts: string[] = []

  normalizedUnits.value.forEach(unitConfig => {
    const value = inputValues.value[unitConfig.unit] || ''
    parts.push(unitConfig.unit ? `${unitConfig.unit}${value}` : value)
  })

  return parts.join('')
}

/**
 * 校验单个输入框
 */
const validateField = (unit: string): boolean => {
  const unitConfig = normalizedUnits.value.find(u => u.unit === unit)
  if (!unitConfig) return true

  const value = inputValues.value[unit] || ''

  // 必填校验
  if (unitConfig.inputConfig?.required && !value.trim()) {
    errors.value[unit] = `${unit || '此项'}为必填项`
    return false
  }

  // 自定义校验规则
  if (props.rules?.[unit]) {
    for (const rule of props.rules[unit]) {
      const result = rule.validator(value, inputValues.value)
      if (result !== true) {
        errors.value[unit] = typeof result === 'string' ? result : `${unit || '此项'}校验失败`
        return false
      }
    }
  }

  // 校验通过,清除错误
  delete errors.value[unit]
  return true
}

/**
 * 校验所有输入框
 */
const validateAll = (): boolean => {
  let isValid = true

  normalizedUnits.value.forEach(unitConfig => {
    if (!validateField(unitConfig.unit)) {
      isValid = false
    }
  })

  emit('validate', isValid, errors.value)
  return isValid
}

/**
 * 处理输入事件
 */
const handleInput = (unit: string, event: Event) => {
  const target = event.target as HTMLInputElement
  inputValues.value[unit] = target.value

  // 如果开启了实时校验,则在输入时进行校验
  if (props.validateOnInput) {
    validateField(unit)
  }

  // 更新modelValue
  const newValue = composeValue()
  emit('update:modelValue', newValue)
}

/**
 * 处理失焦事件
 */
const handleBlur = (unit: string) => {
  validateField(unit)
}

/**
 * 监听modelValue变化,同步到内部状态
 */
watch(
  () => props.modelValue,
  newValue => {
    const parsed = parseModelValue(newValue)
    inputValues.value = { ...parsed }
  },
  { immediate: true }
)

/**
 * 初始化时解析初始值
 */
onMounted(() => {
  if (props.modelValue) {
    const parsed = parseModelValue(props.modelValue)
    inputValues.value = { ...parsed }
  }
})

/**
 * 暴露给父组件的方法
 */
defineExpose({
  /** 校验所有字段 */
  validate: validateAll,
  /** 清除所有错误 */
  clearErrors: () => {
    errors.value = {}
  },
  /** 重置所有输入 */
  reset: () => {
    inputValues.value = {}
    errors.value = {}
    emit('update:modelValue', '')
  }
})
</script>

<style scoped>
.string-input-component {
  width: 100%;
}

.input-wrapper {
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 8px;
}

.unit-label {
  display: inline-flex;
  align-items: center;
  height: 32px;
  padding: 0 8px;
  font-size: 14px;
  font-weight: 500;
  color: #333;
  background-color: #f5f5f5;
  border-radius: 4px;
  white-space: nowrap;
}

.input-container {
  display: inline-flex;
  flex-direction: column;
  position: relative;
}

.input-field {
  height: 32px;
  padding: 4px 12px;
  font-size: 14px;
  color: #333;
  background-color: #fff;
  border: 1px solid #d9d9d9;
  border-radius: 4px;
  outline: none;
  transition: all 0.3s;
  min-width: 80px;
}

.input-field:hover:not(.is-disabled) {
  border-color: #40a9ff;
}

.input-field:focus {
  border-color: #40a9ff;
  box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}

.input-field.is-error {
  border-color: #ff4d4f;
}

.input-field.is-error:focus {
  box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2);
}

.input-field.is-disabled {
  color: rgba(0, 0, 0, 0.25);
  background-color: #f5f5f5;
  cursor: not-allowed;
}

.error-message {
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: 4px;
  font-size: 12px;
  color: #ff4d4f;
  line-height: 1.5;
  white-space: nowrap;
}

/* 响应式布局 */
@media (max-width: 768px) {
  .input-wrapper {
    flex-direction: column;
    align-items: stretch;
  }

  .unit-label,
  .input-field {
    width: 100%;
  }
}
</style>

✨ 总结

StringInputComponent 是一个功能完善、易于使用的字符串输入组件:

  • ✅ 简洁的 API 设计
  • ✅ 灵活的配置选项
  • ✅ 完整的校验功能
  • ✅ 响应式设计
  • ✅ TypeScript 支持

适用于货币输入、电话号码、时间格式等多种场景。