├── .babelrc
├── .electron-vue
├── build.config.js
├── build.js
├── dev-client.js
├── dev-runner.js
├── webpack.main.config.js
├── webpack.renderer.config.js
└── webpack.web.config.js
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── README.md
├── build
└── icons
│ ├── 256x256.png
│ ├── icon.icns
│ └── icon.ico
├── package-lock.json
├── package.json
├── screenshot
└── app.gif
├── src
├── index.ejs
├── main
│ ├── index.dev.js
│ └── index.js
└── renderer
│ ├── App.vue
│ ├── components
│ └── optional.vue
│ ├── libs
│ └── constant.js
│ ├── main.js
│ ├── router
│ └── index.js
│ ├── scss
│ ├── app.scss
│ ├── index.scss
│ ├── optional.scss
│ └── stock.scss
│ ├── store
│ ├── index.js
│ └── modules
│ │ └── index.js
│ └── view
│ ├── index.vue
│ └── stock.vue
└── static
├── .gitkeep
└── stock.ico
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "comments": false,
3 | "env": {
4 | "main": {
5 | "presets": [
6 | [
7 | "env",
8 | {
9 | "targets": {
10 | "node": 7
11 | }
12 | }
13 | ],
14 | "stage-0"
15 | ]
16 | },
17 | "renderer": {
18 | "presets": [
19 | [
20 | "env",
21 | {
22 | "modules": false
23 | }
24 | ],
25 | "stage-0"
26 | ]
27 | },
28 | "web": {
29 | "presets": [
30 | [
31 | "env",
32 | {
33 | "modules": false
34 | }
35 | ],
36 | "stage-0"
37 | ]
38 | }
39 | },
40 | "plugins": [
41 | "transform-runtime"
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/.electron-vue/build.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | /**
4 | * `electron-packager` options
5 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-packager.html
6 | */
7 | module.exports = {
8 | arch: 'x64',
9 | asar: true,
10 | dir: path.join(__dirname, '../'),
11 | icon: path.join(__dirname, '../build/icons/icon'),
12 | ignore: /(^\/(src|test|\.[a-z]+|README|yarn|static|dist\/web))|\.gitkeep/,
13 | out: path.join(__dirname, '../build'),
14 | overwrite: true,
15 | platform: process.env.BUILD_TARGET || 'all'
16 | }
17 |
--------------------------------------------------------------------------------
/.electron-vue/build.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.NODE_ENV = 'production'
4 |
5 | const { say } = require('cfonts')
6 | const chalk = require('chalk')
7 | const del = require('del')
8 | const packager = require('electron-packager')
9 | const webpack = require('webpack')
10 | const Multispinner = require('multispinner')
11 |
12 | const buildConfig = require('./build.config')
13 | const mainConfig = require('./webpack.main.config')
14 | const rendererConfig = require('./webpack.renderer.config')
15 | const webConfig = require('./webpack.web.config')
16 |
17 | const doneLog = chalk.bgGreen.white(' DONE ') + ' '
18 | const errorLog = chalk.bgRed.white(' ERROR ') + ' '
19 | const okayLog = chalk.bgBlue.white(' OKAY ') + ' '
20 | const isCI = process.env.CI || false
21 |
22 | if (process.env.BUILD_TARGET === 'clean') clean()
23 | else if (process.env.BUILD_TARGET === 'web') web()
24 | else build()
25 |
26 | function clean () {
27 | del.sync(['build/*', '!build/icons', '!build/icons/icon.*'])
28 | console.log(`\n${doneLog}\n`)
29 | process.exit()
30 | }
31 |
32 | function build () {
33 | greeting()
34 |
35 | del.sync(['dist/electron/*', '!.gitkeep'])
36 |
37 | const tasks = ['main', 'renderer']
38 | const m = new Multispinner(tasks, {
39 | preText: 'building',
40 | postText: 'process'
41 | })
42 |
43 | let results = ''
44 |
45 | m.on('success', () => {
46 | process.stdout.write('\x1B[2J\x1B[0f')
47 | console.log(`\n\n${results}`)
48 | console.log(`${okayLog}take it away ${chalk.yellow('`electron-packager`')}\n`)
49 | bundleApp()
50 | })
51 |
52 | pack(mainConfig).then(result => {
53 | results += result + '\n\n'
54 | m.success('main')
55 | }).catch(err => {
56 | m.error('main')
57 | console.log(`\n ${errorLog}failed to build main process`)
58 | console.error(`\n${err}\n`)
59 | process.exit(1)
60 | })
61 |
62 | pack(rendererConfig).then(result => {
63 | results += result + '\n\n'
64 | m.success('renderer')
65 | }).catch(err => {
66 | m.error('renderer')
67 | console.log(`\n ${errorLog}failed to build renderer process`)
68 | console.error(`\n${err}\n`)
69 | process.exit(1)
70 | })
71 | }
72 |
73 | function pack (config) {
74 | return new Promise((resolve, reject) => {
75 | config.mode = 'production'
76 | webpack(config, (err, stats) => {
77 | if (err) reject(err.stack || err)
78 | else if (stats.hasErrors()) {
79 | let err = ''
80 |
81 | stats.toString({
82 | chunks: false,
83 | colors: true
84 | })
85 | .split(/\r?\n/)
86 | .forEach(line => {
87 | err += ` ${line}\n`
88 | })
89 |
90 | reject(err)
91 | } else {
92 | resolve(stats.toString({
93 | chunks: false,
94 | colors: true
95 | }))
96 | }
97 | })
98 | })
99 | }
100 |
101 | function bundleApp () {
102 | buildConfig.mode = 'production'
103 | packager(buildConfig, (err, appPaths) => {
104 | if (err) {
105 | console.log(`\n${errorLog}${chalk.yellow('`electron-packager`')} says...\n`)
106 | console.log(err + '\n')
107 | } else {
108 | console.log(`\n${doneLog}\n`)
109 | }
110 | })
111 | }
112 |
113 | function web () {
114 | del.sync(['dist/web/*', '!.gitkeep'])
115 | webConfig.mode = 'production'
116 | webpack(webConfig, (err, stats) => {
117 | if (err || stats.hasErrors()) console.log(err)
118 |
119 | console.log(stats.toString({
120 | chunks: false,
121 | colors: true
122 | }))
123 |
124 | process.exit()
125 | })
126 | }
127 |
128 | function greeting () {
129 | const cols = process.stdout.columns
130 | let text = ''
131 |
132 | if (cols > 85) text = 'lets-build'
133 | else if (cols > 60) text = 'lets-|build'
134 | else text = false
135 |
136 | if (text && !isCI) {
137 | say(text, {
138 | colors: ['yellow'],
139 | font: 'simple3d',
140 | space: false
141 | })
142 | } else console.log(chalk.yellow.bold('\n lets-build'))
143 | console.log()
144 | }
--------------------------------------------------------------------------------
/.electron-vue/dev-client.js:
--------------------------------------------------------------------------------
1 | const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
2 |
3 | hotClient.subscribe(event => {
4 | /**
5 | * Reload browser when HTMLWebpackPlugin emits a new index.html
6 | *
7 | * Currently disabled until jantimon/html-webpack-plugin#680 is resolved.
8 | * https://github.com/SimulatedGREG/electron-vue/issues/437
9 | * https://github.com/jantimon/html-webpack-plugin/issues/680
10 | */
11 | // if (event.action === 'reload') {
12 | // window.location.reload()
13 | // }
14 |
15 | /**
16 | * Notify `mainWindow` when `main` process is compiling,
17 | * giving notice for an expected reload of the `electron` process
18 | */
19 | if (event.action === 'compiling') {
20 | document.body.innerHTML += `
21 |
34 |
35 |
36 | Compiling Main Process...
37 |
38 | `
39 | }
40 | })
41 |
--------------------------------------------------------------------------------
/.electron-vue/dev-runner.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const chalk = require('chalk')
4 | const electron = require('electron')
5 | const path = require('path')
6 | const { say } = require('cfonts')
7 | const { spawn } = require('child_process')
8 | const webpack = require('webpack')
9 | const WebpackDevServer = require('webpack-dev-server')
10 | const webpackHotMiddleware = require('webpack-hot-middleware')
11 |
12 | const mainConfig = require('./webpack.main.config')
13 | const rendererConfig = require('./webpack.renderer.config')
14 |
15 | let electronProcess = null
16 | let manualRestart = false
17 | let hotMiddleware
18 |
19 | function logStats (proc, data) {
20 | let log = ''
21 |
22 | log += chalk.yellow.bold(`┏ ${proc} Process ${new Array((19 - proc.length) + 1).join('-')}`)
23 | log += '\n\n'
24 |
25 | if (typeof data === 'object') {
26 | data.toString({
27 | colors: true,
28 | chunks: false
29 | }).split(/\r?\n/).forEach(line => {
30 | log += ' ' + line + '\n'
31 | })
32 | } else {
33 | log += ` ${data}\n`
34 | }
35 |
36 | log += '\n' + chalk.yellow.bold(`┗ ${new Array(28 + 1).join('-')}`) + '\n'
37 |
38 | console.log(log)
39 | }
40 |
41 | function startRenderer () {
42 | return new Promise((resolve, reject) => {
43 | rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer)
44 | rendererConfig.mode = 'development'
45 | const compiler = webpack(rendererConfig)
46 | hotMiddleware = webpackHotMiddleware(compiler, {
47 | log: false,
48 | heartbeat: 2500
49 | })
50 |
51 | compiler.hooks.compilation.tap('compilation', compilation => {
52 | compilation.hooks.htmlWebpackPluginAfterEmit.tapAsync('html-webpack-plugin-after-emit', (data, cb) => {
53 | hotMiddleware.publish({ action: 'reload' })
54 | cb()
55 | })
56 | })
57 |
58 | compiler.hooks.done.tap('done', stats => {
59 | logStats('Renderer', stats)
60 | })
61 |
62 | const server = new WebpackDevServer(
63 | compiler,
64 | {
65 | contentBase: path.join(__dirname, '../'),
66 | quiet: true,
67 | before (app, ctx) {
68 | app.use(hotMiddleware)
69 | ctx.middleware.waitUntilValid(() => {
70 | resolve()
71 | })
72 | }
73 | }
74 | )
75 |
76 | server.listen(9080)
77 | })
78 | }
79 |
80 | function startMain () {
81 | return new Promise((resolve, reject) => {
82 | mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main)
83 | mainConfig.mode = 'development'
84 | const compiler = webpack(mainConfig)
85 |
86 | compiler.hooks.watchRun.tapAsync('watch-run', (compilation, done) => {
87 | logStats('Main', chalk.white.bold('compiling...'))
88 | hotMiddleware.publish({ action: 'compiling' })
89 | done()
90 | })
91 |
92 | compiler.watch({}, (err, stats) => {
93 | if (err) {
94 | console.log(err)
95 | return
96 | }
97 |
98 | logStats('Main', stats)
99 |
100 | if (electronProcess && electronProcess.kill) {
101 | manualRestart = true
102 | process.kill(electronProcess.pid)
103 | electronProcess = null
104 | startElectron()
105 |
106 | setTimeout(() => {
107 | manualRestart = false
108 | }, 5000)
109 | }
110 |
111 | resolve()
112 | })
113 | })
114 | }
115 |
116 | function startElectron () {
117 | var args = [
118 | '--inspect=5858',
119 | path.join(__dirname, '../dist/electron/main.js')
120 | ]
121 |
122 | // detect yarn or npm and process commandline args accordingly
123 | if (process.env.npm_execpath.endsWith('yarn.js')) {
124 | args = args.concat(process.argv.slice(3))
125 | } else if (process.env.npm_execpath.endsWith('npm-cli.js')) {
126 | args = args.concat(process.argv.slice(2))
127 | }
128 |
129 | electronProcess = spawn(electron, args)
130 |
131 | electronProcess.stdout.on('data', data => {
132 | electronLog(data, 'blue')
133 | })
134 | electronProcess.stderr.on('data', data => {
135 | electronLog(data, 'red')
136 | })
137 |
138 | electronProcess.on('close', () => {
139 | if (!manualRestart) process.exit()
140 | })
141 | }
142 |
143 | function electronLog (data, color) {
144 | let log = ''
145 | data = data.toString().split(/\r?\n/)
146 | data.forEach(line => {
147 | log += ` ${line}\n`
148 | })
149 | if (/[0-9A-z]+/.test(log)) {
150 | console.log(
151 | chalk[color].bold('┏ Electron -------------------') +
152 | '\n\n' +
153 | log +
154 | chalk[color].bold('┗ ----------------------------') +
155 | '\n'
156 | )
157 | }
158 | }
159 |
160 | function greeting () {
161 | const cols = process.stdout.columns
162 | let text = ''
163 |
164 | if (cols > 104) text = 'electron-vue'
165 | else if (cols > 76) text = 'electron-|vue'
166 | else text = false
167 |
168 | if (text) {
169 | say(text, {
170 | colors: ['yellow'],
171 | font: 'simple3d',
172 | space: false
173 | })
174 | } else console.log(chalk.yellow.bold('\n electron-vue'))
175 | console.log(chalk.blue(' getting ready...') + '\n')
176 | }
177 |
178 | function init () {
179 | greeting()
180 |
181 | Promise.all([startRenderer(), startMain()])
182 | .then(() => {
183 | startElectron()
184 | })
185 | .catch(err => {
186 | console.error(err)
187 | })
188 | }
189 |
190 | init()
191 |
--------------------------------------------------------------------------------
/.electron-vue/webpack.main.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.BABEL_ENV = 'main'
4 |
5 | const path = require('path')
6 | const { dependencies } = require('../package.json')
7 | const webpack = require('webpack')
8 |
9 | const BabiliWebpackPlugin = require('babili-webpack-plugin')
10 |
11 | let mainConfig = {
12 | entry: {
13 | main: path.join(__dirname, '../src/main/index.js')
14 | },
15 | externals: [
16 | ...Object.keys(dependencies || {})
17 | ],
18 | module: {
19 | rules: [
20 | {
21 | test: /\.(js)$/,
22 | enforce: 'pre',
23 | exclude: /node_modules/,
24 | use: {
25 | loader: 'eslint-loader',
26 | options: {
27 | formatter: require('eslint-friendly-formatter')
28 | }
29 | }
30 | },
31 | {
32 | test: /\.js$/,
33 | use: 'babel-loader',
34 | exclude: /node_modules/
35 | },
36 | {
37 | test: /\.node$/,
38 | use: 'node-loader'
39 | }
40 | ]
41 | },
42 | node: {
43 | __dirname: process.env.NODE_ENV !== 'production',
44 | __filename: process.env.NODE_ENV !== 'production'
45 | },
46 | output: {
47 | filename: '[name].js',
48 | libraryTarget: 'commonjs2',
49 | path: path.join(__dirname, '../dist/electron')
50 | },
51 | plugins: [
52 | new webpack.NoEmitOnErrorsPlugin()
53 | ],
54 | resolve: {
55 | extensions: ['.js', '.json', '.node']
56 | },
57 | target: 'electron-main'
58 | }
59 |
60 | /**
61 | * Adjust mainConfig for development settings
62 | */
63 | if (process.env.NODE_ENV !== 'production') {
64 | mainConfig.plugins.push(
65 | new webpack.DefinePlugin({
66 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`
67 | })
68 | )
69 | }
70 |
71 | /**
72 | * Adjust mainConfig for production settings
73 | */
74 | if (process.env.NODE_ENV === 'production') {
75 | mainConfig.plugins.push(
76 | new BabiliWebpackPlugin(),
77 | new webpack.DefinePlugin({
78 | 'process.env.NODE_ENV': '"production"'
79 | })
80 | )
81 | }
82 |
83 | module.exports = mainConfig
84 |
--------------------------------------------------------------------------------
/.electron-vue/webpack.renderer.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.BABEL_ENV = 'renderer'
4 |
5 | const path = require('path')
6 | const { dependencies } = require('../package.json')
7 | const webpack = require('webpack')
8 |
9 | const BabiliWebpackPlugin = require('babili-webpack-plugin')
10 | const CopyWebpackPlugin = require('copy-webpack-plugin')
11 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
12 | const HtmlWebpackPlugin = require('html-webpack-plugin')
13 | const { VueLoaderPlugin } = require('vue-loader')
14 |
15 | /**
16 | * List of node_modules to include in webpack bundle
17 | *
18 | * Required for specific packages like Vue UI libraries
19 | * that provide pure *.vue files that need compiling
20 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals
21 | */
22 | let whiteListedModules = ['vue']
23 |
24 | let rendererConfig = {
25 | devtool: '#cheap-module-eval-source-map',
26 | entry: {
27 | renderer: path.join(__dirname, '../src/renderer/main.js')
28 | },
29 | externals: [
30 | ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d))
31 | ],
32 | module: {
33 | rules: [
34 | {
35 | test: /\.(js|vue)$/,
36 | enforce: 'pre',
37 | exclude: /node_modules/,
38 | use: {
39 | loader: 'eslint-loader',
40 | options: {
41 | formatter: require('eslint-friendly-formatter')
42 | }
43 | }
44 | },
45 | {
46 | test: /\.scss$/,
47 | use: ['vue-style-loader', 'css-loader', 'sass-loader']
48 | },
49 | {
50 | test: /\.sass$/,
51 | use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax']
52 | },
53 | {
54 | test: /\.less$/,
55 | use: ['vue-style-loader', 'css-loader', 'less-loader']
56 | },
57 | {
58 | test: /\.css$/,
59 | use: ['vue-style-loader', 'css-loader']
60 | },
61 | {
62 | test: /\.html$/,
63 | use: 'vue-html-loader'
64 | },
65 | {
66 | test: /\.js$/,
67 | use: 'babel-loader',
68 | exclude: /node_modules/
69 | },
70 | {
71 | test: /\.node$/,
72 | use: 'node-loader'
73 | },
74 | {
75 | test: /\.vue$/,
76 | use: {
77 | loader: 'vue-loader',
78 | options: {
79 | extractCSS: process.env.NODE_ENV === 'production',
80 | loaders: {
81 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
82 | scss: 'vue-style-loader!css-loader!sass-loader',
83 | less: 'vue-style-loader!css-loader!less-loader'
84 | }
85 | }
86 | }
87 | },
88 | {
89 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
90 | use: {
91 | loader: 'url-loader',
92 | query: {
93 | limit: 10000,
94 | name: 'imgs/[name]--[folder].[ext]'
95 | }
96 | }
97 | },
98 | {
99 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
100 | loader: 'url-loader',
101 | options: {
102 | limit: 10000,
103 | name: 'media/[name]--[folder].[ext]'
104 | }
105 | },
106 | {
107 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
108 | use: {
109 | loader: 'url-loader',
110 | query: {
111 | limit: 10000,
112 | name: 'fonts/[name]--[folder].[ext]'
113 | }
114 | }
115 | }
116 | ]
117 | },
118 | node: {
119 | __dirname: process.env.NODE_ENV !== 'production',
120 | __filename: process.env.NODE_ENV !== 'production'
121 | },
122 | plugins: [
123 | new VueLoaderPlugin(),
124 | new MiniCssExtractPlugin({filename: 'styles.css'}),
125 | new HtmlWebpackPlugin({
126 | filename: 'index.html',
127 | template: path.resolve(__dirname, '../src/index.ejs'),
128 | minify: {
129 | collapseWhitespace: true,
130 | removeAttributeQuotes: true,
131 | removeComments: true
132 | },
133 | nodeModules: process.env.NODE_ENV !== 'production'
134 | ? path.resolve(__dirname, '../node_modules')
135 | : false
136 | }),
137 | new webpack.HotModuleReplacementPlugin(),
138 | new webpack.NoEmitOnErrorsPlugin()
139 | ],
140 | output: {
141 | filename: '[name].js',
142 | libraryTarget: 'commonjs2',
143 | path: path.join(__dirname, '../dist/electron')
144 | },
145 | resolve: {
146 | alias: {
147 | '@': path.join(__dirname, '../src/renderer'),
148 | 'vue$': 'vue/dist/vue.esm.js'
149 | },
150 | extensions: ['.js', '.vue', '.json', '.css', '.node']
151 | },
152 | target: 'electron-renderer'
153 | }
154 |
155 | /**
156 | * Adjust rendererConfig for development settings
157 | */
158 | if (process.env.NODE_ENV !== 'production') {
159 | rendererConfig.plugins.push(
160 | new webpack.DefinePlugin({
161 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`
162 | })
163 | )
164 | }
165 |
166 | /**
167 | * Adjust rendererConfig for production settings
168 | */
169 | if (process.env.NODE_ENV === 'production') {
170 | rendererConfig.devtool = ''
171 |
172 | rendererConfig.plugins.push(
173 | new BabiliWebpackPlugin(),
174 | new CopyWebpackPlugin([
175 | {
176 | from: path.join(__dirname, '../static'),
177 | to: path.join(__dirname, '../dist/electron/static'),
178 | ignore: ['.*']
179 | }
180 | ]),
181 | new webpack.DefinePlugin({
182 | 'process.env.NODE_ENV': '"production"'
183 | }),
184 | new webpack.LoaderOptionsPlugin({
185 | minimize: true
186 | })
187 | )
188 | }
189 |
190 | module.exports = rendererConfig
191 |
--------------------------------------------------------------------------------
/.electron-vue/webpack.web.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.BABEL_ENV = 'web'
4 |
5 | const path = require('path')
6 | const webpack = require('webpack')
7 |
8 | const BabiliWebpackPlugin = require('babili-webpack-plugin')
9 | const CopyWebpackPlugin = require('copy-webpack-plugin')
10 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
11 | const HtmlWebpackPlugin = require('html-webpack-plugin')
12 | const { VueLoaderPlugin } = require('vue-loader')
13 |
14 | let webConfig = {
15 | devtool: '#cheap-module-eval-source-map',
16 | entry: {
17 | web: path.join(__dirname, '../src/renderer/main.js')
18 | },
19 | module: {
20 | rules: [
21 | {
22 | test: /\.(js|vue)$/,
23 | enforce: 'pre',
24 | exclude: /node_modules/,
25 | use: {
26 | loader: 'eslint-loader',
27 | options: {
28 | formatter: require('eslint-friendly-formatter')
29 | }
30 | }
31 | },
32 | {
33 | test: /\.scss$/,
34 | use: ['vue-style-loader', 'css-loader', 'sass-loader']
35 | },
36 | {
37 | test: /\.sass$/,
38 | use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax']
39 | },
40 | {
41 | test: /\.less$/,
42 | use: ['vue-style-loader', 'css-loader', 'less-loader']
43 | },
44 | {
45 | test: /\.css$/,
46 | use: ['vue-style-loader', 'css-loader']
47 | },
48 | {
49 | test: /\.html$/,
50 | use: 'vue-html-loader'
51 | },
52 | {
53 | test: /\.js$/,
54 | use: 'babel-loader',
55 | include: [ path.resolve(__dirname, '../src/renderer') ],
56 | exclude: /node_modules/
57 | },
58 | {
59 | test: /\.vue$/,
60 | use: {
61 | loader: 'vue-loader',
62 | options: {
63 | extractCSS: true,
64 | loaders: {
65 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
66 | scss: 'vue-style-loader!css-loader!sass-loader',
67 | less: 'vue-style-loader!css-loader!less-loader'
68 | }
69 | }
70 | }
71 | },
72 | {
73 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
74 | use: {
75 | loader: 'url-loader',
76 | query: {
77 | limit: 10000,
78 | name: 'imgs/[name].[ext]'
79 | }
80 | }
81 | },
82 | {
83 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
84 | use: {
85 | loader: 'url-loader',
86 | query: {
87 | limit: 10000,
88 | name: 'fonts/[name].[ext]'
89 | }
90 | }
91 | }
92 | ]
93 | },
94 | plugins: [
95 | new VueLoaderPlugin(),
96 | new MiniCssExtractPlugin({filename: 'styles.css'}),
97 | new HtmlWebpackPlugin({
98 | filename: 'index.html',
99 | template: path.resolve(__dirname, '../src/index.ejs'),
100 | minify: {
101 | collapseWhitespace: true,
102 | removeAttributeQuotes: true,
103 | removeComments: true
104 | },
105 | nodeModules: false
106 | }),
107 | new webpack.DefinePlugin({
108 | 'process.env.IS_WEB': 'true'
109 | }),
110 | new webpack.HotModuleReplacementPlugin(),
111 | new webpack.NoEmitOnErrorsPlugin()
112 | ],
113 | output: {
114 | filename: '[name].js',
115 | path: path.join(__dirname, '../dist/web')
116 | },
117 | resolve: {
118 | alias: {
119 | '@': path.join(__dirname, '../src/renderer'),
120 | 'vue$': 'vue/dist/vue.esm.js'
121 | },
122 | extensions: ['.js', '.vue', '.json', '.css']
123 | },
124 | target: 'web'
125 | }
126 |
127 | /**
128 | * Adjust webConfig for production settings
129 | */
130 | if (process.env.NODE_ENV === 'production') {
131 | webConfig.devtool = ''
132 |
133 | webConfig.plugins.push(
134 | new BabiliWebpackPlugin(),
135 | new CopyWebpackPlugin([
136 | {
137 | from: path.join(__dirname, '../static'),
138 | to: path.join(__dirname, '../dist/web/static'),
139 | ignore: ['.*']
140 | }
141 | ]),
142 | new webpack.DefinePlugin({
143 | 'process.env.NODE_ENV': '"production"'
144 | }),
145 | new webpack.LoaderOptionsPlugin({
146 | minimize: true
147 | })
148 | )
149 | }
150 |
151 | module.exports = webConfig
152 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinhaoxx/stock-viewer-tool/715a1671c8bfe92118b80a88c3370e5626811d32/.eslintignore
--------------------------------------------------------------------------------
/.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 | extends: 'standard',
12 | globals: {
13 | __static: true
14 | },
15 | plugins: [
16 | 'html'
17 | ],
18 | 'rules': {
19 | // allow paren-less arrow functions
20 | 'arrow-parens': 0,
21 | // allow async-await
22 | 'generator-star-spacing': 0,
23 | // allow debugger during development
24 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | dist/electron/*
3 | dist/web/*
4 | build/*
5 | !build/icons
6 | node_modules/
7 | npm-debug.log
8 | npm-debug.log.*
9 | thumbs.db
10 | !.gitkeep
11 | \.idea/
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > 一款基于 electron + vue + element-ui 的实时股票股价浏览工具
2 | > 目前在 windows 上能正常运行,除了部分已知问题
3 |
4 | > 如果只需要查看实时股价的话,那么这工具应该比较适合你
5 | > 相比在各大网站或者股票软件或者app,查看效率会比较高
6 |
7 | > 如果感兴趣或者能对你有所帮助的话,可以点一下 star :)
8 |
9 | ## 功能特点
10 | - 支持沪深港
11 | - 支持窗口贴顶自动缩起(类似QQ)
12 | - 映射数字+英文键,无需点击添加按钮即可快捷添加自选
13 | - 支持自选行拖拽排序功能
14 | - 右键点击个股可快速前往雪球或股吧查看个股
15 | - 股票详情(部分)
16 | - 实时K线图(不可移动查看详情)
17 | - 买卖
18 | - 实时成交
19 |
20 |
21 | ## 程序预览
22 | 
23 |
24 | ## 下载使用
25 | > 已经发布到了 Release 中,下载后打开即可使用
26 |
27 | ## 技术栈及依赖
28 | - electron(electron-vue)
29 | - vue
30 | - element-ui
31 | - sortable-js
32 | - mousetrap
33 |
34 | ## 开发中
35 | - 实时K线图(移动查看详情)
36 | - 港股的详情显示
37 | - 隐藏个股详情窗口
38 | - 其他功能...
39 |
40 | ## 已知 BUG
41 | - Windows下如果调整了显示倍率(>=125%),那么拖拽顶栏时会出现无限增加宽度的问题
42 |
43 | ## 启动构建
44 | ````bash
45 | # 安装项目依赖
46 | npm install
47 |
48 | # 启动项目
49 | npm run dev
50 |
51 | # 构建项目
52 | npm run build
53 | ````
54 |
--------------------------------------------------------------------------------
/build/icons/256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinhaoxx/stock-viewer-tool/715a1671c8bfe92118b80a88c3370e5626811d32/build/icons/256x256.png
--------------------------------------------------------------------------------
/build/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinhaoxx/stock-viewer-tool/715a1671c8bfe92118b80a88c3370e5626811d32/build/icons/icon.icns
--------------------------------------------------------------------------------
/build/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinhaoxx/stock-viewer-tool/715a1671c8bfe92118b80a88c3370e5626811d32/build/icons/icon.ico
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stockviewertool",
3 | "productName": "stockviewertool",
4 | "version": "0.0.7",
5 | "author": "",
6 | "description": "基于 Electron + Vue 的实时股价查看器(工具)",
7 | "license": null,
8 | "main": "./dist/electron/main.js",
9 | "scripts": {
10 | "build": "node .electron-vue/build.js",
11 | "build:darwin": "cross-env BUILD_TARGET=darwin node .electron-vue/build.js",
12 | "build:linux": "cross-env BUILD_TARGET=linux node .electron-vue/build.js",
13 | "build:mas": "cross-env BUILD_TARGET=mas node .electron-vue/build.js",
14 | "build:win32": "cross-env BUILD_TARGET=win32 node .electron-vue/build.js",
15 | "build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js",
16 | "build:web": "cross-env BUILD_TARGET=web node .electron-vue/build.js",
17 | "dev": "node .electron-vue/dev-runner.js",
18 | "lint": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter src",
19 | "lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix src",
20 | "pack": "npm run pack:main && npm run pack:renderer",
21 | "pack:main": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.main.config.js",
22 | "pack:renderer": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.renderer.config.js",
23 | "postinstall": "npm run lint:fix"
24 | },
25 | "dependencies": {
26 | "axios": "^0.18.0",
27 | "electron-drag": "^1.2.2",
28 | "sortablejs": "^1.8.4",
29 | "vue": "^2.5.16",
30 | "vue-electron": "^1.0.6",
31 | "vue-router": "^3.0.1",
32 | "vue-toasted": "^1.1.26",
33 | "vuex": "^3.0.1",
34 | "vuex-electron": "^1.0.0"
35 | },
36 | "devDependencies": {
37 | "ajv": "^6.5.0",
38 | "babel-core": "^6.26.3",
39 | "babel-eslint": "^8.2.3",
40 | "babel-loader": "^7.1.4",
41 | "babel-plugin-component": "^1.1.1",
42 | "babel-plugin-transform-runtime": "^6.23.0",
43 | "babel-preset-env": "^1.7.0",
44 | "babel-preset-stage-0": "^6.24.1",
45 | "babel-register": "^6.26.0",
46 | "babili-webpack-plugin": "^0.1.2",
47 | "cfonts": "^2.1.2",
48 | "chalk": "^2.4.1",
49 | "copy-webpack-plugin": "^4.5.1",
50 | "cross-env": "^5.1.6",
51 | "css-loader": "^0.28.11",
52 | "del": "^3.0.0",
53 | "devtron": "^1.4.0",
54 | "electron": "^4.1.1",
55 | "electron-debug": "^1.5.0",
56 | "electron-devtools-installer": "^2.2.4",
57 | "electron-packager": "^12.1.0",
58 | "electron-rebuild": "^1.8.1",
59 | "element-ui": "^2.6.3",
60 | "eslint": "^4.19.1",
61 | "eslint-config-standard": "^11.0.0",
62 | "eslint-friendly-formatter": "^4.0.1",
63 | "eslint-loader": "^2.0.0",
64 | "eslint-plugin-html": "^4.0.3",
65 | "eslint-plugin-import": "^2.12.0",
66 | "eslint-plugin-node": "^6.0.1",
67 | "eslint-plugin-promise": "^3.8.0",
68 | "eslint-plugin-standard": "^3.1.0",
69 | "file-loader": "^1.1.11",
70 | "html-webpack-plugin": "^3.2.0",
71 | "mini-css-extract-plugin": "0.4.0",
72 | "mousetrap": "^1.6.3",
73 | "multispinner": "^0.2.1",
74 | "node-loader": "^0.6.0",
75 | "node-sass": "^4.9.2",
76 | "sass-loader": "^7.0.3",
77 | "style-loader": "^0.21.0",
78 | "url-loader": "^1.0.1",
79 | "vue-html-loader": "^1.2.4",
80 | "vue-loader": "^15.2.4",
81 | "vue-style-loader": "^4.1.0",
82 | "vue-template-compiler": "^2.5.16",
83 | "webpack": "^4.15.1",
84 | "webpack-cli": "^3.0.8",
85 | "webpack-dev-server": "^3.1.4",
86 | "webpack-hot-middleware": "^2.22.2",
87 | "webpack-merge": "^4.1.3"
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/screenshot/app.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinhaoxx/stock-viewer-tool/715a1671c8bfe92118b80a88c3370e5626811d32/screenshot/app.gif
--------------------------------------------------------------------------------
/src/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | stocktools
6 |
7 | <% if (htmlWebpackPlugin.options.nodeModules) { %>
8 |
9 |
12 | <% } %>
13 |
14 |
15 |
16 |
17 | <% if (!process.browser) { %>
18 |
21 | <% } %>
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/main/index.dev.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is used specifically and only for development. It installs
3 | * `electron-debug` & `vue-devtools`. There shouldn't be any need to
4 | * modify this file, but it can be used to extend your development
5 | * environment.
6 | */
7 |
8 | /* eslint-disable */
9 |
10 | // Install `electron-debug` with `devtron`
11 | require('electron-debug')({ showDevTools: true })
12 |
13 | // Install `vue-devtools`
14 | require('electron').app.on('ready', () => {
15 | let installExtension = require('electron-devtools-installer')
16 | installExtension.default(installExtension.VUEJS_DEVTOOLS)
17 | .then(() => {})
18 | .catch(err => {
19 | console.log('Unable to install `vue-devtools`: \n', err)
20 | })
21 | })
22 |
23 | // Require `main` process to boot app
24 | require('./index')
--------------------------------------------------------------------------------
/src/main/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import {app, BrowserWindow, ipcMain, Tray, Menu, MenuItem} from 'electron'
4 | import path from 'path'
5 |
6 | // 静态路径 __static
7 | if (process.env.NODE_ENV !== 'development') {
8 | global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
9 | }
10 | // 关闭安全警告
11 | process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'
12 |
13 | // 入口路径(根据开发环境)
14 | const winURL = process.env.NODE_ENV === 'development'
15 | ? `http://localhost:9080`
16 | : `file://${__dirname}/index.html`
17 |
18 | // 初始化应用
19 | function init () {
20 | createSubWindow()
21 | createWindow()
22 | }
23 |
24 | /**
25 | * 创建主窗体
26 | */
27 | let mainWindow // 主窗体
28 | function createWindow () {
29 | mainWindow = new BrowserWindow({
30 | width: 420,
31 | height: 663,
32 | frame: false,
33 | transparent: true,
34 | alwaysOnTop: true,
35 | maximizable: false,
36 | resizable: false,
37 | skipTaskbar: true,
38 | focusable: true,
39 | webPreferences: {webSecurity: false},
40 | useContentSize: true
41 | })
42 | mainWindow.loadURL(winURL + '#/index') // 默认加载根路径
43 | mainWindow.on('closed', () => {
44 | app.quit()
45 | })
46 | createMainTray() // 创建系统托盘
47 | createMainIPCListener() // 监听渲染端事件
48 | }
49 |
50 | /**
51 | * app 事件监听
52 | */
53 | app.on('ready', init)
54 | app.on('activate', () => {
55 | if (mainWindow === null) {
56 | init()
57 | }
58 | })
59 | app.on('window-all-closed', () => {
60 | if (process.platform !== 'darwin') {
61 | app.quit()
62 | }
63 | })
64 |
65 | /**
66 | * 注册主窗体 IPC 事件
67 | */
68 | const createMainIPCListener = function () {
69 | // 缩小&关闭主窗体
70 | ipcMain.on('main-window-min', () => {
71 | mainWindow.minimize()
72 | })
73 | ipcMain.on('main-window-close', () => {
74 | app.quit()
75 | })
76 | // 主窗体贴顶缩起展开功能
77 | ipcMain.on('main-mouse-enter', mainMouseEnter)
78 | ipcMain.on('main-mouse-leave', mainMouseLeave)
79 | // 个股&指数右键菜单
80 | ipcMain.on('main-right-click', (event, code) => {
81 | const menu = new Menu()
82 | menu.append(new MenuItem({
83 | label: '在雪球中查看',
84 | click: () => {
85 | event.sender.send('show-xueqiu', code)
86 | }
87 | }))
88 | menu.append(new MenuItem({
89 | label: '在股吧中查看',
90 | click: () => {
91 | event.sender.send('show-guba', code)
92 | }
93 | }))
94 |
95 | const stocksIndex = ['sh000001', 'sz399001', 'sz399006']
96 | if (stocksIndex.indexOf(code.toLowerCase()) === -1) {
97 | menu.append(new MenuItem({type: 'separator'}))
98 | menu.append(new MenuItem({
99 | label: '快速置顶',
100 | click: () => {
101 | event.sender.send('place-top', code)
102 | }
103 | }))
104 | menu.append(new MenuItem({type: 'separator'}))
105 | menu.append(new MenuItem({
106 | label: '删除自选',
107 | click: () => {
108 | event.sender.send('delete-stock', code)
109 | }
110 | }))
111 | }
112 | menu.popup(mainWindow)
113 | })
114 | // 更新系统托盘 Tooltips
115 | ipcMain.on('main-tray-update', (event, arg) => {
116 | tray.setToolTip('自选小工具:\n' + arg)
117 | })
118 | // 开启开发者工具
119 | ipcMain.on('open-devtools', () => {
120 | mainWindow.openDevTools()
121 | })
122 | // 显示详情窗口
123 | ipcMain.on('create', (event, arg) => {
124 | if (!subWindow.isVisible()) subWindow.show()
125 | subWindow.webContents.send('change-code', arg)
126 | })
127 | }
128 |
129 | /**
130 | * 创建主窗体的系统托盘
131 | */
132 | let tray // 系统托盘
133 | const createMainTray = function () {
134 | // 创建系统托盘
135 | tray = new Tray(path.join(__static, '/stock.ico'))
136 | // 点击系统托盘显示or隐藏主窗体
137 | tray.on('click', () => {
138 | if (mainWindow.isVisible()) {
139 | // 判断是否在顶部,在顶部则执行展开
140 | if (mainWindow.getPosition()[1] < -10) {
141 | mainMouseEnter()
142 | } else {
143 | mainWindow.hide()
144 | }
145 | } else {
146 | mainWindow.show()
147 | }
148 | })
149 | // 系统托盘菜单
150 | const contextMenu = Menu.buildFromTemplate([
151 | {
152 | label: '退出程序',
153 | click: () => {
154 | mainWindow.close()
155 | }
156 | }
157 | ])
158 | tray.setContextMenu(contextMenu)
159 | }
160 |
161 | /**
162 | * 主窗体展开&缩起
163 | */
164 | let timeout = null // 定时器
165 | let isAnimating = false // 是否动画中标识
166 | const mainMouseEnter = function () {
167 | clearTimeout(timeout)
168 | if (isAnimating) return false
169 | let pos = mainWindow.getPosition()
170 | if (pos[1] < -10) {
171 | isAnimating = true
172 | for (let i = pos[1]; i <= -10; i += 2) {
173 | mainWindow.setPosition(pos[0], i)
174 | }
175 | isAnimating = false
176 | mainWindow.focus()
177 | }
178 | }
179 | const mainMouseLeave = function () {
180 | if (isAnimating) return false
181 | let pos = mainWindow.getPosition()
182 | if (pos[1] <= -10) {
183 | timeout = setTimeout(() => {
184 | isAnimating = true
185 | const height = mainWindow.getSize()[1] - 10
186 | for (let i = -10; i > (-1 * height + 1); i -= 2) {
187 | mainWindow.setPosition(pos[0], i)
188 | }
189 | isAnimating = false
190 | }, 300)
191 | }
192 | }
193 |
194 | /**
195 | * 创建子窗口
196 | */
197 | let subWindow // 子窗口
198 | const createSubWindow = function () {
199 | subWindow = new BrowserWindow({
200 | width: 660,
201 | height: 555,
202 | frame: false,
203 | transparent: true,
204 | alwaysOnTop: true,
205 | maximizable: false,
206 | resizable: false,
207 | skipTaskbar: true,
208 | focusable: true,
209 | webPreferences: {webSecurity: false},
210 | useContentSize: true
211 | })
212 | subWindow.loadURL(winURL + '#/stock/sh000001')
213 | subWindow.hide()
214 | ipcMain.on('sub-window-close', () => {
215 | subWindow.hide()
216 | })
217 | }
218 |
--------------------------------------------------------------------------------
/src/renderer/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
14 |
--------------------------------------------------------------------------------
/src/renderer/components/optional.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
17 |
23 |
24 | {{ transMarketName(item.market) }}
25 | {{ item.name }}
26 | {{ item.code }}
27 | {{ item.letter }}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
138 |
141 |
--------------------------------------------------------------------------------
/src/renderer/libs/constant.js:
--------------------------------------------------------------------------------
1 | const apiUrl = 'http://qt.gtimg.cn/' // 接口地址
2 | const timeSpan = 3000 // 时间间隔
3 | const stockIndex = ['sh000001', 'sz399001', 'sz399006'] // 三大指数
4 |
5 | /**
6 | * 比较两个金额大小并返回相应类名
7 | * @param value 被比较值
8 | * @param compare 比较值
9 | * @returns {string} 类名
10 | */
11 | const comparePrice = (value, compare) => {
12 | return value > compare ? 'gain-more' : (value < compare ? 'gain-less' : '')
13 | }
14 |
15 | /**
16 | * 将金额转为万为单位
17 | * @description 有万则加万字,没有则不显示万字
18 | * @param number {string|number} - 需要被转换的金额
19 | * @return {string} 返回转换后的金额字符串
20 | */
21 | const transVolume = (number) => {
22 | number = parseInt(number) + ''
23 | if (number.length > 4) {
24 | let integer = number.substring(0, number.length - 4)
25 | let decimal = integer.length > 3 ? '' : ('.' + number.substring(number.length - 5, number.length - 3))
26 | return integer + decimal + '万'
27 | } else {
28 | return number
29 | }
30 | }
31 |
32 | /**
33 | * 转换日期时间字符串
34 | * @param str {string} - 需要转换的时间字符串
35 | * @returns {string} - 转换后的时间字符串
36 | */
37 | const transDate = (str) => {
38 | let year = str.substring(0, 4)
39 | let month = str.substring(4, 6)
40 | let day = str.substring(6, 8)
41 | let hour = str.substring(8, 10)
42 | let minute = str.substring(10, 12)
43 | let second = str.substring(12, 14)
44 | return `${year}年${month}月${day}日 ${hour}:${minute}:${second}`
45 | }
46 |
47 | /**
48 | * 获取市场名称
49 | * @param code {string} - 市场代码
50 | * @returns {string} - 中文市场名称
51 | */
52 | const transMarketName = (code) => {
53 | switch (code) {
54 | case 'sh':
55 | return '上证'
56 | case 'sz':
57 | return '深证'
58 | case 'hk':
59 | return '港股'
60 | }
61 | }
62 |
63 | export {
64 | apiUrl,
65 | timeSpan,
66 | stockIndex,
67 | comparePrice,
68 | transVolume,
69 | transDate,
70 | transMarketName
71 | }
72 |
--------------------------------------------------------------------------------
/src/renderer/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import axios from 'axios'
3 | import Toasted from 'vue-toasted' // 底部提示弹窗
4 | import {Table, TableColumn, Input, Select, Option, Form, FormItem, Dialog, Button, Loading} from 'element-ui' // 按需加载
5 | import 'element-ui/lib/theme-chalk/index.css'
6 | import App from './App'
7 | import router from './router'
8 | import store from './store'
9 | import {comparePrice, transVolume, transDate, transMarketName} from './libs/constant' // 公共方法
10 |
11 | // element-ui 按需引用
12 | Vue.use(Table)
13 | Vue.use(TableColumn)
14 | Vue.use(Input)
15 | Vue.use(Select)
16 | Vue.use(Option)
17 | Vue.use(Form)
18 | Vue.use(FormItem)
19 | Vue.use(Dialog)
20 | Vue.use(Button)
21 | Vue.use(Toasted)
22 | Vue.use(Loading)
23 |
24 | // 全局绑定公共方法
25 | Vue.prototype.comparePrice = comparePrice
26 | Vue.prototype.transVolume = transVolume
27 | Vue.prototype.transDate = transDate
28 | Vue.prototype.transMarketName = transMarketName
29 |
30 | if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
31 | axios.defaults.headers.get['Content-Type'] = 'text/plain'
32 | Vue.http = Vue.prototype.$http = axios
33 | Vue.config.productionTip = false
34 |
35 | /* eslint-disable no-new */
36 | new Vue({
37 | components: {App},
38 | router,
39 | store,
40 | template: ''
41 | }).$mount('#app')
42 |
--------------------------------------------------------------------------------
/src/renderer/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 |
4 | Vue.use(Router)
5 |
6 | export default new Router({
7 | mode: 'hash',
8 | routes: [
9 | {
10 | path: '/index',
11 | name: 'index',
12 | component: require('@/view/index').default
13 | },
14 | {
15 | path: '/stock/:code',
16 | name: 'stock',
17 | component: require('@/view/stock').default
18 | }
19 | ]
20 | })
21 |
--------------------------------------------------------------------------------
/src/renderer/scss/app.scss:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | margin: 0;
4 | padding: 0;
5 | -webkit-user-select: none; /* Chrome/Safari/Opera */
6 | user-select: none;
7 | }
8 |
9 | html {
10 | padding: 10px;
11 | }
12 |
13 | body {
14 | font-family: PingFang SC, 'Source Sans Pro', sans-serif;
15 | background: rgba(0, 0, 0, 0);
16 | overflow: hidden;
17 | }
18 |
19 | // 选择
20 | $--scroll-bar-color: #333;
21 | ::selection {
22 | background: #ddd;
23 | color: $--scroll-bar-color;
24 | }
25 |
26 | ::-webkit-selection {
27 | background: #ddd;
28 | color: $--scroll-bar-color;
29 | }
30 |
31 | // scrollbar
32 | ::-webkit-scrollbar {
33 | width: 5px;
34 | height: 5px;
35 | overflow: auto;
36 | }
37 |
38 | ::-webkit-scrollbar-thumb {
39 | background-color: #ccc;
40 | border-radius: 15px;
41 | }
42 |
43 | ::-webkit-scrollbar-track {
44 | background-color: #eee;
45 | }
46 |
47 |
48 | // iconfont
49 | [class^="icon-"] {
50 | display: inline-block;
51 | font-family: "icon" !important;
52 | font-size: 16px;
53 | font-style: normal;
54 | -webkit-font-smoothing: antialiased;
55 | -moz-osx-font-smoothing: grayscale;
56 | }
57 |
--------------------------------------------------------------------------------
/src/renderer/scss/index.scss:
--------------------------------------------------------------------------------
1 | $--gain-more-color: #F94848; // 红色
2 | $--gain-less-color: #2CB532; // 绿色
3 |
4 |
5 | #box {
6 | width: 100%;
7 | border-radius: 2px;
8 | overflow: hidden;
9 | box-shadow: 0 2px 5px rgba(0, 0, 0, .3);
10 |
11 | > .header {
12 | display: flex;
13 | justify-content: space-between;
14 | background-color: #fff;
15 | $--header-height: 35px;
16 | padding-top: 3px;
17 |
18 | > h1 {
19 | padding: 0 12px;
20 | font-size: 14px;
21 | line-height: $--header-height;
22 | }
23 |
24 | > .action-buttons {
25 | display: flex;
26 | align-items: center;
27 | padding-right: 5px;
28 | -webkit-app-region: no-drag; // 按钮不可拖拽
29 |
30 |
31 | > a {
32 | display: block;
33 | height: 14px;
34 | line-height: 14px;
35 | width: 30px;
36 | text-align: center;
37 | cursor: pointer;
38 | color: #888;
39 |
40 | > i {
41 | font-size: 14px;
42 | }
43 |
44 | &:hover {
45 | color: #000;
46 | }
47 |
48 | &.button-min {
49 | margin-left: 8px;
50 | padding-left: 8px;
51 | border-left: 1px solid #e0e0e0;
52 | }
53 |
54 | &.button-close {
55 | &:hover {
56 | color: #F84B4B;
57 | }
58 | }
59 | }
60 | }
61 | }
62 |
63 | > .stock-index {
64 | display: flex;
65 | justify-content: space-between;
66 | border-bottom: 1px solid #eee;
67 | background-color: #fff;
68 | padding: 10px 10px 20px 10px;
69 |
70 | > div {
71 | width: 32%;
72 | font-size: 13px;
73 | box-sizing: border-box;
74 | padding: 8px 5px;
75 | background-color: #aaa;
76 | border-radius: 2px;
77 | transition: all .1s linear;
78 |
79 | &.gain-more {
80 | background-color: $--gain-more-color;
81 |
82 | &:hover {
83 | background-color: darken($--gain-more-color, 5%);
84 | }
85 | }
86 |
87 | &.gain-less {
88 | background-color: $--gain-less-color;
89 |
90 | &:hover {
91 | background-color: darken($--gain-less-color, 5%);
92 | }
93 | }
94 |
95 |
96 | > .upper-info {
97 | align-items: baseline;
98 | font-weight: bold;
99 | color: #fff;
100 | text-align: center;
101 |
102 | > h3 {
103 | font-size: 13px;
104 | font-style: normal;
105 | }
106 |
107 |
108 | > p {
109 | font-size: 16px;
110 | }
111 | }
112 |
113 | > .index-gain {
114 | display: flex;
115 | justify-content: space-between;
116 | font-size: 11px;
117 | color: #fff;
118 | }
119 | }
120 | }
121 |
122 | .optional-stock-table {
123 | overflow: hidden;
124 |
125 | &:before {
126 | display: none !important;
127 | }
128 |
129 | .caret-wrapper {
130 | width: 18px !important;
131 | }
132 |
133 | .stock-info {
134 | > div {
135 | display: flex;
136 | align-items: center;
137 | padding-top: 4px;
138 | margin-bottom: 5px;
139 |
140 | > h3 {
141 | display: inline-block;
142 | font-size: 14px;
143 | line-height: 14px;
144 | color: #000;
145 | height: 14px;
146 | max-width: 70px;
147 | white-space: nowrap;
148 | overflow: hidden;
149 | text-overflow: ellipsis;
150 | }
151 |
152 | > span {
153 | display: block;
154 | margin-left: 3px;
155 | padding: 0 2px;
156 | background-color: #aaa;
157 | border-radius: 2px;
158 | line-height: 14px;
159 | font-size: 11px;
160 | color: #fff;
161 | }
162 |
163 | }
164 |
165 |
166 | > p {
167 | font-size: 11px;
168 | line-height: 11px;
169 | color: #aaa;
170 | }
171 | }
172 |
173 | .stock-price {
174 | font-weight: bold;
175 | }
176 |
177 | .stock-volume {
178 | font-weight: bold;
179 | }
180 |
181 | .gain-price {
182 | color: #888;
183 | font-weight: bold;
184 |
185 | &.gain-more {
186 | color: $--gain-more-color;
187 | }
188 |
189 | &.gain-less {
190 | color: $--gain-less-color;
191 | }
192 | }
193 |
194 | .gain-percent {
195 | display: inline-block;
196 | padding: 0 5px;
197 | width: 65px;
198 | color: #fff;
199 | text-align: right;
200 | font-weight: bold;
201 | border-radius: 2px;
202 | background-color: #ccc;
203 |
204 | &.gain-more {
205 | background-color: $--gain-more-color;
206 | }
207 |
208 | &.gain-less {
209 | background-color: $--gain-less-color;
210 | }
211 | }
212 | }
213 | }
214 |
215 |
216 | // element's dialog rewrite
217 | .el-dialog__wrapper {
218 | top: 50px;
219 | left: 10px;
220 | bottom: 10px;
221 | right: 10px;
222 |
223 | > .el-dialog {
224 | margin-top: 0 !important;
225 | width: 400px !important;
226 | border-radius: 0;
227 | box-shadow: none !important;
228 | }
229 | }
230 |
231 | .v-modal {
232 | top: 50px;
233 | left: 10px;
234 | bottom: 10px;
235 | right: 10px;
236 | width: calc(100% - 20px);
237 | height: calc(100% - 60px);
238 | }
239 |
--------------------------------------------------------------------------------
/src/renderer/scss/optional.scss:
--------------------------------------------------------------------------------
1 | .optional-dialog {
2 | * {
3 | font-family: PingFang SC, 'Source Sans Pro', sans-serif;
4 | text-align: center;
5 | }
6 |
7 | .el-dialog__header {
8 | display: none;
9 | }
10 |
11 | .el-dialog__body {
12 | padding: 35px 0 20px 0;
13 |
14 | .el-form-item__error {
15 | width: 100%;
16 | }
17 |
18 | .code-select {
19 | width: 100%;
20 |
21 | .el-input__inner {
22 | font-size: 25px;
23 | border: none;
24 | letter-spacing: 2px;
25 | }
26 | }
27 | }
28 |
29 |
30 | }
31 |
32 | .hint-option-item {
33 | display: flex;
34 | align-items: center;
35 | justify-content: space-between;
36 |
37 | .hint-market {
38 | display: block;
39 | padding: 0 4px;
40 | border-radius: 1px;
41 | background-color: #ccc;
42 | color: #fff;
43 | line-height: 14px;
44 | height: 14px;
45 | text-align: center;
46 | margin-right: 5px;
47 | font-size: 11px;
48 | }
49 |
50 | .hint-name {
51 | width: 110px;
52 | height: 14px;
53 | line-height: 14px;
54 | text-align: left;
55 | overflow: hidden;
56 | text-overflow: ellipsis;
57 | font-size: 13px;
58 | }
59 |
60 | .hint-code {
61 | width: 60px;
62 | text-align: center;
63 | font-size: 13px;
64 | }
65 |
66 | .hint-letter {
67 | text-align: right;
68 | width: 90px;
69 | font-size: 13px;
70 | color: #aaa;
71 | }
72 | }
73 |
74 | // 魔改下拉组件
75 | .el-select-dropdown {
76 | border-radius: 0px;
77 | box-shadow: none;
78 | border-left: 0;
79 | border-right: 0;
80 |
81 | &.el-popper[x-placement^=bottom] {
82 | margin-top: -1px !important;
83 | }
84 |
85 | .popper__arrow {
86 | display: none !important;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/renderer/scss/stock.scss:
--------------------------------------------------------------------------------
1 | $--gain-more-color: #F94848; // 红色
2 | $--gain-less-color: #2CB532; // 绿色
3 |
4 | .stock-window {
5 | width: 100%;
6 | border-radius: 2px;
7 | overflow: hidden;
8 | box-shadow: 0 2px 5px rgba(0, 0, 0, .3);
9 | background-color: #fff;
10 |
11 |
12 | > .window-content {
13 |
14 | > .stock-top {
15 | display: flex;
16 | align-items: start;
17 | padding: 15px 10px 0 15px;
18 | -webkit-app-region: drag;
19 |
20 | > .stock-name {
21 | font-size: 18px;
22 | }
23 |
24 | > .stock-code {
25 | flex: 1;
26 | margin-left: 5px;
27 | font-size: 18px;
28 | font-weight: 300;
29 | color: #888;
30 | }
31 |
32 |
33 | > .action-buttons {
34 | position: relative;
35 | z-index: 9999999;
36 | display: flex;
37 | align-items: center;
38 | padding-right: 5px;
39 | -webkit-app-region: no-drag; // 按钮不可拖拽
40 |
41 |
42 | > a {
43 | display: block;
44 | height: 14px;
45 | line-height: 14px;
46 | width: 30px;
47 | text-align: center;
48 | cursor: pointer;
49 | color: #888;
50 |
51 | > i {
52 | font-size: 14px;
53 | }
54 |
55 | &:hover {
56 | color: #000;
57 | }
58 |
59 | &.button-min {
60 | margin-left: 8px;
61 | padding-left: 8px;
62 | border-left: 1px solid #e0e0e0;
63 | }
64 |
65 | &.button-close {
66 | &:hover {
67 | color: #F84B4B;
68 | }
69 | }
70 | }
71 | }
72 | }
73 |
74 | > .price-row {
75 | padding: 10px 15px;
76 | display: flex;
77 | align-items: baseline;
78 | justify-content: space-between;
79 |
80 | > .left-part {
81 | display: flex;
82 | align-items: baseline;
83 | color: #555;
84 |
85 | > .current-price {
86 | font-size: 30px;
87 | font-family: sans-serif;
88 | font-weight: bold;
89 | }
90 |
91 | > .gain-price, > .gain-percent {
92 | margin-left: 10px;
93 | font-size: 14px;
94 | }
95 |
96 | &.gain-more {
97 | color: $--gain-more-color;
98 | }
99 |
100 | &.gain-less {
101 | color: $--gain-less-color;
102 | }
103 | }
104 |
105 | > .right-part {
106 | font-size: 12px;
107 | color: #aaa;
108 | }
109 |
110 | }
111 |
112 | > .stock-info-form {
113 | padding: 0 15px;
114 | overflow: hidden;
115 |
116 | .el-form-item {
117 | $form-item-height: 21px;
118 | margin-right: 0;
119 | margin-bottom: 0;
120 | height: $form-item-height;
121 | display: flex;
122 | float: left;
123 | width: 25%;
124 |
125 | &.gain-more {
126 | color: $--gain-more-color;
127 | }
128 |
129 | &.gain-less {
130 | color: $--gain-less-color;
131 | }
132 |
133 | > label {
134 | width: 50px;
135 | font-size: 13px !important;
136 | padding-right: 5px;
137 | line-height: $form-item-height;
138 | color: #333;
139 | }
140 |
141 | > .el-form-item__content {
142 | line-height: $form-item-height !important;
143 |
144 | > span {
145 | display: inline-block;
146 | height: 20px;
147 | font-size: 13px !important;
148 | line-height: $form-item-height !important;
149 | font-weight: bold;
150 | }
151 | }
152 | }
153 | }
154 |
155 | > .k-line {
156 | margin-top: 15px;
157 | border-top: 1px solid #eee;
158 | width: 100%;
159 | display: flex;
160 |
161 | > .line-picture {
162 | display: flex;
163 | align-items: center;
164 | justify-content: center;
165 | }
166 |
167 |
168 | > .order-list {
169 | border-left: 1px solid #eee;
170 |
171 | > .inner-title {
172 | text-align: center;
173 | font-size: 12px;
174 | background-color: #fafafa;
175 | border-bottom: 1px solid #eaeaea;
176 | }
177 |
178 | $list-line-height: 18px;
179 |
180 | > ul.sell-order, > ul.buy-order {
181 | padding: 2px 8px;
182 | list-style: none;
183 | border-bottom: 1px solid #eaeaea;
184 |
185 | > li {
186 | font-size: 11px;
187 | line-height: $list-line-height;
188 | width: 120px;
189 | display: flex;
190 |
191 | > span {
192 | display: inline-block;
193 | }
194 |
195 | > .order-index {
196 | width: 30px;
197 | }
198 |
199 | > .order-price {
200 | width: 60px;
201 | text-align: center;
202 | color: #888;
203 |
204 | &.gain-more {
205 | color: $--gain-more-color;
206 | }
207 |
208 | &.gain-less {
209 | color: $--gain-less-color;
210 | }
211 | }
212 |
213 | > .order-count {
214 | width: 60px;
215 | text-align: right;
216 | }
217 |
218 | }
219 | }
220 |
221 | > ul.deal-detail {
222 | padding: 3px 8px;
223 | list-style: none;
224 |
225 | > li {
226 | font-size: 11px;
227 | line-height: $list-line-height;
228 | width: 120px;
229 | display: flex;
230 |
231 | > span {
232 | display: inline-block;
233 | }
234 |
235 | > .deal-time {
236 | width: 40px;
237 | }
238 |
239 | > .deal-price {
240 | width: 60px;
241 | text-align: center;
242 |
243 | &.gain-more {
244 | color: $--gain-more-color;
245 | }
246 |
247 | &.gain-less {
248 | color: $--gain-less-color;
249 | }
250 | }
251 |
252 | > .deal-count {
253 | width: 40px;
254 | text-align: right;
255 |
256 | &.deal-buy {
257 | color: $--gain-more-color;
258 | }
259 |
260 | &.deal-sell {
261 | color: $--gain-less-color;
262 | }
263 | }
264 | }
265 | }
266 | }
267 | }
268 | }
269 | }
270 |
271 | // 修改加载中遮罩样式
272 | .el-loading-mask {
273 | margin: 10px;
274 | border-radius: 2px;
275 | overflow: hidden;
276 | height: 535px;
277 | }
278 |
--------------------------------------------------------------------------------
/src/renderer/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | import { createPersistedState, createSharedMutations } from 'vuex-electron'
5 |
6 | import modules from './modules'
7 |
8 | Vue.use(Vuex)
9 |
10 | export default new Vuex.Store({
11 | modules,
12 | plugins: [
13 | createPersistedState(),
14 | createSharedMutations()
15 | ],
16 | strict: process.env.NODE_ENV !== 'production'
17 | })
18 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The file enables `@/store/index.js` to import all vuex modules
3 | * in a one-shot manner. There should not be any reason to edit this file.
4 | */
5 |
6 | const files = require.context('.', false, /\.js$/)
7 | const modules = {}
8 |
9 | files.keys().forEach(key => {
10 | if (key === './index.js') return
11 | modules[key.replace(/(\.\/|\.js)/g, '')] = files(key).default
12 | })
13 |
14 | export default modules
15 |
--------------------------------------------------------------------------------
/src/renderer/view/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
18 |
19 |
20 |
25 |
26 |
{{item.name}}
27 |
{{item.price.toFixed(2)}}
28 |
29 |
30 | {{item.gain.price>0?'+':''}}{{item.gain.price.toFixed(2)}}
31 | {{item.gain.percent>0?'+':''}}{{item.gain.percent.toFixed(2)}}%
32 |
33 |
34 |
35 |
36 |
37 |
45 |
46 |
47 |
48 |
49 |
{{props.row.name}}
50 |
51 | {{props.row.status==='S'?'停':'退'}}
52 |
53 |
54 |
{{props.row.code.toUpperCase()}}
55 |
56 |
57 |
58 |
59 |
60 | {{props.row.price.toFixed(2)}}
61 |
62 |
63 |
64 |
65 |
66 | {{transVolume(props.row.volume)}}{{props.row.code.indexOf('hk')>-1 ?
67 | '股':'手'}}
68 |
69 |
70 |
71 |
72 |
73 |
74 |
76 | {{props.row.gain.price>0?'+':''}}{{props.row.gain.price.toFixed(2)}}
77 |
78 |
80 | {{props.row.gain.percent>0?'+':''}}{{props.row.gain.percent.toFixed(2)}}%
81 |
82 |
83 |
84 | -
85 | 0.00%
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
351 |
352 |
355 |
--------------------------------------------------------------------------------
/src/renderer/view/stock.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{stock.name}}
8 |
9 |
({{this.code.toUpperCase()}})
10 |
15 |
16 |
17 |
18 |
19 |
20 | ¥{{stock.current.toFixed(2)}}
21 |
22 |
23 | {{stock.gain.price>0?'+':''}}{{stock.gain.price.toFixed(2)}}
24 |
25 |
26 | {{stock.gain.percent>0?'+':''}}{{stock.gain.percent.toFixed(2)}}%
27 |
28 |
29 |
30 | {{transDate(stock.time)}} (数据获取时间)
31 |
32 |
33 |
34 |
35 |
36 | {{stock.highest.toFixed(2)}}
37 |
38 |
39 | {{stock.today.toFixed(2)}}
40 |
41 |
42 | {{stock.limit.up.toFixed(2)}}
43 |
44 |
45 | {{transVolume(stock.volume.total)}}手
46 |
47 |
48 | {{stock.lowest.toFixed(2)}}
49 |
50 |
51 | {{stock.yesterday.toFixed(2)}}
52 |
53 |
54 | {{stock.limit.down.toFixed(2)}}
55 |
56 |
57 | {{stock.volume.turn}}万
58 |
59 |
60 | {{stock.turnover.toFixed(2)}}%
61 |
62 |
63 | {{stock.cap}}亿
64 |
65 |
66 | {{stock.ratio}}
67 |
68 |
69 | {{stock.swing.toFixed(2)}}%
70 |
71 |
72 | {{stock.float}}亿
73 |
74 |
75 |
76 |
77 |
78 |
79 |
![]()
80 |
81 |
82 |
五档盘口
83 |
84 |
85 | -
86 | 卖{{reverseSell.length - index}}
87 | {{item.price.toFixed(2)}}
89 | {{transVolume(item.count)}}
90 |
91 |
92 |
93 |
94 | -
95 | 买{{index+1}}
96 | {{item.price.toFixed(2)}}
98 | {{transVolume(item.count)}}
99 |
100 |
101 |
成交明细
102 |
103 |
104 | -
105 | {{item.time}}
106 | {{item.price.toFixed(2)}}
108 | {{transVolume(item.count)}}
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
297 |
298 |
301 |
--------------------------------------------------------------------------------
/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinhaoxx/stock-viewer-tool/715a1671c8bfe92118b80a88c3370e5626811d32/static/.gitkeep
--------------------------------------------------------------------------------
/static/stock.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinhaoxx/stock-viewer-tool/715a1671c8bfe92118b80a88c3370e5626811d32/static/stock.ico
--------------------------------------------------------------------------------