feat: read pretty input number field support display format config (#3815)
* feat: input number support display format * feat: input number support display format * feat: input number support display format * feat: input number support display format * feat: input number support display format * feat: input number support display format * refactor: local improve * refactor: local improve * refactor: code improve * refactor: locale improve * test: input-number * test: input-number * test: input-number
This commit is contained in:
parent
ac5a82fde3
commit
59e6b4a757
@ -909,5 +909,14 @@
|
|||||||
"Second": "秒",
|
"Second": "秒",
|
||||||
"Unix Timestamp": "Unix 时间戳",
|
"Unix Timestamp": "Unix 时间戳",
|
||||||
"Field value do not meet the requirements": "字符不符合要求",
|
"Field value do not meet the requirements": "字符不符合要求",
|
||||||
"Field value size is": "字符长度要求"
|
"Field value size is": "字符长度要求",
|
||||||
|
"Style": "风格",
|
||||||
|
"Unit conversion": "单位换算",
|
||||||
|
"Separator": "分隔符",
|
||||||
|
"Prefix": "前缀",
|
||||||
|
"Suffix": "后缀",
|
||||||
|
"Multiply by":"乘以",
|
||||||
|
"Divide by":"除以",
|
||||||
|
"Scientifix notation":"科学计数法",
|
||||||
|
"Normal":"常规"
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
import { useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
|
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
|
||||||
|
import { SchemaSettingsNumberFormat } from '../../../../schema-settings/SchemaSettingsNumberFormat';
|
||||||
|
import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator';
|
||||||
|
import { useIsFieldReadPretty } from '../../../../schema-component/antd/form-item/FormItem.Settings';
|
||||||
|
export const inputNumberComponentFieldSettings = new SchemaSettings({
|
||||||
|
name: 'fieldSettings:component:InputNumber',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: 'displayFormat',
|
||||||
|
Component: SchemaSettingsNumberFormat as any,
|
||||||
|
useComponentProps() {
|
||||||
|
const schema = useFieldSchema();
|
||||||
|
const { fieldSchema: tableColumnSchema } = useColumnSchema();
|
||||||
|
const fieldSchema = tableColumnSchema || schema;
|
||||||
|
return {
|
||||||
|
fieldSchema,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
useVisible() {
|
||||||
|
const isFieldReadPretty = useIsFieldReadPretty();
|
||||||
|
return isFieldReadPretty;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -2,21 +2,112 @@ import { isValid } from '@formily/shared';
|
|||||||
import { toFixedByStep } from '@nocobase/utils/client';
|
import { toFixedByStep } from '@nocobase/utils/client';
|
||||||
import type { InputProps } from 'antd/es/input';
|
import type { InputProps } from 'antd/es/input';
|
||||||
import type { InputNumberProps } from 'antd/es/input-number';
|
import type { InputNumberProps } from 'antd/es/input-number';
|
||||||
import React from 'react';
|
import * as math from 'mathjs';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { format } from 'd3-format';
|
||||||
|
|
||||||
|
function countDecimalPlaces(value) {
|
||||||
|
const number = Number(value);
|
||||||
|
if (!Number.isFinite(number)) return 0;
|
||||||
|
|
||||||
|
const decimalPart = String(number).split('.')[1];
|
||||||
|
return decimalPart ? decimalPart.length : 0;
|
||||||
|
}
|
||||||
|
const separators = {
|
||||||
|
'0,0.00': { thousands: ',', decimal: '.' },
|
||||||
|
'0.0,00': { thousands: '.', decimal: ',' },
|
||||||
|
'0 0,00': { thousands: ' ', decimal: '.' },
|
||||||
|
'0.00': { thousands: '', decimal: '.' }, // 没有千位分隔符
|
||||||
|
};
|
||||||
|
//分隔符换算
|
||||||
|
export function formatNumberWithSeparator(number, format = '0,0.00', step = 1) {
|
||||||
|
let formattedNumber = '';
|
||||||
|
|
||||||
|
if (separators[format]) {
|
||||||
|
const { thousands, decimal } = separators[format];
|
||||||
|
formattedNumber = number
|
||||||
|
.toLocaleString('en-US', {
|
||||||
|
style: 'decimal',
|
||||||
|
minimumFractionDigits: step,
|
||||||
|
maximumFractionDigits: step,
|
||||||
|
})
|
||||||
|
.replace(/,/g, 'comma_placeholder')
|
||||||
|
.replace(/\./g, 'dot_placeholder')
|
||||||
|
.replace(/comma_placeholder/g, thousands)
|
||||||
|
.replace(/dot_placeholder/g, decimal);
|
||||||
|
} else {
|
||||||
|
formattedNumber = number.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
//单位换算
|
||||||
|
export function formatUnitConversion(value, operator = '*', multiplier) {
|
||||||
|
if (!multiplier) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (operator === '*') {
|
||||||
|
result = value * multiplier;
|
||||||
|
} else if (operator === '/') {
|
||||||
|
if (multiplier !== 0) {
|
||||||
|
result = value / multiplier;
|
||||||
|
} else {
|
||||||
|
console.error('Error: Division by zero.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("Error: Invalid operator. Use '*' for multiplication or '/' for division.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return math.round(result, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
//科学计数法显示
|
||||||
|
export function scientificNotation(number, decimalPlaces, separator = '.') {
|
||||||
|
const formatter = format(`.${decimalPlaces}e`);
|
||||||
|
const formattedNumber = formatter(number).replace('.', separator);
|
||||||
|
|
||||||
|
// 匹配科学计数法中的指数部分,判断正负情况
|
||||||
|
const result = formattedNumber.replace(/e([+-]?\d+)/, (match, exponent) => {
|
||||||
|
if (exponent.startsWith('+')) {
|
||||||
|
// 正数指数,不显示符号
|
||||||
|
return ` × 10<sup>${exponent.slice(1)}</sup>`;
|
||||||
|
} else {
|
||||||
|
// 负数指数,显示 "-" 符号
|
||||||
|
return ` × 10<sup>-${exponent.slice(1)}</sup>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
export const ReadPretty: React.FC<InputProps & InputNumberProps> = (props: any) => {
|
export const ReadPretty: React.FC<InputProps & InputNumberProps> = (props: any) => {
|
||||||
const { step, value, addonBefore, addonAfter } = props;
|
const { step, formatStyle, value, addonBefore, addonAfter, unitConversion, unitConversionType, separator } = props;
|
||||||
if (!isValid(props.value)) {
|
if (!isValid(props.value)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const result = toFixedByStep(value, step);
|
//单位换算
|
||||||
if (isNaN(result)) {
|
const unitData = formatUnitConversion(value, unitConversionType, unitConversion);
|
||||||
|
//精度换算
|
||||||
|
const preciationData = toFixedByStep(unitData, step);
|
||||||
|
let result;
|
||||||
|
//分隔符换算
|
||||||
|
result = formatNumberWithSeparator(Number(preciationData), separator, countDecimalPlaces(step));
|
||||||
|
if (formatStyle === 'scientifix') {
|
||||||
|
//科学计数显示
|
||||||
|
result = scientificNotation(Number(unitData), countDecimalPlaces(step), separators?.[separator]?.['decimal']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={'nb-read-pretty-input-number'}>
|
<div className={'nb-read-pretty-input-number'}>
|
||||||
{addonBefore}
|
{addonBefore}
|
||||||
{result}
|
<span dangerouslySetInnerHTML={{ __html: result }} />
|
||||||
{addonAfter}
|
{addonAfter}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||||||
import App2 from '../demos/addonBefore&addonAfter';
|
import App2 from '../demos/addonBefore&addonAfter';
|
||||||
import App3 from '../demos/highPrecisionDecimals';
|
import App3 from '../demos/highPrecisionDecimals';
|
||||||
import App1 from '../demos/inputNumber';
|
import App1 from '../demos/inputNumber';
|
||||||
|
import { formatNumberWithSeparator, formatUnitConversion, scientificNotation } from '../ReadPretty';
|
||||||
|
|
||||||
describe('InputNumber', () => {
|
describe('InputNumber', () => {
|
||||||
it('should display the title', () => {
|
it('should display the title', () => {
|
||||||
@ -43,7 +44,7 @@ describe('InputNumber: addonBefore/addonAfter', () => {
|
|||||||
fireEvent.change(input, { target: { value: 1 } });
|
fireEvent.change(input, { target: { value: 1 } });
|
||||||
expect(input.value).toBe('1');
|
expect(input.value).toBe('1');
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(screen.getByText('¥1万元')).toBeInTheDocument();
|
expect(screen.getByText('¥')).toBeInTheDocument();
|
||||||
|
|
||||||
// empty value
|
// empty value
|
||||||
fireEvent.change(input, { target: { value: '' } });
|
fireEvent.change(input, { target: { value: '' } });
|
||||||
@ -67,7 +68,7 @@ describe('InputNumber: High precision decimals', () => {
|
|||||||
fireEvent.change(input, { target: { value: 1 } });
|
fireEvent.change(input, { target: { value: 1 } });
|
||||||
expect(input.value).toBe('1.00');
|
expect(input.value).toBe('1.00');
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(screen.getByText('1.00%')).toBeInTheDocument();
|
expect(screen.getByText('1.00')).toBeInTheDocument();
|
||||||
|
|
||||||
// empty value
|
// empty value
|
||||||
fireEvent.change(input, { target: { value: '' } });
|
fireEvent.change(input, { target: { value: '' } });
|
||||||
@ -75,3 +76,43 @@ describe('InputNumber: High precision decimals', () => {
|
|||||||
expect(screen.queryByText('NaN')).toBeNull();
|
expect(screen.queryByText('NaN')).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ReadPretty:formatNumberWithSeparator', () => {
|
||||||
|
// Test case 1: Format a number with default format '0,0.00'
|
||||||
|
test('Format number with default separator', () => {
|
||||||
|
const formatted = formatNumberWithSeparator(1234567.89);
|
||||||
|
expect(formatted).toBe('1,234,567.9');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test case 2: Format a number with custom format '0.00'
|
||||||
|
test('Format number with custom separator', () => {
|
||||||
|
const formatted = formatNumberWithSeparator(1234567.89, '0.00', 1);
|
||||||
|
expect(formatted).toBe('1234567.9');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('ReadPretty:formatUnitConversion', () => {
|
||||||
|
// Test case 1: Multiply a value by 2
|
||||||
|
test('Multiply value by 2', () => {
|
||||||
|
const result = formatUnitConversion(10, '*', 2);
|
||||||
|
expect(result).toBe(20);
|
||||||
|
});
|
||||||
|
// Test case 2: Divide a value by 0 (error case)
|
||||||
|
test('Divide value by zero', () => {
|
||||||
|
const result = formatUnitConversion(10, '/', 0);
|
||||||
|
expect(result).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ReadPretty:scientificNotation', () => {
|
||||||
|
// Test case 1: Format a number into scientific notation with 2 decimal places
|
||||||
|
test('Format number into scientific notation', () => {
|
||||||
|
const formatted = scientificNotation(1234567.89, 2);
|
||||||
|
expect(formatted).toBe('1.23 × 10<sup>6</sup>');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test case 2: Format a number into scientific notation with custom separator '.'
|
||||||
|
test('Format number into scientific notation with custom separator', () => {
|
||||||
|
const formatted = scientificNotation(1234567.89, 2, '.');
|
||||||
|
expect(formatted).toBe('1.23 × 10<sup>6</sup>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -0,0 +1,156 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { ISchema, Schema, useField, useForm } from '@formily/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Select } from 'antd';
|
||||||
|
import { useCollectionManager_deprecated, useDesignable } from '..';
|
||||||
|
import { SchemaSettingsModalItem } from './SchemaSettings';
|
||||||
|
|
||||||
|
const UnitConversion = ({ unitConversionType }) => {
|
||||||
|
const form = useForm();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
defaultValue={unitConversionType || '*'}
|
||||||
|
style={{ width: 160 }}
|
||||||
|
onChange={(value) => {
|
||||||
|
form.setValuesIn('unitConversionType', value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select.Option value="*">{t('Multiply by')}</Select.Option>
|
||||||
|
<Select.Option value="/">{t('Divide by')}</Select.Option>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SchemaSettingsNumberFormat = function NumberFormatConfig(props: { fieldSchema: Schema }) {
|
||||||
|
const { fieldSchema } = props;
|
||||||
|
const field = useField();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { getCollectionJoinField } = useCollectionManager_deprecated();
|
||||||
|
const collectionField = getCollectionJoinField(fieldSchema?.['x-collection-field']) || {};
|
||||||
|
const { formatStyle, unitConversion, unitConversionType, separator, step, addonBefore, addonAfter } =
|
||||||
|
fieldSchema['x-component-props'] || {};
|
||||||
|
const { step: prescition } = collectionField?.uiSchema['x-component-props'] || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaSettingsModalItem
|
||||||
|
title={t('Format')}
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
formatStyle: {
|
||||||
|
type: 'string',
|
||||||
|
default: formatStyle || 'normal',
|
||||||
|
enum: [
|
||||||
|
{
|
||||||
|
value: 'normal',
|
||||||
|
label: t('Normal'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'scientifix',
|
||||||
|
label: t('Scientifix notation'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Select',
|
||||||
|
title: "{{t('Style')}}",
|
||||||
|
},
|
||||||
|
unitConversion: {
|
||||||
|
type: 'number',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'InputNumber',
|
||||||
|
title: "{{t('Unit conversion')}}",
|
||||||
|
default: unitConversion,
|
||||||
|
'x-component-props': {
|
||||||
|
style: { width: '100%' },
|
||||||
|
addonBefore: <UnitConversion unitConversionType={unitConversionType} />,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
type: 'string',
|
||||||
|
default: separator || '0,0.00',
|
||||||
|
enum: [
|
||||||
|
{
|
||||||
|
value: '0,0.00',
|
||||||
|
label: t('100,000.00'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '0.0,00',
|
||||||
|
label: t('100.000,00'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '0 0,00',
|
||||||
|
label: t('100 000.00'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '0.00',
|
||||||
|
label: t('100000.00'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Select',
|
||||||
|
title: "{{t('Separator')}}",
|
||||||
|
},
|
||||||
|
step: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Precision")}}',
|
||||||
|
'x-component': 'Select',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
default: step || prescition || '1',
|
||||||
|
enum: [
|
||||||
|
{ value: '1', label: '1' },
|
||||||
|
{ value: '0.1', label: '1.0' },
|
||||||
|
{ value: '0.01', label: '1.00' },
|
||||||
|
{ value: '0.001', label: '1.000' },
|
||||||
|
{ value: '0.0001', label: '1.0000' },
|
||||||
|
{ value: '0.00001', label: '1.00000' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
addonBefore: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Prefix")}}',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
default: addonBefore,
|
||||||
|
},
|
||||||
|
addonAfter: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Suffix")}}',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
default: addonAfter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
onSubmit={(data) => {
|
||||||
|
const schema = {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
};
|
||||||
|
schema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||||
|
fieldSchema['x-component-props'] = {
|
||||||
|
...(fieldSchema['x-component-props'] || {}),
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
schema['x-component-props'] = fieldSchema['x-component-props'];
|
||||||
|
field.componentProps = fieldSchema['x-component-props'];
|
||||||
|
//子表格/表格区块
|
||||||
|
const parts = (field.path.entire as string).split('.');
|
||||||
|
parts.pop();
|
||||||
|
const modifiedString = parts.join('.');
|
||||||
|
field.query(`${modifiedString}.*[0:].${fieldSchema.name}`).forEach((f) => {
|
||||||
|
if (f.props.name === fieldSchema.name) {
|
||||||
|
f.setComponentProps({ ...data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -44,6 +44,7 @@ import { selectComponentFieldSettings } from '../modules/fields/component/Select
|
|||||||
import { subTablePopoverComponentFieldSettings } from '../modules/fields/component/SubTable/subTablePopoverComponentFieldSettings';
|
import { subTablePopoverComponentFieldSettings } from '../modules/fields/component/SubTable/subTablePopoverComponentFieldSettings';
|
||||||
import { tagComponentFieldSettings } from '../modules/fields/component/Tag/tagComponentFieldSettings';
|
import { tagComponentFieldSettings } from '../modules/fields/component/Tag/tagComponentFieldSettings';
|
||||||
import { unixTimestampComponentFieldSettings } from '../modules/fields/component/UnixTimestamp/unixTimestampComponentFieldSettings';
|
import { unixTimestampComponentFieldSettings } from '../modules/fields/component/UnixTimestamp/unixTimestampComponentFieldSettings';
|
||||||
|
import { inputNumberComponentFieldSettings } from '../modules/fields/component/InputNumber/inputNumberComponentFieldSettings';
|
||||||
|
|
||||||
export class SchemaSettingsPlugin extends Plugin {
|
export class SchemaSettingsPlugin extends Plugin {
|
||||||
async load() {
|
async load() {
|
||||||
@ -94,6 +95,7 @@ export class SchemaSettingsPlugin extends Plugin {
|
|||||||
this.schemaSettingsManager.add(subTablePopoverComponentFieldSettings);
|
this.schemaSettingsManager.add(subTablePopoverComponentFieldSettings);
|
||||||
this.schemaSettingsManager.add(datePickerComponentFieldSettings);
|
this.schemaSettingsManager.add(datePickerComponentFieldSettings);
|
||||||
this.schemaSettingsManager.add(unixTimestampComponentFieldSettings);
|
this.schemaSettingsManager.add(unixTimestampComponentFieldSettings);
|
||||||
|
this.schemaSettingsManager.add(inputNumberComponentFieldSettings);
|
||||||
|
|
||||||
this.schemaSettingsManager.add(fileManagerComponentFieldSettings);
|
this.schemaSettingsManager.add(fileManagerComponentFieldSettings);
|
||||||
this.schemaSettingsManager.add(tagComponentFieldSettings);
|
this.schemaSettingsManager.add(tagComponentFieldSettings);
|
||||||
|
Loading…
Reference in New Issue
Block a user