基于Electron27+React18仿华为MatePad桌面OS系统
【摘要】 前几天有分享过一篇react18+arco-design开发后台管理系统,今天再分享一篇react18+跨端技术electron打造桌面端OS系统应用。https://bbs.huaweicloud.cn/blogs/413317electron-react-macOs 基于electron27+react18+arco.esign+zustand+axios等技术打造的一款桌面版仿华为m...
前几天有分享过一篇react18+arco-design开发后台管理系统,今天再分享一篇react18+跨端技术electron打造桌面端OS系统应用。
https://bbs.huaweicloud.cn/blogs/413317
electron-react-macOs 基于electron27+react18+arco.esign+zustand+axios等技术打造的一款桌面版仿华为matePad平板OS框架系统解决方案。支持中英文/繁体、dark+light主题、桌面多层级路由、多窗口路由页面、动态换肤、Dock悬浮菜单等功能。
使用技术
- 开发工具:vscode
- 框架技术:vite4+react18+zustand+react-router
- 跨端技术:electron^27.0.1
- 打包工具:electron-builder^24.6.4
- UI组件库:arco-design (字节react轻量级UI组件库)
- 图表组件:bizcharts^4.1.23
- 拖拽库:sortablejs
- 模拟请求:axios
项目目录
主入口main.js
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import '@arco-design/web-react/dist/css/arco.css'
import './styles/common.scss'
import { launchWin } from '@/windows/action'
launchWin().then(config => {
console.log('——+——+——窗口参数:', config)
console.log('——+——+——窗口id:', config.id)
// 设置全局存储窗口配置
window.config = config
ReactDOM.createRoot(document.getElementById('root')).render(<App />)
})
Electron多进程通讯管理
/**
* Electron多进程通讯
* @author Andy
*/
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
// 通过 channel 向主进程发送异步消息。主进程使用 ipcMain.on() 监听 channel
send: (channel, data) => {
ipcRenderer.send(channel, data)
},
// 通过 channel 向主进程发送消息,并异步等待结果。主进程应该使用 ipcMain.handle() 监听 channel
invoke: (channel, data) => {
return new Promise(resolve => ipcRenderer.invoke(channel, data).then(res => resolve(res)).catch(e => console.log(e)))
},
// 监听 channel 事件
receive: (channel, func) => {
console.log("preload-receive called. args: ")
ipcRenderer.on(channel, (event, ...args) => func(event, ...args))
},
// 一次性监听事件
once: (channel, func) => {
ipcRenderer.once(channel, (event, ...args) => func(event, ...args))
}
})
Electron桌面端OS布局
<div className="radmin__layout flexbox flex-col">
{/* 导航栏 */}
<Header />
{/* 桌面区域 */}
<div className="ra__layout-desktop flex1 flexbox" onContextMenu={handleDeskCtxMenu} style={{marginBottom: 70}}>
<DeskMenu />
</div>
{/* Dock菜单 */}
<Dock />
</div>
Electron+react实现Dock菜单
<div className="ra__docktool">
<div className={clsx('ra__dock-wrap', !dock ? 'compact' : 'split')}>
{dockMenu.map((res, key) => {
return (
<div key={key} className="ra__dock-group">
{ res?.children?.map((item, index) => {
return (
<a key={index} className={clsx('ra__dock-item', {'active': item.active, 'filter': item.filter})} onClick={() => handleDockClick(item)}>
<span className="tooltips">{item.label}</span>
<div className="img">
{ item.type != 'icon' ? <img src={item.image} /> : <Icon name={item.image} size={32} style={{color: 'inherit'}} /> }
</div>
</a>
)
})}
</div>
)
})}
</div>
</div>
const dockMenu = [
{
// 图片图标
children: [
{label: 'Safari', image: '/static/mac/safari.png', active: true},
{label: 'Launchpad', image: '/static/mac/launchpad.png'},
{label: 'Contacts', image: '/static/mac/contacts.png'},
{label: 'Messages', image: '/static/mac/messages.png', active: true}
]
},
{
// 自定义iconfont图标
children: [
{label: 'Home', image: <IconDesktop />, type: 'icon'},
{label: 'About', image: 've-icon-about', type: 'icon'}
]
},
{
children: [
{label: 'Appstore', image: '/static/mac/appstore.png'},
{label: 'Mail', image: '/static/mac/mail.png'},
{label: 'Maps', image: '/static/mac/maps.png', active: true},
{label: 'Photos', image: '/static/mac/photos.png'},
{label: 'Facetime', image: '/static/mac/facetime.png'},
{label: 'Calendar', image: '/static/mac/calendar.png'},
{label: 'Notes', image: '/static/mac/notes.png'},
{label: 'Calculator', image: '/static/mac/calculator.png'},
{label: 'Music', image: '/static/mac/music.png'}
]
},
{
children: [
{label: 'System', image: '/static/mac/system.png', active: true, filter: true},
{label: 'Empty', image: '/static/mac/bin.png', filter: true}
]
}
]
// 点击dock菜单
const handleDockClick = (item) => {
const { label } = item
if(label == 'Home') {
createWin({
title: '首页',
route: '/home',
width: 900,
height: 600
})
}else if(label == 'About') {
setWinData({ type: 'CREATE_WIN_ABOUT' })
}else if(label == 'System') {
createWin({
title: '网站设置',
route: '/setting/system/website',
isNewWin: true,
width: 900,
height: 600
})
}
}
useEffect(() => {
const dockGroup = document.getElementsByClassName('ra__dock-group')
// 组拖拽
for(let i = 0, len = dockGroup.length; i < len; i++) {
Sortable.create(dockGroup[i], {
group: 'share',
handle: '.ra__dock-item',
filter: '.filter',
animation: 200,
delay: 0,
onEnd({ newIndex, oldIndex }) {
console.log('新索引:', newIndex)
console.log('旧索引:', oldIndex)
}
})
}
}, [])
Electron+react实现多级路由桌面菜单
import { lazy } from 'react'
import {
IconDesktop, IconDashboard, IconLink, IconCommand, IconUserGroup, IconLock,
IconSafe, IconBug, IconUnorderedList, IconStop
} from '@arco-design/web-react/icon'
import Layout from '@/layouts'
import Desk from '@/layouts/desk'
import Blank from '@/layouts/blank'
import lazyload from '../lazyload'
export default [
/* 桌面模块 */
{
path: '/desk',
key: '/desk',
element: <Desk />,
meta: {
icon: <IconDesktop />,
name: 'layout__main-menu__desk',
title: 'Appstore',
isWhite: true, // 路由白名单
isAuth: true, // 需要鉴权
isHidden: false, // 是否隐藏菜单
}
},
{
path: '/home',
key: '/home',
element: <Layout>{lazyload(lazy(() => import('@views/home')))}</Layout>,
meta: {
icon: '/static/mac/appstore.png',
name: 'layout__main-menu__home-index',
title: '首页',
isAuth: true,
isNewWin: true
}
},
{
path: '/dashboard',
key: '/dashboard',
element: <Layout>{lazyload(lazy(() => import('@views/home/dashboard')))}</Layout>,
meta: {
icon: <IconDashboard />,
name: 'layout__main-menu__home-workplace',
title: '工作台',
isAuth: true
}
},
{
path: 'https://react.dev/',
key: 'https://react.dev/',
meta: {
icon: <IconLink />,
name: 'layout__main-menu__home-apidocs',
title: 'react.js官方文档',
rootRoute: '/home'
}
},
/* 组件模块 */
{
path: '/components',
key: '/components',
redirect: '/components/table/allTable', // 一级路由重定向
element: <Blank />,
meta: {
icon: <IconCommand />,
name: 'layout__main-menu__component',
title: '组件示例',
isAuth: true,
isHidden: false
},
children: [
{
path: 'table',
key: '/components/table',
element: <Blank />,
meta: {
icon: 've-icon-table',
name: 'layout__main-menu__component-table',
title: '表格',
isAuth: true
},
children: [
{
path: 'allTable',
key: '/components/table/allTable',
element: <Layout>{lazyload(lazy(() => import('@views/components/table/all')))}</Layout>,
meta: {
name: 'layout__main-menu__component-table_all',
title: '所有表格'
}
},
{
path: 'customTable',
key: '/components/table/customTable',
element: <Layout>{lazyload(lazy(() => import('@views/components/table/custom')))}</Layout>,
meta: {
name: 'layout__main-menu__component-table_custom',
title: '自定义表格'
}
},
{
path: 'search',
key: '/components/table/search',
element: <Blank />,
meta: {
name: 'layout__main-menu__component-table_search',
title: '搜索'
},
children: [
{
path: 'searchList',
key: '/components/table/search/searchList',
element: <Layout>{lazyload(lazy(() => import('@views/components/table/search')))}</Layout>,
meta: {
name: 'layout__main-menu__component-table_search_list',
title: '搜索列表'
}
}
]
}
]
},
{
path: 'list',
key: '/components/list',
element: <Layout>{lazyload(lazy(() => import('@views/components/list')))}</Layout>,
meta: {
icon: 've-icon-order-o',
name: 'layout__main-menu__component-list',
title: '列表'
}
},
{
path: 'form',
key: '/components/form',
element: <Blank />,
meta: {
icon: 've-icon-exception',
name: 'layout__main-menu__component-form',
title: '表单',
isAuth: true
},
children: [
{
path: 'allForm',
key: '/components/form/allForm',
element: <Layout>{lazyload(lazy(() => import('@views/components/form/all')))}</Layout>,
meta: {
name: 'layout__main-menu__component-form_all',
title: '所有表单'
}
},
{
path: 'customForm',
key: '/components/form/customForm',
element: <Layout>{lazyload(lazy(() => import('@views/components/form/custom')))}</Layout>,
meta: {
name: 'layout__main-menu__component-form_custom',
title: '自定义表单'
}
}
]
},
{
path: 'markdown',
key: '/components/markdown',
element: <Layout>{lazyload(lazy(() => import('@views/components/markdown')))}</Layout>,
meta: {
icon: <IconUnorderedList />,
name: 'layout__main-menu__component-markdown',
title: 'markdown编辑器'
}
},
{
path: 'qrcode',
key: '/components/qrcode',
meta: {
icon: 've-icon-qrcode',
name: 'layout__main-menu__component-qrcode',
title: '二维码'
}
},
{
path: 'print',
key: '/components/print',
meta: {
icon: 've-icon-printer',
name: 'layout__main-menu__component-print',
title: '打印'
}
},
{
path: 'pdf',
key: '/components/pdf',
meta: {
icon: 've-icon-pdffile',
name: 'layout__main-menu__component-pdf',
title: 'pdf'
}
}
]
},
/* 用户管理模块 */
{
path: '/user',
key: '/user',
redirect: '/user/userManage',
element: <Blank />,
meta: {
// icon: 've-icon-team',
icon: <IconUserGroup />,
name: 'layout__main-menu__user',
title: '用户管理',
isAuth: true,
isHidden: false
},
children: [
...
]
},
/* 配置模块 */
{
path: '/setting',
key: '/setting',
redirect: '/setting/system/website',
element: <Blank />,
meta: {
icon: 've-icon-settings-o',
name: 'layout__main-menu__setting',
title: '设置',
isHidden: false
},
children: [
...
]
},
/* 权限模块 */
{
path: '/permission',
key: '/permission',
redirect: '/permission/admin',
element: <Blank />,
meta: {
// icon: 've-icon-unlock',
icon: <IconLock />,
name: 'layout__main-menu__permission',
title: '权限管理',
isAuth: true,
isHidden: false
},
children: [
...
]
}
]
DeskMenu.js桌面路由
/**
* Desk桌面多层级路由菜单
* Create by andy Q:282310962
*/
export default function DeskMenu() {
const t = Locales()
const filterRoutes = routes.filter(item => !item?.meta?.isWhite)
// 桌面二级菜单弹框
const DeskPopup = (item) => {
const { key, meta, children } = item
return (
!meta?.isHidden &&
<RScroll maxHeight={220}>
<div className="ra__deskmenu-popup__body">
{ children.map(item => {
if(item?.children) {
return DeskSubMenu(item)
}
return DeskMenu(item)
})}
</div>
</RScroll>
)
}
// 桌面菜单项
const DeskMenu = (item) => {
const { key, meta, children } = item
return (
!meta?.isHidden &&
<div key={key} className="ra__deskmenu-block">
<a className="ra__deskmenu-item" onClick={()=>handleDeskClick(item)} onContextMenu={handleDeskCtxMenu}>
<div className="img">
{meta?.icon ?
isImg(meta?.icon) ? <img src={meta.icon} /> : <Icon name={meta.icon} size={40} />
:
<Icon name="ve-icon-file" size={40} />
}
</div>
{ meta?.name && <span className="title clamp2">{t[meta.name]}</span> }
</a>
</div>
)
}
// 桌面二级菜单项
const DeskSubMenu = (item) => {
const { key, meta, children } = item
return (
!meta?.isHidden &&
<div key={key} className="ra__deskmenu-block">
<a className="ra__deskmenu-item group" onContextMenu={e=>e.stopPropagation()}>
<Popover
title={<div className="ra__deskmenu-popup__title">{meta?.name && t[meta.name]}</div>}
content={() => DeskPopup(item)}
trigger="hover"
position="right"
triggerProps={{
popupStyle: {padding: 5},
popupAlign: {
right: [10, 45]
},
mouseEnterDelay: 300,
// showArrow: false
}}
style={{zIndex: 100}}
>
<div className="img">
{children.map((child, index) => {
if(child?.meta?.isHidden) return
return child?.meta?.icon ?
isImg(child?.meta?.icon) ? <img key={index} src={child.meta.icon} /> : <Icon key={index} name={child.meta.icon} size={10} />
:
<Icon key={index} name="ve-icon-file" size={10} />
})}
</div>
</Popover>
{ meta?.name && <span className="title clamp2">{t[meta.name]}</span> }
</a>
</div>
)
}
// 点击dock菜单
const handleDeskClick = (item) => {
const { key, meta, element } = item
const reg = /[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?/
if(reg.test(key)) {
window.open(key)
}else {
if(meta?.isNewWin) {
// 新窗口打开
createWin({
title: t[meta?.name] || meta?.title,
route: key,
width: 900,
height: 600
})
}else {
// 弹窗打开
rdialog({
title: t[meta?.name] || meta?.title,
content: <BrowserRouter>{element}</BrowserRouter>,
maxmin: true,
showConfirm: false,
area: ['900px', '550px'],
className: 'rc__dialogOS',
customStyle: {padding: 0},
zIndex: 100
})
}
}
}
// 右键菜单
const handleDeskCtxMenu = (e) => {
e.stopPropagation()
let pos = [e.clientX, e.clientY]
rdialog({
type: 'contextmenu',
follow: pos,
opacity: .1,
dialogStyle: {borderRadius: 3, overflow: 'hidden'},
btns: [
{text: '打开'},
{text: '重命名/配置'},
{
text: '删除',
click: () => {
rdialog.close()
}
}
]
})
}
useEffect(() => {
const deskEl = document.getElementById('deskSortable')
Sortable.create(deskEl, {
handle: '.ra__deskmenu-block',
animation: 200,
delay: 0,
onEnd({ newIndex, oldIndex }) {
console.log('新索引:', newIndex)
console.log('旧索引:', oldIndex)
}
})
}, [])
return (
<div className="ra__deskmenu" id="deskSortable">
{ filterRoutes.map(item => {
if(item?.children) {
return DeskSubMenu(item)
}
return DeskMenu(item)
})}
</div>
)
}
好了,今天的分享就到这里,后续还会分享一些实例项目。
来源: segmentfault.com,作者:xiaoyan2017,版权归原作者所有,如需转载,请联系作者。
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)