mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
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:
parent
0aae4f86db
commit
b79e9035cb
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
`}
|
||||
>
|
||||
|
@ -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',
|
||||
|
@ -11,5 +11,7 @@
|
||||
"Grid": "栅格",
|
||||
"List": "列表",
|
||||
"Add popup": "添加弹窗",
|
||||
"Add custom request":"添加自定义请求"
|
||||
"Add custom request":"添加自定义请求",
|
||||
"At least 1, up to 6": "最多6个,最少一个",
|
||||
"Items per row":"每行显示个数"
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user