├── .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 |
2 | .dt-switch(
3 | :class="getClass"
4 | )
5 | .dt-switch-title {{ title }}
6 | .dt-switch-container(
7 | @click="toggle"
8 | )
9 | .dt-switch-container-toggle
10 |
11 |
12 |
46 |
47 |
97 |
--------------------------------------------------------------------------------
/src/renderer/settingWin/components/button/button.vue:
--------------------------------------------------------------------------------
1 |
2 | button.dt-button(
3 | :class="getClass",
4 | @click="click($event)"
5 | )
6 | slot
7 |
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[](https://travis-ci.org/nashaofu/dingtalk)[](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 | 
46 | 2. 账号密码登录页面
47 | 
48 | 3. 登录后页面展示
49 | 
50 | 4. 邮箱打开效果
51 | 
52 | 5. 截图效果预览
53 | 
54 | 6. 网络错误页面
55 | 
56 | 7. 系统设置界面
57 | 
58 | 8. 关于界面
59 | 
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 | 
106 |
--------------------------------------------------------------------------------
/src/renderer/aboutWin/App.vue:
--------------------------------------------------------------------------------
1 |
2 | .app
3 | .app-logo
4 | img.app-logo-image(src="./logo.png")
5 | .app-logo-title 钉钉 {{ version }}
6 |
7 | .app-update
8 | button.app-update-button(@click="checkForUpdates") 检查更新
9 |
10 | .app-desc {{ description }}
11 | .app-info
12 | .app-info-title 作者:
13 | .app-info-desc {{ author }}
14 | .app-info
15 | .app-info-title 协议:
16 | .app-info-desc {{ license }}
17 | .app-info
18 | .app-info-title 主页:
19 | .app-info-desc(@click="openURL") {{ homepage }}
20 |
21 |
22 |
69 |
70 |
167 |
--------------------------------------------------------------------------------
/src/renderer/settingWin/App.vue:
--------------------------------------------------------------------------------
1 |
2 | .app
3 | .app-item
4 | dt-switch(
5 | v-model="enableCapture"
6 | title="截图"
7 | )
8 | dt-keybinding(
9 | :disabled="!enableCapture"
10 | v-model="shortcutCapture"
11 | )
12 | .app-item
13 | dt-switch(
14 | v-model="enableFlicker"
15 | title="新消息闪烁"
16 | )
17 | .app-item
18 | dt-switch(
19 | v-model="autoupdate"
20 | title="自动更新"
21 | )
22 | .app-item
23 | .app-item-button
24 | dt-button(@click="reset") 还原设置
25 | dt-button(
26 | type="primary"
27 | @click="save"
28 | ) 保存设置
29 |
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 |
2 | .dt-keybinding
3 | .dt-keybinding-title(v-if="showTitle") {{ title }}
4 | .dt-keybinding-value
5 | input(
6 | type="text",
7 | :value="keys | upperFirst",
8 | :disabled="disabled",
9 | @keydown="keydown",
10 | @input="input",
11 | @focus="focus",
12 | @blur="blur"
13 | )
14 |
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 |
2 | .app
3 | .app-lamp
4 | .app-lamp-1 E
5 | .app-lamp-2 R
6 | .app-lamp-3 R
7 | .app-lamp-4 O
8 | .app-lamp-5 R
9 | .app-desc 网络错误
10 | .app-buttons
11 | .app-buttons-retry(@click="retry") 重试
12 | .app-buttons-close(@click="close") 关闭
13 |
14 |
15 |
41 |
42 |
263 |
--------------------------------------------------------------------------------