cl-input-number.uvue 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. <template>
  2. <view
  3. class="cl-input-number"
  4. :class="[
  5. {
  6. 'cl-input-number--disabled': disabled
  7. },
  8. pt.className
  9. ]"
  10. >
  11. <view
  12. class="cl-input-number__minus"
  13. :class="[
  14. {
  15. 'is-disabled': !isMinus
  16. },
  17. pt.op?.className,
  18. pt.op?.minus?.className
  19. ]"
  20. hover-class="!bg-surface-200"
  21. :hover-stay-time="50"
  22. :style="{
  23. height: parseRpx(size!),
  24. width: parseRpx(size!)
  25. }"
  26. @touchstart="onMinus"
  27. @touchend="longPress.stop"
  28. @touchcancel="longPress.stop"
  29. >
  30. <cl-icon
  31. name="subtract-line"
  32. :size="pt.op?.icon?.size ?? 36"
  33. :color="pt.op?.icon?.color ?? 'info'"
  34. :pt="{
  35. className: pt.op?.icon?.className
  36. }"
  37. ></cl-icon>
  38. </view>
  39. <view class="cl-input-number__value">
  40. <cl-input
  41. :model-value="`${value}`"
  42. :type="inputType"
  43. :disabled="disabled"
  44. :clearable="false"
  45. :readonly="inputable == false"
  46. :placeholder="placeholder"
  47. :hold-keyboard="false"
  48. :pt="{
  49. className: `!h-full w-[120rpx] ${pt.value?.className}`,
  50. inner: {
  51. className: `text-center ${pt.value?.input?.className}`
  52. }
  53. }"
  54. @change="onInput"
  55. ></cl-input>
  56. </view>
  57. <view
  58. class="cl-input-number__plus"
  59. :class="[
  60. {
  61. 'is-disabled': !isPlus
  62. },
  63. pt.op?.className,
  64. pt.op?.plus?.className
  65. ]"
  66. hover-class="!bg-primary-600"
  67. :hover-stay-time="50"
  68. :style="{
  69. height: parseRpx(size!),
  70. width: parseRpx(size!)
  71. }"
  72. @touchstart="onPlus"
  73. @touchend="longPress.stop"
  74. @touchcancel="longPress.stop"
  75. >
  76. <cl-icon
  77. name="add-line"
  78. :size="pt.op?.icon?.size ?? 36"
  79. :color="pt.op?.icon?.color ?? 'white'"
  80. :pt="{
  81. className: pt.op?.icon?.className
  82. }"
  83. ></cl-icon>
  84. </view>
  85. </view>
  86. </template>
  87. <script lang="ts" setup>
  88. import { computed, nextTick, ref, watch, type PropType } from "vue";
  89. import type { PassThroughProps } from "../../types";
  90. import type { ClIconProps } from "../cl-icon/props";
  91. import { useLongPress, parsePt, parseRpx } from "@/cool";
  92. defineOptions({
  93. name: "cl-input-number"
  94. });
  95. // 定义组件属性
  96. const props = defineProps({
  97. modelValue: {
  98. type: Number,
  99. default: 0
  100. },
  101. // 透传样式配置
  102. pt: {
  103. type: Object,
  104. default: () => ({})
  105. },
  106. // 占位符 - 输入框为空时显示的提示文本
  107. placeholder: {
  108. type: String,
  109. default: ""
  110. },
  111. // 步进值 - 点击加减按钮时改变的数值
  112. step: {
  113. type: Number,
  114. default: 1
  115. },
  116. // 最大值 - 允许输入的最大数值
  117. max: {
  118. type: Number,
  119. default: 100
  120. },
  121. // 最小值 - 允许输入的最小数值
  122. min: {
  123. type: Number,
  124. default: 0
  125. },
  126. // 输入框类型 - digit表示带小数点的数字键盘,number表示纯数字键盘
  127. inputType: {
  128. type: String as PropType<"digit" | "number">,
  129. default: "number"
  130. },
  131. // 是否可输入 - 控制是否允许手动输入数值
  132. inputable: {
  133. type: Boolean,
  134. default: true
  135. },
  136. // 是否禁用 - 禁用后无法输入和点击加减按钮
  137. disabled: {
  138. type: Boolean,
  139. default: false
  140. },
  141. // 组件大小 - 控制加减按钮的尺寸,支持数字或字符串形式
  142. size: {
  143. type: [Number, String] as PropType<number | string>,
  144. default: 50
  145. }
  146. });
  147. // 定义组件事件
  148. const emit = defineEmits(["update:modelValue", "change"]);
  149. // 长按操作
  150. const longPress = useLongPress();
  151. // 数值样式
  152. type ValuePassThrough = {
  153. className?: string;
  154. input?: PassThroughProps;
  155. };
  156. // 操作按钮样式
  157. type OpPassThrough = {
  158. className?: string;
  159. minus?: PassThroughProps;
  160. plus?: PassThroughProps;
  161. icon?: ClIconProps;
  162. };
  163. // 定义透传样式类型
  164. type PassThrough = {
  165. className?: string;
  166. value?: ValuePassThrough;
  167. op?: OpPassThrough;
  168. };
  169. // 解析透传样式配置
  170. const pt = computed(() => parsePt<PassThrough>(props.pt));
  171. // 绑定值
  172. const value = ref(props.modelValue);
  173. // 是否可以继续增加数值
  174. const isPlus = computed(() => !props.disabled && value.value < props.max);
  175. // 是否可以继续减少数值
  176. const isMinus = computed(() => !props.disabled && value.value > props.min);
  177. /**
  178. * 更新数值并触发事件
  179. * 确保数值在最大值和最小值范围内
  180. */
  181. function update() {
  182. nextTick(() => {
  183. let val = value.value;
  184. // 处理小于最小值的情况
  185. if (val < props.min) {
  186. val = props.min;
  187. }
  188. // 处理大于最大值的情况
  189. if (val > props.max) {
  190. val = props.max;
  191. }
  192. // 处理最小值大于最大值的异常情况
  193. if (props.min > props.max) {
  194. val = props.max;
  195. }
  196. // 小数点后两位
  197. if (props.inputType == "digit") {
  198. val = parseFloat(val.toFixed(2));
  199. }
  200. // 更新绑定值
  201. if (val != value.value) {
  202. value.value = val;
  203. }
  204. emit("update:modelValue", val);
  205. emit("change", val);
  206. });
  207. }
  208. /**
  209. * 点击加号按钮处理函数 (支持长按)
  210. * 在非禁用状态下增加step值
  211. */
  212. function onPlus() {
  213. if (props.disabled || !isPlus.value) return;
  214. longPress.start(() => {
  215. if (isPlus.value) {
  216. const val = props.max - value.value;
  217. value.value += val > props.step ? props.step : val;
  218. update();
  219. }
  220. });
  221. }
  222. /**
  223. * 点击减号按钮处理函数 (支持长按)
  224. * 在非禁用状态下减少step值
  225. */
  226. function onMinus() {
  227. if (props.disabled || !isMinus.value) return;
  228. longPress.start(() => {
  229. if (isMinus.value) {
  230. const val = value.value - props.min;
  231. value.value -= val > props.step ? props.step : val;
  232. update();
  233. }
  234. });
  235. }
  236. /**
  237. * 输入框输入处理函数
  238. * @param val 输入的字符串值
  239. */
  240. function onInput(val: string) {
  241. if (val == "") {
  242. value.value = 0;
  243. } else {
  244. value.value = parseFloat(val);
  245. }
  246. update();
  247. }
  248. // 监听绑定值变化
  249. watch(
  250. computed(() => props.modelValue),
  251. (val: number) => {
  252. value.value = val;
  253. update();
  254. },
  255. {
  256. immediate: true
  257. }
  258. );
  259. // 监听最大值变化,确保当前值不超过新的最大值
  260. watch(
  261. computed(() => props.max),
  262. update
  263. );
  264. // 监听最小值变化,确保当前值不小于新的最小值
  265. watch(
  266. computed(() => props.min),
  267. update
  268. );
  269. </script>
  270. <style lang="scss" scoped>
  271. .cl-input-number {
  272. @apply flex flex-row items-center;
  273. &__plus,
  274. &__minus {
  275. @apply flex items-center justify-center rounded-md bg-surface-100;
  276. &.is-disabled {
  277. @apply opacity-50;
  278. }
  279. }
  280. &__plus {
  281. @apply bg-primary-500;
  282. }
  283. &__value {
  284. @apply flex flex-row items-center justify-center h-full;
  285. margin: 0 12rpx;
  286. }
  287. }
  288. </style>