Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 

496 рядки
15 KiB

  1. <template>
  2. <view class="u-number-box">
  3. <view
  4. class="u-number-box__slot cursor-pointer"
  5. @tap.stop="clickHandler('minus')"
  6. @touchstart="onTouchStart('minus')"
  7. @touchend.stop="clearTimeout"
  8. v-if="showMinus && !hideMinus && $slots.minus"
  9. >
  10. <slot name="minus" />
  11. </view>
  12. <view
  13. v-else-if="showMinus && !hideMinus"
  14. class="u-number-box__minus cursor-pointer"
  15. @tap.stop="clickHandler('minus')"
  16. @touchstart="onTouchStart('minus')"
  17. @touchend.stop="clearTimeout"
  18. hover-class="u-number-box__minus--hover"
  19. hover-stay-time="150"
  20. :class="{ 'u-number-box__minus--disabled': isDisabled('minus') }"
  21. :style="[buttonStyle('minus')]"
  22. >
  23. <up-icon
  24. name="minus"
  25. :color="isDisabled('minus') ? resolvedDisabledIconColor : resolvedColor"
  26. size="15"
  27. bold
  28. :customStyle="iconStyle"
  29. ></up-icon>
  30. </view>
  31. <template v-if="!hideMinus">
  32. <slot name="input">
  33. <!-- #ifdef MP-WEIXIN -->
  34. <input
  35. :disabled="disabledInput || disabled"
  36. :cursor-spacing="getCursorSpacing"
  37. :class="{ 'u-number-box__input--disabled': disabled || disabledInput }"
  38. :value="currentValue"
  39. class="u-number-box__input"
  40. @blur="onBlur"
  41. @focus="onFocus"
  42. @input="onInput"
  43. :type="integer ? 'number' : 'digit'"
  44. :style="[inputStyle]"
  45. />
  46. <!-- #endif -->
  47. <!-- #ifndef MP-WEIXIN -->
  48. <input
  49. :disabled="disabledInput || disabled"
  50. :cursor-spacing="getCursorSpacing"
  51. :class="{ 'u-number-box__input--disabled': disabled || disabledInput }"
  52. v-model="currentValue"
  53. class="u-number-box__input"
  54. @blur="onBlur"
  55. @focus="onFocus"
  56. @input="onInput"
  57. :type="integer ? 'number' : 'digit'"
  58. :style="[inputStyle]"
  59. />
  60. <!-- #endif -->
  61. </slot>
  62. </template>
  63. <view
  64. class="u-number-box__slot cursor-pointer"
  65. @tap.stop="clickHandler('plus')"
  66. @touchstart="onTouchStart('plus')"
  67. @touchend.stop="clearTimeout"
  68. v-if="showPlus && $slots.plus"
  69. >
  70. <slot name="plus" />
  71. </view>
  72. <view
  73. v-else-if="showPlus"
  74. class="u-number-box__plus cursor-pointer"
  75. @tap.stop="clickHandler('plus')"
  76. @touchstart="onTouchStart('plus')"
  77. @touchend.stop="clearTimeout"
  78. hover-class="u-number-box__plus--hover"
  79. hover-stay-time="150"
  80. :class="{ 'u-number-box__minus--disabled': isDisabled('plus') }"
  81. :style="[buttonStyle('plus')]"
  82. >
  83. <up-icon
  84. name="plus"
  85. :color="isDisabled('plus') ? resolvedDisabledIconColor : resolvedColor"
  86. size="15"
  87. bold
  88. :customStyle="iconStyle"
  89. ></up-icon>
  90. </view>
  91. </view>
  92. </template>
  93. <script>
  94. import { props } from './props';
  95. import { mpMixin } from '../../libs/mixin/mpMixin';
  96. import { mixin } from '../../libs/mixin/mixin';
  97. import { getPx, addUnit } from '../../libs/function/index';
  98. /**
  99. * numberBox 步进器
  100. * @description 该组件一般用于商城购物选择物品数量的场景。
  101. * @tutorial https://uview-plus.jiangruyi.com/components/numberBox.html
  102. * @property {String | Number} name 步进器标识符,在change回调返回
  103. * @property {String | Number} value 用于双向绑定的值,初始化时设置设为默认min值(最小值) (默认 0 )
  104. * @property {String | Number} min 最小值 (默认 1 )
  105. * @property {String | Number} max 最大值 (默认 Number.MAX_SAFE_INTEGER )
  106. * @property {String | Number} step 加减的步长,可为小数 (默认 1 )
  107. * @property {Boolean} integer 是否只允许输入整数 (默认 false )
  108. * @property {Boolean} disabled 是否禁用,包括输入框,加减按钮 (默认 false )
  109. * @property {Boolean} disabledInput 是否禁用输入框 (默认 false )
  110. * @property {Boolean} asyncChange 是否开启异步变更,开启后需要手动控制输入值 (默认 false )
  111. * @property {String | Number} inputWidth 输入框宽度,单位为px (默认 35 )
  112. * @property {Boolean} showMinus 是否显示减少按钮 (默认 true )
  113. * @property {Boolean} showPlus 是否显示增加按钮 (默认 true )
  114. * @property {String | Number} decimalLength 显示的小数位数
  115. * @property {Boolean} longPress 是否开启长按加减手势 (默认 true )
  116. * @property {String} color 输入框文字和加减按钮图标的颜色 (默认 '#323233' )
  117. * @property {String | Number} buttonSize 按钮大小,宽高等于此值,单位px,输入框高度和此值保持一致 (默认 30 )
  118. * @property {String} bgColor 输入框和按钮的背景颜色 (默认 '#EBECEE' )
  119. * @property {String | Number} cursorSpacing 指定光标于键盘的距离,避免键盘遮挡输入框,单位px (默认 100 )
  120. * @property {Boolean} disablePlus 是否禁用增加按钮 (默认 false )
  121. * @property {Boolean} disableMinus 是否禁用减少按钮 (默认 false )
  122. * @property {Object | String} iconStyle 加减按钮图标的样式
  123. *
  124. * @event {Function} onFocus 输入框活动焦点
  125. * @event {Function} onBlur 输入框失去焦点
  126. * @event {Function} onInput 输入框值发生变化
  127. * @event {Function} onChange
  128. * @example <u-number-box v-model="value" @change="valChange"></u-number-box>
  129. */
  130. export default {
  131. name: 'u-number-box',
  132. mixins: [mpMixin, mixin, props],
  133. data() {
  134. return {
  135. // 输入框实际操作的值
  136. currentValue: '',
  137. // 定时器
  138. longPressTimer: null
  139. }
  140. },
  141. watch: {
  142. // 多个值之间,只要一个值发生变化,都要重新检查check()函数
  143. watchChange(n) {
  144. this.check()
  145. },
  146. // #ifdef VUE2
  147. // 监听v-mode的变化,重新初始化内部的值
  148. value(n) {
  149. if (n !== this.currentValue) {
  150. this.currentValue = this.format(this.value)
  151. }
  152. },
  153. // #endif
  154. // #ifdef VUE3
  155. // 监听v-mode的变化,重新初始化内部的值
  156. modelValue: {
  157. handler: function (newV, oldV) {
  158. if (newV !== this.currentValue) {
  159. this.currentValue = this.format(this.modelValue)
  160. }
  161. },
  162. immediate: true
  163. }
  164. // #endif
  165. },
  166. computed: {
  167. resolvedColor() {
  168. return this.color || this.upThemeVar('--up-main-color', '#303133')
  169. },
  170. resolvedDisabledIconColor() {
  171. return this.upThemeVar('--up-disabled-color', '#c8c9cc')
  172. },
  173. resolvedBgColor() {
  174. return this.bgColor || this.upThemeVar('--up-bg-color', '#EBECEE')
  175. },
  176. resolvedDisabledBgColor() {
  177. return this.disabledBgColor || this.upThemeVar('--up-card-bg-color', '#f7f8fa')
  178. },
  179. resolvedInputBgColor() {
  180. return this.inputBgColor || this.resolvedBgColor
  181. },
  182. hideMinus() {
  183. return this.currentValue == 0 && this.miniMode == true
  184. },
  185. getCursorSpacing() {
  186. // 判断传入的单位,如果为px单位,需要转成px
  187. return getPx(this.cursorSpacing)
  188. },
  189. // 按钮的样式
  190. buttonStyle() {
  191. return (type) => {
  192. const style = {
  193. backgroundColor: this.resolvedBgColor,
  194. width: addUnit(this.buttonWidth),
  195. height: addUnit(this.buttonSize),
  196. color: this.resolvedColor,
  197. borderRadius: this.buttonRadius
  198. }
  199. if (this.isDisabled(type)) {
  200. style.backgroundColor = this.resolvedDisabledBgColor
  201. }
  202. return style
  203. }
  204. },
  205. // 输入框的样式
  206. inputStyle() {
  207. const style = {
  208. color: this.resolvedColor,
  209. backgroundColor: this.resolvedInputBgColor,
  210. height: addUnit(this.buttonSize),
  211. width: addUnit(this.inputWidth)
  212. }
  213. return style
  214. },
  215. // 用于监听多个值发生变化
  216. watchChange() {
  217. return [this.integer, this.decimalLength, this.min, this.max]
  218. },
  219. isDisabled() {
  220. return (type) => {
  221. if (type === 'plus') {
  222. // 在点击增加按钮情况下,判断整体的disabled,是否单独禁用增加按钮,以及当前值是否大于最大的允许值
  223. return (
  224. this.disabled ||
  225. this.disablePlus ||
  226. this.currentValue >= this.max
  227. )
  228. }
  229. // 点击减少按钮同理
  230. return (
  231. this.disabled ||
  232. this.disableMinus ||
  233. this.currentValue <= this.min
  234. )
  235. }
  236. },
  237. },
  238. mounted() {
  239. this.init()
  240. },
  241. // #ifdef VUE3
  242. emits: ['update:modelValue', 'focus', 'blur', 'overlimit', 'change', 'plus', 'minus'],
  243. // #endif
  244. methods: {
  245. init() {
  246. // #ifdef VUE3
  247. this.currentValue = this.format(this.modelValue)
  248. // #endif
  249. // #ifdef VUE2
  250. this.currentValue = this.format(this.value)
  251. // #endif
  252. },
  253. // 格式化整理数据,限制范围
  254. format(value) {
  255. value = this.filter(value)
  256. // 如果为空字符串,那么设置为0,同时将值转为Number类型
  257. value = value === '' ? 0 : +value
  258. // 对比最大最小值,取在min和max之间的值
  259. value = Math.max(Math.min(this.max, value), this.min)
  260. // 如果设定了最大的小数位数,使用toFixed去进行格式化
  261. if (this.decimalLength !== null) {
  262. value = value.toFixed(this.decimalLength)
  263. }
  264. return value
  265. },
  266. // 过滤非法的字符
  267. filter(value) {
  268. // 只允许0-9之间的数字,"."为小数点,"-"为负数时候使用
  269. value = String(value).replace(/[^0-9.-]/g, '')
  270. // 如果只允许输入整数,则过滤掉小数点后的部分
  271. if (this.integer && value.indexOf('.') !== -1) {
  272. value = value.split('.')[0]
  273. }
  274. return value;
  275. },
  276. check() {
  277. // 格式化了之后,如果前后的值不相等,那么设置为格式化后的值
  278. const val = this.format(this.currentValue);
  279. if (val !== this.currentValue) {
  280. this.currentValue = val
  281. this.emitChange(val)
  282. }
  283. },
  284. // 判断是否出于禁止操作状态
  285. // isDisabled(type) {
  286. // if (type === 'plus') {
  287. // // 在点击增加按钮情况下,判断整体的disabled,是否单独禁用增加按钮,以及当前值是否大于最大的允许值
  288. // return (
  289. // this.disabled ||
  290. // this.disablePlus ||
  291. // this.currentValue >= this.max
  292. // )
  293. // }
  294. // // 点击减少按钮同理
  295. // return (
  296. // this.disabled ||
  297. // this.disableMinus ||
  298. // this.currentValue <= this.min
  299. // )
  300. // },
  301. // 输入框活动焦点
  302. onFocus(event) {
  303. this.$emit('focus', {
  304. ...event.detail,
  305. name: this.name,
  306. })
  307. },
  308. // 输入框失去焦点
  309. onBlur(event) {
  310. // 对输入值进行格式化,失焦时强制修正到合法范围(如最小值)
  311. const raw = event.detail.value
  312. const value = (raw === '' || raw === null || raw === undefined)
  313. ? this.min
  314. : this.format(raw)
  315. this.emitChange(value)
  316. // 发出blur事件
  317. this.$emit(
  318. 'blur',{
  319. ...event.detail,
  320. name: this.name,
  321. }
  322. )
  323. },
  324. // 输入框值发生变化
  325. onInput(e) {
  326. const {
  327. value = ''
  328. } = e.detail || {}
  329. // 为空时不立即修正,等失焦时再处理,允许用户清空后输入新值
  330. if (value === '') {
  331. this.currentValue = ''
  332. return
  333. }
  334. let formatted = this.filter(value)
  335. // https://github.com/ijry/uview-plus/issues/613
  336. this.emitChange(value);
  337. // 最大允许的小数长度
  338. if (this.decimalLength !== null && formatted.indexOf('.') !== -1) {
  339. const pair = formatted.split('.')
  340. formatted = `${pair[0]}.${pair[1].slice(0, this.decimalLength)}`
  341. }
  342. formatted = this.format(formatted)
  343. this.emitChange(formatted);
  344. // #ifdef MP-WEIXIN
  345. return formatted
  346. // #endif
  347. },
  348. // 发出change事件,type目前只支持点击时有值,手动输入不支持。
  349. emitChange(value, type = '') {
  350. // 如果开启了异步变更值,则不修改内部的值,需要用户手动在外部通过v-model变更
  351. if (!this.asyncChange) {
  352. this.$nextTick(() => {
  353. // #ifdef VUE3
  354. this.$emit('update:modelValue', value)
  355. // #endif
  356. // #ifdef VUE2
  357. this.$emit('input', value)
  358. // #endif
  359. this.currentValue = value
  360. this.$forceUpdate()
  361. })
  362. }
  363. this.$emit('change', {
  364. value,
  365. name: this.name,
  366. type: type // 当前变更类型
  367. });
  368. },
  369. onChange() {
  370. const {
  371. type
  372. } = this
  373. if (this.isDisabled(type)) {
  374. return this.$emit('overlimit', type)
  375. }
  376. const diff = type === 'minus' ? -this.step : +this.step
  377. const value = this.format(this.add(+this.currentValue, diff))
  378. this.emitChange(value, type)
  379. this.$emit(type)
  380. },
  381. // 对值扩大后进行四舍五入,再除以扩大因子,避免出现浮点数操作的精度问题
  382. add(num1, num2) {
  383. const cardinal = Math.pow(10, 10);
  384. return Math.round((num1 + num2) * cardinal) / cardinal
  385. },
  386. // 点击加减按钮
  387. clickHandler(type) {
  388. this.type = type
  389. this.onChange()
  390. },
  391. longPressStep() {
  392. // 每隔一段时间,重新调用longPressStep方法,实现长按加减
  393. this.clearTimeout()
  394. this.longPressTimer = setTimeout(() => {
  395. this.onChange()
  396. this.longPressStep()
  397. }, 250);
  398. },
  399. onTouchStart(type) {
  400. if (!this.longPress) return
  401. this.clearTimeout()
  402. this.type = type
  403. // 一定时间后,默认达到长按状态
  404. this.longPressTimer = setTimeout(() => {
  405. this.onChange()
  406. this.longPressStep()
  407. }, 600)
  408. },
  409. // 触摸结束,清除定时器,停止长按加减
  410. onTouchEnd() {
  411. if (!this.longPress) return
  412. this.clearTimeout()
  413. },
  414. // 清除定时器
  415. clearTimeout() {
  416. clearTimeout(this.longPressTimer)
  417. this.longPressTimer = null
  418. }
  419. }
  420. }
  421. </script>
  422. <style lang="scss" scoped>
  423. $u-numberBox-hover-bgColor: var(--up-border-color, #E6E6E6) !default;
  424. $u-numberBox-disabled-color: var(--up-disabled-color, #c8c9cc) !default;
  425. $u-numberBox-disabled-bgColor: var(--up-card-bg-color, #f7f8fa) !default;
  426. $u-numberBox-plus-radius: 4px !default;
  427. $u-numberBox-minus-radius: 4px !default;
  428. $u-numberBox-input-text-align: center !default;
  429. $u-numberBox-input-font-size: 15px !default;
  430. $u-numberBox-input-padding: 0 !default;
  431. $u-numberBox-input-margin: 0 2px !default;
  432. $u-numberBox-input-disabled-color: var(--up-disabled-color, #c8c9cc) !default;
  433. $u-numberBox-input-disabled-bgColor: var(--up-bg-color, #f2f3f5) !default;
  434. .u-number-box {
  435. @include flex(row);
  436. align-items: center;
  437. &__slot {
  438. /* #ifndef APP-NVUE */
  439. touch-action: none;
  440. /* #endif */
  441. }
  442. &__plus,
  443. &__minus {
  444. width: 35px;
  445. @include flex;
  446. justify-content: center;
  447. align-items: center;
  448. /* #ifndef APP-NVUE */
  449. touch-action: none;
  450. /* #endif */
  451. &--hover {
  452. background-color: $u-numberBox-hover-bgColor !important;
  453. }
  454. &--disabled {
  455. color: $u-numberBox-disabled-color;
  456. background-color: $u-numberBox-disabled-bgColor;
  457. }
  458. }
  459. &__plus {
  460. border-top-right-radius: $u-numberBox-plus-radius;
  461. border-bottom-right-radius: $u-numberBox-plus-radius;
  462. }
  463. &__minus {
  464. border-top-left-radius: $u-numberBox-minus-radius;
  465. border-bottom-left-radius: $u-numberBox-minus-radius;
  466. }
  467. &__input {
  468. position: relative;
  469. text-align: $u-numberBox-input-text-align;
  470. font-size: $u-numberBox-input-font-size;
  471. padding: $u-numberBox-input-padding;
  472. margin: $u-numberBox-input-margin;
  473. @include flex;
  474. align-items: center;
  475. justify-content: center;
  476. &--disabled {
  477. color: $u-numberBox-input-disabled-color;
  478. background-color: $u-numberBox-input-disabled-bgColor;
  479. }
  480. }
  481. }
  482. </style>