feat: 汇总区块添加表格样式及配置项排序 (#536)

Reviewed-on: daoyoucloud/tachycode#536
Co-authored-by: wjh <wwwjh0710@163.com>
Co-committed-by: wjh <wwwjh0710@163.com>
This commit is contained in:
wjh 2024-03-30 02:22:44 +08:00 committed by sealday
parent dc7c6a5052
commit 1675b39d0e
5 changed files with 351 additions and 330 deletions

View File

@ -1,9 +1,10 @@
import { useField, useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import { findFilterTargets, useAPIClient, useBlockRequestContext, useFilterBlock } from '@nocobase/client'; import { findFilterTargets, useAPIClient, useBlockRequestContext, useFilterBlock } from '@nocobase/client';
import { Descriptions, DescriptionsProps, Spin } from 'antd'; import { Descriptions, DescriptionsProps, Space, Spin } from 'antd';
import { transformers } from './GroupBlockConfigure/transformers'; import { transformers } from './GroupBlockConfigure/transformers';
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useAsyncEffect } from 'ahooks'; import { useAsyncEffect } from 'ahooks';
import { GroupConfigure } from './GroupBlockConfigure/GroupConfigure';
type DataItem = { type DataItem = {
labels: string[]; labels: string[];
@ -20,11 +21,8 @@ export const GroupBlock = (props) => {
const params = fieldSchema.parent['x-decorator-props'].params; const params = fieldSchema.parent['x-decorator-props'].params;
const { resource, service } = useBlockRequestContext(); const { resource, service } = useBlockRequestContext();
const { getDataBlocks } = useFilterBlock(); const { getDataBlocks } = useFilterBlock();
const { targets = [], uid } = findFilterTargets(fieldSchema);
const api = useAPIClient();
const [result, setResult] = useState([]);
const items: DescriptionsProps['items'] = [];
const Blocks = getDataBlocks(); const Blocks = getDataBlocks();
const [group, setGroup] = useState([]);
if (!service.params.length) { if (!service.params.length) {
getDataBlocks().map((block) => { getDataBlocks().map((block) => {
if (Object.keys(block.defaultFilter).length) { if (Object.keys(block.defaultFilter).length) {
@ -37,84 +35,35 @@ export const GroupBlock = (props) => {
}); });
} }
useAsyncEffect(async () => {
const filter = service?.params[0] ? service.params[0].filter : service?.params;
setResult(
await Promise.all(
params?.config?.request?.map(async (requeatItem) => {
return [
await api.request({
url: requeatItem.fieldFormat.requestUrl,
method: 'POST',
data: {
filter: { ...filter },
collection: params.collection,
},
}),
requeatItem,
];
}) ?? [],
),
);
}, [Blocks, service.params, service.params[0]]);
if (service.loading && !field.loaded) { if (service.loading && !field.loaded) {
return <Spin />; return <Spin />;
} }
const data = service.data?.data[0] ? { ...service.data?.data[0] } : {}; return (
<>
params?.config?.measures?.forEach((measuresItem) => { <p style={{ fontWeight: 600 }}></p>
const value = measuresItem.fieldFormat.fieldValue; {params?.config.map((configItem, index) => {
if (!data[value]) data[value] = 0; if (configItem) {
data[value] = fieldTransformers(measuresItem, data[value], api); return <GroupConfigure {...props} configItem={configItem} service={service} key={index} />;
});
for (const key in data) {
params?.config?.measures?.forEach((value) => {
if (value.field[0] === key) {
if (value.display) {
items.push({
key: key,
label: value.label,
children: data[key],
});
} }
} })}
}); </>
} );
result.forEach(([data, requeatItem]) => {
data?.data?.data?.forEach((requestData: requestData) => {
const dataItem = { key: requestData.label, label: requestData.label, children: '' };
if (typeof requestData.value === 'object') {
requestData.value.labels.forEach((labelItem, index) => {
dataItem.children =
dataItem.children +
` ${labelItem} ${fieldTransformers(requeatItem, requestData.value['values'][index], api)}`;
});
} else {
dataItem.children = fieldTransformers(requeatItem, requestData.value.toString(), api);
}
items.push(dataItem);
});
});
return <Descriptions title="汇总:" items={items} />;
}; };
const fieldTransformers = (item, data, api) => { export const fieldTransformers = (item, data, api) => {
const { option: tOption } = transformers; const { option: tOption } = transformers;
const locale = api.auth.getLocale(); const locale = api.auth.getLocale();
if (item.fieldFormat) { if (item) {
const option = item.fieldFormat.option; const format = item.format;
const decimal = item.fieldFormat.decimal; const digits = item?.digits;
if (option && option !== 'decimal') { if (format && format !== 'decimal') {
const component = tOption.filter((tValue) => tValue.value === option)[0].component; const component = tOption.find((tValue) => tValue.value === format).component;
data = String(data).includes(',') ? String(data).replace(/,/g, '') : data; data = String(data).includes(',') ? String(data).replace(/,/g, '') : data;
return component(data, locale); return component(data, locale);
} else if (option && option === 'decimal') { } else if (format && format === 'decimal' && digits) {
const component = tOption const component = tOption
.filter((tValue) => tValue.value === 'decimal')[0] .filter((tValue) => tValue.value === 'decimal')[0]
.childrens.filter((decimalOption) => decimalOption.value === decimal)[0].component; .childrens.filter((decimalOption) => decimalOption.value === digits)[0].component;
data = String(data).includes(',') ? String(data).replace(/,/g, '') : data; data = String(data).includes(',') ? String(data).replace(/,/g, '') : data;
return component(data); return component(data);
} }

View File

@ -1,21 +1,205 @@
import { SchemaSettingsItem } from '@nocobase/client'; import {
SchemaComponentOptions,
SchemaSettingsItem,
SchemaSettingsModalItem,
useBlockRequestContext,
useDesignable,
} from '@nocobase/client';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { useTranslation } from '../../../locale'; import { useTranslation } from '../../../locale';
import { Modal } from 'antd';
import { GroupBlockContext } from '../../../schema-initializer/blocks/GroupBlockInitializer'; import { GroupBlockContext } from '../../../schema-initializer/blocks/GroupBlockInitializer';
import { transformers } from './transformers';
import { ISchema, connect, useFieldSchema } from '@formily/react';
import { ArrayItems, FormItem, Space } from '@formily/antd-v5';
import { PullRequestOutlined } from '@ant-design/icons';
export const GroupBlockConfigure = (props) => { export const GroupBlockConfigure = connect((props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { setVisible, visible } = useContext(GroupBlockContext); const fieldSchema = useFieldSchema();
const { dn } = useDesignable();
const { service } = useBlockRequestContext();
const params = fieldSchema['x-decorator-props']?.params;
const measures = params?.measures;
const decimal = transformers.option.filter((item) => item.value === 'decimal')[0].childrens;
const valueOption = measures.map((item) => {
return {
label: item.label,
value: item.field[0],
};
});
const { scope } = useContext(GroupBlockContext);
const schema = modalSchema(t, params, valueOption, decimal);
return ( return (
<SchemaSettingsItem <SchemaComponentOptions scope={{ ...scope }} components={{ ArrayItems, FormItem, Space, PullRequestOutlined }}>
<SchemaSettingsModalItem
title="Configure" title="Configure"
key="configure" key="configure"
onClick={async () => { onSubmit={(fieldOptionItem) => {
setVisible(true); fieldSchema['x-decorator-props'].params['config'] = [...fieldOptionItem.configField];
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
dn.refresh();
service?.refresh();
}} }}
> schema={schema as ISchema}
{t('Configure')} />
</SchemaSettingsItem> </SchemaComponentOptions>
); );
});
const fieldType = [
{
label: '字段配置',
value: 'field',
},
{
label: '请求配置',
value: 'custom',
},
];
const styleOption = [
{
label: '描述',
value: 'describe',
},
{
label: '表格',
value: 'table',
},
];
const modalSchema = (t, params, valueOption, decimal) => {
const schema = {
title: t('Configure Group'),
type: 'object',
properties: {
configField: {
title: 'Config Field',
type: 'array',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems',
default: params.config,
items: {
type: 'object',
'x-decorator': 'ArrayItems.Item',
properties: {
space: {
type: 'void',
'x-component': 'Space',
properties: {
sort: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.SortHandle',
},
type: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
placeholder: '{{t("type")}}',
},
enum: fieldType,
require: true,
},
field: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
placeholder: '{{t("field")}}',
},
'x-visbile': false,
enum: valueOption,
'x-reactions': {
dependencies: ['.type'],
fulfill: {
schema: {
'x-visible': "{{$deps[0]==='field'}}",
},
},
},
},
reqUrl: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '{{t("reqUrl")}}',
addonBefore: <PullRequestOutlined />,
},
'x-visible': false,
'x-reactions': {
dependencies: ['.type'],
fulfill: {
schema: {
'x-visible': "{{$deps[0]==='custom'}}",
},
},
},
},
format: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
placeholder: '{{t("format")}}',
},
enum: transformers.option,
require: true,
},
digits: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
placeholder: '{{t("digits")}}',
},
enum: decimal,
'x-visible': false,
'x-reactions': {
dependencies: ['.format'],
fulfill: {
schema: {
'x-visible': "{{$deps[0]==='decimal'}}",
},
},
},
},
style: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
placeholder: '{{t("style")}}',
},
enum: styleOption,
require: true,
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: 'Add',
'x-component': 'ArrayItems.Addition',
},
},
},
},
};
return schema;
}; };

View File

@ -1,231 +1,132 @@
import { useField, useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import { useDesignable, useFieldNames, useFilterBlock } from '@nocobase/client'; import { useAPIClient, useFilterBlock, useRequest } from '@nocobase/client';
import { App, Button, Flex, Input, Modal, Select } from 'antd'; import { Descriptions, DescriptionsProps, Spin, Table } from 'antd';
import React, { useContext, useEffect, useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from '../../../locale'; import { useAsyncEffect } from 'ahooks';
import { GroupBlockContext } from '../../../schema-initializer/blocks/GroupBlockInitializer'; import { fieldTransformers } from '../GroupBlock';
import { transformers } from './transformers';
import { DeleteOutlined, PullRequestOutlined } from '@ant-design/icons';
import { uid } from '@formily/shared';
export type SelectedField = { type reqData = {
field: string | string[]; labels: any[];
alias?: string; values: any[];
};
export type fieldOptionType = {
label: string;
fieldFormat: {
fieldValue: string;
option: string;
type: string;
decimal?: string;
requestUrl?: string;
};
aggregation?: string;
field?: [string];
display?: boolean;
}; };
export const GroupConfigure = (props) => { export const GroupConfigure = (props) => {
const { visible, setVisible } = useContext(GroupBlockContext); const { configItem, service } = props;
const { t } = useTranslation();
const { dn } = useDesignable();
const { modal } = App.useApp();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const measures = fieldSchema['x-decorator-props']?.params?.measures; const field = useField<any>();
const option: fieldOptionType[] = []; const params = fieldSchema.parent['x-decorator-props'].params;
const params = fieldSchema['x-decorator-props']?.params; const [result, setResult] = useState({});
if (params['config']) { const { getDataBlocks } = useFilterBlock();
if (params['config']['measures']) { const Blocks = getDataBlocks();
option.push(...params['config']['measures']); const api = useAPIClient();
} useAsyncEffect(async () => {
if (params['config']['request']) { const filter = service?.params[0] ? service.params[0].filter : service?.params;
option.push(...params['config']['request']); if (configItem.reqUrl) {
} setResult(
} (await api.request({
url: configItem.reqUrl,
const [fieldOption, setFieldOption] = useState(JSON.parse(JSON.stringify(option))); method: 'POST',
const fieldType = [ data: {
{ filter: { ...filter },
label: '字段配置', collection: params.collection,
value: 'field',
}, },
{ })) ?? {},
label: '请求配置',
value: 'custom',
},
];
const valueOption = measures.map((item) => {
return {
label: item.label,
value: item.field[0],
};
});
const decimal = transformers.option.filter((item) => item.value === 'decimal')[0].childrens;
useEffect(() => {
if (!visible) {
return;
}
}, [visible]);
return (
<Modal
title={t('Configure Group')}
open={visible}
onOk={() => {
const request = [];
const configMeasures = [];
fieldOption.forEach((fieldOptionItem) => {
if (fieldOptionItem.fieldFormat.type === 'field') {
const { measureItem, index } = measures
.map((measureItem, index) => {
if (measureItem.field[0] === fieldOptionItem?.fieldFormat?.fieldValue) {
return { measureItem, index };
} else {
return {};
}
})
.filter((value) => value.measureItem)[0];
if (measureItem) {
measureItem['fieldFormat'] = { ...fieldOptionItem.fieldFormat };
configMeasures.push(measureItem);
}
} else if (fieldOptionItem.fieldFormat.type === 'custom') {
request.push({ ...fieldOptionItem, label: '自定义请求', field: [uid()] });
}
});
fieldSchema['x-decorator-props']['params']['config'] = {
measures: configMeasures,
request,
};
dn.emit('patch', {
schema: fieldSchema,
});
setVisible(false);
dn.refresh();
}}
onCancel={() => {
modal.confirm({
title: t('Are you sure to cancel?'),
content: t('You changes are not saved. If you click OK, your changes will be lost.'),
okButtonProps: {
danger: true,
},
onOk: () => {
setVisible(false);
},
});
}}
width={'30%'}
>
<div style={{ display: 'flex', marginBottom: '10px', marginTop: '10px', flexDirection: 'column' }}>
{fieldOption.map((item, index) => {
return (
<div style={{ marginBottom: '7px' }} key={index}>
<Select
options={fieldType}
placeholder="Type"
style={{ marginRight: '5px' }}
value={item?.fieldFormat?.type ? item?.fieldFormat?.type : undefined}
onChange={(v) => {
fieldConfig('type', fieldOption, setFieldOption, index, v);
}}
/>
{item?.fieldFormat?.type === 'field' ? (
<Select
options={valueOption}
placeholder="Field"
style={{ marginRight: '5px' }}
value={item?.fieldFormat?.fieldValue ? item?.fieldFormat?.fieldValue : undefined}
onChange={(v) => {
fieldConfig('field', fieldOption, setFieldOption, index, v);
}}
/>
) : null}
{item?.fieldFormat?.type === 'custom' ? (
<Input
addonBefore={<PullRequestOutlined />}
placeholder="RequestUrl"
allowClear
style={{ marginRight: '5px', marginTop: '5px', marginBottom: '5px' }}
defaultValue={item?.fieldFormat?.requestUrl ? item?.fieldFormat?.requestUrl : undefined}
onChange={(v) => {
fieldConfig('url', fieldOption, setFieldOption, index, v.currentTarget.value);
}}
/>
) : null}
<Select
options={transformers.option}
placeholder="Format"
style={{ marginRight: '5px' }}
value={item?.fieldFormat?.option ? item?.fieldFormat?.option : undefined}
onChange={(v) => {
fieldConfig('format', fieldOption, setFieldOption, index, v);
}}
/>
{item?.fieldFormat?.option === 'decimal' ? (
<Select
options={decimal}
placeholder="Digits"
style={{ marginRight: '5px' }}
value={item.fieldFormat.decimal}
onChange={(v) => fieldConfig('decimal', fieldOption, setFieldOption, index, v)}
/>
) : null}
<DeleteOutlined
style={{ fontSize: '14px' }}
onClick={() => {
fieldConfig('del', fieldOption, setFieldOption, index);
}}
/>
</div>
);
})}
</div>
<Button
type="dashed"
style={{ borderRadius: '0', width: '30%' }}
onClick={() => {
fieldConfig('add', fieldOption, setFieldOption);
}}
>
+ Add Field
</Button>
</Modal>
); );
}
}, [Blocks, service.params, service.params[0]]);
if (configItem.style === 'describe') {
const item: DescriptionsProps['items'] = describeItem(configItem, result, service, params, api);
return <Descriptions style={{ marginBottom: '10px' }} items={item} />;
} else if (configItem.style === 'table') {
const { columns, options } = tableItem(configItem, result, service, params, api);
return <Table style={{ marginBottom: '10px' }} columns={columns} dataSource={options} pagination={false} />;
}
}; };
const fieldConfig = (type, fieldOption, setFieldOption, index?, record?) => { const describeItem = (configItem, result, service, params, api) => {
const option = [...fieldOption]; const item = [];
if (type === 'add') { if (configItem.type === 'field') {
option.push({}); const measuresData = service.data?.data;
} else if (type === 'del') { if (measuresData) {
option.splice(index, 1); let data = measuresData.map((value) => {
} else if (type === 'type') { return value[configItem.field];
option[index]['fieldFormat'] = { })[0];
...option[index]['fieldFormat'], data = fieldTransformers(configItem, data, api);
type: record, const label = params.measures.find((value) => value.field[0] === configItem.field).label;
}; item.push({
} else if (type === 'url') { key: configItem.field,
option[index]['fieldFormat'] = { label,
...option[index]['fieldFormat'], children: data,
requestUrl: record, });
};
} else if (type === 'field') {
option[index]['fieldFormat'] = {
...option[index]['fieldFormat'],
fieldValue: record,
};
} else if (type === 'format') {
option[index]['fieldFormat'] = {
...option[index]['fieldFormat'],
option: record,
};
} else if (type === 'decimal') {
option[index]['fieldFormat'] = {
...option[index]['fieldFormat'],
decimal: record,
};
} }
setFieldOption(option); } else if (configItem.type === 'custom') {
if (Object.keys(result).length && result?.['data']?.data) {
const data: reqData = { ...result?.['data']?.data };
const label = data.labels;
data.values.forEach((valueItem, index) => {
let childrenText = '';
label.forEach((value, index) => {
if (index === 0) return;
childrenText += `${value}${fieldTransformers(configItem, valueItem[index], api)} `;
});
item.push({
key: index,
label: valueItem[0],
children: childrenText,
});
});
}
}
return item;
};
const tableItem = (configItem, result, service, params, api) => {
const columns = [];
const options = [];
if (configItem.type === 'custom') {
if (Object.keys(result).length && result?.['data']?.data) {
const data: reqData = { ...result?.['data']?.data };
data.labels.forEach((value, index) => {
columns.push({ title: value, dataIndex: 'value' + index, key: index });
});
data.values.forEach((value, index) => {
const item = {
key: index,
};
columns.forEach((colItem, index) => {
const data = isNaN(value[index]) ? value[index] : fieldTransformers(configItem, value[index], api);
item[colItem.dataIndex] = data;
});
options.push(item);
});
}
} else if (configItem.type === 'field') {
const measuresData = service.data?.data;
if (measuresData) {
let data = measuresData.map((value) => {
return value[configItem.field];
})[0];
data = fieldTransformers(configItem, data, api);
const label = params.measures.find((value) => value.field[0] === configItem.field).label;
columns.push(
{
title: '名称',
dataIndex: 'name',
key: 'name',
},
{
title: '数量',
dataIndex: 'number',
key: 'number',
},
);
options.push({
key: label,
name: label,
number: data,
});
}
}
return { columns, options };
}; };

View File

@ -17,7 +17,6 @@ import React, { createContext, useState } from 'react';
import { uid } from '@nocobase/utils/client'; import { uid } from '@nocobase/utils/client';
import { Checkbox, Spin } from 'antd'; import { Checkbox, Spin } from 'antd';
import { GroupBlockConfigure } from '../../schema-components/blocks/GroupBlockConfigure/GroupBlockConfigure'; import { GroupBlockConfigure } from '../../schema-components/blocks/GroupBlockConfigure/GroupBlockConfigure';
import { GroupConfigure } from '../../schema-components/blocks/GroupBlockConfigure/GroupConfigure';
export const GroupBlockContext = createContext<any>({}); export const GroupBlockContext = createContext<any>({});
@ -43,7 +42,6 @@ const InternalGroupBlockProvider = (props) => {
}} }}
> >
{props.children} {props.children}
<GroupConfigure />
</GroupBlockContext.Provider> </GroupBlockContext.Provider>
); );
}; };

View File

@ -80,15 +80,14 @@ export class RecordPreviewController {
items[product.name].total -= count; items[product.name].total -= count;
} }
}); });
ctx.body = _.toArray(items) const result = {
labels: ['名称', '出库数量', '入库数量', '小计'],
values: [],
};
_.toArray(items)
.sort((a, b) => a.sort - b.sort) .sort((a, b) => a.sort - b.sort)
.map((item) => ({ .forEach((item) => result.values.push([item.name, item.out, item.in, item.total]));
label: item.name, ctx.body = { ...result };
value: {
labels: ['出库数量', '入库数量', '小计'],
values: [item.out, item.in, item.total],
},
}));
} }
} }
@Action('allweight') @Action('allweight')
@ -106,15 +105,10 @@ export class RecordPreviewController {
const inNum = records.find((item) => item.movement === Movement.in)?.weight ?? 0; const inNum = records.find((item) => item.movement === Movement.in)?.weight ?? 0;
const outNum = records.find((item) => item.movement === Movement.out)?.weight ?? 0; const outNum = records.find((item) => item.movement === Movement.out)?.weight ?? 0;
const data = [ const data = {
{ labels: ['名称', '出库数量(吨)', '入库数量(吨)', '小计(吨)'],
label: '实际重量(吨)', values: [['实际重量(吨)', outNum, inNum, inNum - outNum]],
value: { };
labels: ['出库数量(吨)', '入库数量(吨)', '小计(吨)'],
values: [outNum, inNum, inNum - outNum],
},
},
];
ctx.body = data; ctx.body = data;
} }
@Action('allprice') @Action('allprice')
@ -132,15 +126,10 @@ export class RecordPreviewController {
const inNum = records.find((item) => item.movement === Movement.in)?.all_price ?? 0; const inNum = records.find((item) => item.movement === Movement.in)?.all_price ?? 0;
const outNum = records.find((item) => item.movement === Movement.out)?.all_price ?? 0; const outNum = records.find((item) => item.movement === Movement.out)?.all_price ?? 0;
const data = [ const data = {
{ labels: ['名称', '收入', '支出', '小计'],
label: '总金额', values: [['总金额', outNum, inNum, outNum - inNum]],
value: { };
labels: ['收入', '支出', '小计'],
values: [outNum, inNum, outNum - inNum],
},
},
];
ctx.body = data; ctx.body = data;
} }