wd-input.vue 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. <template>
  2. <view :class="rootClass" :style="customStyle" @click="handleClick">
  3. <view v-if="label || $slots.label" :class="labelClass" :style="labelStyle">
  4. <text v-if="isRequired && markerSide === 'before'" class="wd-input__required wd-input__required--left">*</text>
  5. <view v-if="prefixIcon || $slots.prefix" class="wd-input__prefix">
  6. <wd-icon v-if="prefixIcon && !$slots.prefix" custom-class="wd-input__icon" :name="prefixIcon" @click="onClickPrefixIcon" />
  7. <slot v-else name="prefix"></slot>
  8. </view>
  9. <view class="wd-input__label-inner">
  10. <text v-if="label && !$slots.label">{{ label }}</text>
  11. <slot v-else-if="$slots.label" name="label"></slot>
  12. </view>
  13. <text v-if="isRequired && markerSide === 'after'" class="wd-input__required">*</text>
  14. </view>
  15. <view class="wd-input__body">
  16. <view class="wd-input__value">
  17. <view v-if="(prefixIcon || $slots.prefix) && !label" class="wd-input__prefix">
  18. <wd-icon v-if="prefixIcon && !$slots.prefix" custom-class="wd-input__icon" :name="prefixIcon" @click="onClickPrefixIcon" />
  19. <slot v-else name="prefix"></slot>
  20. </view>
  21. <input
  22. :class="[
  23. 'wd-input__inner',
  24. prefixIcon ? 'wd-input__inner--prefix' : '',
  25. showWordCount ? 'wd-input__inner--count' : '',
  26. alignRight ? 'is-align-right' : '',
  27. customInputClass
  28. ]"
  29. :type="type"
  30. :password="showPassword && !isPwdVisible"
  31. v-model="inputValue"
  32. :placeholder="placeholderValue"
  33. :disabled="disabled || readonly"
  34. :maxlength="maxlength"
  35. :focus="focused"
  36. :confirm-type="confirmType"
  37. :confirm-hold="confirmHold"
  38. :cursor="cursor"
  39. :cursor-spacing="cursorSpacing"
  40. :placeholder-style="placeholderStyle"
  41. :selection-start="selectionStart"
  42. :selection-end="selectionEnd"
  43. :adjust-position="adjustPosition"
  44. :hold-keyboard="holdKeyboard"
  45. :always-embed="alwaysEmbed"
  46. :placeholder-class="inputPlaceholderClass"
  47. :ignoreCompositionEvent="ignoreCompositionEvent"
  48. :inputmode="inputmode"
  49. @input="handleInput"
  50. @focus="handleFocus"
  51. @blur="handleBlur"
  52. @confirm="handleConfirm"
  53. @keyboardheightchange="handleKeyboardheightchange"
  54. />
  55. <view v-if="props.readonly" class="wd-input__readonly-mask" />
  56. <view v-if="showClear || showPassword || suffixIcon || showWordCount || $slots.suffix" class="wd-input__suffix">
  57. <wd-icon v-if="showClear" custom-class="wd-input__clear" name="error-fill" @click="handleClear" />
  58. <wd-icon v-if="showPassword" custom-class="wd-input__icon" :name="isPwdVisible ? 'view' : 'eye-close'" @click="togglePwdVisible" />
  59. <view v-if="showWordCount" class="wd-input__count">
  60. <text
  61. :class="[
  62. inputValue && String(inputValue).length > 0 ? 'wd-input__count-current' : '',
  63. String(inputValue).length > maxlength! ? 'is-error' : ''
  64. ]"
  65. >
  66. {{ String(inputValue).length }}
  67. </text>
  68. /{{ maxlength }}
  69. </view>
  70. <wd-icon v-if="suffixIcon && !$slots.suffix" custom-class="wd-input__icon" :name="suffixIcon" @click="onClickSuffixIcon" />
  71. <slot v-else name="suffix"></slot>
  72. </view>
  73. </view>
  74. <view v-if="errorMessage" class="wd-input__error-message">{{ errorMessage }}</view>
  75. </view>
  76. </view>
  77. </template>
  78. <script lang="ts">
  79. export default {
  80. name: 'wd-input',
  81. options: {
  82. virtualHost: true,
  83. addGlobalClass: true,
  84. styleIsolation: 'shared'
  85. }
  86. }
  87. </script>
  88. <script lang="ts" setup>
  89. import { computed, ref, watch, useSlots, type Slots } from 'vue'
  90. import wdIcon from '../wd-icon/wd-icon.vue'
  91. import { isDef, objToStyle, pause, isEqual } from '../common/util'
  92. import { useCell } from '../composables/useCell'
  93. import { FORM_KEY, type FormItemRule } from '../wd-form/types'
  94. import { useParent } from '../composables/useParent'
  95. import { useTranslate } from '../composables/useTranslate'
  96. import { inputProps } from './types'
  97. interface InputSlots extends Slots {
  98. prefix?: () => any
  99. suffix?: () => any
  100. label?: () => any
  101. }
  102. const props = defineProps(inputProps)
  103. const emit = defineEmits([
  104. 'update:modelValue',
  105. 'clear',
  106. 'blur',
  107. 'focus',
  108. 'input',
  109. 'keyboardheightchange',
  110. 'confirm',
  111. 'clicksuffixicon',
  112. 'clickprefixicon',
  113. 'click'
  114. ])
  115. const slots = useSlots() as InputSlots
  116. const { translate } = useTranslate('input')
  117. const isPwdVisible = ref<boolean>(false)
  118. const clearing = ref<boolean>(false) // 是否正在清空操作,避免重复触发失焦
  119. const focused = ref<boolean>(false) // 控制聚焦
  120. const focusing = ref<boolean>(false) // 当前是否激活状态
  121. const inputValue = ref<string | number>(getInitValue()) // 输入框的值
  122. const cell = useCell()
  123. watch(
  124. () => props.focus,
  125. (newValue) => {
  126. focused.value = newValue
  127. },
  128. { immediate: true, deep: true }
  129. )
  130. watch(
  131. () => props.modelValue,
  132. (newValue) => {
  133. inputValue.value = isDef(newValue) ? String(newValue) : ''
  134. }
  135. )
  136. const { parent: form } = useParent(FORM_KEY)
  137. const placeholderValue = computed(() => {
  138. return isDef(props.placeholder) ? props.placeholder : translate('placeholder')
  139. })
  140. /**
  141. * 展示清空按钮
  142. */
  143. const showClear = computed(() => {
  144. const { disabled, readonly, clearable, clearTrigger } = props
  145. if (clearable && !readonly && !disabled && inputValue.value && (clearTrigger === 'always' || (props.clearTrigger === 'focus' && focusing.value))) {
  146. return true
  147. } else {
  148. return false
  149. }
  150. })
  151. /**
  152. * 展示字数统计
  153. */
  154. const showWordCount = computed(() => {
  155. const { disabled, readonly, maxlength, showWordLimit } = props
  156. return Boolean(!disabled && !readonly && isDef(maxlength) && maxlength > -1 && showWordLimit)
  157. })
  158. /**
  159. * 表单错误提示信息
  160. */
  161. const errorMessage = computed(() => {
  162. if (form && props.prop && form.errorMessages && form.errorMessages[props.prop]) {
  163. return form.errorMessages[props.prop]
  164. } else {
  165. return ''
  166. }
  167. })
  168. // 是否展示必填
  169. const isRequired = computed(() => {
  170. let formRequired = false
  171. if (form && form.props.rules) {
  172. const rules = form.props.rules
  173. for (const key in rules) {
  174. if (Object.prototype.hasOwnProperty.call(rules, key) && key === props.prop && Array.isArray(rules[key])) {
  175. formRequired = rules[key].some((rule: FormItemRule) => rule.required)
  176. }
  177. }
  178. }
  179. return props.required || props.rules.some((rule) => rule.required) || formRequired
  180. })
  181. const rootClass = computed(() => {
  182. return `wd-input ${props.label || slots.label ? 'is-cell' : ''} ${props.center ? 'is-center' : ''} ${cell.border.value ? 'is-border' : ''} ${
  183. props.size ? 'is-' + props.size : ''
  184. } ${props.error ? 'is-error' : ''} ${props.disabled ? 'is-disabled' : ''} ${
  185. inputValue.value && String(inputValue.value).length > 0 ? 'is-not-empty' : ''
  186. } ${props.noBorder ? 'is-no-border' : ''} ${props.customClass}`
  187. })
  188. const labelClass = computed(() => {
  189. return `wd-input__label ${props.customLabelClass}`
  190. })
  191. const inputPlaceholderClass = computed(() => {
  192. return `wd-input__placeholder ${props.placeholderClass}`
  193. })
  194. const labelStyle = computed(() => {
  195. return props.labelWidth
  196. ? objToStyle({
  197. 'min-width': props.labelWidth,
  198. 'max-width': props.labelWidth
  199. })
  200. : ''
  201. })
  202. // 状态初始化
  203. function getInitValue() {
  204. const formatted = formatValue(props.modelValue)
  205. if (!isValueEqual(formatted, props.modelValue)) {
  206. emit('update:modelValue', formatted)
  207. }
  208. return formatted
  209. }
  210. function formatValue(value: string | number) {
  211. const { maxlength } = props
  212. if (isDef(maxlength) && maxlength !== -1 && String(value).length > maxlength) {
  213. return value.toString().slice(0, maxlength)
  214. }
  215. return value
  216. }
  217. function togglePwdVisible() {
  218. isPwdVisible.value = !isPwdVisible.value
  219. }
  220. async function handleClear() {
  221. focusing.value = false
  222. inputValue.value = ''
  223. if (props.focusWhenClear) {
  224. clearing.value = true
  225. focused.value = false
  226. }
  227. await pause()
  228. if (props.focusWhenClear) {
  229. focused.value = true
  230. focusing.value = true
  231. }
  232. emit('update:modelValue', inputValue.value)
  233. emit('clear')
  234. }
  235. async function handleBlur() {
  236. // 等待150毫秒,clear执行完毕
  237. await pause(150)
  238. if (clearing.value) {
  239. clearing.value = false
  240. return
  241. }
  242. focusing.value = false
  243. emit('blur', {
  244. value: inputValue.value
  245. })
  246. }
  247. function handleFocus({ detail }: any) {
  248. focusing.value = true
  249. emit('focus', detail)
  250. }
  251. function handleInput({ detail }: any) {
  252. emit('update:modelValue', inputValue.value)
  253. emit('input', detail)
  254. }
  255. function handleKeyboardheightchange({ detail }: any) {
  256. emit('keyboardheightchange', detail)
  257. }
  258. function handleConfirm({ detail }: any) {
  259. emit('confirm', detail)
  260. }
  261. function onClickSuffixIcon() {
  262. emit('clicksuffixicon')
  263. }
  264. function onClickPrefixIcon() {
  265. emit('clickprefixicon')
  266. }
  267. function handleClick(event: MouseEvent) {
  268. emit('click', event)
  269. }
  270. function isValueEqual(value1: number | string, value2: number | string) {
  271. return isEqual(String(value1), String(value2))
  272. }
  273. </script>
  274. <style lang="scss" scoped>
  275. @import './index.scss';
  276. </style>
  277. <style lang="scss">
  278. @import './placeholder.scss';
  279. </style>