2025/2/8第一次更新

This commit is contained in:
爱吃咸鱼小猫咪
2025-02-08 18:50:38 +08:00
commit d7af560866
26519 changed files with 5046029 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
import { withInstall } from '../../utils'
import Input from './src/input.vue'
export const TnInput = withInstall(Input)
export default TnInput
export * from './src/input'
export type { TnInputInstance } from './src/instance'
@@ -0,0 +1,2 @@
export * from './input-custom'
export * from './use-input'
@@ -0,0 +1,125 @@
import { computed, toRef } from 'vue'
import { useFormSize } from '../../../form'
import { useComponentColor, useNamespace } from '../../../../hooks'
import { formatDomSizeValue, isEmpty } from '../../../../utils'
import type { CSSProperties, Ref } from 'vue'
import type { InputProps } from '../input'
import type { FormItemValidateStates } from '../../../form'
export const useInputCustomStyle = (
props: InputProps,
validateState: Ref<FormItemValidateStates>,
disabled: Ref<boolean>
) => {
const ns = useNamespace('input')
// 输入框的尺寸
const inputSize = useFormSize(props.size)
// 解析边框颜色
const [borderColorClass, borderColorStyle] = useComponentColor(
toRef(props, 'borderColor'),
'border'
)
// 解析字数统计颜色
const [wordLimitColorClass, wordLimitColorStyle] = useComponentColor(
toRef(props, 'wordLimitColor'),
'text'
)
// 输入框placeholder样式
const placeholderStyle = computed<string>(() => {
const style: CSSProperties = {
color: 'var(--tn-text-color-secondary)',
}
if (!isEmpty(props.placeholderStyle))
Object.assign(style, props.placeholderStyle)
return Object.entries(style)
.map(([key, value]) => `${key}:${value}`)
.join(';')
})
// 输入框所属类
const inputClass = computed<string>(() => {
const cls: string[] = [ns.b()]
// 禁止输入
if (disabled.value && props.type !== 'select') cls.push(ns.m('disabled'))
// 设置边框尺寸
if (inputSize.value) cls.push(ns.m(inputSize.value))
// 设置文字对齐方式
if (props.textAlign) cls.push(ns.m(`text-${props.textAlign}`))
// 是否发生错误
if (validateState.value === 'error') cls.push(ns.m('error'))
// 设置边框
if (props.border || props.underline || validateState.value === 'error') {
cls.push(props.underline ? 'tn-border-bottom' : 'tn-border')
if (validateState.value === 'error') cls.push('tn-red_border')
else if (borderColorClass.value) cls.push(borderColorClass.value)
}
if (props.underline) {
cls.push(ns.m('underline'))
}
if (props.customClass) cls.push(props.customClass)
return cls.join(' ')
})
// 输入框样式
const inputStyle = computed<CSSProperties>(() => {
const style: CSSProperties = {}
// 设置高度
if (props.height) style.height = formatDomSizeValue(props.height)
// 设置边框颜色
if (
props.border &&
borderColorStyle.value &&
validateState.value !== 'error'
)
style.borderColor = borderColorStyle.value
if (!isEmpty(props.customStyle)) Object.assign(style, props.customStyle)
return style
})
// 字数统计类
const wordLimitClass = computed<string>(() => {
const cls: string[] = [ns.e('word-limit')]
if (wordLimitColorClass.value) cls.push(wordLimitColorClass.value)
return cls.join(' ')
})
// 字数统计样式
const wordLimitStyle = computed<CSSProperties>(() => {
const style: CSSProperties = {}
if (!wordLimitColorClass.value) {
style.color = wordLimitColorStyle.value || 'var(--tn-color-gray)'
}
return style
})
return {
ns,
inputClass,
inputStyle,
placeholderStyle,
wordLimitClass,
wordLimitStyle,
}
}
@@ -0,0 +1,172 @@
import { computed, nextTick, ref, watch } from 'vue'
import { trim } from '../../../../libs/lodash'
import {
CHANGE_EVENT,
INPUT_EVENT,
UPDATE_MODEL_EVENT,
} from '../../../../constants'
import {
FormValidateIconsMap,
debugWarn,
isEmptyVariableInDefault,
} from '../../../../utils'
import { useToggle } from '../../../../hooks'
import { useFormDisabled, useFormItem } from '../../../form'
import type { SetupContext } from 'vue'
import type { FormItemValidateStates } from '../../../form'
import type { InputEmit, InputProps } from '../input'
export const useInput = (
props: InputProps,
emits: SetupContext<InputEmit>['emit']
) => {
const { form, formItem } = useFormItem()
// 输入框内容
const inputText = ref<string>(
String(isEmptyVariableInDefault(props.modelValue, ''))
)
watch(
() => props.modelValue,
(val) => {
inputText.value = String(isEmptyVariableInDefault(val, ''))
if (props.validateEvent) {
formItem?.validate?.('change').catch((err) => {
debugWarn(err)
})
}
}
)
// 显示/隐藏密码状态
const [passwordVisible, togglePasswordVisible] = useToggle(false)
// 是否显示状态图标
const needStatusIcon = computed(() =>
isEmptyVariableInDefault(form?.statusIcon, false)
)
// 校验状态
const validateState = computed(() =>
isEmptyVariableInDefault<FormItemValidateStates>(
formItem?.validateState,
''
)
)
// 校验状态图标
const validateIcon = computed(
() => validateState.value && FormValidateIconsMap[validateState.value]
)
// 密码显示密码图标
const passwordIcon = computed(() =>
passwordVisible.value ? 'eye-hide' : 'eye'
)
// 是否显示图标
const showIcon = computed(() => {
let status = false
if (validateState.value && needStatusIcon.value && validateIcon.value)
status = true
if (props.showPassword) status = true
if (props.rightIcon) status = true
if (props.clearable) status = true
return status
})
// 输入框禁止事件
const disabled = useFormDisabled(props.disabled)
// 是否显示字数统计
const showWordLimit = computed<boolean>(
() =>
props.type === 'textarea' && !!props?.maxlength && !!props?.showWordLimit
)
// 当前的字数
const currentWordCount = computed<number>(() => {
if (props.showWordLimit && props.type === 'textarea') {
return inputText.value?.length || 0
}
return 0
})
// 内容输入触发事件
const inputInputEvent = (event: any) => {
const { value } = event.detail
_updateInputText(value)
}
// 输入框聚焦事件
const inputFocusEvent = (event: any) => {
emits('focus', event)
}
// 输入框失去焦点事件
const inputBlurEvent = (event: any) => {
emits('blur', event)
if (props.validateEvent) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
formItem?.validate?.('blur').catch((err) => {
debugWarn(err)
})
}
}
// 点击完成时触发事件
const confirmEvent = (event: any) => {
const { value } = event.detail
emits('confirm', _formatInputText(value))
}
// 点击清除按钮
const clearClickEvent = () => {
if (disabled.value) return
_updateInputText('')
emits('clear')
}
// 更新输入框内容
const _updateInputText = (value: string) => {
value = props.trim ? trim(value) : value
// inputText.value = value
emits(UPDATE_MODEL_EVENT, _formatInputText(value))
nextTick(() => {
emits(INPUT_EVENT, _formatInputText(value))
emits(CHANGE_EVENT, _formatInputText(value))
})
}
// 输入框点击事件
const inputClickEvent = () => {
if (props.type === 'select') {
emits('click')
}
}
const _formatInputText = (value: string) => {
if (value === '') return ''
if (props.type === 'number' || props.type === 'digit')
return Number.parseFloat(value)
return value
}
return {
inputText,
needStatusIcon,
validateState,
validateIcon,
passwordVisible,
passwordIcon,
showIcon,
disabled,
showWordLimit,
currentWordCount,
togglePasswordVisible,
inputInputEvent,
inputFocusEvent,
inputBlurEvent,
clearClickEvent,
confirmEvent,
inputClickEvent,
}
}
+277
View File
@@ -0,0 +1,277 @@
import {
buildProps,
definePropType,
isNumber,
isObject,
isString,
} from '../../../utils'
import {
useComponentCustomStyleProp,
useFormSizeProps,
} from '../../base/composables/use-component-common-props'
import {
CHANGE_EVENT,
INPUT_EVENT,
UPDATE_MODEL_EVENT,
} from '../../../constants'
import type { ExtractPropTypes } from 'vue'
const inputTypes = [
'text',
'number',
'idcard',
'digit',
'textarea',
'password',
'select',
] as const
const inputConfirmTypes = [
'',
'send',
'search',
'next',
'go',
'done',
'return',
] as const
export const inputProps = buildProps({
/**
* @description 绑定的值
*/
modelValue: {
type: definePropType<string | number | null | undefined>([
String,
Number,
Object,
]),
default: '',
},
/**
* @description 输入框尺寸
*/
size: useFormSizeProps,
/**
* @description 输入框高度
*/
height: {
type: [String, Number],
},
/**
* @description 是否禁用
*/
disabled: Boolean,
/**
* @description 输入框类型
*/
type: {
type: String,
values: inputTypes,
default: 'text',
},
/**
* @description 输入框占位文本
*/
placeholder: String,
/**
* @description 文字对齐方式
*/
textAlign: {
type: String,
values: ['left', 'center', 'right'],
default: 'left',
},
/**
* @description 输入框占位文本的样式
*/
placeholderStyle: useComponentCustomStyleProp,
/**
* @description 是否显示边框
*/
border: {
type: Boolean,
default: true,
},
/**
* @description 边框颜色
*/
borderColor: {
type: String,
default: 'tn-gray-disabled',
},
/**
* @description 下划线边框
*/
underline: Boolean,
/**
* @description 自定义样式
*/
customStyle: useComponentCustomStyleProp,
/**
* @description 自定义类名
*/
customClass: String,
/**
* @description 最大可输入长度,设置为 -1 的时候不限制最大长度
*/
maxlength: {
type: Number,
default: -1,
},
/**
* @description 根据内容自动调整高度,仅在 textarea 模式下生效,如果设置了 height 则优先级最高
*/
autoHeight: {
type: Boolean,
default: true,
},
/**
* @description 设置键盘右下角按钮的文字,仅在使用系统键盘时生效
*/
confirmType: {
type: String,
values: inputConfirmTypes,
default: 'done',
},
/**
* @description 获取焦点
*/
focus: Boolean,
/**
* @description 是否展示清除按钮
*/
clearable: Boolean,
/**
* @description 是否显示切换密码显示/隐藏按钮,仅在 type="password" 时生效
*/
showPassword: {
type: Boolean,
default: true,
},
/**
* @description 指定光标与键盘的距离,单位 px
*/
cursorSpacing: {
type: Number,
default: 0,
},
/**
* @description 光标起始位置,自动聚集时有效,需与selection-end搭配使用
*/
selectionStart: {
type: Number,
default: -1,
},
/**
* @description 光标结束位置,自动聚集时有效,需与selection-start搭配使用
*/
selectionEnd: {
type: Number,
default: -1,
},
/**
* @description 是否展示键盘上方带有”完成“按钮那一栏
*/
showConfirmBar: {
type: Boolean,
default: true,
},
/**
* @description 显示输入框右图标
*/
rightIcon: String,
/**
* @description 自动去除两端空格
*/
trim: {
type: Boolean,
default: true,
},
/**
* @description 显示字数统计,只有在 textarea 模式下且设置maxlength时生效
*/
showWordLimit: {
type: Boolean,
default: false,
},
/**
* @description 字数统计文字颜色,以tn开头使用图鸟内置的颜色
*/
wordLimitColor: String,
/**
* @description 输入时是否触发表单验证
*/
validateEvent: {
type: Boolean,
default: true,
},
})
export const inputEmits = {
[UPDATE_MODEL_EVENT]: (value: string | number) =>
isString(value) || isNumber(value),
/**
* @description 输入框输入内容时触发
*/
[INPUT_EVENT]: (value: string | number) => isString(value) || isNumber(value),
/**
* @description 输入框内容变化时触发
*/
[CHANGE_EVENT]: (value: string | number) =>
isString(value) || isNumber(value),
/**
* @description 输入框点击时触发
*/
click: () => true,
/**
* @description 输入框聚焦时触发
*/
focus: (e: InputFocusEvent) => isObject(e),
/**
* @description 输入框失去焦点时触发
*/
blur: (e: InputBlurEvent) => isObject(e),
/**
* @description 点击清除按钮时触发
*/
clear: () => true,
/**
* @description 点击键盘右下角按钮时触发
*/
confirm: (value: string | number) => isString(value) || isNumber(value),
}
export type InputProps = ExtractPropTypes<typeof inputProps>
export type InputEmit = typeof inputEmits
export type InputType = (typeof inputTypes)[number]
export type InputConfirmType = (typeof inputConfirmTypes)[number]
/**
* @description 输入框聚焦事件
*/
export interface InputFocusEvent {
detail: {
/**
* @description 输入框内容
*/
value: string
/**
* @description 键盘高度
*/
height: number
}
}
/**
* @description 输入框失去焦点事件
*/
export interface InputBlurEvent {
detail: {
/**
* @description 输入框内容
*/
value: string
}
}
+146
View File
@@ -0,0 +1,146 @@
<script lang="ts" setup>
import TnIcon from '../../icon/src/icon.vue'
import { inputEmits, inputProps } from './input'
import { useInput, useInputCustomStyle } from './composables'
const props = defineProps(inputProps)
const emits = defineEmits(inputEmits)
const {
inputText,
needStatusIcon,
validateState,
validateIcon,
passwordVisible,
passwordIcon,
showIcon,
disabled,
showWordLimit,
currentWordCount,
togglePasswordVisible,
inputInputEvent,
inputFocusEvent,
inputBlurEvent,
clearClickEvent,
confirmEvent,
inputClickEvent,
} = useInput(props, emits)
const {
ns,
inputClass,
inputStyle,
placeholderStyle,
wordLimitClass,
wordLimitStyle,
} = useInputCustomStyle(props, validateState, disabled)
</script>
<template>
<view
:class="[
inputClass,
`${type === 'textarea' ? ns.m('textarea') : ns.m('input')}`,
ns.is('show-word-limit', showWordLimit),
]"
:style="inputStyle"
@tap="inputClickEvent"
>
<view v-if="$slots.prefix" :class="[ns.em('slot', 'left')]">
<slot name="prefix" />
</view>
<!-- 文本域 -->
<textarea
v-if="type === 'textarea'"
:class="[
ns.e('base'),
ns.e('textarea'),
ns.is('custom-height', !!height),
]"
:value="inputText"
:placeholder="placeholder"
:placeholder-style="placeholderStyle"
:disabled="disabled"
:maxlength="maxlength"
:focus="focus"
:confirm-type="confirmType"
:auto-height="!height && autoHeight"
:selection-start="selectionStart"
:selection-end="selectionEnd"
:cursor-spacing="cursorSpacing"
:show-confirm-bar="showConfirmBar"
@input="inputInputEvent"
@focus="inputFocusEvent"
@blur="inputBlurEvent"
@confirm="confirmEvent"
/>
<!-- 文本框 -->
<input
v-else
:class="[ns.e('base'), ns.e('input'), ns.em('input', type)]"
:type="type === 'password' || type === 'select' ? 'text' : type"
:value="inputText"
:placeholder="placeholder"
:password="type === 'password' && !passwordVisible"
:placeholder-style="placeholderStyle"
:disabled="disabled || type === 'select'"
:maxlength="maxlength"
:focus="focus"
:confirm-type="confirmType"
:selection-start="selectionStart"
:selection-end="selectionEnd"
:cursor-spacing="cursorSpacing"
:show-confirm-bar="showConfirmBar"
@input="inputInputEvent"
@focus="inputFocusEvent"
@blur="inputBlurEvent"
@confirm="confirmEvent"
/>
<!-- 图标 -->
<view v-if="showIcon" :class="[ns.e('icon')]">
<!-- 右边图标 -->
<view v-if="rightIcon" :class="[ns.em('icon', 'custom')]">
<TnIcon :name="rightIcon" />
</view>
<!-- 密码显示/隐藏 -->
<view
v-if="type === 'password' && showPassword"
:class="[ns.em('icon', 'password')]"
@tap.stop="togglePasswordVisible"
>
<TnIcon :name="passwordIcon" />
</view>
<!-- 清除按钮 -->
<view
v-else-if="clearable && inputText"
:class="[ns.em('icon', 'clear')]"
@tap.stop="clearClickEvent"
>
<TnIcon name="close" />
</view>
<!-- 错误提示图标 -->
<view
v-if="validateState && validateIcon && needStatusIcon"
:class="[ns.em('icon', `validate-${validateState}`)]"
>
<TnIcon :name="validateIcon" />
</view>
</view>
<view v-if="$slots.suffix" :class="[ns.em('slot', 'right')]">
<slot name="suffix" />
</view>
<!-- 字数统计 -->
<view
v-if="showWordLimit"
:class="[wordLimitClass]"
:style="wordLimitStyle"
>
{{ currentWordCount }} / {{ maxlength }}
</view>
</view>
</template>
<style lang="scss" scoped>
@import '../../../theme-chalk/src/input.scss';
</style>
@@ -0,0 +1,3 @@
import type TnInput from './input.vue'
export type TnInputInstance = InstanceType<typeof TnInput>