diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json index 30f5d8f07..6b8af6a34 100644 --- a/packages/core/client/src/locale/zh-CN.json +++ b/packages/core/client/src/locale/zh-CN.json @@ -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":"常规" } diff --git a/packages/core/client/src/modules/fields/component/InputNumber/inputNumberComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/InputNumber/inputNumberComponentFieldSettings.tsx new file mode 100644 index 000000000..0a890813d --- /dev/null +++ b/packages/core/client/src/modules/fields/component/InputNumber/inputNumberComponentFieldSettings.tsx @@ -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; + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-component/antd/input-number/ReadPretty.tsx b/packages/core/client/src/schema-component/antd/input-number/ReadPretty.tsx index b7ed4371e..91eb12e37 100644 --- a/packages/core/client/src/schema-component/antd/input-number/ReadPretty.tsx +++ b/packages/core/client/src/schema-component/antd/input-number/ReadPretty.tsx @@ -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${exponent.slice(1)}`; + } else { + // 负数指数,显示 "-" 符号 + return ` × 10-${exponent.slice(1)}`; + } + }); + + return result; +} export const ReadPretty: React.FC = (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 (
{addonBefore} - {result} + {addonAfter}
); diff --git a/packages/core/client/src/schema-component/antd/input-number/__tests__/input-number.test.tsx b/packages/core/client/src/schema-component/antd/input-number/__tests__/input-number.test.tsx index 678e4707e..01adf6902 100644 --- a/packages/core/client/src/schema-component/antd/input-number/__tests__/input-number.test.tsx +++ b/packages/core/client/src/schema-component/antd/input-number/__tests__/input-number.test.tsx @@ -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 × 106'); + }); + + // 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 × 106'); + }); +}); diff --git a/packages/core/client/src/schema-settings/SchemaSettingsNumberFormat.tsx b/packages/core/client/src/schema-settings/SchemaSettingsNumberFormat.tsx new file mode 100644 index 000000000..2f578b0d3 --- /dev/null +++ b/packages/core/client/src/schema-settings/SchemaSettingsNumberFormat.tsx @@ -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 ( + + ); +}; + +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 ( + , + }, + }, + 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(); + }} + /> + ); +}; diff --git a/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts b/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts index 7272480d3..e9c608b05 100644 --- a/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts +++ b/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts @@ -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);