Stepper.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. <template>
  2. <FlexRow position="relative" align="center">
  3. <slot name="addonBefore">
  4. <template v-if="addonBefore">
  5. <text>{{ addonBefore }}</text>
  6. <Width :width="prefixTextSpace" />
  7. </template>
  8. </slot>
  9. <slot name="minus" :disabled="disabled || value <= min" :onClick="minus">
  10. <IconButton
  11. :icon="minusIcon"
  12. :disabled="disabled || value <= min"
  13. :buttonStyle="{
  14. ...themeStyles.button.value,
  15. ...selectObjectByType(size, 'medium', {
  16. small: themeStyles.buttonSmall.value,
  17. medium: themeStyles.buttonMedium.value,
  18. large: themeStyles.buttonLarge.value,
  19. })
  20. }"
  21. :padding="5"
  22. :size="iconSize"
  23. @click="minus"
  24. />
  25. </slot>
  26. <slot
  27. name="center"
  28. :disableInput="disableInput"
  29. :value="stringValue"
  30. :integer="integer",
  31. :onChangeText="onChangeText"
  32. :onBlur="onTextBlur"
  33. :editable="!disabled"
  34. >
  35. <view :style="{
  36. ...themeStyles.inputWrapper.value,
  37. ...selectObjectByType(size, 'medium', {
  38. small: themeStyles.inputWrapperSmall.value,
  39. medium: themeStyles.inputWrapperMedium.value,
  40. large: themeStyles.inputWrapperLarge.value,
  41. }),
  42. }">
  43. <input
  44. :style="{
  45. ...themeStyles.input.value,
  46. width: themeContext.resolveThemeSize(inputWidth),
  47. color: themeContext.resolveThemeColor(textColor)
  48. }"
  49. :disabled="disabled || disableInput"
  50. :value="stringValue"
  51. :placeholder="placeholder"
  52. :placeholder-style="`color: ${themeContext.resolveThemeColor(placeholderTextColor)}`"
  53. type="number"
  54. confirm-type="done"
  55. @input="(e: any) => onChangeText(e.detail.value)"
  56. @blur="onTextBlur"
  57. @confirm="onTextBlur"
  58. />
  59. </view>
  60. </slot>
  61. <slot name="add" :disabled="disabled || !max || value >= max" :onClick="add">
  62. <IconButton
  63. :icon="addIcon"
  64. :disabled="disabled || (max ? value >= max : undefined)"
  65. :buttonStyle="{
  66. ...themeStyles.button.value,
  67. ...selectObjectByType(size, 'medium', {
  68. small: themeStyles.buttonSmall.value,
  69. medium: themeStyles.buttonMedium.value,
  70. large: themeStyles.buttonLarge.value,
  71. })
  72. }"
  73. :padding="5"
  74. :size="iconSize"
  75. @click="add"
  76. />
  77. </slot>
  78. <slot name="addonAfter">
  79. <template v-if="addonAfter">
  80. <Width :width="prefixTextSpace" />
  81. <text>{{ addonAfter }}</text>
  82. </template>
  83. </slot>
  84. </FlexRow>
  85. </template>
  86. <script setup lang="ts">
  87. import { computed, onMounted, ref, toRef, watch } from 'vue';
  88. import IconButton from '../basic/IconButton.vue';
  89. import FlexRow from '../layout/FlexRow.vue';
  90. import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
  91. import { StringUtils } from '@imengyu/imengyu-utils';
  92. import { DynamicColor, DynamicSize, selectObjectByType } from '../theme/ThemeTools';
  93. import { useFieldChildValueInjector } from './FormContext';
  94. import Width from '../layout/space/Width.vue';
  95. export interface StepperProps {
  96. /**
  97. * 当前数值
  98. * @default 0
  99. */
  100. modelValue?: number;
  101. /**
  102. * 最大值,为空无限制
  103. * @default undefined
  104. */
  105. max?: number;
  106. /**
  107. * 最小值
  108. * @default 1
  109. */
  110. min?: number;
  111. /**
  112. * 中间输入框的宽度
  113. * @default 50
  114. */
  115. inputWidth?: number;
  116. /**
  117. * 组件大小
  118. * @default 'medium'
  119. */
  120. size?: 'small' | 'medium' | 'large';
  121. /**
  122. * 步长,每次点击时改变的值
  123. * @default 1
  124. */
  125. step?: number;
  126. /**
  127. * 初始值,当 value 为空时生效
  128. * @default 0
  129. */
  130. defaultValue?: number;
  131. /**
  132. * 固定显示的小数位数
  133. * @default 0
  134. */
  135. decimalLength?: number;
  136. /**
  137. * 是否禁用
  138. * @default false
  139. */
  140. disabled?: boolean;
  141. /**
  142. * 是否禁用输入框
  143. * @default false
  144. */
  145. disableInput?: boolean;
  146. /**
  147. * 是否只允许输入整数
  148. * @default false
  149. */
  150. integer?: boolean;
  151. /**
  152. * 输入框的文本颜色
  153. */
  154. textColor?: string,
  155. /**
  156. * 输入框的占位符
  157. */
  158. placeholder?: string,
  159. /**
  160. * 输入框的占位符颜色
  161. */
  162. placeholderTextColor?: string,
  163. /**
  164. * 加号图标名称
  165. * @default 'add-bold'
  166. */
  167. addIcon?: string;
  168. /**
  169. * 键号图标名称
  170. * @default 'minus-bold'
  171. */
  172. minusIcon?: string;
  173. /**
  174. * 组件左侧添加的文本
  175. */
  176. addonBefore?: string,
  177. /**
  178. * 组件右侧添加的文本
  179. */
  180. addonAfter?: string,
  181. }
  182. const emit = defineEmits([ 'update:modelValue' ])
  183. const props = withDefaults(defineProps<StepperProps>(), {
  184. modelValue: 0,
  185. max: undefined,
  186. min: 1,
  187. inputWidth: () => propGetThemeVar('StepperInputWidth', 100),
  188. step: 1,
  189. defaultValue: 0,
  190. decimalLength: 0,
  191. disabled: false,
  192. disableInput: false,
  193. integer: false,
  194. addIcon: () => propGetThemeVar('StepperAddIcon', 'add-bold'),
  195. minusIcon: () => propGetThemeVar('StepperMinusIcon', 'minus-bold'),
  196. });
  197. const themeContext = useTheme();
  198. const themeStyles = themeContext.useThemeStyles({
  199. button: {
  200. borderRadius: DynamicSize('StepperButtonBorderRadius', 0),
  201. backgroundColor: DynamicColor('StepperButtonBackgroundColor', 'light'),
  202. },
  203. buttonLarge: {
  204. borderRadius: DynamicSize('StepperButtonBorderRadius', 0),
  205. paddingTop: DynamicSize('StepperButtonPaddingVertical', 8),
  206. paddingBottom: DynamicSize('StepperButtonPaddingVertical', 8),
  207. paddingLeft: DynamicSize('StepperButtonPaddingHorizontal', 8),
  208. paddingRight: DynamicSize('StepperButtonPaddingHorizontal', 8),
  209. backgroundColor: DynamicColor('StepperButtonBackgroundColor', 'light'),
  210. },
  211. buttonMedium: {
  212. paddingTop: DynamicSize('StepperButtonPaddingVertical', 4),
  213. paddingBottom: DynamicSize('StepperButtonPaddingVertical', 4),
  214. paddingLeft: DynamicSize('StepperButtonPaddingHorizontal', 4),
  215. paddingRight: DynamicSize('StepperButtonPaddingHorizontal', 4),
  216. },
  217. buttonSmall: {
  218. borderRadius: DynamicSize('StepperButtonBorderRadius', 0),
  219. paddingTop: DynamicSize('StepperButtonPaddingVertical', 2),
  220. paddingBottom: DynamicSize('StepperButtonPaddingVertical', 2),
  221. paddingLeft: DynamicSize('StepperButtonPaddingHorizontal', 2),
  222. paddingRight: DynamicSize('StepperButtonPaddingHorizontal', 2),
  223. },
  224. inputWrapper: {
  225. display: 'flex',
  226. justifyContent: 'center',
  227. alignItems: 'center',
  228. alignSelf: 'stretch',
  229. backgroundColor: DynamicColor('StepperInputBackgroundColor', 'light'),
  230. },
  231. inputWrapperSmall: {
  232. paddingTop: DynamicSize('StepperInputPaddingVertical', 0),
  233. paddingBottom: DynamicSize('StepperInputPaddingVertical', 0),
  234. paddingLeft: DynamicSize('StepperInputPaddingHorizontal', 6),
  235. paddingRight: DynamicSize('StepperInputPaddingHorizontal', 6),
  236. marginLeft: DynamicSize('StepperInputMarginHorizontal', 2),
  237. marginRight: DynamicSize('StepperInputMarginHorizontal', 2),
  238. marginTop: DynamicSize('StepperInputMarginVertical', 0),
  239. marginBottom: DynamicSize('StepperInputMarginVertical', 0),
  240. },
  241. inputWrapperMedium: {
  242. paddingTop: DynamicSize('StepperInputPaddingVertical', 0),
  243. paddingBottom: DynamicSize('StepperInputPaddingVertical', 0),
  244. paddingLeft: DynamicSize('StepperInputPaddingHorizontal', 10),
  245. paddingRight: DynamicSize('StepperInputPaddingHorizontal', 10),
  246. marginLeft: DynamicSize('StepperInputMarginHorizontal', 4),
  247. marginRight: DynamicSize('StepperInputMarginHorizontal', 4),
  248. marginTop: DynamicSize('StepperInputMarginVertical', 0),
  249. marginBottom: DynamicSize('StepperInputMarginVertical', 0),
  250. },
  251. inputWrapperLarge: {
  252. paddingTop: DynamicSize('StepperInputPaddingVertical', 0),
  253. paddingBottom: DynamicSize('StepperInputPaddingVertical', 0),
  254. paddingLeft: DynamicSize('StepperInputPaddingHorizontal', 20),
  255. paddingRight: DynamicSize('StepperInputPaddingHorizontal', 20),
  256. marginLeft: DynamicSize('StepperInputMarginHorizontal', 8),
  257. marginRight: DynamicSize('StepperInputMarginHorizontal', 8),
  258. marginTop: DynamicSize('StepperInputMarginVertical', 0),
  259. marginBottom: DynamicSize('StepperInputMarginVertical', 0),
  260. },
  261. input: {
  262. textAlign: 'center',
  263. color: DynamicColor('StepperInputTextColor', 'text.content'),
  264. },
  265. });
  266. const prefixTextSpace = computed(() => themeContext.resolveThemeSize('StepperPrefixTextSpace', 12));
  267. const iconSize = computed(() => {
  268. switch (props.size) {
  269. case 'small':
  270. return themeContext.resolveThemeSize('StepperIconSizeSmall', 24);
  271. default:
  272. case 'medium':
  273. return themeContext.resolveThemeSize('StepperIconSizeMedium', 36);
  274. case 'large':
  275. return themeContext.resolveThemeSize('StepperIconSizeLarge', 48);
  276. }
  277. });
  278. const {
  279. value,
  280. updateValue,
  281. } = useFieldChildValueInjector(
  282. toRef(props, 'modelValue'),
  283. (v) => emit('update:modelValue', v)
  284. );
  285. const stringValue = ref('');
  286. watch(() => [ value.value, props.decimalLength, props.integer ], () => {
  287. const v = value.value, d = props.decimalLength, i = props.integer;
  288. //数据更改时更新文字
  289. stringValue.value = ((i || d === 0) ? v.toString() : v.toFixed(d));
  290. })
  291. function setTextString(v: number) {
  292. stringValue.value = ((props.integer || props.decimalLength === 0) ? Math.floor(v).toString() : v.toFixed(props.decimalLength));
  293. }
  294. function onChangeText(v: string) {
  295. stringValue.value = v;
  296. }
  297. function onTextBlur() {
  298. const s = stringValue.value;
  299. //输入框失去焦点后校验数值,用户可以从输入框直接输入数据
  300. if (StringUtils.isNullOrEmpty(s) || !StringUtils.isNumber(s)) {
  301. setTextString(props.defaultValue);
  302. emitChange(props.defaultValue);
  303. return;
  304. }
  305. let newValue = props.integer ? parseInt(s, 10) : parseFloat(s);
  306. if (newValue < props.min )
  307. newValue = props.min;
  308. if (props.max && newValue > props.max)
  309. newValue = props.max;
  310. setTextString(newValue);
  311. emitChange(newValue);
  312. }
  313. function emitChange(newValue: number) {
  314. updateValue(newValue);
  315. }
  316. function add() {
  317. //加
  318. let newValue = value.value + props.step;
  319. if (props.max && newValue > props.max)
  320. newValue = props.max;
  321. emitChange(newValue);
  322. }
  323. function minus() {
  324. //键
  325. let newValue = value.value - props.step;
  326. if (newValue < props.min )
  327. newValue = props.min;
  328. emitChange(newValue);
  329. }
  330. onMounted(() => {
  331. setTextString(props.modelValue);
  332. })
  333. </script>