├── .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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
11 |
19 |
20 |
21 |
22 |
68 |
69 |
108 |
--------------------------------------------------------------------------------
/app/src/components/Start.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
26 |
27 |
58 |
--------------------------------------------------------------------------------
/app/src/components/player/Add.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 开始
6 | 参与者列表
7 | 添加参与者
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 取消
19 | 确定
20 |
21 |
22 |
23 |
24 |
25 |
26 |
87 |
88 |
118 |
--------------------------------------------------------------------------------
/app/src/components/player/List.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 开始
6 | 参与者列表
7 |
8 |
9 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
95 |
96 |
136 |
--------------------------------------------------------------------------------
/app/src/components/prize/Add.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 开始
6 | 奖项列表
7 | 添加奖项
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 取消
25 | 确定
26 |
27 |
28 |
29 |
30 |
31 |
32 |
97 |
98 |
128 |
129 | width: 100%;
130 | }
131 |
132 |
--------------------------------------------------------------------------------
/app/src/components/prize/List.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 开始
6 | 奖项列表
7 |
8 |
9 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
75 |
76 |
110 |
--------------------------------------------------------------------------------
/app/src/components/prize/Overview.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 开始
6 | 奖项概览
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
![]()
15 |
16 |
17 |
18 |
19 | {{ col.name }}
20 |
21 |
22 | {{ col.giftName }}
23 |
24 |
25 |
26 |
27 | 待开奖
28 |
29 |
30 | 开始
31 |
32 |
33 |
34 |
35 | 中奖者
36 |
37 |
38 | {{ col.playerName }}
39 |
40 |
41 |
42 |
43 | 重新开奖
44 |
45 |
46 | 重置奖项
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
138 |
139 |
194 |
--------------------------------------------------------------------------------
/app/src/components/prize/Run.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 开始
6 | 奖项概览
7 | 开奖
8 |
9 |
10 |
11 | 🎁 {{ prize.name }}: {{ prize.giftName }}
12 |
13 |
14 |
15 | 参与者:
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 👏💐 恭喜
26 | 🎁 {{ $route.query.name }}
27 | 得主
28 |
29 | {{ player.name }}
30 |
31 |
![]()
32 |
33 |
34 | 完成
35 |
36 |
37 |
38 |
39 |
40 |
41 |
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 |
6 |
7 |
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 |
--------------------------------------------------------------------------------