基於React18+Electron27+ArcoDesign仿macOS桌面端系統框架ElectronMacOS。 electron-react-macOs 基於electron27.x+vite4+react18+arcoDesign+zustand等技術構建桌面版仿MacOs框架系統解決方案。 ...
基於React18+Electron27+ArcoDesign仿macOS桌面端系統框架ElectronMacOS。
electron-react-macOs 基於electron27.x+vite4+react18+arcoDesign+zustand等技術構建桌面版仿MacOs框架系統解決方案。支持中英文/繁體、dark+light主題、桌面多層級路由、多視窗路由頁面、動態換膚、Dock懸浮菜單等功能。
ElectronReactOS系統是首創自研的桌面多層級路由菜單、支持electron多開視窗+彈窗路由視窗。
技術棧
- 開發工具: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
- 彈窗組件:rdialog (基於react多功能layer彈窗)
- 美化滾動條:rscroll (基於react虛擬滾動條組件)
特性
- 桌面路由頁面支持暗黑+亮色模式
- 內置中英文/繁體國際化
- 經典桌面Dock懸浮菜單
- 可拖拽桌面路由+程式塢Dock菜單
- 桌面路由支持多個子級路由配置
- 動態視覺效果,自定義桌面換膚背景
- 可視化多視窗路由,支持electron新開視窗+rdialog彈窗頁面
大家如果對Electron封裝多視窗感興趣,可以去看看下麵這篇分享文章。
https://www.cnblogs.com/xiaoyan2017/p/17788495.html
項目結構
使用vite4構建react18項目,整合electron跨端技術,搭建桌面版OS管理系統。
Electron桌面os佈局模板
桌面os佈局分為頂部操作欄+桌面端路由菜單+底部Dock菜單三大模塊。
<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菜單
底部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.jsx模板
/** * 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> ) }
OK,以上就是Electron27+React18開發仿製MacOS桌面系統的一些分享,希望對大家有些幫助哈~~
最後附上兩個最近實例項目
https://www.cnblogs.com/xiaoyan2017/p/17695193.html
https://www.cnblogs.com/xiaoyan2017/p/17552562.html
本文為博主原創文章,未經博主允許不得轉載,歡迎大家一起交流 QQ(282310962) wx(xy190310)