cl-input-number.uvue 6.5 KB

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