Electorn 项目实战

创建 React 应用

create-react-app my-app

基础配置

  • concurrently: 连接多个命令,中间使用空格分开
  • wait-on:等待某个结果执行之后再去执行后续的命令
  • cross-env : 跨平台的环境变量设置

package.json

"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "dev": "concurrently \"cross-env BROWSER=none npm start\" \"wait-on http://localhost:3000 && electron .\""
  },

桌面应用主入口 main.js

  • electron-is-dev :环境变量判断
const { app, BrowserWindow, Menu } = require('electron')
const isDev = require('electron-is-dev')
const Store = require('electron-store')
const menuTemp = require('./src/temp/menuTemp')

Store.initRenderer()

let mainWindow

app.on('ready', () => {
  mainWindow = new BrowserWindow({
    width: 1024,
    height: 650,
    minWidth: 600,
    webPreferences: {
      nodeIntegration: true,
      enableRemoteModule: true
    }
  })

  const urlLocation = isDev ? "http://localhost:3000" : 'myUrl'

  mainWindow.loadURL(urlLocation)

  // 添加自定义的原生菜单
  const menu = Menu.buildFromTemplate(menuTemp)
  Menu.setApplicationMenu(menu)
})

styled-components

  • 自定义样式
import styled from 'styled-components'

// 自定义左侧容器
let LeftDiv = styled.div.attrs({
  className: 'col-3 left-panel'
})`
  position: relative;
  background-color: #7b8c7c;
  min-height: 100vh;
  .btn_list{
    left: 0;
    bottom: 0;
    width: 100%;
    position: absolute;
  }
`

字体文件

  • https://fontawesome.com/icons
  • @fortawesome/fontawesome-svg-core: 核心文件需要安装
  • @fortawesome/react-fontawesome :react 风格
  • @fortawesome/free-solid-svg-icons :solid 类型字体库
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFileAlt } from '@fortawesome/free-solid-svg-icons'

<FontAwesomeIcon icon={faFileAlt}></FontAwesomeIcon>

组件传入数据 props 类型校验

  • prop-types
import React, { Fragment, useState, useEffect, useRef } from "react"
import styled from "styled-components"
import PropTypes from 'prop-types'

const FileList = ({ files, editFile, saveFile, deleteFile }) => {
  return (
    ...
  )
}

FileList.propTypes = {
  files: PropTypes.array,
  editFile: PropTypes.func,
  saveFile: PropTypes.func,
  deleteFile: PropTypes.func,
}

export default FileList 

引用不同 className 判断

  • classnames
import classNames from 'classnames'

// 组合类名
let finalClass = classNames({
  "nav-link": true,
  "active": activeItem === file.id,
  "unSaveMark": unSaveMark
})

引入开源 markdown 编辑器

  • react-simplemde-editor
import SimpleMDE from "react-simplemde-editor"

<SimpleMDE
   key={activeFile && activeFile.id}
   onChange={(value) => { changeFile(activeFile.id, value) }}
   value={activeFile.body}
   options={{
     autofocus: true,
     spellChecker: false,
     minHeight: "445px"
   }}
/>

完整示列代码

App.js

import { v4 } from 'uuid'
import React, { useEffect, useState } from 'react'
import styled from 'styled-components'
import 'bootstrap/dist/css/bootstrap.min.css'
import SearchFile from './components/SearchFile'
import initFiles from './utils/initFiles'
import FileList from './components/FileList'
import ButtonItem from './components/ButtonItem'
import { faFileImport, faPlus } from '@fortawesome/free-solid-svg-icons'
import TabList from './components/TabList'
import SimpleMDE from "react-simplemde-editor"
import "easymde/dist/easymde.min.css"
import { mapArr, objToArr, readFile, writeFile, renameFile, deleteFile } from './utils/helper'
import useIpcRenderer from './hooks/useIpcRenderer'

const path = window.require('path')
const { remote, ipcRenderer } = window.require('electron')
const Store = window.require('electron-store')

const fileStore = new Store({ "name": "filesInfo" })

// 定义方法实现具体属性的持久化存储
const saveInfoToStore = (files) => {
  const storeObj = objToArr(files).reduce((ret, file) => {
    const { id, title, createTime, path } = file
    ret[id] = {
      id,
      path,
      title,
      createTime
    }
    return ret
  }, {})
  fileStore.set('files', storeObj)
}

// 自定义左侧容器
let LeftDiv = styled.div.attrs({
  className: 'col-3 left-panel'
})`
  position: relative;
  background-color: #7b8c7c;
  min-height: 100vh;
  .btn_list{
    left: 0;
    bottom: 0;
    width: 100%;
    position: absolute;
    p{
      border: 0;
      width: 50%;
      color: #fff;
      border-radius: 0;
      margin-bottom: 0!important;
    }
    p:nth-of-type(1){
      background-color: #8ba39e;
    }
    p:nth-of-type(2){
      background-color: #98b4b3;
    }
  }
`

// 自定义右侧容器
let RightDiv = styled.div.attrs({
  className: 'col-9 right-panel'
})`
  background-color: #c9d8cd;
  .init-page{
    color: #888;
    text-align:center;
    font: normal 28px/300px '微软雅黑';
  }
`

function App() {

  const [files, setFiles] = useState(fileStore.get('files') || {})  // 代表所有的文件信息
  const [activeId, setActiveId] = useState('')  // 当前正在编辑的文件id
  const [openIds, setOpenIds] = useState([]) // 当前已打开的所有文件信息 ids
  const [unSaveIds, setUnSaveIds] = useState([]) // 当前未被保存的所有文件信息 ids
  const [searchFiles, setSearchFiles] = useState([])  // 将左侧展示的搜索列表与默认列表信息进行区分

  // 自定义一个当前磁盘里存放文件的路径
  const savedPath = remote.app.getPath('documents') + '/testMk'
  console.log(remote.app.getPath('userData'))

  // 计算已打开的所有文件信息
  const openFiles = openIds.map(openId => {
    return files[openId]
  })

  // 计算正在编辑的文件信息
  const activeFile = files[activeId]

  // 计算当前左侧列表需要展示什么样的信息
  const fileList = (searchFiles.length > 0) ? searchFiles : objToArr(files)

  // 01 点击左侧文件显示编辑页
  const openItem = (id) => {
    // 将当前 id 设置为 active id 
    setActiveId(id)
    // 点击某个文件项时读取它里面的内容显示
    const currentFile = files[id]
    if (!currentFile.isLoaded) {
      readFile(currentFile.path).then((data) => {
        const newFile = { ...currentFile, body: data, isLoaded: true }
        setFiles({ ...files, [id]: newFile })
      })
    }
    // 将id添加至 open ids
    if (!openIds.includes(id)) {
      setOpenIds([...openIds, id])
    }
  }

  // 02 点击某个选项时切换当前状态
  const changeActive = (id) => {
    setActiveId(id)
  }
  // 03 点击关闭按钮
  const closeFile = (id) => {
    // 将当前的 id 从已经 open 的数组中去除
    const retOpen = openIds.filter(openId => openId !== id)
    setOpenIds(retOpen)
    // 当某一个选项被关闭之后还需要给所有已打开文件设置一个当前状态
    if (retOpen.length > 0 && (activeId == id)) {
      setActiveId(retOpen[0])
    } else if (retOpen.length > 0 && (activeId !== id)) {
      setActiveId(activeId)
    }
    else {
      setActiveId('')
    }
  }

  // 04 当文件内容更新时
  const changeFile = (id, newValue) => {
    if (newValue !== files[id].body) {
      if (!unSaveIds.includes(id)) {
        setUnSaveIds([...unSaveIds, id])
      }
      const newFile = { ...files[id], body: newValue }
      setFiles({ ...files, [id]: newFile })
    }
  }

  // 05 删除某个文件项
  const deleteItem = (id) => {
    const file = files[id]
    if (!file.isNew) {
      deleteFile(file.path).then(() => {
        delete files[id]
        setFiles(files)
        saveInfoToStore(files)
        closeFile(id)
      })
    } else {
      delete files[id]
      setFiles(files)
      saveInfoToStore(files)
      closeFile(id)
    }
  }

  // 06 依据关键字搜索文件
  const searchFile = (keyWord) => {
    const newFiles = objToArr(files).filter(file => file.title.includes(keyWord))
    setSearchFiles(newFiles)
  }

  // 07 重命名
  const saveData = (id, newTitle, isNew) => {
    const item = objToArr(files).find(file => file.title == newTitle)
    if (item) {
      newTitle += '_copy'
    }
    const newPath = isNew ? path.join(savedPath, `${newTitle}.md`) : path.join(path.dirname(files[id].path), `${newTitle}.md`)
    const newFile = { ...files[id], title: newTitle, isNew: false, path: newPath }
    const newFiles = { ...files, [id]: newFile }
    if (isNew) {
      // 执行创建
      writeFile(newPath, files[id].body).then(() => {
        setFiles(newFiles)
        saveInfoToStore(newFiles)
      })
    } else {
      // 执行更新
      const oldPath = files[id].path
      renameFile(oldPath, newPath).then(() => {
        setFiles(newFiles)
        saveInfoToStore(newFiles)
      })
    }

  }

  // 08 新建操作
  const createFile = () => {
    const newId = v4()
    const newFile = {
      id: newId,
      title: '',
      isNew: true,
      body: '## 初始化内容',
      createTime: new Date().getTime()
    }
    let flag = objToArr(files).find(file => file.isNew)
    if (!flag) {
      setFiles({ ...files, [newId]: newFile })
    }
  }

  // 09 保存当前正在编辑的文件
  const saveCurrentFile = () => {
    writeFile(activeFile.path, activeFile.body).then(() => {
      setUnSaveIds(unSaveIds.filter(id => id !== activeFile.id))
    })
  }

  // 10 执行外部 md 文件导入
  const importFile = () => {
    remote.dialog.showOpenDialog({
      defaultPath: __dirname,
      buttonLabel: '请选择',
      title: '选择md文件',
      properties: ['openFile', 'multiSelections'],
      filters: [
        { "name": "md文档", extensions: ["md"] },
        { "name": "其它类型", extensions: ["js", 'json', 'html'] },
      ]
    }).then((ret) => {
      const paths = ret.filePaths
      if (paths.length) {
        // 01 判断当前路径们,是否存在于 files 当中,如果已经存在则无须再执行导入操作
        const validPaths = paths.filter(filePath => {
          // 判断当前 path 是否已经存在过了
          const existed = Object.values(files).find(file => {
            return file.path == filePath
          })
          return !existed
        })

        // 02 将上述的路径信息组装成 files 格式, id title path 
        const packageData = validPaths.map(filePath => {
          return {
            id: v4(),
            title: path.basename(filePath, '.md'),
            path: filePath
          }
        })

        // 03 将上述的数据格式处理为 files 所需要的
        const newFiles = { ...files, ...mapArr(packageData) }

        // 04 更新数据重新渲染
        setFiles(newFiles)
        // TODO: 完成持久化操作
        saveInfoToStore(newFiles)

        // 05 成功导入提示
        if (packageData.length) {
          remote.dialog.showMessageBox({
            type: 'info',
            title: "导入md文档",
            message: '文件导入成功'
          })
        }
      } else {
        console.log('未选择文件导入')
      }
    })
  }

  // 实现主进程与渲染进程的事件通信
  useIpcRenderer({
    'execute-create-file': createFile,
    'execute-import-file': importFile,
    'execute-save-file': saveCurrentFile,
  })

  return (
    <div className="App container-fluid px-0">
      <div className="row no-gutters">
        <LeftDiv>
          <SearchFile
            title={'我的文档'}
            onSearch={searchFile}
          ></SearchFile>

          <FileList
            files={fileList}
            editFile={openItem}
            deleteFile={deleteItem}
            saveFile={saveData}
          />

        </LeftDiv>
        <RightDiv>
          {
            activeFile &&
            <>
              <TabList
                files={openFiles}
                activeItem={activeId}
                unSaveItems={unSaveIds}
                clickItem={changeActive}
                closeItem={closeFile}
              />
              <SimpleMDE
                key={activeFile && activeFile.id}
                onChange={(value) => { changeFile(activeFile.id, value) }}
                value={activeFile.body}
                options={{
                  autofocus: true,
                  spellChecker: false,
                  minHeight: "445px"
                }}
              />
            </>
          }
          {
            !activeFile &&
            <div className="init-page">新建或者导入具体的文档</div>
          }
        </RightDiv>
      </div>
    </div >
  )
}

export default App

components/ButtonItem.js

在这里插入代码片import React, { Fragment, useState, useEffect, useRef } from "react"
import styled from "styled-components"
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import PropTypes from 'prop-types'

// 自定义P标签模拟按钮操作
const BtnP = styled.p.attrs({
  className: 'btn'
})``

const ButtonItem = ({ title, btnClick, icon }) => {
  return (
    <BtnP onClick={btnClick}>
      <FontAwesomeIcon icon={icon} />
      <span className="ml-2">{title}</span>
    </BtnP>
  )
}

export default ButtonItem

components/FileList.js

import React, { Fragment, useState, useEffect, useRef } from "react"
import styled from "styled-components"
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFileAlt, faEdit, faTrashAlt, faTimes } from '@fortawesome/free-solid-svg-icons'
import PropTypes from 'prop-types'
import useKeyHandler from '../hooks/useKeyHandler'
import useContextMenu from '../hooks/useContextMenu'
import { getParentNode } from '../utils/helper'

// node_modules 模块导入
const { remote } = window.require('electron')
const { Menu } = remote

// ul 标签
let GroupUl = styled.ul.attrs({
  className: "list-group list-group-flush menu-box"
})`
  li{
    color: #fff;
    background: none;
  }
`

const FileList = ({ files, editFile, saveFile, deleteFile }) => {

  const [editItem, setEditItem] = useState(false)
  const [value, setValue] = useState('')
  const enterPressed = useKeyHandler(13)
  const escPressed = useKeyHandler(27)

  // 定义关闭行为
  const closeFn = () => {
    setEditItem(false)
    setValue('')
  }

  const contextMenuTmp = [
    {
      label: '重命名',
      click() {
        console.log('执行重命名')
        let retNode = getParentNode(currentEle.current, 'menu-item')
        setEditItem(retNode.dataset.id)
      }
    },
    {
      label: '删除',
      click() {
        let retNode = getParentNode(currentEle.current, 'menu-item')
        deleteFile(retNode.dataset.id)
      }
    }
  ]

  const currentEle = useContextMenu(contextMenuTmp, '.menu-box')


  useEffect(() => {
    const newFile = files.find(file => file.isNew)
    if (newFile && editItem !== newFile.id) {
      // 此时就说明我们本意是想新建一个文件,但是没有将新建文件操作完成就又去点击了其它的文件项
      deleteFile(newFile.id)
    }
  }, [editItem])

  useEffect(() => {
    const newFile = files.find(file => file.isNew)
    if (newFile) {

      setEditItem(newFile.id)
      setValue(newFile.title)
    }
  }, [files])

  useEffect(() => {
    if (enterPressed && editItem && value.trim() !== '') {
      const file = files.find(file => file.id == editItem)
      saveFile(editItem, value, file.isNew)
      closeFn()
    }

    if (escPressed && editItem) {
      closeFn()
    }
  })

  return (
    <GroupUl>
      {
        files.map(file => {
          return (
            <li
              className="list-group-item d-flex align-items-center menu-item"
              key={file.id}
              data-id={file.id}
              data-title={file.title}
            >
              {
                ((file.id !== editItem) && !file.isNew) &&
                <>
                  <span className="mr-2">
                    <FontAwesomeIcon icon={faFileAlt}></FontAwesomeIcon>
                  </span>
                  <span
                    className="col-8"
                    onClick={() => { editFile(file.id); closeFn() }}
                  >{file.title}</span>
                </>
              }
              {
                ((file.id == editItem) || file.isNew) &&
                <>

                  <input
                    className="col-9"
                    value={value}
                    onChange={(e) => { setValue(e.target.value) }}
                  />

                </>
              }
            </li>
          )
        })
      }
    </GroupUl >
  )
}

FileList.propTypes = {
  files: PropTypes.array,
  editFile: PropTypes.func,
  saveFile: PropTypes.func,
  deleteFile: PropTypes.func,
}

export default FileList 

components/SearchFile.js

import React, { Fragment, useState, useEffect, useRef } from "react"
import styled from "styled-components"
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTimes, faSearch } from '@fortawesome/free-solid-svg-icons'
import PropTypes from 'prop-types'
import useKeyHandler from '../hooks/useKeyHandler'
import useIpcRenderer from '../hooks/useIpcRenderer'

// 自定义搜索区域的 div 
let SearchDiv = styled.div.attrs({
  className: 'd-flex align-items-center justify-content-between'
})`
  border-bottom: 1px solid #fff;
  span{
    color:#fff;
    padding: 0 10px;
    font: normal 16px/40px '微软雅黑'
  }
  input{
    border: none;
    border-radius: 4px;
    margin-left: 10px;
  }
`

const SearchFile = ({ title, onSearch }) => {
  const [searchActive, setSearchActive] = useState(false)
  const [value, setValue] = useState('')
  const enterPressed = useKeyHandler(13)
  const escPressed = useKeyHandler(27)

  const oInput = useRef(null)

  const closeSearch = () => {
    setSearchActive(false)
    setValue('')

    // 当我们关闭搜索功能的时候,可以给它提供一个空字符,这样就没有满足条件的搜索
    // 结果,此时就能将原来列表数据重新展示出来
    onSearch('')
  }

  useEffect(() => {
    if (enterPressed && searchActive) {
      onSearch(value)
    }

    if (escPressed && searchActive) {
      closeSearch()
    }
  })

  useEffect(() => {
    if (searchActive) {
      oInput.current.focus()
    }
  }, [searchActive])

  useIpcRenderer({
    'execute-search-file': () => {
      setSearchActive(true)
    }
  })

  return (
    <Fragment>
      {
        !searchActive &&
        <>
          <SearchDiv>
            <span>{title}</span>
            <span onClick={() => { setSearchActive(true) }}>
              <FontAwesomeIcon icon={faSearch}></FontAwesomeIcon>
            </span>
          </SearchDiv>
        </>
      }
      {
        searchActive &&
        <>
          <SearchDiv>
            <input
              value={value}
              ref={oInput}
              onChange={(e) => { setValue(e.target.value) }}
            />
            <span onClick={closeSearch}>
              <FontAwesomeIcon icon={faTimes}></FontAwesomeIcon>
            </span>
          </SearchDiv>
        </>
      }
    </Fragment>
  )
}

SearchFile.propTypes = {
  title: PropTypes.string,
  onSearch: PropTypes.func.isRequired
}

SearchFile.defaultProps = {
  title: '文档列表'
}

export default SearchFile


components/TabList.js

import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import classNames from 'classnames'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTimes } from '@fortawesome/free-solid-svg-icons'

// 自定义 ul 标签
let TabUl = styled.ul.attrs({
  className: 'nav nav-pills'
})`
  border-bottom: 1px solid #fff;
  li a{
    color: #fff;
    border-radius: 0px!important;
  }
  li a.active{
    background-color: #3e403f!important;
  }
  .nav-link.unSaveMark .rounded-circle{
    width: 11px;
    height: 11px;
    display: inline-block;
    background-color:#b80233; 
  }
  .nav-link.unSaveMark .icon-close{
    display: none;
  }
  .nav-link.unSaveMark:hover .icon-close{
    display: inline-block;
  }
  .nav-link.unSaveMark:hover .rounded-circle{
    display: none;
  }
`

const TabList = ({ files, activeItem, unSaveItems, clickItem, closeItem }) => {
  return (
    <TabUl>
      {
        files.map(file => {
          // 定义变量控制未保存状态
          let unSaveMark = unSaveItems.includes(file.id)

          // 组合类名
          let finalClass = classNames({
            "nav-link": true,
            "active": activeItem === file.id,
            "unSaveMark": unSaveMark
          })
          return (
            <li className="nav-item" key={file.id}>
              <a
                href="#"
                className={finalClass}
                onClick={(e) => { e.preventDefault(); clickItem(file.id) }}
              >
                {file.title}
                <span
                  className="ml-2 icon-close"
                  onClick={(e) => { e.stopPropagation(); closeItem(file.id) }}
                >
                  <FontAwesomeIcon icon={faTimes} />
                </span>
                {unSaveMark && <span className="ml-2 rounded-circle"></span>}
              </a>
            </li>
          )
        })
      }
    </TabUl>
  )
}

TabList.propTypes = {
  files: PropTypes.array,
  activeItem: PropTypes.string,
  unSaveItems: PropTypes.array,
  clickItem: PropTypes.func,
  closeItem: PropTypes.func
}

TabList.defaultProps = {
  unSaveItems: []
}

export default TabList

hooks/useContextMenu.js

import { useEffect, useRef } from "react"
const { remote } = window.require('electron')
const { Menu } = remote

function useContextMenu(contextMenuTmp, areaClass) {
  const currentEle = useRef(null)
  useEffect(() => {
    // 获取需要触发右键菜单的区域的元素
    const areaEle = document.querySelector(areaClass)
    const menu = Menu.buildFromTemplate(contextMenuTmp)
    const contextMenuHandle = (ev) => {
      if (areaEle.contains(ev.target)) {
        currentEle.current = ev.target
        menu.popup({ window: remote.getCurrentWindow })
      }
    }
    window.addEventListener('contextmenu', contextMenuHandle)
    return () => {
      window.removeEventListener('contextmenu', contextMenuHandle)
    }
  })
  return currentEle
}

export default useContextMenu

hooks/useIpcRenderer.js

import { useEffect } from 'react'
const { ipcRenderer } = window.require('electron')

function useIpcRenderer(actionMap) {
  useEffect(() => {
    Object.keys(actionMap).forEach((action) => {
      ipcRenderer.on(action, actionMap[action])
    })
    return () => {
      Object.keys(actionMap).forEach((action) => {
        ipcRenderer.removeListener(action, actionMap[action])
      })
    }
  })
}

export default useIpcRenderer

hooks/useKeyHandler.js

import { useState, useEffect } from 'react'

const useKeyHandler = (code) => {

  const [keyPressed, setKeyPress] = useState(false)

  // 按下
  const keyDownHandler = ({ keyCode }) => {
    if (keyCode == code) {
      setKeyPress(true)
    }
  }
  // 抬起
  const keyUpHandler = ({ keyCode }) => {
    if (keyCode == code) {
      setKeyPress(false)
    }
  }

  useEffect(() => {
    document.addEventListener('keyup', keyUpHandler)
    document.addEventListener('keydown', keyDownHandler)
    return () => {
      document.removeEventListener('keyup', keyUpHandler)
      document.removeEventListener('keydown', keyDownHandler)
    }
  }, [])

  return keyPressed
}

export default useKeyHandler

utils/helper.js

const fs = window.require('fs').promises

export const mapArr = (arr) => {
  return arr.reduce((map, item) => {
    map[item.id] = item
    return map
  }, {})
}

export const objToArr = (obj) => {
  return Object.keys(obj).map(key => obj[key])
}

export const readFile = (path) => {
  return fs.readFile(path, 'utf-8')
}

export const writeFile = (path, content) => {
  return fs.writeFile(path, content, 'utf-8')
}

export const renameFile = (path, newPath) => {
  return fs.rename(path, newPath)
}

export const deleteFile = (path) => {
  return fs.unlink(path)
}

export const getParentNode = (node, parentClassName) => {
  let currentEle = node
  while (currentEle !== null) {
    if (currentEle.classList.contains(parentClassName)) {
      return currentEle
    }
    currentEle = currentEle.parentNode
  }
  return false
}

utils/initFiles.js

const initFiles = [
  {
    id: '1',
    title: '文件1',
    body: '## 文件1内容',
    createTime: '12345678'
  },
  {
    id: '2',
    title: '文件2',
    body: '拉勾教育666',
    createTime: '12345678'
  },
  {
    id: '3',
    title: '文件3',
    body: '前端前端',
    createTime: '12345678'
  },
  {
    id: '4',
    title: '标题1',
    body: '44444',
    createTime: '12345678'
  },
  {
    id: '5',
    title: '标题2',
    body: '555555',
    createTime: '12345678'
  },
]

export default initFiles

temp/menuTemp.js

const { shell } = require('electron')

const template = [
  {
    label: '文件',
    submenu: [
      {
        label: '新建',
        accelerator: 'CmdOrCtrl+N',
        click(menuItem, browserWindow, event) {
          browserWindow.webContents.send('execute-create-file')
        }
      },
      {
        label: '保存',
        accelerator: 'CmdOrCtrl+S',
        click(menuItem, browserWindow, event) {
          browserWindow.webContents.send('execute-save-file')
        }
      },
      {
        label: '搜索',
        accelerator: 'CmdOrCtrl + F',
        click(menuItem, browserWindow, event) {
          browserWindow.webContents.send('execute-search-file')
        }
      },
      {
        label: '导入',
        accelerator: 'CmdOrCtrl + o',
        click(menuItem, browserWindow, event) {
          browserWindow.webContents.send('execute-import-file')
        }
      }
    ]
  },
  {
    label: '编辑',
    submenu: [
      {
        label: '撤销',
        accelerator: 'CmdOrCtrl + Z',
        role: 'undo'
      },
      {
        label: '重做',
        accelerator: 'Shift + CmdOrCtrl + z',
        role: 'redo'
      },
      {
        type: 'separator'
      },
      {
        label: '剪切',
        accelerator: 'CmdOrCtrl+X',
        role: 'cut'
      },
      {
        label: '复制',
        accelerator: 'CmdOrCtrl + c',
        role: 'copy'
      },
      {
        label: '全选',
        accelerator: 'CmdOrCtrl + A',
        role: 'selectall'
      }
    ]
  },
  {
    label: '视图',
    submenu: [
      {
        label: '刷新',
        accelerator: 'Shift + CmdOrCtrl + R',
        click(item, focusedWindow) {
          if (focusedWindow) {
            focusedWindow.reload()
          }
        }
      },
      {
        label: '最大化',
        accelerator: (() => {
          if (process.platform === 'darwin') {
            return 'Ctrl + Command + F'
          } else {
            return 'F11'
          }
        })(),
        click(item, focusedWindow) {
          if (focusedWindow) {
            focusedWindow.setFullScreen(!focusedWindow.isFullScreen())
          }
        }
      },
      {
        label: '开发者工具',
        accelerator: (() => {
          if (process.platform === 'darwin') {
            return 'Alt + Command + I'
          } else {
            return 'Ctrl + Shift + I'
          }
        })(),
        click(item, focusedWindow) {
          if (focusedWindow) {
            focusedWindow.toggleDevTools()
          }
        }
      }
    ]
  },
  {
    label: '窗口',
    role: 'window',
    submenu: [
      {
        label: '最小化',
        accelerator: 'CmdOrCtrl + M',
        role: 'minimize'
      },
      {
        label: '关闭',
        accelerator: 'CmdOrCtrl + W',
        role: 'close'
      }
    ]
  },
  {
    label: '帮助',
    role: 'help',
    submenu: [
      {
        label: '更多',
        click() {
          shell.openExternal("http://electronjs.org")
        }
      }
    ]
  }
]

module.exports = template
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值