tn-input.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. <template>
  2. <view
  3. class="tn-input-class tn-input"
  4. :class="{
  5. 'tn-input--border': border,
  6. 'tn-input--error': validateState
  7. }"
  8. :style="{
  9. padding: `0 ${border ? 20 : 0}rpx`,
  10. borderColor: borderColor,
  11. textAlign: inputAlign
  12. }"
  13. @tap.stop="inputClick"
  14. >
  15. <textarea
  16. v-if="type === 'textarea'"
  17. class="tn-input__input tn-input__textarea"
  18. :style="[inputStyle]"
  19. :value="defaultValue"
  20. :placeholder="placeholder"
  21. :placeholderStyle="placeholderStyle"
  22. :disabled="disabled || type === 'select'"
  23. :maxlength="maxLength"
  24. :fixed="fixed"
  25. :focus="focus"
  26. :autoHeight="autoHeight"
  27. :selectionStart="elSelectionStart"
  28. :selectionEnd="elSelectionEnd"
  29. :cursorSpacing="cursorSpacing"
  30. :showConfirmBar="showConfirmBar"
  31. @input="handleInput"
  32. @blur="handleBlur"
  33. @focus="onFocus"
  34. @confirm="onConfirm"
  35. />
  36. <input
  37. v-else
  38. class="tn-input__input"
  39. :type="type === 'password' ? 'text' : type"
  40. :style="[inputStyle]"
  41. :value="defaultValue"
  42. :password="type === 'password' && !showPassword"
  43. :placeholder="placeholder"
  44. :placeholderStyle="placeholderStyle"
  45. :disabled="disabled || type === 'select'"
  46. :maxlength="maxLength"
  47. :focus="focus"
  48. :confirmType="confirmType"
  49. :selectionStart="elSelectionStart"
  50. :selectionEnd="elSelectionEnd"
  51. :cursorSpacing="cursorSpacing"
  52. :showConfirmBar="showConfirmBar"
  53. @input="handleInput"
  54. @blur="handleBlur"
  55. @focus="onFocus"
  56. @confirm="onConfirm"
  57. />
  58. <!-- 右边的icon -->
  59. <view class="tn-input__right-icon tn-flex tn-flex-col-center">
  60. <!-- 清除按钮 -->
  61. <view
  62. v-if="clearable && value !== '' && focused"
  63. class="tn-input__right-icon__item tn-input__right-icon__clear"
  64. @tap="onClear"
  65. >
  66. <view class="icon tn-icon-close"></view>
  67. </view>
  68. <view
  69. v-else-if="type === 'text' && !focused && showRightIcon && rightIcon !== ''"
  70. class="tn-input__right-icon__item tn-input__right-icon__clear"
  71. >
  72. <view class="icon" :class="[`tn-icon-${rightIcon}`]"></view>
  73. </view>
  74. <!-- 显示密码按钮 -->
  75. <view
  76. v-if="passwordIcon && type === 'password'"
  77. class="tn-input__right-icon__item tn-input__right-icon__clear"
  78. @tap="showPassword = !showPassword"
  79. >
  80. <view v-if="!showPassword" class="tn-icon-eye-hide"></view>
  81. <view v-else class="icon tn-icon-eye"></view>
  82. </view>
  83. <!-- 可选项箭头 -->
  84. <view
  85. v-if="type === 'select'"
  86. class="tn-input__right-icon__item tn-input__right-icon__select"
  87. :class="{
  88. 'tn-input__right-icon__select--reverse': selectOpen
  89. }"
  90. >
  91. <view class="icon tn-icon-up-triangle"></view>
  92. </view>
  93. </view>
  94. </view>
  95. </template>
  96. <script>
  97. import Emitter from '../../libs/utils/emitter.js'
  98. export default {
  99. mixins: [Emitter],
  100. name: 'tn-input',
  101. props: {
  102. value: {
  103. type: [String, Number],
  104. default: ''
  105. },
  106. // 输入框的类型
  107. type: {
  108. type: String,
  109. default: 'text'
  110. },
  111. // 输入框文字对齐方式
  112. inputAlign: {
  113. type: String,
  114. default: 'left'
  115. },
  116. // 文本框为空时显示的信息
  117. placeholder: {
  118. type: String,
  119. default: ''
  120. },
  121. placeholderStyle: {
  122. type: String,
  123. default: 'color: #AAAAAA'
  124. },
  125. // 是否禁用输入框
  126. disabled: {
  127. type: Boolean,
  128. default: false
  129. },
  130. // 可输入文字的最大长度
  131. maxLength: {
  132. type: Number,
  133. default: 255
  134. },
  135. // 输入框高度
  136. height: {
  137. type: Number,
  138. default: 0
  139. },
  140. // 根据内容自动调整高度
  141. autoHeight: {
  142. type: Boolean,
  143. default: true
  144. },
  145. // 键盘右下角显示的文字,仅在text时生效
  146. confirmType: {
  147. type: String,
  148. default: 'done'
  149. },
  150. // 输入框自定义样式
  151. customStyle: {
  152. type: Object,
  153. default() {
  154. return {}
  155. }
  156. },
  157. // 是否固定输入框
  158. fixed: {
  159. type: Boolean,
  160. default: false
  161. },
  162. // 是否自动获取焦点
  163. focus: {
  164. type: Boolean,
  165. default: false
  166. },
  167. // 当type为password时,是否显示右侧密码图标
  168. passwordIcon: {
  169. type: Boolean,
  170. default: true
  171. },
  172. // 当type为 input或者textarea时是否显示边框
  173. border: {
  174. type: Boolean,
  175. default: false
  176. },
  177. // 边框的颜色
  178. borderColor: {
  179. type: String,
  180. default: '#dcdfe6'
  181. },
  182. // 当type为select时,旋转右侧图标,标记当时select是打开还是关闭
  183. selectOpen: {
  184. type: Boolean,
  185. default: false
  186. },
  187. // 是否可清空
  188. clearable: {
  189. type: Boolean,
  190. default: true
  191. },
  192. // 光标与键盘的距离
  193. cursorSpacing: {
  194. type: Number,
  195. default: 0
  196. },
  197. // selectionStart和selectionEnd需要搭配使用,自动聚焦时生效
  198. // 光标起始位置
  199. selectionStart: {
  200. type: Number,
  201. default: -1
  202. },
  203. // 光标结束位置
  204. selectionEnd: {
  205. type: Number,
  206. default: -1
  207. },
  208. // 自动去除两端空格
  209. trim: {
  210. type: Boolean,
  211. default: true
  212. },
  213. // 是否显示键盘上方的完成按钮
  214. showConfirmBar: {
  215. type: Boolean,
  216. default: true
  217. },
  218. // 是否在输入框内最右边显示图标
  219. showRightIcon: {
  220. type: Boolean,
  221. default: false
  222. },
  223. // 最右边图标的名称
  224. rightIcon: {
  225. type: String,
  226. default: ''
  227. }
  228. },
  229. computed: {
  230. // 输入框样式
  231. inputStyle() {
  232. let style = {}
  233. // 如果没有设置高度,根据不同的类型设置一个默认值
  234. style.minHeight = this.height ? this.height + 'rpx' :
  235. this.type === 'textarea' ? this.textareaHeight + 'rpx' : this.inputHeight + 'rpx'
  236. style = Object.assign(style, this.customStyle)
  237. return style
  238. },
  239. // 光标起始位置
  240. elSelectionStart() {
  241. return String(this.selectionStart)
  242. },
  243. // 光标结束位置
  244. elSelectionEnd() {
  245. return String(this.selectionEnd)
  246. }
  247. },
  248. data() {
  249. return {
  250. // 默认值
  251. defaultValue: this.value,
  252. // 输入框高度
  253. inputHeight: 70,
  254. // textarea的高度
  255. textareaHeight: 100,
  256. // 标记验证的状态
  257. validateState: false,
  258. // 标记是否获取到焦点
  259. focused: false,
  260. // 是否预览密码
  261. showPassword: false,
  262. // 用于头条小程序,判断@input中,前后的值是否发生了变化,因为头条中文下,按下键没有输入内容,也会触发@input事件
  263. lastValue: '',
  264. }
  265. },
  266. watch: {
  267. value(newVal, oldVal) {
  268. this.defaultValue = newVal
  269. // 当值发生变化时,并且type为select时,不会触发input事件
  270. // 模拟input事件
  271. if (newVal !== oldVal && this.type === 'select') {
  272. this.handleInput({
  273. detail: {
  274. value: newVal
  275. }
  276. })
  277. }
  278. }
  279. },
  280. created() {
  281. // 监听form-item发出的错误事件,将输入框变成红色
  282. this.$on("on-form-item-error", this.onFormItemError)
  283. },
  284. methods: {
  285. /**
  286. * input事件
  287. */
  288. handleInput(event) {
  289. let value = event.detail.value
  290. // 是否需要去掉空格
  291. if (this.trim) value = this.$t.string.trim(value)
  292. // 原生事件
  293. this.$emit('input', value)
  294. // model赋值
  295. this.defaultValue = value
  296. // 过一个生命周期再发送事件给tn-form-item,否则this.$emit('input')更新了父组件的值,但是微信小程序上
  297. // 尚未更新到tn-form-item,导致获取的值为空,从而校验混论
  298. // 这里不能延时时间太短,或者使用this.$nextTick,否则在头条上,会造成混乱
  299. setTimeout(() => {
  300. // 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
  301. // #ifdef MP-TOUTIAO
  302. if (this.$t.string.trim(value) === this.lastValue) return
  303. this.lastValue = value
  304. // #endif
  305. // 发送当前的值到form-item进行校验
  306. this.dispatch('tn-form-item','on-form-change', value)
  307. }, 40)
  308. },
  309. /**
  310. * blur事件
  311. */
  312. handleBlur(event) {
  313. let value = event.detail.value
  314. // 由于点击清除图标也会触发blur事件,导致图标消失从而无法点击
  315. setTimeout(() => {
  316. this.focused = false
  317. }, 100)
  318. // 原生事件
  319. this.$emit('blur', value)
  320. // 过一个生命周期再发送事件给tn-form-item,否则this.$emit('blur')更新了父组件的值,但是微信小程序上
  321. // 尚未更新到tn-form-item,导致获取的值为空,从而校验混论
  322. // 这里不能延时时间太短,或者使用this.$nextTick,否则在头条上,会造成混乱
  323. setTimeout(() => {
  324. // 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
  325. // #ifdef MP-TOUTIAO
  326. if (this.$t.string.trim(value) === this.lastValue) return
  327. this.lastValue = value
  328. // #endif
  329. // 发送当前的值到form-item进行校验
  330. this.dispatch('tn-form-item','on-form-blur', value)
  331. }, 40)
  332. },
  333. // 处理校验错误
  334. onFormItemError(status) {
  335. this.validateState = status
  336. },
  337. // 聚焦事件
  338. onFocus(event) {
  339. this.focused = true
  340. this.$emit('focus')
  341. },
  342. // 点击确认按钮事件
  343. onConfirm(event) {
  344. this.$emit('confirm', event.detail.value)
  345. },
  346. // 清除事件
  347. onClear(event) {
  348. this.$emit('input', '')
  349. },
  350. // 点击事件
  351. inputClick() {
  352. this.$emit('click')
  353. }
  354. }
  355. }
  356. </script>
  357. <style lang="scss" scoped>
  358. .tn-input {
  359. display: flex;
  360. flex-direction: row;
  361. position: relative;
  362. flex: 1;
  363. &__input {
  364. font-size: 28rpx;
  365. color: $tn-font-color;
  366. flex: 1;
  367. }
  368. &__textarea {
  369. width: auto;
  370. font-size: 28rpx;
  371. color: $tn-font-color;
  372. padding: 10rpx 0;
  373. line-height: normal;
  374. flex: 1;
  375. }
  376. &--border {
  377. border-radius: 6rpx;
  378. border: 2rpx solid $tn-border-solid-color;
  379. }
  380. &--error {
  381. border-color: $tn-color-red !important;
  382. }
  383. &__right-icon {
  384. line-height: 1;
  385. .icon {
  386. color: $tn-font-sub-color;
  387. }
  388. &__item {
  389. margin-left: 10rpx;
  390. }
  391. &__clear {
  392. .icon {
  393. font-size: 32rpx;
  394. }
  395. }
  396. &__select {
  397. transition: transform .4s;
  398. .icon {
  399. font-size: 26rpx;
  400. }
  401. &--reverse {
  402. transform: rotate(-180deg);
  403. }
  404. }
  405. }
  406. }
  407. </style>