Commit 8199b9fc authored by 崔佳豪's avatar 崔佳豪

perf: 常用菜单优化

parent e8b58e1d
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -98,6 +98,7 @@
"@ant-design/pro-utils": "^1.10.4",
"@babel/polyfill": "7.4.3",
"@babel/runtime": "^7.10.5",
"@wisdom-components/empty": "^1.4.1",
"@wisdom-utils/components": "0.0.14",
"@wisdom-utils/runtime": "0.0.15",
"@wisdom-utils/utils": "0.0.52",
......
/* eslint-disable no-useless-constructor */
import React, { useState, useEffect, useRef } from 'react';
import { Input, Button, Tree, notification, Spin, message } from 'antd';
import React, { useState, useEffect } from 'react';
import { Input, Button, notification, Spin, message, Modal } from 'antd';
import { CheckOutlined, PlusOutlined } from '@ant-design/icons';
import { connect } from 'react-redux';
import classnames from 'classnames';
import * as _ from 'lodash';
import PinyinMatch from 'pinyin-match';
import PandaEmpty from '@wisdom-components/empty';
import { appService } from '@/api';
import { useHistory } from '@wisdom-utils/runtime';
import { savePagePartInfo } from '@/api/service/base';
import styles from './index.less';
import thumbnail from '../../assets/images/commonMenu/常用菜单.png';
import pageLogo from '../../assets/images/commonMenu/page-logo.png';
import starIcon from '../../assets/images/commonMenu/矢量智能对象 拷贝 3@2x.png';
// 是否是灰色的图标(灰色图标在白色背景中看不见,添加滤镜变色)
const isNeedFilterIcon = (icon = '') => {
return !icon.includes('一级') && !icon.includes('ios/');
};
const isNeedFilterIcon = (icon = '') =>
icon && !icon.includes('一级') && !icon.includes('ios/');
const CommonMenu = props => {
const history = useHistory();
const { menus } = props;
const [commonMenus, setCommonMenus] = useState([]); // 常用菜单信息
const [commonMenus, setCommonMenus] = useState([]); // 收藏菜单信息
const [menuList, setMenuList] = useState([]);
const [showMenuInfo, setShowMenuInfo] = useState(false); // 菜单列表显示隐藏
const [loading, setLoading] = useState(true); // loading显示隐藏
const [searchInfo, setSearchInfo] = useState('');
const page = useRef(null);
const searchBox = useRef(null);
const defaultSubmitMenu = () => {
let submitData = [];
submitData = commonMenus.map(item => ({
PartID: item.menuID,
PartName: item.menuName,
PartUrl: item.menuUrl,
icon: item.menuIcon,
}));
return submitData;
};
/**
* 添加、删除菜单
* @param {string} opr add/delete,添加/删除
* @param {object} extData { PartID, label, icon, url, product },操作的菜单信息widget
*/
const updateCommonMenu = (opr, node) => {
const { href, extData } = node;
const { PartID, label, icon, url, product } = extData;
setLoading(true);
let submitData = defaultSubmitMenu();
if (opr === 'add') {
submitData.push({
PartID: `${PartID}`,
PartName: label,
PartUrl: href,
icon,
});
} else if (opr === 'delete') {
submitData = submitData.filter(item => item.PartID !== `${PartID}`);
}
savePagePartInfo({
query: {
UserID: window.globalConfig?.userInfo?.OID ?? '',
},
data: submitData,
}).then(res => {
if (res.statusCode === '0000') {
message.success('修改常用菜单成功!');
fetchMenus();
} else {
message.error('修改常用菜单失败!');
}
});
};
const [loading, setLoading] = useState(true); // loading显示隐藏
const [isShowMenuModal, setIsShowMenuModal] = useState(false);
/**
* 渲染自定义树节点
* @param {object} node 树节点信息,至少包含key, title, extData, children
* @returns {ReactDOM} 返回自定义节点DOM
* 获取收藏的菜单信息
*/
const renderTitle = node => {
const { key, title, extData = {}, children } = node;
const { icon, url, isAdded } = extData;
return (
<div className={styles.customTitle}>
<div
className={classnames(
styles.titleInfo,
isNeedFilterIcon(icon) ? styles.filterIconBox : '',
)}
>
<span className={styles.iconBox}>
<img src={icon} alt="" />
</span>
<span>{title}</span>
</div>
{url && (
<div className={styles.titleControl}>
{isAdded ? (
<div>
<p className={styles.chooseLabel}>已添加</p>
<div
className={styles.chooseBtn}
onClick={e => {
e.stopPropagation();
updateCommonMenu('delete', node);
}}
>
取消添加
</div>
</div>
) : (
<div>
<div
className={styles.chooseBtn}
onClick={e => {
e.stopPropagation();
updateCommonMenu('add', node);
}}
>
添加
</div>
</div>
)}
</div>
)}
</div>
);
};
const fetchMenus = () => {
appService
.getPagePartInfo({
......@@ -142,34 +46,28 @@ const CommonMenu = props => {
description: res.say.errMsg,
});
const newMenus = [];
// 过滤出当前client的菜单
// 过滤出当前client的菜单(widget_client_xxx)
const data = res.getMe.filter(item => {
const client = item.PartID.split('_')[1] ?? '';
return client === window.globalConfig.client;
});
data.forEach(item => {
const newMenu = {
menuIcon: '',
menuName: '',
menuGroup: '',
menuID: '',
menuUrl: '',
menuPic: '',
icon: item.icon,
name: item.PartName,
path: item.PartUrl,
pic: item.BgPicUrl,
id: item.PartID,
};
newMenu.menuIcon = item.icon;
newMenu.menuName = item.PartName;
// 所属产品
const [product] = item.PartUrl.split('/').filter(d => !!d);
newMenu.product = product;
// 所属一级分组
const PartIDArr = item.PartID.split('_');
newMenu.menuGroup = PartIDArr.splice(2, PartIDArr.length - 3).join(
'_',
);
newMenu.menuID = item.PartID;
newMenu.menuUrl = item.PartUrl;
newMenu.menuPic = item.BgPicUrl;
newMenu.topGroup = PartIDArr.splice(2, 1).join('_');
newMenus.push(newMenu);
});
setCommonMenus(newMenus);
// const w = getWidgets(newMenus);
// setWidgets(w);
})
.catch(error => {
notification.error({
......@@ -185,13 +83,12 @@ const CommonMenu = props => {
useEffect(() => {
const isAddedMenu = menu => {
const menuTmp = commonMenus.find(
item => item.menuID === menu.extData.PartID,
);
const menuTmp = commonMenus.find(item => item.id === menu.extData.PartID);
return !!menuTmp;
};
const loop = (data, prePartID) => {
return data.map(item => {
const newData = [];
data.forEach(item => {
const { name, level, href, key, path, routes } = item;
const newmenu = { ...item };
newmenu.key = href ? key : path;
......@@ -221,58 +118,94 @@ const CommonMenu = props => {
if (routes) {
newmenu.children = loop(routes, `${prePartID}_${name}`);
}
return newmenu;
if (routes && newmenu.children && newmenu.children.length) {
newData.push(newmenu);
} else if (!routes && index > -1) {
newData.push(newmenu);
}
});
return newData;
};
const newmenus = loop(menus, `widget_${window.globalConfig.client}`);
setMenuList(newmenus);
}, [menus, searchInfo, commonMenus]);
const loop = data => {
return data.map(item => {
const index = item.name.indexOf(searchInfo);
const beforeStr = item.name.substr(0, index);
const afterStr = item.name.substr(index + searchInfo.length);
/**
* 菜单跳转
* @param {*} menu
*/
const linkToMenu = menu => {
const { path } = menu;
history.push(path);
};
const mdOk = submitData => {
setLoading(true);
savePagePartInfo({
query: {
UserID: window.globalConfig?.userInfo?.OID ?? '',
},
data: submitData,
})
.then(res => {
setLoading(false);
if (res.statusCode === '0000') {
message.success('修改常用菜单成功!');
setIsShowMenuModal(false);
fetchMenus();
} else {
message.error('修改常用菜单失败!');
}
})
.catch(err => {
setLoading(false);
});
};
const mdCancel = () => {
setIsShowMenuModal(false);
};
const rednerMenuCardData = () => {
const menuCardData = [];
commonMenus &&
commonMenus.forEach(item => {
const { name } = item;
const m = PinyinMatch.match(name, searchInfo);
if (!m) return;
const [before, after] = m;
const beforeStr = name.slice(0, before);
const centerStr = name.slice(before, after + 1);
const afterStr = name.slice(after + 1);
const title =
index > -1 ? (
after > -1 ? (
<span>
{beforeStr}
<span className={styles.treeSearchInfo}>{searchInfo}</span>
<em style={{ fontStyle: 'normal', color: 'orange' }}>
{centerStr}
</em>
{afterStr}
</span>
) : (
<span>{item.name}</span>
<span>{name}</span>
);
menuCardData.push(
<MenuCard
menu={{ ...item, title }}
linkToMenu={linkToMenu}
key={item.id}
/>,
);
if (item.children) {
return { ...item, title, children: loop(item.children) };
}
return { ...item, title };
});
};
const linkToMenu = menu => {
const { menuUrl } = menu;
history.push(menuUrl);
};
const focusOutSearchInfo = e => {
e.stopPropagation();
let element = e.target;
// 事件对象逐层网上找,只要不是菜单搜索信息内的就隐藏起来
while (element && element !== page.current) {
if (element === searchBox.current) return;
element = element.parentNode;
}
setShowMenuInfo(false);
return menuCardData.length > 0 ? menuCardData : <PandaEmpty />;
};
return (
<div className={styles.commonMenu} ref={page} onClick={focusOutSearchInfo}>
<div className={styles.commonMenu}>
<Spin spinning={loading}>
<div className={styles.searchWrapper}>
<div className={styles.searchBox} ref={searchBox}>
<div className={styles.searchBox}>
<div className={styles.searchTitle}>
{/* <i className="iconfont">&#xe611;</i> */}
<img
style={{ width: '20px', marginRight: '0.5em' }}
src={starIcon}
......@@ -280,44 +213,42 @@ const CommonMenu = props => {
/>
<span>我的常用菜单</span>
<span>{commonMenus.length}</span>
<PlusOutlined
className={styles.addIcon}
title="添加常用菜单"
onClick={() => {
setIsShowMenuModal(true);
}}
/>
</div>
<div className={styles.searchInput}>
<Input
<Input.Search
maxLength={50}
width={400}
placeholder="搜索功能菜单"
onChange={e => {
setShowMenuInfo(true);
setSearchInfo(e.target.value);
}}
onFocus={() => setShowMenuInfo(true)}
/>
</div>
<div
className={classnames(
styles.searchInfoBox,
showMenuInfo ? '' : styles.searchInfoBox_hide,
)}
>
<Tree
height={400}
showIcon
defaultExpandAll
defaultSelectedKeys={['0-0-0']}
// switcherIcon={<DownOutlined />}
treeData={menuList}
titleRender={renderTitle}
selectable={false}
/>
</div>
</div>
{/* <Button className={styles.searchBtn}>添加菜单</Button> */}
</div>
<div className={styles.menuCardWrapper}>
{commonMenus.map(item => (
<MenuCard menu={item} linkToMenu={linkToMenu} key={item.menuID} />
))}
{/* <Button
className={styles.searchBtn}
onClick={() => {
setIsShowMenuModal(true);
}}
>
添加菜单
</Button> */}
</div>
<MenuAddModal
isShowMenuModal={isShowMenuModal}
setIsShowMenuModal={setIsShowMenuModal}
menuList={menuList}
mdOk={mdOk}
mdCancel={mdCancel}
/>
<div className={styles.menuCardWrapper}>{rednerMenuCardData()}</div>
<div className={styles.pageLogo}>
<img src={pageLogo} alt="" />
</div>
......@@ -326,34 +257,226 @@ const CommonMenu = props => {
);
};
// 搜藏菜单卡片展示
const MenuCard = ({ menu, linkToMenu }) => {
const { menuIcon, menuName, menuGroup, menuID, menuUrl, menuPic } = menu;
const { icon, name, title, topGroup, menuID, path, pic } = menu;
return (
<div className={styles.menuCard} onClick={() => linkToMenu(menu)}>
{/* <Link to={menuUrl} title={menuName}> */}
<img className={styles.cardThumbnail} src={menuPic || thumbnail} alt="" />
<img className={styles.cardThumbnail} src={pic || thumbnail} alt="" />
<div className={styles.cardLabel}>
<div
className={classnames(
styles.cardTitle,
isNeedFilterIcon(menuIcon) ? styles.filterIconBox : '',
isNeedFilterIcon(icon) ? styles.filterIconBox : '',
)}
>
<span className={styles.iconBox}>
<img className={styles.cardIcon} src={menuIcon} alt="" />
<img className={styles.cardIcon} src={icon} alt="" />
</span>
<span className={styles.cardName}>{menuName}</span>
<span className={styles.cardName}>{title || name}</span>
</div>
<div className={styles.cardGroup}>{menuGroup}</div>
<div className={styles.cardGroup}>{topGroup}</div>
</div>
{/* </Link> */}
</div>
);
};
const ModalTitle = () => <p className={styles.modalTitle}>添加常用菜单</p>;
const MenuAddModal = props => {
const { isShowMenuModal, menuList, mdOk, mdCancel } = props;
const [tempMenuList, setTempMenuList] = useState([]); // 模态框菜单列表数据
const [submitData, setSubmitData] = useState([]);
const [searchInfo, setSearchInfo] = useState('');
useEffect(() => {
const newSubmitData = [];
const loop = data => {
data &&
data.forEach(item => {
if (item.href && item.extData.isAdded) {
newSubmitData.push({
PartID: item.extData.PartID,
PartName: item.name,
PartUrl: item.path,
icon: item.extData.icon,
});
}
item.children && loop(item.children);
});
};
loop(menuList);
setSubmitData(newSubmitData);
}, [menuList, isShowMenuModal]);
useEffect(() => {
let newTempMenuList = menuList.map(item => {
const childMenus = [];
const deep = (data = []) => {
data.forEach(d => {
if (d.href) {
const m = PinyinMatch.match(d.name, searchInfo);
const tempIsAdded = !!submitData.find(
sd => sd.PartID === d.extData.PartID,
);
if (m)
childMenus.push({
...d,
extData: { ...d.extData, isAdded: tempIsAdded },
match: m,
});
} else {
deep(d.children);
}
});
};
if (item.href) {
childMenus.push({ ...item });
} else {
deep(item.children);
}
return { ...item, childMenus };
});
newTempMenuList = newTempMenuList.filter(
item => item.childMenus && item.childMenus.length > 0,
);
setTempMenuList(newTempMenuList);
}, [menuList, isShowMenuModal, searchInfo]);
const toggleMenu = menu => {
const { key } = menu;
const newTempMenuList = _.cloneDeep(tempMenuList);
let newSubmitData = [];
let flag = false;
for (let i = 0; i < newTempMenuList.length; i += 1) {
const { childMenus } = newTempMenuList[i];
for (let j = 0; j < childMenus.length; j += 1) {
if (childMenus[j].key === key) {
flag = true;
const {
name,
href,
extData: { PartID, icon, isAdded },
} = childMenus[j];
childMenus[j].extData.isAdded = !isAdded;
if (submitData.find(item => item.PartID === PartID)) {
newSubmitData = submitData.filter(item => item.PartID !== PartID);
} else {
newSubmitData = [
...submitData,
{
PartID,
PartName: name,
PartUrl: href,
icon,
},
];
}
break;
}
}
if (flag) break;
}
setTempMenuList(newTempMenuList);
setSubmitData(newSubmitData);
};
return (
<Modal
wrapClassName={styles.MenuModal}
title={<ModalTitle />}
width={900}
okText="确定"
cancelText="取消"
visible={isShowMenuModal}
bodyStyle={{ padding: 0 }}
onOk={() => {
mdOk(submitData);
}}
onCancel={() => {
mdCancel();
}}
>
<div className={styles.modalBodyHead}>
<Input
allowClear
placeholder="搜索功能菜单"
prefix={
<img
src={starIcon}
alt=""
style={{ height: '18px', marginRight: '5px' }}
/>
// <SearchOutlined style={{ color: '#1d8dff', fontSize: '16px' }} />
}
onChange={e => {
setSearchInfo(e.target.value);
}}
/>
</div>
<div className={classnames(styles.modalBodyMain)}>
{tempMenuList.length ? (
tempMenuList.map(item => (
<div className={styles.menuWrapper} key={item.key}>
<p className={styles.menuGroupTitle}>{item.name}</p>
<div className={styles.menuList}>
{item.childMenus.map(child => {
const { match, name } = child;
const [before, after] = match;
const { isAdded } = child.extData;
const bname = after > -1 ? name.slice(0, before) : '';
const cname =
after > -1 ? (
<em style={{ color: 'orange', fontStyle: 'normal' }}>
{name.slice(before, after + 1)}
</em>
) : (
''
);
const aname =
after > -1 ? name.slice(after + 1, name.length) : name;
return (
<div
key={child.key}
className={classnames(
styles.menuItem,
isAdded ? styles.menuItem_added : '',
)}
onClick={() => {
toggleMenu(child);
}}
>
<div>
<span>
{bname}
{cname}
{aname}
</span>
{isAdded ? (
<p className={styles.addedFlag}>
<CheckOutlined />
</p>
) : null}
</div>
</div>
);
})}
</div>
</div>
))
) : (
<PandaEmpty />
)}
</div>
</Modal>
);
};
const mapStateToProps = state => ({
menus: state.getIn(['global', 'menu']),
});
export default connect(
mapStateToProps,
null,
......
......@@ -7,7 +7,7 @@
background: url(assets/images/commonMenu/page-background.png);
font-family: Microsoft YaHei;
padding-top: 57px;
background-color: #F5F6FC;
background-color: #f5f6fc;
overflow: hidden;
:global {
......@@ -22,7 +22,8 @@
.filterIconBox {
.iconBox {
filter: invert(33%) sepia(100%) saturate(1499%) hue-rotate(199deg) brightness(102%) contrast(104%);
filter: invert(33%) sepia(100%) saturate(1499%) hue-rotate(199deg)
brightness(102%) contrast(104%);
img {
filter: brightness(0%);
}
......@@ -40,8 +41,8 @@
.searchBox {
flex: 1;
height: 48px;
background: #FFFFFF;
box-shadow: 0px 5px 10px 0px #F3F3F3;
background: #ffffff;
box-shadow: 0px 5px 10px 0px #f3f3f3;
border-radius: 2px;
display: flex;
justify-content: space-between;
......@@ -50,13 +51,20 @@
position: relative;
.searchTitle {
&>i {
display: flex;
align-items: center;
& > i {
margin-right: 15px;
color: #3B7FDF;
color: #3b7fdf;
}
.addIcon {
&:hover {
color: @primary-color;
}
}
}
.searchInput {
flex: 1;
// flex: 1;
overflow: hidden;
margin-left: 50px;
}
......@@ -140,19 +148,21 @@
flex: none;
width: 135px;
height: 48px;
background: linear-gradient(0deg, #1685FF 0%, #49A0FF 100%);
box-shadow: 0px 5px 10px 0px #F5F6FC;
background: linear-gradient(0deg, #1685ff 0%, #49a0ff 100%);
box-shadow: 0px 5px 10px 0px #f5f6fc;
border-radius: 2px;
margin-left: 36px;
font-weight: bold;
color: #FFFFFF;
color: #ffffff;
}
}
.menuCardWrapper {
display: grid;
grid-template-columns: repeat(4, 25%);
// display: grid;
// grid-template-columns: repeat(4, 25%);
display: flex;
justify-content: center;
flex-wrap: wrap;
height: calc(100% - 185px);
padding: 0 5%;
overflow: auto;
......@@ -184,7 +194,8 @@
right: 10px;
// left: 50%;
// transform: translateX(-50%);
background: url("assets/images/commonMenu/card-label.png") center/100% no-repeat;
background: url('assets/images/commonMenu/card-label.png') center/100%
no-repeat;
display: flex;
justify-content: space-between;
align-items: center;
......@@ -204,13 +215,14 @@
font-size: 14px;
color: #999999;
::before {
content: "·";
content: '·';
margin-right: 0.5em;
}
}
}
}
// 底部logo
.pageLogo {
width: 197px;
position: absolute;
......@@ -223,3 +235,110 @@
}
}
.MenuModal {
:global {
.ant-modal-header {
padding: 0 16px;
background: url('../../assets/basic/图层\ 998@2x.png') center/100% 100%
no-repeat;
.ant-modal-title {
height: 56px;
}
}
.ant-modal-close {
color: #fff;
}
}
}
// 菜单modal弹窗
.modalTitle {
margin: 0;
border-bottom: none;
height: 100%;
line-height: 56px;
color: #fff;
&::before {
content: '';
display: inline-block;
width: 0px;
height: 16px;
background: #1d8dff;
margin-right: 12px;
vertical-align: middle;
}
}
.modalBodyHead {
// background: url('../../assets/basic/图层\ 998@2x.png') center/100% 100% no-repeat;
padding: 20px 30px;
// color: red;
:global {
.ant-input-affix-wrapper {
border-radius: 20px;
}
}
}
.modalBodyMain {
height: 500px;
overflow: auto;
padding: 0 20px 20px;
// margin-top: 24px;
border-top: 1px solid #ebebeb;
.menuWrapper {
border-bottom: 1px solid #ebebeb;
padding-bottom: 20px;
.menuGroupTitle {
color: #323232;
font-size: 16px;
margin: 0;
padding: 20px 10px 10px;
}
.menuList {
display: flex;
flex-wrap: wrap;
.menuItem {
width: 20%;
padding: 10px;
&.menuItem_added {
& > div {
border: 1px solid #78bbff;
background: #e7f2ff;
color: @primary-color;
}
}
& > div {
// width: 111px;
height: 37px;
line-height: 37px;
text-align: center;
border: 1px solid #f5f5f5;
background: #f5f5f5;
border-radius: 3px;
color: #646464;
font-size: 14px;
cursor: pointer;
position: relative;
&:hover {
color: @primary-color;
border-color: @primary-color;
}
}
.addedFlag {
position: absolute;
width: 20px;
height: 20px;
top: 0;
right: 0;
border-radius: 0 2px 0 100%;
background: @primary-color;
color: #fff;
font-size: 12px;
text-align: center;
line-height: 12px;
padding-left: 3px;
}
}
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment