cl-input-number.uvue 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. <template>
  2. <view
  3. class="cl-input-number"
  4. :class="[
  5. {
  6. 'cl-input-number--disabled': isDisabled
  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="isDisabled"
  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. @blur="onBlur"
  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. import { useForm } from "../../hooks";
  93. defineOptions({
  94. name: "cl-input-number"
  95. });
  96. // 定义组件属性
  97. const props = defineProps({
  98. modelValue: {
  99. type: Number,
  100. default: 0
  101. },
  102. // 透传样式配置
  103. pt: {
  104. type: Object,
  105. default: () => ({})
  106. },
  107. // 占位符 - 输入框为空时显示的提示文本
  108. placeholder: {
  109. type: String,
  110. default: ""
  111. },
  112. // 步进值 - 点击加减按钮时改变的数值
  113. step: {
  114. type: Number,
  115. default: 1
  116. },
  117. // 最大值 - 允许输入的最大数值
  118. max: {
  119. type: Number,
  120. default: 100
  121. },
  122. // 最小值 - 允许输入的最小数值
  123. min: {
  124. type: Number,
  125. default: 0
  126. },
  127. // 输入框类型 - digit表示带小数点的数字键盘,number表示纯数字键盘
  128. inputType: {
  129. type: String as PropType<"digit" | "number">,
  130. default: "number"
  131. },
  132. // 是否可输入 - 控制是否允许手动输入数值
  133. inputable: {
  134. type: Boolean,
  135. default: true
  136. },
  137. // 是否禁用 - 禁用后无法输入和点击加减按钮
  138. disabled: {
  139. type: Boolean,
  140. default: false
  141. },
  142. // 组件大小 - 控制加减按钮的尺寸,支持数字或字符串形式
  143. size: {
  144. type: [Number, String] as PropType<number | string>,
  145. default: 50
  146. }
  147. });
  148. // 定义组件事件
  149. const emit = defineEmits(["update:modelValue", "change"]);
  150. // 长按操作
  151. const longPress = useLongPress();
  152. // cl-form 上下文
  153. const { disabled } = useForm();
  154. // 是否禁用
  155. const isDisabled = computed(() => {
  156. return disabled.value || props.disabled;
  157. });
  158. // 数值样式
  159. type ValuePassThrough = {
  160. className?: string;
  161. input?: PassThroughProps;
  162. };
  163. // 操作按钮样式
  164. type OpPassThrough = {
  165. className?: string;
  166. minus?: PassThroughProps;
  167. plus?: PassThroughProps;
  168. icon?: ClIconProps;
  169. };
  170. // 定义透传样式类型
  171. type PassThrough = {
  172. className?: string;
  173. value?: ValuePassThrough;
  174. op?: OpPassThrough;
  175. };
  176. // 解析透传样式配置
  177. const pt = computed(() => parsePt<PassThrough>(props.pt));
  178. // 绑定值
  179. const value = ref(props.modelValue);
  180. // 是否可以继续增加数值
  181. const isPlus = computed(() => !isDisabled.value && value.value < props.max);
  182. // 是否可以继续减少数值
  183. const isMinus = computed(() => !isDisabled.value && value.value > props.min);
  184. /**
  185. * 更新数值并触发事件
  186. * 确保数值在最大值和最小值范围内
  187. */
  188. function update() {
  189. nextTick(() => {
  190. let val = value.value;
  191. // 处理小于最小值的情况
  192. if (val < props.min) {
  193. val = props.min;
  194. }
  195. // 处理大于最大值的情况
  196. if (val > props.max) {
  197. val = props.max;
  198. }
  199. // 处理最小值大于最大值的异常情况
  200. if (props.min > props.max) {
  201. val = props.max;
  202. }
  203. // 小数点后两位
  204. if (props.inputType == "digit") {
  205. val = parseFloat(val.toFixed(2));
  206. }
  207. // 更新绑定值
  208. if (val != value.value) {
  209. value.value = val;
  210. }
  211. emit("update:modelValue", val);
  212. emit("change", val);
  213. });
  214. }
  215. /**
  216. * 点击加号按钮处理函数 (支持长按)
  217. * 在非禁用状态下增加step值
  218. */
  219. function onPlus() {
  220. if (isDisabled.value || !isPlus.value) return;
  221. longPress.start(() => {
  222. if (isPlus.value) {
  223. const val = props.max - value.value;
  224. value.value += val > props.step ? props.step : val;
  225. update();
  226. }
  227. });
  228. }
  229. /**
  230. * 点击减号按钮处理函数 (支持长按)
  231. * 在非禁用状态下减少step值
  232. */
  233. function onMinus() {
  234. if (isDisabled.value || !isMinus.value) return;
  235. longPress.start(() => {
  236. if (isMinus.value) {
  237. const val = value.value - props.min;
  238. value.value -= val > props.step ? props.step : val;
  239. update();
  240. }
  241. });
  242. }
  243. /**
  244. * 输入框失去焦点处理函数
  245. * @param val 输入的字符串值
  246. */
  247. function onBlur(e: UniInputBlurEvent) {
  248. if (e.detail.value == "") {
  249. value.value = 0;
  250. } else {
  251. value.value = parseFloat(e.detail.value);
  252. }
  253. update();
  254. }
  255. // 监听绑定值变化
  256. watch(
  257. computed(() => props.modelValue),
  258. (val: number) => {
  259. value.value = val;
  260. update();
  261. },
  262. {
  263. immediate: true
  264. }
  265. );
  266. // 监听最大值变化,确保当前值不超过新的最大值
  267. watch(
  268. computed(() => props.max),
  269. update
  270. );
  271. // 监听最小值变化,确保当前值不小于新的最小值
  272. watch(
  273. computed(() => props.min),
  274. update
  275. );
  276. </script>
  277. <style lang="scss" scoped>
  278. .cl-input-number {
  279. @apply flex flex-row items-center;
  280. &__plus,
  281. &__minus {
  282. @apply flex items-center justify-center rounded-md bg-surface-100;
  283. &.is-disabled {
  284. @apply opacity-50;
  285. }
  286. }
  287. &__plus {
  288. @apply bg-primary-500;
  289. }
  290. &__value {
  291. @apply flex flex-row items-center justify-center h-full;
  292. margin: 0 12rpx;
  293. }
  294. }
  295. </style>