├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── README.md ├── app ├── electron.js ├── icons │ ├── icon.icns │ └── icon.ico ├── index.ejs ├── package.json └── src │ ├── App.vue │ ├── assets │ └── multimedia │ │ ├── cheering.mp3 │ │ └── soda_pop.mp3 │ ├── components │ ├── FileSelector.vue │ ├── Start.vue │ ├── player │ │ ├── Add.vue │ │ └── List.vue │ └── prize │ │ ├── Add.vue │ │ ├── List.vue │ │ ├── Overview.vue │ │ └── Run.vue │ ├── config.js │ ├── lib │ ├── animate-css.js │ ├── fs.js │ └── h5-async-file-reader.js │ ├── main.js │ ├── model │ ├── Player.js │ └── Prize.js │ ├── routes.js │ └── vuex │ ├── actions.js │ ├── getters.js │ ├── modules │ ├── counters.js │ └── index.js │ ├── mutation-types.js │ └── store.js ├── builds └── .gitkeep ├── config.js ├── package.json ├── tasks ├── release.js ├── runner.js ├── vue │ ├── route.js │ ├── route.template.txt │ └── routes.template.txt └── vuex │ ├── module.js │ └── module.template.txt └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0" 5 | ], 6 | "plugins": [ 7 | "transform-runtime", 8 | "transform-async-to-generator", 9 | [ 10 | "component", 11 | [ 12 | { 13 | "libraryName": "element-ui", 14 | "styleLibraryName": "theme-default" 15 | } 16 | ] 17 | ] 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | app/node_modules/** 2 | app/dist/** 3 | app/electron.js 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | browser: true, 9 | node: true 10 | }, 11 | plugins: [ 12 | 'html' 13 | ], 14 | 'rules': { 15 | "semi": [ 16 | "warn", 17 | "never" 18 | ], 19 | // allow debugger during development 20 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 21 | } 22 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | app/dist 3 | builds/* 4 | node_modules 5 | npm-debug.log 6 | npm-debug.log.* 7 | thumbs.db 8 | !.gitkeep 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lottery 2 | 3 | 🎁 抽奖小程序 4 | 5 | ## Showcase 6 | 7 | * [自动版 - branch master](http://og9g58alt.bkt.clouddn.com/lottery.gif) 8 | 9 | * [手动版 - branch manual](http://og9g58alt.bkt.clouddn.com/lottery2.gif) 10 | 11 | ## Build Setup 12 | 13 | ``` bash 14 | # install dependencies 15 | npm install 16 | 17 | # builds for macOS 18 | npm run build:darwi 19 | ``` 20 | More information can be found [here](https://simulatedgreg.gitbooks.io/electron-vue/content/). 21 | 22 | --- 23 | 24 | ## Thanks 25 | 26 | * [Electron](http://electron.atom.io/) 27 | * [Vue.js](https://vuejs.org/) 28 | * [Element](http://element.eleme.io/) 29 | * [electron-vue](https://github.com/SimulatedGREG/electron-vue) 30 | -------------------------------------------------------------------------------- /app/electron.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const electron = require('electron') 4 | const path = require('path') 5 | const app = electron.app 6 | const BrowserWindow = electron.BrowserWindow 7 | const fs = require('fs') 8 | const Menu = electron.Menu 9 | const dialog = electron.dialog 10 | 11 | const afs = { 12 | readFile: function(file, options = null) { 13 | return new Promise((resolve, reject) => { 14 | fs.readFile(file, options, (err, data) => { 15 | if (err) { 16 | if (err.code === 'ENOENT') { 17 | reject() 18 | } else { 19 | throw err 20 | } 21 | } 22 | resolve(data) 23 | }) 24 | }) 25 | }, 26 | mkdir: function(path, mode) { 27 | return new Promise((resolve) => { 28 | fs.mkdir(path, mode, (err) => { 29 | if (err && err.code !== 'EEXIST') 30 | throw err 31 | resolve() 32 | }) 33 | }) 34 | }, 35 | writeFile: function(file, data) { 36 | return new Promise((resove, reject) => { 37 | fs.writeFile(file, data, function(err) { 38 | if (err) 39 | throw err 40 | resove() 41 | }) 42 | }) 43 | } 44 | } 45 | 46 | let mainWindow 47 | let config = {} 48 | 49 | if (process.env.NODE_ENV === 'development') { 50 | config = require('../config') 51 | config.url = `http://localhost:${config.port}` 52 | } else { 53 | config.devtron = false 54 | config.url = `file://${__dirname}/dist/index.html` 55 | } 56 | 57 | function createDataFiles() { 58 | let appDataDir = path.join(app.getPath('appData'), 'com.hsiaosiyuan0.lottery') 59 | let dataDir = path.join(appDataDir, 'data') 60 | 61 | return afs.mkdir(appDataDir, 0o744).then(() => { 62 | return afs.mkdir(dataDir, 0o744) 63 | }).then(function() { 64 | let playersJSONFile = path.join(dataDir, 'players.db') 65 | let prizesJSONFile = path.join(dataDir, 'prizes.db') 66 | let photosDir = path.join(dataDir, 'photos') 67 | 68 | let tasks = [] 69 | 70 | tasks.push(afs.readFile(playersJSONFile).then(() => { 71 | return Promise.resolve() 72 | }, (err) => { 73 | return afs.writeFile(playersJSONFile, JSON.stringify([{ 74 | name: 'Test', 75 | photoSrc: '' 76 | }], null, 4)) 77 | })) 78 | 79 | tasks.push(afs.readFile(prizesJSONFile).then(() => { 80 | return Promise.resolve() 81 | }, () => { 82 | return afs.writeFile(prizesJSONFile, JSON.stringify([], null, 4)) 83 | })) 84 | 85 | tasks.push(afs.mkdir(photosDir, 0o744)) 86 | 87 | return Promise.all(tasks) 88 | }) 89 | } 90 | 91 | function createWindow() { 92 | let template = [ 93 | { 94 | label: 'Application', 95 | submenu: [ 96 | { 97 | label: 'About', 98 | click: function() { 99 | dialog.showMessageBox({ 100 | type: 'info', 101 | icon: null, 102 | message: 'A little 🎁 lottery soft written by hsiaosiyuan0' 103 | }) 104 | } 105 | }, 106 | { 107 | label: 'Quit', 108 | accelerator: 'Command+Q', 109 | click: function() { 110 | app.quit() 111 | } 112 | } 113 | ] 114 | }, 115 | { 116 | label: 'Edit', 117 | submenu: [ 118 | { 119 | label: 'Undo', 120 | accelerator: 'CmdOrCtrl+Z', 121 | selector: 'undo:' 122 | }, 123 | { 124 | label: 'Redo', 125 | accelerator: 'Shift+CmdOrCtrl+Z', 126 | selector: 'redo:' 127 | }, 128 | { 129 | type: 'separator' 130 | }, 131 | { 132 | label: 'Cut', 133 | accelerator: 'CmdOrCtrl+X', 134 | selector: 'cut:' 135 | }, 136 | { 137 | label: 'Copy', 138 | accelerator: 'CmdOrCtrl+C', 139 | selector: 'copy:' 140 | }, 141 | { 142 | label: 'Paste', 143 | accelerator: 'CmdOrCtrl+V', 144 | selector: 'paste:' 145 | }, 146 | { 147 | label: 'Select All', 148 | accelerator: 'CmdOrCtrl+A', 149 | selector: 'selectAll:' 150 | } 151 | ] 152 | } 153 | ] 154 | 155 | Menu.setApplicationMenu(Menu.buildFromTemplate(template)) 156 | 157 | /** 158 | * Initial window options 159 | */ 160 | mainWindow = new BrowserWindow({ 161 | height: 600, 162 | width: 800 163 | }) 164 | 165 | mainWindow.loadURL(config.url) 166 | 167 | if (process.env.NODE_ENV === 'development') { 168 | BrowserWindow.addDevToolsExtension(path.join(__dirname, '../node_modules/devtron')) 169 | 170 | let installExtension = require('electron-devtools-installer') 171 | 172 | installExtension.default(installExtension.VUEJS_DEVTOOLS) 173 | .then((name) => mainWindow.webContents.openDevTools()) 174 | .catch((err) => console.log('An error occurred: ', err)) 175 | } 176 | 177 | mainWindow.on('closed', () => { 178 | mainWindow = null 179 | }) 180 | 181 | console.log('mainWindow opened') 182 | } 183 | 184 | app.on('ready', () => { 185 | electron.protocol.registerFileProtocol('local', (req, callback) => { 186 | callback(req.url.substr(8)) 187 | }, (err) => { 188 | if (err) 189 | throw err 190 | }) 191 | 192 | createDataFiles().then(() => { 193 | createWindow() 194 | }) 195 | }) 196 | 197 | app.on('window-all-closed', () => { 198 | if (process.platform !== 'darwin') { 199 | app.quit() 200 | } 201 | }) 202 | 203 | app.on('activate', () => { 204 | if (mainWindow === null) { 205 | createWindow() 206 | } 207 | }) 208 | -------------------------------------------------------------------------------- /app/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-garden/lottery/0ab2ec6de5a99d81fbe2fa2c69f900b81a81a1c3/app/icons/icon.icns -------------------------------------------------------------------------------- /app/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-garden/lottery/0ab2ec6de5a99d81fbe2fa2c69f900b81a81a1c3/app/icons/icon.ico -------------------------------------------------------------------------------- /app/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= htmlWebpackPlugin.options.title %> 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lottery", 3 | "version": "0.0.0", 4 | "description": "lottery", 5 | "main": "electron.js", 6 | "dependencies": { 7 | "vue": "^2.0.1", 8 | "vue-electron": "^1.0.0", 9 | "vue-resource": "^1.0.3", 10 | "vue-router": "^2.0.0", 11 | "vuex": "^2.0.0" 12 | }, 13 | "devDependencies": {}, 14 | "author": "hsiaosiyuan0 " 15 | } 16 | -------------------------------------------------------------------------------- /app/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 71 | -------------------------------------------------------------------------------- /app/src/assets/multimedia/cheering.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-garden/lottery/0ab2ec6de5a99d81fbe2fa2c69f900b81a81a1c3/app/src/assets/multimedia/cheering.mp3 -------------------------------------------------------------------------------- /app/src/assets/multimedia/soda_pop.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-garden/lottery/0ab2ec6de5a99d81fbe2fa2c69f900b81a81a1c3/app/src/assets/multimedia/soda_pop.mp3 -------------------------------------------------------------------------------- /app/src/components/FileSelector.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 68 | 69 | 108 | -------------------------------------------------------------------------------- /app/src/components/Start.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | 27 | 58 | -------------------------------------------------------------------------------- /app/src/components/player/Add.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 87 | 88 | 118 | -------------------------------------------------------------------------------- /app/src/components/player/List.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 95 | 96 | 136 | -------------------------------------------------------------------------------- /app/src/components/prize/Add.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 97 | 98 | 128 | 129 | width: 100%; 130 | } 131 | 132 | -------------------------------------------------------------------------------- /app/src/components/prize/List.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 75 | 76 | 110 | -------------------------------------------------------------------------------- /app/src/components/prize/Overview.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 138 | 139 | 194 | -------------------------------------------------------------------------------- /app/src/components/prize/Run.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 208 | 209 | 291 | -------------------------------------------------------------------------------- /app/src/config.js: -------------------------------------------------------------------------------- 1 | import electron from 'electron' 2 | import path from 'path' 3 | 4 | let app = electron.remote.app 5 | 6 | let appDataDir = path.join(app.getPath('appData'), 'com.hsiaosiyuan0.lottery') 7 | 8 | let dataDir = path.join(appDataDir, 'data') 9 | let playersJSONFile = path.join(dataDir, 'players.db') 10 | let prizesJSONFile = path.join(dataDir, 'prizes.db') 11 | 12 | let photosDir = path.join(dataDir, 'photos') 13 | 14 | export default { 15 | dataDir, 16 | playersJSONFile, 17 | prizesJSONFile, 18 | photosDir 19 | } 20 | -------------------------------------------------------------------------------- /app/src/lib/animate-css.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery' 2 | 3 | $.fn.extend({ 4 | animateCss: function(animationName) { 5 | let animationEnd = 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend' 6 | return new Promise((resolve) => { 7 | this.addClass('animated ' + animationName).one(animationEnd, function() { 8 | $(this).removeClass('animated ' + animationName) 9 | resolve(this) 10 | }) 11 | }) 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /app/src/lib/fs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | module.exports = { 4 | readFile: function(file, options = null) { 5 | return new Promise((resolve, reject) => { 6 | fs.readFile(file, options, (err, data) => { 7 | if (err) { 8 | if (err.code === 'ENOENT') { 9 | reject() 10 | } else { 11 | throw err 12 | } 13 | } 14 | resolve(data) 15 | }) 16 | }) 17 | }, 18 | mkdir: function(path, mode) { 19 | return new Promise((resolve) => { 20 | fs.mkdir(path, mode, (err) => { 21 | if (err && err.code !== 'EEXIST') 22 | throw err 23 | resolve() 24 | }) 25 | }) 26 | }, 27 | writeFile: function(file, data) { 28 | return new Promise((resove, reject) => { 29 | fs.writeFile(file, data, function(err) { 30 | if (err) 31 | throw err 32 | resove() 33 | }) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/lib/h5-async-file-reader.js: -------------------------------------------------------------------------------- 1 | let readFileAsDataUrl = (file) => { 2 | let reader = new window.FileReader() 3 | return new Promise((resolve, reject) => { 4 | reader.onload = (evt) => resolve(evt.target.result) 5 | reader.onerror = (evt) => reject(evt) 6 | reader.onabort = (evt) => reject(evt) 7 | reader.readAsDataURL(file) 8 | }) 9 | } 10 | 11 | export { readFileAsDataUrl } 12 | -------------------------------------------------------------------------------- /app/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Electron from 'vue-electron' 3 | import Resource from 'vue-resource' 4 | import Router from 'vue-router' 5 | import ElementUI from 'element-ui' 6 | import 'element-ui/lib/theme-default/index.css' 7 | 8 | import App from './App' 9 | import routes from './routes' 10 | 11 | Vue.use(Electron) 12 | Vue.use(Resource) 13 | Vue.use(Router) 14 | Vue.use(ElementUI) 15 | 16 | Vue.config.debug = true 17 | 18 | const router = new Router({ 19 | scrollBehavior: () => ({ 20 | y: 0 21 | }), 22 | routes 23 | }) 24 | 25 | /* eslint-disable no-new */ 26 | new Vue({ 27 | router, 28 | ...App 29 | }).$mount('#app') 30 | -------------------------------------------------------------------------------- /app/src/model/Player.js: -------------------------------------------------------------------------------- 1 | import electron from 'electron' 2 | import path from 'path' 3 | import fs from '../lib/fs' 4 | import config from '../config' 5 | import uuid from 'uuid/v4' 6 | import Prize from './Prize' 7 | 8 | let list = null 9 | 10 | export default class Player { 11 | name = '' 12 | photoSrc = '' 13 | 14 | static fromDict(dict) { 15 | let ret = new Player() 16 | ret.name = dict.name 17 | ret.photoSrc = dict.photoSrc 18 | return ret 19 | } 20 | 21 | static async loadList() { 22 | if (list === null) { 23 | list = [] 24 | let data = await fs.readFile(config.playersJSONFile, 'utf8') 25 | JSON.parse(data).forEach((row) => { 26 | list.push(Player.fromDict(row)) 27 | }) 28 | } 29 | return list 30 | } 31 | 32 | static async loadRemainList() { 33 | let prizes = await Prize.loadList() 34 | let names = [] 35 | prizes.forEach((prize) => { 36 | if (prize.playerName) { 37 | names.push(prize.playerName) 38 | } 39 | }) 40 | let list = await this.loadList() 41 | let ret = [] 42 | list.forEach((player) => { 43 | if (names.indexOf(player.name) === -1) { 44 | ret.push(player) 45 | } 46 | }) 47 | return ret 48 | } 49 | 50 | static async isNameExist(name) { 51 | let list = await this.loadList() 52 | let ret = false 53 | list.every((player) => { 54 | if (player.name === name) { 55 | ret = true 56 | return false 57 | } 58 | return true 59 | }) 60 | return ret 61 | } 62 | 63 | static async add(name, photoSrc) { 64 | let isNameExist = await this.isNameExist(name) 65 | if (isNameExist) { 66 | throw new Error('姓名已存在') 67 | } else { 68 | let players = await this.loadList() 69 | players.push(Player.fromDict({ 70 | name, 71 | photoSrc 72 | })) 73 | list = players 74 | await this.sync() 75 | } 76 | } 77 | 78 | static async delete(name) { 79 | let players = await this.loadList() 80 | let ret = [] 81 | players.forEach((player) => { 82 | if (player.name !== name) { 83 | ret.push(player) 84 | } 85 | }) 86 | list = ret 87 | await this.sync() 88 | } 89 | 90 | static async sync() { 91 | await fs.writeFile(config.playersJSONFile, JSON.stringify(list, null, 4)) 92 | } 93 | 94 | static async savePhoto(dataUrl, ext) { 95 | let buf = new Buffer(dataUrl.replace(/^data:image\/(png|gif|jpg|jpeg);base64,/, ''), 'base64') 96 | let filePath = path.join(config.photosDir, uuid() + '.' + ext) 97 | await fs.writeFile(filePath, buf) 98 | return filePath 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/src/model/Prize.js: -------------------------------------------------------------------------------- 1 | import electron from 'electron' 2 | import path from 'path' 3 | import fs from '../lib/fs' 4 | import config from '../config' 5 | import uuid from 'uuid/v4' 6 | 7 | let list = null 8 | 9 | export default class Prize { 10 | name = '' 11 | giftName = '' 12 | giftPrice = 0 13 | giftPhotoSrc = '' 14 | done = false 15 | playerName = '' 16 | 17 | static fromDict(dict) { 18 | let ret = new Prize() 19 | ret.name = dict.name 20 | ret.giftName = dict.giftName 21 | ret.giftPrice = dict.giftPrice 22 | ret.giftPhotoSrc = dict.giftPhotoSrc 23 | ret.done = dict.done 24 | ret.playerName = dict.playerName 25 | return ret 26 | } 27 | 28 | static async findByName(name) { 29 | let list = await this.loadList() 30 | let ret = null 31 | list.every((prize) => { 32 | if (name === prize.name) { 33 | ret = prize 34 | return false 35 | } 36 | return true 37 | }) 38 | if (ret) { 39 | return ret 40 | } else { 41 | throw new Error('人员不存在') 42 | } 43 | } 44 | 45 | static async loadList() { 46 | if (list === null) { 47 | list = [] 48 | let data = await fs.readFile(config.prizesJSONFile, 'utf8') 49 | JSON.parse(data).forEach((row) => { 50 | list.push(Prize.fromDict(row)) 51 | }) 52 | } 53 | return list 54 | } 55 | 56 | static async isNameExist(name) { 57 | let list = await this.loadList() 58 | let ret = false 59 | list.every((prize) => { 60 | if (prize.name === name) { 61 | ret = true 62 | return false 63 | } 64 | return true 65 | }) 66 | return ret 67 | } 68 | 69 | static async add(json) { 70 | let isNameExist = await this.isNameExist(json.name) 71 | if (isNameExist) { 72 | throw new Error('奖项名称已存在') 73 | } else { 74 | json.giftPhotoSrc = await Prize.savePhoto(json.giftPhotoDataUrl, json.giftPhotoExt) 75 | let prizes = await this.loadList() 76 | prizes.push(Prize.fromDict(json)) 77 | list = prizes 78 | await this.sync() 79 | } 80 | } 81 | 82 | static async delete(name) { 83 | let prizes = await this.loadList() 84 | let ret = [] 85 | prizes.forEach((prize) => { 86 | if (prize.name !== name) { 87 | ret.push(prize) 88 | } 89 | }) 90 | list = ret 91 | await this.sync() 92 | } 93 | 94 | static async sync() { 95 | await fs.writeFile(config.prizesJSONFile, JSON.stringify(list, null, 4)) 96 | } 97 | 98 | static async savePhoto(dataUrl, ext) { 99 | let buf = new Buffer(dataUrl.replace(/^data:image\/(png|gif|jpg|jpeg);base64,/, ''), 'base64') 100 | let filePath = path.join(config.photosDir, uuid() + '.' + ext) 101 | await fs.writeFile(filePath, buf) 102 | return filePath 103 | } 104 | 105 | async save() { 106 | await Prize.sync() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /app/src/routes.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '', 4 | redirect: '/start' 5 | }, 6 | { 7 | path: '/start', 8 | name: 'start', 9 | component: require('components/Start') 10 | }, 11 | { 12 | path: '/prize/list', 13 | name: 'prize-list', 14 | component: require('components/prize/List') 15 | }, 16 | { 17 | path: '/prize/add', 18 | name: 'prize-add', 19 | component: require('components/prize/Add') 20 | }, 21 | { 22 | path: '/player/list', 23 | name: 'player-list', 24 | component: require('components/player/List') 25 | }, 26 | { 27 | path: '/player/add', 28 | name: 'player-add', 29 | component: require('components/player/Add') 30 | }, 31 | { 32 | path: '/prize/overview', 33 | name: 'prize-overview', 34 | component: require('components/prize/Overview') 35 | }, 36 | { 37 | path: '/prize/run', 38 | name: 'prize-run', 39 | component: require('components/prize/Run') 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /app/src/vuex/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutation-types' 2 | 3 | export const decrementMain = ({ commit }) => { 4 | commit(types.DECREMENT_MAIN_COUNTER) 5 | } 6 | 7 | export const incrementMain = ({ commit }) => { 8 | commit(types.INCREMENT_MAIN_COUNTER) 9 | } 10 | -------------------------------------------------------------------------------- /app/src/vuex/getters.js: -------------------------------------------------------------------------------- 1 | export const mainCounter = state => state.counters.main 2 | -------------------------------------------------------------------------------- /app/src/vuex/modules/counters.js: -------------------------------------------------------------------------------- 1 | import * as types from '../mutation-types' 2 | 3 | const state = { 4 | main: 0 5 | } 6 | 7 | const mutations = { 8 | [types.DECREMENT_MAIN_COUNTER] (state) { 9 | state.main-- 10 | }, 11 | [types.INCREMENT_MAIN_COUNTER] (state) { 12 | state.main++ 13 | } 14 | } 15 | 16 | export default { 17 | state, 18 | mutations 19 | } 20 | -------------------------------------------------------------------------------- /app/src/vuex/modules/index.js: -------------------------------------------------------------------------------- 1 | const files = require.context('.', false, /\.js$/) 2 | const modules = {} 3 | 4 | files.keys().forEach((key) => { 5 | if (key === './index.js') return 6 | modules[key.replace(/(\.\/|\.js)/g, '')] = files(key).default 7 | }) 8 | 9 | export default modules 10 | -------------------------------------------------------------------------------- /app/src/vuex/mutation-types.js: -------------------------------------------------------------------------------- 1 | export const DECREMENT_MAIN_COUNTER = 'DECREMENT_MAIN_COUNTER' 2 | export const INCREMENT_MAIN_COUNTER = 'INCREMENT_MAIN_COUNTER' 3 | -------------------------------------------------------------------------------- /app/src/vuex/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import * as actions from './actions' 4 | import * as getters from './getters' 5 | import modules from './modules' 6 | 7 | Vue.use(Vuex) 8 | 9 | export default new Vuex.Store({ 10 | actions, 11 | getters, 12 | modules, 13 | strict: true 14 | }) 15 | -------------------------------------------------------------------------------- /builds/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-garden/lottery/0ab2ec6de5a99d81fbe2fa2c69f900b81a81a1c3/builds/.gitkeep -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | 5 | let config = { 6 | // Name of electron app 7 | // Will be used in production builds 8 | name: 'Lottery', 9 | 10 | // Use ESLint (extends `none`) 11 | // Further changes can be made in `.eslintrc.js` 12 | eslint: true, 13 | 14 | // webpack-dev-server port 15 | port: 9080, 16 | 17 | // electron-packager options 18 | // Docs: https://simulatedgreg.gitbooks.io/electron-vue/content/docs/building_your_app.html 19 | building: { 20 | arch: 'x64', 21 | asar: true, 22 | dir: path.join(__dirname, 'app'), 23 | icon: path.join(__dirname, 'app/icons/icon'), 24 | ignore: /\b(node_modules|src|index\.ejs|icons)\b/, 25 | out: path.join(__dirname, 'builds'), 26 | overwrite: true, 27 | platform: process.env.PLATFORM_TARGET || 'all' 28 | } 29 | } 30 | 31 | config.building.name = config.name 32 | 33 | module.exports = config 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lottery", 3 | "version": "0.0.0", 4 | "description": "lottery", 5 | "scripts": { 6 | "build": "node tasks/release.js", 7 | "build:clean": "cross-env PLATFORM_TARGET=clean node tasks/release.js", 8 | "build:darwin": "cross-env PLATFORM_TARGET=darwin node tasks/release.js", 9 | "build:linux": "cross-env PLATFORM_TARGET=linux node tasks/release.js", 10 | "build:mas": "cross-env PLATFORM_TARGET=mas node tasks/release.js", 11 | "build:win32": "cross-env PLATFORM_TARGET=win32 node tasks/release.js", 12 | "dev": "node tasks/runner.js", 13 | "lint": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter app", 14 | "lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix app", 15 | "pack": "cross-env NODE_ENV=production webpack -p --progress --colors", 16 | "vue:route": "node tasks/vue/route.js", 17 | "vuex:module": "node tasks/vuex/module.js", 18 | "postinstall": "npm run lint:fix && cd app && npm install" 19 | }, 20 | "author": "Greg Holguin ", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "babel-core": "^6.8.0", 24 | "babel-eslint": "^7.0.0", 25 | "babel-loader": "^6.2.4", 26 | "babel-plugin-component": "^0.8.0", 27 | "babel-plugin-transform-async-to-generator": "^6.16.0", 28 | "babel-plugin-transform-runtime": "^6.8.0", 29 | "babel-preset-es2015": "^6.6.0", 30 | "babel-preset-stage-0": "^6.5.0", 31 | "babel-runtime": "^6.6.1", 32 | "cross-env": "^1.0.7", 33 | "css-loader": "^0.23.1", 34 | "del": "^2.2.1", 35 | "devtron": "^1.1.0", 36 | "electron": "^1.3.1", 37 | "electron-devtools-installer": "^1.1.4", 38 | "electron-packager": "^8.0.0", 39 | "electron-rebuild": "^1.1.3", 40 | "eslint": "^2.10.2", 41 | "eslint-friendly-formatter": "^2.0.5", 42 | "eslint-loader": "^1.3.0", 43 | "eslint-plugin-html": "^1.3.0", 44 | "extract-text-webpack-plugin": "^1.0.1", 45 | "file-loader": "^0.8.5", 46 | "html-webpack-plugin": "^2.16.1", 47 | "json-loader": "^0.5.4", 48 | "style-loader": "^0.13.1", 49 | "tree-kill": "^1.1.0", 50 | "url-loader": "^0.5.7", 51 | "vue-hot-reload-api": "^1.3.2", 52 | "vue-html-loader": "^1.2.2", 53 | "vue-loader": "^9.5.1", 54 | "vue-style-loader": "^1.0.0", 55 | "webpack": "^1.13.0", 56 | "webpack-dev-server": "^1.14.1" 57 | }, 58 | "dependencies": { 59 | "animate.css": "^3.5.2", 60 | "element-ui": "^1.1.4", 61 | "font-awesome": "^4.7.0", 62 | "jquery": "^3.1.1", 63 | "vue": "^2.1.8" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tasks/release.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const exec = require('child_process').exec 4 | const packager = require('electron-packager') 5 | 6 | if (process.env.PLATFORM_TARGET === 'clean') { 7 | require('del').sync(['builds/*', '!.gitkeep']) 8 | console.log('\x1b[33m`builds` directory cleaned.\n\x1b[0m') 9 | } else pack() 10 | 11 | /** 12 | * Build webpack in production 13 | */ 14 | function pack () { 15 | console.log('\x1b[33mBuilding webpack in production mode...\n\x1b[0m') 16 | let pack = exec('npm run pack') 17 | 18 | pack.stdout.on('data', data => console.log(data)) 19 | pack.stderr.on('data', data => console.error(data)) 20 | pack.on('exit', code => build()) 21 | } 22 | 23 | /** 24 | * Use electron-packager to build electron app 25 | */ 26 | function build () { 27 | let options = require('../config').building 28 | 29 | console.log('\x1b[34mBuilding electron app(s)...\n\x1b[0m') 30 | packager(options, (err, appPaths) => { 31 | if (err) { 32 | console.error('\x1b[31mError from `electron-packager` when building app...\x1b[0m') 33 | console.error(err) 34 | } else { 35 | console.log('Build(s) successful!') 36 | console.log(appPaths) 37 | 38 | console.log('\n\x1b[34mDONE\n\x1b[0m') 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /tasks/runner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Credits to https://github.com/bradstewart/electron-boilerplate-vue/blob/master/build/dev-runner.js 3 | */ 4 | 'use strict' 5 | 6 | const config = require('../config') 7 | const exec = require('child_process').exec 8 | const treeKill = require('tree-kill') 9 | 10 | let YELLOW = '\x1b[33m' 11 | let BLUE = '\x1b[34m' 12 | let END = '\x1b[0m' 13 | 14 | let isElectronOpen = false 15 | 16 | function format (command, data, color) { 17 | return color + command + END + 18 | ' ' + // Two space offset 19 | data.toString().trim().replace(/\n/g, '\n' + repeat(' ', command.length + 2)) + 20 | '\n' 21 | } 22 | 23 | function repeat (str, times) { 24 | return (new Array(times + 1)).join(str) 25 | } 26 | 27 | let children = [] 28 | 29 | function run (command, color, name) { 30 | let child = exec(command) 31 | 32 | child.stdout.on('data', data => { 33 | console.log(format(name, data, color)) 34 | 35 | /** 36 | * Start electron after VALID build 37 | * (prevents electron from opening a blank window that requires refreshing) 38 | * 39 | * NOTE: needs more testing for stability 40 | */ 41 | if (/VALID/g.test(data.toString().trim().replace(/\n/g, '\n' + repeat(' ', command.length + 2))) && !isElectronOpen) { 42 | console.log(`${BLUE}Starting electron...\n${END}`) 43 | run('cross-env NODE_ENV=development electron app/electron.js', BLUE, 'electron') 44 | isElectronOpen = true 45 | } 46 | }) 47 | 48 | child.stderr.on('data', data => console.error(format(name, data, color))) 49 | child.on('exit', code => exit(code)) 50 | 51 | children.push(child) 52 | } 53 | 54 | function exit (code) { 55 | children.forEach(child => { 56 | treeKill(child.pid) 57 | }) 58 | } 59 | 60 | console.log(`${YELLOW}Starting webpack-dev-server...\n${END}`) 61 | run(`webpack-dev-server --inline --hot --colors --port ${config.port} --content-base app/dist`, YELLOW, 'webpack') 62 | -------------------------------------------------------------------------------- /tasks/vue/route.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | let routeName = process.argv[2] 7 | let routes = fs.readFileSync( 8 | path.join(__dirname, '../../app/src/routes.js'), 9 | 'utf8' 10 | ).split('\n') 11 | let routeTemplate = fs.readFileSync( 12 | path.join(__dirname, 'route.template.txt'), 13 | 'utf8' 14 | ) 15 | let routesTemplate = fs.readFileSync( 16 | path.join(__dirname, 'routes.template.txt'), 17 | 'utf8' 18 | ) 19 | 20 | routes[routes.length - 3] = routes[routes.length - 3] + ',' 21 | routes.splice( 22 | routes.length - 2, 23 | 0, 24 | routesTemplate 25 | .replace(//g, routeName) 26 | .replace(/\n$/, '') 27 | ) 28 | 29 | fs.writeFileSync( 30 | path.join(__dirname, `../../app/src/components/${routeName}View.vue`), 31 | routeTemplate 32 | ) 33 | 34 | fs.mkdirSync(path.join(__dirname, `../../app/src/components/${routeName}View`)) 35 | 36 | fs.writeFileSync( 37 | path.join(__dirname, '../../app/src/routes.js'), 38 | routes.join('\n') 39 | ) 40 | 41 | console.log(`\n\x1b[33m[vue]\x1b[0m route "${routeName}" has been created`) 42 | console.log(' [ \n' + [ 43 | ' ' + path.join(__dirname, `../../app/src/components/${routeName}View.vue`), 44 | path.join(__dirname, `../../app/src/components/${routeName}View`), 45 | path.join(__dirname, '../../app/src/routes.js'), 46 | ].join(',\n ') + '\n ]') 47 | -------------------------------------------------------------------------------- /tasks/vue/route.template.txt: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /tasks/vue/routes.template.txt: -------------------------------------------------------------------------------- 1 | '/': { 2 | component: require('./components/View'), 3 | name: '' 4 | } 5 | -------------------------------------------------------------------------------- /tasks/vuex/module.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | let moduleName = process.argv[2] 7 | let template = fs.readFileSync( 8 | path.join(__dirname, 'module.template.txt'), 9 | 'utf8' 10 | ) 11 | 12 | fs.writeFileSync( 13 | path.join(__dirname, `../../app/src/vuex/modules/${moduleName}.js`), 14 | template 15 | ) 16 | 17 | console.log(`\n\x1b[33m[vuex]\x1b[0m module "${moduleName}" has been created`) 18 | console.log(path.join(__dirname, `../../app/src/vuex/modules/${moduleName}.js`)) 19 | -------------------------------------------------------------------------------- /tasks/vuex/module.template.txt: -------------------------------------------------------------------------------- 1 | import {} from '../mutation-types' 2 | 3 | const state = { 4 | all: [] 5 | } 6 | 7 | const mutations = { 8 | 9 | } 10 | 11 | export default { 12 | state, 13 | mutations 14 | } 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const pkg = require('./app/package.json') 5 | const settings = require('./config.js') 6 | const webpack = require('webpack') 7 | 8 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | 11 | let config = { 12 | devtool: '#eval-source-map', 13 | eslint: { 14 | formatter: require('eslint-friendly-formatter') 15 | }, 16 | entry: { 17 | build: path.join(__dirname, 'app/src/main.js') 18 | }, 19 | module: { 20 | preLoaders: [], 21 | loaders: [ 22 | { 23 | test: /\.css$/, 24 | loader: ExtractTextPlugin.extract('style-loader', 'css-loader') 25 | }, 26 | { 27 | test: /\.html$/, 28 | loader: 'vue-html-loader' 29 | }, 30 | { 31 | test: /\.js$/, 32 | loader: 'babel-loader', 33 | exclude: /node_modules/ 34 | }, 35 | { 36 | test: /\.json$/, 37 | loader: 'json-loader' 38 | }, 39 | { 40 | test: /\.vue$/, 41 | loader: 'vue-loader' 42 | }, 43 | { 44 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 45 | loader: 'url-loader', 46 | query: { 47 | limit: 10000, 48 | name: 'imgs/[name].[hash:7].[ext]' 49 | } 50 | }, 51 | { 52 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 53 | loader: 'url-loader', 54 | query: { 55 | limit: 10000, 56 | name: 'fonts/[name].[hash:7].[ext]' 57 | } 58 | }, 59 | { 60 | test: /\.(wav|mp3|mp4)(\?.*)?$/, 61 | loader: 'url-loader', 62 | query: { 63 | limit: 10000, 64 | name: 'multimedia/[name].[hash:7].[ext]' 65 | } 66 | } 67 | ] 68 | }, 69 | plugins: [ 70 | new ExtractTextPlugin('styles.css'), 71 | new HtmlWebpackPlugin({ 72 | filename: 'index.html', 73 | template: './app/index.ejs', 74 | title: settings.name 75 | }), 76 | new webpack.NoErrorsPlugin() 77 | ], 78 | output: { 79 | filename: '[name].js', 80 | path: path.join(__dirname, 'app/dist') 81 | }, 82 | resolve: { 83 | alias: { 84 | 'components': path.join(__dirname, 'app/src/components'), 85 | 'src': path.join(__dirname, 'app/src') 86 | }, 87 | extensions: ['', '.js', '.vue', '.json', '.css'], 88 | fallback: [path.join(__dirname, 'app/node_modules')] 89 | }, 90 | resolveLoader: { 91 | root: path.join(__dirname, 'node_modules') 92 | }, 93 | target: 'electron-renderer', 94 | vue: { 95 | loaders: { 96 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', 97 | scss: 'vue-style-loader!css-loader!sass-loader' 98 | } 99 | } 100 | } 101 | 102 | if (process.env.NODE_ENV !== 'production') { 103 | /** 104 | * Apply ESLint 105 | */ 106 | if (settings.eslint) { 107 | config.module.preLoaders.push( 108 | { 109 | test: /\.js$/, 110 | loader: 'eslint-loader', 111 | exclude: /node_modules/ 112 | }, 113 | { 114 | test: /\.vue$/, 115 | loader: 'eslint-loader' 116 | } 117 | ) 118 | } 119 | } 120 | 121 | /** 122 | * Adjust config for production settings 123 | */ 124 | if (process.env.NODE_ENV === 'production') { 125 | config.devtool = '' 126 | 127 | config.plugins.push( 128 | new webpack.DefinePlugin({ 129 | 'process.env.NODE_ENV': '"production"' 130 | }), 131 | new webpack.optimize.OccurenceOrderPlugin(), 132 | new webpack.optimize.UglifyJsPlugin({ 133 | compress: { 134 | warnings: false 135 | } 136 | }) 137 | ) 138 | } 139 | 140 | module.exports = config 141 | --------------------------------------------------------------------------------