l-input.uvue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. <template>
  2. <view class="l-input"
  3. :style="[styles, lStyle]"
  4. ref="rootRef"
  5. :class="[
  6. 'l-input--' + layout,
  7. classic ? 'l-input--classic-' + status : '',
  8. {'l-input--classic': classic},
  9. {'l-input--classic-disabled': classic && disabled},
  10. {'l-input--classic-focused': classic && !disabled && innerFocused},
  11. {'l-input--border': bordered && !classic}]">
  12. <view class="l-input__wrap--prefix" v-if="label != null || $slots['label'] != null || $slots['prefix-icon'] != null || prefixIcon != null">
  13. <!-- #ifndef UNI-APP-X && APP -->
  14. <view class="l-input__icon--prefix" v-if="$slots['prefix-icon'] != null || prefixIcon != null">
  15. <slot name="prefix-icon">
  16. <l-icon :name="prefixIcon" v-if="prefixIcon != null" :color="prefixIconColor" :size="prefixIconSize"></l-icon>
  17. </slot>
  18. </view>
  19. <!-- #endif -->
  20. <!-- #ifdef UNI-APP-X && APP -->
  21. <slot name="prefix-icon">
  22. <l-icon class="l-input__icon--prefix" :name="prefixIcon" v-if="prefixIcon != null" :color="prefixIconColor" :size="prefixIconSize"></l-icon>
  23. </slot>
  24. <!-- #endif -->
  25. <text class="l-input__label"
  26. :style="[labelStyle]"
  27. :class="{ 'l-input__label--gap': prefixIcon != null || $slots['prefix-icon'] != null}"
  28. v-if="label != null || $slots['label'] != null ">
  29. <slot name="label">{{label}}</slot>
  30. </text>
  31. </view>
  32. <view class="l-input__wrap">
  33. <view class="l-input__content">
  34. <input
  35. class="l-input__control"
  36. :style="[inputStyle]"
  37. :class="[
  38. 'l-input--' + align,
  39. {
  40. 'l-input__control--disabled': isDisabled,
  41. 'l-input__control--read-only': isReadonly,
  42. }
  43. ]"
  44. :maxlength="maxlength"
  45. :disabled="isDisabled || isReadonly"
  46. :placeholder="placeholder"
  47. :placeholder-style="innerPlaceholderStyle"
  48. :placeholder-class="innerPlaceholderStyle == '' ? (isDisabled || isReadonly ? 'l-input__placeholder--disabled' : 'l-input__placeholder') : ''"
  49. :value="innerValue"
  50. :type="type == 'password' ? 'text' : type"
  51. :password="type == 'password'"
  52. :focus="focus"
  53. :confirm-type="confirmType"
  54. :confirm-hold="confirmHold"
  55. :cursor="cursor"
  56. :cursor-color="cursorColor"
  57. :cursor-spacing="cursorSpacing"
  58. :adjust-position="adjustPosition"
  59. :auto-focus="autoFocus"
  60. :always-embed="alwaysEmbed"
  61. :selection-start="selectionStart"
  62. :selection-end="selectionEnd"
  63. :hold-keyboard="holdKeyboard"
  64. :safe-password-cert-path="safePasswordCertPath"
  65. :safe-password-length="safePasswordLength"
  66. :safe-password-time-stamp="safePasswordTimeStamp"
  67. :safe-password-nonce="safePasswordNonce"
  68. :safe-password-salt="safePasswordSalt"
  69. :safe-password-custom-hash="safePasswordCustomHash"
  70. :aria-label="label"
  71. :aria-roledescription="label"
  72. @input="onInput"
  73. @focus="onFocus"
  74. @blur="onBlur"
  75. @confirm="onConfirm"
  76. @keyboardheightchange="onKeyboardHeightChange"
  77. @nicknamereview="onNickNameReview" />
  78. <!-- #ifndef UNI-APP-X && APP -->
  79. <view class="l-input__wrap--clearable-icon" v-if="clearable" @click="clearInput" v-show="showClearIcon">
  80. <l-icon name="close-circle-filled" :size="clearIconSize"></l-icon>
  81. </view>
  82. <!-- #endif -->
  83. <!-- #ifdef UNI-APP-X && APP -->
  84. <view v-if="clearable" v-show="showClearIcon" @click="clearInput">
  85. <l-icon class="l-input__wrap--clearable-icon" name="close-circle-filled" :size="clearIconSize" ></l-icon>
  86. </view>
  87. <!-- <l-icon class="l-input__wrap--clearable-icon" name="close-circle-filled" size="44rpx" @click="clearInput" v-if="clearable" v-show="showClearIcon"></l-icon> -->
  88. <!-- #endif -->
  89. <view class="l-input__wrap--suffix" @click="onSuffixClick" v-if="suffix != null || $slots['suffix'] != null">
  90. <slot name="suffix">
  91. <text class="l-input__wrap--suffix-text">{{suffix}}</text>
  92. </slot>
  93. </view>
  94. <!-- #ifndef UNI-APP-X && APP -->
  95. <view class="l-input__wrap--suffix-icon" v-if="suffixIcon != null || $slots['suffix-icon'] != null">
  96. <slot name="suffix-icon">
  97. <l-icon :name="suffixIcon" @click="onSuffixIconClick" :size="suffixIconSize" :color="suffixIconColor" v-if="suffixIcon != null"></l-icon>
  98. </slot>
  99. </view>
  100. <!-- #endif -->
  101. <!-- #ifdef UNI-APP-X && APP -->
  102. <slot name="suffix-icon">
  103. <l-icon class="l-input__wrap--suffix-icon" :name="suffixIcon" :size="suffixIconSize" :color="suffixIconColor" v-if="suffixIcon != null"></l-icon>
  104. </slot>
  105. <!-- #endif -->
  106. </view>
  107. <text class="l-input__tips" :style="[tipsStyle]" :class="['l-input__tips--' + status]" v-if="tips != null && tips!.length > 0 || $slots['tips'] != null">
  108. <slot name="tips">{{tips}}</slot>
  109. </text>
  110. </view>
  111. <!-- #ifdef APP -->
  112. <view class="l-input__border" v-if="bordered && !classic"></view>
  113. <!-- #endif -->
  114. </view>
  115. </template>
  116. <script lang="uts" setup>
  117. /**
  118. * Input 输入框组件
  119. * @description 增强版输入组件,支持安全加密输入、多类型验证和智能交互
  120. * <br> 插件类型:LInputComponentPublicInstance
  121. * @tutorial https://ext.dcloud.net.cn/plugin?name=lime-input
  122. *
  123. * @property {boolean} adjustPosition 键盘弹起时页面自动上推
  124. * @property {'left'|'center'|'right'} align 文本对齐方式
  125. * @value left
  126. * @value center
  127. * @value right
  128. * @property {boolean} alwaysEmbed iOS强制同层渲染(仅iOS生效)
  129. * @property {boolean} autoFocus 自动聚焦(即将废弃,建议使用focus)
  130. * @property {boolean} bordered 边框模式
  131. * @property {'always'|'focus'} clearTrigger 清空按钮显示规则
  132. * @value always
  133. * @value focus
  134. * @property {boolean} clearable 显示清空按钮
  135. * @property {boolean} confirmHold 点击确认按钮保持键盘
  136. * @property {'send'|'search'|'next'|'go'|'done'} confirmType 确认按钮类型
  137. * @value send 显示"发送"
  138. * @value search 显示"搜索"
  139. * @value next 显示"下一个"
  140. * @value go 显示"前往"
  141. * @value done 显示"完成"
  142. * @property {number} cursor 初始光标位置(需配合selectionStart/End)
  143. * @property {string} cursorColor 光标颜色
  144. * @property {number} cursorSpacing 光标与键盘间距(单位px)
  145. * @property {boolean} disabled 禁用状态
  146. * @property {boolean} focus 强制聚焦
  147. * @property {boolean} focused 是否显示获焦样式,用于结合自定义键盘使用时显示高亮效果
  148. * @property {boolean} classic 是否使用经典边框样式
  149. * @property {boolean} holdKeyboard 聚焦时保持键盘不收起
  150. * @property {string} label 左侧标签文本
  151. * @property {'horizontal'|'vertical'} layout 标签布局
  152. * @value horizontal
  153. * @value vertical
  154. * @property {number} maxcharacter 最大字符长度(中文算2字符)
  155. * @property {number} maxlength 最大输入长度(中文算1字符)
  156. * @property {string} placeholder 占位文本
  157. * @property {string} placeholderStyle 占位符内联样式
  158. * @property {string} placeholderClass 占位符CSS类名
  159. * @property {boolean} readonly 只读模式
  160. * @property {string} safePasswordCertPath 安全证书路径(仅App)
  161. * @property {string} safePasswordCustomHash 自定义哈希算法(仅App)
  162. * @property {number} safePasswordLength 安全密码长度(仅App)
  163. * @property {string} safePasswordNonce 加密随机数(仅App)
  164. * @property {string} safePasswordSalt 加密盐值(仅App)
  165. * @property {number} safePasswordTimeStamp 加密时间戳(仅App)
  166. * @property {'default'|'success'|'warning'|'error'} status 校验状态
  167. * @value default
  168. * @value success
  169. * @value warning
  170. * @value error
  171. * @property {string} prefixIcon 前缀图标(支持图片路径/字体图标)
  172. * @property {string} prefixIconSize 前缀图标尺寸(带单位)
  173. * @property {string} prefixIconColor 前缀图标颜色
  174. * @property {string} suffix 后缀文本内容
  175. * @property {string} suffixIcon 后缀图标(支持图片路径/字体图标)
  176. * @property {string} suffixIconSize 后缀图标尺寸(带单位)
  177. * @property {string} clearIconSize 删除图标尺寸
  178. * @property {string} suffixIconColor 后缀图标颜色
  179. * @property {string} tips 底部提示文本(根据status变色)
  180. * @property {'text'|'number'|'idcard'|'digit'|'safe-password'|'password'|'nickname'} type 输入类型
  181. * @value text 普通文本
  182. * @value number 数字键盘(非强制)
  183. * @value idcard 身份证键盘(带X)
  184. * @value digit 强制数字键盘
  185. * @value safe-password 安全加密输入
  186. * @value password 密码输入
  187. * @value nickname 昵称输入(过滤特殊字符)
  188. * @property {string} value 输入值(支持v-model)
  189. * @property {string} modelValue 输入值(支持v-model)
  190. * @property {string} lStyle 根节点自定义样式
  191. * @property {string} labelStyle 标签样式
  192. * @property {string} tipsStyle 提示文本样式
  193. * @property {string} inputStyle 输入域样式
  194. * @property {string} borderColor 边框颜色(bordered时生效)
  195. * @property {string} focusedBorderColor 聚焦时边框颜色(bordered时生效)
  196. * @event {Function} change 输入时触发
  197. * @event {Function} focus 聚焦时触发
  198. * @event {Function} blur 失焦时触发
  199. * @event {Function} confirm 点击完成按钮触发
  200. * @event {Function} clear 点击清空按钮触发
  201. * @event {Function} click-icon 点击触发
  202. */
  203. import { InputProps } from './type';
  204. import { characterLimit, type CharacterLengthResult } from '@/uni_modules/lime-shared/characterLimit';
  205. import { objToCss } from '@/uni_modules/lime-shared/objToCss';
  206. // #ifdef APP
  207. const themeVars = inject('limeConfigProviderThemeVars', computed(()=> ({})))
  208. // import { useDrawBorder, DrawBorderOptions } from '@/uni_modules/lime-style/hairline'
  209. // #endif
  210. defineOptions({
  211. behaviors: ['wx://form-field']
  212. })
  213. const emit = defineEmits(['change', 'update:modelValue' ,'focus', 'blur', 'confirm', 'clear', 'keyboardheightchange', 'nicknamereview' ,'click-icon'])
  214. const props = withDefaults(defineProps<InputProps>(),{
  215. adjustPosition: true,
  216. align: 'left',
  217. alwaysEmbed: false,
  218. autoFocus: false,
  219. bordered: true,
  220. clearTrigger: 'focus',
  221. clearable: false,
  222. confirmHold: false,
  223. confirmType: 'done',
  224. cursor: 0,
  225. cursorColor: '',
  226. cursorSpacing: 0,
  227. disabled: false,
  228. focus: false,
  229. holdKeyboard: false,
  230. layout: 'horizontal',
  231. maxlength: -1,
  232. placeholder: '',
  233. placeholderStyle: '',
  234. readonly: false,
  235. safePasswordCertPath: '',
  236. safePasswordCustomHash: '',
  237. // safePasswordLength: 0,
  238. safePasswordNonce: '',
  239. safePasswordSalt: '',
  240. // safePasswordTimeStamp: 0,
  241. selectionEnd: -1,
  242. selectionStart: -1,
  243. status: 'default',
  244. // suffixIcon: '',
  245. type: 'text',
  246. classic: false,
  247. focused: false
  248. })
  249. const formItemBlur = inject<(() => void)|null>('formItemBlur', null);
  250. const formDisabled = inject<Ref<boolean|null>|null>('formDisabled', null)
  251. const formReadonly = inject<Ref<boolean|null>|null>('formReadonly', null)
  252. const calculateValue = (value: string|number):CharacterLengthResult => {
  253. const { maxlength, maxcharacter } = props;
  254. if(maxcharacter != null && maxcharacter > 0) {
  255. return characterLimit('maxcharacter', `${value}`, maxcharacter)
  256. }
  257. return {
  258. characters: `${value}`,
  259. length: `${value}`.length
  260. } as CharacterLengthResult
  261. }
  262. let _innerValue = ref<string|number>('');
  263. const innerFocused = ref(props.focus || props.focused);
  264. const innerValue = computed({
  265. set(value: string|number) {
  266. _innerValue.value = value;
  267. emit('change', value)
  268. emit('update:modelValue', value)
  269. },
  270. get():string|number {
  271. const _value = props.value ?? props.modelValue
  272. if(_innerValue.value != _value && props.type != 'number') {
  273. const { characters } = calculateValue(`${_value ?? _innerValue.value}`);
  274. return characters
  275. }
  276. return _value ?? _innerValue.value
  277. }
  278. } as WritableComputedOptions<string>)
  279. const isDisabled = computed(():boolean => props.disabled || (formDisabled?.value ?? false))
  280. const isReadonly = computed(():boolean => props.readonly || (formReadonly?.value ?? false))
  281. const innerPlaceholderStyle = computed(():string=>{
  282. let style = ''
  283. // #ifdef APP
  284. // 直接使用固定值,不依赖 themeVars
  285. // 注意:Android 原生 input 的 placeholder-style 不支持 rpx,必须使用 px
  286. style += `color: ${themeVars.value['inputPlaceholderTextColor'] ?? '#999999'};`
  287. // #endif
  288. return style + (typeof props.placeholderStyle == 'string' ? `${props.placeholderStyle}` : objToCss(props.placeholderStyle as UTSJSONObject))
  289. })
  290. const styles = computed(():Map<string, any>=>{
  291. const style = new Map<string, any>()
  292. // #ifndef UNI-APP-X && APP
  293. if(props.borderColor != null) {
  294. style.set('--l-input-border-color', props.borderColor)
  295. }
  296. if(props.focusedBorderColor != null) {
  297. style.set('--l-input-focused-border-color', props.focusedBorderColor)
  298. }
  299. // #endif
  300. return style
  301. })
  302. const showClearIcon = computed(():boolean => {
  303. const { clearTrigger, disabled, readonly } = props;
  304. if(disabled || readonly) {
  305. return false
  306. }
  307. return `${innerValue.value}`.length > 0 || clearTrigger == 'always'
  308. })
  309. const onInput = (e: UniInputEvent) => {
  310. const { value, cursor, keyCode } = e.detail;
  311. if(props.type == 'number') {
  312. const _v:number = parseFloat(`${value}`)
  313. // @ts-ignore
  314. innerValue.value = isNaN(_v) ? '' : _v;
  315. } else {
  316. const { characters } = calculateValue(value);
  317. innerValue.value = characters;
  318. }
  319. }
  320. const onFocus = (event: UniInputFocusEvent) => {
  321. innerFocused.value = true;
  322. emit('focus', event)
  323. }
  324. const onBlur = (event: UniInputBlurEvent) => {
  325. innerFocused.value = false;
  326. emit('blur', event)
  327. formItemBlur?.()
  328. }
  329. const onConfirm = (event: UniInputConfirmEvent) => {
  330. emit('confirm', event)
  331. }
  332. const onKeyboardHeightChange = (event: UniInputKeyboardHeightChangeEvent) => {
  333. emit('keyboardheightchange', event)
  334. }
  335. const onNickNameReview = (event: any) => {
  336. emit('nicknamereview', event)
  337. }
  338. const clearInput = () => {
  339. innerValue.value = '';
  340. emit('clear')
  341. }
  342. const onSuffixClick = () => {
  343. emit('click-icon', { trigger: 'suffix' })
  344. }
  345. const onSuffixIconClick = () => {
  346. emit('click-icon', { trigger: 'suffix-icon' })
  347. }
  348. watchEffect(()=> {
  349. innerFocused.value = props.focus || props.focused;
  350. })
  351. // #ifdef APP-ANDROID || APP-IOS
  352. const rootRef = ref<UniElement|null>(null);
  353. onMounted(()=>{
  354. watchEffect(()=>{
  355. if(!props.classic) {
  356. if(props.borderColor != null && !innerFocused.value) {
  357. rootRef.value?.style.setProperty('border-color', props.borderColor)
  358. }
  359. if(props.focusedBorderColor != null && innerFocused.value) {
  360. rootRef.value?.style.setProperty('border-color', props.focusedBorderColor)
  361. }
  362. }
  363. })
  364. })
  365. // #endif
  366. </script>
  367. <style lang="scss">
  368. @import './index';
  369. </style>