This commit is contained in:
2026-01-05 12:47:14 +08:00
commit 1fc846fae3
1614 changed files with 162035 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
.nut-theme-dark {
.nut-input-number {
&__icon {
color: $dark-color;
&--disabled {
color: $dark-color-gray;
}
}
input,
&__text--readonly {
color: $dark-color;
background-color: $dark-background;
border: 1px solid $dark-color-gray;
}
&--disabled {
input {
color: $dark-color-gray;
}
}
}
}
.nut-input-number {
box-sizing: $inputnumber-border-box;
display: $inputnumber-display;
align-items: center;
height: $inputnumber-height;
line-height: $inputnumber-line-height;
border: $inputnumber-border;
border-radius: $inputnumber-border-radius;
&--disabled {
input {
color: $inputnumber-icon-void-color;
}
}
&__icon {
display: flex;
align-items: center;
color: $inputnumber-icon-color;
cursor: pointer;
.nut-icon {
width: $inputnumber-icon-size;
height: $inputnumber-icon-size;
font-size: $inputnumber-icon-size;
}
&--disabled {
color: $inputnumber-icon-void-color;
cursor: not-allowed;
}
}
input {
border-top: 0 !important;
border-bottom: 0 !important;
}
input,
&__text--readonly,
&__text--input {
display: flex;
align-items: center;
justify-content: center;
width: $inputnumber-input-width;
height: 100%;
margin: $inputnumber-input-margin;
font-size: $inputnumber-input-font-size;
color: $inputnumber-input-font-color;
text-align: center;
background-color: $inputnumber-input-background-color;
border: $inputnumber-input-border;
border-radius: $inputnumber-input-border-radius;
outline: none;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
appearance: none;
}
}

View File

@@ -0,0 +1 @@
export * from './inputnumber'

View File

@@ -0,0 +1,64 @@
import type { BaseEvent, InputOnBlurEvent, InputOnFocusEvent } from '@uni-helper/uni-app-types'
import type { ExtractPropTypes } from 'vue'
import { BLUR_EVENT, CHANGE_EVENT, FOCUS_EVENT, UPDATE_MODEL_EVENT } from '../_constants'
import { commonProps, makeNumericProp, nullableBooleanProp } from '../_utils'
export const inputnumberProps = {
...commonProps,
/**
* @description 初始值
*/
modelValue: makeNumericProp(0),
/**
* @description 最小值限制
*/
min: makeNumericProp(1),
/**
* @description 最大值限制
*/
max: makeNumericProp(9999),
/**
* @description 步长
*/
step: makeNumericProp(1),
/**
* @description 是否只能输入 step 的倍数
*/
stepStrictly: Boolean,
/**
* @description 设置保留的小数位
*/
decimalPlaces: makeNumericProp(0),
/**
* @description 禁用所有功能
*/
disabled: nullableBooleanProp,
/**
* @description 只读状态禁用输入框操作行为
*/
readonly: Boolean,
/**
* @description 输入框宽度
*/
inputWidth: makeNumericProp(''),
/**
* @description 操作加减按钮的尺寸
*/
buttonSize: makeNumericProp(''),
}
export type InputNumberProps = ExtractPropTypes<typeof inputnumberProps>
/* eslint-disable unused-imports/no-unused-vars */
export const inputnumberEmits = {
[UPDATE_MODEL_EVENT]: (value: number) => true,
[CHANGE_EVENT]: (value: number, event?: BaseEvent) => true,
[FOCUS_EVENT]: (event: InputOnFocusEvent) => true,
[BLUR_EVENT]: (event: InputOnBlurEvent) => true,
reduce: (event: BaseEvent) => true,
add: (event: BaseEvent) => true,
overlimit: (event: BaseEvent, type: 'reduce' | 'add') => true,
}
/* eslint-enable unused-imports/no-unused-vars */
export type InputNumberEmits = typeof inputnumberEmits

View File

@@ -0,0 +1,323 @@
<script lang="ts" setup>
import type { BaseEvent, InputOnBlurEvent, InputOnFocusEvent, InputOnInputEvent } from '@uni-helper/uni-app-types'
import type { CSSProperties } from 'vue'
import { computed, defineComponent, nextTick, onMounted, ref, toRef, useSlots, watch } from 'vue'
import { BLUR_EVENT, CHANGE_EVENT, FOCUS_EVENT, PREFIX, UPDATE_MODEL_EVENT } from '../_constants'
import { getMainClass, pxCheck } from '../_utils'
import { useFormDisabled } from '../form/form'
import NutIcon from '../icon/icon.vue'
import { inputnumberEmits, inputnumberProps } from './inputnumber'
type UpdateSource = '' | 'click' | 'input' | 'blur'
const props = defineProps(inputnumberProps)
const emit = defineEmits(inputnumberEmits)
const slots = useSlots()
const formDisabled = useFormDisabled(toRef(props, 'disabled'))
const classes = computed(() => {
return getMainClass(props, componentName, {
[`${componentName}--disabled`]: formDisabled.value,
})
})
function toNumber(value: number | string) {
if (typeof value === 'number') {
return value
}
return Number(value)
}
const innerValue = computed(() => {
return toNumber(props.modelValue)
})
const innerMinValue = computed(() => {
return toNumber(props.min)
})
const innerMaxValue = computed(() => {
return toNumber(props.max)
})
const innerStepValue = computed(() => {
return toNumber(props.step)
})
const innerDigits = computed(() => {
return toNumber(props.decimalPlaces)
})
const allowDecrease = computed(() => {
if (formDisabled.value) {
return false
}
return innerValue.value > innerMinValue.value
})
const allowIncrease = computed(() => {
if (formDisabled.value) {
return false
}
return innerValue.value < innerMaxValue.value
})
const decreaseClasses = computed(() => {
return {
[`${componentName}__icon--disabled`]: !allowDecrease.value,
}
})
const inputStyles = computed(() => {
const value: CSSProperties = {}
const { inputWidth, buttonSize } = props
if (inputWidth) {
value.width = pxCheck(inputWidth)
}
if (buttonSize) {
value.height = pxCheck(buttonSize)
}
return value
})
const increaseClasses = computed(() => {
return {
[`${componentName}__icon--disabled`]: !allowIncrease.value,
}
})
const inputValue = ref('')
let updateSource: UpdateSource = ''
function precisionValue(value: number, type: 'number'): number
function precisionValue(value: number, type: 'string'): string
function precisionValue(value: number, type: 'number' | 'string') {
const fixedValue = value.toFixed(innerDigits.value)
if (type === 'string') {
return fixedValue
}
return Number(fixedValue)
}
function updateInputValue(value: number) {
const finalValue = precisionValue(value, 'string')
if (finalValue !== inputValue.value) {
inputValue.value = finalValue
}
else {
inputValue.value = ''
nextTick(() => {
inputValue.value = finalValue
})
}
}
function formatValue(value: number | string) {
let trulyValue = Math.max(
innerMinValue.value,
Math.min(
innerMaxValue.value,
toNumber(value),
),
)
if (props.stepStrictly) {
trulyValue = Math.round(trulyValue / innerStepValue.value) * innerStepValue.value
}
return precisionValue(trulyValue, 'number')
}
function emitChange(source: UpdateSource, value: number | string, event?: BaseEvent) {
updateSource = source
const formattedValue = formatValue(value)
if (['', 'blur'].includes(updateSource)) {
updateInputValue(formattedValue)
}
if (formattedValue !== props.modelValue) {
emit(UPDATE_MODEL_EVENT, formattedValue)
emit(CHANGE_EVENT, formattedValue, event)
}
}
function handleInput(event: InputOnInputEvent) {
if (formDisabled.value || props.readonly)
return
emitChange('input', event.detail.value, event)
}
function handleDecrease(event: BaseEvent) {
if (formDisabled.value)
return
emit('reduce', event)
const finalValue = innerValue.value - innerStepValue.value
if (allowDecrease.value && finalValue >= innerMinValue.value) {
emitChange('click', finalValue, event)
}
else {
emit('overlimit', event, 'reduce')
emitChange('click', innerMinValue.value, event)
}
}
function handleIncrease(event: BaseEvent) {
if (formDisabled.value)
return
emit('add', event)
const finalValue = innerValue.value + innerStepValue.value
if (allowIncrease.value && finalValue <= innerMaxValue.value) {
emitChange('click', finalValue, event)
}
else {
emit('overlimit', event, 'add')
emitChange('click', innerMaxValue.value, event)
}
}
function handleFocus(event: InputOnFocusEvent) {
if (formDisabled.value || props.readonly)
return
emit(FOCUS_EVENT, event)
}
function handleBlur(event: InputOnBlurEvent) {
if (formDisabled.value || props.readonly)
return
emit(BLUR_EVENT, event)
emitChange('blur', event.detail.value, event)
}
function correctValue() {
emitChange('', props.modelValue)
}
watch(() => props.modelValue, () => {
if (updateSource === 'input') {
updateSource = ''
return
}
correctValue()
})
watch(() => [
props.min,
props.max,
props.step,
props.stepStrictly,
props.decimalPlaces,
], () => {
correctValue()
})
onMounted(() => {
correctValue()
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-input-number`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view :class="classes" :style="props.customStyle">
<view
class="nut-input-number__icon nut-input-number__left"
:class="decreaseClasses"
@click="handleDecrease"
>
<slot v-if="slots.leftIcon" name="leftIcon" />
<NutIcon v-else name="minus" :size="props.buttonSize" />
</view>
<view v-if="props.readonly" class="nut-input-number__text--readonly">
{{ inputValue }}
</view>
<template v-else>
<!-- #ifdef H5 -->
<input
v-model="inputValue"
v-bind="$attrs"
class="nut-input-number__text--input"
:style="inputStyles"
type="number"
:min="props.min"
:max="props.max"
:disabled="formDisabled"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
>
<!-- #endif -->
<!-- #ifndef H5 -->
<input
v-model="inputValue"
class="nut-input-number__text--input"
:style="inputStyles"
type="number"
:min="props.min"
:max="props.max"
:disabled="formDisabled"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
>
<!-- #endif -->
</template>
<view
class="nut-input-number__icon nut-input-number__right"
:class="increaseClasses"
@click="handleIncrease"
>
<slot v-if="slots.rightIcon" name="rightIcon" />
<NutIcon v-else name="plus" :size="props.buttonSize" />
</view>
</view>
</template>
<style lang="scss">
@import "./index";
</style>