diff --git a/packages/s2-core/__tests__/spreadsheet/spread-sheet-with-switcher-spec.tsx b/packages/s2-core/__tests__/spreadsheet/switcher-spec.tsx similarity index 77% rename from packages/s2-core/__tests__/spreadsheet/spread-sheet-with-switcher-spec.tsx rename to packages/s2-core/__tests__/spreadsheet/switcher-spec.tsx index 9391e4f2e5..648f985ecc 100644 --- a/packages/s2-core/__tests__/spreadsheet/spread-sheet-with-switcher-spec.tsx +++ b/packages/s2-core/__tests__/spreadsheet/switcher-spec.tsx @@ -1,10 +1,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { act } from 'react-dom/test-utils'; -import { getContainer } from '../util/helpers'; -import { SheetEntry } from '../util/sheet-entry'; +import { getContainer } from 'tests/util/helpers'; import { Switcher } from '@/components/switcher'; -import { SwitcherItem } from '@/components/switcher/interface'; +import { SwitcherFields, SwitcherItem } from '@/components/switcher/interface'; +import 'antd/dist/antd.min.css'; const mockRows: SwitcherItem[] = [ { id: 'area', displayName: '区域' }, @@ -43,29 +43,30 @@ const mockValues: SwitcherItem[] = [ ]; function MainLayout() { - const values = ['cost', 'price', 'cost/price']; - + const fields: SwitcherFields = { + rows: { + items: mockRows, + }, + columns: { + items: mockCols, + }, + values: { + selectable: true, + expandable: true, + items: mockValues, + }, + }; return (
{ // eslint-disable-next-line no-console console.log('result: ', result); }} />
-
); } diff --git a/packages/s2-core/__tests__/spreadsheet/table-sheet-spec.tsx b/packages/s2-core/__tests__/spreadsheet/table-sheet-spec.tsx index 7bb5767fad..48dd53a1ce 100644 --- a/packages/s2-core/__tests__/spreadsheet/table-sheet-spec.tsx +++ b/packages/s2-core/__tests__/spreadsheet/table-sheet-spec.tsx @@ -15,7 +15,7 @@ import { TableSheet, } from '@/index'; import { Switcher } from '@/components/switcher'; -import { SwitcherItem } from '@/components/switcher/interface'; +import { SwitcherFields } from '@/components/switcher/interface'; let s2: TableSheet; @@ -159,14 +159,18 @@ function MainLayout({ callback }) { }; }, []); - const switcherValues: SwitcherItem[] = columns.map((field) => { - return { - id: field, - displayName: find(meta, { field })?.name, - checked: true, - }; - }); - + const switcherFields: SwitcherFields = { + columns: { + selectable: true, + items: columns.map((field) => { + return { + id: field, + displayName: find(meta, { field })?.name, + checked: true, + }; + }), + }, + }; useEffect(() => { callback({ setShowPagination, @@ -177,11 +181,11 @@ function MainLayout({ callback }) { { console.log('result: ', result); - const { hiddenValues } = result; - setHiddenColumnFields(hiddenValues); + const { hideItems } = result.columns; + setHiddenColumnFields(hideItems.map((i) => i.id)); }} /> { - describe('useVisible Test', () => { - test('should update visible status correctly when call show or hide function', () => { - const { result } = renderHook(() => useVisible()); - - expect(result.current.visible).toBeFalsy(); - - act(() => { - result.current.show(); - }); - expect(result.current.visible).toBeTruthy(); - - act(() => { - result.current.hide(); - }); - expect(result.current.visible).toBeFalsy(); - }); - - test('should update visible status correctly when call toggle function', () => { - const { result } = renderHook(() => useVisible(true)); - - expect(result.current.visible).toBeTruthy(); - - act(() => { - result.current.toggle(); - }); - expect(result.current.visible).toBeFalsy(); - - act(() => { - result.current.toggle(); - }); - expect(result.current.visible).toBeTruthy(); - }); - }); - - describe('useCustomChild Test', () => { - test('should render default child without specify custom child', () => { - const { result } = renderHook(() => - useCustomChild(
default child
), - ); - - expect(result.current.props.children).toEqual('default child'); - }); - - test('should render custom child with specify custom child', () => { - const { result } = renderHook(() => - useCustomChild(
default child
,
custom child
), - ); - - expect(result.current.props.children).toEqual('custom child'); - }); - }); - - describe('useHide Test', () => { - test('should return true when every dimension items is empty', () => { - const mockData: DimensionType[] = [ - { - type: 'value', - displayName: '指标', - items: [], - }, - ]; - const { result } = renderHook(() => useHide(mockData)); - - expect(result.current).toBeTruthy(); - }); - - test('should return true when every dimension has items', () => { - const mockData: DimensionType[] = [ - { - type: 'value', - displayName: '指标', - items: [ - { - id: 'price', - displayName: '价格', - checked: true, - }, - { - id: 'city', - displayName: '城市', - checked: true, - }, - ], - }, - ]; - const { result } = renderHook(() => useHide(mockData)); - - expect(result.current).toBeFalsy(); - }); - }); -}); diff --git a/packages/s2-core/__tests__/unit/components/dimension-switch/util.spec.ts b/packages/s2-core/__tests__/unit/components/dimension-switch/util.spec.ts deleted file mode 100644 index a1b693c666..0000000000 --- a/packages/s2-core/__tests__/unit/components/dimension-switch/util.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { DimensionType } from '@/components/dimension-switch/dimension'; -import { getDimensionsByPredicate } from '@/components/dimension-switch/util'; - -describe('Dimension Switch Util Test', () => { - const mockData: DimensionType[] = [ - { - type: 'measure', - displayName: '维度', - items: [ - { - id: 'city', - displayName: '城市', - checked: true, - }, - { - id: 'count', - displayName: '数量', - checked: true, - }, - { - id: 'price', - displayName: '价格', - checked: false, - }, - ], - }, - ]; - - test('should return all checked item ids', () => { - expect( - getDimensionsByPredicate(mockData, (value) => value.checked), - ).toEqual({ - measure: ['city', 'count'], - }); - }); - - test('should return all unchecked item ids', () => { - expect( - getDimensionsByPredicate(mockData, (value) => !value.checked), - ).toEqual({ - measure: ['price'], - }); - }); -}); diff --git a/packages/s2-core/__tests__/unit/components/switcher/util.spec.ts b/packages/s2-core/__tests__/unit/components/switcher/util.spec.ts index 2fcf98843a..7d4fa22cb6 100644 --- a/packages/s2-core/__tests__/unit/components/switcher/util.spec.ts +++ b/packages/s2-core/__tests__/unit/components/switcher/util.spec.ts @@ -1,15 +1,20 @@ -import { getNonEmptyFieldCount } from './../../../../src/components/switcher/util'; import { FieldType } from '@/components/switcher/constant'; -import { SwitcherItem, SwitcherState } from '@/components/switcher/interface'; import { + SwitcherItem, + SwitcherState, + SwitcherFields, +} from '@/components/switcher/interface'; +import { + getNonEmptyFieldCount, + getSwitcherState, getSwitcherClassName, getMainLayoutClassName, - shouldDimensionCrossRows, - isMeasureType, + shouldCrossRows, moveItem, checkItem, generateSwitchResult, } from '@/components/switcher/util'; + describe('switcher util test', () => { test('should return correct class name with prefix', () => { expect(getSwitcherClassName('content', 'text')).toEqual( @@ -46,21 +51,16 @@ describe('switcher util test', () => { ); test('should return true if nonempty count is less than max count', () => { - expect(shouldDimensionCrossRows(2)).toBeTrue(); + expect(shouldCrossRows(2, FieldType.Rows)).toBeTrue(); }); test('should return true if nonempty count is greater than max count', () => { - expect(shouldDimensionCrossRows(3)).toBeFalse(); - expect(shouldDimensionCrossRows(4)).toBeFalse(); - }); - - test('should return true if field type is values', () => { - expect(isMeasureType(FieldType.Values)).toBeTrue(); + expect(shouldCrossRows(3, FieldType.Rows)).toBeFalse(); + expect(shouldCrossRows(4, FieldType.Rows)).toBeFalse(); }); - test('should return false if field type is not values', () => { - expect(isMeasureType(FieldType.Rows)).toBeFalse(); - expect(isMeasureType(FieldType.Cols)).toBeFalse(); + test('should return true if field type is value', () => { + expect(shouldCrossRows(1, FieldType.Values)).toBeTrue(); }); describe('move item test', () => { @@ -256,10 +256,39 @@ describe('switcher util test', () => { }); test('should return generate switch result when values have no children', () => { expect(generateSwitchResult(state)).toEqual({ - rows: ['r1', 'r2'], - cols: ['c1', 'c2'], - values: ['v1', 'v2'], - hiddenValues: [], + rows: { + items: [ + { + id: 'r1', + }, + { + id: 'r2', + }, + ], + hideItems: [], + }, + columns: { + items: [ + { + id: 'c1', + }, + { + id: 'c2', + }, + ], + hideItems: [], + }, + values: { + items: [ + { + id: 'v1', + }, + { + id: 'v2', + }, + ], + hideItems: [], + }, }); }); @@ -280,11 +309,47 @@ describe('switcher util test', () => { id: 'v2', }, ]; + expect(generateSwitchResult(state)).toEqual({ - rows: ['r1', 'r2'], - cols: ['c1', 'c2'], - values: ['v1', 'vc1', 'vc2', 'v2'], - hiddenValues: [], + rows: { + items: [ + { + id: 'r1', + }, + { + id: 'r2', + }, + ], + hideItems: [], + }, + columns: { + items: [ + { + id: 'c1', + }, + { + id: 'c2', + }, + ], + hideItems: [], + }, + values: { + items: [ + { + id: 'v1', + }, + { + id: 'vc1', + }, + { + id: 'vc2', + }, + { + id: 'v2', + }, + ], + hideItems: [], + }, }); }); @@ -314,11 +379,84 @@ describe('switcher util test', () => { }, ]; expect(generateSwitchResult(state)).toEqual({ - rows: ['r1', 'r2'], - cols: ['c1', 'c2'], - values: ['v1', 'vc1', 'vc2', 'v2', 'vc3'], - hiddenValues: ['vc1', 'v2', 'vc3'], + rows: { + items: [ + { + id: 'r1', + }, + { + id: 'r2', + }, + ], + hideItems: [], + }, + columns: { + items: [ + { + id: 'c1', + }, + { + id: 'c2', + }, + ], + hideItems: [], + }, + values: { + items: [ + { + id: 'v1', + }, + { + id: 'vc1', + checked: false, + }, + { + id: 'vc2', + }, + { + id: 'v2', + checked: false, + }, + { + id: 'vc3', + checked: false, + }, + ], + hideItems: [ + { + id: 'vc1', + checked: false, + }, + { + id: 'v2', + checked: false, + }, + { + id: 'vc3', + checked: false, + }, + ], + }, }); }); }); + + test('should return switcher state from switcher fields', () => { + const fields: SwitcherFields = { + rows: { + items: [{ id: 'row' }], + }, + columns: { + items: [{ id: 'column' }], + }, + values: { + items: [{ id: 'value' }], + }, + }; + expect(getSwitcherState(fields)).toEqual({ + rows: [{ id: 'row' }], + columns: [{ id: 'column' }], + values: [{ id: 'value' }], + }); + }); }); diff --git a/packages/s2-core/package.json b/packages/s2-core/package.json index 408113a4ab..5ae14fabd1 100644 --- a/packages/s2-core/package.json +++ b/packages/s2-core/package.json @@ -75,7 +75,7 @@ "bundlesize": [ { "path": "./dist/s2.min.js", - "maxSize": "120 kB" + "maxSize": "200 kB" }, { "path": "./dist/s2.min.css", diff --git a/packages/s2-core/src/common/i18n/en_US.ts b/packages/s2-core/src/common/i18n/en_US.ts index 4d89b8cdca..55fc0075ad 100644 --- a/packages/s2-core/src/common/i18n/en_US.ts +++ b/packages/s2-core/src/common/i18n/en_US.ts @@ -30,5 +30,5 @@ export const EN_US = { 行头: 'Rows', 列头: 'Cols', 值: 'Values', - 展开同环比: 'Expand DerivedValues', + 展开子项: 'Expand Children', }; diff --git a/packages/s2-core/src/common/i18n/zh_CN.ts b/packages/s2-core/src/common/i18n/zh_CN.ts index b9aef61901..1eb01ccfca 100644 --- a/packages/s2-core/src/common/i18n/zh_CN.ts +++ b/packages/s2-core/src/common/i18n/zh_CN.ts @@ -31,5 +31,5 @@ export const ZH_CN = { 行头: '行头', 列头: '列头', 值: '值', - 展开同环比: '展开同环比', + 展开子项: '展开子项', }; diff --git a/packages/s2-core/src/components/dimension-switch/dimension/index.less b/packages/s2-core/src/components/dimension-switch/dimension/index.less deleted file mode 100644 index 2685c9d7c7..0000000000 --- a/packages/s2-core/src/components/dimension-switch/dimension/index.less +++ /dev/null @@ -1,83 +0,0 @@ -.dimension { - display: flex; - flex-direction: column; - width: 200px; - - &-display-name { - display: flex; - align-items: center; - justify-content: space-between; - height: 24px; - color: black; - font-size: 12px; - - .search-btn { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - padding: 0; - border: none; - box-shadow: none; - - &:hover { - background-color: rgba(0, 0, 0, 0.03); - } - } - } - - &-items { - flex: auto; - min-height: 90px; - max-height: 180px; - margin-top: 8px; - padding: 0 8px; - overflow-y: auto; - border: 1px solid rgba(0, 0, 0, 0.15); - - .dimension-item { - .ant-checkbox-wrapper { - &:hover { - background-color: rgba(0, 0, 0, 0.03); - } - - display: flex; - width: 100%; - height: 24px; - margin: 2px 0; - - & > span:nth-child(2) { - flex: 1; - padding: 0; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - } - - &-name { - margin-left: 8px; - font-size: 12px; - } - } - } - - .summary { - height: 28px; - margin-top: 8px; - padding-left: 8px; - line-height: 28px; - cursor: pointer; - - .total { - margin-left: 4px; - color: rgba(0, 0, 0, 0.45); - } - - .reset { - float: right; - color: rgba(0, 0, 0, 0.45); - } - } -} diff --git a/packages/s2-core/src/components/dimension-switch/dimension/index.tsx b/packages/s2-core/src/components/dimension-switch/dimension/index.tsx deleted file mode 100644 index 66d95013b5..0000000000 --- a/packages/s2-core/src/components/dimension-switch/dimension/index.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import { SearchOutlined } from '@ant-design/icons'; -import { Button, Checkbox, Input, Tooltip } from 'antd'; -import React, { FC, useEffect, useRef, useState } from 'react'; -import { i18n } from '@/common/i18n'; -import './index.less'; - -export interface DimensionItem { - id: string; - displayName: string; - checked: boolean; - disabled?: boolean; -} - -export interface DimensionType { - type: string; - displayName: string; - items: DimensionItem[]; -} - -interface DimensionProps extends DimensionType { - keepSearching?: boolean; - onSelect: (type: string, idList: string[], checked: boolean) => void; -} - -export const Dimension: FC = ({ - type, - displayName, - items, - keepSearching = false, - onSelect, -}) => { - const [height, setHeight] = useState(0); - const ref = useRef(); - - const [searching, setSearching] = useState(keepSearching); - const [filterChecked, setFilterChecked] = useState(false); - const [keyword, setKeyword] = useState(''); - - const checkedList = items.filter((i) => i.checked).map((i) => i.id); - - // eslint-disable-next-line no-nested-ternary - const filterResult = filterChecked - ? items.filter((i) => i.checked) - : keyword - ? items.filter((i) => new RegExp(keyword, 'i').test(i.displayName)) - : items; - - const filterCheckedCount = filterResult.filter((i) => i.checked).length; - - const indeterminate = - filterCheckedCount > 0 && filterCheckedCount < filterResult.length; - const checkedAll = - filterResult.length > 0 && filterCheckedCount === filterResult.length; - - const onShowSearchInput = () => { - setSearching(true); - setFilterChecked(false); - - const onHideSearchingInput = (event) => { - const target: HTMLElement = event.target; - // 排除items区域,所有 checkbox 及其 label 文字的点击 - if ( - target.tagName === 'INPUT' || - target.tagName === 'LABEL' || - (target.tagName === 'SPAN' && - target.parentElement.tagName === 'LABEL') || - target.className.startsWith('dimension-item') - ) { - return; - } - - setSearching(false); - setKeyword(''); - - document.removeEventListener('click', onHideSearchingInput); - }; - - document.addEventListener('click', onHideSearchingInput); - }; - - const onUpdateCheckItem = (itemId: string, checked: boolean) => { - onSelect(type, [itemId], checked); - }; - - const onUpdateCheckedAll = (checked: boolean) => { - onSelect( - type, - filterResult.map((i) => i.id), - checked, - ); - }; - - const onFilterAllChecked = () => { - setFilterChecked(true); - setKeyword(''); - if (!keepSearching) { - setSearching(false); - } - }; - useEffect(() => { - setHeight(ref.current.offsetHeight); - }, []); - - return ( -
-
- {searching ? ( - } - autoFocus={true} - placeholder={i18n('请输入关键字搜索')} - allowClear={true} - value={keyword} - onChange={(e) => setKeyword(e.target.value)} - /> - ) : ( - <> - {displayName} - -
- -
- {filterResult.map((i) => ( - - ))} -
- -
- onUpdateCheckedAll(e.target.checked)} - /> - - - {i18n('已选 {} 项').replace('{}', checkedList.length)} - - - {filterChecked && ( - - )} -
-
- ); -}; - -interface DimensionItemProps extends DimensionItem { - onUpdateCheckItem: (id: string, checked: boolean) => void; -} - -const DimensionItem: FC = ({ - id, - checked, - disabled, - displayName, - onUpdateCheckItem, -}) => { - const ref = useRef(); - const [ellipsis, setEllipsis] = useState(false); - - useEffect(() => { - // 针对超长文字,添加 tooltip - setEllipsis( - ref.current.offsetWidth > ref.current.parentElement.offsetWidth, - ); - }, []); - - return ( -
- { - onUpdateCheckItem(id, e.target.checked); - }} - > - {ellipsis ? ( - - - {displayName} - - - ) : ( - - {displayName} - - )} - -
- ); -}; diff --git a/packages/s2-core/src/components/dimension-switch/hooks.tsx b/packages/s2-core/src/components/dimension-switch/hooks.tsx deleted file mode 100644 index d96e5ac0c4..0000000000 --- a/packages/s2-core/src/components/dimension-switch/hooks.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { isEmpty } from 'lodash'; -import { Children, ReactElement, ReactNode, useState } from 'react'; -import { DimensionType } from './dimension'; - -export const useVisible = (defaultVisible = false) => { - const [visible, setVisible] = useState(defaultVisible); - const show = () => { - setVisible(true); - }; - const hide = () => { - setVisible(false); - }; - const toggle = () => { - setVisible(!visible); - }; - return { visible, show, hide, toggle }; -}; - -export const useCustomChild = ( - defaultChild: ReactElement, - child?: ReactNode, -) => { - return (child ? Children.only(child) : defaultChild) as ReactElement; -}; - -export const useHide = (data: DimensionType[]) => { - return data.every((dimension) => isEmpty(dimension.items)); -}; diff --git a/packages/s2-core/src/components/dimension-switch/index.less b/packages/s2-core/src/components/dimension-switch/index.less deleted file mode 100644 index fdaa7b57b2..0000000000 --- a/packages/s2-core/src/components/dimension-switch/index.less +++ /dev/null @@ -1,69 +0,0 @@ -@prefix: s2-dimension-switch; -@dropdown-prefix: s2-dropdown; - -.@{prefix}-overlay { - .ant-modal-body, - .ant-popover-inner { - overflow: hidden; - } -} - -.@{prefix}-icon-button { - padding: 0 !important; - border: none !important; - box-shadow: none !important; -} - -.@{prefix}-popover-title { - display: flex; - align-items: center; - justify-content: space-between; - height: 40px; - .@{prefix}-icon-button { - color: rgba(0, 0, 0, 0.45); - } -} - -.@{dropdown-prefix}-overlay { - .ant-popover-inner-content { - position: relative; - padding: 8px; - - [class^='dimension-items'] { - border: none; - } - - [class^='summary'] { - position: absolute; - bottom: 10px; - z-index: 10; - width: 148px; - } - } -} -.@{dropdown-prefix} { - display: inline-flex; - align-items: center; - min-width: 94px; - max-width: 250px; - height: 24px; - font-size: 12px; - border-bottom: 1px solid #f0f0f0; - cursor: pointer; - - .label { - margin-right: 10px; - color: #000; - white-space: nowrap; - opacity: 0.65; - } - - .content { - flex: auto; - margin-right: 5px; - overflow: hidden; - color: #000; - white-space: nowrap; - text-overflow: ellipsis; - } -} diff --git a/packages/s2-core/src/components/dimension-switch/index.tsx b/packages/s2-core/src/components/dimension-switch/index.tsx deleted file mode 100644 index 8edf6ee8c0..0000000000 --- a/packages/s2-core/src/components/dimension-switch/index.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import React, { cloneElement, FC } from 'react'; -import { Modal, Popover, Button } from 'antd'; -import { - DownOutlined, - EditOutlined, - SwapOutlined, - UpOutlined, - CloseOutlined, -} from '@ant-design/icons'; -import cx from 'classnames'; -import { DimensionType } from './dimension'; -import { DimensionSwitch } from './switch'; -import { useCustomChild, useVisible, useHide } from './hooks'; -import { i18n } from '@/common/i18n'; -import './index.less'; - -interface DimensionSwitchMultipleProps { - overlayClassName?: string; - data: DimensionType[]; - onUpdateDisableItems?: (type: string, checkedList: string[]) => string[]; - onSubmit?: (result: DimensionType[]) => void; -} -export const DimensionSwitchModal: FC< - DimensionSwitchMultipleProps & { onModalVisibilityChange?: () => void } -> = ({ - overlayClassName, - data, - onSubmit: onOuterSubmit, - onUpdateDisableItems, - children, - onModalVisibilityChange, -}) => { - const { visible, show, hide } = useVisible(); - const shouldHide = useHide(data); - const child = useCustomChild( - , - children, - ); - const onSubmit = (result: DimensionType[]) => { - hide(); - onOuterSubmit?.(result); - }; - return shouldHide ? null : ( - - {i18n('选择分析信息')}{' '} - - - - - ); -}; diff --git a/packages/s2-core/src/components/dimension-switch/util.ts b/packages/s2-core/src/components/dimension-switch/util.ts deleted file mode 100644 index 57a4920580..0000000000 --- a/packages/s2-core/src/components/dimension-switch/util.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { DimensionType, DimensionItem } from './dimension'; - -export interface OperatedType { - [type: string]: string[]; -} - -export const getDimensionsByPredicate = ( - data: DimensionType[], - predicate: (value: DimensionItem) => boolean, -) => { - return data.reduce((obj, t) => { - obj[t.type] = t.items.filter(predicate).map((i) => i.id); - return obj; - }, {} as OperatedType); -}; diff --git a/packages/s2-core/src/components/icons/index.less b/packages/s2-core/src/components/icons/index.less index cd8504598f..6679a48a3f 100644 --- a/packages/s2-core/src/components/icons/index.less +++ b/packages/s2-core/src/components/icons/index.less @@ -1,4 +1,4 @@ -@pre: s2-icon; +@pre: antv-s2-icon; .@{pre} { margin-right: 10px; diff --git a/packages/s2-core/src/components/icons/index.tsx b/packages/s2-core/src/components/icons/index.tsx index 98b944ace1..c4290aadd5 100644 --- a/packages/s2-core/src/components/icons/index.tsx +++ b/packages/s2-core/src/components/icons/index.tsx @@ -1,11 +1,11 @@ import React, { FC } from 'react'; import './index.less'; -const PRECLASS = 'spreadsheet-icon'; +const ICON_CLS = 'antv-s2-icon'; export const CalendarIcon: FC = () => ( ( export const TextIcon: FC = () => ( ( export const LocationIcon: FC = () => ( ( export const SearchIcon: FC = () => ( ( export const SwitcherIcon: FC = () => ( ( export const RowIcon: FC = () => ( ( export const ColIcon: FC = () => ( ( export const ValueIcon: FC = () => ( { return ; } }; + +export { DrillDown, DrillDownProps } from './drill-down'; +export { Switcher, SwitcherProps } from './switcher'; diff --git a/packages/s2-core/src/components/switcher/constant.ts b/packages/s2-core/src/components/switcher/constant.ts index 6dc69b53eb..49f336e43e 100644 --- a/packages/s2-core/src/components/switcher/constant.ts +++ b/packages/s2-core/src/components/switcher/constant.ts @@ -5,7 +5,7 @@ export const SWITCHER_PREFIX_CLS = 'switcher'; export enum FieldType { Rows = 'rows', - Cols = 'cols', + Cols = 'columns', Values = 'values', } @@ -14,18 +14,27 @@ export enum DroppableType { Measure = 'measure', } +export const SWITCHER_FIELDS = [ + FieldType.Rows, + FieldType.Cols, + FieldType.Values, +]; + export const SWITCHER_CONFIG = { [FieldType.Rows]: { text: i18n('行头'), icon: RowIcon, + droppableType: DroppableType.Dimension, }, [FieldType.Cols]: { text: i18n('列头'), icon: ColIcon, + droppableType: DroppableType.Dimension, }, [FieldType.Values]: { text: i18n('值'), icon: ValueIcon, + droppableType: DroppableType.Measure, }, } as const; diff --git a/packages/s2-core/src/components/switcher/content/index.less b/packages/s2-core/src/components/switcher/content/index.less index ebc2d5b4b1..4956ce3d59 100644 --- a/packages/s2-core/src/components/switcher/content/index.less +++ b/packages/s2-core/src/components/switcher/content/index.less @@ -37,12 +37,6 @@ grid-template-columns: 1fr; } - &-dimension-option { - .description { - margin-left: 4px; - } - } - &-reset-button { color: #3572f9; font-size: @large-font-size; @@ -57,7 +51,7 @@ &::after { width: 4.8px; - height: 9px; + height: 8px; } } } diff --git a/packages/s2-core/src/components/switcher/content/index.tsx b/packages/s2-core/src/components/switcher/content/index.tsx index 1eb97fd815..12f864a13c 100644 --- a/packages/s2-core/src/components/switcher/content/index.tsx +++ b/packages/s2-core/src/components/switcher/content/index.tsx @@ -1,6 +1,5 @@ import { ReloadOutlined } from '@ant-design/icons'; -import { Button, Checkbox } from 'antd'; -import { CheckboxChangeEvent } from 'antd/lib/checkbox'; +import { Button } from 'antd'; import { isEmpty } from 'lodash'; import cx from 'classnames'; import React, { forwardRef, useImperativeHandle, useState } from 'react'; @@ -9,17 +8,18 @@ import { DragDropContext, DropResult, } from 'react-beautiful-dnd'; -import { DroppableType, FieldType } from '../constant'; +import { FieldType, SWITCHER_CONFIG, SWITCHER_FIELDS } from '../constant'; import { Dimension } from '../dimension'; -import { SwitcherResult, SwitcherState } from '../interface'; +import { SwitcherFields, SwitcherResult, SwitcherState } from '../interface'; import { checkItem, generateSwitchResult, getMainLayoutClassName, getNonEmptyFieldCount, getSwitcherClassName, + getSwitcherState, moveItem, - shouldDimensionCrossRows, + shouldCrossRows, } from '../util'; import { i18n } from '@/common/i18n'; import './index.less'; @@ -29,26 +29,19 @@ export interface SwitcherContentRef { getResult: () => SwitcherResult; } -export interface SwitcherContentProps extends SwitcherState { - expandBtnText?: string; - resetBtnText?: string; +export interface SwitcherContentProps extends SwitcherFields { + resetText?: string; } export const SwitcherContent = forwardRef( - ( - { expandBtnText, resetBtnText, ...defaultState }: SwitcherContentProps, - ref, - ) => { + ({ resetText, ...defaultFields }: SwitcherContentProps, ref) => { + const defaultState = getSwitcherState(defaultFields); + const [state, setState] = useState(defaultState); - const [expand, setExpand] = useState(true); - const [draggingItemId, setDraggingItemId] = useState(); + const [draggingItemId, setDraggingItemId] = useState(null); const nonEmptyCount = getNonEmptyFieldCount(defaultState); - const onUpdateExpand = (event: CheckboxChangeEvent) => { - setExpand(event.target.checked); - }; - const onBeforeDragStart = (initial: BeforeCapture) => { setDraggingItemId(initial.draggableId); }; @@ -93,8 +86,8 @@ export const SwitcherContent = forwardRef( ); const onVisibleItemChange = ( - checked: boolean, fieldType: FieldType, + checked: boolean, id: string, parentId?: string, ) => { @@ -120,42 +113,20 @@ export const SwitcherContent = forwardRef( getMainLayoutClassName(nonEmptyCount), )} > - {[FieldType.Rows, FieldType.Cols].map( + {SWITCHER_FIELDS.map( (type) => isEmpty(defaultState[type]) || ( ), )} - - {isEmpty(defaultState.values) || ( - - - - {expandBtnText ?? i18n('展开同环比')} - - - } - /> - )}
@@ -177,8 +148,7 @@ export const SwitcherContent = forwardRef( ); SwitcherContent.displayName = 'SwitcherContent'; + SwitcherContent.defaultProps = { - rows: [], - cols: [], - values: [], + resetText: i18n('恢复默认'), }; diff --git a/packages/s2-core/src/components/switcher/dimension/index.less b/packages/s2-core/src/components/switcher/dimension/index.less index 083146aa00..25db61e840 100644 --- a/packages/s2-core/src/components/switcher/dimension/index.less +++ b/packages/s2-core/src/components/switcher/dimension/index.less @@ -28,6 +28,12 @@ display: inline-flex; align-items: center; } + + .expand-option { + .description { + margin-left: 4px; + } + } } &-items { diff --git a/packages/s2-core/src/components/switcher/dimension/index.tsx b/packages/s2-core/src/components/switcher/dimension/index.tsx index 7fcb5d8b7e..8ab514852d 100644 --- a/packages/s2-core/src/components/switcher/dimension/index.tsx +++ b/packages/s2-core/src/components/switcher/dimension/index.tsx @@ -1,28 +1,37 @@ import cx from 'classnames'; -import React, { FC, ReactNode } from 'react'; +import React, { FC, useState } from 'react'; import { Droppable } from 'react-beautiful-dnd'; +import { CheckboxChangeEvent } from 'antd/lib/checkbox'; +import { Checkbox } from 'antd'; import { DroppableType, SWITCHER_CONFIG } from '../constant'; -import { SwitcherItem } from '../interface'; +import { SwitcherField, SwitcherItem } from '../interface'; import { DimensionCommonProps, DimensionItem } from '../item'; import { getSwitcherClassName } from '../util'; +import { i18n } from '@/common/i18n'; import './index.less'; const CLASS_NAME_PREFIX = 'dimension'; -interface DimensionProps extends DimensionCommonProps { - data: SwitcherItem[]; - droppableType: DroppableType; - crossRows?: boolean; - option?: ReactNode; -} +type DimensionProps = SwitcherField & + DimensionCommonProps & { + droppableType: DroppableType; + crossRows?: boolean; + }; export const Dimension: FC = ({ - data, fieldType, - droppableType, crossRows, - option, + expandable, + expandText, + items, + droppableType, ...rest }) => { + const [expandChildren, setExpandChildren] = useState(false); + + const onUpdateExpand = (event: CheckboxChangeEvent) => { + setExpandChildren(event.target.checked); + }; + const { text, icon: Icon } = SWITCHER_CONFIG[fieldType]; return (
= ({
{text}
- {option} + {expandable && ( +
+ + {expandText} +
+ )}
@@ -49,12 +63,14 @@ export const Dimension: FC = ({ crossRows, })} > - {data.map((item: SwitcherItem, index: number) => ( + {items.map((item: SwitcherItem, index: number) => ( ))} @@ -68,4 +84,8 @@ export const Dimension: FC = ({ Dimension.defaultProps = { crossRows: false, + expandable: false, + expandText: i18n('展开子项'), + selectable: false, + items: [], }; diff --git a/packages/s2-core/src/components/switcher/index.tsx b/packages/s2-core/src/components/switcher/index.tsx index d7e0c6c2ba..bdf188461d 100644 --- a/packages/s2-core/src/components/switcher/index.tsx +++ b/packages/s2-core/src/components/switcher/index.tsx @@ -1,5 +1,5 @@ import { Button, Popover } from 'antd'; -import React, { FC, useRef } from 'react'; +import React, { FC, useRef, ReactNode } from 'react'; import { SwitcherIcon } from '../icons'; import { SwitcherContent, @@ -12,10 +12,11 @@ import { getSwitcherClassName } from './util'; import { i18n } from '@/common/i18n'; export interface SwitcherProps extends SwitcherContentProps { + title?: ReactNode; onSubmit?: (result: SwitcherResult) => void; } -export const Switcher: FC = ({ onSubmit, ...props }) => { +export const Switcher: FC = ({ title, onSubmit, ...props }) => { const ref = useRef(); return ( = ({ onSubmit, ...props }) => { } }} > - + {title || ( + + )} ); }; diff --git a/packages/s2-core/src/components/switcher/interface.ts b/packages/s2-core/src/components/switcher/interface.ts index 9e5c0efe05..09a0fb0620 100644 --- a/packages/s2-core/src/components/switcher/interface.ts +++ b/packages/s2-core/src/components/switcher/interface.ts @@ -1,10 +1,12 @@ import { FieldType } from './constant'; +type SwitcherItemWithoutChildren = Omit; + export interface SwitcherItem { id: string; displayName?: string; checked?: boolean; - children?: Omit[]; + children?: SwitcherItemWithoutChildren[]; } export interface SwitcherState { @@ -13,9 +15,25 @@ export interface SwitcherState { [FieldType.Values]?: SwitcherItem[]; } +export interface SwitcherField { + expandable?: boolean; + expandText?: string; + selectable?: boolean; + items: SwitcherItem[]; +} + +export interface SwitcherFields { + [FieldType.Rows]?: SwitcherField; + [FieldType.Cols]?: SwitcherField; + [FieldType.Values]?: SwitcherField; +} + +export interface SwitcherResultItem { + items: SwitcherItemWithoutChildren[]; + hideItems: SwitcherItemWithoutChildren[]; +} export interface SwitcherResult { - [FieldType.Rows]: string[]; - [FieldType.Cols]: string[]; - [FieldType.Values]: string[]; - hiddenValues: string[]; + [FieldType.Rows]: SwitcherResultItem; + [FieldType.Cols]: SwitcherResultItem; + [FieldType.Values]: SwitcherResultItem; } diff --git a/packages/s2-core/src/components/switcher/item/index.less b/packages/s2-core/src/components/switcher/item/index.less index e7f790347c..c565d9bbb0 100644 --- a/packages/s2-core/src/components/switcher/item/index.less +++ b/packages/s2-core/src/components/switcher/item/index.less @@ -1,8 +1,8 @@ @prefix: antv-s2-switcher; -@dimension-color: #d9eeff; -@measure-color: #d3f4e5; -@child-measure-color: #effbf6; +@normal-item-color: #d9eeff; +@checkable-item-color: #d3f4e5; +@checkable-child-color: #effbf6; @large-span: 12px; @normal-span: 4px; @@ -20,18 +20,18 @@ text-overflow: ellipsis; } - &.dimension-item { + &.normal-item { padding: 0 @large-span; - background-color: @dimension-color; + background-color: @normal-item-color; align-items: center; } - &.measure-item { + &.checkable-item { padding: 0 @normal-span; - background-color: @measure-color; + background-color: @checkable-item-color; align-items: baseline; - &.measure-collapse { + &.item-collapse { transition: border-radius 0s 0.2s; border-radius: @border-radius !important; } @@ -50,8 +50,8 @@ } } -.@{prefix}-dimension-list, -.@{prefix}-measure-list { +.@{prefix}-normal-list, +.@{prefix}-checkable-list { border-radius: @border-radius; & + & { @@ -62,14 +62,14 @@ box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.1); } - .child-measures { + .child-items { transition: max-height 0.2s, opacity 0.4s; overflow: hidden; max-height: 1000px; - .measure-item { + .checkable-item { padding: 0 @large-span; - background-color: @child-measure-color; + background-color: @checkable-child-color; margin-top: 0; border-radius: 0; @@ -78,7 +78,7 @@ } } - &.measures-hidden { + &.item-hidden { max-height: 0; opacity: 0; } diff --git a/packages/s2-core/src/components/switcher/item/index.tsx b/packages/s2-core/src/components/switcher/item/index.tsx index cffbb33609..23233888db 100644 --- a/packages/s2-core/src/components/switcher/item/index.tsx +++ b/packages/s2-core/src/components/switcher/item/index.tsx @@ -3,19 +3,18 @@ import { isEmpty } from 'lodash'; import React, { FC } from 'react'; import { Draggable } from 'react-beautiful-dnd'; import { FieldType } from '../constant'; -import { SwitcherItem } from '../interface'; -import { getSwitcherClassName, isMeasureType } from '../util'; +import { SwitcherField, SwitcherItem } from '../interface'; +import { getSwitcherClassName } from '../util'; import { SingleItem } from './single-item'; import './index.less'; -export interface DimensionCommonProps { +export interface DimensionCommonProps + extends Pick { fieldType: FieldType; - expandChildren?: boolean; draggingItemId?: string; - - onVisibleItemChange?: ( - checked: boolean, + onVisibleItemChange: ( fieldType: FieldType, + checked: boolean, id: string, parentId?: string, ) => void; @@ -24,18 +23,19 @@ export interface DimensionCommonProps { export type DimensionItemProps = DimensionCommonProps & { index: number; item: SwitcherItem; + expandChildren: boolean; }; export const DimensionItem: FC = ({ fieldType, item: { id, displayName, checked = true, children = [] }, + expandable, expandChildren, + selectable, index, draggingItemId, onVisibleItemChange, }) => { - const isMeasure = isMeasureType(fieldType); - return ( {(provided, snapshot) => ( @@ -44,7 +44,7 @@ export const DimensionItem: FC = ({ {...provided.dragHandleProps} ref={provided.innerRef} className={cx( - getSwitcherClassName(isMeasure ? 'measure-list' : 'dimension-list'), + getSwitcherClassName(selectable ? 'checkable-list' : 'normal-list'), { 'list-dragging': snapshot.isDragging, }, @@ -56,38 +56,39 @@ export const DimensionItem: FC = ({ displayName={displayName} checked={checked} onVisibleItemChange={onVisibleItemChange} - className={cx(isMeasure ? 'measure-item' : 'dimension-item', { - 'measure-collapse': !expandChildren, + selectable={selectable} + className={cx(selectable ? 'checkable-item' : 'normal-item', { + 'item-collapse': !expandChildren, })} /> - {isMeasure && !isEmpty(children) && draggingItemId !== id && ( -
- {children.map((item) => ( - - ))} -
- )} + {expandable && + expandChildren && + !isEmpty(children) && + draggingItemId !== id && ( +
+ {children.map((item) => ( + + ))} +
+ )} )}
); }; - -DimensionItem.defaultProps = { - expandChildren: false, -}; diff --git a/packages/s2-core/src/components/switcher/item/single-item.tsx b/packages/s2-core/src/components/switcher/item/single-item.tsx index 5b3d3a66ea..e6dff1dce9 100644 --- a/packages/s2-core/src/components/switcher/item/single-item.tsx +++ b/packages/s2-core/src/components/switcher/item/single-item.tsx @@ -1,23 +1,25 @@ import { Checkbox, Tooltip } from 'antd'; import cx from 'classnames'; import React, { FC, useEffect, useRef, useState } from 'react'; -import { SwitcherItem } from '../interface'; +import { SwitcherField, SwitcherItem } from '../interface'; import { getSwitcherClassName } from '../util'; import { DimensionCommonProps } from '.'; const CLASS_NAME_PREFIX = 'item'; type SingleItemProps = Omit & - Pick & { + Pick & + DimensionCommonProps & { parentId?: string; - className: string; disabled?: boolean; + className: string; }; export const SingleItem: FC = ({ fieldType, id, displayName, + selectable, checked, parentId, className, @@ -39,12 +41,12 @@ export const SingleItem: FC = ({ unchecked: !checked, })} > - {onVisibleItemChange && ( + {selectable && ( - onVisibleItemChange?.(e.target.checked, fieldType, id, parentId) + onVisibleItemChange?.(fieldType, e.target.checked, id, parentId) } /> )} diff --git a/packages/s2-core/src/components/switcher/util.ts b/packages/s2-core/src/components/switcher/util.ts index 93bbd95fec..d839476039 100644 --- a/packages/s2-core/src/components/switcher/util.ts +++ b/packages/s2-core/src/components/switcher/util.ts @@ -1,11 +1,17 @@ -import { filter, flatten, isEmpty, map } from 'lodash'; +import { filter, flatten, isEmpty, map, mapValues } from 'lodash'; import { DraggableLocation } from 'react-beautiful-dnd'; import { FieldType, MAX_DIMENSION_COUNT, SWITCHER_PREFIX_CLS, } from './constant'; -import { SwitcherItem, SwitcherResult, SwitcherState } from './interface'; +import { + SwitcherItem, + SwitcherResult, + SwitcherState, + SwitcherFields, + SwitcherResultItem, +} from './interface'; import { getClassNameWithPrefix } from '@/utils/get-classnames'; export const getSwitcherClassName = (...classNames: string[]) => @@ -30,11 +36,8 @@ export const getMainLayoutClassName = (nonEmptyCount: number) => { } }; -export const shouldDimensionCrossRows = (nonEmptyCount: number) => - nonEmptyCount < MAX_DIMENSION_COUNT; - -export const isMeasureType = (fieldType: FieldType) => - fieldType === FieldType.Values; +export const shouldCrossRows = (nonEmptyCount: number, type: FieldType) => + nonEmptyCount < MAX_DIMENSION_COUNT || type === FieldType.Values; export const moveItem = ( source: SwitcherItem[], @@ -92,30 +95,33 @@ export const checkItem = ( }; export const generateSwitchResult = (state: SwitcherState): SwitcherResult => { - const mapIds = (items: SwitcherItem[]) => map(items, 'id'); - // rows and cols can't be hidden - const rows = mapIds(state[FieldType.Rows]); - const cols = mapIds(state[FieldType.Cols]); - - // flatten all values and derived values - const flattenValues = (items: SwitcherItem[]) => - flatten( - map(items, (item) => { - return [ - { id: item.id, checked: item.checked }, - ...flattenValues(item.children), - ]; - }), + const generateFieldResult = (items: SwitcherItem[]): SwitcherResultItem => { + const flattenValues = (list: SwitcherItem[]) => + flatten( + map(list, ({ children, ...rest }) => { + return [{ ...rest }, ...flattenValues(children)]; + }), + ); + + const allItems = flattenValues(items); + + // get all hidden values + const hideItems = filter( + allItems, + (item: SwitcherItem) => item.checked === false, ); + return { + items: allItems, + hideItems, + }; + }; - const flattedValues = flattenValues(state[FieldType.Values]); - - const values = mapIds(flattedValues); - - // get all hidden values - const hiddenValues = mapIds( - filter(flattedValues, (item: SwitcherItem) => item.checked === false), - ); - - return { rows, cols, values, hiddenValues }; + return { + [FieldType.Rows]: generateFieldResult(state[FieldType.Rows]), + [FieldType.Cols]: generateFieldResult(state[FieldType.Cols]), + [FieldType.Values]: generateFieldResult(state[FieldType.Values]), + }; }; + +export const getSwitcherState = (fields: SwitcherFields): SwitcherState => + mapValues(fields, 'items'); diff --git a/packages/s2-core/src/index.ts b/packages/s2-core/src/index.ts index b6fbb25ab9..c1c4d8859c 100644 --- a/packages/s2-core/src/index.ts +++ b/packages/s2-core/src/index.ts @@ -5,7 +5,7 @@ export { Node } from './facet/layout/node'; export { Hierarchy } from './facet/layout/hierarchy'; export { BaseEvent, BaseEventImplement } from './interaction/base-interaction'; export { GuiIcon } from './common/icons/gui-icon'; -export { DrillDown, DrillDownProps } from './components/drill-down'; + export * from './components/index'; export * from './utils'; export * from './cell'; diff --git a/s2-site/docs/api/components/drill-down.en.md b/s2-site/docs/api/components/drill-down.en.md index 60f927f575..744ccd3d8c 100644 --- a/s2-site/docs/api/components/drill-down.en.md +++ b/s2-site/docs/api/components/drill-down.en.md @@ -1,6 +1,6 @@ --- title: Drill Down -order: 2 +order: 1 --- `markdown:docs/api/components/drill-down.zh.md` diff --git a/s2-site/docs/api/components/drill-down.zh.md b/s2-site/docs/api/components/drill-down.zh.md index 96e5692ea5..2f0fcfbb9b 100644 --- a/s2-site/docs/api/components/drill-down.zh.md +++ b/s2-site/docs/api/components/drill-down.zh.md @@ -1,6 +1,6 @@ --- title: 下钻 -order: 2 +order: 1 --- # 下钻 diff --git a/s2-site/docs/api/components/switcher.en.md b/s2-site/docs/api/components/switcher.en.md new file mode 100644 index 0000000000..8d9d5f07fa --- /dev/null +++ b/s2-site/docs/api/components/switcher.en.md @@ -0,0 +1,6 @@ +--- +title: Switcher +order: 2 +--- + +`markdown:docs/api/components/switcher.zh.md` diff --git a/s2-site/docs/api/components/switcher.zh.md b/s2-site/docs/api/components/switcher.zh.md new file mode 100644 index 0000000000..f531ca6e0a --- /dev/null +++ b/s2-site/docs/api/components/switcher.zh.md @@ -0,0 +1,58 @@ +--- +title: 维度切换组件 +order: 2 +--- + +# 维度切换组件 + +## Switcher 组件 Props + +| 属性 | 类型 | 必选 | 默认值 | 功能描述 | +| :---------- | :--------------- | :---- | :------ | :---------- | +| rows | `SwitcherField` | | `[]` | 行头配置描述 | +| columns | `SwitcherField` | | `[]` | 列头配置描述 | +| values | `SwitcherField` | | `[]` | 指标配置描述 | +| title | `ReactNode` | | 默认按钮 | 打开切换弹窗的触发节点 | +| resetText | `string` | | `恢复默认` | 重置按钮文字 | +| onSubmit | `(result: SwitcherResult) => void` | | - | 关闭弹窗后,处理行列切换结果的回调函数 | + +## SwitcherField + +行列头以及指标值得配置描述对象 + +| 属性 | 类型 | 必选 | 默认值 | 功能描述 | +| :---------- | :--------------- | :---- | :------ | :---------- | +| items | `SwitcherItem[]` | ✓ | - | 配置字段对象 | +| expandable | `boolean` | | `false` | 是否打开展开子项的 checkbox 用于控制展开和隐藏子项 | +| expandText | `string` | | `展开子项` | 展开子项的 checkbox 对应的文字 | +| selectable | `boolean` | | `false` | 是否打开字段的 checkbox用于控制显隐 | + +## SwitcherItem + +配置字段对象 + +| 属性 | 类型 | 必选 | 默认值 | 功能描述 | +| :---------- | :--------------- | :---- | :------ | :---------- | +| id | `string` | ✓ | - | 字段 id | +| displayName | `string` | | - | 字段显示名字,该字段不存在时直接显示 id | +| checked | `boolean` | | `true` | 字段是否需要显示 | +| children | `SwitcherItem[]` | | `[]` | 如果字段存在关联子项(如:同环比),使用该属性配置子项 | + +## SwitcherResult + +关闭弹窗后,处理行列切换结果的回调函数的参数 + +| 属性 | 类型 | 必选 | 默认值 | 功能描述 | +| :---------- | :--------------- | :---- | :------ | :---------- | +| rows | `SwitcherResultItem[]` | | `[]` | 所有行头字段操作结果 | +| cols | `SwitcherResultItem[]` | | `[]` | 所有列头字段操作结果 | +| values | `SwitcherResultItem[]` | | `[]` | 所有指标字段操作结果 | + +## SwitcherResultItem + +关闭弹窗后,每个维度结果的描述对象 + +| 属性 | 类型 | 必选 | 默认值 | 功能描述 | +| :---------- | :--------------- | :---- | :------ | :---------- | +| items | `SwitcherItem[]` | | `[]` | 全部字段的被**压平**集合,按拖拽后顺序排序 | +| hideItems | `SwitcherItem[]` | | `[]` | 所有需要隐藏字段被**压平**的集合,按拖拽后顺序排序 | diff --git a/s2-site/gatsby-config.js b/s2-site/gatsby-config.js index ddd471bda3..eb7ab64f08 100644 --- a/s2-site/gatsby-config.js +++ b/s2-site/gatsby-config.js @@ -131,6 +131,14 @@ module.exports = { en: 'Total', }, }, + { + slug: 'analysis', + icon: 'facet', + title: { + zh: '分析组件', + en: 'Analyze component', + }, + }, ], // 编辑器配置 playground: {