diff --git a/packages/ui-vue/components/number-spinner/index.tsx b/packages/ui-vue/components/number-spinner/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..080278366c867c864c4397d0149237325e0c4ad4 --- /dev/null +++ b/packages/ui-vue/components/number-spinner/index.tsx @@ -0,0 +1,12 @@ +import type { App } from 'vue'; +import FComboList from './src/number-spinner.component'; + +export * from './src/number-spinner.props'; + +export { FComboList }; + +export default { + install(app: App): void { + app.component(FComboList.name, FComboList); + }, +}; diff --git a/packages/ui-vue/components/number-spinner/src/composition/types.ts b/packages/ui-vue/components/number-spinner/src/composition/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..067e4e319c7faf742203fafc51e23bd296f3ab89 --- /dev/null +++ b/packages/ui-vue/components/number-spinner/src/composition/types.ts @@ -0,0 +1,40 @@ +import { BigNumber } from 'bignumber.js'; + +export interface UseData { + onModelChange: (realVal: any, updateOn:string) => void; + up: (e: Event) => void; + down: (e: Event) => void; + compute: (type:string) => void; + onFocusTextBox: ($event: Event) => void; + onBlurTextBox: ($event: Event) => void; + onInput: ($event: Event, updateOn:string) => void; + onKeyDown: ($event: KeyboardEvent) => void; +} +export interface UseUtil { + isEmpty: (value: any) => boolean; + setDisabledState: (isDisabled: boolean) => void; + cleanNumString: (value:any) => string; + getPrecision: () => Number; + toFixed: (value: BigNumber | number) => string; + _getRealValue: (value: BigNumber) => string | number; + getRealValue: (value: any) => string | number; + isDisableOfBtn: (type: string, value?: any) => boolean; + buildFormatOptions: () => Object; + _modelChanged: (realVal: any) => void; + validInterval: (bn: BigNumber) => BigNumber; + format: (value:any) => string; + _toFormat: (_bgNum: BigNumber, fmt: NumberFormatter) => string +} + +export interface NumberFormatter { + /** 前置符号 */ + prefix?: string; + /** 后缀 */ + suffix?: string; + /** 小数点 */ + decimalSeparator?: string; + /** 千分位符号 */ + groupSeparator?: string; + /** 千分位分组 */ + groupSize?: number; +} \ No newline at end of file diff --git a/packages/ui-vue/components/number-spinner/src/composition/use-data.ts b/packages/ui-vue/components/number-spinner/src/composition/use-data.ts new file mode 100644 index 0000000000000000000000000000000000000000..093d4cc866497de18e9c87fc9a0eeec1c77768e7 --- /dev/null +++ b/packages/ui-vue/components/number-spinner/src/composition/use-data.ts @@ -0,0 +1,156 @@ +import { UseData } from './types'; +import { NumberSpinnerProps } from '../number-spinner.props'; +import { computed, SetupContext, Ref, ref } from 'vue'; +import { BigNumber } from 'bignumber.js'; +import { UseUtil } from './use-util'; +export function UseData(props: NumberSpinnerProps, context: SetupContext, realValue: Ref, inputValue: Ref, isFocus: Ref, formatOptions: Ref): UseData { + /** + * popValue,isActiveTip为后续接入popover保留字段 + * updateOn为内容修改时的类型,包括change、blur + */ + const popValue = ref(''); + const isActiveTip = ref(false); + const updateType = ref('change'); + + const { isDisableOfBtn, format, getRealValue, cleanNumString, isEmpty, _modelChanged } = UseUtil(props, context, realValue, formatOptions) + + + + + /** + * 手动输入值变化时或失去焦点时的方法 + * @param realVal 输入框中的实际数值 + * @param updateOn 更新类型,分为'change'| 'blur' + */ + function onModelChange(realVal: any, updateOn = 'change') { + let _resultValue = realVal; + if (updateOn === 'change') { + _resultValue = getRealValue(realVal); + } + isActiveTip.value = false; + popValue.value = format(_resultValue); + if (updateType.value === updateOn) { + _modelChanged(_resultValue); + } + } + + /** + * 点击微增按钮时或键盘触发 ArrowUp 时执行的方法 + */ + function up(e: Event) { + compute('up'); + e.stopPropagation(); + } + + /** + * 点击微减按钮时或键盘触发 ArrowDown 时执行的方法 + */ + function down(e: Event) { + compute('down'); + e.stopPropagation(); + } + /** + * 触发微调事件时,计算结果的方法 + * @param tye 微调类型 up微增 down微减 + */ + function compute(tye = 'up') { + if (isDisableOfBtn(tye)) { + let _resultValue; + const realBigNum = new BigNumber(realValue.value || 0); + if (tye === 'up') { + _resultValue = realBigNum.plus(Number(props.step)); + } else { + _resultValue = realBigNum.minus(Number(props.step)); + } + + const value = _resultValue.toFixed(); + if (!isFocus.value) { + inputValue.value = format(value); + } else { + inputValue.value = value; + } + // this.input.nativeElement.value = this.value; + _modelChanged(getRealValue(_resultValue)); + } + } + + + /** + * 输入框失焦时执行的方法 + */ + function onBlurTextBox($event: Event) { + $event.stopPropagation(); + if (props.readonly || props.disabled) { + return; + } + + if (updateType.value === 'blur') { + const val = cleanNumString(inputValue.value); + realValue.value = getRealValue(val); + } + + inputValue.value = format(realValue.value); + isFocus.value = false; + onModelChange(realValue.value, 'blur'); + context.emit('blur', { event: $event, formatted: inputValue.value, value: realValue.value }) + } + + /** + * 输入框获取焦点时执行的方法 + */ + function onFocusTextBox($event: Event) { + $event.stopPropagation(); + if (props.readonly || props.disabled) { + isFocus.value = false; + return; + } + + inputValue.value = isEmpty(realValue.value) ? '' : ((!props.showZero && realValue.value == '0') ? '' : realValue.value); + isFocus.value = true; + context.emit('focus', { event: $event, formatted: inputValue.value, value: realValue.value }) + } + + /** + * 输入框的input事件 + */ + function onInput($event: Event) { + $event.stopPropagation(); + console.log('onInput', $event) + if (props.disabled) { + // context.emit('clickButton', $event); + } + inputValue.value = ($event.target as HTMLTextAreaElement)?.value + onModelChange(inputValue.value, 'change') + + } + + /** + * 焦点状态下键盘监听事件 + */ + function onKeyDown(e: KeyboardEvent) { + if (e.key === 'ArrowDown') { + e.preventDefault(); + down(e); + } + + if (e.key === 'ArrowUp') { + e.preventDefault(); + up(e); + } + + e.stopPropagation(); + } + return { + // _modelChanged, + onModelChange, + up, + down, + compute, + onBlurTextBox, + onFocusTextBox, + onInput, + onKeyDown + + + }; +} diff --git a/packages/ui-vue/components/number-spinner/src/composition/use-util.ts b/packages/ui-vue/components/number-spinner/src/composition/use-util.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc452f12e1f694e3d6ea3c87b2f1c19cf0070ed8 --- /dev/null +++ b/packages/ui-vue/components/number-spinner/src/composition/use-util.ts @@ -0,0 +1,241 @@ +import { UseUtil, NumberFormatter } from './types'; +import { NumberSpinnerProps } from '../number-spinner.props'; +import { computed, SetupContext, Ref } from 'vue'; +import { BigNumber } from 'bignumber.js'; +export function UseUtil(props: NumberSpinnerProps, context: SetupContext, realValue: Ref, formatOptions: Ref): UseUtil { + + + /** + * 判断输入框中的值是否为空 + * @param val 输入值 + * @returns 返回是否时空值的判断结果 + */ + function isEmpty(val: any): boolean { + return isNaN(val) || val === null || val === undefined || val === ''; + } + /** + * 设置输入框是否为可用状态 + * @param isDisabled 要设置的输入框状态 + */ + function setDisabledState(isDisabled: boolean): void { + props.disabled = isDisabled; + } + + /** + * 清洗数据为数字 + * @param val 输入值,带有前缀、后缀等 + * @returns 返回的纯数字数据 + */ + function cleanNumString(val: any) { + val = (val === null || val === undefined || val === '') ? '' : String(val); + val = val.replace(new RegExp(props.prefix, 'g'), '') + .replace(new RegExp(props.suffix, 'g'), '').replace(/\,/g, ''); + if (props.groupSeparator && props.groupSeparator !== ',') { + val = val.replace(new RegExp(`\\${props.groupSeparator}`, 'g'), ''); + } + + if (props.decimalSeparator && props.decimalSeparator !== '.') { + val = val.replace(new RegExp(`\\${props.decimalSeparator}`, 'g'), '.'); + } + return val; + } + + /** + * 获取精度 + * @returns 返回精度具体值 + */ + function getPrecision() { + return Number(props.precision || 0); + } + + /** + * 基于精度参数修改tofixed方法 + * @param value + * @returns + */ + function toFixed(value: BigNumber | number) { + if (props.precision !== null && props.precision !== undefined) { + return value.toFixed(getPrecision()); + } + return value.toFixed();5 + } + + /** + * 获取实际数值,支持大数时返回bigNumber类型,否则返回Number类型 + * @param value + * @returns + */ + function _getRealValue(value: BigNumber) { + const fixedValue = toFixed(value); + return props.bigNumber ? fixedValue : Number(value); + } + + /** + * 获取实际数值 + * @param val + * @returns + */ + function getRealValue(val: any) { + if (props.parser) { + if (!isNaN(Number(val))) { + return val; + } else { + return props.parser(val); + } + } + + let value = validInterval(new BigNumber(val)); + if (value.isNaN()) { + + if (props.canNull) { + return null; + } else { + const minBigNum = new BigNumber('' + props.min); + const maxBigNum = new BigNumber('' + props.max); + + if (!minBigNum.isNaN()) { + value = minBigNum; + } else if (!maxBigNum.isNaN()) { + value = maxBigNum; + } else { + return 0; + } + } + + // if (this.canNull || minBigNum.isNaN()) { + // return null; + // } else { + // value = minBigNum; + // } + } + + return _getRealValue(value); + } + + /** + * 判断为按钮是否为可用 + * @param type 'up'|'down' + * @param value 输入框的真实数字值 + * @returns true为可用, false不可用 + */ + function isDisableOfBtn(type: string, value?: any) { + if (value === undefined) { + value = realValue.value; + } + value = new BigNumber(value); + + if (type === 'up' && props.max && !(new BigNumber(props.max)).isNaN() && value.gte(new BigNumber(props.max))) { + return false; + } + if (type === 'down' && props.min && !(new BigNumber(props.min)).isNaN() && value.lte(new BigNumber(props.min))) { + return false; + } + return true; + } + + /** + * 生成格式化对象 + */ + function buildFormatOptions() { + return { + prefix: props.prefix, + suffix: props.suffix, + decimalSeparator: props.decimalSeparator, + groupSeparator: props.useThousands ? props.groupSeparator : '', + groupSize: props.groupSize + }; + } + + /** + * 输入框真实数值修改,并通知回调 + * @param realVal + */ + function _modelChanged(realVal: any) { + + realValue.value = realVal; + context.emit('valueChange', realVal) + } + /** + * 最值校验 + * @param bn + * @returns + */ + function validInterval(bn: BigNumber) { + let _bnVal = bn; + + if (!isEmpty(props.max)) { + const _maxBigNum = new BigNumber('' + props.max); + if (bn.gt(_maxBigNum)) { + _bnVal = _maxBigNum; + const _resultValue = _getRealValue(_maxBigNum); + _modelChanged(_resultValue); + } + } + + if (!isEmpty(props.min)) { + const _minBigNum = new BigNumber('' + props.min); + if (bn.lt(_minBigNum)) { + _bnVal = _minBigNum; + const _resultValue = _getRealValue(_minBigNum); + _modelChanged(_resultValue); + } + } + + return _bnVal; + } + /**格式化数据 */ + function format(val: any) { + val = cleanNumString(val); + const bigVal = new BigNumber(val); + const _bgNum = validInterval(bigVal); + + + if (_bgNum.valueOf() == '0' && !props.showZero) { + return ''; + } + + if (props.canNull && bigVal.isNaN()) { + return ''; + } else { + if (_bgNum.isNaN()) { + return ''; + } + } + + if (props.formatter) { + return props.formatter(_bgNum.toNumber()); + } else { + + if (!Object.keys(formatOptions.value).length) { + formatOptions.value = buildFormatOptions(); + } + + return _toFormat(_bgNum, formatOptions.value); + } + } + + function _toFormat(_bgNum: BigNumber, fmt: NumberFormatter) { + if (props.precision !== null && props.precision !== undefined) { + return _bgNum.toFormat(getPrecision(), fmt); + } else { + return _bgNum.toFormat(fmt); + } + } + + + return { + isEmpty, + setDisabledState, + cleanNumString, + getPrecision, + toFixed, + _getRealValue, + getRealValue, + isDisableOfBtn, + buildFormatOptions, + _modelChanged, + validInterval, + format, + _toFormat + }; +} diff --git a/packages/ui-vue/components/number-spinner/src/index.scss b/packages/ui-vue/components/number-spinner/src/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..0c90eedeaf6b3958df3b4e66f4af182243fb8f62 --- /dev/null +++ b/packages/ui-vue/components/number-spinner/src/index.scss @@ -0,0 +1,58 @@ +.input-group { + .btn-group-number { + height: 1.50003rem; + flex-direction: column; + background-color: #fff; + .btn-number-flag { + height: 50%; + display: flex; + box-shadow: none; + padding: 0 5px; + margin-left: 1px; + border-left: 1px solid rgb(217, 217, 217); + overflow: hidden; + transition: all .1s linear; + .number-arrow-chevron { + flex: 1; + line-height: 1; + } + &:hover { + height: 60%!important; + } + &:nth-child(2) { + border-top: 1px solid rgb(217, 217, 217); + } + } + + } +} +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none !important; + margin: 0; +} +.number-range { + position: relative; + .input-container { + display: flex; + padding: 0; + .sub-input-group { + flex: 1; + position: relative; + display: flex; + transition: all .3s ease-out; + .sub-input { + width: 100%; + border: none; + outline: none; + background-color: rgba(0, 0, 0, 0); + min-width: 2px; + padding: 0.125rem 4px 0.125rem 0.5rem; + } + } + .spliter { + width: 15px; + text-align: center; + } + } +} diff --git a/packages/ui-vue/components/number-spinner/src/number-spinner.component.tsx b/packages/ui-vue/components/number-spinner/src/number-spinner.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..844d87cacf4badae39e96a3345a06a40fcdbf728 --- /dev/null +++ b/packages/ui-vue/components/number-spinner/src/number-spinner.component.tsx @@ -0,0 +1,77 @@ +import { defineComponent, computed, ref, onMounted, onBeforeUpdate, watch } from 'vue'; +import type { SetupContext } from 'vue'; +import { numberSpinnerProps, NumberSpinnerProps } from './number-spinner.props'; +import { UseData } from './composition/use-data'; +import { UseUtil } from './composition/use-util'; +import './index.scss' + +export default defineComponent({ + name: 'FNumberSpinner', + props: numberSpinnerProps, + emits: ['valueChange', 'blur', 'focus', 'click', 'input'], + setup(props: NumberSpinnerProps, context: SetupContext) { + const realValue = ref(null) //真实值 数字 + const isFocus = ref(false) + const inputValue = ref('') + const formatOptions = ref({}) + const { isDisableOfBtn, format, buildFormatOptions } = UseUtil(props, context, realValue, formatOptions) + const { onFocusTextBox, onBlurTextBox, onInput, up, down, onKeyDown } = UseData(props, context, realValue, inputValue, isFocus, formatOptions) + + onMounted(() => { + + }) + + watch(() => [props.precision, props.useThousands, props.prefix, props.suffix], ([newPrecision, newUseThousands, newPrefix, newSuffix]) => { + formatOptions.value = buildFormatOptions() + inputValue.value = format(realValue.value) + + }) + return () => ( +
+
+ onInput(e, 'change')} + disabled={props.disabled} + readonly={props.readonly || !props.editable} + placeholder={props.disabled || props.readonly || !props.editable ? '' : props.placeholder} + onKeydown={(e) => { onKeyDown(e) }} + /> + { + !props.disabled && !props.readonly && props.showButton && +
+ + + + +
+ + } + +
+
realvalue: {realValue.value}
+
inputValue: {inputValue.value}
+
+ + ); + } +}); diff --git a/packages/ui-vue/components/number-spinner/src/number-spinner.props.tsx b/packages/ui-vue/components/number-spinner/src/number-spinner.props.tsx new file mode 100644 index 0000000000000000000000000000000000000000..abbcb6d0295b3721c948c2f5d5893680ede26bf7 --- /dev/null +++ b/packages/ui-vue/components/number-spinner/src/number-spinner.props.tsx @@ -0,0 +1,102 @@ +import { ExtractPropTypes, PropType } from 'vue'; +type TextAlignType = 'left' | 'right' | 'center' | 'start' | 'end' | 'justify'; +export const numberSpinnerProps = { + /** + * 组件标识 + */ + id: String, + /** + * 是否禁用 + */ + disabled: { type: Boolean, default: false }, + /** + * 是否只读 + */ + readonly: { type: Boolean, default: false }, + /** + * 是否可编辑 + */ + editable: { type: Boolean, default: true }, + /** + * 格式化 formatter 和 parser 必须同时存在 + * formatter: (val: number) => string; + * parser: (val: string | number) => number; + */ + formatter: { type: Function }, + parser: { type: Function }, + /** + * 空白提示文本 + */ + placeholder: { type: String, default: '请输入数字' }, + /** + * up or down 步长 + */ + step: { type: Number, default: 1 }, + /** + * 最大值 + */ + max: { type: String }, + /** + * 最小值 + */ + min: { type: String }, + /** + * 启用大数支持 + */ + bigNumber: { type: Boolean, default: false }, + /** + * 是否显示加减按钮 + */ + showButton: { type: Boolean, default: true }, + /** + * 是否使用千分值 + */ + useThousands: { type: Boolean, default: true }, + /** + * 文本方向 + */ + textAlign: { type: String as PropType, default: 'left' }, + /** + * 自动补全小数 + */ + autoDecimal: { type: Boolean, default: true }, + /** + * 允许为空 + */ + canNull: { type: Boolean, default: false }, + /** + * 精度 + */ + precision: { type: Number, default: 0 }, + /** + * 前缀 + */ + prefix: { type: String, default: '' }, + /** + * 后缀 + */ + suffix: { type: String, default: '' }, + /** + * 小数点符号 + */ + decimalSeparator: { type: String, default: '.' }, + /** + * 千分位符号 + */ + groupSeparator: { type: String, default: ',' }, + /** + * 使用千分位时,每组显示的字符数 + */ + groupSize: { type: Number, default: 3 }, + /** + * 值 + */ + value: { type: String, default: '' }, + /** + * 显示0值 + */ + showZero: { type: Boolean, default: true } + +}; + +export type NumberSpinnerProps = ExtractPropTypes; diff --git a/packages/ui-vue/components/number-spinner/src/types.ts b/packages/ui-vue/components/number-spinner/src/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/packages/ui-vue/src/app.vue b/packages/ui-vue/src/app.vue index 888c110163f9adefd9bbe1e3a6e49b3f5a2a1fbf..57164d0b9acf4dffb90265bd2e4915f2083febcb 100644 --- a/packages/ui-vue/src/app.vue +++ b/packages/ui-vue/src/app.vue @@ -28,7 +28,7 @@ import Section from './components/section.vue'; import SwitchBasic from '../demos/switch/basic.vue'; import Tabs from './components/tabs.vue'; import Tooltip from './components/tooltip.vue'; - +import NumberSpinner from "./components/number-spinner.vue" const routes: Record = { '/': HelloWorld, '/accordion': Accordion, @@ -55,7 +55,8 @@ const routes: Record = { '/section': Section, '/switch/basic': SwitchBasic, '/tabs': Tabs, - '/tooltip': Tooltip + '/tooltip': Tooltip, + '/numberSpinner': NumberSpinner }; const currentPath = ref(window.location.hash); diff --git a/packages/ui-vue/src/components/number-spinner.vue b/packages/ui-vue/src/components/number-spinner.vue new file mode 100644 index 0000000000000000000000000000000000000000..406a83b5ee4f96626ba3e07d0c4996017df4e9df --- /dev/null +++ b/packages/ui-vue/src/components/number-spinner.vue @@ -0,0 +1,100 @@ + + + + \ No newline at end of file