linyaostalker
5 years ago
6 changed files with 609 additions and 0 deletions
-
30backend/web/src/custom-menu/AddItem.js
-
124backend/web/src/custom-menu/Editor.js
-
176backend/web/src/custom-menu/MenuItem.js
-
247backend/web/src/custom-menu/index.js
-
7backend/web/src/custom-menu/index.scss
-
25backend/web/src/custom-menu/utils.js
@ -0,0 +1,30 @@ |
|||||
|
import React from 'react' |
||||
|
import {Icon} from "antd"; |
||||
|
import styled from "styled-components"; |
||||
|
|
||||
|
export default function AddItem({ subMenu = false, onClick }) { |
||||
|
return ( |
||||
|
<AddItemRoot className={subMenu ? 'sub-menu' : ''} onClick={onClick}> |
||||
|
<Icon type='plus' /> |
||||
|
</AddItemRoot> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
const AddItemRoot = styled.div`
|
||||
|
flex: 1; |
||||
|
display: inline-flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
width: 100%; |
||||
|
cursor: default; |
||||
|
&:active { |
||||
|
background: #eee; |
||||
|
} |
||||
|
&.sub-menu { |
||||
|
height: 40px; |
||||
|
border: none; |
||||
|
&:not(:last-child) { |
||||
|
border-bottom: 1px solid #eee; |
||||
|
} |
||||
|
} |
||||
|
`;
|
@ -0,0 +1,124 @@ |
|||||
|
import React, { createContext, useContext } from 'react' |
||||
|
import { Form, Input, Radio, Select } from "antd"; |
||||
|
import styled from "styled-components"; |
||||
|
import { produce } from "immer"; |
||||
|
|
||||
|
const FormItem = Form.Item; |
||||
|
const RadioGroup = Radio.Group; |
||||
|
const Option = Select.Option; |
||||
|
|
||||
|
const { customPageList } = window; |
||||
|
|
||||
|
const MenuContext = createContext(); |
||||
|
const MenuContextProvider = MenuContext.Provider; |
||||
|
|
||||
|
function InputItem({ label, name }) { |
||||
|
const { activeMenu, formItemLayout, onChange } = useContext(MenuContext); |
||||
|
const rawValue = activeMenu.content.value; |
||||
|
const value = name ? (rawValue ? rawValue[name] : '') : rawValue; |
||||
|
|
||||
|
function handleChange(e) { |
||||
|
const newValue = e.target.value; |
||||
|
onChange(name ? { |
||||
|
...rawValue, |
||||
|
[name]: newValue |
||||
|
} : newValue); |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<FormItem label={label} {...formItemLayout}> |
||||
|
<Input |
||||
|
value={value} |
||||
|
placeholder={'请输入' + label} |
||||
|
onChange={handleChange} |
||||
|
/> |
||||
|
</FormItem> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default function Editor({ activeMenu, onChange }) { |
||||
|
function changeType(e) { |
||||
|
onChange('content', produce(activeMenu.content, draft => { |
||||
|
draft.type = e.target.value; |
||||
|
draft.value = null; |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
function changeValue(newValue) { |
||||
|
onChange('content', { |
||||
|
type: activeMenu.content.type, |
||||
|
value: newValue |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const formItemLayout = { |
||||
|
labelCol: {span: 10}, |
||||
|
wrapperCol: {span: 14}, |
||||
|
}; |
||||
|
|
||||
|
const typeTable = { |
||||
|
view: { |
||||
|
label: '跳转网页', |
||||
|
content: <InputItem label='网页链接' /> |
||||
|
}, |
||||
|
customPage: { |
||||
|
label: '自定义页面', |
||||
|
content: ( |
||||
|
<FormItem label='自定义页面' {...formItemLayout}> |
||||
|
<Select defaultValue={activeMenu.content.value} onChange={changeValue}> |
||||
|
{customPageList.map(({id, title}) => <Option key={id} value={id.toString()}>{title}</Option>)} |
||||
|
</Select> |
||||
|
</FormItem> |
||||
|
) |
||||
|
}, |
||||
|
miniprogram: { |
||||
|
label: '跳转小程序', |
||||
|
content: ( |
||||
|
<> |
||||
|
<InputItem label='小程序 AppId' name='appId' /> |
||||
|
<InputItem label='小程序页面链接' name='url' /> |
||||
|
<InputItem label='备用网页链接' name='spareWebUrl' /> |
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<Root> |
||||
|
<Form> |
||||
|
<FormItem label='菜单名称' {...formItemLayout}> |
||||
|
<Input |
||||
|
value={activeMenu.title} |
||||
|
onInput={e => onChange('title', e.target.value)} |
||||
|
/> |
||||
|
</FormItem> |
||||
|
{(!activeMenu.children || activeMenu.children.length === 0) && ( |
||||
|
<ContentValue> |
||||
|
<FormItem label='菜单内容' {...formItemLayout}> |
||||
|
<RadioGroup value={activeMenu.content.type} onChange={changeType}> |
||||
|
{Object.keys(typeTable).map(name => ( |
||||
|
<Radio key={name} value={name}>{typeTable[name].label}</Radio> |
||||
|
))} |
||||
|
</RadioGroup> |
||||
|
</FormItem> |
||||
|
{activeMenu.content.type && ( |
||||
|
<MenuContextProvider value={{ activeMenu, onChange: changeValue, formItemLayout }}> |
||||
|
{typeTable[activeMenu.content.type].content} |
||||
|
</MenuContextProvider> |
||||
|
)} |
||||
|
</ContentValue> |
||||
|
)} |
||||
|
</Form> |
||||
|
</Root> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
const Root = styled.div`
|
||||
|
margin-left: 20px; |
||||
|
padding: 20px; |
||||
|
background: #fff; |
||||
|
`;
|
||||
|
|
||||
|
const ContentValue = styled.div`
|
||||
|
|
||||
|
`;
|
@ -0,0 +1,176 @@ |
|||||
|
import React, { useState } from 'react' |
||||
|
import { Button, Popover } from "antd"; |
||||
|
import styled from "styled-components"; |
||||
|
import ClassNames from 'classnames' |
||||
|
import AddItem from './AddItem' |
||||
|
import { DragDropContext, Draggable, Droppable} from "react-beautiful-dnd"; |
||||
|
|
||||
|
function BaseMenuItem({ title, active, onClick, onDelete, children, provided, snapshot }) { |
||||
|
const providedProps = provided ? { |
||||
|
ref: provided.innerRef, |
||||
|
...provided.draggableProps, |
||||
|
...provided.dragHandleProps |
||||
|
} : {}; |
||||
|
|
||||
|
return ( |
||||
|
<MenuItemRoot |
||||
|
{...providedProps} |
||||
|
className={ClassNames({ |
||||
|
active, |
||||
|
dragging: snapshot.isDragging |
||||
|
})} |
||||
|
onClick={onClick} |
||||
|
> |
||||
|
<MenuItemWrapper> |
||||
|
<MenuItemText>{title}</MenuItemText> |
||||
|
<DeleteButton |
||||
|
type='danger' |
||||
|
shape='circle' |
||||
|
size='small' |
||||
|
icon='close' |
||||
|
onClick={e => { |
||||
|
e.stopPropagation(); |
||||
|
onDelete(e); |
||||
|
}} |
||||
|
/> |
||||
|
{children} |
||||
|
</MenuItemWrapper> |
||||
|
</MenuItemRoot> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default function MenuItem( |
||||
|
{ |
||||
|
title, children, active, activeSubMenuId, showSubMenu, provided, snapshot, onActivate, |
||||
|
onAddSubMenu, onDeleteSubMenu, onReorderSubMenu, onDelete |
||||
|
}) { |
||||
|
const content = ( |
||||
|
<> |
||||
|
<DragDropContext |
||||
|
onDragStart={({ draggableId }) => onActivate(draggableId)} |
||||
|
onDragEnd={result => onReorderSubMenu(result)} |
||||
|
> |
||||
|
<Droppable droppableId="droppable"> |
||||
|
{provided => ( |
||||
|
<div |
||||
|
{...provided.droppableProps} |
||||
|
ref={provided.innerRef} |
||||
|
> |
||||
|
{children && children.map(({id, title}, subIndex) => ( |
||||
|
<Draggable key={id} draggableId={id} index={subIndex}> |
||||
|
{(provided, snapshot) => ( |
||||
|
<SubMenuItem |
||||
|
ref={provided.innerRef} |
||||
|
{...provided.draggableProps} |
||||
|
{...provided.dragHandleProps} |
||||
|
> |
||||
|
<BaseMenuItem |
||||
|
title={title} |
||||
|
active={activeSubMenuId === id} |
||||
|
snapshot={snapshot} |
||||
|
onClick={() => onActivate(id)} |
||||
|
onDelete={() => onDeleteSubMenu(subIndex)} |
||||
|
/> |
||||
|
</SubMenuItem> |
||||
|
)} |
||||
|
</Draggable> |
||||
|
))} |
||||
|
{provided.placeholder} |
||||
|
</div> |
||||
|
)} |
||||
|
</Droppable> |
||||
|
</DragDropContext> |
||||
|
{(!children || children.length < 5) && <AddItem subMenu onClick={onAddSubMenu}/>} |
||||
|
</> |
||||
|
); |
||||
|
|
||||
|
return ( |
||||
|
<Popover |
||||
|
trigger='click' |
||||
|
content={content} |
||||
|
overlayStyle={{width: '100px', padding: '0'}} |
||||
|
visible={showSubMenu} |
||||
|
> |
||||
|
<BaseMenuItem |
||||
|
provided={provided} |
||||
|
snapshot={snapshot} |
||||
|
title={title} |
||||
|
active={active} |
||||
|
onClick={() => onActivate()} |
||||
|
onDelete={onDelete} |
||||
|
> |
||||
|
{(children && children.length > 0) && <SpreadSymbol/>} |
||||
|
</BaseMenuItem> |
||||
|
</Popover> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
const DeleteButton = styled(Button)`
|
||||
|
position: absolute; |
||||
|
right: 0; |
||||
|
top: 0; |
||||
|
transform: translate(50%, -50%); |
||||
|
box-shadow: 0 0 10px 0 rgba(0 0 0 .1); |
||||
|
visibility: hidden; |
||||
|
z-index: 2000; |
||||
|
`;
|
||||
|
|
||||
|
const fontSizeRatio = 0.03; |
||||
|
const MenuItemText = styled.div`
|
||||
|
padding: 0 5px; |
||||
|
white-space: nowrap; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
font-size: ${70 * fontSizeRatio}vh; |
||||
|
`;
|
||||
|
|
||||
|
const SubMenuItem = styled.div`
|
||||
|
display: flex; |
||||
|
width: 100%; |
||||
|
height: 40px; |
||||
|
border-bottom: 1px solid #eee; |
||||
|
`;
|
||||
|
|
||||
|
const MenuItemWrapper = styled.div`
|
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
`;
|
||||
|
|
||||
|
const triangleSize = 8; |
||||
|
const spreadOffset = 3; |
||||
|
const SpreadSymbol = styled.div`
|
||||
|
position: absolute; |
||||
|
right: ${spreadOffset}px; |
||||
|
bottom: ${spreadOffset}px; |
||||
|
width: 0; |
||||
|
height: 0; |
||||
|
border-top: ${triangleSize}px solid transparent; |
||||
|
border-right: ${triangleSize}px solid #ddd; |
||||
|
`;
|
||||
|
|
||||
|
const MenuItemRoot = styled.div`
|
||||
|
align-items: stretch; |
||||
|
position: relative; |
||||
|
flex: 1; |
||||
|
padding: 5px 0; |
||||
|
cursor: default; |
||||
|
background: #fff; |
||||
|
border: 2px solid transparent; |
||||
|
width: 0; |
||||
|
transition: box-shadow .1s; |
||||
|
&.active { |
||||
|
border-color: #09bb07; |
||||
|
} |
||||
|
&.dragging { |
||||
|
box-shadow: 0 0 10px 0 rgba(0, 0, 0, .1); |
||||
|
} |
||||
|
&:not(:last-child) ${MenuItemWrapper} { |
||||
|
border-right: 1px solid #eee; |
||||
|
} |
||||
|
&:hover ${DeleteButton} { |
||||
|
visibility: visible; |
||||
|
} |
||||
|
`;
|
@ -0,0 +1,247 @@ |
|||||
|
import React, { useState } from 'react' |
||||
|
import ReactDOM from 'react-dom' |
||||
|
import styled from 'styled-components' |
||||
|
import { Button, Icon, message } from 'antd' |
||||
|
import { produce } from 'immer' |
||||
|
import axios from 'axios' |
||||
|
import { DragDropContext, Draggable, Droppable} from "react-beautiful-dnd"; |
||||
|
import './index.scss' |
||||
|
import Editor from './Editor' |
||||
|
import AddItem from './AddItem' |
||||
|
import MenuItem from './MenuItem' |
||||
|
import '../utils/ajax' |
||||
|
import { handleDragEnd } from './utils' |
||||
|
import uuid from 'uuid' |
||||
|
|
||||
|
const IconFont = Icon.createFromIconfontCN({ |
||||
|
scriptUrl: '//at.alicdn.com/t/font_827976_5ojdd8bmdsw.js', |
||||
|
}); |
||||
|
|
||||
|
const { data: initMenuList = [] } = window; |
||||
|
|
||||
|
function getActiveMenu(menuList, activeMenuInfo) { |
||||
|
const activeParent = menuList.find(menu => menu.id === activeMenuInfo.id); |
||||
|
return (activeParent && activeMenuInfo.subId !== null && activeParent.children.length > 0) ? |
||||
|
activeParent.children.find(child => child.id === activeMenuInfo.subId) : activeParent; |
||||
|
} |
||||
|
|
||||
|
function App() { |
||||
|
const [menuList, setMenuList] = useState(initMenuList); |
||||
|
const [activeMenuInfo, setActiveMenuInfo] = useState({ id: null, subId: null }); |
||||
|
|
||||
|
const activeMenu = getActiveMenu(menuList, activeMenuInfo); |
||||
|
|
||||
|
function addMenu() { |
||||
|
const id = uuid(); |
||||
|
|
||||
|
setMenuList([ |
||||
|
...menuList, |
||||
|
{ |
||||
|
id, |
||||
|
title: '菜单名称', |
||||
|
content: { |
||||
|
type: null, |
||||
|
value: null |
||||
|
}, |
||||
|
children: [] |
||||
|
} |
||||
|
]); |
||||
|
|
||||
|
setActiveMenuInfo({ |
||||
|
id, |
||||
|
subId: null |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function deleteMenu(index) { |
||||
|
setMenuList(produce(menuList, (draft => { |
||||
|
draft.splice(index, 1); |
||||
|
}))); |
||||
|
} |
||||
|
|
||||
|
function deleteSubMenu(index, subIndex) { |
||||
|
setMenuList(produce(menuList, (draft => { |
||||
|
draft[index].children.splice(subIndex, 1); |
||||
|
}))); |
||||
|
} |
||||
|
|
||||
|
function addSubMenu(index) { |
||||
|
const id = uuid(); |
||||
|
|
||||
|
setMenuList(produce(menuList, (draft => { |
||||
|
draft[index].children.push({ |
||||
|
id, |
||||
|
title: '菜单名称', |
||||
|
content: { |
||||
|
type: null, |
||||
|
value: null |
||||
|
} |
||||
|
}) |
||||
|
}))); |
||||
|
setActiveMenuInfo({ |
||||
|
id: menuList[index].id, |
||||
|
subId: id |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function changeForm(name, value) { |
||||
|
setMenuList(produce(menuList, (draft => { |
||||
|
const activeMenu = getActiveMenu(draft, activeMenuInfo); |
||||
|
activeMenu[name] = value; |
||||
|
}))); |
||||
|
} |
||||
|
|
||||
|
function checkData() { |
||||
|
return menuList.every(menu => { |
||||
|
if (menu.children && menu.children.length > 0) { |
||||
|
return menu.title && menu.children.every(item => { |
||||
|
if (item.content.type === 'weapp') { |
||||
|
const { appId, url, spareWebUrl } = item.content.value; |
||||
|
return item.title && appId && url && spareWebUrl |
||||
|
} else { |
||||
|
return item.title && item.content.type && item.content.value |
||||
|
} |
||||
|
}) |
||||
|
} else { |
||||
|
return menu.title && menu.content.type && menu.content.value |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
async function submit() { |
||||
|
if (checkData()) { |
||||
|
await axios.post('', { |
||||
|
data: menuList |
||||
|
}); |
||||
|
message.success('保存成功'); |
||||
|
} else { |
||||
|
message.error('数据填写不完整,请检查是否有数据漏填') |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function reorderSubMenu(index, result) { |
||||
|
handleDragEnd(menuList[index].children, result, newSubMenu => { |
||||
|
setMenuList(produce(menuList, draft => { |
||||
|
draft[index].children = newSubMenu; |
||||
|
})) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<Root> |
||||
|
<Previewer> |
||||
|
<TopBar src='/img/wechat-top.png' /> |
||||
|
<BottomBar> |
||||
|
<KeyBoardIcon> |
||||
|
<IconFont |
||||
|
type='fa-keyboard' |
||||
|
style={{ |
||||
|
fontSize: '1.3em', |
||||
|
padding: '0 10px', |
||||
|
alignSelf: 'center', |
||||
|
borderRight: '1px solid #eee' |
||||
|
}} |
||||
|
/> |
||||
|
</KeyBoardIcon> |
||||
|
<MenuWrapper> |
||||
|
<DragDropContext |
||||
|
onDragStart={({ draggableId }) => setActiveMenuInfo({ id: draggableId, subId: null })} |
||||
|
onDragEnd={result => handleDragEnd(menuList, result, setMenuList)} |
||||
|
> |
||||
|
<Droppable droppableId='droppable' direction='horizontal'> |
||||
|
{provided => ( |
||||
|
<MenuGroup |
||||
|
{...provided.droppableProps} |
||||
|
ref={provided.innerRef} |
||||
|
length={menuList.length} |
||||
|
> |
||||
|
{menuList.map((menu, index) => ( |
||||
|
<Draggable key={menu.id} draggableId={menu.id} index={index}> |
||||
|
{(provided, snapshot) => ( |
||||
|
<MenuItem |
||||
|
key={menu.id} |
||||
|
{...menu} |
||||
|
provided={provided} |
||||
|
snapshot={snapshot} |
||||
|
showSubMenu={activeMenuInfo.id === menu.id} |
||||
|
active={activeMenuInfo.id === menu.id && activeMenuInfo.subId === null} |
||||
|
activeSubMenuId={activeMenuInfo.id === menu.id && activeMenuInfo.subId} |
||||
|
onAddSubMenu={() => addSubMenu(index)} |
||||
|
onActivate={(subId = null) => setActiveMenuInfo({ id: menu.id, subId })} |
||||
|
onDelete={() => deleteMenu(index)} |
||||
|
onDeleteSubMenu={(subIndex) => deleteSubMenu(index, subIndex)} |
||||
|
onReorderSubMenu={result => reorderSubMenu(index, result)} |
||||
|
/> |
||||
|
)} |
||||
|
</Draggable> |
||||
|
))} |
||||
|
{provided.placeholder} |
||||
|
</MenuGroup> |
||||
|
)} |
||||
|
</Droppable> |
||||
|
</DragDropContext> |
||||
|
{menuList.length < 3 && <AddItem onClick={addMenu} />} |
||||
|
</MenuWrapper> |
||||
|
</BottomBar> |
||||
|
</Previewer> |
||||
|
{activeMenu && <Editor activeMenu={activeMenu} onChange={changeForm} />} |
||||
|
<SubmitButton type='primary' shape='circle' icon='check' size='large' onClick={submit} /> |
||||
|
</Root> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
const Root = styled.div`
|
||||
|
display: flex; |
||||
|
`;
|
||||
|
|
||||
|
const TopBar = styled.img`
|
||||
|
width: 100%; |
||||
|
height: auto; |
||||
|
`;
|
||||
|
|
||||
|
const BottomBar = styled.div`
|
||||
|
display: flex; |
||||
|
width: 100%; |
||||
|
height: 35px; |
||||
|
background: #fff; |
||||
|
`;
|
||||
|
|
||||
|
const KeyBoardIcon = styled.div`
|
||||
|
padding: 10px 0; |
||||
|
`;
|
||||
|
|
||||
|
const height = 70; |
||||
|
const ratio = 16 / 9; |
||||
|
|
||||
|
const Previewer = styled.div`
|
||||
|
flex-shrink: 0; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: space-between; |
||||
|
width: ${height / ratio}vh; |
||||
|
height: ${height}vh; |
||||
|
background: #eee; |
||||
|
border: 1px solid #eee; |
||||
|
box-shadow: 0 0 20px 0 rgba(0, 0, 0, .1); |
||||
|
`;
|
||||
|
|
||||
|
const SubmitButton = styled(Button)`
|
||||
|
position: fixed; |
||||
|
width: 60px!important; |
||||
|
height: 60px!important; |
||||
|
font-size: 1.8em!important; |
||||
|
bottom: 40px; |
||||
|
right: 40px; |
||||
|
`;
|
||||
|
|
||||
|
const MenuWrapper = styled.div`
|
||||
|
flex-grow: 1; |
||||
|
display: flex; |
||||
|
width: 0; |
||||
|
`;
|
||||
|
|
||||
|
const MenuGroup = styled(MenuWrapper)`
|
||||
|
flex-grow: ${({ length }) => length}; |
||||
|
`;
|
||||
|
|
||||
|
ReactDOM.render(<App />, document.getElementById('app')); |
@ -0,0 +1,7 @@ |
|||||
|
.ant-popover-inner-content { |
||||
|
padding: 0!important; |
||||
|
} |
||||
|
|
||||
|
.ant-popover-arrow { |
||||
|
display: none!important; |
||||
|
} |
@ -0,0 +1,25 @@ |
|||||
|
export const reorder = (list, startIndex, endIndex) => { |
||||
|
const result = Array.from(list); |
||||
|
const [removed] = result.splice(startIndex, 1); |
||||
|
result.splice(endIndex, 0, removed); |
||||
|
|
||||
|
return result; |
||||
|
}; |
||||
|
|
||||
|
export function handleDragEnd(list, result, setList) { |
||||
|
if (!result.destination) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (result.destination.index === result.source.index) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const newList = reorder( |
||||
|
list, |
||||
|
result.source.index, |
||||
|
result.destination.index |
||||
|
); |
||||
|
|
||||
|
setList(newList); |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue