import React, { Children, createContext, useContext, useEffect, useRef, useState, forwardRef, } from 'react'; import { connect, observer, mapProps, mapReadPretty, useField, useFieldSchema, RecursionField, Schema, SchemaOptionsContext, FormProvider, useForm, } from '@formily/react'; import { Menu as AntdMenu, MenuProps, MenuItemProps, SubMenuProps, DividerProps, Dropdown, Modal, Button, Space, } from 'antd'; import { uid } from '@formily/shared'; import cls from 'classnames'; import { SchemaField, SchemaRenderer, useDesignable, } from '../../components/schema-renderer'; import { MenuOutlined, PlusOutlined, GroupOutlined, LinkOutlined, DeleteOutlined, EditOutlined, ArrowUpOutlined, ArrowDownOutlined, ArrowRightOutlined, DragOutlined, LockOutlined, } from '@ant-design/icons'; import { IconPicker } from '../../components/icon-picker'; import { createSchema, removeSchema, updateSchema, useDefaultAction } from '..'; import { useMount } from 'ahooks'; import './style.less'; import { findPropertyByPath, getSchemaPath, useSchemaPath } from '../../'; import { generateDefaultSchema } from './defaultSchemas'; import _, { cloneDeep, get, isNull } from 'lodash'; import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd'; import deepmerge from 'deepmerge'; import { onFieldChange } from '@formily/core'; import { VisibleContext } from '../../context'; import { DragHandle, SortableItem, SortableItemContext, } from '../../components/Sortable'; import { DndContext, DragOverlay } from '@dnd-kit/core'; import { createPortal } from 'react-dom'; import { Resource } from '../../resource'; export interface MenuContextProps { schema?: Schema; onRemove?: any; } export const MenuModeContext = createContext(null); export const MenuContext = createContext(null); const SideMenu = (props: any) => { const { selectedKey, defaultSelectedKeys, onSelect, path } = props; const { schema } = useDesignable(); const [selectedKeys, setSelectedKeys] = useState(defaultSelectedKeys); useEffect(() => { setSelectedKeys(defaultSelectedKeys); }, [defaultSelectedKeys]); if (!selectedKey) { return null; } const child = schema.properties && schema.properties[selectedKey]; if (!child || child['x-component'] !== 'Menu.SubMenu') { return null; } return ( { setSelectedKeys(openKeys); }} openKeys={selectedKeys} selectedKeys={selectedKeys} > ); }; export const MenuSelectedKeysContext = createContext([]); export const Menu: any = observer((props: any) => { const { mode, onSelect, sideMenuRef, defaultSelectedKeys: keys, getSelectedKeys, onRemove, ...others } = props; const defaultSelectedKeys = useContext(MenuSelectedKeysContext); const { root, schema, insertAfter, remove } = useDesignable(); const moveToAfter = (path1, path2) => { if (!path1 || !path2) { return; } if (path1.join('.') === path2.join('.')) { return; } const data = findPropertyByPath(root, path1); if (!data) { return; } remove(path1); return insertAfter(data.toJSON(), path2); }; const fieldSchema = useFieldSchema(); console.log('Menu.schema', schema, fieldSchema); const [selectedKey, setSelectedKey] = useState( defaultSelectedKeys[0] || null, ); const ref = useRef(); const path = useSchemaPath(); const child = schema.properties && schema.properties[selectedKey]; const isSubMenu = child && child['x-component'] === 'Menu.SubMenu'; useMount(() => { if (mode !== 'mix') { return; } const sideMenuElement = sideMenuRef && (sideMenuRef.current as HTMLElement); if (sideMenuElement && ref.current) { sideMenuElement.querySelector(':scope > div').appendChild(ref.current); } sideMenuElement.style.display = isSubMenu ? 'block' : 'none'; }); useEffect(() => { const sideMenuElement = sideMenuRef && (sideMenuRef.current as HTMLElement); if (!sideMenuElement) { return; } sideMenuElement.style.display = isSubMenu ? 'block' : 'none'; }, [selectedKey]); const [dragOverlayContent, setDragOverlayContent] = useState(''); // console.log('defaultSelectedKeys', defaultSelectedKeys, getSelectedKeys); return ( { console.log({ event }); setDragOverlayContent(event.active.data?.current?.title || ''); }} onDragEnd={async (event) => { console.log({ event }); const path1 = event.active?.data?.current?.path; const path2 = event.over?.data?.current?.path; const data = moveToAfter(path1, path2); await updateSchema(data); }} > {createPortal( {dragOverlayContent} , document.body, )} { if (mode === 'mix') { setSelectedKey(info.key); } const selectedSchema = schema.properties[info.key]; console.log({ selectedSchema }); onSelect && onSelect({ ...info, schema: selectedSchema }); }} > {/* */} {mode === 'mix' && (
{ const keyPath = [selectedKey, ...[...info.keyPath].reverse()]; const selectedSchema = findPropertyByPath(schema, keyPath); console.log('keyPath', keyPath, selectedSchema); onSelect && onSelect({ ...info, keyPath, schema: selectedSchema }); }} defaultSelectedKeys={defaultSelectedKeys || []} selectedKey={selectedKey} sideMenuRef={sideMenuRef} />
)}
); }); Menu.Divider = observer(AntdMenu.Divider); Menu.Item = observer((props: any) => { const { icon } = props; const { schema, DesignableBar } = useDesignable(); return ( {icon && ( )} {schema.title} ); }); Menu.Link = observer((props: any) => { const { icon } = props; const { schema, DesignableBar } = useDesignable(); return ( {icon && ( )} {schema.title} {/* {schema.title} */} ); }); Menu.URL = observer((props: any) => { const { icon } = props; const { schema, DesignableBar } = useDesignable(); return ( { window.open(props.href); }} > {icon && ( )} {schema.title} ); }); Menu.Action = observer((props: any) => { const { icon, useAction = useDefaultAction, ...others } = props; const { schema, DesignableBar } = useDesignable(); const field = useField(); const [visible, setVisible] = useState(false); const { run } = useAction(); return ( } onClick={async () => { await run(); setVisible(true); }} > {schema.title} {props.children} {/* */} ); }); Menu.SubMenu = observer((props: any) => { const { icon } = props; const { schema, DesignableBar } = useDesignable(); const mode = useContext(MenuModeContext); return mode === 'mix' ? ( ) : ( } title={ {icon && ( )} {schema.title} // <> // {schema.title} // } eventKey={schema.name} key={schema.name} > ); }); Menu.AddNew = observer((props: any) => { const { designable, appendChild } = useDesignable(props.path); if (!designable) { return null; } const schemas = { 'Menu.Link': { icon: , title: '添加页面', schema: { type: 'object', properties: { title: { type: 'string', title: '名称', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, 'x-component-props.icon': { type: 'string', title: '图标', 'x-decorator': 'FormItem', 'x-component': 'IconPicker', }, }, }, }, 'Menu.SubMenu': { icon: , title: '添加分组', schema: { type: 'object', properties: { title: { type: 'string', title: '名称', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, 'x-component-props.icon': { type: 'string', title: '图标', 'x-decorator': 'FormItem', 'x-component': 'IconPicker', }, }, }, }, 'Menu.URL': { icon: , title: '添加链接', schema: { type: 'object', properties: { title: { type: 'string', title: '名称', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, 'x-component-props.icon': { type: 'string', title: '图标', 'x-decorator': 'FormItem', 'x-component': 'IconPicker', }, 'x-component-props.href': { type: 'string', title: '链接', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, }, }; return ( { console.log({ info }); const values = await FormDialog(schemas[info.key].title, () => { return ( ); }).open(); const defaults = generateDefaultSchema(info.key); const data = appendChild(deepmerge(defaults, values)); await createSchema(data); }} > }> 分组 }> 页面 }> 链接 } > {props.children} } /> ); }); Menu.DesignableBar = (props) => { const schemas = { 'Menu.Action': { icon: , title: '页面', schema: { type: 'object', properties: { title: { type: 'string', title: '名称', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, 'x-component-props.icon': { type: 'string', title: '图标', 'x-decorator': 'FormItem', 'x-component': 'IconPicker', }, }, }, }, 'Menu.Item': { icon: , title: '页面', schema: { type: 'object', properties: { title: { type: 'string', title: '名称', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, 'x-component-props.icon': { type: 'string', title: '图标', 'x-decorator': 'FormItem', 'x-component': 'IconPicker', }, }, }, }, 'Menu.Link': { icon: , title: '页面', schema: { type: 'object', properties: { title: { type: 'string', title: '名称', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, 'x-component-props.icon': { type: 'string', title: '图标', 'x-decorator': 'FormItem', 'x-component': 'IconPicker', }, }, }, }, 'Menu.SubMenu': { icon: , title: '分组', schema: { type: 'object', properties: { title: { type: 'string', title: '名称', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, 'x-component-props.icon': { type: 'string', title: '图标', 'x-decorator': 'FormItem', 'x-component': 'IconPicker', }, }, }, }, 'Menu.URL': { icon: , title: '链接', schema: { type: 'object', properties: { title: { type: 'string', title: '名称', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, 'x-component-props.icon': { type: 'string', title: '图标', 'x-decorator': 'FormItem', 'x-component': 'IconPicker', }, 'x-component-props.href': { type: 'string', title: '链接', required: true, 'x-decorator': 'FormItem', 'x-component': 'Input', }, }, }, }, }; const field = useField(); const [visible, setVisible] = useState(false); const { designable, schema, remove, refresh, insertAfter, insertBefore, appendChild, } = useDesignable(); const formConfig = schemas[schema['x-component']]; const isSubMenu = schema['x-component'] === 'Menu.SubMenu'; const ctx = useContext(MenuContext); const mode = useContext(MenuModeContext); return (
{ e.stopPropagation(); e.preventDefault(); }} className={'designable-bar-actions'} > { if (['update', 'move', 'delete'].includes(info.key)) { return; } const methodLabels = { insertBefore: '之前', insertAfter: '之后', appendChild: '里', }; const keys = info.key.split('.'); const method = keys.shift(); const type = keys.join('.'); const config = schemas[type]; if (!config) { return; } const values = await FormDialog( `在「${schema.title}」${methodLabels[method]}插入${config.title}`, () => { return ( ); }, ).open(); const methods = { insertAfter, insertBefore, appendChild, }; const defaults = generateDefaultSchema(type); const data = methods[method](deepmerge(defaults, values)); await createSchema(data); }} > { const initialValues = {}; Object.keys(formConfig.schema.properties).forEach( (name) => { _.set(initialValues, name, get(schema, name)); }, ); const values = await FormDialog(`编辑菜单项`, () => { return ( ); }).open({ initialValues, }); if (values.title) { schema.title = values.title; } const icon = _.get(values, 'x-component-props.icon') || null; schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props']['icon'] = icon; field.componentProps['icon'] = icon; refresh(); await updateSchema(schema); }} > 编辑菜单项 { let menuSchema: Schema; let parent = schema.parent; while (parent) { if (parent['x-component'] === 'Menu') { menuSchema = parent; break; } parent = parent.parent; } console.log({ menuSchema }); const toTreeData = (s: Schema) => { const items = []; Object.keys(s.properties || {}).forEach((name) => { const current = s.properties[name]; if ( !(current['x-component'] as string).startsWith( 'Menu.', ) ) { return; } // if (current.name === schema.name) { // return; // } items.push({ key: current['key'] || current.name, label: current.title, title: current.title, value: getSchemaPath(current).join('.'), children: toTreeData(current), }); }); return items; }; const dataSource = toTreeData(menuSchema); const values = await FormDialog( `将「${schema.title}」移动到`, () => { return ( ); }, ).open({ effects(form) { onFieldChange('path', (field) => { const target = findPropertyByPath( schema.root, // @ts-ignore field.value, ); console.log({ field }); field.query('method').take((f) => { // @ts-ignore // f.value = 'insertAfter'; // @ts-ignore f.dataSource = target['x-component'] === 'Menu.SubMenu' ? [ { label: '之后', value: 'insertAfter' }, { label: '之前', value: 'insertBefore' }, { label: '组里', value: 'appendChild' }, ] : [ { label: '之后', value: 'insertAfter' }, { label: '之前', value: 'insertBefore' }, ]; }); }); }, }); const methods = { insertAfter, insertBefore, appendChild, }; const data = schema.toJSON(); remove(); const source = methods[values.method](data, values.path); await updateSchema(source); }} > 移动到 } title={`向${mode == 'inline' ? '上' : '左'}插入`} > } > 分组 } > 页面 } > 链接 } title={`向${mode == 'inline' ? '下' : '右'}插入`} > } > 分组 } > 页面 } > 链接 {isSubMenu && ( } title={'向里插入'} > } > 分组 } > 页面 } > 链接 )} } onClick={async () => { const loadRoles = async () => { const resource = Resource.make('roles'); const data = await resource.list(); console.log('loadRoles', data); return data?.data.map((item) => ({ label: item.title, value: item.name, })); }; const resource = Resource.make({ associatedName: 'ui_schemas', associatedKey: schema['key'], resourceName: 'roles', }); const uiSchemasRoles = await resource.list(); console.log({ uiSchemasRoles }); const values = await FormDialog(`设置权限`, () => { return ( ); }).open({ initialValues: { roles: uiSchemasRoles?.data?.map((role) => role.name), }, }); await Resource.make({ resourceName: 'ui_schemas', resourceKey: schema['key'], }).save(values); }} > 设置权限 { Modal.confirm({ title: `删除${formConfig.title}`, content: '删除后无法恢复,确定要删除吗?', onOk: async () => { const target = remove(); await removeSchema(target); ctx.onRemove && ctx.onRemove(target); }, }); }} > 删除 } >
); }; export default Menu;