├── .browserslistrc ├── screenshot ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png └── reward.png ├── resources ├── logo.png ├── dingtalk.psd ├── icons │ ├── 16x16.png │ ├── 24x24.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 64x64.png │ ├── 96x96.png │ ├── icon.icns │ ├── icon.ico │ ├── 128x128.png │ ├── 256x256.png │ └── 512x512.png └── tray │ ├── 16x16.png │ ├── 20x20.png │ ├── 64x64.png │ ├── n-16x16.png │ ├── n-20x20.png │ └── n-64x64.png ├── postcss.config.js ├── src ├── main │ ├── index.js │ ├── online.js │ ├── index.dev.js │ ├── shortcut.js │ ├── notify.js │ ├── logo.js │ ├── autoUpdate.js │ ├── download.js │ ├── errorWin.js │ ├── aboutWin.js │ ├── setting.js │ ├── contextMenu.js │ ├── settingWin.js │ ├── emailWin.js │ ├── dingtalkTray.js │ ├── mainWin.js │ └── dingtalk.js ├── renderer │ ├── aboutWin │ │ ├── logo.png │ │ ├── index.js │ │ └── App.vue │ ├── settingWin │ │ ├── components │ │ │ ├── button │ │ │ │ ├── index.js │ │ │ │ └── button.vue │ │ │ ├── switch │ │ │ │ ├── index.js │ │ │ │ └── switch.vue │ │ │ └── keybinding │ │ │ │ ├── index.js │ │ │ │ └── keybinding.vue │ │ ├── index.js │ │ └── App.vue │ ├── index.html │ └── errorWin │ │ ├── index.js │ │ └── App.vue └── preload │ ├── mainWin │ ├── open.js │ ├── openEmail.js │ ├── notify.js │ ├── winOperation.js │ ├── notifyMessage.js │ ├── utils.js │ ├── index.js │ ├── css.less │ ├── download.js │ ├── fileTask.js │ └── Events.js │ └── emailWin │ └── index.js ├── .editorconfig ├── babel.config.js ├── .eslintrc.js ├── appveyor.yml ├── .travis.yml ├── .vscode └── launch.json ├── LICENSE ├── .gitignore ├── CONTRIBUTING.md ├── dependencies.js ├── README.md └── package.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /screenshot/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/screenshot/1.png -------------------------------------------------------------------------------- /screenshot/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/screenshot/2.png -------------------------------------------------------------------------------- /screenshot/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/screenshot/3.png -------------------------------------------------------------------------------- /screenshot/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/screenshot/4.png -------------------------------------------------------------------------------- /screenshot/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/screenshot/5.png -------------------------------------------------------------------------------- /screenshot/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/screenshot/6.png -------------------------------------------------------------------------------- /screenshot/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/screenshot/7.png -------------------------------------------------------------------------------- /screenshot/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/screenshot/8.png -------------------------------------------------------------------------------- /resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/logo.png -------------------------------------------------------------------------------- /screenshot/reward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/screenshot/reward.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /resources/dingtalk.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/dingtalk.psd -------------------------------------------------------------------------------- /resources/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/icons/16x16.png -------------------------------------------------------------------------------- /resources/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/icons/24x24.png -------------------------------------------------------------------------------- /resources/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/icons/32x32.png -------------------------------------------------------------------------------- /resources/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/icons/48x48.png -------------------------------------------------------------------------------- /resources/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/icons/64x64.png -------------------------------------------------------------------------------- /resources/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/icons/96x96.png -------------------------------------------------------------------------------- /resources/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/icons/icon.icns -------------------------------------------------------------------------------- /resources/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/icons/icon.ico -------------------------------------------------------------------------------- /resources/tray/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/tray/16x16.png -------------------------------------------------------------------------------- /resources/tray/20x20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/tray/20x20.png -------------------------------------------------------------------------------- /resources/tray/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/tray/64x64.png -------------------------------------------------------------------------------- /resources/tray/n-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/tray/n-16x16.png -------------------------------------------------------------------------------- /resources/tray/n-20x20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/tray/n-20x20.png -------------------------------------------------------------------------------- /resources/tray/n-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/tray/n-64x64.png -------------------------------------------------------------------------------- /resources/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/icons/128x128.png -------------------------------------------------------------------------------- /resources/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/icons/256x256.png -------------------------------------------------------------------------------- /resources/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/resources/icons/512x512.png -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | import DingTalk from './dingtalk' 2 | 3 | /* eslint-disable no-new */ 4 | new DingTalk() 5 | -------------------------------------------------------------------------------- /src/renderer/aboutWin/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/dingtalk/master/src/renderer/aboutWin/logo.png -------------------------------------------------------------------------------- /src/renderer/settingWin/components/button/index.js: -------------------------------------------------------------------------------- 1 | import Button from './button.vue' 2 | 3 | export default Vue => { 4 | Vue.component(Button.name, Button) 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/settingWin/components/switch/index.js: -------------------------------------------------------------------------------- 1 | import Switch from './switch.vue' 2 | 3 | export default Vue => { 4 | Vue.component(Switch.name, Switch) 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/settingWin/components/keybinding/index.js: -------------------------------------------------------------------------------- 1 | import Keybinding from './keybinding.vue' 2 | 3 | export default Vue => { 4 | Vue.component(Keybinding.name, Keybinding) 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= htmlWebpackPlugin.options.title %> 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | electron: 5 8 | } 9 | } 10 | ] 11 | ], 12 | plugins: [ 13 | '@babel/plugin-transform-runtime', 14 | '@babel/plugin-proposal-class-properties', 15 | '@babel/plugin-proposal-export-default-from' 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/main/online.js: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron' 2 | 3 | export default dingtalk => () => { 4 | ipcMain.on('online', (e, online) => { 5 | if (online === false) { 6 | // 第一次启动窗口 7 | if (dingtalk.online === null) { 8 | dingtalk.showErrorWin() 9 | } 10 | } else { 11 | dingtalk.hideErrorWin() 12 | } 13 | dingtalk.online = online 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/aboutWin/index.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | 6 | Vue.config.productionTip = false 7 | 8 | /* eslint-disable no-new */ 9 | new Vue({ 10 | el: '#app', 11 | components: { App }, 12 | template: '' 13 | }) 14 | -------------------------------------------------------------------------------- /src/renderer/errorWin/index.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | 6 | Vue.config.productionTip = false 7 | 8 | /* eslint-disable no-new */ 9 | new Vue({ 10 | el: '#app', 11 | components: { App }, 12 | template: '' 13 | }) 14 | -------------------------------------------------------------------------------- /src/main/index.dev.js: -------------------------------------------------------------------------------- 1 | import { app } from 'electron' 2 | import debug from 'electron-debug' 3 | import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer' 4 | import './index' 5 | 6 | app.on('ready', () => { 7 | installExtension(VUEJS_DEVTOOLS).catch(err => { 8 | console.log('Unable to install `vue-devtools`: \n', err) 9 | }) 10 | debug({ showDevTools: 'undocked' }) 11 | }) 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | parser: 'babel-eslint' 5 | }, 6 | globals: { 7 | angular: 'readonly' 8 | }, 9 | env: { 10 | node: true, 11 | browser: true 12 | }, 13 | extends: ['standard', 'plugin:vue/essential'], 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | image: Visual Studio 2017 3 | platform: Any CPU 4 | environment: 5 | nodejs_version: 10 6 | GH_TOKEN: 7 | secure: xdnnENSuvDPoe93wuR2RayHBr4opZ+r6YGbMh68EZAkqSCk049zMHTmiOq6wpG1t 8 | 9 | cache: 10 | - node_modules 11 | 12 | install: 13 | - ps: Install-Product node $env:nodejs_version 14 | - npm install 15 | 16 | build_script: 17 | - npm run lint 18 | - npm run build 19 | - npm run release 20 | 21 | branches: 22 | only: 23 | - master 24 | -------------------------------------------------------------------------------- /src/main/shortcut.js: -------------------------------------------------------------------------------- 1 | import { globalShortcut } from 'electron' 2 | 3 | export default dingtalk => () => { 4 | const actions = { 5 | 'shortcut-capture': () => dingtalk.shortcutCapture() 6 | } 7 | const keymap = dingtalk.setting.keymap 8 | 9 | if (!dingtalk.setting.enableCapture) delete actions['shortcut-capture'] 10 | 11 | // 注销所有的快捷键 12 | globalShortcut.unregisterAll() 13 | Object.keys(actions).forEach(key => { 14 | if (keymap[key] && keymap[key].length) { 15 | globalShortcut.register(keymap[key].join('+'), actions[key]) 16 | } 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /src/preload/mainWin/open.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 劫持window.open 3 | */ 4 | export default injector => { 5 | const op = window.open 6 | const iframe = document.createElement('iframe') 7 | iframe.style.display = 'none' 8 | document.body.append(iframe) 9 | 10 | window.open = function (url, ...args) { 11 | if (url.indexOf('https://space.dingtalk.com/auth/download') === 0) { 12 | iframe.src = url 13 | } else if (url.indexOf('https://space.dingtalk.com/attachment') === 0) { 14 | iframe.src = url 15 | } 16 | return op.call(window, url, ...args) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/settingWin/index.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import Keybinding from './components/keybinding' 6 | import Switch from './components/switch' 7 | import Button from './components/button' 8 | 9 | Vue.config.productionTip = false 10 | 11 | Vue.use(Switch) 12 | Vue.use(Keybinding) 13 | Vue.use(Button) 14 | 15 | /* eslint-disable no-new */ 16 | new Vue({ 17 | el: '#app', 18 | components: { App }, 19 | template: '' 20 | }) 21 | -------------------------------------------------------------------------------- /src/preload/emailWin/index.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer, webFrame } = require('electron') 2 | 3 | class EmailWinInjector { 4 | constructor () { 5 | this.init() 6 | } 7 | 8 | // 初始化 9 | init () { 10 | ipcRenderer.on('dom-ready', () => { 11 | this.injectJs() 12 | }) 13 | } 14 | 15 | // 注入JS 16 | injectJs () { 17 | this.setZoomLevel() 18 | } 19 | 20 | setZoomLevel () { 21 | // 设置缩放限制 22 | webFrame.setZoomFactor(100) 23 | webFrame.setZoomLevel(0) 24 | webFrame.setVisualZoomLevelLimits(1, 1) 25 | } 26 | } 27 | 28 | /* eslint-disable no-new */ 29 | new EmailWinInjector() 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | 4 | language: node_js 5 | node_js: 6 | - '10' 7 | 8 | matrix: 9 | include: 10 | - os: linux 11 | before_install: # 为了支持打包rpm格式的包 12 | - sudo apt-get update 13 | - sudo apt-get install --no-install-recommends -y gcc-multilib g++-multilib 14 | - sudo apt-get install --no-install-recommends -y rpm 15 | - os: osx 16 | 17 | cache: 18 | directories: 19 | - node_modules 20 | 21 | install: 22 | - yarn install 23 | 24 | script: 25 | - yarn lint 26 | - yarn build 27 | - yarn release 28 | 29 | branches: 30 | only: 31 | - master 32 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "cwd": "${workspaceRoot}", 12 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 13 | "windows": { 14 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" 15 | }, 16 | "args": ["."] 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/preload/mainWin/openEmail.js: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron' 2 | 3 | export default () => { 4 | document.addEventListener('click', e => { 5 | const $email = document.querySelector('#menu-pannel > ul.extra-options.ng-scope > div > org-email > li') 6 | 7 | if (!$email) return 8 | if (!$email.contains(e.target)) return 9 | 10 | const key = Object.keys(localStorage).find(key => /^\d+_mailUrl/.test(key)) 11 | if (!key) return 12 | const url = localStorage.getItem(key) 13 | if (!url) return 14 | // 停止事件冒泡和默认事件 15 | e.stopPropagation() 16 | e.preventDefault() 17 | ipcRenderer.send('MAINWIN:open-email', url) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/main/notify.js: -------------------------------------------------------------------------------- 1 | import logo from './logo' 2 | import Events from 'events' 3 | import { Notification } from 'electron' 4 | 5 | export default class Notify extends Events { 6 | $notify = null 7 | /** 8 | * 显示提示 9 | * @param {String} body 10 | */ 11 | show (body) { 12 | this.close() 13 | this.$notify = new Notification({ 14 | title: '钉钉', 15 | body, 16 | icon: logo 17 | }) 18 | this.$notify.on('click', () => { 19 | this.close() 20 | this.emit('click') 21 | }) 22 | this.$notify.show() 23 | } 24 | 25 | /** 26 | * 关闭提示 27 | */ 28 | close () { 29 | if (this.$notify) { 30 | this.$notify.close() 31 | this.$notify = null 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/preload/mainWin/notify.js: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron' 2 | 3 | let notify 4 | function msg (body) { 5 | notify = new Notification('钉钉', { 6 | body: body, 7 | icon: 'https://g.alicdn.com/dingding/web/0.1.8/img/logo.png' 8 | }) 9 | notify.addEventListener('click', () => { 10 | ipcRenderer.send('MAINWIN:window-show') 11 | }) 12 | } 13 | 14 | export default message => { 15 | if (notify) notify.close() 16 | if (Notification.permission === 'granted') { 17 | msg(message) 18 | } else if (Notification.permission !== 'denied') { 19 | Notification.requestPermission(permission => { 20 | // 如果用户同意,就可以向他们发送通知 21 | if (permission === 'granted') { 22 | msg(message) 23 | } 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/preload/mainWin/winOperation.js: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron' 2 | 3 | export default () => { 4 | const $ul = document.createElement('ul') 5 | $ul.setAttribute('class', 'dingtalk-window-operations') 6 | 7 | // 按钮className,同时也是事件名称 8 | const li = ['window-close', 'window-maximization', 'window-minimize'] 9 | li.forEach(item => { 10 | // 创建按钮 11 | const $li = document.createElement('li') 12 | $li.setAttribute('class', `operation-button ${item}`) 13 | $ul.appendChild($li) 14 | 15 | // 点击按钮通知主进程 16 | $li.addEventListener('click', () => ipcRenderer.send(`MAINWIN:${item}`)) 17 | }) 18 | // 把生成的按钮添加到DOM 19 | const $layoutContainer = document.querySelector('#layout-container') 20 | if ($layoutContainer) { 21 | document.body.insertBefore($ul, $layoutContainer.nextSibling) 22 | } else { 23 | document.body.appendChild($ul) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 nashaofu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | dist/ 61 | release/ 62 | -------------------------------------------------------------------------------- /src/preload/mainWin/notifyMessage.js: -------------------------------------------------------------------------------- 1 | import notify from './notify' 2 | import { ipcRenderer } from 'electron' 3 | 4 | export default injector => { 5 | let oldCount = 0 6 | injector.setTimer(() => { 7 | let count = 0 8 | const $mainMenus = document.querySelector('#menu-pannel>.main-menus') 9 | if ($mainMenus) { 10 | const $menuItems = $mainMenus.querySelectorAll('li.menu-item') 11 | $menuItems.forEach($item => { 12 | const $unread = $item.querySelector('all-conv-unread-count em.ng-binding') 13 | if ($unread) { 14 | const badge = parseInt($unread.innerText) 15 | count += isNaN(badge) ? 0 : badge 16 | } 17 | }) 18 | } 19 | if (oldCount !== count) { 20 | // 当有新消息来时才发送提示信息 21 | if (count !== 0 && oldCount < count) { 22 | const msg = `您有${count}条消息未查收!` 23 | /** 24 | * 尝试修复linux消息导致系统崩溃问题 25 | * https://github.com/nashaofu/dingtalk/issues/176 26 | */ 27 | if (process.platform === 'linux') { 28 | notify(msg) 29 | } else { 30 | ipcRenderer.send('notify', msg) 31 | } 32 | } 33 | oldCount = count 34 | ipcRenderer.send('MAINWIN:badge', count) 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /src/main/logo.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { app, screen } from 'electron' 3 | 4 | export default path.join(app.getAppPath(), './resources/logo.png') 5 | 6 | /** 7 | * 没有消息时的托盘图标 8 | */ 9 | export function getNoMessageTrayIcon () { 10 | if (process.platform === 'darwin') { 11 | return path.join(app.getAppPath(), './resources/tray/16x16.png') 12 | } else if (process.platform === 'win32') { 13 | return path.join(app.getAppPath(), './resources/tray/64x64.png') 14 | } else if (screen.getPrimaryDisplay().scaleFactor > 1) { 15 | return path.join(app.getAppPath(), './resources/tray/64x64.png') 16 | } else { 17 | return path.join(app.getAppPath(), './resources/tray/20x20.png') 18 | } 19 | } 20 | 21 | /** 22 | * 有消息时的托盘图标 23 | */ 24 | export function getMessageTrayIcon () { 25 | if (process.platform === 'darwin') { 26 | return path.join(app.getAppPath(), './resources/tray/n-16x16.png') 27 | } else if (process.platform === 'win32') { 28 | return path.join(app.getAppPath(), './resources/tray/n-64x64.png') 29 | } else if (screen.getPrimaryDisplay().scaleFactor > 1) { 30 | return path.join(app.getAppPath(), './resources/tray/n-64x64.png') 31 | } else { 32 | return path.join(app.getAppPath(), './resources/tray/n-20x20.png') 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/autoUpdate.js: -------------------------------------------------------------------------------- 1 | import { 2 | app, 3 | dialog, 4 | shell 5 | } from 'electron' 6 | import axios from 'axios' 7 | import { autoUpdater } from 'electron-updater' 8 | 9 | export default dingtalk => () => { 10 | autoUpdater.on('update-downloaded', info => { 11 | dialog.showMessageBox(dingtalk.$mainWin, { 12 | type: 'question', 13 | title: '立即更新', 14 | message: `新版本${info.version}已经下载完成,是否立即更新?`, 15 | noLink: true, 16 | buttons: ['是', '否'] 17 | }, index => { 18 | if (index === 0) { 19 | autoUpdater.quitAndInstall() 20 | } 21 | }) 22 | }) 23 | 24 | autoUpdater.on('error', e => { 25 | axios.get('https://api.github.com/repos/nashaofu/dingtalk/releases/latest') 26 | .then(({ data }) => { 27 | // 检查版本号 28 | // 如果本地版本小于远程版本则更新 29 | if (data.tag_name.slice(1) > app.getVersion()) { 30 | dialog.showMessageBox(dingtalk.$mainWin, { 31 | type: 'question', 32 | title: '版本更新', 33 | message: '已有新版本更新,是否立即前往下载最新安装包?', 34 | noLink: true, 35 | buttons: ['是', '否'] 36 | }, index => { 37 | if (index === 0) { 38 | shell.openExternal('https://github.com/nashaofu/dingtalk/releases/latest') 39 | } 40 | }) 41 | } 42 | }) 43 | }) 44 | 45 | if (dingtalk.setting.autoupdate) { 46 | autoUpdater.checkForUpdates() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/download.js: -------------------------------------------------------------------------------- 1 | import { app } from 'electron' 2 | 3 | export default $win => { 4 | // 文件下载拦截 5 | const files = [] 6 | $win.webContents.session.on('will-download', (event, item, webContents) => { 7 | const time = Date.now() 8 | const clientId = `${time}_${files.filter(({ clientId }) => clientId.indexOf(time) === 0).length}` 9 | files.push({ clientId, item }) 10 | const file = { 11 | clientId, 12 | name: item.getFilename(), 13 | fileSize: item.getTotalBytes(), 14 | finishSize: item.getReceivedBytes(), 15 | url: item.getURL(), 16 | state: item.getState() 17 | } 18 | if (!$win.isDestroyed()) { 19 | webContents.send('MAINWIN:download-start', file) 20 | } 21 | 22 | // 监听下载过程,计算并设置进度条进度 23 | item.on('updated', (e, state) => { 24 | file.state = state 25 | file.finishSize = item.getReceivedBytes() 26 | if (!$win.isDestroyed()) { 27 | webContents.send('MAINWIN:download-updated', file) 28 | $win.setProgressBar(file.finishSize / file.fileSize) 29 | } 30 | }) 31 | 32 | // 监听下载结束事件 33 | item.on('done', (e, state) => { 34 | file.state = state 35 | file.finishSize = item.getReceivedBytes() 36 | if (!$win.isDestroyed()) { 37 | webContents.send('MAINWIN:download-done', file) 38 | $win.setProgressBar(-1) 39 | } 40 | if (app.dock) { 41 | app.dock.bounce('informational') 42 | } 43 | }) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/main/errorWin.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import logo from './logo' 3 | import { app, BrowserWindow, ipcMain } from 'electron' 4 | 5 | export default dingtalk => () => { 6 | if (dingtalk.$errorWin) { 7 | dingtalk.$errorWin.show() 8 | dingtalk.$errorWin.focus() 9 | return dingtalk.$errorWin 10 | } 11 | 12 | const $win = new BrowserWindow({ 13 | title: '网络错误', 14 | width: 600, 15 | height: 320, 16 | useContentSize: true, 17 | resizable: false, 18 | center: true, 19 | frame: false, 20 | menu: false, 21 | transparent: true, 22 | show: false, 23 | closable: false, 24 | skipTaskbar: true, 25 | icon: logo, 26 | webPreferences: { 27 | nodeIntegration: true 28 | } 29 | }) 30 | 31 | $win.on('ready-to-show', () => { 32 | $win.show() 33 | $win.focus() 34 | }) 35 | 36 | $win.on('closed', () => { 37 | dingtalk.$errorWin = null 38 | }) 39 | 40 | ipcMain.on('ERRORWIN:retry', () => { 41 | dingtalk.hideErrorWin() 42 | if (dingtalk.$mainWin) { 43 | dingtalk.$mainWin.reload() 44 | dingtalk.showMainWin() 45 | } 46 | }) 47 | 48 | ipcMain.on('ERRORWIN:close', () => { 49 | dingtalk.hideErrorWin() 50 | }) 51 | 52 | // 加载URL地址 53 | const URL = 54 | process.env.NODE_ENV === 'development' 55 | ? 'http://localhost:8080/errorWin.html' 56 | : `file://${path.join(app.getAppPath(), './dist/renderer/errorWin.html')}` 57 | 58 | $win.loadURL(URL) 59 | return $win 60 | } 61 | -------------------------------------------------------------------------------- /src/main/aboutWin.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import logo from './logo' 3 | import contextMenu from './contextMenu' 4 | import { autoUpdater } from 'electron-updater' 5 | import { app, BrowserWindow, ipcMain } from 'electron' 6 | 7 | export default dingtalk => () => { 8 | if (dingtalk.$aboutWin) { 9 | dingtalk.$aboutWin.show() 10 | dingtalk.$aboutWin.focus() 11 | return dingtalk.$aboutWin 12 | } 13 | const $win = new BrowserWindow({ 14 | title: '关于', 15 | width: 320, 16 | height: 400, 17 | useContentSize: true, 18 | resizable: false, 19 | menu: false, 20 | parent: dingtalk.$mainWin, 21 | modal: process.platform !== 'darwin', 22 | show: false, 23 | icon: logo, 24 | webPreferences: { 25 | nodeIntegration: true 26 | } 27 | }) 28 | 29 | $win.on('ready-to-show', () => { 30 | $win.show() 31 | $win.focus() 32 | }) 33 | 34 | // 窗口关闭后手动让$window为null 35 | $win.on('closed', () => { 36 | dingtalk.$aboutWin = null 37 | }) 38 | 39 | $win.webContents.on('dom-ready', () => { 40 | if (!$win.webContents.isDestroyed()) $win.webContents.send('dom-ready') 41 | }) 42 | 43 | // 右键上下文菜单 44 | $win.webContents.on('context-menu', (e, params) => { 45 | e.preventDefault() 46 | contextMenu($win, params) 47 | }) 48 | 49 | ipcMain.on('ABOUTWIN:checkForUpdates', () => { 50 | autoUpdater.checkForUpdates() 51 | }) 52 | 53 | // 加载URL地址 54 | const URL = process.env.NODE_ENV === 'development' 55 | ? 'http://localhost:8080/aboutWin.html' 56 | : `file://${path.join(app.getAppPath(), './dist/renderer/aboutWin.html')}` 57 | 58 | $win.loadURL(URL) 59 | return $win 60 | } 61 | -------------------------------------------------------------------------------- /src/main/setting.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { app } from 'electron' 4 | 5 | /** 6 | * 初始化设置选项 7 | */ 8 | export const initSetting = dingtalk => () => { 9 | const filename = path.join(app.getPath('userData'), 'setting.json') 10 | return new Promise((resolve, reject) => { 11 | fs.access(filename, fs.constants.R_OK | fs.constants.W_OK, async err => { 12 | if (err) { 13 | if (err.code === 'ENOENT') { 14 | return resolve(await dingtalk.writeSetting()) 15 | } else { 16 | return reject(err) 17 | } 18 | } 19 | resolve(await dingtalk.readSetting()) 20 | }) 21 | }) 22 | } 23 | 24 | /** 25 | * 从文件中读取设置信息 26 | */ 27 | export const readSetting = dingtalk => () => { 28 | const filename = path.join(app.getPath('userData'), 'setting.json') 29 | return new Promise((resolve, reject) => { 30 | fs.readFile(filename, (err, data) => { 31 | if (err) return reject(err) 32 | try { 33 | const setting = JSON.parse(data) 34 | if (typeof setting.keymap['shortcut-capture'] === 'string') { 35 | setting.keymap['shortcut-capture'] = setting.keymap['shortcut-capture'].split('+') 36 | } 37 | resolve({ ...dingtalk.setting, ...setting }) 38 | } catch (e) { 39 | resolve(dingtalk.setting) 40 | } 41 | }) 42 | }) 43 | } 44 | 45 | /** 46 | * 写入设置到文件 47 | */ 48 | export const writeSetting = dingtalk => () => { 49 | const filename = path.join(app.getPath('userData'), 'setting.json') 50 | return new Promise((resolve, reject) => { 51 | fs.writeFile(filename, JSON.stringify(dingtalk.setting, null, 2), err => { 52 | if (err) return reject(err) 53 | resolve(dingtalk.setting) 54 | }) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /src/main/contextMenu.js: -------------------------------------------------------------------------------- 1 | import { Menu } from 'electron' 2 | 3 | export default ($win, params) => { 4 | // 菜单执行命令 5 | const menuCmd = { 6 | copy: { 7 | id: 1, 8 | label: '复制' 9 | }, 10 | cut: { 11 | id: 2, 12 | label: '剪切' 13 | }, 14 | paste: { 15 | id: 3, 16 | label: '粘贴' 17 | }, 18 | selectall: { 19 | id: 4, 20 | label: '全选' 21 | } 22 | } 23 | 24 | const { selectionText, isEditable, editFlags } = params 25 | 26 | // 生成菜单模板 27 | const template = Object.keys(menuCmd) 28 | .map(cmd => { 29 | const { id, label } = menuCmd[cmd] 30 | let enabled = false 31 | let visible = false 32 | const { canCopy, canCut, canPaste, canSelectAll } = editFlags 33 | switch (cmd) { 34 | case 'copy': 35 | // 有文字选中就显示 36 | visible = !!selectionText 37 | enabled = canCopy 38 | break 39 | case 'cut': 40 | // 可以编辑就显示项目 41 | visible = !!isEditable 42 | // 有文字选中才可用 43 | enabled = visible && !!selectionText && canCut 44 | break 45 | case 'paste': 46 | // 可以编辑就显示项目 47 | visible = !!isEditable 48 | enabled = visible && canPaste 49 | break 50 | case 'selectall': 51 | // 可以编辑就显示项目 52 | visible = !!isEditable 53 | enabled = visible && canSelectAll 54 | break 55 | default: 56 | break 57 | } 58 | return { 59 | id, 60 | label, 61 | role: cmd, 62 | enabled, 63 | visible 64 | } 65 | }) 66 | .filter(item => item.visible) 67 | .sort((a, b) => a.id > b.id) 68 | 69 | // 用模板生成菜单 70 | if (template.length && !$win.isDestroyed()) { 71 | const menu = Menu.buildFromTemplate(template) 72 | menu.popup($win) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/settingWin.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import logo from './logo' 3 | import contextMenu from './contextMenu' 4 | import { autoUpdater } from 'electron-updater' 5 | import { app, BrowserWindow, ipcMain } from 'electron' 6 | 7 | export default dingtalk => () => { 8 | if (dingtalk.$settingWin) { 9 | dingtalk.$settingWin.show() 10 | dingtalk.$settingWin.focus() 11 | return dingtalk.$settingWin 12 | } 13 | const $win = new BrowserWindow({ 14 | title: '设置', 15 | width: 320, 16 | height: 330, 17 | useContentSize: true, 18 | resizable: false, 19 | menu: false, 20 | parent: dingtalk.$mainWin, 21 | modal: process.platform !== 'darwin', 22 | show: false, 23 | icon: logo, 24 | webPreferences: { 25 | nodeIntegration: true 26 | } 27 | }) 28 | 29 | $win.on('ready-to-show', () => { 30 | $win.show() 31 | $win.focus() 32 | }) 33 | 34 | // 窗口关闭后手动让$window为null 35 | $win.on('closed', () => { 36 | dingtalk.$settingWin = null 37 | }) 38 | 39 | $win.webContents.on('dom-ready', () => { 40 | $win.webContents.send('dom-ready', dingtalk.setting) 41 | }) 42 | 43 | // 右键上下文菜单 44 | $win.webContents.on('context-menu', (e, params) => { 45 | e.preventDefault() 46 | contextMenu($win, params) 47 | }) 48 | 49 | ipcMain.on('SETTINGWIN:setting', async (e, setting) => { 50 | dingtalk.setting = setting 51 | await dingtalk.writeSetting() 52 | dingtalk.bindShortcut() 53 | dingtalk.resetTrayMenu() 54 | if (dingtalk.setting.autoupdate) { 55 | autoUpdater.checkForUpdates() 56 | } 57 | dingtalk.hideSettingWin() 58 | }) 59 | 60 | // 加载URL地址 61 | const URL = process.env.NODE_ENV === 'development' 62 | ? 'http://localhost:8080/settingWin.html' 63 | : `file://${path.join(app.getAppPath(), './dist/renderer/settingWin.html')}` 64 | 65 | $win.loadURL(URL) 66 | return $win 67 | } 68 | -------------------------------------------------------------------------------- /src/preload/mainWin/utils.js: -------------------------------------------------------------------------------- 1 | export const transFileClassName = filename => { 2 | let extension = '' 3 | if (filename) { 4 | const index = filename.lastIndexOf('.') 5 | extension = index >= 0 ? filename.substring(index + 1).toLowerCase() : '' 6 | } 7 | const audio = { 8 | mp3: 1, 9 | mp2: 1, 10 | ogg: 1, 11 | wav: 1, 12 | m4a: 1, 13 | ape: 1, 14 | mid: 1, 15 | aac: 1, 16 | au: 1, 17 | wma: 1, 18 | flac: 1, 19 | ac3: 1 20 | } 21 | const video = { 22 | aiff: 1, 23 | avi: 1, 24 | mov: 1, 25 | mp4: 1, 26 | mpeg: 1, 27 | mpg: 1, 28 | qt: 1, 29 | ram: 1, 30 | viv: 1, 31 | wmv: 1, 32 | rm: 1, 33 | rmvb: 1, 34 | mkv: 1 35 | } 36 | const img = { 37 | gif: 1, 38 | bmp: 1, 39 | png: 1, 40 | jpg: 1, 41 | jpeg: 1, 42 | ico: 1, 43 | tiff: 1, 44 | tif: 1, 45 | tga: 1 46 | } 47 | const files = { 48 | pdf: 1, 49 | ai: 1, 50 | doc: 1, 51 | docx: 1, 52 | ppt: 1, 53 | pptx: 1, 54 | psd: 1, 55 | rar: 1, 56 | txt: 1, 57 | xls: 1, 58 | xlsx: 1, 59 | zip: 1 60 | } 61 | if (!extension) { 62 | return 'ico_file_unknown' 63 | } 64 | extension = extension.toLowerCase() 65 | if (audio[extension]) { 66 | return 'ico_file_audio' 67 | } else if (video[extension]) { 68 | return 'ico_file_video' 69 | } else if (img[extension]) { 70 | return 'ico_file_img' 71 | } else if (files[extension]) { 72 | return `ico_file_${extension}` 73 | } else { 74 | return 'ico_file_unknown' 75 | } 76 | } 77 | 78 | export const formatFileSize = size => { 79 | const units = ['B', 'KB', 'MB', 'GB', 'TB'] 80 | if (size === 0) return '0 B' 81 | const index = parseInt(Math.floor(Math.log(size) / Math.log(1024))) 82 | let fileSize = (Math.round(10 * size / Math.pow(1024, index)) / 10).toString() 83 | if (fileSize.indexOf('.') === -1) { 84 | fileSize += '.0' 85 | } 86 | return `${fileSize} ${units[index]}` 87 | } 88 | -------------------------------------------------------------------------------- /src/renderer/settingWin/components/switch/switch.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 46 | 47 | 97 | -------------------------------------------------------------------------------- /src/renderer/settingWin/components/button/button.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 50 | 51 | 89 | -------------------------------------------------------------------------------- /src/preload/mainWin/index.js: -------------------------------------------------------------------------------- 1 | import open from './open' 2 | import download from './download' 3 | import openEmail from './openEmail' 4 | import winOperation from './winOperation' 5 | import notifyMessage from './notifyMessage' 6 | import { ipcRenderer, webFrame } from 'electron' 7 | 8 | import './css.less' 9 | 10 | class MainWinInjector { 11 | constructor () { 12 | // timer循环数据 13 | this.callback = [] 14 | this.timer = setInterval(() => { 15 | this.callback.forEach(item => item()) 16 | }, 1000) 17 | this.init() 18 | } 19 | 20 | // 初始化 21 | init () { 22 | ipcRenderer.on('dom-ready', () => { 23 | ipcRenderer.send('online', navigator.onLine) 24 | if (!navigator.onLine) { 25 | return 26 | } 27 | this.injectJs() 28 | }) 29 | } 30 | 31 | // 注入JS 32 | injectJs () { 33 | this.setZoomLevel() 34 | /** 35 | * 插入窗口操作按钮 36 | * 关闭/最大化/最小化 37 | */ 38 | this.winOperation() 39 | 40 | /** 41 | * 劫持window.open 42 | */ 43 | 44 | this.open() 45 | 46 | /** 47 | * 检测是否有未读消息 48 | * 发送未读消息条数到主进程 49 | */ 50 | this.notifyMessage() 51 | 52 | /** 53 | * 打开邮箱界面 54 | */ 55 | this.openEmail() 56 | /** 57 | * 文件下载监听 58 | */ 59 | this.download() 60 | } 61 | 62 | // 设置缩放等级 63 | setZoomLevel () { 64 | // 设置缩放限制 65 | webFrame.setZoomFactor(100) 66 | webFrame.setZoomLevel(0) 67 | webFrame.setVisualZoomLevelLimits(1, 1) 68 | } 69 | 70 | setTimer (callback) { 71 | this.callback.push(callback) 72 | } 73 | 74 | // 插入窗口操作按钮 75 | winOperation () { 76 | winOperation(this) 77 | } 78 | 79 | // 消息通知发送到主进程 80 | notifyMessage () { 81 | notifyMessage(this) 82 | } 83 | 84 | // 打开邮箱 85 | openEmail () { 86 | openEmail(this) 87 | } 88 | 89 | // 文件下载劫持 90 | download () { 91 | download(this) 92 | } 93 | 94 | // window.open重写 95 | open () { 96 | open(this) 97 | } 98 | } 99 | 100 | /* eslint-disable no-new */ 101 | new MainWinInjector() 102 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | 3 | 对于想要给项目提交 pr 的小伙伴,请关注一下以下内容,这里将介绍项目的相关结构,以帮助你更快的能够开发你想要的功能。贡献代码,你需要掌握[electron](https://github.com/electron/electron)、[vue](https://github.com/vuejs/vue)、[webpack](https://github.com/webpack/webpack)、[node](https://github.com/nodejs/node)相关知识,并且对操作系统有一定的了解。 4 | 5 | ## 项目结构说明 6 | 7 | 1. 项目整体划分:项目整体划分为 3 个部分,分别为**main**、**preload**、**renderer**,三个部分分别为: 8 | 9 | - **main**: 主进程相关内容 10 | - **preload**: 由于本程序的主要界面是直接基于钉钉的网页版做的,所以再网页版页面做成的主窗口界面的功能都是通过动态注入 js 实现的,所以这一部分都被归类为**preload**,其中涉及了两个窗口,程序主窗口和钉邮窗口 11 | - **renderer**: 该部分的页面是钉钉本来没有,在由网页版改为桌面版的过程中,为了满足部分功能需要,而添加的窗口,包含关于、设置和网络错误窗口 12 | 13 | 2. 项目窗口:项目中窗口主要包含聊天主窗口(mainWin)、钉邮(emailWin)、设置(settingWin)、关于(aboutWin)和网络错误窗口(errorWin),其中 mainWin 和 emailWin 的渲染进程代码位于**preload**文件夹下,且是采用原生 js 编写,其余几个窗口都位于**renderer**文件夹下,都是采用 vue 编写 14 | 3. 文件夹说明: 15 | 16 | - **build**文件夹:该文件夹主要是 webpack 打包相关的配置文件,子目录分别对应项目整体划分的 3 各部分 17 | - **icon**文件夹:该文件夹是存放钉钉图标文件的,除了会在项目源码中引用,还会在编译为各个平台安装包的时候作为程序 icon 18 | - **screenshot**文件夹:该文件夹是用来存放程序截图的文件夹,开发中几乎不会使用到 19 | - **src**文件夹:该文件夹为项目的源码文件,包含了主进程和渲染进程的全部代码 20 | 21 | 4. 编译打包:编译打包使用了[electron-builder](https://github.com/electron-userland/electron-builder)这个模块,用了这个模块之后可以配合**electron-updater**提供自动升级功能,可以说炒鸡好用 22 | 23 | ## 代码风格: 24 | 25 | 1. 在主进程(main)和 preload 中,为了减小单个文件的大小,并且合理管理代码,所有的功能模块都是通过**高阶函数**的方式加载进来的,使用高阶函数的目的是为了让我们在所有的模块代码中都可以访问到 dingtalk 对象 26 | 2. 主进程的入口文件在调试环境时使用**index.dev.js**,正式环境使用**index.js** 27 | 3. rendder 部分和普通 vue 项目一样,具体请参考 vue 官方提出的编码风格指南 28 | 4. 在所有的进程中,如果需要访问图片资源或者其他资源,请通过`path.join(app.getAppPath(), './icon/32x32.png')`的方式来获取,因为打包之后相对的目录结构不能保证一致,所以请务必注意 29 | 5. 编码时请使用 ES6 语法,并遵守 ESLint 的规则,在提交 pr 的时候请尽量在代码中补充好注释,并去掉不必要的代码,也好减轻 review 的工作量 30 | 31 | ## pr 流程 32 | 33 | 1. 请在提交 pr 的时候详细说明修改的内容 34 | 2. 在提交 pr 之前,请同步代码到最新,不要提交很久以前版本的代码过来 35 | 3. pr 提交代码,请提交到**dev**分支,因为**dev**分支是最新的 36 | 37 | ## 关于 Issues 38 | 39 | 1. 能够提供上图的尽量上图 40 | 2. 描述尽可能详细一点,这样我们这边才能更快的了解情况,减少反复交流的时间成本 41 | 42 | ## 调试主进程说明 43 | 44 | 1. 进入项目根目录下 45 | 2. 修改`build/main/webpack.dev.conf.js`,注释掉`ElectronDevWebpackPlugin`插件,修改`devtool`以申城 sourcemap 方便调试 46 | 3. 运行`npm run dev`,服务都启动后,在`dist`文件夹下找到`main.js`,在文件中随意打一个断点,然后在`vscode`启动调试,就可以在源码中打断点了。**注意:一定先要在`dist/main.js`中打一个端点,否则`vscode`不能捕获在源码中的断点** 47 | 48 | 以上内容比较简陋,很多地方都没能说得很详细,如有疑问,欢迎直接交流。写得不好的地方也欢迎修改 49 | -------------------------------------------------------------------------------- /src/preload/mainWin/css.less: -------------------------------------------------------------------------------- 1 | body { 2 | border: 1px solid rgba(90, 131, 183, 0.3); 3 | } 4 | 5 | #layout-main { 6 | width: 100% !important; 7 | height: 100% !important; 8 | flex: 0 1 100% !important; 9 | } 10 | 11 | #header { 12 | display: block !important; 13 | 14 | // 登录时去掉背景 15 | &.login-header { 16 | background: none; 17 | } 18 | 19 | > upload-list > .upload-container-wrap.ng-isolate-scope { 20 | margin-right: 96px !important; 21 | } 22 | } 23 | 24 | #body { 25 | height: 100% !important; 26 | flex: 0 1 100% !important; 27 | } 28 | 29 | @font-face { 30 | font-family: 'dingtalk-font'; 31 | src: url('//at.alicdn.com/t/font_zib5yatbnvw5qaor.eot?t=1498576064752'); 32 | src: url('//at.alicdn.com/t/font_zib5yatbnvw5qaor.eot?t=1498576064752#iefix') format('embedded-opentype'), 33 | url('//at.alicdn.com/t/font_zib5yatbnvw5qaor.woff?t=1498576064752') format('woff'), 34 | url('//at.alicdn.com/t/font_zib5yatbnvw5qaor.ttf?t=1498576064752') format('truetype'), 35 | url('//at.alicdn.com/t/font_zib5yatbnvw5qaor.svg?t=1498576064752#iconfont') format('svg'); 36 | } 37 | 38 | ul.dingtalk-window-operations { 39 | position: absolute; 40 | top: 0; 41 | right: 0; 42 | padding: 7px 3px; 43 | z-index: 1; 44 | 45 | > li { 46 | font-family: 'dingtalk-font' !important; 47 | font-style: normal; 48 | -webkit-font-smoothing: antialiased; 49 | float: right; 50 | width: 23px; 51 | height: 23px; 52 | line-height: 23px; 53 | margin: 0 1px; 54 | font-size: 14px; 55 | text-align: center; 56 | color: #666; 57 | cursor: pointer; 58 | -webkit-app-region: no-drag; 59 | 60 | &:hover { 61 | color: #1f1f1f; 62 | font-weight: 400; 63 | } 64 | 65 | &.window-minimize:before { 66 | content: '\e776'; 67 | } 68 | 69 | &.window-maximization:before { 70 | content: '\e777'; 71 | } 72 | 73 | &.window-close:before { 74 | content: '\e778'; 75 | } 76 | } 77 | } 78 | 79 | .login-form { 80 | &.login-tab { 81 | height: 360px; 82 | } 83 | .password-login .offline-announcement { 84 | top: 310px; 85 | } 86 | } 87 | .ding-modal { 88 | .login-form { 89 | height: 310px; 90 | } 91 | } 92 | 93 | .client-download-guide { 94 | display: none; 95 | } 96 | -------------------------------------------------------------------------------- /src/main/emailWin.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import logo from './logo' 4 | import contextMenu from './contextMenu' 5 | import { app, BrowserWindow } from 'electron' 6 | 7 | export default dingtalk => url => { 8 | if (dingtalk.$emailWin) { 9 | dingtalk.$emailWin.show() 10 | dingtalk.$emailWin.focus() 11 | return dingtalk.$emailWin 12 | } 13 | if (!url) return 14 | const $win = new BrowserWindow({ 15 | title: '钉邮', 16 | width: 980, 17 | height: 640, 18 | minWidth: 720, 19 | minHeight: 450, 20 | useContentSize: true, 21 | resizable: true, 22 | menu: false, 23 | show: false, 24 | icon: logo, 25 | webPreferences: { 26 | nodeIntegration: true 27 | } 28 | }) 29 | 30 | $win.on('ready-to-show', () => { 31 | $win.show() 32 | $win.focus() 33 | }) 34 | 35 | // 窗口关闭后手动让$window为null 36 | $win.on('closed', () => { 37 | dingtalk.$emailWin = null 38 | }) 39 | 40 | $win.webContents.on('dom-ready', () => { 41 | dingtalk.$mainWin.webContents.session.cookies.get({ domain: '.dingtalk.com' }, (err, cookies) => { 42 | if (err) return 43 | cookies.forEach(cookie => { 44 | if (cookie.domain !== '.dingtalk.com') return 45 | $win.webContents.session.cookies.set( 46 | { 47 | ...cookie, 48 | url: 'https://mail.dingtalk.com' 49 | }, 50 | err => { 51 | // 回调函数为必传,否则会报错 52 | console.log('dingtalk emailWin cookies log:', err) 53 | } 54 | ) 55 | }) 56 | }) 57 | 58 | const filename = path.join(app.getAppPath(), './dist/preload/emailWin.js') 59 | // 读取js文件并执行 60 | fs.access(filename, fs.constants.R_OK, err => { 61 | if (err) return 62 | fs.readFile(filename, (error, data) => { 63 | if (error || $win.webContents.isDestroyed()) return 64 | $win.webContents.executeJavaScript(data.toString(), () => { 65 | if (!$win.webContents.isDestroyed()) $win.webContents.send('dom-ready') 66 | }) 67 | }) 68 | }) 69 | }) 70 | 71 | // 右键菜单 72 | $win.webContents.on('context-menu', (e, params) => { 73 | e.preventDefault() 74 | contextMenu($win, params) 75 | }) 76 | 77 | // 加载URL地址 78 | $win.loadURL(url) 79 | return $win 80 | } 81 | -------------------------------------------------------------------------------- /src/main/dingtalkTray.js: -------------------------------------------------------------------------------- 1 | import { Tray, Menu } from 'electron' 2 | import { getMessageTrayIcon, getNoMessageTrayIcon } from './logo' 3 | 4 | export default class DingtalkTray { 5 | _dingtalk = null 6 | // 图标闪烁定时 7 | _flickerTimer = null 8 | 9 | // 托盘对象 10 | $tray = null 11 | // 图标文件 12 | messageTrayIcon = getMessageTrayIcon() 13 | noMessageTrayIcon = getNoMessageTrayIcon() 14 | 15 | constructor ({ dingtalk }) { 16 | this._dingtalk = dingtalk 17 | // 生成托盘图标及其菜单项实例 18 | this.$tray = new Tray(this.noMessageTrayIcon) 19 | // 设置鼠标悬浮时的标题 20 | this.$tray.setToolTip('钉钉') 21 | this.initEvent() 22 | this.setMenu() 23 | } 24 | 25 | /** 26 | * 初始化事件 27 | */ 28 | initEvent () { 29 | this.$tray.on('click', () => this._dingtalk.showMainWin()) 30 | this.$tray.on('double-click', () => this._dingtalk.showMainWin()) 31 | } 32 | 33 | /** 34 | * 设置菜单 35 | */ 36 | setMenu () { 37 | const menu = [ 38 | { 39 | label: '显示窗口', 40 | click: () => this._dingtalk.showMainWin() 41 | }, 42 | { 43 | label: '设置', 44 | click: () => this._dingtalk.showSettingWin() 45 | }, 46 | { 47 | label: '关于', 48 | click: () => this._dingtalk.showAboutWin() 49 | }, 50 | { 51 | label: '退出', 52 | click: () => this._dingtalk.quit() 53 | } 54 | ] 55 | 56 | if (this._dingtalk.setting.enableCapture) { 57 | menu.splice(1, 0, { 58 | label: '屏幕截图', 59 | click: () => this._dingtalk.shortcutCapture() 60 | }) 61 | } 62 | 63 | // 绑定菜单 64 | this.$tray.setContextMenu(Menu.buildFromTemplate(menu)) 65 | } 66 | 67 | /** 68 | * 控制图标是否闪烁 69 | * @param {Boolean} is 70 | */ 71 | flicker (is) { 72 | const { enableFlicker } = this._dingtalk.setting 73 | if (is) { 74 | let icon = this.messageTrayIcon 75 | if (enableFlicker) { 76 | // 防止连续调用多次,导致图标切换时间间隔不是1000ms 77 | if (this._flickerTimer !== null) return 78 | this._flickerTimer = setInterval(() => { 79 | this.$tray.setImage(icon) 80 | icon = icon === this.messageTrayIcon ? this.noMessageTrayIcon : this.messageTrayIcon 81 | }, 1000) 82 | } else { 83 | this.$tray.setImage(icon) 84 | } 85 | } else { 86 | clearInterval(this._flickerTimer) 87 | this._flickerTimer = null 88 | this.$tray.setImage(this.noMessageTrayIcon) 89 | } 90 | } 91 | 92 | /** 93 | * 判断托盘是否销毁 94 | */ 95 | isDestroyed () { 96 | return this.$tray.isDestroyed() 97 | } 98 | 99 | /** 100 | * 销毁托盘图标 101 | */ 102 | destroy () { 103 | return this.$tray.destroy() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /dependencies.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const axios = require('axios') 3 | const chalk = require('chalk') 4 | const http = require('http') 5 | const https = require('https') 6 | const registryUrl = require('registry-url') 7 | const registryAuthToken = require('registry-auth-token') 8 | 9 | const httpAgent = new http.Agent({ 10 | keepAlive: true, 11 | maxSockets: 50 12 | }) 13 | const httpsAgent = new https.Agent({ 14 | keepAlive: true, 15 | maxSockets: 50 16 | }) 17 | 18 | /** 19 | * 把一个长的并发一步任务转换为 20 | * 一个切片形式的串行任务 21 | * @param {Array} tasks 任务数据 22 | * @return {Promise} Promise对象按切片执行结果 23 | */ 24 | function parallelToSerial (tasks) { 25 | const reslut = [] 26 | async function next () { 27 | // 如果数据执行完之后就直接返回 28 | if (!tasks.length) return reslut 29 | // 执行处理逻辑 30 | reslut.push(await tasks.shift()()) 31 | // 循环下一个切片 32 | await next() 33 | return reslut 34 | } 35 | return next() 36 | } 37 | 38 | /** 39 | * 拉取最新的包 40 | * @param {*} pkg 41 | * @param {*} pkgInfo 42 | */ 43 | async function getPackageVersion (pkg, pkgInfo) { 44 | console.log(`get ${pkg} ...`) 45 | const scope = pkg.split('/')[0] 46 | const registry = registryUrl(scope) 47 | const authInfo = registryAuthToken(registry, { recursive: true }) 48 | const headers = { 49 | accept: 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*' 50 | } 51 | 52 | if (authInfo) { 53 | headers.authorization = `${authInfo.type} ${authInfo.token}` 54 | } 55 | 56 | const time = Date.now() 57 | try { 58 | const { data } = await axios.get(`${encodeURIComponent(pkg).replace(/^%40/, '@')}/latest`, { 59 | baseURL: registry, 60 | headers, 61 | httpAgent, 62 | httpsAgent 63 | }) 64 | console.log( 65 | chalk.bgGreen.black(' DONE '), 66 | JSON.stringify( 67 | { 68 | ...pkgInfo, 69 | id: pkg, 70 | status: 200, 71 | time: Date.now() - time 72 | }, 73 | null, 74 | 2 75 | ) 76 | ) 77 | return data.version 78 | } catch (e) { 79 | const status = ((e || {}).response || {}).status 80 | console.log( 81 | chalk.bgRed.black(' ERROR '), 82 | JSON.stringify( 83 | { 84 | ...pkgInfo, 85 | id: pkg, 86 | status: status || e.response, 87 | time: Date.now() - time 88 | }, 89 | null, 90 | 2 91 | ) 92 | ) 93 | } 94 | } 95 | 96 | /** 97 | * 比较版本 98 | * @param {*} pkg 99 | * @param {*} type 100 | */ 101 | function diffVersion (pkg, type) { 102 | return Object.keys(pkg[type]).map(key => async () => { 103 | // 排除内部依赖 104 | const version = await getPackageVersion(key, { 105 | name: pkg.name, 106 | type, 107 | version: pkg[type][key] 108 | }) 109 | pkg[type][key] = version ? `^${version}` : pkg[type][key] 110 | }) 111 | } 112 | 113 | const pkg = require('./package.json') 114 | const dependencies = diffVersion(pkg, 'dependencies') 115 | const devDependencies = diffVersion(pkg, 'devDependencies') 116 | 117 | parallelToSerial(dependencies) 118 | .then(() => parallelToSerial(devDependencies)) 119 | .then(() => { 120 | fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2)) 121 | }) 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dingtalk[![Build Status](https://travis-ci.org/nashaofu/dingtalk.svg?branch=master)](https://travis-ci.org/nashaofu/dingtalk)[![Build status](https://ci.appveyor.com/api/projects/status/jptk80n78gdogd18/branch/master?svg=true)](https://ci.appveyor.com/project/nashaofu/dingtalk/branch/master) 2 | 3 | 钉钉桌面版,基于 electron 和钉钉网页版开发,支持 Windows、Linux 和 macOS 4 | 5 | ## 公司招人 6 | 7 | 招聘 react 技术栈的开发者,公司研发中心位于杭州,当前是北京团队招聘初级、中级和高级前端人员。高级或者中级工程师可培养为团队 leader,有带团队的经验加分,有意向的请私聊我,邮箱:diaocheng@outlook.com。 8 | 9 | ## 安装步骤 10 | 11 | > 直接从[GitHub releases](https://github.com/nashaofu/dingtalk/releases/latest)页面下载最新版安装包即可 12 | 13 | ## 国内仓库与版本安装包 14 | 15 | - 国内 git 地址:[https://gitee.com/nashaofu/dingtalk](https://gitee.com/nashaofu/dingtalk) 16 | - 安装包:[https://pan.baidu.com/s/12pM3fi5nphCdgGH9WAnXvw](https://pan.baidu.com/s/12pM3fi5nphCdgGH9WAnXvw) 17 | 18 | ### 特别说明,提 issue 请尽量到[GitHub](https://github.com/nashaofu/dingtalk),分别处理多个仓库实在精力有限 19 | 20 | ## 手动构建 21 | 22 | ```bash 23 | # 安装依赖 24 | # linux系统构建rpm请运行如下命令,否则可能会打包失败 25 | # sudo apt-get -qq update 26 | # sudo apt-get install --no-install-recommends -y gcc-multilib g++-multilib 27 | # sudo apt-get install --no-install-recommends -y rpm 28 | 29 | npm install 30 | 31 | # 打包源码 32 | npm run build 33 | 34 | # 生成安装包 35 | npm run pack 36 | ``` 37 | 38 | ## 贡献指南 39 | 40 | 非常欢迎有兴趣的小伙伴一起来贡献力量,我写了一份很简单的[贡献指南](./CONTRIBUTING.md),希望能帮助你快速上手 41 | 42 | ## 截图效果 43 | 44 | 1. 二维码登录页面 45 | ![1.png](./screenshot/1.png) 46 | 2. 账号密码登录页面 47 | ![2.png](./screenshot/2.png) 48 | 3. 登录后页面展示 49 | ![3.png](./screenshot/3.png) 50 | 4. 邮箱打开效果 51 | ![4.png](./screenshot/4.png) 52 | 5. 截图效果预览 53 | ![5.png](./screenshot/5.png) 54 | 6. 网络错误页面 55 | ![6.png](./screenshot/6.png) 56 | 7. 系统设置界面 57 | ![7.png](./screenshot/7.png) 58 | 8. 关于界面 59 | ![8.png](./screenshot/8.png) 60 | 61 | ## 功能说明 62 | 63 | 1. 本版本是基于网页版钉钉和 electron 制作的 64 | 2. 本版本与网页版的区别 65 | - 解决了网页版钉钉内容区域无法最大化的问题 66 | - 除了少数的功能未能够完全实现,其余的使用体验和 PC 版钉钉基本一致 67 | 3. 支持屏幕截图,并且支持多显示器截图。截图快捷键为`ctrl+alt+a` 68 | 4. 添加应用分类,[Linux 系统分类](https://specifications.freedesktop.org/menu-spec/latest/apa.html#main-category-registry) 69 | 5. 目前已经支持 Linux、macOS 和 Windows 三个平台 70 | 71 | ## 更新说明 72 | 73 | 1. 支持屏幕截图,并且支持多显示器截图。截图快捷键为`ctrl+alt+a`,2017-10-23 74 | 2. 支持网络错误页面提示,网络恢复自动跳转到登陆页面,2017-12-28 75 | 3. 修改网络错误页面,支持快捷键设置,2018-02-07 76 | 4. 更新截图功能,支持多显示器截图,目前确认支持 Ubuntu16,Ubuntu17 不支持,其他 Linux 系统未测试,其中使用了[shortcut-capture](https://github.com/nashaofu/shortcut-capture)模块来实现截图;修复设置页面不修改快捷键时,点击保存时提示错误的 BUG,2018-03-03 77 | 5. 整个项目采用 webpack 打包,采用 electron-builder 来构建应用,分别构建生成三大平台安装包,2018-03-22 78 | 6. 添加关于页面,文件下载进度支持,消息提示不弹出问题修复,修复 Linux 更新问题,2018-04-01 79 | 7. 修复消息提示 node-notifier 图标显示问题,2018-04-07 80 | 8. 修改消息提示太多不能关闭导致卡顿问题,支持 rpm 打包,升级截图工具,2018-05-30 81 | 9. 修复视频点击之后页面跳转问题,支持一下 Mac,升级一下 electron,2018-08-13 82 | 10. 支持自动更新检测设置 2018-03-09 83 | 11. 支持截图开启和关闭功能 2018-04-27 84 | 12. 支持新消息托盘图标闪烁开关设置 2018-07-04 85 | 86 | ## TODO 87 | 88 | - [x] 支持网络断开时显示错误页 89 | - [x] 添加关于页面 90 | - [x] 消息提示在 windows 上不出来的 BUG,或者替换为 node-notifier 模块 91 | - [x] windows 弹出下载提示问题 92 | - [ ] 邮箱打不开问题 93 | 94 | ## 关于支持加密信息的说明 95 | 96 | 加密信息暂不支持,详情请看[企业信息加密相关](https://github.com/nashaofu/dingtalk/issues/2),也欢迎各位朋友能够去研究一下,帮助实现这个功能 97 | 98 | ## 关于 Linux 程序占用资源过高的问题 99 | 100 | 程序托盘闪烁功能可能会导致占用资源过高,所以新版本可关闭新消息托盘闪烁功能 101 | 102 | ## 打赏 103 | 104 | 如果你觉得作者的辛苦付出有帮助到你,你可以给作者买杯咖啡!🤣 105 | ![打赏](./screenshot/reward.png) 106 | -------------------------------------------------------------------------------- /src/renderer/aboutWin/App.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 69 | 70 | 167 | -------------------------------------------------------------------------------- /src/renderer/settingWin/App.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 118 | 119 | 157 | -------------------------------------------------------------------------------- /src/main/mainWin.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import logo from './logo' 4 | import download from './download' 5 | import autoUpdate from './autoUpdate' 6 | import contextMenu from './contextMenu' 7 | import { app, BrowserWindow, shell, ipcMain } from 'electron' 8 | 9 | let lastUrl 10 | let time = Date.now() 11 | /** 12 | * 打开外部链接 13 | * @param {String} url 14 | */ 15 | function openExternal (url) { 16 | if (url === 'about:blank') return 17 | if (url === 'https://im.dingtalk.com/') return 18 | if (url.indexOf('https://space.dingtalk.com/auth/download') === 0) return 19 | if (url.indexOf('https://space.dingtalk.com/attachment') === 0) return 20 | // 防止短时间快速点击链接 21 | if (lastUrl === url && Date.now() - time < 800) return 22 | lastUrl = url 23 | time = Date.now() 24 | shell.openExternal(url) 25 | } 26 | 27 | export default dingtalk => () => { 28 | if (dingtalk.$mainWin) { 29 | dingtalk.showMainWin() 30 | return 31 | } 32 | // 创建浏览器窗口 33 | const $win = new BrowserWindow({ 34 | title: '钉钉', 35 | width: 960, 36 | height: 600, 37 | minWidth: 720, 38 | minHeight: 450, 39 | useContentSize: true, 40 | center: true, 41 | frame: false, 42 | show: false, 43 | backgroundColor: '#5a83b7', 44 | icon: logo, 45 | resizable: true, 46 | webPreferences: { 47 | nodeIntegration: true 48 | } 49 | }) 50 | 51 | /** 52 | * 优雅的显示窗口 53 | */ 54 | $win.once('ready-to-show', () => { 55 | $win.show() 56 | $win.focus() 57 | 58 | /** 59 | * 先让主窗口显示后在执行检查更新 60 | * 防止对话框跑到主窗口后面 61 | * 导致窗口点击不了 62 | * https://github.com/nashaofu/dingtalk/issues/186 63 | */ 64 | autoUpdate(dingtalk) 65 | }) 66 | 67 | /** 68 | * 窗体关闭事件处理 69 | * 默认只会隐藏窗口 70 | */ 71 | $win.on('close', e => { 72 | e.preventDefault() 73 | $win.hide() 74 | }) 75 | 76 | $win.webContents.on('dom-ready', () => { 77 | // 页面初始化图标不跳动 78 | if (dingtalk.$tray) dingtalk.$tray.flicker(false) 79 | const filename = path.join(app.getAppPath(), './dist/preload/mainWin.js') 80 | // 读取js文件并执行 81 | fs.access(filename, fs.constants.R_OK, err => { 82 | if (err) return 83 | fs.readFile(filename, (error, data) => { 84 | if (error || $win.webContents.isDestroyed()) return 85 | $win.webContents.executeJavaScript(data.toString(), () => { 86 | if (!$win.webContents.isDestroyed()) $win.webContents.send('dom-ready') 87 | }) 88 | }) 89 | }) 90 | }) 91 | 92 | // 右键菜单 93 | $win.webContents.on('context-menu', (e, params) => { 94 | e.preventDefault() 95 | contextMenu($win, params) 96 | }) 97 | 98 | // 浏览器中打开链接 99 | $win.webContents.on('new-window', (e, url) => { 100 | e.preventDefault() 101 | openExternal(url) 102 | }) 103 | 104 | // 主窗口导航拦截 105 | $win.webContents.on('will-navigate', (e, url) => { 106 | e.preventDefault() 107 | openExternal(url) 108 | }) 109 | 110 | ipcMain.on('MAINWIN:window-minimize', () => $win.minimize()) 111 | 112 | ipcMain.on('MAINWIN:window-maximization', () => { 113 | if ($win.isMaximized()) { 114 | $win.unmaximize() 115 | } else { 116 | $win.maximize() 117 | } 118 | }) 119 | 120 | ipcMain.on('MAINWIN:window-close', () => $win.hide()) 121 | ipcMain.on('MAINWIN:open-email', (e, url) => dingtalk.showEmailWin(url)) 122 | 123 | ipcMain.on('MAINWIN:window-show', () => { 124 | $win.show() 125 | $win.focus() 126 | }) 127 | 128 | ipcMain.on('MAINWIN:badge', (e, count) => { 129 | app.setBadgeCount(count) 130 | if (dingtalk.$tray) dingtalk.$tray.flicker(!!count) 131 | if (app.dock) { 132 | app.dock.show() 133 | app.dock.bounce('critical') 134 | } 135 | }) 136 | 137 | download($win) 138 | // 加载URL地址 139 | $win.loadURL('https://im.dingtalk.com/') 140 | return $win 141 | } 142 | -------------------------------------------------------------------------------- /src/preload/mainWin/download.js: -------------------------------------------------------------------------------- 1 | import FileTask from './fileTask' 2 | import cloneDeep from 'lodash/cloneDeep' 3 | import findIndex from 'lodash/findIndex' 4 | import { ipcRenderer } from 'electron' 5 | 6 | export default injector => { 7 | const files = [] 8 | 9 | const addFile = file => { 10 | const percent = file.finishSize / file.fileSize 11 | files.push({ 12 | clientId: file.clientId, 13 | name: file.name, 14 | fileSize: file.fileSize, 15 | state: file.state, 16 | url: file.url, 17 | percent: percent, 18 | isDownload: true, 19 | isCompress: false, 20 | isFinish: false, 21 | isFile: true, 22 | isDownloadCancel: file.state === 'cancelled', 23 | status: { 24 | begin: true, 25 | done: file.state === 'completed', 26 | error: file.state === 'interrupted', 27 | finishSize: file.finishSize, 28 | progress: Math.floor(percent * 100) 29 | } 30 | }) 31 | } 32 | 33 | const updateFile = (index, file) => { 34 | const percent = file.finishSize / file.fileSize 35 | files[index] = { 36 | ...files[index], 37 | state: file.state, 38 | percent: percent, 39 | isDownloadCancel: file.state === 'cancelled', 40 | status: { 41 | begin: true, 42 | done: file.state === 'completed', 43 | error: file.state === 'interrupted', 44 | finishSize: file.finishSize, 45 | progress: Math.floor(percent * 100) 46 | } 47 | } 48 | } 49 | 50 | ipcRenderer.on('MAINWIN:download-start', (e, file) => { 51 | addFile(file) 52 | updateList() 53 | }) 54 | 55 | ipcRenderer.on('MAINWIN:download-updated', (e, file) => { 56 | const index = findIndex(files, { clientId: file.clientId }) 57 | if (index !== -1) { 58 | updateFile(index, file) 59 | } else { 60 | addFile(file) 61 | } 62 | updateList() 63 | }) 64 | 65 | ipcRenderer.on('MAINWIN:download-done', (e, file) => { 66 | const index = findIndex(files, { clientId: file.clientId }) 67 | if (index !== -1) { 68 | updateFile(index, file) 69 | } else { 70 | addFile(file) 71 | } 72 | updateList() 73 | const status = { 74 | completed: '下载完成', 75 | interrupted: '下载失败' 76 | } 77 | if (status[file.state]) { 78 | ipcRenderer.send('notify', `${file.name}${status[file.state]}`) 79 | } 80 | }) 81 | 82 | const updateList = () => { 83 | const uploadList = angular.element('#header>upload-list') 84 | if (!uploadList.length) { 85 | return 86 | } 87 | uploadList.scope().$apply(() => { 88 | const data = uploadList.data() 89 | const $uploadListController = data.$uploadListController 90 | const fileTaskList = $uploadListController.fileTaskList 91 | files.forEach(file => { 92 | const index = findIndex(fileTaskList, { clientId: file.clientId }) 93 | if (index === -1) { 94 | fileTaskList.unshift(new FileTask(cloneDeep(file))) 95 | } else { 96 | if (file.state === 'progressing') { 97 | fileTaskList[index].onProgress(cloneDeep(file)) 98 | } else if (file.state === 'completed') { 99 | fileTaskList[index].onProgress(cloneDeep(file)) 100 | fileTaskList[index].onDownloadSuccess(cloneDeep(file)) 101 | } else if (file.state === 'interrupted') { 102 | fileTaskList[index].onProgress(cloneDeep(file)) 103 | fileTaskList[index].onDownloadError(cloneDeep(file)) 104 | } else if (file.state === 'cancelled') { 105 | files.splice(index, 1) 106 | fileTaskList.splice(index, 1) 107 | } 108 | } 109 | }) 110 | $uploadListController.uploadListCount = fileTaskList.filter(({ isFinish }) => !isFinish).length 111 | }) 112 | } 113 | injector.setTimer(() => updateList()) 114 | } 115 | -------------------------------------------------------------------------------- /src/renderer/settingWin/components/keybinding/keybinding.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 132 | 133 | 178 | -------------------------------------------------------------------------------- /src/preload/mainWin/fileTask.js: -------------------------------------------------------------------------------- 1 | import { formatFileSize, transFileClassName } from './utils' 2 | import Events from './Events' 3 | 4 | export default class FileTask extends Events { 5 | EventsName = { 6 | FILE_STATUS_UPDATE: 'file_status_update' 7 | } 8 | 9 | constructor (file) { 10 | super() 11 | this.initialize(file) 12 | } 13 | 14 | initialize (file) { 15 | this.type = 2 16 | this.name = file.name 17 | this.clientId = file.clientId 18 | this.fileSize = file.fileSize 19 | this.isDownload = file.isDownload 20 | this.isFile = file.isFile 21 | this.url = file.url 22 | this.isFinish = file.isFinish 23 | this.setCreateTime() 24 | this.setIconClass() 25 | this.initStatus(file.status) 26 | this.id = this.clientId 27 | this.formatSize = formatFileSize(this.fileSize) 28 | this.onProgress = this.onProgress.bind(this) 29 | this.onUploadSuccess = this.onUploadSuccess.bind(this) 30 | this.onDownloadError = this.onDownloadError.bind(this) 31 | this.onUploadError = this.onUploadError.bind(this) 32 | this.onDownloadSuccess = this.onDownloadSuccess.bind(this) 33 | this.onUploadStopAll = this.onUploadStopAll.bind(this) 34 | } 35 | 36 | setIconClass () { 37 | this.iconClass = transFileClassName(this.name) 38 | } 39 | 40 | setCreateTime () { 41 | if (this.isDownload) { 42 | this.createTime = Date.now() 43 | } else { 44 | this.createTime = parseInt(this.clientId.split('_').shift()) 45 | } 46 | } 47 | 48 | setIsFinish (isFinish) { 49 | this.isFinish = isFinish 50 | } 51 | 52 | initStatus (status) { 53 | this.status = status || {} 54 | } 55 | 56 | onProgress (file) { 57 | if (file && file.clientId === this.clientId) { 58 | if (file.isDownload) { 59 | this.fileSize = file.fileSize 60 | this.formatSize = formatFileSize(this.fileSize) 61 | } 62 | if (file.isCompress) { 63 | this.status.isCompress = true 64 | } else { 65 | if (file.percent !== undefined) { 66 | this.status.progress = Math.ceil(100 * file.percent) 67 | this.status.finishSize = formatFileSize(file.percent * this.fileSize) 68 | if (!file.isDownload) { 69 | this.status.error = false 70 | } 71 | } 72 | } 73 | this.status.begin = true 74 | this.emit(this.EventsName.FILE_STATUS_UPDATE) 75 | } 76 | } 77 | 78 | onUploadSuccess (file) { 79 | if (file && file.clientId === this.clientId) { 80 | this.status.done = true 81 | this.setIsFinish(true) 82 | this.emit(this.EventsName.FILE_STATUS_UPDATE) 83 | } 84 | } 85 | 86 | onDownloadError (file) { 87 | if (file && file.clientId === this.clientId) { 88 | this.status.done = true 89 | this.status.error = true 90 | this.status.errorMsg = file.msg || '下载失败' 91 | this.status.isDownloadCancel = file.isDownloadCancel 92 | this.setIsFinish(true) 93 | this.emit(this.EventsName.FILE_STATUS_UPDATE) 94 | } 95 | } 96 | 97 | onUploadError (file) { 98 | if (file && file.clientId === this.clientId) { 99 | if (file.reason === 'STOP_REJECT' || file.isFromIM) { 100 | this.status = {} 101 | } else { 102 | if (file.errorCode === '200012') { 103 | this.status.spaceFull = true 104 | } 105 | this.status.done = true 106 | this.status.error = true 107 | this.status.errorMsg = file.msg 108 | } 109 | this.setIsFinish(true) 110 | this.emit(this.EventsName.FILE_STATUS_UPDATE) 111 | } 112 | } 113 | 114 | onDownloadSuccess (file) { 115 | if (file && file.clientId === this.clientId) { 116 | this.status.done = true 117 | this.status.error = false 118 | this.setIsFinish(true) 119 | this.emit(this.EventsName.FILE_STATUS_UPDATE) 120 | } 121 | } 122 | 123 | onUploadStopAll (file) {} 124 | 125 | isTaskFinish () { 126 | return this.isFinish 127 | } 128 | 129 | getPayload () { 130 | return { 131 | type: this.type, 132 | name: this.name, 133 | clientId: this.clientId, 134 | fileSize: this.fileSize, 135 | isDownload: this.isDownload, 136 | isFile: this.isFile, 137 | url: this.url, 138 | isFinish: this.isFinish, 139 | status: this.status 140 | } 141 | } 142 | 143 | canSaveToLocal () { 144 | return this.status.done === true 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/preload/mainWin/Events.js: -------------------------------------------------------------------------------- 1 | export default class Events { 2 | static noConflict () { 3 | return Events 4 | } 5 | 6 | getListeners (event) { 7 | let listener 8 | const events = this._getEvents() 9 | if (event instanceof RegExp) { 10 | listener = {} 11 | Object.keys(events).forEach(key => { 12 | if (event.test(key)) { 13 | listener[key] = events[key] 14 | } 15 | }) 16 | } else { 17 | listener = events[event] || [] 18 | } 19 | return listener 20 | } 21 | 22 | flattenListeners (events) { 23 | return events.map(({ listener }) => listener) 24 | } 25 | 26 | getListenersAsObject (event) { 27 | const listener = this.getListeners(event) 28 | let listenerAsObject = {} 29 | if (listener instanceof Array) { 30 | listenerAsObject[event] = listener 31 | } else { 32 | listenerAsObject = listener 33 | } 34 | return listenerAsObject 35 | } 36 | 37 | addListener (event, listener) { 38 | const isObject = typeof listener === 'object' 39 | const listenerAsObject = this.getListenersAsObject(event) 40 | Object.keys(listenerAsObject).forEach(key => { 41 | const index = listenerAsObject[key].findIndex(item => item.listener === listener) 42 | if (index !== -1) { 43 | listenerAsObject[key].push(isObject ? listener : { listener, once: false }) 44 | } 45 | }) 46 | return this 47 | } 48 | 49 | on (event, listener) { 50 | return this.addListener(event, listener) 51 | } 52 | 53 | addOnceListener (event, listener) { 54 | return this.addListener(event, { 55 | listener, 56 | once: true 57 | }) 58 | } 59 | 60 | once (event, listener) { 61 | return this.addOnceListener(event, listener) 62 | } 63 | 64 | defineEvent (event) { 65 | this.getListeners(event) 66 | return this 67 | } 68 | 69 | defineEvents (events) { 70 | events.forEach(event => this.defineEvent(event)) 71 | return this 72 | } 73 | 74 | removeListener (event, listener) { 75 | const listenerAsObject = this.getListenersAsObject(event) 76 | Object.keys(listenerAsObject).forEach(key => { 77 | const index = listenerAsObject[key].findIndex(item => item.listener === listener) 78 | if (index !== -1) { 79 | listenerAsObject[key].splice(index, 1) 80 | } 81 | }) 82 | return this 83 | } 84 | 85 | off (event, listener) { 86 | return this.removeListener(event, listener) 87 | } 88 | 89 | addListeners (event, listener) { 90 | return this.manipulateListeners(false, event, listener) 91 | } 92 | 93 | removeListeners (event, listener) { 94 | return this.manipulateListeners(true, event, listener) 95 | } 96 | 97 | manipulateListeners (is, events, listeners) { 98 | const single = is ? this.removeListener : this.addListener 99 | const manipulate = is ? this.removeListeners : this.addListeners 100 | if (typeof events !== 'object' || events instanceof RegExp) { 101 | listeners.forEach(listener => single.call(this, events, listener)) 102 | } else { 103 | Object.keys(events).forEach(key => { 104 | const listener = events[key] 105 | if (typeof listener === 'function') { 106 | single.call(this, key, listener) 107 | } else { 108 | manipulate.call(this, key, listener) 109 | } 110 | }) 111 | } 112 | return this 113 | } 114 | 115 | removeEvent (event) { 116 | const events = this._getEvents() 117 | if (typeof event === 'string') { 118 | delete events[event] 119 | } else if (event instanceof RegExp) { 120 | Object.keys(events).forEach(key => { 121 | if (event.test(key)) delete events[key] 122 | }) 123 | } else { 124 | delete this._events 125 | } 126 | return this 127 | } 128 | 129 | removeAllListeners (event) { 130 | return this.removeEvent(event) 131 | } 132 | 133 | emitEvent (event, args) { 134 | const listenersAsObject = this.getListenersAsObject(event) 135 | Object.keys(listenersAsObject).forEach(key => { 136 | listenersAsObject[key].forEach(listener => { 137 | if (listener.once) { 138 | this.removeListener(event, listener) 139 | } 140 | if (listener.apply(this, args || []) === this._getOnceReturnValue()) { 141 | this.removeListener(event, listener) 142 | } 143 | }) 144 | }) 145 | return this 146 | } 147 | 148 | trigger (event, args) { 149 | return this.emitEvent(event, args) 150 | } 151 | 152 | emit (event, ...args) { 153 | return this.emitEvent(event, args) 154 | } 155 | 156 | setOnceReturnValue (val) { 157 | this._onceReturnValue = val 158 | return this 159 | } 160 | 161 | _getOnceReturnValue = function () { 162 | return Object.prototype.hasOwnProperty.call(this, '_onceReturnValue') || this._onceReturnValue 163 | } 164 | 165 | _getEvents = function () { 166 | return this._events || {} 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dingtalk", 3 | "version": "2.1.1", 4 | "description": "钉钉桌面版,基于electron和钉钉网页版开发,支持Windows、Linux和macOS", 5 | "author": "nashaofu ", 6 | "main": "dist/main.js", 7 | "scripts": { 8 | "start": "electron .", 9 | "lint": "eslint --ext .js,.vue src", 10 | "dev": "node build/webpack.dev.conf.js", 11 | "postinstall": "electron-builder install-app-deps", 12 | "dev:main": "webpack --config build/main/webpack.dev.conf.js", 13 | "dev:preload": "webpack --config build/preload/webpack.dev.conf.js", 14 | "dev:renderer": "webpack-dev-server --config build/renderer/webpack.dev.conf.js", 15 | "build": "webpack --config build/webpack.prod.conf.js", 16 | "build:main": "webpack --config build/main/webpack.prod.conf.js", 17 | "build:preload": "webpack --config build/preload/webpack.prod.conf.js", 18 | "build:renderer": "webpack --config build/renderer/webpack.prod.conf.js", 19 | "pack": "electron-builder", 20 | "release": "electron-builder" 21 | }, 22 | "keywords": [ 23 | "dingtalk", 24 | "钉钉", 25 | "linux", 26 | "macOS", 27 | "Windows" 28 | ], 29 | "dependencies": { 30 | "@babel/runtime": "^7.5.5", 31 | "axios": "^0.19.0", 32 | "electron-updater": "^4.1.2", 33 | "lodash": "^4.17.15", 34 | "normalize.css": "^8.0.1", 35 | "shortcut-capture": "^1.2.1", 36 | "vue": "^2.6.10" 37 | }, 38 | "devDependencies": { 39 | "@babel/core": "^7.5.5", 40 | "@babel/plugin-proposal-class-properties": "^7.5.5", 41 | "@babel/plugin-proposal-export-default-from": "^7.5.2", 42 | "@babel/plugin-transform-runtime": "^7.5.5", 43 | "@babel/preset-env": "^7.5.5", 44 | "autoprefixer": "^9.6.1", 45 | "babel-eslint": "^10.0.3", 46 | "babel-loader": "^8.0.6", 47 | "chalk": "^2.4.2", 48 | "css-loader": "^3.2.0", 49 | "electron": "^5.0.10", 50 | "electron-builder": "^21.2.0", 51 | "electron-debug": "^3.0.1", 52 | "electron-dev-webpack-plugin": "^1.0.4", 53 | "electron-devtools-installer": "^2.2.4", 54 | "eslint": "^6.2.2", 55 | "eslint-config-standard": "^14.0.1", 56 | "eslint-friendly-formatter": "^4.0.1", 57 | "eslint-loader": "^3.0.0", 58 | "eslint-plugin-import": "^2.18.2", 59 | "eslint-plugin-node": "^9.1.0", 60 | "eslint-plugin-promise": "^4.2.1", 61 | "eslint-plugin-standard": "^4.0.1", 62 | "eslint-plugin-vue": "^5.2.3", 63 | "file-loader": "^4.2.0", 64 | "friendly-errors-webpack-plugin": "^1.7.0", 65 | "html-webpack-plugin": "^3.2.0", 66 | "less": "^3.10.3", 67 | "less-loader": "^5.0.0", 68 | "mini-css-extract-plugin": "^0.8.0", 69 | "postcss-loader": "^3.0.0", 70 | "pug": "^2.0.4", 71 | "pug-plain-loader": "^1.0.0", 72 | "registry-auth-token": "^4.0.0", 73 | "registry-url": "^5.1.0", 74 | "url-loader": "^2.1.0", 75 | "vue-loader": "^15.7.1", 76 | "vue-template-compiler": "^2.6.10", 77 | "webpack": "^4.39.3", 78 | "webpack-cli": "^3.3.7", 79 | "webpack-dev-server": "^3.8.0", 80 | "webpack-merge": "^4.2.1" 81 | }, 82 | "build": { 83 | "appId": "com.electron.dingtalk", 84 | "productName": "钉钉", 85 | "artifactName": "dingtalk-${version}-${channel}-${arch}.${ext}", 86 | "copyright": "Copyright © year nashaofu", 87 | "asar": true, 88 | "directories": { 89 | "buildResources": "resources/icons", 90 | "output": "release" 91 | }, 92 | "files": [ 93 | "dist/**/*", 94 | "resources/tray/*", 95 | "resources/logo.png" 96 | ], 97 | "publish": { 98 | "provider": "github", 99 | "owner": "nashaofu", 100 | "repo": "dingtalk" 101 | }, 102 | "mac": { 103 | "target": "dmg", 104 | "icon": "./resources/icons/icon.icns", 105 | "category": "public.app-category.instant-messaging" 106 | }, 107 | "win": { 108 | "icon": "./resources/icons/icon.ico", 109 | "target": [ 110 | { 111 | "target": "nsis", 112 | "arch": [ 113 | "x64", 114 | "ia32" 115 | ] 116 | } 117 | ] 118 | }, 119 | "linux": { 120 | "target": [ 121 | { 122 | "target": "AppImage", 123 | "arch": [ 124 | "x64", 125 | "ia32" 126 | ] 127 | }, 128 | { 129 | "target": "deb", 130 | "arch": [ 131 | "x64", 132 | "ia32" 133 | ] 134 | }, 135 | { 136 | "target": "rpm", 137 | "arch": [ 138 | "x64", 139 | "ia32" 140 | ] 141 | } 142 | ], 143 | "executableName": "dingtalk", 144 | "icon": "./resources/icons", 145 | "category": "InstantMessaging;Network" 146 | }, 147 | "nsis": { 148 | "oneClick": false, 149 | "perMachine": true, 150 | "allowToChangeInstallationDirectory": true, 151 | "displayLanguageSelector": true, 152 | "language": 2052 153 | } 154 | }, 155 | "license": "MIT", 156 | "repository": { 157 | "type": "git", 158 | "url": "git+https://github.com/nashaofu/dingtalk.git" 159 | }, 160 | "bugs": { 161 | "url": "https://github.com/nashaofu/dingtalk/issues" 162 | }, 163 | "homepage": "https://github.com/nashaofu/dingtalk#readme" 164 | } 165 | -------------------------------------------------------------------------------- /src/main/dingtalk.js: -------------------------------------------------------------------------------- 1 | import { app, Menu, ipcMain, BrowserWindow } from 'electron' 2 | import { initSetting, readSetting, writeSetting } from './setting' 3 | import online from './online' 4 | import Notify from './notify' 5 | import mainWin from './mainWin' 6 | import emailWin from './emailWin' 7 | import errorWin from './errorWin' 8 | import aboutWin from './aboutWin' 9 | import shortcut from './shortcut' 10 | import settingWin from './settingWin' 11 | import DingtalkTray from './dingtalkTray' 12 | import ShortcutCapture from 'shortcut-capture' 13 | 14 | export default class DingTalk { 15 | // app对象是否ready 16 | _ready = null 17 | // 托盘图标 18 | $tray = null 19 | // 主窗口 20 | $mainWin = null 21 | // 邮箱窗口 22 | $emailWin = null 23 | // 错误窗口 24 | $errorWin = null 25 | // 设置窗口 26 | $settingWin = null 27 | // 关于窗口 28 | $aboutWin = null 29 | // 截图对象 30 | $shortcutCapture = null 31 | // 网络情况,默认为null,必须等到页面报告状态 32 | online = null 33 | // 默认配置 34 | setting = { 35 | autoupdate: true, 36 | enableCapture: true, 37 | enableFlicker: true, 38 | keymap: { 39 | 'shortcut-capture': ['Control', 'Alt', 'A'] 40 | } 41 | } 42 | 43 | constructor () { 44 | if (!app.requestSingleInstanceLock()) return app.quit() 45 | this.init().then(() => { 46 | app.setAppUserModelId('com.electron.dingtalk') 47 | // 移除窗口菜单 48 | Menu.setApplicationMenu(null) 49 | this.initMainWin() 50 | this.initTray() 51 | this.initShortcutCapture() 52 | this.initNotify() 53 | this.bindShortcut() 54 | }) 55 | } 56 | 57 | /** 58 | * 初始化 59 | * @return {Promise} setting 60 | */ 61 | async init () { 62 | online(this)() 63 | this.setting = await initSetting(this)() 64 | // 重复打开应用就显示窗口 65 | app.on('second-instance', (event, commandLine, workingDirectory) => this.showMainWin()) 66 | // 所有窗口关闭之后退出应用 67 | app.once('window-all-closed', () => { 68 | if (process.platform !== 'darwin') { 69 | if (this.$tray && !this.$tray.isDestroyed()) { 70 | this.$tray.destroy() 71 | this.$tray = null 72 | } 73 | app.quit() 74 | } 75 | }) 76 | return app.whenReady() 77 | } 78 | 79 | /** 80 | * 初始化主窗口 81 | */ 82 | initMainWin () { 83 | this.$mainWin = mainWin(this)() 84 | } 85 | 86 | /** 87 | * 初始化托盘图标 88 | */ 89 | initTray () { 90 | this.$tray = new DingtalkTray({ dingtalk: this }) 91 | } 92 | 93 | /** 94 | * 初始化截图 95 | */ 96 | initShortcutCapture () { 97 | this.$shortcutCapture = new ShortcutCapture() 98 | } 99 | 100 | /** 101 | * 初始化消息提示 102 | */ 103 | initNotify () { 104 | this.$notify = new Notify() 105 | ipcMain.on('notify', (e, body) => this.$notify.show(body)) 106 | this.$notify.on('click', () => this.showMainWin()) 107 | } 108 | 109 | /** 110 | * 从文件中读取设置信息 111 | * @return {Promise} setting 112 | */ 113 | readSetting () { 114 | return readSetting(this)() 115 | } 116 | 117 | /** 118 | * 写入设置到文件 119 | * @return {Promise} setting 120 | */ 121 | writeSetting () { 122 | return writeSetting(this)() 123 | } 124 | 125 | /** 126 | * 退出应用 127 | */ 128 | quit () { 129 | const windows = BrowserWindow.getAllWindows() 130 | windows.forEach(item => item.destroy()) 131 | if (process.platform !== 'darwin') { 132 | if (this.$tray && !this.$tray.isDestroyed()) { 133 | this.$tray.destroy() 134 | this.$tray = null 135 | } 136 | app.quit() 137 | } 138 | } 139 | 140 | /** 141 | * 绑定快捷键 142 | */ 143 | bindShortcut () { 144 | shortcut(this)() 145 | } 146 | 147 | /** 148 | * 显示主窗口 149 | */ 150 | showMainWin () { 151 | if (this.$mainWin) { 152 | if (this.online) { 153 | if (this.$mainWin.isMinimized()) this.$mainWin.restore() 154 | this.$mainWin.show() 155 | this.$mainWin.focus() 156 | } else if (this.online === false) { 157 | /** 158 | * this.online === null不显示 159 | * 因为可能此时还没有初始化online 160 | * 即$mainWin还没有触发dom-ready 161 | */ 162 | this.showErrorWin() 163 | } 164 | } 165 | } 166 | 167 | /** 168 | * 截图 169 | */ 170 | shortcutCapture () { 171 | if (this.$shortcutCapture) { 172 | this.$shortcutCapture.shortcutCapture() 173 | } 174 | } 175 | 176 | /** 177 | * 显示邮箱窗口 178 | * @param {Object} storage 179 | */ 180 | showEmailWin (storage) { 181 | this.$emailWin = emailWin(this)(storage) 182 | } 183 | 184 | /** 185 | * 显示错误窗口 186 | */ 187 | showErrorWin () { 188 | this.$errorWin = errorWin(this)() 189 | } 190 | 191 | /** 192 | * 隐藏错误窗口 193 | */ 194 | hideErrorWin () { 195 | if (this.$errorWin) { 196 | this.$errorWin.close() 197 | } 198 | } 199 | 200 | /** 201 | * 显示设置窗口 202 | */ 203 | showSettingWin () { 204 | this.$settingWin = settingWin(this)() 205 | } 206 | 207 | /** 208 | * 关闭设置窗口 209 | */ 210 | hideSettingWin () { 211 | if (this.$settingWin) { 212 | this.$settingWin.close() 213 | } 214 | } 215 | 216 | resetTrayMenu () { 217 | if (this.$tray && !this.$tray.isDestroyed()) { 218 | this.$tray.setMenu() 219 | } 220 | } 221 | 222 | /** 223 | * 显示关于窗口 224 | */ 225 | showAboutWin () { 226 | this.$aboutWin = aboutWin(this)() 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/renderer/errorWin/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 41 | 42 | 263 | --------------------------------------------------------------------------------