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": "秒",
|
||||
"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":"常规"
|
||||
}
|
||||
|
@ -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 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>
|
||||
);
|
||||
|
@ -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>');
|
||||
});
|
||||
});
|
||||
|
@ -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 { 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);
|
||||
|
Loading…
Reference in New Issue
Block a user