tn-number-box.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. <template>
  2. <view class="tn-number-box-class tn-number-box">
  3. <!-- 减 -->
  4. <view
  5. class="tn-number-box__btn__minus"
  6. :class="[
  7. backgroundColorClass,
  8. fontColorClass,
  9. {'tn-number-box__btn--disabled': disabled || inputValue <= min}
  10. ]"
  11. :style="{
  12. backgroundColor: backgroundColorStyle,
  13. height: $t.string.getLengthUnitValue(inputHeight),
  14. color: fontColorStyle,
  15. fontSize: fontSizeStyle
  16. }"
  17. @touchstart.stop.prevent="touchStart('minus')"
  18. @touchend.stop.prevent="clearTimer"
  19. >
  20. <view class="tn-icon-reduce"></view>
  21. </view>
  22. <!-- 输入框 -->
  23. <input
  24. v-model="inputValue"
  25. :disabled="disabledInput || disabled"
  26. :cursor-spacing="getCursorSpacing"
  27. class="tn-number-box__input"
  28. :class="[
  29. fontColorClass,
  30. {'tn-number-box__input--disabled': disabledInput || disabled}
  31. ]"
  32. :style="{
  33. width: $t.string.getLengthUnitValue(inputWidth),
  34. height: $t.string.getLengthUnitValue(inputHeight),
  35. color: fontColorStyle,
  36. fontSize: fontSizeStyle,
  37. backgroundColor: backgroundColorStyle
  38. }"
  39. @blur="blurInput"
  40. @focus="focusInput"
  41. />
  42. <!-- 加 -->
  43. <view
  44. class="tn-number-box__btn__plus"
  45. :class="[
  46. backgroundColorClass,
  47. fontColorClass,
  48. {'tn-number-box__btn--disabled': disabled || inputValue >= max}
  49. ]"
  50. :style="{
  51. backgroundColor: backgroundColorStyle,
  52. height: $t.string.getLengthUnitValue(inputHeight),
  53. color: fontColorStyle,
  54. fontSize: fontSizeStyle
  55. }"
  56. @touchstart.stop.prevent="touchStart('plus')"
  57. @touchend.stop.prevent="clearTimer"
  58. >
  59. <view class="tn-icon-add"></view>
  60. </view>
  61. </view>
  62. </template>
  63. <script>
  64. import componentsColor from '../../libs/mixin/components_color.js'
  65. export default {
  66. mixins: [componentsColor],
  67. name: 'tn-number-box',
  68. props: {
  69. value: {
  70. type: Number,
  71. default: 1
  72. },
  73. // 索引
  74. index: {
  75. type: [Number, String],
  76. default: ''
  77. },
  78. // 最小值
  79. min: {
  80. type: Number,
  81. default: 0
  82. },
  83. // 最大值
  84. max: {
  85. type: Number,
  86. default: 99999
  87. },
  88. // 步进值
  89. step: {
  90. type: Number,
  91. default: 1
  92. },
  93. // 禁用
  94. disabled: {
  95. type: Boolean,
  96. default: false
  97. },
  98. // 是否禁用输入
  99. disabledInput: {
  100. type: Boolean,
  101. default: false
  102. },
  103. // 输入框的宽度
  104. inputWidth: {
  105. type: Number,
  106. default: 88
  107. },
  108. // 输入框的高度
  109. inputHeight: {
  110. type: Number,
  111. default: 50
  112. },
  113. // 输入框和键盘之间的距离
  114. cursorSpacing: {
  115. type: Number,
  116. default: 100
  117. },
  118. // 是否开启长按进行连续递增减
  119. longPress: {
  120. type: Boolean,
  121. default: true
  122. },
  123. // 长按触发间隔
  124. longPressTime: {
  125. type: Number,
  126. default: 250
  127. },
  128. // 是否只能输入正整数
  129. positiveInteger: {
  130. type: Boolean,
  131. default: true
  132. }
  133. },
  134. computed: {
  135. getCursorSpacing() {
  136. return Number(uni.upx2px(this.cursorSpacing))
  137. }
  138. },
  139. data() {
  140. return {
  141. // 输入框的值
  142. inputValue: 1,
  143. // 长按定时器
  144. longPressTimer: null,
  145. // 标记值的改变是来自外部还是内部
  146. changeFromInner: false,
  147. // 内部定时器
  148. innerChangeTimer: null
  149. }
  150. },
  151. watch: {
  152. value(val) {
  153. // 只有value的改变是来自外部的时候,才去同步inputValue的值,否则会造成循环错误
  154. if (!this.changeFromInner) {
  155. this.updateInputValue()
  156. // 因为inputValue变化后,会触发this.handleChange(),在其中changeFromInner会再次被设置为true,
  157. // 造成外面修改值,也导致被认为是内部修改的混乱,这里进行this.$nextTick延时,保证在运行周期的最后处
  158. // 将changeFromInner设置为false
  159. this.$nextTick(() => {
  160. this.changeFromInner = false
  161. })
  162. }
  163. },
  164. inputValue(newVal, oldVal) {
  165. // 为了让用户能够删除所有输入值,重新输入内容,删除所有值后,内容为空字符串
  166. if (newVal === '') return
  167. let value = 0
  168. // 首先判断是否数值,并且在min和max之间,如果不是,使用原来值
  169. let isNumber = this.$t.test.number(newVal)
  170. if (isNumber && newVal >= this.min && newVal <= this.max) value = newVal
  171. else value = oldVal
  172. // 判断是否只能输入大于等于0的整数
  173. if (this.positiveInteger) {
  174. // 小于0或者带有小数点
  175. if (newVal < 0 || String(newVal).indexOf('.') !== -1) {
  176. value = Math.floor(newVal)
  177. // 双向绑定input的值,必须要使用$nextTick修改显示的值
  178. this.$nextTick(() => {
  179. this.inputValue = value
  180. })
  181. }
  182. }
  183. this.handleChange(value, 'change')
  184. },
  185. min() {
  186. this.updateInputValue()
  187. },
  188. max() {
  189. this.updateInputValue()
  190. }
  191. },
  192. created() {
  193. this.updateInputValue()
  194. },
  195. methods: {
  196. // 开始点击按钮
  197. touchStart(func) {
  198. // 先执行一遍方法,否则会造成松开手时,就执行了clearTimer,导致无法实现功能
  199. this[func]()
  200. // 如果没有开启长按功能,直接返回
  201. if (!this.longPress) return
  202. // 清空长按定时器,防止重复注册
  203. if (this.longPressTimer) {
  204. clearInterval(this.longPressTimer)
  205. this.longPressTimer = null
  206. }
  207. this.longPressTimer = setInterval(() => {
  208. // 执行加减操作
  209. this[func]()
  210. }, this.longPressTime)
  211. },
  212. // 清除定时器
  213. clearTimer() {
  214. this.$nextTick(() => {
  215. if (this.longPressTimer) {
  216. clearInterval(this.longPressTimer)
  217. this.longPressTimer = null
  218. }
  219. })
  220. },
  221. // 减
  222. minus() {
  223. this.computeValue('minus')
  224. },
  225. // 加
  226. plus() {
  227. this.computeValue('plus')
  228. },
  229. // 处理小数相加减出现溢出问题
  230. calcPlus(num1, num2) {
  231. let baseNum = 0, baseNum1 = 0, baseNum2 = 0
  232. try {
  233. baseNum1 = num1.toString().split('.')[1].length
  234. } catch(e) {
  235. baseNum1 = 0
  236. }
  237. try {
  238. baseNum2 = num2.toString().split('.')[1].length
  239. } catch(e) {
  240. baseNum2 = 0
  241. }
  242. baseNum = Math.pow(10, Math.max(baseNum1, baseNum2))
  243. // 精度
  244. let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2
  245. return ((num1 * baseNum + num2 * baseNum) / baseNum).toFixed(precision)
  246. },
  247. calcMinus(num1, num2) {
  248. let baseNum = 0, baseNum1 = 0, baseNum2 = 0
  249. try {
  250. baseNum1 = num1.toString().split('.')[1].length
  251. } catch(e) {
  252. baseNum1 = 0
  253. }
  254. try {
  255. baseNum2 = num2.toString().split('.')[1].length
  256. } catch(e) {
  257. baseNum2 = 0
  258. }
  259. baseNum = Math.pow(10, Math.max(baseNum1, baseNum2))
  260. // 精度
  261. let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2
  262. return ((num1 * baseNum - num2 * baseNum) / baseNum).toFixed(precision)
  263. },
  264. // 处理操作后的值
  265. computeValue(type) {
  266. uni.hideKeyboard()
  267. if (this.disabled) return
  268. let value = 0
  269. if (type === 'minus') {
  270. // 减
  271. value = this.calcMinus(this.inputValue, this.step)
  272. } else if (type === 'plus') {
  273. // 加
  274. value = this.calcPlus(this.inputValue, this.step)
  275. }
  276. // 判断是否比最小值小和操作最大值
  277. if (value < this.min || value > this.max) return
  278. this.inputValue = value
  279. this.handleChange(value, type)
  280. },
  281. // 处理用户手动输入
  282. blurInput(event) {
  283. let val = 0,
  284. value = event.detail.value
  285. // 如果为非0-9数字组成,或者其第一位数值为0,直接让其等于min值
  286. // 这里不直接判断是否正整数,是因为用户传递的props min值可能为0
  287. if (!/(^\d+$)/.test(value) || value[0] == 0) {
  288. val = this.min
  289. } else {
  290. val = +value
  291. }
  292. if (val > this.max) {
  293. val = this.max
  294. } else if (val < this.min) {
  295. val = this.min
  296. }
  297. this.$nextTick(() => {
  298. this.inputValue = val
  299. })
  300. this.handleChange(val, 'blur')
  301. },
  302. // 获取焦点
  303. focusInput() {
  304. this.$emit('focus')
  305. },
  306. // 初始化inputValue
  307. updateInputValue() {
  308. let value = this.value
  309. if (value <= this.min) {
  310. value = this.min
  311. } else if (value >= this.max) {
  312. value = this.max
  313. }
  314. this.inputValue = Number(value)
  315. },
  316. // 处理值改变状态
  317. handleChange(value, type) {
  318. if (this.disabled) return
  319. // 清除定时器,防止混乱
  320. if (this.innerChangeTimer) {
  321. clearTimeout(this.innerChangeTimer)
  322. this.innerChangeTimer = null
  323. }
  324. // 内部修改值
  325. this.changeFromInner = true
  326. // 一定时间内,清除changeFromInner标记,否则内部值改变后
  327. // 外部通过程序修改value值,将会无效
  328. this.innerChangeTimer = setTimeout(() => {
  329. this.changeFromInner = false
  330. }, 150)
  331. this.$emit('input', Number(value))
  332. this.$emit(type, {
  333. value: Number(value),
  334. index: this.index
  335. })
  336. }
  337. }
  338. }
  339. </script>
  340. <style lang="scss" scoped>
  341. .tn-number-box {
  342. display: inline-flex;
  343. align-items: center;
  344. &__btn {
  345. &__plus,&__minus {
  346. width: 60rpx;
  347. display: flex;
  348. flex-direction: row;
  349. justify-content: center;
  350. align-items: center;
  351. background-color: $tn-font-holder-color;
  352. }
  353. &__plus {
  354. border-radius: 0 8rpx 8rpx 0;
  355. }
  356. &__minus {
  357. border-radius: 8rpx 0 0 8rpx;
  358. }
  359. &--disabled {
  360. color: $tn-font-sub-color !important;
  361. background: $tn-font-holder-color !important;
  362. }
  363. }
  364. &__input {
  365. display: flex;
  366. flex-direction: row;
  367. align-items: center;
  368. justify-content: center;
  369. position: relative;
  370. text-align: center;
  371. box-sizing: border-box;
  372. padding: 0 4rpx;
  373. margin: 0 6rpx;
  374. background-color: $tn-font-holder-color;
  375. &--disabled {
  376. color: $tn-font-sub-color !important;
  377. background: $tn-font-holder-color !important;
  378. }
  379. }
  380. }
  381. </style>