index.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. <template>
  2. <div class="number-range-container">
  3. <div :id="usePrepend ? 'prepend' : ''" :class="{ 'slot-default': slotStyle === 'default', 'slot-pend ': usePrepend }">
  4. <slot name="prepend">
  5. <!-- 前缀插槽 -->
  6. </slot>
  7. </div>
  8. <div
  9. class="number-range"
  10. :class="{
  11. 'is-disabled': disabled,
  12. 'is-focus': isFocus,
  13. 'number-range-left-border-radius-0': usePrepend,
  14. 'number-range-right-border-radius-0': useAppend,
  15. }"
  16. >
  17. <el-input-number
  18. :disabled="disabled"
  19. placeholder="最小值"
  20. @blur="handleBlur"
  21. @focus="handleFocus"
  22. @change="handleChangeMinValue"
  23. v-model="minValue_"
  24. v-bind="$attrs"
  25. :controls="false"
  26. />
  27. <div class="to">
  28. <span>{{ to }}</span>
  29. </div>
  30. <el-input-number
  31. :disabled="disabled"
  32. placeholder="最大值"
  33. @blur="handleBlur"
  34. @focus="handleFocus"
  35. @change="handleChangeMaxValue"
  36. v-model="maxValue_"
  37. v-bind="$attrs"
  38. :controls="false"
  39. />
  40. </div>
  41. <div :id="useAppend ? 'append' : ''" :class="{ 'slot-default': slotStyle === 'default', 'slot-pend ': useAppend }">
  42. <slot name="append">
  43. <!-- 后缀插槽 -->
  44. </slot>
  45. </div>
  46. </div>
  47. </template>
  48. <script lang="ts" setup>
  49. const props = defineProps({
  50. modelValue: {
  51. type: Array,
  52. default: () => [null, null], // 调用时使用v-model="[min,max]" 绑定
  53. },
  54. minValue: {
  55. type: Number,
  56. default: null, // 调用时使用v-model:min-value="" 绑定多个v-model
  57. },
  58. maxValue: {
  59. type: Number,
  60. default: null, // 调用时使用v-model:max-value="" 绑定多个v-model
  61. },
  62. // 是否禁用
  63. disabled: {
  64. type: Boolean,
  65. default: false,
  66. },
  67. to: {
  68. type: String,
  69. default: '至',
  70. },
  71. // 精度参数 -保留小数位数
  72. precision: {
  73. type: Number,
  74. default: 0,
  75. validator(val: number) {
  76. return val >= 0 && val === parseInt(String(val), 10);
  77. },
  78. },
  79. // 限制取值范围
  80. valueRange: {
  81. type: Array,
  82. default: () => [],
  83. validator(val: []) {
  84. if (val && val.length > 0) {
  85. if (val.length !== 2) {
  86. throw new Error('请传入长度为2的Number数组');
  87. }
  88. if (typeof val[0] !== 'number' || typeof val[1] !== 'number') {
  89. throw new Error('取值范围只接受Number类型,请确认');
  90. }
  91. if (val[1] < val[0]) {
  92. throw new Error('valueRange格式须为[最小值,最大值],请确认');
  93. }
  94. }
  95. return true;
  96. },
  97. },
  98. // 插槽样式
  99. slotStyle: {
  100. type: String, // default --异色背景 | plain--无背景色
  101. default: 'default',
  102. },
  103. });
  104. const emit = defineEmits(['update:modelValue', 'update:minValue', 'update:maxValue', 'change']);
  105. const minValue_ = computed({
  106. get() {
  107. return props.minValue ?? props.modelValue[0] ?? null;
  108. },
  109. set(value) {
  110. emit('update:minValue', value);
  111. emit('update:modelValue', [value, maxValue_.value]);
  112. },
  113. });
  114. const maxValue_ = computed({
  115. get() {
  116. return props.maxValue ?? props.modelValue[1] ?? null;
  117. },
  118. set(value) {
  119. emit('update:maxValue', value);
  120. emit('update:modelValue', [minValue_.value, value]);
  121. },
  122. });
  123. const handleChangeMinValue = (value: number) => {
  124. // 非数字和空值返回null
  125. if (value === '' || value === null || isNaN(value)) {
  126. emit('update:minValue', null);
  127. return;
  128. }
  129. if (maxValue_.value === '' || maxValue_.value === null || isNaN(maxValue_.value)){
  130. emit('update:minValue', value);
  131. emit('update:maxValue', null);
  132. return;
  133. }
  134. const newMinValue = parsePrecision(value, props.precision);
  135. // min > max 交换min max
  136. if (parseFloat(String(newMinValue)) > parseFloat(String(maxValue_.value))) {
  137. const { min, max } = decideValueRange(Number(maxValue_.value), newMinValue);
  138. updateValue(min, max);
  139. } else {
  140. const { min, max } = decideValueRange(newMinValue, Number(maxValue_.value));
  141. updateValue(min, max);
  142. }
  143. };
  144. const handleChangeMaxValue = (value: number) => {
  145. // 非数字空返回null
  146. if (value === '' || value === null || isNaN(value)) {
  147. emit('update:maxValue', null);
  148. return;
  149. }
  150. if (minValue_.value === '' || minValue_.value === null || isNaN(minValue_.value)){
  151. emit('update:maxValue', value);
  152. emit('update:minValue', null);
  153. return;
  154. }
  155. console.log(value)
  156. const newMaxValue = parsePrecision(value, props.precision); // 初始化数字精度
  157. // max < min 交换min max
  158. if (parseFloat(String(newMaxValue)) < parseFloat(String(minValue_.value))) {
  159. const { min, max } = decideValueRange(newMaxValue, Number(minValue_.value));
  160. updateValue(min, max);
  161. } else {
  162. const { min, max } = decideValueRange(Number(minValue_.value), newMaxValue);
  163. updateValue(min, max);
  164. }
  165. };
  166. // 更新数据
  167. const updateValue = (min: number, max: number) => {
  168. emit('update:minValue', min);
  169. emit('update:maxValue', max);
  170. emit('update:modelValue', [min, max]);
  171. emit('change', {min, max});
  172. };
  173. // 取值范围判定
  174. const decideValueRange = (min: number, max: number) => {
  175. if (props.valueRange && props.valueRange.length > 0) {
  176. min = min < props.valueRange[0] ? props.valueRange[0] : min > props.valueRange[1] ? props.valueRange[1] : min;
  177. max = max > props.valueRange[1] ? props.valueRange[1] : max;
  178. }
  179. return { min, max };
  180. };
  181. // input焦点事件
  182. const isFocus = ref();
  183. const handleFocus = () => {
  184. isFocus.value = true;
  185. };
  186. const handleBlur = () => {
  187. isFocus.value = false;
  188. };
  189. // 处理数字精度
  190. const parsePrecision = (number: number, precision = 0) => {
  191. return parseFloat(String(Math.round(number * Math.pow(10, precision)) / Math.pow(10, precision)));
  192. };
  193. // 判断插槽是否被使用
  194. // 组件外部使用时插入了
  195. // <template #插槽名 >
  196. // </template>
  197. // 无论template标签内是否插入了内容,均视为已使用该插槽
  198. const slots = useSlots();
  199. const usePrepend = computed(() => {
  200. // 前缀插槽
  201. return slots && slots.prepend ? true : false;
  202. });
  203. const useAppend = computed(() => {
  204. // 后缀插槽
  205. return slots && slots.append ? true : false;
  206. });
  207. </script>
  208. <style lang="scss" scoped>
  209. .number-range-container {
  210. display: flex;
  211. height: 100%;
  212. .slot-pend {
  213. white-space: nowrap;
  214. color: var(--el-color-info);
  215. border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
  216. display: flex;
  217. align-items: center;
  218. justify-content: center;
  219. }
  220. #prepend {
  221. padding: 0 15px;
  222. box-shadow: 1px 0 0 0 var(--el-input-border-color, var(--el-border-color)) inset,
  223. 0 1px 0 0 var(--el-input-border-color, var(--el-border-color)) inset, 0 -1px 0 0 var(--el-input-border-color, var(--el-border-color)) inset;
  224. border-right: 0;
  225. border-top-right-radius: 0;
  226. border-bottom-right-radius: 0;
  227. }
  228. #append {
  229. padding: 0 15px;
  230. box-shadow: 0 1px 0 0 var(--el-input-border-color, var(--el-border-color)) inset,
  231. 0 -1px 0 0 var(--el-input-border-color, var(--el-border-color)) inset, -1px 0 0 0 var(--el-input-border-color, var(--el-border-color)) inset;
  232. border-left: 0;
  233. border-top-left-radius: 0;
  234. border-bottom-left-radius: 0;
  235. }
  236. .slot-default {
  237. background-color: var(--el-fill-color-light);
  238. display: flex;
  239. align-items: center;
  240. justify-content: center;
  241. }
  242. .number-range-left-border-radius-0 {
  243. border-top-left-radius: 0 !important;
  244. border-bottom-left-radius: 0 !important;
  245. }
  246. .number-range-right-border-radius-0 {
  247. border-top-right-radius: 0 !important;
  248. border-bottom-right-radius: 0 !important;
  249. }
  250. .number-range {
  251. background-color: var(--el-bg-color) !important;
  252. box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
  253. border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
  254. padding: 0 2px;
  255. display: flex;
  256. flex-direction: row;
  257. width: 100%;
  258. justify-content: center;
  259. align-items: center;
  260. color: var(--el-input-text-color, var(--el-text-color-regular));
  261. transition: var(--el-transition-box-shadow);
  262. transform: translate3d(0, 0, 0);
  263. overflow: hidden;
  264. .to {
  265. margin-top: 1px;
  266. }
  267. }
  268. .is-focus {
  269. transition: all 0.3s;
  270. box-shadow: 0 0 0 1px var(--el-color-primary) inset !important;
  271. }
  272. .is-disabled {
  273. background-color: var(--el-input-bg-color);
  274. color: var(--el-input-text-color, var(--el-text-color-regular));
  275. cursor: not-allowed;
  276. .to {
  277. height: calc(100% - 3px);
  278. background-color: var(--el-fill-color-light) !important;
  279. }
  280. }
  281. }
  282. :deep(.el-input) {
  283. border: none;
  284. }
  285. :deep(.el-input__wrapper) {
  286. margin: 0;
  287. padding: 0 15px;
  288. background-color: transparent;
  289. border: none !important;
  290. box-shadow: none !important;
  291. &.is-focus {
  292. border: none !important;
  293. box-shadow: none !important;
  294. }
  295. }
  296. </style>