cl-input-number.uvue 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  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="250"
  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="250"
  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. value.value = val;
  209. // 如果值发生变化,则触发事件
  210. if (val != props.modelValue) {
  211. emit("update:modelValue", val);
  212. emit("change", val);
  213. }
  214. });
  215. }
  216. /**
  217. * 点击加号按钮处理函数 (支持长按)
  218. * 在非禁用状态下增加step值
  219. */
  220. function onPlus() {
  221. if (isDisabled.value || !isPlus.value) return;
  222. longPress.start(() => {
  223. if (isPlus.value) {
  224. const val = props.max - value.value;
  225. value.value += val > props.step ? props.step : val;
  226. update();
  227. }
  228. });
  229. }
  230. /**
  231. * 点击减号按钮处理函数 (支持长按)
  232. * 在非禁用状态下减少step值
  233. */
  234. function onMinus() {
  235. if (isDisabled.value || !isMinus.value) return;
  236. longPress.start(() => {
  237. if (isMinus.value) {
  238. const val = value.value - props.min;
  239. value.value -= val > props.step ? props.step : val;
  240. update();
  241. }
  242. });
  243. }
  244. /**
  245. * 输入框失去焦点处理函数
  246. * @param val 输入的字符串值
  247. */
  248. function onBlur(e: UniInputBlurEvent) {
  249. if (e.detail.value == "") {
  250. value.value = 0;
  251. } else {
  252. value.value = parseFloat(e.detail.value);
  253. }
  254. update();
  255. }
  256. // 监听绑定值变化
  257. watch(
  258. computed(() => props.modelValue),
  259. (val: number) => {
  260. value.value = val;
  261. update();
  262. },
  263. {
  264. immediate: true
  265. }
  266. );
  267. // 监听最大值变化,确保当前值不超过新的最大值
  268. watch(
  269. computed(() => props.max),
  270. update
  271. );
  272. // 监听最小值变化,确保当前值不小于新的最小值
  273. watch(
  274. computed(() => props.min),
  275. update
  276. );
  277. </script>
  278. <style lang="scss" scoped>
  279. .cl-input-number {
  280. @apply flex flex-row items-center;
  281. &__plus,
  282. &__minus {
  283. @apply flex items-center justify-center rounded-md bg-surface-100;
  284. &.is-disabled {
  285. @apply opacity-50;
  286. }
  287. }
  288. &__plus {
  289. @apply bg-primary-500;
  290. }
  291. &__value {
  292. @apply flex flex-row items-center justify-center h-full;
  293. margin: 0 12rpx;
  294. }
  295. }
  296. </style>