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:
katherinehhh 2024-03-26 17:23:14 +08:00 committed by GitHub
parent ac5a82fde3
commit 59e6b4a757
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 333 additions and 8 deletions

View File

@ -909,5 +909,14 @@
"Second": "秒",
"Unix Timestamp": "Unix 时间戳",
"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":"常规"
}

View File

@ -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;
},
},
],
});

View File

@ -2,21 +2,112 @@ import { isValid } from '@formily/shared';
import { toFixedByStep } from '@nocobase/utils/client';
import type { InputProps } from 'antd/es/input';
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) => {
const { step, value, addonBefore, addonAfter } = props;
const { step, formatStyle, value, addonBefore, addonAfter, unitConversion, unitConversionType, separator } = props;
if (!isValid(props.value)) {
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 (
<div className={'nb-read-pretty-input-number'}>
{addonBefore}
{result}
<span dangerouslySetInnerHTML={{ __html: result }} />
{addonAfter}
</div>
);

View File

@ -3,6 +3,7 @@ import React from 'react';
import App2 from '../demos/addonBefore&addonAfter';
import App3 from '../demos/highPrecisionDecimals';
import App1 from '../demos/inputNumber';
import { formatNumberWithSeparator, formatUnitConversion, scientificNotation } from '../ReadPretty';
describe('InputNumber', () => {
it('should display the title', () => {
@ -43,7 +44,7 @@ describe('InputNumber: addonBefore/addonAfter', () => {
fireEvent.change(input, { target: { value: 1 } });
expect(input.value).toBe('1');
// @ts-ignore
expect(screen.getByText(1万元')).toBeInTheDocument();
expect(screen.getByText(')).toBeInTheDocument();
// empty value
fireEvent.change(input, { target: { value: '' } });
@ -67,7 +68,7 @@ describe('InputNumber: High precision decimals', () => {
fireEvent.change(input, { target: { value: 1 } });
expect(input.value).toBe('1.00');
// @ts-ignore
expect(screen.getByText('1.00%')).toBeInTheDocument();
expect(screen.getByText('1.00')).toBeInTheDocument();
// empty value
fireEvent.change(input, { target: { value: '' } });
@ -75,3 +76,43 @@ describe('InputNumber: High precision decimals', () => {
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>');
});
});

View File

@ -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();
}}
/>
);
};

View File

@ -44,6 +44,7 @@ import { selectComponentFieldSettings } from '../modules/fields/component/Select
import { subTablePopoverComponentFieldSettings } from '../modules/fields/component/SubTable/subTablePopoverComponentFieldSettings';
import { tagComponentFieldSettings } from '../modules/fields/component/Tag/tagComponentFieldSettings';
import { unixTimestampComponentFieldSettings } from '../modules/fields/component/UnixTimestamp/unixTimestampComponentFieldSettings';
import { inputNumberComponentFieldSettings } from '../modules/fields/component/InputNumber/inputNumberComponentFieldSettings';
export class SchemaSettingsPlugin extends Plugin {
async load() {
@ -94,6 +95,7 @@ export class SchemaSettingsPlugin extends Plugin {
this.schemaSettingsManager.add(subTablePopoverComponentFieldSettings);
this.schemaSettingsManager.add(datePickerComponentFieldSettings);
this.schemaSettingsManager.add(unixTimestampComponentFieldSettings);
this.schemaSettingsManager.add(inputNumberComponentFieldSettings);
this.schemaSettingsManager.add(fileManagerComponentFieldSettings);
this.schemaSettingsManager.add(tagComponentFieldSettings);