tn-form-item.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. <template>
  2. <view
  3. class="tn-form-item-class tn-form-item"
  4. :class="{
  5. 'tn-border-solid-bottom': elBorderBottom,
  6. 'tn-form-item__border-bottom--error': validateState === 'error' && showError('border-bottom')
  7. }"
  8. >
  9. <view
  10. class="tn-form-item__body"
  11. :style="{
  12. flexDirection: elLabelPosition == 'left' ? 'row' : 'column'
  13. }"
  14. >
  15. <!-- 处理微信小程序中设置属性的问题,不设置值的时候会变成true -->
  16. <view
  17. class="tn-form-item--left"
  18. :style="{
  19. width: wLabelWidth,
  20. flex: `0 0 ${wLabelWidth}`,
  21. marginBottom: elLabelPosition == 'left' ? 0 : '10rpx'
  22. }"
  23. >
  24. <!-- 块对齐 -->
  25. <view v-if="required || leftIcon || label" class="tn-form-item--left__content"
  26. :style="[leftContentStyle]"
  27. >
  28. <!-- nvue不支持伪元素before -->
  29. <view v-if="leftIcon" class="tn-form-item--left__content__icon">
  30. <view :class="[`tn-icon-${leftIcon}`]" :style="leftIconStyle"></view>
  31. </view>
  32. <!-- <view
  33. class="tn-form-item--left__content__label"
  34. :style="[elLabelStyle, {
  35. 'justify-content': elLabelAlign === 'left' ? 'flex-satrt' : elLabelAlign === 'center' ? 'center' : 'flex-end'
  36. }]"
  37. >
  38. {{label}}
  39. </view> -->
  40. <view
  41. class="tn-form-item--left__content__label"
  42. :style="[elLabelStyle]"
  43. >
  44. {{label}}
  45. </view>
  46. <text v-if="required" class="tn-form-item--left__content--required">*</text>
  47. </view>
  48. </view>
  49. <view class="tn-form-item--right tn-flex">
  50. <view class="tn-form-item--right__content">
  51. <view class="tn-form-item--right__content__slot">
  52. <slot></slot>
  53. </view>
  54. <view v-if="$slots.right || rightIcon" class="tn-form-item--right__content__icon tn-flex">
  55. <view v-if="rightIcon" :class="[`tn-icon-${rightIcon}`]" :style="rightIconStyle"></view>
  56. <slot name="right"></slot>
  57. </view>
  58. </view>
  59. </view>
  60. </view>
  61. <view
  62. v-if="validateState === 'error' && showError('message')"
  63. class="tn-form-item__message"
  64. :style="{
  65. paddingLeft: elLabelPosition === 'left' ? elLabelWidth + 'rpx' : '0'
  66. }"
  67. >
  68. {{validateMessage}}
  69. </view>
  70. </view>
  71. </template>
  72. <script>
  73. import Emitter from '../../libs/utils/emitter.js'
  74. import schema from '../../libs/utils/async-validator.js'
  75. // 去除警告信息
  76. schema.warning = function() {}
  77. export default {
  78. mixins: [Emitter],
  79. name: 'tn-form-item',
  80. inject: {
  81. tnForm: {
  82. default() {
  83. return null
  84. }
  85. }
  86. },
  87. props: {
  88. // label提示语
  89. label: {
  90. type: String,
  91. default: ''
  92. },
  93. // 绑定的值
  94. prop: {
  95. type: String,
  96. default: ''
  97. },
  98. // 是否显示表单域的下划线边框
  99. borderBottom: {
  100. type:Boolean,
  101. default: true
  102. },
  103. // label(标签名称)的位置
  104. // left - 左边
  105. // top - 上边
  106. labelPosition: {
  107. type: String,
  108. default: ''
  109. },
  110. // label的宽度
  111. labelWidth: {
  112. type: Number,
  113. default: 0
  114. },
  115. // label的对齐方式
  116. // left - 左对齐
  117. // top - 上对齐
  118. // right - 右对齐
  119. // bottom - 下对齐
  120. labelAlign: {
  121. type: String,
  122. default: ''
  123. },
  124. // label 的样式
  125. labelStyle: {
  126. type: Object,
  127. default() {
  128. return {}
  129. }
  130. },
  131. // 左侧图标
  132. leftIcon: {
  133. type: String,
  134. default: ''
  135. },
  136. // 右侧图标
  137. rightIcon: {
  138. type: String,
  139. default: ''
  140. },
  141. // 左侧图标样式
  142. leftIconStyle: {
  143. type: Object,
  144. default() {
  145. return {}
  146. }
  147. },
  148. // 右侧图标样式
  149. rightIconStyle: {
  150. type: Object,
  151. default() {
  152. return {}
  153. }
  154. },
  155. // 是否显示必填项的*,不做校验用途
  156. required: {
  157. type: Boolean,
  158. default: false
  159. }
  160. },
  161. computed: {
  162. // 处理微信小程序label的宽度
  163. wLabelWidth() {
  164. // 如果用户设置label为空字符串(微信小程序空字符串最终会变成字符串的'true'),意味着要将label的位置宽度设置为auto
  165. return this.elLabelPosition === 'left' ? (this.label === 'true' || this.label === '' ? 'auto' : this.elLabelWidth + 'rpx') : '100%'
  166. },
  167. // 是否显示错误提示
  168. showError() {
  169. return type => {
  170. if (this.errorType.indexOf('none') >= 0) return false
  171. else if (this.errorType.indexOf(type) >= 0) return true
  172. else return false
  173. }
  174. },
  175. // label的宽度(默认值为90)
  176. elLabelWidth() {
  177. return this.labelWidth != 0 ? this.labelWidth : (this.parentData.labelWidth != 0 ? this.parentData.labelWidth : 90)
  178. },
  179. // label的样式
  180. elLabelStyle() {
  181. return Object.keys(this.labelStyle).length ? this.labelStyle : (Object.keys(this.parentData.labelStyle).length ? this.parentData.labelStyle : {})
  182. },
  183. // label显示位置
  184. elLabelPosition() {
  185. return this.labelPosition ? this.labelPosition : (this.parentData.labelPosition ? this.parentData.labelPosition : 'left')
  186. },
  187. // label对齐方式
  188. elLabelAlign() {
  189. return this.labelAlign ? this.labelAlign : (this.parentData.labelAlign ? this.parentData.labelAlign : 'left')
  190. },
  191. // label下划线
  192. elBorderBottom() {
  193. return this.borderBottom !== '' ? this.borderBottom : (this.parentData.borderBottom !== '' ? this.parentData.borderBottom : true)
  194. },
  195. leftContentStyle() {
  196. let style = {}
  197. if (this.elLabelPosition === 'left') {
  198. switch(this.elLabelAlign) {
  199. case 'left':
  200. style.justifyContent = 'flex-start'
  201. break
  202. case 'center':
  203. style.justifyContent = 'center'
  204. break
  205. default:
  206. style.justifyContent = 'flex-end'
  207. break
  208. }
  209. }
  210. return style
  211. }
  212. },
  213. data() {
  214. return {
  215. // 默认值
  216. initialValue: '',
  217. // 是否校验成功
  218. validateState: '',
  219. // 校验失败提示信息
  220. validateMessage: '',
  221. // 错误的提示方式(参考form组件)
  222. errorType: ['message'],
  223. // 当前子组件输入的值
  224. fieldValue: '',
  225. // 父组件的参数
  226. // 由于再computed中无法得知this.parent的变化,所以放在data中
  227. parentData: {
  228. borderBottom: true,
  229. labelWidth: 90,
  230. labelPosition: 'left',
  231. labelAlign: 'left',
  232. labelStyle: {},
  233. }
  234. }
  235. },
  236. watch: {
  237. validateState(val) {
  238. this.broadcastInputError()
  239. },
  240. "tnForm.errorType"(val) {
  241. this.errorType = val
  242. this.broadcastInputError()
  243. }
  244. },
  245. mounted() {
  246. // 组件创建完成后,保存当前实例到form组件中
  247. // 支付宝、头条小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用\
  248. this.parent = this.$t.$parent.call(this, 'tn-form')
  249. if (this.parent) {
  250. // 遍历parentData属性,将parent中同名的属性赋值给parentData
  251. Object.keys(this.parentData).map(key => {
  252. this.parentData[key] = this.parent[key]
  253. })
  254. // 如果没有传入prop或者tnForm为空(单独使用form-item组件的时候),就不进行校验
  255. if (this.prop) {
  256. // 将本实例添加到父组件中
  257. this.parent.fields.push(this)
  258. this.errorType = this.parent.errorType
  259. // 设置初始值
  260. this.initialValue = this.fieldValue
  261. // 添加表单校验,这里必须要写在$nextTick中,因为tn-form的rules是通过ref手动传入的
  262. // 不在$nextTick中的话,可能会造成执行此处代码时,父组件还没通过ref把规则给tn-form,导致规则为空
  263. this.$nextTick(() => {
  264. this.setRules()
  265. })
  266. }
  267. }
  268. },
  269. beforeDestroy() {
  270. // 组件销毁前,将实例从tn-form的缓存中移除
  271. // 如果当前没有prop的话表示当前不进行删除
  272. if (this.parent && this.prop) {
  273. this.parent.fields.map((item, index) => {
  274. if (item === this) this.parent.fields.splice(index, 1)
  275. })
  276. }
  277. },
  278. methods: {
  279. // 向input组件发出错误事件
  280. broadcastInputError() {
  281. this.broadcast('tn-input', 'on-form-item-error', this.validateState === 'error' && this.showError('border'))
  282. },
  283. // 设置校验规则
  284. setRules() {
  285. let that = this
  286. // 从父组件tn-form拿到当前tn-form-item需要验证 的规则
  287. // let rules = this.getRules()
  288. // if (rules.length) {
  289. // this.isRequired = rules.some(rule => {
  290. // // 如果有必填项,就返回,没有的话,就是undefined
  291. // return rule.required
  292. // })
  293. // }
  294. // blur事件
  295. this.$on('on-form-blur', that.onFieldBlur)
  296. // change事件
  297. this.$on('on-form-change', that.onFieldChange)
  298. },
  299. // 从form的rules属性中取出当前form-item的校验规则
  300. getRules() {
  301. let rules = this.parent.rules
  302. rules = rules ? rules[this.prop] : []
  303. // 返回数值形式的值
  304. return [].concat(rules || [])
  305. },
  306. // blur事件时进行表单认证
  307. onFieldBlur() {
  308. this.validation('blur')
  309. },
  310. // change事件时进行表单认证
  311. onFieldChange() {
  312. this.validation('change')
  313. },
  314. // 过滤出符合要求的rule规则
  315. getFilterRule(triggerType = '') {
  316. let rules = this.getRules()
  317. // 整体验证表单时,triggerType为空字符串,此时返回所有规则进行验证
  318. if (!triggerType) return rules
  319. // 某些场景可能的判断规则,可能不存在trigger属性,故先判断是否存在此属性
  320. // 历遍判断规则是否有对应的事件,比如blur,change触发等的事件
  321. // 使用indexOf判断,是因为某些时候设置的验证规则的trigger属性可能为多个,比如['blur','change']
  322. return rules.filter(rule => rule.trigger && rule.trigger.indexOf(triggerType) !== -1)
  323. },
  324. // 校验数据
  325. validation(trigger, callback = ()=>{}) {
  326. // 校验之前先获取需要校验的值
  327. this.fieldValue = this.parent.model[this.prop]
  328. // blur和change是否有当前方式的校验规则
  329. let rules = this.getFilterRule(trigger)
  330. // 判断是否有验证规则,如果没有规则,也调用回调方法,否则父组件tn-form会因为
  331. // 对count变量的统计错误而无法进入上一层的回调
  332. if (!rules || rules.length === 0) {
  333. return callback('')
  334. }
  335. // 设置当前为校验中
  336. this.validateState = 'validating'
  337. // 调用async-validator的方法
  338. let validator = new schema({
  339. [this.prop]: rules
  340. })
  341. validator.validate({
  342. [this.prop]: this.fieldValue
  343. }, {
  344. firstFields: true
  345. }, (errors, fields) => {
  346. // 记录状态和报错信息
  347. this.validateState = !errors ? 'success' : 'error'
  348. this.validateMessage = errors ? errors[0].message : ''
  349. callback(this.validateMessage)
  350. })
  351. },
  352. // 清空当前item信息
  353. resetField() {
  354. this.parent.model[this.prop] = this.initialValue
  355. // 清空错误标记
  356. this.validateState = 'success'
  357. }
  358. }
  359. }
  360. </script>
  361. <style lang="scss" scoped>
  362. .tn-form-item {
  363. display: flex;
  364. flex-direction: column;
  365. padding: 20rpx 0;
  366. font-size: 28rpx;
  367. color: $tn-font-color;
  368. box-sizing: border-box;
  369. line-height: $tn-form-item-height;
  370. &__border-bottom--error:after {
  371. border-color: $tn-color-red;
  372. }
  373. &__body {
  374. display: flex;
  375. flex-direction: row;
  376. }
  377. &--left {
  378. display: flex;
  379. flex-direction: row;
  380. align-items: center;
  381. &__content {
  382. display: flex;
  383. flex-direction: row;
  384. position: relative;
  385. align-items: center;
  386. padding-right: 18rpx;
  387. flex: 1;
  388. &--required {
  389. position: relative;
  390. right: 0;
  391. vertical-align: middle;
  392. color: $tn-color-red;
  393. }
  394. &__icon {
  395. color: $tn-font-sub-color;
  396. margin-right: 8rpx;
  397. }
  398. &__label {
  399. // display: flex;
  400. // flex-direction: row;
  401. // align-items: center;
  402. // flex: 1;
  403. }
  404. }
  405. }
  406. &--right {
  407. flex: 1;
  408. &__content {
  409. display: flex;
  410. flex-direction: row;
  411. align-items: center;
  412. flex: 1;
  413. &__slot {
  414. flex: 1;
  415. /* #ifndef MP */
  416. display: flex;
  417. flex-direction: row;
  418. align-items: center;
  419. /* #endif */
  420. }
  421. &__icon {
  422. margin-left: 10rpx;
  423. color: $tn-font-sub-color;
  424. font-size: 30rpx;
  425. }
  426. }
  427. }
  428. &__message {
  429. font-size: 24rpx;
  430. line-height: 24rpx;
  431. color: $tn-color-red;
  432. margin-top: 12rpx;
  433. }
  434. }
  435. </style>