Appearance
字符串输入组件
📝 组件概述
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:基础货币输入
带类型限制的货币输入,支持数字类型和占位符
RMBYUANJIAOFEN
输出值:
RMB100YUAN50JIAO5FEN8查看代码
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:必填校验
支持必填字段验证,点击校验按钮触发表单验证
RMBYUANJIAO
输出值:
RMB11YUAN123查看代码
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
RMBYUAN
输出值:
(空)查看代码
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>
<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: 简单使用(货币单位)
RMBYUANJIAOFEN
输出值:
RMB100YUAN50JIAO5FEN8示例2: 支持空单位(第一个输入框无单位标签)
YUANJIAOFEN
输出值:
123YUAN50JIAO5FEN8示例3: 配置输入框属性
金额元角分
输出值:
(空)示例4: 必填校验
RMBYUANJIAO
输出值:
(空)示例5: 自定义校验规则
规则:RMB必须是整数,YUAN必须小于100,JIAO和FEN必须是0-9的数字
RMBYUANJIAOFEN
输出值:
(空)示例6: 禁用部分输入框
RMBYUANJIAOFEN
输出值:
RMB888YUAN66JIAOFEN示例7: 时间格式
时分秒
输出值:
14时30分00秒查看完整演示代码
完整代码请查看 src/record/examples/StringInputDemo.vue
📋 API 文档
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
units | string[] | UnitInputConfig[] | 必填 | 单位配置 |
modelValue | string | '' | v-model 绑定值 |
defaultInputConfig | InputConfig | { type: 'text', placeholder: '请输入' } | 默认输入框配置 |
rules | Record<string, ValidationRule[]> | - | 校验规则(key 为单位名称) |
validateOnInput | boolean | false | 是否实时校验 |
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
}工作原理:
- 按单位顺序遍历
- 找到当前单位位置
- 提取到下一个单位之间的内容
- 继续处理剩余字符串
组合算法
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 支持
适用于货币输入、电话号码、时间格式等多种场景。