diff --git a/backend/web/src/custom-menu/AddItem.js b/backend/web/src/custom-menu/AddItem.js new file mode 100644 index 0000000..ecbc659 --- /dev/null +++ b/backend/web/src/custom-menu/AddItem.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 ( + + + + ) +} + +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; + } + } +`; \ No newline at end of file diff --git a/backend/web/src/custom-menu/Editor.js b/backend/web/src/custom-menu/Editor.js new file mode 100644 index 0000000..f7c56c0 --- /dev/null +++ b/backend/web/src/custom-menu/Editor.js @@ -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 ( + + + + ) +} + +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: + }, + customPage: { + label: '自定义页面', + content: ( + + + + ) + }, + miniprogram: { + label: '跳转小程序', + content: ( + <> + + + + + ) + } + }; + + return ( + +
+ + onChange('title', e.target.value)} + /> + + {(!activeMenu.children || activeMenu.children.length === 0) && ( + + + + {Object.keys(typeTable).map(name => ( + {typeTable[name].label} + ))} + + + {activeMenu.content.type && ( + + {typeTable[activeMenu.content.type].content} + + )} + + )} +
+
+ ) +} + +const Root = styled.div` + margin-left: 20px; + padding: 20px; + background: #fff; +`; + +const ContentValue = styled.div` + +`; \ No newline at end of file diff --git a/backend/web/src/custom-menu/MenuItem.js b/backend/web/src/custom-menu/MenuItem.js new file mode 100644 index 0000000..eaaad1e --- /dev/null +++ b/backend/web/src/custom-menu/MenuItem.js @@ -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 ( + + + {title} + { + e.stopPropagation(); + onDelete(e); + }} + /> + {children} + + + ) +} + +export default function MenuItem( + { + title, children, active, activeSubMenuId, showSubMenu, provided, snapshot, onActivate, + onAddSubMenu, onDeleteSubMenu, onReorderSubMenu, onDelete + }) { + const content = ( + <> + onActivate(draggableId)} + onDragEnd={result => onReorderSubMenu(result)} + > + + {provided => ( +
+ {children && children.map(({id, title}, subIndex) => ( + + {(provided, snapshot) => ( + + onActivate(id)} + onDelete={() => onDeleteSubMenu(subIndex)} + /> + + )} + + ))} + {provided.placeholder} +
+ )} +
+
+ {(!children || children.length < 5) && } + + ); + + return ( + + onActivate()} + onDelete={onDelete} + > + {(children && children.length > 0) && } + + + ) +} + +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; + } +`; \ No newline at end of file diff --git a/backend/web/src/custom-menu/index.js b/backend/web/src/custom-menu/index.js new file mode 100644 index 0000000..a969cdd --- /dev/null +++ b/backend/web/src/custom-menu/index.js @@ -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 ( + + + + + + + + + setActiveMenuInfo({ id: draggableId, subId: null })} + onDragEnd={result => handleDragEnd(menuList, result, setMenuList)} + > + + {provided => ( + + {menuList.map((menu, index) => ( + + {(provided, snapshot) => ( + addSubMenu(index)} + onActivate={(subId = null) => setActiveMenuInfo({ id: menu.id, subId })} + onDelete={() => deleteMenu(index)} + onDeleteSubMenu={(subIndex) => deleteSubMenu(index, subIndex)} + onReorderSubMenu={result => reorderSubMenu(index, result)} + /> + )} + + ))} + {provided.placeholder} + + )} + + + {menuList.length < 3 && } + + + + {activeMenu && } + + + ) +} + +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(, document.getElementById('app')); \ No newline at end of file diff --git a/backend/web/src/custom-menu/index.scss b/backend/web/src/custom-menu/index.scss new file mode 100644 index 0000000..87e9898 --- /dev/null +++ b/backend/web/src/custom-menu/index.scss @@ -0,0 +1,7 @@ +.ant-popover-inner-content { + padding: 0!important; +} + +.ant-popover-arrow { + display: none!important; +} \ No newline at end of file diff --git a/backend/web/src/custom-menu/utils.js b/backend/web/src/custom-menu/utils.js new file mode 100644 index 0000000..115db70 --- /dev/null +++ b/backend/web/src/custom-menu/utils.js @@ -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); +} \ No newline at end of file