feat: support configuring the number of icons per row in the mobile … (#6106)

* feat: support configuring  the number of icons per row in the mobile  action penal

* fix: bug
This commit is contained in:
Katherine 2025-01-21 13:36:56 +08:00 committed by GitHub
parent 0aae4f86db
commit b79e9035cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 189 additions and 45 deletions

View File

@ -37,7 +37,7 @@ function Button() {
const { layout } = useContext(WorkbenchBlockContext);
const { styles, cx } = useStyles();
return layout === WorkbenchLayout.Grid ? (
<div title={fieldSchema.title} style={{ width: '70px', overflow: 'hidden' }}>
<div title={fieldSchema.title} style={{ width: '100%', overflow: 'hidden' }} className="nb-action-panel-container">
<Avatar style={{ backgroundColor }} size={54} icon={<Icon type={icon} />} />
<div className={cx(styles.title)}>{fieldSchema.title}</div>
</div>

View File

@ -17,10 +17,12 @@ import {
withDynamicSchemaProps,
Icon,
useBlockHeight,
useOpenModeContext,
useBlockHeightProps,
} from '@nocobase/client';
import { css } from '@emotion/css';
import { Space, List, Avatar, theme } from 'antd';
import React, { createContext, useState, useEffect } from 'react';
import React, { createContext, useEffect, useState, useRef, useMemo, useLayoutEffect } from 'react';
import { WorkbenchLayout } from './workbenchBlockSettings';
const ConfigureActionsButton = observer(
@ -31,50 +33,127 @@ const ConfigureActionsButton = observer(
},
{ displayName: 'WorkbenchConfigureActionsButton' },
);
function isMobile() {
return window.matchMedia('(max-width: 768px)').matches;
}
const ResponsiveSpace = () => {
const fieldSchema = useFieldSchema();
const isMobileMedia = isMobile();
const { isMobile: underMobileCtx } = useOpenModeContext() || {};
const { itemsPerRow = 4 } = fieldSchema.parent['x-decorator-props'] || {};
const isUnderMobile = isMobileMedia || underMobileCtx;
const containerRef = useRef(null); // 引用容器
const [containerWidth, setContainerWidth] = useState(0); // 容器宽度
const gap = 8;
// 使用 ResizeObserver 动态获取容器宽度
useEffect(() => {
const handleResize = () => {
if (containerRef.current) {
setContainerWidth(containerRef.current.offsetWidth); // 更新宽度
}
};
// 初始化 ResizeObserver
const resizeObserver = new ResizeObserver(handleResize);
// 监听容器宽度变化
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
handleResize(); // 初始化时获取一次宽度
}
return () => {
if (containerRef.current) {
resizeObserver.unobserve(containerRef.current);
}
};
}, []);
useLayoutEffect(() => {
if (!containerRef.current) return;
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
setContainerWidth(entry.contentRect.width); // 更新宽度
}
});
observer.observe(containerRef.current);
return () => {
observer.unobserve(containerRef.current);
};
}, []);
// 计算每个元素的宽度
const itemWidth = useMemo(() => {
if (isUnderMobile) {
const totalGapWidth = gap * itemsPerRow;
const availableWidth = containerWidth - totalGapWidth;
return availableWidth / itemsPerRow;
}
return 70;
}, [itemsPerRow, gap, containerWidth]);
// 计算 Avatar 的宽度
const avatarSize = useMemo(() => {
return isUnderMobile ? (Math.floor(itemWidth * 0.8) > 70 ? 60 : Math.floor(itemWidth * 0.8)) : 54; // Avatar 大小为 item 宽度的 60%
}, [itemWidth, itemsPerRow, containerWidth]);
return (
<div ref={containerRef} style={{ width: '100%' }}>
<Space
wrap
style={{ width: '100%', display: 'flex' }}
size={gap}
align="start"
className={css`
.ant-space-item {
width: ${isUnderMobile ? itemWidth + 'px' : '100%'}
display: flex;
.ant-nb-action {
padding: ${isUnderMobile ? '4px 0px' : null};
}
.nb-action-panel-container {
width: ${itemWidth}px !important;
}
.ant-avatar-circle {
width: ${avatarSize}px !important;
height: ${avatarSize}px !important;
line-height: ${avatarSize}px !important;
}
}
`}
>
{fieldSchema.mapProperties((s, key) => (
<div
key={key}
style={
isUnderMobile && {
flexBasis: `${itemWidth}px`,
flexShrink: 0,
flexGrow: 0,
display: 'flex',
}
}
>
<RecursionField name={key} schema={s} key={key} />
</div>
))}
</Space>
</div>
);
};
const InternalIcons = () => {
const fieldSchema = useFieldSchema();
const { designable } = useDesignable();
const { layout = WorkbenchLayout.Grid } = fieldSchema.parent['x-component-props'] || {};
const [gap, setGap] = useState(8); // 初始 gap 值
useEffect(() => {
const calculateGap = () => {
const container = document.getElementsByClassName('mobile-page-content')[0] as any;
if (container) {
const containerWidth = container.offsetWidth - 48;
const itemWidth = 100; // 每个 item 的宽度
const itemsPerRow = Math.floor(containerWidth / itemWidth); // 每行能容纳的 item 数
// 计算实际需要的 gap 值
const totalItemWidth = itemsPerRow * itemWidth;
const totalAvailableWidth = containerWidth;
const totalGapsWidth = totalAvailableWidth - totalItemWidth;
if (totalGapsWidth > 0) {
setGap(totalGapsWidth / (itemsPerRow - 1));
} else {
setGap(0); // 如果没有足够的空间,则设置 gap 为 0
}
}
};
window.addEventListener('resize', calculateGap);
calculateGap(); // 初始化时计算 gap
return () => {
window.removeEventListener('resize', calculateGap);
};
}, [Object.keys(fieldSchema?.properties || {}).length]);
return (
<div style={{ marginBottom: designable ? '1rem' : 0 }} className="nb-action-panel-warp">
<DndContext>
{layout === WorkbenchLayout.Grid ? (
<Space wrap size={gap}>
{fieldSchema.mapProperties((s, key) => (
<RecursionField name={key} schema={s} key={key} />
))}
</Space>
<ResponsiveSpace />
) : (
<List itemLayout="horizontal">
{fieldSchema.mapProperties((s, key) => {
@ -91,6 +170,7 @@ const InternalIcons = () => {
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
margin: 0 0 0 0;
}
.ant-list-item-meta-title button {
font-size: 14px;
@ -124,18 +204,26 @@ export const WorkbenchBlock: any = withDynamicSchemaProps(
const targetHeight = useBlockHeight();
const { token } = theme.useToken();
const { designable } = useDesignable();
const { heightProps } = useBlockHeightProps() || {};
const { titleHeight } = heightProps || {};
const internalHeight = 2 * token.paddingLG + token.controlHeight + token.marginLG + titleHeight;
return (
<div className="nb-action-penal-container">
<div
className={css`
.nb-action-panel-warp {
height: ${targetHeight ? targetHeight - (designable ? 4 : 2) * token.marginLG + 'px' : '100%'};
height: ${targetHeight
? targetHeight -
(designable ? internalHeight : 2 * token.paddingLG + token.marginLG + titleHeight) +
'px'
: '100%'};
overflow-y: auto;
margin-left: -24px;
margin-right: -24px;
padding-left: 24px;
padding-right: 24px;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
`}
>

View File

@ -12,6 +12,8 @@ import {
SchemaSettingsSelectItem,
useDesignable,
SchemaSettingsBlockHeightItem,
SchemaSettingsModalItem,
useOpenModeContext,
} from '@nocobase/client';
import { CustomSchemaSettingsBlockTitleItem } from './SchemaSettingsBlockTitleItem';
import React from 'react';
@ -53,6 +55,49 @@ const ActionPanelLayout = () => {
);
};
export function ActionPanelItemsPerRow() {
const field = useField();
const fieldSchema = useFieldSchema();
const { dn } = useDesignable();
const { t } = useTranslation();
return (
<SchemaSettingsModalItem
title={t('Items per row', { ns: 'block-workbench' })}
schema={{
type: 'object',
properties: {
itemsPerRow: {
type: 'number',
default: fieldSchema?.['x-decorator-props']?.['itemsPerRow'] || 4,
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
'x-component-props': {
min: 1,
max: 6,
},
description: t('At least 1, up to 6', { ns: 'block-workbench' }),
required: true,
},
},
}}
onSubmit={({ itemsPerRow }) => {
const componentProps = fieldSchema['x-decorator-props'] || {};
componentProps.itemsPerRow = itemsPerRow;
fieldSchema['x-decorator-props'] = componentProps;
field.decoratorProps.ItemsPerRow = itemsPerRow;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
dn.refresh();
}}
/>
);
}
export const workbenchBlockSettings = new SchemaSettings({
name: 'blockSettings:workbench',
items: [
@ -68,6 +113,15 @@ export const workbenchBlockSettings = new SchemaSettings({
name: 'layout',
Component: ActionPanelLayout,
},
{
name: 'itemsPerRow',
Component: ActionPanelItemsPerRow,
useVisible() {
const { isMobile } = useOpenModeContext() || {};
const fieldSchema = useFieldSchema();
return isMobile && fieldSchema?.['x-component-props']?.layout !== WorkbenchLayout.List;
},
},
{
type: 'remove',
name: 'remove',

View File

@ -11,5 +11,7 @@
"Grid": "栅格",
"List": "列表",
"Add popup": "添加弹窗",
"Add custom request":"添加自定义请求"
"Add custom request":"添加自定义请求",
"At least 1, up to 6": "最多6个最少一个",
"Items per row":"每行显示个数"
}

View File

@ -46,11 +46,11 @@ export const useStyles = createStyles(({ token, css }) => {
.nb-action-panel {
padding-top: 10px;
}
.nb-action-panel .ant-avatar-circle {
width: 48px !important;
height: 48px !important;
line-height: 48px !important;
}
// .nb-action-panel .ant-avatar-circle {
// width: 48px !important;
// height: 48px !important;
// line-height: 48px !important;
// }
.nb-chart-block .ant-card .ant-card-body {
padding-bottom: 0px;
padding-top: 0px;