The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .gitignore
├── LICENSE
├── README.md
├── backend
    ├── docs
    │   ├── 1-todo-2020-12-06.md
    │   └── git-push-tag.md
    ├── jobs
    │   ├── 18h_daily_job.py
    │   ├── README.txt
    │   ├── aps_job.py
    │   ├── basic_job.py
    │   ├── cron.daily
    │   │   └── run_daily
    │   ├── cron.hourly
    │   │   └── run_hourly
    │   ├── cron.minutely
    │   │   └── run_1minute
    │   ├── cron.monthly
    │   │   └── run_monthly
    │   ├── crontab
    │   ├── daily_job.py
    │   ├── guess_indicators_daily_buy_job.py
    │   ├── guess_indicators_daily_job.py
    │   ├── guess_indicators_daily_sell_job.py
    │   ├── guess_rsrs_daily_job.py
    │   ├── quarter_job.py
    │   ├── restart_mnist_serving.sh
    │   ├── restart_web.sh
    │   ├── run_cron.sh
    │   ├── run_init.sh
    │   ├── run_jupyter.sh
    │   ├── run_web.sh
    │   ├── start_mariadb.sh
    │   └── test_akshare
    │   │   ├── test_stock_zh_a_daily.py
    │   │   ├── test_stock_zh_a_spot.py
    │   │   └── test_stock_zh_index_spot.py
    ├── libs
    │   ├── common.py
    │   ├── stock_web_dic.py
    │   └── stock_web_dic.py.bk
    ├── old_jobs
    │   ├── README.md
    │   ├── guess_indicators_lite_buy_daily_job.py
    │   ├── guess_indicators_lite_sell_daily_job.py
    │   ├── guess_period_daily_job.py
    │   ├── guess_return_daily_job.py
    │   └── guess_sklearn_ma_daily_job.py
    ├── supervisor
    │   ├── example_supervisord_conf
    │   └── supervisord.conf
    └── web
    │   ├── README.md
    │   ├── base.py
    │   ├── chartHandler.py
    │   ├── dataEditorHandler.py
    │   ├── dataIndicatorsHandler.py
    │   ├── dataTableHandler.py
    │   ├── demo-chart.py
    │   ├── main.py
    │   ├── minstServingHandler.py
    │   ├── static
    │       ├── css
    │       │   ├── ace.min.css
    │       │   ├── bokeh-tables.min.css
    │       │   ├── bokeh-widgets.min.css
    │       │   ├── bokeh.min.css
    │       │   ├── bootstrap-colorpicker.min.css
    │       │   ├── bootstrap-datepicker3.min.css
    │       │   ├── bootstrap-datetimepicker.min.css
    │       │   ├── bootstrap-timepicker.min.css
    │       │   ├── bootstrap.min.css
    │       │   ├── buttons.dataTables.min.css
    │       │   ├── chosen.min.css
    │       │   ├── daterangepicker.min.css
    │       │   ├── editor.dataTables.min.css
    │       │   ├── font-awesome.min.css
    │       │   ├── fonts.googleapis.com.css
    │       │   ├── jquery-ui.custom.min.css
    │       │   ├── jquery-ui.min.css
    │       │   └── select.dataTables.min.css
    │       ├── font-awesome
    │       │   ├── 4.5.0
    │       │   │   ├── css
    │       │   │   │   └── font-awesome.min.css
    │       │   │   └── fonts
    │       │   │   │   ├── fontawesome-webfont.ttf
    │       │   │   │   ├── fontawesome-webfont.woff
    │       │   │   │   └── fontawesome-webfont.woff2
    │       │   └── opensans
    │       │   │   └── v13
    │       │   │       ├── DXI1ORHCpsQm3Vp6mXoaTXhCUOGz7vYGh680lGh-uXM.woff
    │       │   │       └── cJZKeOuBrn4kERxqtaUH3T8E0i7KZn-EPnyo3HZu7kw.woff
    │       ├── img
    │       │   ├── diff-n-bokeh.png
    │       │   ├── stock-show-01.jpg
    │       │   ├── stock2-001.png
    │       │   ├── stock2-002.png
    │       │   ├── stock2-003.png
    │       │   └── 支付宝--微信支付.jpg
    │       ├── js
    │       │   ├── ace-elements.min.js
    │       │   ├── ace-extra.min.js
    │       │   ├── ace.min.js
    │       │   ├── autosize.min.js
    │       │   ├── bokeh-api.min.js
    │       │   ├── bokeh-gl.min.js
    │       │   ├── bokeh-tables.min.js
    │       │   ├── bokeh-widgets.min.js
    │       │   ├── bokeh.min.js
    │       │   ├── bootbox.js
    │       │   ├── bootstrap-datepicker.min.js
    │       │   ├── bootstrap-datepicker.zh-CN.js
    │       │   ├── bootstrap-datetimepicker.min.js
    │       │   ├── bootstrap-timepicker.min.js
    │       │   ├── bootstrap.min.js
    │       │   ├── buttons.colVis.min.js
    │       │   ├── buttons.html5.min.js
    │       │   ├── buttons.print.min.js
    │       │   ├── chosen.jquery.min.js
    │       │   ├── dataTables.buttons.min.js
    │       │   ├── dataTables.editor.min.js
    │       │   ├── dataTables.select.min.js
    │       │   ├── datatables.Chinese.json
    │       │   ├── daterangepicker.min.js
    │       │   ├── draw.js
    │       │   ├── grid.locale-en.js
    │       │   ├── holder.min.js
    │       │   ├── jquery-2.1.4.min.js
    │       │   ├── jquery-ui.custom.min.js
    │       │   ├── jquery-ui.min.js
    │       │   ├── jquery.colorbox.min.js
    │       │   ├── jquery.dataTables.bootstrap.min.js
    │       │   ├── jquery.dataTables.min.js
    │       │   ├── jquery.validate.min.js
    │       │   └── moment.min.js
    │       └── update_bokeh.sh
    │   ├── templates
    │       ├── bokeh_embed.html
    │       ├── common
    │       │   ├── footer.html
    │       │   ├── header.html
    │       │   ├── left_menu.html
    │       │   └── meta.html
    │       ├── data_editor.html
    │       ├── index.html
    │       ├── layout
    │       │   ├── default.html
    │       │   ├── indicators-main.html
    │       │   ├── indicators.html
    │       │   ├── main.html
    │       │   ├── single_default.html
    │       │   └── single_main.html
    │       ├── minst_serving.html
    │       ├── stock_chart.html
    │       ├── stock_indicators.html
    │       ├── stock_web.html
    │       ├── test.html
    │       └── test2.html
    │   ├── test_thread.py
    │   ├── test_thread_v2.py
    │   └── tornado_bokeh_embed.py
├── docker-compose
    ├── .gitignore
    ├── LICENSE
    ├── README.md
    ├── build_stock.sh
    ├── dev-docker-compose-restart.sh
    ├── dev-docker-compose.yml
    ├── docker-compose.yml
    ├── docker
    │   ├── DevBackendDockerfile
    │   ├── DevFrontendDockerfile
    │   ├── Dockerfile
    │   ├── ProdBackendDockerfile
    │   ├── ProdFrontendDockerfile
    │   ├── README.md
    │   └── build.sh
    ├── mysql
    │   ├── init.sql
    │   └── my.cnf
    ├── nginx.conf
    └── nginx
    │   └── nginx.conf
└── frontend
    ├── .eslintignore
    ├── .gitignore
    ├── LICENSE
    ├── README.md
    ├── babel.config.js
    ├── docker-build.sh
    ├── docker-entrypoint.sh
    ├── jest.config.js
    ├── jsconfig.json
    ├── mock
        ├── index.js
        ├── mock-server.js
        ├── table.js
        ├── user.js
        └── utils.js
    ├── package.json
    ├── postcss.config.js
    ├── public
        ├── 40x.html
        ├── 50x.html
        ├── favicon.ico
        ├── freewebsys-logo.jpg
        ├── index.html
        ├── stock-001.png
        ├── stock-002.png
        └── stock-003.png
    ├── src
        ├── App.vue
        ├── api
        │   ├── article.js
        │   ├── menu.js
        │   ├── package.js
        │   ├── table.js
        │   └── user.js
        ├── assets
        │   └── 404_images
        │   │   ├── 404.png
        │   │   └── 404_cloud.png
        ├── components
        │   ├── Breadcrumb
        │   │   └── index.vue
        │   ├── Hamburger
        │   │   └── index.vue
        │   ├── Pagination
        │   │   └── index.vue
        │   └── SvgIcon
        │   │   └── index.vue
        ├── directive
        │   ├── el-table
        │   │   ├── adaptive.js
        │   │   └── index.js
        │   └── waves
        │   │   ├── index.js
        │   │   ├── waves.css
        │   │   └── waves.js
        ├── icons
        │   ├── index.js
        │   ├── svg
        │   │   ├── dashboard.svg
        │   │   ├── example.svg
        │   │   ├── eye-open.svg
        │   │   ├── eye.svg
        │   │   ├── form.svg
        │   │   ├── link.svg
        │   │   ├── nested.svg
        │   │   ├── password.svg
        │   │   ├── table.svg
        │   │   ├── tree.svg
        │   │   └── user.svg
        │   └── svgo.yml
        ├── layout
        │   ├── components
        │   │   ├── AppMain.vue
        │   │   ├── Navbar.vue
        │   │   ├── Sidebar
        │   │   │   ├── FixiOSBug.js
        │   │   │   ├── Item.vue
        │   │   │   ├── Link.vue
        │   │   │   ├── Logo.vue
        │   │   │   ├── SidebarItem.vue
        │   │   │   └── index.vue
        │   │   └── index.js
        │   ├── index.vue
        │   └── mixin
        │   │   └── ResizeHandler.js
        ├── main.js
        ├── permission.js
        ├── router
        │   └── index.js
        ├── settings.js
        ├── store
        │   ├── getters.js
        │   ├── index.js
        │   └── modules
        │   │   ├── app.js
        │   │   ├── settings.js
        │   │   └── user.js
        ├── styles
        │   ├── element-ui.scss
        │   ├── index.scss
        │   ├── mixin.scss
        │   ├── sidebar.scss
        │   ├── transition.scss
        │   └── variables.scss
        ├── utils
        │   ├── auth.js
        │   ├── get-page-title.js
        │   ├── index.js
        │   ├── request.js
        │   ├── scroll-to.js
        │   └── validate.js
        ├── vendor
        │   └── Export2Excel.js
        └── views
        │   ├── 404.vue
        │   ├── dashboard
        │       └── index.vue
        │   ├── form
        │       └── index.vue
        │   ├── login
        │       └── index.vue
        │   ├── nested
        │       ├── menu1
        │       │   ├── index.vue
        │       │   ├── menu1-1
        │       │   │   └── index.vue
        │       │   ├── menu1-2
        │       │   │   ├── index.vue
        │       │   │   ├── menu1-2-1
        │       │   │   │   └── index.vue
        │       │   │   └── menu1-2-2
        │       │   │   │   └── index.vue
        │       │   └── menu1-3
        │       │   │   └── index.vue
        │       └── menu2
        │       │   └── index.vue
        │   ├── table
        │       ├── complex-table.vue
        │       └── index.vue
        │   └── tree
        │       └── index.vue
    ├── tests
        └── unit
        │   ├── .eslintrc.js
        │   ├── components
        │       ├── Breadcrumb.spec.js
        │       ├── Hamburger.spec.js
        │       └── SvgIcon.spec.js
        │   └── utils
        │       ├── formatTime.spec.js
        │       ├── param2Obj.spec.js
        │       ├── parseTime.spec.js
        │       └── validate.spec.js
    └── vue.config.js


/.gitignore:
--------------------------------------------------------------------------------
  1 | # Byte-compiled / optimized / DLL files
  2 | __pycache__/
  3 | *.py[cod]
  4 | *$py.class
  5 | 
  6 | # C extensions
  7 | *.so
  8 | 
  9 | data
 10 | 
 11 | # Distribution / packaging
 12 | .Python
 13 | env/
 14 | build/
 15 | develop-eggs/
 16 | dist/
 17 | downloads/
 18 | eggs/
 19 | .eggs/
 20 | lib/
 21 | lib64/
 22 | parts/
 23 | sdist/
 24 | var/
 25 | wheels/
 26 | *.egg-info/
 27 | .installed.cfg
 28 | *.egg
 29 | 
 30 | notebooks
 31 | nohup.out
 32 | 
 33 | # PyInstaller
 34 | #  Usually these files are written by a python script from a template
 35 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
 36 | *.manifest
 37 | *.spec
 38 | 
 39 | # Installer logs
 40 | pip-log.txt
 41 | pip-delete-this-directory.txt
 42 | 
 43 | # Unit test_akshare / coverage reports
 44 | htmlcov/
 45 | .tox/
 46 | .coverage
 47 | .coverage.*
 48 | .cache
 49 | nosetests.xml
 50 | coverage.xml
 51 | *.cover
 52 | .hypothesis/
 53 | 
 54 | # Translations
 55 | *.mo
 56 | *.pot
 57 | 
 58 | # Django stuff:
 59 | *.log
 60 | local_settings.py
 61 | 
 62 | # Flask stuff:
 63 | instance/
 64 | .webassets-cache
 65 | 
 66 | # Scrapy stuff:
 67 | .scrapy
 68 | 
 69 | # Sphinx documentation
 70 | docs/_build/
 71 | 
 72 | # PyBuilder
 73 | target/
 74 | 
 75 | # Jupyter Notebook
 76 | .ipynb_checkpoints
 77 | 
 78 | # pyenv
 79 | .python-version
 80 | 
 81 | # celery beat schedule file
 82 | celerybeat-schedule
 83 | 
 84 | # SageMath parsed files
 85 | *.sage.py
 86 | 
 87 | # dotenv
 88 | .env
 89 | 
 90 | # virtualenv
 91 | .venv
 92 | venv/
 93 | ENV/
 94 | 
 95 | # Spyder project settings
 96 | .spyderproject
 97 | .spyproject
 98 | 
 99 | # Rope project settings
100 | .ropeproject
101 | 
102 | # mkdocs documentation
103 | /site
104 | 
105 | # mypy
106 | .mypy_cache/
107 | 
108 | .idea
109 | *.iml
110 | .DS_Store
111 | *.zip
112 | *.log
113 | *.pyc
114 | doc
115 | /bin
116 | pkg
117 | *.tmp


--------------------------------------------------------------------------------
/backend/docs/git-push-tag.md:
--------------------------------------------------------------------------------
1 | 
2 | ## 创建 tag 并发布到 github 上
3 | 
4 | 
5 | git tag -a v2.0 -m "v2.0"
6 | 
7 | git push origin --tags
8 | 


--------------------------------------------------------------------------------
/backend/jobs/README.txt:
--------------------------------------------------------------------------------
1 | 1,计算每日买全部推荐买。
2 | 2,计算每日全部推荐卖数据。
3 | 3,设置个人账号,设置购买和卖的数据。进行关联查询。
4 | 4,最重要的沪深300,中正500数据。进行大盘股分析。


--------------------------------------------------------------------------------
/backend/jobs/aps_job.py:
--------------------------------------------------------------------------------
 1 | #!/usr/local/bin/python3
 2 | # -*- coding: utf-8 -*-
 3 | from pytz import utc
 4 | from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
 5 | from apscheduler.schedulers.blocking import BlockingScheduler
 6 | 
 7 | from apscheduler.executors.pool import ProcessPoolExecutor
 8 | import libs.common as common
 9 | 
10 | # doc : http://apscheduler.readthedocs.io/en/latest/modules/jobstores/sqlalchemy.html
11 | jobstores = {
12 |     'default': SQLAlchemyJobStore(url=common.MYSQL_CONN_URL, tablename='apscheduler_jobs')
13 | }
14 | executors = {
15 |     'default': {'type': 'threadpool', 'max_workers': 20},
16 |     'processpool': ProcessPoolExecutor(max_workers=5)
17 | }
18 | job_defaults = {
19 |     'coalesce': False,
20 |     'max_instances': 3
21 | }
22 | scheduler = BlockingScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults, timezone=utc)
23 | scheduler.start()
24 | print("start ...")
25 | 


--------------------------------------------------------------------------------
/backend/jobs/basic_job.py:
--------------------------------------------------------------------------------
 1 | #!/usr/local/bin/python3
 2 | # -*- coding: utf-8 -*-
 3 | 
 4 | import libs.common as common
 5 | import MySQLdb
 6 | 
 7 | # 创建新数据库。
 8 | def create_new_database():
 9 |     with MySQLdb.connect(common.MYSQL_HOST, common.MYSQL_USER, common.MYSQL_PWD, "mysql", charset="utf8") as db:
10 |         try:
11 |             create_sql = " CREATE DATABASE IF NOT EXISTS %s CHARACTER SET utf8 COLLATE utf8_general_ci " % common.MYSQL_DB
12 |             print(create_sql)
13 |             db.autocommit(on=True)
14 |             db.cursor().execute(create_sql)
15 |         except Exception as e:
16 |             print("error CREATE DATABASE :", e)
17 | 
18 | 
19 | # main函数入口
20 | if __name__ == '__main__':
21 | 
22 |     # 检查,如果执行 select 1 失败,说明数据库不存在,然后创建一个新的数据库。
23 |     try:
24 |         with MySQLdb.connect(common.MYSQL_HOST, common.MYSQL_USER, common.MYSQL_PWD, common.MYSQL_DB,
25 |                              charset="utf8") as db:
26 |             db.autocommit(on=True)
27 |             db.cursor().execute(" select 1 ")
28 |             print("########### db exists ###########")
29 |     except Exception as e:
30 |         print("check  MYSQL_DB error and create new one :", e)
31 |         # 检查数据库失败,
32 |         create_new_database()
33 |     # 执行数据初始化。
34 | 


--------------------------------------------------------------------------------
/backend/jobs/cron.daily/run_daily:
--------------------------------------------------------------------------------
 1 | #!/bin/sh
 2 | 
 3 | mkdir -p /data/logs
 4 | DATETIME=`date +%Y-%m-%d:%H:%M:%S`
 5 | 
 6 | DATE=`date +%Y-%m-%d`
 7 | 
 8 | export PYTHONIOENCODING=utf-8
 9 | export LANG=zh_CN.UTF-8
10 | export PYTHONPATH=/data/stock
11 | export LC_CTYPE=zh_CN.UTF-8
12 | 
13 | 
14 | echo "###################"$DATETIME"###################" >> /data/logs/daily.${DATE}.log
15 | #增加获得今日全部数据和大盘数据
16 | /usr/local/bin/python3 /data/stock/jobs/18h_daily_job.py >> /data/logs/daily.${DATE}.log
17 | 
18 | 
19 | echo "###################"$DATETIME"###################" >> /data/logs/daily.${DATE}.log
20 | #使用股票指标预测。
21 | /usr/local/bin/python3 /data/stock/jobs/guess_indicators_daily_job.py >> /data/logs/daily.${DATE}.log
22 | /usr/local/bin/python3 /data/stock/jobs/guess_indicators_daily_buy_job.py >> /data/logs/daily.${DATE}.log
23 | 
24 | #清除前3天数据。
25 | DATE_20=`date -d '-20 days' +%Y-%m-%d`
26 | MONTH_20=`date -d '-20 days' +%Y-%m`
27 | echo "rm -f /data/cache/hist_data_cache/${MONTH_20}/${DATETIME_20}"
28 | rm -f /data/cache/hist_data_cache/${MONTH_20}/${DATETIME_20}


--------------------------------------------------------------------------------
/backend/jobs/cron.hourly/run_hourly:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | 
3 | mkdir -p /data/logs
4 | DATE=`date +%Y-%m-%d:%H:%M:%S`
5 | echo $DATE >> /data/logs/hourly.log
6 | 
7 | 


--------------------------------------------------------------------------------
/backend/jobs/cron.minutely/run_1minute:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | 
3 | mkdir -p /data/logs
4 | DATE=`date +%Y-%m-%d:%H:%M:%S`
5 | echo $DATE >> /data/logs/1min.log
6 | 


--------------------------------------------------------------------------------
/backend/jobs/cron.monthly/run_monthly:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | 
3 | mkdir -p /data/logs
4 | DATE=`date +%Y-%m-%d:%H:%M:%S`
5 | echo $DATE >> /data/logs/monthly.log
6 | 
7 | 


--------------------------------------------------------------------------------
/backend/jobs/crontab:
--------------------------------------------------------------------------------
1 | SHELL=/bin/sh 
2 | PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin 
3 | */1     *       *       *       *       /bin/run-parts /etc/cron.minutely 
4 | 10       *       *       *       *       /bin/run-parts /etc/cron.hourly 
5 | 30       16       *       *       *       /bin/run-parts /etc/cron.daily 
6 | 30       17       1,10,20       *       *       /bin/run-parts /etc/cron.monthly 
7 | 


--------------------------------------------------------------------------------
/backend/jobs/daily_job.py:
--------------------------------------------------------------------------------
 1 | #!/usr/local/bin/python3
 2 | # -*- coding: utf-8 -*-
 3 | 
 4 | 
 5 | import libs.common as common
 6 | import sys
 7 | import os
 8 | import time
 9 | import pandas as pd
10 | import tushare as ts
11 | from sqlalchemy.types import NVARCHAR
12 | from sqlalchemy import inspect
13 | import datetime
14 | import shutil
15 | 
16 | 
17 | ####### 使用 5.pdf,先做 基本面数据 的数据,然后在做交易数据。
18 | #
19 | def stat_all(tmp_datetime):
20 |     datetime_str = (tmp_datetime).strftime("%Y-%m-%d")
21 |     datetime_int = (tmp_datetime).strftime("%Y%m%d")
22 | 
23 |     cache_dir = common.bash_stock_tmp % (datetime_str[0:7], datetime_str)
24 |     if os.path.exists(cache_dir):
25 |         shutil.rmtree(cache_dir)
26 |         print("remove cache dir force :", cache_dir)
27 | 
28 |     print("datetime_str:", datetime_str)
29 |     print("datetime_int:", datetime_int)
30 |     data = ts.top_list(datetime_str)
31 |     # 处理重复数据,保存最新一条数据。最后一步处理,否则concat有问题。
32 |     #
33 |     if not data is None and len(data) > 0:
34 |         # 插入数据库。
35 |         # del data["reason"]
36 |         data["date"] = datetime_int  # 修改时间成为int类型。
37 |         data = data.drop_duplicates(subset="code", keep="last")
38 |         data.head(n=1)
39 |         common.insert_db(data, "ts_top_list", False, "`date`,`code`")
40 |     else:
41 |         print("no data .")
42 | 
43 |     print(datetime_str)
44 | 
45 | 
46 | # main函数入口
47 | if __name__ == '__main__':
48 |     # 使用方法传递。
49 |     tmp_datetime = common.run_with_args(stat_all)
50 | 


--------------------------------------------------------------------------------
/backend/jobs/guess_indicators_daily_buy_job.py:
--------------------------------------------------------------------------------
 1 | #!/usr/local/bin/python3
 2 | # -*- coding: utf-8 -*-
 3 | 
 4 | 
 5 | import libs.common as common
 6 | import pandas as pd
 7 | import numpy as np
 8 | import math
 9 | import datetime
10 | import stockstats
11 | from sqlalchemy import text
12 | 
13 | 
14 | ### 对每日指标数据,进行筛选。将符合条件的。二次筛选出来。
15 | ### 只是做简单筛选
16 | def stat_all_lite_buy(tmp_datetime):
17 |     datetime_str = (tmp_datetime).strftime("%Y-%m-%d")
18 |     datetime_int = (tmp_datetime).strftime("%Y%m%d")
19 |     print("datetime_str:", datetime_str)
20 |     print("datetime_int:", datetime_int)
21 | 
22 |     # 查询参数
23 |     params = {"datetime": datetime_int}
24 | 
25 |     sql_kdjk = text(" SELECT avg(`kdjk`) as avg_kdjk FROM  guess_indicators_daily  ")
26 |     data_kdjk  = pd.read_sql(sql=sql_kdjk, con=common.engine(), params=params)
27 |     kdjk = data_kdjk["avg_kdjk"][0]
28 | 
29 |     sql_kdjd = text(" SELECT avg(`kdjd`) as avg_kdjd FROM  guess_indicators_daily  ")
30 |     data_kdjd  = pd.read_sql(sql=sql_kdjd, con=common.engine(), params=params)
31 |     kdjd = data_kdjd["avg_kdjd"][0]
32 | 
33 |     sql_kdjj = text(" SELECT avg(`kdjj`) as avg_kdjj FROM  guess_indicators_daily  ")
34 |     data_kdjj  = pd.read_sql(sql=sql_kdjj, con=common.engine(), params=params)
35 |     kdjj = data_kdjj["avg_kdjj"][0]
36 | 
37 |     # K值在80以上,D值在70以上,J值大于90时为超买。
38 |     # J大于100时为超买,小于10时为超卖。
39 |     # 当六日指标上升到达80时,表示股市已有超买现象
40 |     # 当CCI>﹢100 时,表明股价已经进入非常态区间——超买区间,股价的异动现象应多加关注。
41 |     params_1 = {"datetime": datetime_int, "kdjk": kdjk, "kdjd": kdjd, "kdjj": kdjj}
42 |     sql_1 = text("""
43 |             SELECT `date`,`code`,`name`,`last_price`,`change_percent`,`change_amount`,`volume`,`turnover`,
44 |                             `amplitude`,`high`,`low`,`open`,`closed`,`volume_ratio`,`turnover_rate`,
45 |                             `pe_ratio`,`pb_ratio`,`market_cap`,`circulating_market_cap`,`rise_speed`,
46 |                             `change_5min`,`change_ercent_60day`,`ytd_change_percent`,
47 |          `boll`, `boll_lb`, `boll_ub`, `kdjd`, `kdjj`, `kdjk`, `macd`, `macdh`,
48 |          `macds`, `pdi`,`trix`, `trix_9_sma`, `vr`, `vr_6_sma`, `wr_10`, `wr_6`     
49 |         FROM stock_data.guess_indicators_daily WHERE `date` = :datetime
50 |                         and kdjk >= :kdjk and kdjd >= :kdjd and kdjj >= :kdjj  
51 |     """)  # and kdjj > 100 and rsi_6 > 80  and cci > 100 # 调整参数,提前获得股票增长。
52 | 
53 |     try:
54 |         # 删除老数据。
55 |         del_sql = " DELETE FROM `stock_data`.`guess_indicators_lite_buy_daily` WHERE `date`= '%s' " % datetime_int
56 |         common.insert(del_sql)
57 |     except Exception as e:
58 |         print("error :", e)
59 |     
60 |     print(f"sql_1 : {sql_1}")
61 |     data = pd.read_sql(sql=sql_1, con=common.engine(), params=params_1)
62 |     data = data.drop_duplicates(subset="code", keep="last")
63 |     print("######## stat_all_lite_buy len data ########:", len(data))
64 | 
65 |     try:
66 |         common.insert_db(data, "guess_indicators_lite_buy_daily", False, "`date`,`code`")
67 |     except Exception as e:
68 |         print("error :", e)
69 | 
70 | 
71 | 
72 | # main函数入口
73 | if __name__ == '__main__':
74 |     # 使用方法传递。
75 |     # 二次筛选数据。直接计算买卖股票数据。
76 |     tmp_datetime = common.run_with_args(stat_all_lite_buy)
77 | 
78 | 


--------------------------------------------------------------------------------
/backend/jobs/guess_indicators_daily_sell_job.py:
--------------------------------------------------------------------------------
 1 | #!/usr/local/bin/python3
 2 | # -*- coding: utf-8 -*-
 3 | 
 4 | 
 5 | import libs.common as common
 6 | import pandas as pd
 7 | import numpy as np
 8 | import math
 9 | import datetime
10 | import stockstats
11 | from sqlalchemy import text
12 | 
13 | # 设置卖出数据。
14 | def stat_all_lite_sell(tmp_datetime):
15 |     datetime_str = (tmp_datetime).strftime("%Y-%m-%d")
16 |     datetime_int = (tmp_datetime).strftime("%Y%m%d")
17 |     print("datetime_str:", datetime_str)
18 |     print("datetime_int:", datetime_int)
19 | 
20 |     # 超卖区:K值在20以下,D值在30以下为超卖区。一般情况下,股价有可能上涨,反弹的可能性增大。局内人不应轻易抛出股票,局外人可寻机入场。
21 |     # J大于100时为超买,小于10时为超卖。
22 |     # 当六日强弱指标下降至20时,表示股市有超卖现象
23 |     # 当CCI<﹣100时,表明股价已经进入另一个非常态区间——超卖区间,投资者可以逢低吸纳股票。
24 |     sql_1 = text("""
25 |             SELECT `date`,`code`,`name`,`last_price`,`change_percent`,`change_amount`,`volume`,`turnover`,
26 |                             `amplitude`,`high`,`low`,`open`,`closed`,`volume_ratio`,`turnover_rate`,
27 |                             `pe_ratio`,`pb_ratio`,`market_cap`,`circulating_market_cap`,`rise_speed`,
28 |                             `change_5min`,`change_ercent_60day`,`ytd_change_percent`,
29 |          `boll`, `boll_lb`, `boll_ub`, `kdjd`, `kdjj`, `kdjk`, `macd`, `macdh`,
30 |          `macds`, `pdi`,`trix`, `trix_9_sma`, `vr`, `vr_6_sma`, `wr_10`, `wr_6` 
31 |                         FROM stock_data.guess_indicators_daily WHERE `date` = :datetime
32 |                         and kdjk <= 20 and kdjd <= 30 and kdjj <= 10  
33 |     """)
34 | 
35 |     try:
36 |         # 删除老数据。
37 |         del_sql = " DELETE FROM `stock_data`.`guess_indicators_lite_sell_daily` WHERE `date`= '%s' " % datetime_int
38 |         common.insert(del_sql)
39 |     except Exception as e:
40 |         print("error :", e)
41 | 
42 |     # 查询参数
43 |     params = {"datetime": datetime_int}
44 |     print(sql_1)
45 |     data = pd.read_sql(sql=sql_1, con=common.engine(), params=params)
46 |     data = data.drop_duplicates(subset="code", keep="last")
47 |     print("######## stat_all_lite_sell len data ########:", len(data))
48 | 
49 |     try:
50 |         common.insert_db(data, "guess_indicators_lite_sell_daily", False, "`date`,`code`")
51 |     except Exception as e:
52 |         print("error :", e)
53 | 
54 | 
55 | 
56 | # main函数入口
57 | if __name__ == '__main__':
58 |     # 使用方法传递。
59 |     # 二次筛选数据。直接计算买卖股票数据。
60 |     tmp_datetime = common.run_with_args(stat_all_lite_sell)
61 | 
62 | 


--------------------------------------------------------------------------------
/backend/jobs/quarter_job.py:
--------------------------------------------------------------------------------
 1 | #!/usr/local/bin/python3
 2 | # -*- coding: utf-8 -*-
 3 | 
 4 | 
 5 | import libs.common as common
 6 | import sys
 7 | import time
 8 | import pandas as pd
 9 | import tushare as ts
10 | from sqlalchemy.types import NVARCHAR
11 | from sqlalchemy import inspect
12 | import datetime
13 | 
14 | 
15 | # 增加一个新quarter列,用来存储季度信息。
16 | def concat_quarter(year, quarter, data_array):
17 |     print(len(data_array))
18 |     quarter_str = str(year) + str("%02d" % quarter)  # 格式化季度数据。2位。
19 |     # 增加到列。
20 |     quarter_col = pd.DataFrame([quarter_str for _ in range(len(data_array))], columns=["quarter"])
21 |     return pd.concat([quarter_col, data_array], axis=1)
22 | 
23 | 
24 | #############################基本面数据 http://tushare.org/fundamental.html
25 | def stat_all(tmp_datetime):
26 |     # 返回 31 天前的数据,做上个季度数据统计。
27 |     tmp_datetime_1month = tmp_datetime + datetime.timedelta(days=-31)
28 |     year = int((tmp_datetime_1month).strftime("%Y"))
29 |     quarter = int(pd.Timestamp(tmp_datetime_1month).quarter)  # 获得上个季度的数据。
30 |     print("############ year %d, quarter %d", year, quarter)
31 |     # 业绩报告(主表)
32 |     data = ts.get_report_data(year, quarter)
33 |     # 增加季度字段。
34 |     data = concat_quarter(year, quarter, data)
35 |     # 处理重复数据,保存最新一条数据。最后一步处理,否则concat有问题。
36 |     data = data.drop_duplicates(subset="code", keep="last")
37 |     # 插入数据库。
38 |     common.insert_db(data, "ts_report_data", False, "`quarter`,`code`")
39 | 
40 |     # 盈利能力
41 |     data = ts.get_profit_data(year, quarter)
42 |     # 增加季度字段。
43 |     data = concat_quarter(year, quarter, data)
44 |     # 处理重复数据,保存最新一条数据。
45 |     data = data.drop_duplicates(subset="code", keep="last")
46 |     # 插入数据库。
47 |     common.insert_db(data, "ts_profit_data", False, "`quarter`,`code`")
48 | 
49 |     # 营运能力
50 |     data = ts.get_operation_data(year, quarter)
51 |     # 增加季度字段。
52 |     data = concat_quarter(year, quarter, data)
53 |     # 处理重复数据,保存最新一条数据。最后一步处理,否则concat有问题。
54 |     data = data.drop_duplicates(subset="code", keep="last")
55 |     # 插入数据库。
56 |     common.insert_db(data, "ts_operation_data", False, "`quarter`,`code`")
57 | 
58 |     # 成长能力
59 |     data = ts.get_growth_data(year, quarter)
60 |     # 增加季度字段。
61 |     data = concat_quarter(year, quarter, data)
62 |     # 处理重复数据,保存最新一条数据。最后一步处理,否则concat有问题。
63 |     data = data.drop_duplicates(subset="code", keep="last")
64 |     # 插入数据库。
65 |     common.insert_db(data, "ts_growth_data", False, "`quarter`,`code`")
66 | 
67 |     # 偿债能力
68 |     data = ts.get_debtpaying_data(year, quarter)
69 |     # 增加季度字段。
70 |     data = concat_quarter(year, quarter, data)
71 |     # 处理重复数据,保存最新一条数据。最后一步处理,否则concat有问题。
72 |     data = data.drop_duplicates(subset="code", keep="last")
73 |     # 插入数据库。
74 |     common.insert_db(data, "ts_debtpaying_data", False, "`quarter`,`code`")
75 | 
76 |     # 现金流量
77 |     data = ts.get_cashflow_data(year, quarter)
78 |     # 增加季度字段。
79 |     data = concat_quarter(year, quarter, data)
80 |     # 处理重复数据,保存最新一条数据。最后一步处理,否则concat有问题。
81 |     data = data.drop_duplicates(subset="code", keep="last")
82 |     # 插入数据库。
83 |     common.insert_db(data, "ts_cashflow_data", False, "`quarter`,`code`")
84 | 
85 | 
86 | # main函数入口
87 | if __name__ == '__main__':
88 |     # 使用方法传递。
89 |     tmp_datetime = common.run_with_args(stat_all)
90 | 


--------------------------------------------------------------------------------
/backend/jobs/restart_mnist_serving.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | 
3 | ps -ef | grep 'tensorflow_model_server' | grep -v grep | awk '{print$2}' | xargs kill -9
4 | echo "" > /data/logs/mnist_serving.log
5 | nohup tensorflow_model_server --model_name=mnist --model_base_path=/data/mnist_model >> /data/logs/mnist_serving.log &


--------------------------------------------------------------------------------
/backend/jobs/restart_web.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | 
3 | ps -ef | grep python3 | grep '/data/stock/web/main.py' | awk '{print$2}' | xargs kill -9
4 | echo "restart web ... " > /data/logs/tornado.log
5 | 


--------------------------------------------------------------------------------
/backend/jobs/run_cron.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/sh
 2 | 
 3 | export PYTHONIOENCODING=utf-8
 4 | export LANG=zh_CN.UTF-8
 5 | export PYTHONPATH=/data/stock
 6 | export LC_CTYPE=zh_CN.UTF-8
 7 | 
 8 | mkdir -p /data/logs/tensorflow
 9 | 
10 | 
11 | 
12 | DATE=`date +%Y-%m-%d:%H:%M:%S`
13 | 
14 | echo $DATE >> /data/logs/run_cron.log
15 | 
16 | # 解决定时任务不启动问题,因为权限导致
17 | chmod 755 /etc/cron.minutely/* && chmod 755 /etc/cron.hourly/*
18 | chmod 755 /etc/cron.daily/* && chmod 755 /etc/cron.monthly/*
19 | 
20 | # 配置文件每次都设置权限
21 | chmod 600 /var/spool/cron/crontabs/root
22 | chown root:root /var/spool/cron/crontabs/root
23 | 
24 | #启动cron服务。在前台
25 | /usr/sbin/cron -f


--------------------------------------------------------------------------------
/backend/jobs/run_init.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/sh
 2 | 
 3 | export PYTHONIOENCODING=utf-8
 4 | export LANG=zh_CN.UTF-8
 5 | export PYTHONPATH=/data/stock
 6 | export LC_CTYPE=zh_CN.UTF-8
 7 | 
 8 | mkdir -p /data/logs/tensorflow
 9 | 
10 | 
11 | 
12 | DATE=`date +%Y-%m-%d:%H:%M:%S`
13 | 
14 | echo $DATE >> /data/logs/run_init.log
15 | 
16 | echo "wait 120 second , mysqldb is starting ." >> /data/logs/run_init.log
17 | sleep 120
18 | 
19 | /usr/local/bin/python3 /data/stock/jobs/basic_job.py  >> /data/logs/run_init.log
20 | 
21 | # https://stackoverflow.com/questions/27771781/how-can-i-access-docker-set-environment-variables-from-a-cron-job
22 | # 解决环境变量输出问题。
23 | printenv | grep -v "no_proxy" >> /etc/environment
24 | 
25 | # 第一次后台执行日数据。
26 | nohup bash /data/stock/jobs/cron.daily/run_daily &
27 | 
28 | #防止 supervisor 重复执行
29 | sleep 999999d


--------------------------------------------------------------------------------
/backend/jobs/run_jupyter.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | 
3 | mkdir -p /data/notebooks
4 | 
5 | /usr/local/bin/jupyter notebook --NotebookApp.notebook_dir='/data/notebooks'  --ip=0.0.0.0 \
6 |     --allow-root >> /data/logs/jupyter-notebook.log
7 | 


--------------------------------------------------------------------------------
/backend/jobs/run_web.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | 
3 | export PYTHONIOENCODING=utf-8
4 | export LANG=zh_CN.UTF-8
5 | export PYTHONPATH=/data/stock
6 | export LC_CTYPE=zh_CN.UTF-8
7 | 
8 | echo "" > /data/logs/web.log
9 | /usr/local/bin/python3 /data/stock/web/main.py -log_file_prefix=/data/logs/web.log


--------------------------------------------------------------------------------
/backend/jobs/start_mariadb.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/sh
 2 | 
 3 | DATE=`date +%Y-%m-%d:%H:%M:%S`
 4 | echo $DATE
 5 | 
 6 | if [ ! -d "/data/mariadb" ]; then
 7 |     mkdir -p /data/mariadb
 8 |     /usr/bin/mysql_install_db
 9 | fi
10 | 
11 | 
12 | /usr/bin/mysqld_safe >> /data/logs/start_mariadb.log


--------------------------------------------------------------------------------
/backend/jobs/test_akshare/test_stock_zh_a_daily.py:
--------------------------------------------------------------------------------
 1 | #!/usr/local/bin/python3
 2 | # -*- coding: utf-8 -*-
 3 | 
 4 | import akshare as ak
 5 | import libs.common as common
 6 | 
 7 | print(ak.__version__)
 8 | 
 9 | # 历史行情数据
10 | # 日频率
11 | # 接口: stock_zh_a_daily
12 | # 目标地址: https://finance.sina.com.cn/realstock/company/sh600006/nc.shtml(示例)
13 | # 描述: A 股数据是从新浪财经获取的数据, 历史数据按日频率更新; 注意其中的 sh689009 为 CDR, 请 通过 stock_zh_a_cdr_daily 接口获取
14 | # 限量: 单次返回指定 A 股上市公司指定日期间的历史行情日频率数据
15 | # adjust=""; 默认为空: 返回不复权的数据; qfq: 返回前复权后的数据; hfq: 返回后复权后的数据;
16 | 
17 | stock_zh_a_daily_qfq_df = ak.stock_zh_a_daily(symbol="sz000002", adjust="")
18 | print(stock_zh_a_daily_qfq_df)
19 | 
20 | stock_zh_a_daily_qfq_df = ak.stock_zh_a_daily(symbol="sz000002", start_date="20200101", end_date="20210101", adjust="")
21 | print(stock_zh_a_daily_qfq_df)
22 | 
23 | # 插入到 MySQL 数据库中
24 | common.insert_db(stock_zh_a_daily_qfq_df, "stock_zh_a_daily", True, "`symbol`")
25 | 
26 | 
27 | 
28 | 


--------------------------------------------------------------------------------
/backend/jobs/test_akshare/test_stock_zh_a_spot.py:
--------------------------------------------------------------------------------
 1 | #!/usr/local/bin/python3
 2 | # -*- coding: utf-8 -*-
 3 | 
 4 | import akshare as ak
 5 | import libs.common as common
 6 | 
 7 | print(ak.__version__)
 8 | 
 9 | # 实时行情数据
10 | # 接口: stock_zh_a_spot
11 | # 目标地址: http://vip.stock.finance.sina.com.cn/mkt/#hs_a
12 | # 描述: A 股数据是从新浪财经获取的数据, 重复运行本函数会被新浪暂时封 IP, 建议增加时间间隔
13 | # 限量: 单次返回所有 A 股上市公司的实时行情数据
14 | 
15 | stock_zh_a_spot_df = ak.stock_zh_a_spot()
16 | print(stock_zh_a_spot_df)
17 | 
18 | # 插入到 MySQL 数据库中
19 | common.insert_db(stock_zh_a_spot_df, "stock_zh_a_spot", True, "`symbol`")
20 | 


--------------------------------------------------------------------------------
/backend/jobs/test_akshare/test_stock_zh_index_spot.py:
--------------------------------------------------------------------------------
 1 | #!/usr/local/bin/python3
 2 | # -*- coding: utf-8 -*-
 3 | 
 4 | import akshare as ak
 5 | import libs.common as common
 6 | 
 7 | print(ak.__version__)
 8 | 
 9 | #stock_sse_summary_df = ak.stock_sse_summary()
10 | #print(stock_sse_summary_df)
11 | 
12 | # 接口: stock_zh_index_spot
13 | # 目标地址: http://vip.stock.finance.sina.com.cn/mkt/#hs_s
14 | # 描述: 中国股票指数数据, 注意该股票指数指新浪提供的国内股票指数
15 | # 限量: 单次返回所有指数的实时行情数据
16 | stock_zh_index_spot_df = ak.stock_zh_index_spot()
17 | print(stock_zh_index_spot_df)
18 | 
19 | # 插入到 MySQL 数据库中
20 | common.insert_db(stock_zh_index_spot_df, "stock_zh_index_spot_df", True, "`symbol`")
21 | 


--------------------------------------------------------------------------------
/backend/old_jobs/README.md:
--------------------------------------------------------------------------------
1 | ## 说明
2 | 
3 | 
4 | 之前测试使用的脚本。执行了一段时间,只是用来进行练习使用的。
5 | 


--------------------------------------------------------------------------------
/backend/supervisor/supervisord.conf:
--------------------------------------------------------------------------------
 1 | [unix_http_server]
 2 | file=/tmp/supervisor.sock   ; the path to the socket file
 3 | 
 4 | [inet_http_server]         ; inet (TCP) server disabled by default
 5 | port=*:9001        ; ip_address:port specifier, *:port for all iface
 6 | ;username=user              ; default is no username (open server)
 7 | ;password=123               ; default is no password (open server)
 8 | 
 9 | [supervisord]
10 | logfile=/tmp/supervisord.log ; main log file; default $CWD/supervisord.log
11 | logfile_maxbytes=50MB        ; max main logfile bytes b4 rotation; default 50MB
12 | logfile_backups=10           ; # of main logfile backups; 0 means none, default 10
13 | loglevel=info                ; log level; default info; others: debug,warn,trace
14 | pidfile=/tmp/supervisord.pid ; supervisord pidfile; default supervisord.pid
15 | nodaemon=false               ; start in foreground if true; default false
16 | minfds=1024                  ; min. avail startup file descriptors; default 1024
17 | minprocs=200                 ; min. avail process descriptors;default 200
18 | 
19 | [rpcinterface:supervisor]
20 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
21 | 
22 | [supervisorctl]
23 | serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket
24 | 
25 | [program:init]
26 | command=/data/stock/jobs/run_init.sh
27 | autostart=true
28 | autorestart=true
29 | startsecs=20
30 | priority=1
31 | stopasgroup=true
32 | killasgroup=true
33 | 
34 | [program:cron]
35 | command=/data/stock/jobs/run_cron.sh
36 | autostart=true
37 | autorestart=true
38 | startsecs=20
39 | priority=1
40 | stopasgroup=true
41 | killasgroup=true
42 | 
43 | 
44 | [program:stock-web]
45 | command=/data/stock/jobs/run_web.sh
46 | autostart=true
47 | autorestart=true
48 | startsecs=20
49 | priority=1
50 | stopasgroup=true
51 | killasgroup=true
52 | 


--------------------------------------------------------------------------------
/backend/web/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/README.md


--------------------------------------------------------------------------------
/backend/web/base.py:
--------------------------------------------------------------------------------
 1 | #!/usr/local/bin/python3
 2 | # -*- coding: utf-8 -*-
 3 | 
 4 | import tornado.web
 5 | import libs.stock_web_dic as stock_web_dic
 6 | import libs.common as common
 7 | import logging
 8 | 
 9 | #基础handler,主要负责检查mysql的数据库链接。
10 | class BaseHandler(tornado.web.RequestHandler):
11 |     def set_default_headers(self):
12 |         headers = self.request.headers
13 |         # logging.info('head的类型:',type(headers))
14 |         origin =  headers.get('origin',None)
15 |         logging.info("######################## BaseHandler ########################")
16 |         logging.info(origin)
17 |         
18 |         if origin != None and origin.find("localhost") > 0:
19 |             self.set_header("Access-Control-Allow-Credentials", "true")
20 |             self.set_header("Access-Control-Allow-Origin",origin)
21 |             self.set_header("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS")
22 |             self.set_header("Access-Control-Allow-Headers", "x-token, authorization, Authorization, Content-Type, Access-Control-Allow-Origin, Access-Control-Allow-Headers, X-Requested-By, Access-Control-Allow-Methods")
23 |             self.set_header("Access-Control-Expose-Headers", "Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma")
24 |     # 同时定义一个option方法
25 |     def options(self):
26 |         self.set_status(204)
27 |         self.finish()
28 | 
29 |     @property
30 |     def db(self):
31 |         try:
32 |             # check every time。
33 |             self.application.db.query("SELECT 1 ")
34 |         except Exception as e:
35 |             print(e)
36 |             self.application.db.reconnect()
37 |         return self.application.db
38 | 
39 | class LeftMenu:
40 |     def __init__(self, url):
41 |         self.leftMenuList = stock_web_dic.STOCK_WEB_DATA_LIST
42 |         self.current_url = url
43 | 
44 | # 获得左菜单。
45 | def GetLeftMenu(url):
46 |     return LeftMenu(url)
47 | 


--------------------------------------------------------------------------------
/backend/web/chartHandler.py:
--------------------------------------------------------------------------------
 1 | #!/usr/local/bin/python3
 2 | # -*- coding: utf-8 -*-
 3 | 
 4 | 
 5 | from tornado import gen
 6 | import libs.stock_web_dic as stock_web_dic
 7 | import web.base as webBase
 8 | import libs.common as common
 9 | import logging
10 | import tornado.web
11 | import matplotlib
12 | matplotlib.use('Agg')
13 | import matplotlib.pyplot as plt
14 | import numpy as np
15 | import io
16 | 
17 | def GenImage(freq):
18 |     t = np.linspace(0, 10, 500)
19 |     y = np.sin(t * freq * 2 * 3.141)
20 |     fig1 = plt.figure()
21 |     plt.plot(t, y)
22 |     plt.xlabel('Time [s]')
23 |     memdata = io.BytesIO()
24 |     plt.grid(True)
25 |     plt.savefig(memdata, format='png')
26 |     image = memdata.getvalue()
27 |     return image
28 | 
29 | 
30 | class ImageHandler(tornado.web.RequestHandler):
31 |     @gen.coroutine
32 |     def get(self):
33 |         image = GenImage(0.5)
34 |         self.set_header('Content-type', 'image/png')
35 |         self.set_header('Content-length', len(image))
36 |         self.write(image)
37 | 
38 | # 获得页面数据。
39 | class GetChartHtmlHandler(webBase.BaseHandler):
40 |     @gen.coroutine
41 |     def get(self):
42 |         name = self.get_argument("table_name", default=None, strip=False)
43 |         #stockWeb = stock_web_dic.STOCK_WEB_DATA_MAP[name]
44 |         # self.uri_ = ("self.request.url:", self.request.uri)
45 |         # print self.uri_
46 |         logging.info("chart...")
47 |         self.render("stock_chart.html", entries="",
48 |                     pythonStockVersion=common.__version__,
49 |                     leftMenu=webBase.GetLeftMenu(self.request.uri))
50 | 
51 | 


--------------------------------------------------------------------------------
/backend/web/minstServingHandler.py:
--------------------------------------------------------------------------------
 1 | #!/usr/local/bin/python3
 2 | # -*- coding: utf-8 -*-
 3 | import os.path
 4 | import json
 5 | import subprocess
 6 | import torndb
 7 | import tornado.escape
 8 | from tornado import gen
 9 | import tornado.httpserver
10 | import tornado.ioloop
11 | import tornado.options
12 | import tornado.web
13 | import web.base as webBase
14 | import logging
15 | import numpy as np
16 | from PIL import Image
17 | from PIL import ImageOps
18 | import base64
19 | import io #python2 import StringIO
20 | 
21 | work_dir = "/data/stock/tf/minst_serving/input_data"
22 | out_dir = "/static/img/minst_serving/%s.bmp"
23 | 
24 | 
25 | # 获得页面数据。
26 | class GetMinstServingHtmlHandler(webBase.BaseHandler):
27 |     @gen.coroutine
28 |     def get(self):
29 |         # print self.uri_
30 |         arr = np.arange(30)
31 |         image_array = []
32 |         for idx in arr:
33 |             out_file = out_dir % ("%05d" % idx)
34 |             print(out_file)
35 |             image_array.append(out_file)
36 |         self.render("minst_serving.html", image_array=image_array)
37 | 
38 | 
39 | # 获得股票数据内容。
40 | class GetPredictionDataHandler(webBase.BaseHandler):
41 |     def get(self):
42 |         # 获得分页参数。
43 |         img_url = self.get_argument("img_url", default=0, strip=False)
44 |         print(img_url)
45 |         img_obj = Image.open("/data/stock/web" + img_url)
46 |         print("img_obj", img_obj)
47 |         server = "0.0.0.0:8500"
48 |         prediction = do_inference(server, img_obj)
49 |         print('######### prediction : ', prediction)
50 |         self.write(json.dumps(prediction))
51 | 
52 | 
53 | # 获得股票数据内容。
54 | class GetPrediction2DataHandler(webBase.BaseHandler):
55 |     def post(self):
56 |         # 获得分页参数。
57 |         imgStr = self.get_argument("txt", default="", strip=False)
58 |         # imgStr.replace(" ", "+")
59 |         imgStr = base64.b64decode(imgStr)
60 |         print("imgStr:", type(imgStr))
61 |         image = Image.open(io.StringIO(imgStr))
62 |         image.thumbnail((28, 28), Image.ANTIALIAS)
63 |         image = image.convert('L')
64 |         image = ImageOps.invert(image)
65 |         image.save(work_dir + "/web-tmp.bmp", format="BMP") #保存看看,是否
66 |         #print(image)
67 |         # img_url = self.get_argument("img_url", default=0, strip=False)
68 |         # print(img_url)
69 |         server = "0.0.0.0:8500"
70 |         prediction = do_inference(server, image)
71 |         print('######### prediction : ', prediction)
72 |         self.write(json.dumps(prediction))
73 | 
74 | 
75 | 
76 | # 调用 grpc 代码,将图片转换成数组,让后放到 grpc 调用。
77 | def do_inference(hostport, img_obj):
78 | 
79 |     print("############", hostport)
80 | 
81 | 


--------------------------------------------------------------------------------
/backend/web/static/css/bootstrap-colorpicker.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 |  * Bootstrap Colorpicker
3 |  * http://mjolnic.github.io/bootstrap-colorpicker/
4 |  *
5 |  * Originally written by (c) 2012 Stefan Petre
6 |  * Licensed under the Apache License v2.0
7 |  * http://www.apache.org/licenses/LICENSE-2.0.txt
8 |  *
9 |  */.colorpicker-saturation{float:left;width:100px;height:100px;cursor:crosshair;background-image:url(../images/bootstrap-colorpicker/saturation.png)}.colorpicker-saturation i{position:absolute;top:0;left:0;display:block;width:5px;height:5px;margin:-4px 0 0 -4px;border:1px solid #000;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.colorpicker-saturation i b{display:block;width:5px;height:5px;border:1px solid #fff;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.colorpicker-alpha,.colorpicker-hue{float:left;width:15px;height:100px;margin-bottom:4px;margin-left:4px;cursor:row-resize}.colorpicker-alpha i,.colorpicker-hue i{position:absolute;top:0;left:0;display:block;width:100%;height:1px;margin-top:-1px;background:#000;border-top:1px solid #fff}.colorpicker-hue{background-image:url(../images/bootstrap-colorpicker/hue.png)}.colorpicker-alpha,.colorpicker-color{background-image:url(../images/bootstrap-colorpicker/alpha.png)}.colorpicker-alpha{display:none}.colorpicker:after,.colorpicker:before{position:absolute;display:inline-block;content:''}.colorpicker-alpha,.colorpicker-hue,.colorpicker-saturation{background-size:contain}.colorpicker{top:0;left:0;z-index:2500;min-width:130px;padding:4px;margin-top:1px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*zoom:1}.colorpicker:after,.colorpicker:before{line-height:0}.colorpicker:before{top:-7px;left:6px;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,.2)}.colorpicker:after{clear:both;top:-6px;left:7px;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent}.colorpicker div{position:relative}.colorpicker.colorpicker-with-alpha{min-width:140px}.colorpicker.colorpicker-with-alpha .colorpicker-alpha{display:block}.colorpicker-color{height:10px;margin-top:5px;clear:both;background-position:0 100%}.colorpicker-color div{height:10px}.colorpicker-selectors{display:none;height:10px;margin-top:5px;clear:both}.colorpicker-selectors i{float:left;width:10px;height:10px;cursor:pointer}.colorpicker-selectors i+i{margin-left:3px}.colorpicker-element .add-on i,.colorpicker-element .input-group-addon i{display:inline-block;width:16px;height:16px;vertical-align:text-top;cursor:pointer}.colorpicker.colorpicker-inline{position:relative;z-index:auto;display:inline-block;float:none}.colorpicker.colorpicker-horizontal{width:110px;height:auto;min-width:110px}.colorpicker.colorpicker-horizontal .colorpicker-saturation{margin-bottom:4px}.colorpicker.colorpicker-horizontal .colorpicker-color{width:100px}.colorpicker.colorpicker-horizontal .colorpicker-alpha,.colorpicker.colorpicker-horizontal .colorpicker-hue{float:left;width:100px;height:15px;margin-bottom:4px;margin-left:0;cursor:col-resize}.colorpicker.colorpicker-horizontal .colorpicker-alpha i,.colorpicker.colorpicker-horizontal .colorpicker-hue i{position:absolute;top:0;left:0;display:block;width:1px;height:15px;margin-top:0;background:#fff;border:none}.colorpicker.colorpicker-horizontal .colorpicker-hue{background-image:url(../images/bootstrap-colorpicker/hue-horizontal.png)}.colorpicker.colorpicker-horizontal .colorpicker-alpha{background-image:url(../images/bootstrap-colorpicker/alpha-horizontal.png)}.colorpicker.colorpicker-hidden{display:none}.colorpicker.colorpicker-visible{display:block}.colorpicker-inline.colorpicker-visible{display:inline-block}.colorpicker-right:before{right:6px;left:auto}.colorpicker-right:after{right:7px;left:auto}


--------------------------------------------------------------------------------
/backend/web/static/css/bootstrap-timepicker.min.css:
--------------------------------------------------------------------------------
 1 | /*!
 2 |  * Timepicker Component for Twitter Bootstrap
 3 |  *
 4 |  * Copyright 2013 Joris de Wit
 5 |  *
 6 |  * Contributors https://github.com/jdewit/bootstrap-timepicker/graphs/contributors
 7 |  *
 8 |  * For the full copyright and license information, please view the LICENSE
 9 |  * file that was distributed with this source code.
10 |  */.bootstrap-timepicker{position:relative}.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu{left:auto;right:0}.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu:before{left:auto;right:12px}.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu:after{left:auto;right:13px}.bootstrap-timepicker .input-group-addon{cursor:pointer}.bootstrap-timepicker .input-group-addon i{display:inline-block;width:16px;height:16px}.bootstrap-timepicker-widget.dropdown-menu{padding:4px}.bootstrap-timepicker-widget.dropdown-menu.open{display:inline-block}.bootstrap-timepicker-widget.dropdown-menu:before{border-bottom:7px solid rgba(0,0,0,.2);border-left:7px solid transparent;border-right:7px solid transparent;content:"";display:inline-block;position:absolute}.bootstrap-timepicker-widget.dropdown-menu:after{border-bottom:6px solid #FFF;border-left:6px solid transparent;border-right:6px solid transparent;content:"";display:inline-block;position:absolute}.bootstrap-timepicker-widget.timepicker-orient-left:before{left:6px}.bootstrap-timepicker-widget.timepicker-orient-left:after{left:7px}.bootstrap-timepicker-widget.timepicker-orient-right:before{right:6px}.bootstrap-timepicker-widget.timepicker-orient-right:after{right:7px}.bootstrap-timepicker-widget.timepicker-orient-top:before{top:-7px}.bootstrap-timepicker-widget.timepicker-orient-top:after{top:-6px}.bootstrap-timepicker-widget.timepicker-orient-bottom:before{bottom:-7px;border-bottom:0;border-top:7px solid #999}.bootstrap-timepicker-widget.timepicker-orient-bottom:after{bottom:-6px;border-bottom:0;border-top:6px solid #fff}.bootstrap-timepicker-widget a.btn,.bootstrap-timepicker-widget input{border-radius:4px}.bootstrap-timepicker-widget table{width:100%;margin:0}.bootstrap-timepicker-widget table td{text-align:center;height:30px;margin:0;padding:2px}.bootstrap-timepicker-widget table td:not(.separator){min-width:30px}.bootstrap-timepicker-widget table td span{width:100%}.bootstrap-timepicker-widget table td a{border:1px solid transparent;width:100%;display:inline-block;margin:0;padding:8px 0;outline:0;color:#333}.bootstrap-timepicker-widget table td a:hover{text-decoration:none;background-color:#eee;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;border-color:#ddd}.bootstrap-timepicker-widget table td a i{margin-top:2px;font-size:18px}.bootstrap-timepicker-widget table td input{width:25px;margin:0;text-align:center}.bootstrap-timepicker-widget .modal-content{padding:4px}@media (min-width:767px){.bootstrap-timepicker-widget.modal{width:200px;margin-left:-100px}}@media (max-width:767px){.bootstrap-timepicker,.bootstrap-timepicker .dropdown-menu{width:100%}}


--------------------------------------------------------------------------------
/backend/web/static/css/fonts.googleapis.com.css:
--------------------------------------------------------------------------------
 1 | @font-face {
 2 |   font-family: 'Open Sans';
 3 |   font-style: normal;
 4 |   font-weight: 300;
 5 |   src: local('Open Sans Light'), local('OpenSans-Light'), url(/static/font-awesome/opensans/v13/DXI1ORHCpsQm3Vp6mXoaTXhCUOGz7vYGh680lGh-uXM.woff) format('woff');
 6 | }
 7 | @font-face {
 8 |   font-family: 'Open Sans';
 9 |   font-style: normal;
10 |   font-weight: 400;
11 |   src: local('Open Sans'), local('OpenSans'), url(/static/font-awesome/opensans/v13/cJZKeOuBrn4kERxqtaUH3T8E0i7KZn-EPnyo3HZu7kw.woff) format('woff');
12 | }
13 | 


--------------------------------------------------------------------------------
/backend/web/static/css/jquery-ui.custom.min.css:
--------------------------------------------------------------------------------
1 | /*! jQuery UI - v1.11.4 - 2015-09-20
2 | * http://jqueryui.com
3 | * Includes: core.css, draggable.css, resizable.css, selectable.css, sortable.css, slider.css
4 | * Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:after,.ui-helper-clearfix:before{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-autohide .ui-resizable-handle,.ui-resizable-disabled .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted #000}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}


--------------------------------------------------------------------------------
/backend/web/static/css/select.dataTables.min.css:
--------------------------------------------------------------------------------
1 | table.dataTable tbody>tr.selected,table.dataTable tbody>tr>.selected{background-color:#B0BED9}table.dataTable.stripe tbody>tr.odd.selected,table.dataTable.stripe tbody>tr.odd>.selected,table.dataTable.display tbody>tr.odd.selected,table.dataTable.display tbody>tr.odd>.selected{background-color:#acbad4}table.dataTable.hover tbody>tr.selected:hover,table.dataTable.hover tbody>tr>.selected:hover,table.dataTable.display tbody>tr.selected:hover,table.dataTable.display tbody>tr>.selected:hover{background-color:#aab7d1}table.dataTable.order-column tbody>tr.selected>.sorting_1,table.dataTable.order-column tbody>tr.selected>.sorting_2,table.dataTable.order-column tbody>tr.selected>.sorting_3,table.dataTable.order-column tbody>tr>.selected,table.dataTable.display tbody>tr.selected>.sorting_1,table.dataTable.display tbody>tr.selected>.sorting_2,table.dataTable.display tbody>tr.selected>.sorting_3,table.dataTable.display tbody>tr>.selected{background-color:#acbad5}table.dataTable.display tbody>tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody>tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody>tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody>tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody>tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody>tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody>tr.odd>.selected,table.dataTable.order-column.stripe tbody>tr.odd>.selected{background-color:#a6b4cd}table.dataTable.display tbody>tr.even>.selected,table.dataTable.order-column.stripe tbody>tr.even>.selected{background-color:#acbad5}table.dataTable.display tbody>tr.selected:hover>.sorting_1,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody>tr.selected:hover>.sorting_2,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody>tr.selected:hover>.sorting_3,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_3{background-color:#a5b2cb}table.dataTable.display tbody>tr:hover>.selected,table.dataTable.display tbody>tr>.selected:hover,table.dataTable.order-column.hover tbody>tr:hover>.selected,table.dataTable.order-column.hover tbody>tr>.selected:hover{background-color:#a2aec7}table.dataTable tbody td.select-checkbox,table.dataTable tbody th.select-checkbox{position:relative}table.dataTable tbody td.select-checkbox:before,table.dataTable tbody td.select-checkbox:after,table.dataTable tbody th.select-checkbox:before,table.dataTable tbody th.select-checkbox:after{display:block;position:absolute;top:1.2em;left:50%;width:12px;height:12px;box-sizing:border-box}table.dataTable tbody td.select-checkbox:before,table.dataTable tbody th.select-checkbox:before{content:' ';margin-top:-6px;margin-left:-6px;border:1px solid black;border-radius:3px}table.dataTable tr.selected td.select-checkbox:after,table.dataTable tr.selected th.select-checkbox:after{content:'\2714';margin-top:-11px;margin-left:-4px;text-align:center;text-shadow:1px 1px #B0BED9, -1px -1px #B0BED9, 1px -1px #B0BED9, -1px 1px #B0BED9}div.dataTables_wrapper span.select-info,div.dataTables_wrapper span.select-item{margin-left:0.5em}@media screen and (max-width: 640px){div.dataTables_wrapper span.select-info,div.dataTables_wrapper span.select-item{margin-left:0;display:block}}
2 | 


--------------------------------------------------------------------------------
/backend/web/static/font-awesome/4.5.0/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/font-awesome/4.5.0/fonts/fontawesome-webfont.ttf


--------------------------------------------------------------------------------
/backend/web/static/font-awesome/4.5.0/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/font-awesome/4.5.0/fonts/fontawesome-webfont.woff


--------------------------------------------------------------------------------
/backend/web/static/font-awesome/4.5.0/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/font-awesome/4.5.0/fonts/fontawesome-webfont.woff2


--------------------------------------------------------------------------------
/backend/web/static/font-awesome/opensans/v13/DXI1ORHCpsQm3Vp6mXoaTXhCUOGz7vYGh680lGh-uXM.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/font-awesome/opensans/v13/DXI1ORHCpsQm3Vp6mXoaTXhCUOGz7vYGh680lGh-uXM.woff


--------------------------------------------------------------------------------
/backend/web/static/font-awesome/opensans/v13/cJZKeOuBrn4kERxqtaUH3T8E0i7KZn-EPnyo3HZu7kw.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/font-awesome/opensans/v13/cJZKeOuBrn4kERxqtaUH3T8E0i7KZn-EPnyo3HZu7kw.woff


--------------------------------------------------------------------------------
/backend/web/static/img/diff-n-bokeh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/img/diff-n-bokeh.png


--------------------------------------------------------------------------------
/backend/web/static/img/stock-show-01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/img/stock-show-01.jpg


--------------------------------------------------------------------------------
/backend/web/static/img/stock2-001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/img/stock2-001.png


--------------------------------------------------------------------------------
/backend/web/static/img/stock2-002.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/img/stock2-002.png


--------------------------------------------------------------------------------
/backend/web/static/img/stock2-003.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/img/stock2-003.png


--------------------------------------------------------------------------------
/backend/web/static/img/支付宝--微信支付.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/img/支付宝--微信支付.jpg


--------------------------------------------------------------------------------
/backend/web/static/js/autosize.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | 	Autosize 3.0.15
3 | 	license: MIT
4 | 	http://www.jacklmoore.com/autosize
5 | */
6 | !function(a,b){if("function"==typeof define&&define.amd)define(["exports","module"],b);else if("undefined"!=typeof exports&&"undefined"!=typeof module)b(exports,module);else{var c={exports:{}};b(c.exports,c),a.autosize=c.exports}}(this,function(a,b){"use strict";function c(a){function b(){var b=window.getComputedStyle(a,null);n=b.overflowY,"vertical"===b.resize?a.style.resize="none":"both"===b.resize&&(a.style.resize="horizontal"),m="content-box"===b.boxSizing?-(parseFloat(b.paddingTop)+parseFloat(b.paddingBottom)):parseFloat(b.borderTopWidth)+parseFloat(b.borderBottomWidth),isNaN(m)&&(m=0),e()}function c(b){var c=a.style.width;a.style.width="0px",a.offsetWidth,a.style.width=c,n=b,l&&(a.style.overflowY=b),d()}function d(){var b=window.pageYOffset,c=document.body.scrollTop,d=a.style.height;a.style.height="auto";var e=a.scrollHeight+m;return 0===a.scrollHeight?void(a.style.height=d):(a.style.height=e+"px",o=a.clientWidth,document.documentElement.scrollTop=b,void(document.body.scrollTop=c))}function e(){var b=a.style.height;d();var e=window.getComputedStyle(a,null);if(e.height!==a.style.height?"visible"!==n&&c("visible"):"hidden"!==n&&c("hidden"),b!==a.style.height){var f=g("autosize:resized");a.dispatchEvent(f)}}var h=void 0===arguments[1]?{}:arguments[1],i=h.setOverflowX,j=void 0===i?!0:i,k=h.setOverflowY,l=void 0===k?!0:k;if(a&&a.nodeName&&"TEXTAREA"===a.nodeName&&!f.has(a)){var m=null,n=null,o=a.clientWidth,p=function(){a.clientWidth!==o&&e()},q=function(b){window.removeEventListener("resize",p,!1),a.removeEventListener("input",e,!1),a.removeEventListener("keyup",e,!1),a.removeEventListener("autosize:destroy",q,!1),a.removeEventListener("autosize:update",e,!1),f["delete"](a),Object.keys(b).forEach(function(c){a.style[c]=b[c]})}.bind(a,{height:a.style.height,resize:a.style.resize,overflowY:a.style.overflowY,overflowX:a.style.overflowX,wordWrap:a.style.wordWrap});a.addEventListener("autosize:destroy",q,!1),"onpropertychange"in a&&"oninput"in a&&a.addEventListener("keyup",e,!1),window.addEventListener("resize",p,!1),a.addEventListener("input",e,!1),a.addEventListener("autosize:update",e,!1),f.add(a),j&&(a.style.overflowX="hidden",a.style.wordWrap="break-word"),b()}}function d(a){if(a&&a.nodeName&&"TEXTAREA"===a.nodeName){var b=g("autosize:destroy");a.dispatchEvent(b)}}function e(a){if(a&&a.nodeName&&"TEXTAREA"===a.nodeName){var b=g("autosize:update");a.dispatchEvent(b)}}var f="function"==typeof Set?new Set:function(){var a=[];return{has:function(b){return Boolean(a.indexOf(b)>-1)},add:function(b){a.push(b)},"delete":function(b){a.splice(a.indexOf(b),1)}}}(),g=function(a){return new Event(a)};try{new Event("test")}catch(h){g=function(a){var b=document.createEvent("Event");return b.initEvent(a,!0,!1),b}}var i=null;"undefined"==typeof window||"function"!=typeof window.getComputedStyle?(i=function(a){return a},i.destroy=function(a){return a},i.update=function(a){return a}):(i=function(a,b){return a&&Array.prototype.forEach.call(a.length?a:[a],function(a){return c(a,b)}),a},i.destroy=function(a){return a&&Array.prototype.forEach.call(a.length?a:[a],d),a},i.update=function(a){return a&&Array.prototype.forEach.call(a.length?a:[a],e),a}),b.exports=i});


--------------------------------------------------------------------------------
/backend/web/static/js/bootstrap-datepicker.zh-CN.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Simplified Chinese translation for bootstrap-datepicker
 3 |  * Yuan Cheung <advanimal@gmail.com>
 4 |  */
 5 | ;(function($){
 6 | 	$.fn.datepicker.dates['zh-CN'] = {
 7 | 		days: ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"],
 8 | 		daysShort: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
 9 | 		daysMin:  ["日", "一", "二", "三", "四", "五", "六"],
10 | 		months: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
11 | 		monthsShort: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
12 | 		today: "今日",
13 | 		clear: "清除",
14 | 		format: "yyyy年mm月dd日",
15 | 		titleFormat: "yyyy年mm月",
16 | 		weekStart: 1
17 | 	};
18 | }(jQuery));


--------------------------------------------------------------------------------
/backend/web/static/js/buttons.colVis.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 |  * Column visibility buttons for Buttons and DataTables.
3 |  * 2015 SpryMedia Ltd - datatables.net/license
4 |  */
5 | !function(a){"function"==typeof define&&define.amd?define(["jquery","datatables.net","datatables.net-buttons"],function(b){return a(b,window,document)}):"object"==typeof exports?module.exports=function(b,c){return b||(b=window),c&&c.fn.dataTable||(c=require("datatables.net")(b,c).$),c.fn.dataTable.Buttons||require("datatables.net-buttons")(b,c),a(c,b,b.document)}:a(jQuery,window,document)}(function(a,b,c,d){"use strict";var e=a.fn.dataTable;return a.extend(e.ext.buttons,{colvis:function(a,b){return{extend:"collection",text:function(a){return a.i18n("buttons.colvis","Column visibility")},className:"buttons-colvis",buttons:[{extend:"columnsToggle",columns:b.columns}]}},columnsToggle:function(a,b){var c=a.columns(b.columns).indexes().map(function(a){return{extend:"columnToggle",columns:a}}).toArray();return c},columnToggle:function(a,b){return{extend:"columnVisibility",columns:b.columns}},columnsVisibility:function(a,b){var c=a.columns(b.columns).indexes().map(function(a){return{extend:"columnVisibility",columns:a,visibility:b.visibility}}).toArray();return c},columnVisibility:{columns:d,text:function(a,b,c){return c._columnText(a,c.columns)},className:"buttons-columnVisibility",action:function(a,b,c,e){var f=b.columns(e.columns),g=f.visible();f.visible(e.visibility!==d?e.visibility:!(g.length?g[0]:!1))},init:function(a,b,c){var d=this,e=a.column(c.columns);a.on("column-visibility.dt"+c.namespace,function(a,b,e,f){b.bDestroying||e!==c.columns||d.active(f)}).on("column-reorder.dt"+c.namespace,function(b,e,f){if(1===a.columns(c.columns).count()){"number"==typeof c.columns&&(c.columns=f.mapping[c.columns]);var g=a.column(c.columns);d.text(c._columnText(a,c.columns)),d.active(g.visible())}}),this.active(e.visible())},destroy:function(a,b,c){a.off("column-visibility.dt"+c.namespace).off("column-reorder.dt"+c.namespace)},_columnText:function(a,b){var c=a.column(b).index();return a.settings()[0].aoColumns[c].sTitle.replace(/\n/g," ").replace(/<.*?>/g,"").replace(/^\s+|\s+$/g,"")}},colvisRestore:{className:"buttons-colvisRestore",text:function(a){return a.i18n("buttons.colvisRestore","Restore visibility")},init:function(a,b,c){c._visOriginal=a.columns().indexes().map(function(b){return a.column(b).visible()}).toArray()},action:function(a,b,c,d){b.columns().every(function(a){var c=b.colReorder&&b.colReorder.transpose?b.colReorder.transpose(a,"toOriginal"):a;this.visible(d._visOriginal[c])})}},colvisGroup:{className:"buttons-colvisGroup",action:function(a,b,c,d){b.columns(d.show).visible(!0),b.columns(d.hide).visible(!1)},show:[],hide:[]}}),e.Buttons});


--------------------------------------------------------------------------------
/backend/web/static/js/buttons.print.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 |  * Print button for Buttons and DataTables.
3 |  * 2015 SpryMedia Ltd - datatables.net/license
4 |  */
5 | !function(a){"function"==typeof define&&define.amd?define(["jquery","datatables.net","datatables.net-buttons"],function(b){return a(b,window,document)}):"object"==typeof exports?module.exports=function(b,c){return b||(b=window),c&&c.fn.dataTable||(c=require("datatables.net")(b,c).$),c.fn.dataTable.Buttons||require("datatables.net-buttons")(b,c),a(c,b,b.document)}:a(jQuery,window,document)}(function(a,b,c,d){"use strict";var e=a.fn.dataTable,f=c.createElement("a"),g=function(b){var c,d=a(b).clone()[0];return"link"===d.nodeName.toLowerCase()&&(f.href=d.href,c=f.host,-1===c.indexOf("/")&&0!==f.pathname.indexOf("/")&&(c+="/"),d.href=f.protocol+"//"+c+f.pathname+f.search),d.outerHTML};return e.ext.buttons.print={className:"buttons-print",text:function(a){return a.i18n("buttons.print","Print")},action:function(c,d,e,f){var h=d.buttons.exportData(f.exportOptions),i=function(a,b){for(var c="<tr>",d=0,e=a.length;e>d;d++)c+="<"+b+">"+a[d]+"</"+b+">";return c+"</tr>"},j='<table class="'+d.table().node().className+'">';f.header&&(j+="<thead>"+i(h.header,"th")+"</thead>"),j+="<tbody>";for(var k=0,l=h.body.length;l>k;k++)j+=i(h.body[k],"td");j+="</tbody>",f.footer&&(j+="<tfoot>"+i(h.footer,"th")+"</tfoot>");var m=b.open("",""),n=f.title;"function"==typeof n&&(n=n()),-1!==n.indexOf("*")&&(n=n.replace("*",a("title").text())),m.document.close();var o="<title>"+n+"</title>";a("style, link").each(function(){o+=g(this)}),a(m.document.head).html(o),a(m.document.body).html("<h1>"+n+"</h1><div>"+f.message+"</div>"+j),f.customize&&f.customize(m),setTimeout(function(){f.autoPrint&&(m.print(),m.close())},250)},title:"*",message:"",exportOptions:{},header:!0,footer:!1,autoPrint:!0,customize:null},e.Buttons});


--------------------------------------------------------------------------------
/backend/web/static/js/datatables.Chinese.json:
--------------------------------------------------------------------------------
 1 | {
 2 |     "sProcessing":   "处理中...",
 3 |     "sLengthMenu":   "显示 _MENU_ 项结果",
 4 |     "sZeroRecords":  "没有匹配结果",
 5 |     "sInfo":         "显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项",
 6 |     "sInfoEmpty":    "显示第 0 至 0 项结果,共 0 项",
 7 |     "sInfoFiltered": "(由 _MAX_ 项结果过滤)",
 8 |     "sInfoPostFix":  "",
 9 |     "sSearch":       "搜索:",
10 |     "sUrl":          "",
11 |     "sEmptyTable":     "表中数据为空",
12 |     "sLoadingRecords": "载入中...",
13 |     "sInfoThousands":  ",",
14 |     "oPaginate": {
15 |         "sFirst":    "首页",
16 |         "sPrevious": "上页",
17 |         "sNext":     "下页",
18 |         "sLast":     "末页"
19 |     },
20 |     "oAria": {
21 |         "sSortAscending":  ": 以升序排列此列",
22 |         "sSortDescending": ": 以降序排列此列"
23 |     }
24 | }


--------------------------------------------------------------------------------
/backend/web/static/js/draw.js:
--------------------------------------------------------------------------------
 1 | 
 2 | var drawing = false;
 3 | 
 4 | var context;
 5 | 
 6 | var offset_left = 0;
 7 | var offset_top = 0;
 8 | 
 9 | 
10 | function start_canvas ()
11 | {
12 |     var scribbler = document.getElementById ("the_stage");
13 |     context = scribbler.getContext ("2d");
14 |     scribbler.onmousedown = function (event) {mousedown(event)};
15 |     scribbler.onmousemove = function (event) {mousemove(event)};
16 |     scribbler.onmouseup   = function (event) {mouseup(event)};
17 |     for (var o = scribbler; o ; o = o.offsetParent) {
18 |     offset_left += (o.offsetLeft - o.scrollLeft);
19 |     offset_top  += (o.offsetTop - o.scrollTop);
20 |     }
21 |     draw();
22 | }
23 | 
24 | function getPosition(evt)
25 | {
26 |     evt = (evt) ?  evt : ((event) ? event : null);
27 |     var left = 0;
28 |     var top = 0;
29 |     var scribbler = document.getElementById("the_stage");
30 | 
31 |     if (evt.pageX) {
32 |     left = evt.pageX;
33 |     top  = evt.pageY;
34 |     } else if (document.documentElement.scrollLeft) {
35 |     left = evt.clientX + document.documentElement.scrollLeft;
36 |     top  = evt.clientY + document.documentElement.scrollTop;
37 |     } else  {
38 |     left = evt.clientX + document.body.scrollLeft;
39 |     top  = evt.clientY + document.body.scrollTop;
40 |     }
41 |     left -= offset_left;
42 |     top -= offset_top;
43 | 
44 |     return {x : left, y : top}; 
45 | }
46 | 
47 | function
48 | mousedown(event)
49 | {
50 |     drawing = true;
51 |     var location = getPosition(event);
52 |     context.lineWidth = 20.0;
53 |     context.strokeStyle="#000000";
54 |     context.beginPath();
55 |     context.moveTo(location.x,location.y);
56 | }
57 | 
58 | 
59 | function
60 | mousemove(event)
61 | {
62 |     if (!drawing) 
63 |         return;
64 |     var location = getPosition(event);
65 |     context.lineTo(location.x,location.y);
66 |     context.stroke();
67 | }
68 | 
69 | 
70 | 
71 | function
72 | mouseup(event)
73 | {
74 |     if (!drawing) 
75 |         return;
76 |     mousemove(event);
77 |     drawing = false;
78 | }
79 | 
80 | function draw()
81 | {
82 | 
83 |     context.fillStyle = '#ffffff';
84 |     context.fillRect(0, 0, 400, 400);
85 | 
86 | }
87 | 
88 | function clearCanvas()
89 | {
90 |     context.clearRect (0, 0, 400, 400);
91 |     draw();
92 |     document.getElementById("rec_result").innerHTML = "";
93 | }
94 | 
95 | onload = start_canvas;
96 | 
97 | 


--------------------------------------------------------------------------------
/backend/web/static/js/jquery.dataTables.bootstrap.min.js:
--------------------------------------------------------------------------------
1 | /* Set the defaults for DataTables initialisation */
2 | $.extend(!0,$.fn.dataTable.defaults,{sDom:"<'row'<'col-xs-6'l><'col-xs-6'f>r>t<'row'<'col-xs-6'i><'col-xs-6'p>>",oLanguage:{sLengthMenu:"Display _MENU_ records"}}),$.extend($.fn.dataTableExt.oStdClasses,{sWrapper:"dataTables_wrapper form-inline",sFilterInput:"form-control input-sm",sLengthSelect:"form-control input-sm"}),$.fn.dataTable.Api?($.fn.dataTable.defaults.renderer="bootstrap",$.fn.dataTable.ext.renderer.pageButton.bootstrap=function(a,b,c,d,e,f){var g,h,i=new $.fn.dataTable.Api(a),j=a.oClasses,k=a.oLanguage.oPaginate,l=function(b,d){var m,n,o,p,q=function(a){return a.preventDefault(),$(a.target).parent().hasClass("disabled")?!1:void("ellipsis"!==a.data.action&&i.page(a.data.action).draw(!1))};for(m=0,n=d.length;n>m;m++)if(p=d[m],$.isArray(p))l(b,p);else{switch(g="",h="",p){case"ellipsis":g="&hellip;",h="disabled";break;case"first":g=k.sFirst,h=p+(e>0?"":" disabled");break;case"previous":g=k.sPrevious,h=p+(e>0?"":" disabled");break;case"next":g=k.sNext,h=p+(f-1>e?"":" disabled");break;case"last":g=k.sLast,h=p+(f-1>e?"":" disabled");break;default:g=p+1,h=e===p?"active":""}g&&(o=$("<li>",{"class":j.sPageButton+" "+h,"aria-controls":a.sTableId,tabindex:a.iTabIndex,id:0===c&&"string"==typeof p?a.sTableId+"_"+p:null}).append($("<a>",{href:"#"}).html(g)).appendTo(b),a.oApi._fnBindAction(o,{action:p},q))}};l($(b).empty().html('<ul class="pagination"/>').children("ul"),d)}):($.fn.dataTable.defaults.sPaginationType="bootstrap",$.fn.dataTableExt.oApi.fnPagingInfo=function(a){return{iStart:a._iDisplayStart,iEnd:a.fnDisplayEnd(),iLength:a._iDisplayLength,iTotal:a.fnRecordsTotal(),iFilteredTotal:a.fnRecordsDisplay(),iPage:-1===a._iDisplayLength?0:Math.ceil(a._iDisplayStart/a._iDisplayLength),iTotalPages:-1===a._iDisplayLength?0:Math.ceil(a.fnRecordsDisplay()/a._iDisplayLength)}},$.extend($.fn.dataTableExt.oPagination,{bootstrap:{fnInit:function(a,b,c){var d=(a.oLanguage.oPaginate,function(b){alert(1),b.preventDefault(),a.oApi._fnPageChange(a,b.data.action)&&c(a)});$(b).append('<ul class="pagination"><li class="prev disabled"><a href="#"><i class="fa fa-angle-double-left"></i></a></li><li class="prev disabled"><a href="#"><i class="fa fa-angle-left"></i></a></li><li class="next disabled"><a href="#"><i class="fa fa-angle-right"></i></a></li><li class="next disabled"><a href="#"><i class="fa fa-angle-double-right"></i></a></li></ul>');var e=$("a",b);$(e[0]).bind("click.DT",{action:"first"},d),$(e[1]).bind("click.DT",{action:"previous"},d),$(e[2]).bind("click.DT",{action:"next"},d),$(e[3]).bind("click.DT",{action:"last"},d)},fnUpdate:function(a,b){var c,d,e,f,g,h,i=5,j=a.oInstance.fnPagingInfo(),k=a.aanFeatures.p,l=Math.floor(i/2);for(j.iTotalPages<i?(g=1,h=j.iTotalPages):j.iPage<=l?(g=1,h=i):j.iPage>=j.iTotalPages-l?(g=j.iTotalPages-i+1,h=j.iTotalPages):(g=j.iPage-l+1,h=g+i-1),c=0,d=k.length;d>c;c++){for($("li:gt(0)",k[c]).filter(":not(.next,.prev)").remove(),e=g;h>=e;e++)f=e==j.iPage+1?'class="active"':"",$("<li "+f+'><a href="#">'+e+"</a></li>").insertBefore($("li.next:eq(0)",k[c])[0]).bind("click",function(c){c.preventDefault(),a._iDisplayStart=(parseInt($("a",this).text(),10)-1)*j.iLength,b(a)});0===j.iPage?$("li.prev",k[c]).addClass("disabled"):$("li.prev",k[c]).removeClass("disabled"),j.iPage===j.iTotalPages-1||0===j.iTotalPages?$("li.next",k[c]).addClass("disabled"):$("li.next",k[c]).removeClass("disabled")}}}})),$.fn.DataTable.TableTools&&($.extend(!0,$.fn.DataTable.TableTools.classes,{container:"DTTT btn-group",buttons:{normal:"btn btn-default",disabled:"disabled"},collection:{container:"DTTT_dropdown dropdown-menu",buttons:{normal:"",disabled:"disabled"}},print:{info:"DTTT_print_info modal"},select:{row:"active"}}),$.extend(!0,$.fn.DataTable.TableTools.DEFAULTS.oTags,{collection:{container:"ul",button:"li",liner:"a"}}));


--------------------------------------------------------------------------------
/backend/web/static/update_bokeh.sh:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | rm -f ./js/bokeh.min.js
 4 | rm -f ./js/bokeh-api.min.js
 5 | rm -f ./js/bokeh-gl.min.js
 6 | rm -f ./js/bokeh-tables.min.js
 7 | rm -f ./js/bokeh-widgets.min.js
 8 | 
 9 | cp /usr/local/lib/python3.7/site-packages/bokeh/server/static/js/bokeh.min.js ./js/
10 | cp /usr/local/lib/python3.7/site-packages/bokeh/server/static/js/bokeh-api.min.js ./js/
11 | cp /usr/local/lib/python3.7/site-packages/bokeh/server/static/js/bokeh-gl.min.js ./js/
12 | cp /usr/local/lib/python3.7/site-packages/bokeh/server/static/js/bokeh-tables.min.js ./js/
13 | cp /usr/local/lib/python3.7/site-packages/bokeh/server/static/js/bokeh-widgets.min.js ./js/


--------------------------------------------------------------------------------
/backend/web/templates/bokeh_embed.html:
--------------------------------------------------------------------------------
 1 | <!doctype html>
 2 | 
 3 | <html lang="en">
 4 | <head>
 5 |   <meta charset="utf-8">
 6 |   <title>Embedding a Bokeh Server With {{ framework }}</title>
 7 | </head>
 8 | 
 9 | <body>
10 |   <div>
11 |     This Bokeh app below served by a Bokeh server that has been embedded
12 |     in another web app framework. For more information see the section
13 |     <a  target="_blank"
14 |         href="https://bokeh.pydata.org/en/latest/docs/user_guide/server.html#embedding-bokeh-server-as-a-library">
15 |         Embedding Bokeh Server as a Library</a>
16 |     in the User's Guide.
17 |   </div>
18 |   {{ script|safe }}
19 | </body>
20 | </html>


--------------------------------------------------------------------------------
/backend/web/templates/common/footer.html:
--------------------------------------------------------------------------------
1 | {% block footer %}
2 | {% end %}


--------------------------------------------------------------------------------
/backend/web/templates/common/header.html:
--------------------------------------------------------------------------------
 1 | {% block header %}
 2 | <div id="navbar" class="navbar navbar-default ace-save-state">
 3 |     <div class="navbar-container ace-save-state" id="navbar-container">
 4 |         <div class="navbar-header pull-left">
 5 |             <a href="/stock/" class="navbar-brand">
 6 |                 <small><i class="fa fa-leaf"></i>开源Python全栈股票系统,数据抓取、统计分析、报表展示 版本:{{ pythonStockVersion }}</small>
 7 |             </a>
 8 |         </div>
 9 | 
10 |     </div><!-- /.navbar-container -->
11 | </div>
12 | {% end %}


--------------------------------------------------------------------------------
/backend/web/templates/common/left_menu.html:
--------------------------------------------------------------------------------
 1 | {% block left_menu %}
 2 | <div id="sidebar" class="sidebar responsive ace-save-state">
 3 |     <script type="text/javascript">
 4 |         try{ace.settings.loadState('sidebar')}catch(e){}
 5 |     </script>
 6 | 
 7 |     <ul class="nav nav-list">
 8 |         <li class="">
 9 |             <a href="/">
10 |                 <i class="menu-icon fa fa-tachometer"></i>
11 |                 <span class="menu-text"> Dashboard </span>
12 |             </a>
13 |             <b class="arrow"></b>
14 |         </li>
15 | 
16 |         <li class="active open">
17 |             <!--<a href="#" class="dropdown-toggle">-->
18 |                 <!--<i class="menu-icon fa fa-desktop"></i>-->
19 |                 <!--<span class="menu-text">-->
20 |                     <!--股票原始数据-->
21 |                 <!--</span>-->
22 |                 <!--<b class="arrow fa fa-angle-down"></b>-->
23 |             <!--</a>-->
24 |             <!--<b class="arrow"></b>-->
25 |             <li class="submenu">
26 |                     <li class="open" style="display:none;">
27 |                     {% set loopType = "" %}
28 |                     {% for leftMenuTmp in leftMenu.leftMenuList %}
29 | 
30 |                         {% if leftMenuTmp.type != loopType %}
31 |                         </li>
32 |                         <li class="open">
33 |                             {% set loopType = leftMenuTmp.type %}
34 |                             <a href="#" class="dropdown-toggle">
35 |                                 <i class="menu-icon fa fa-pencil-square-o"></i>
36 |                                 {{ leftMenuTmp.type }}
37 |                                 <b class="arrow fa fa-angle-down"></b>
38 |                             </a>
39 |                             <b class="arrow"></b>
40 | 
41 |                         {% end %}
42 | 
43 |                         <ul class="submenu">
44 |                             <li>
45 |                                 <a href="{{ leftMenuTmp.url }}">
46 |                                     <i class="menu-icon fa fa-caret-right"></i>
47 |                                     {{ leftMenuTmp.name }}
48 |                                 </a>
49 |                                 <b class="arrow"></b>
50 |                             </li>
51 |                         </ul>
52 | 
53 |                     {% end %}
54 |                         </li>
55 |             </ul>
56 | 
57 |         </li>
58 |     </ul><!-- /.nav-list -->
59 | 
60 |     <div class="sidebar-toggle sidebar-collapse" id="sidebar-collapse">
61 |         <i id="sidebar-toggle-icon" class="ace-icon fa fa-angle-double-left ace-save-state" data-icon1="ace-icon fa fa-angle-double-left" data-icon2="ace-icon fa fa-angle-double-right"></i>
62 |     </div>
63 | </div>
64 | {% end %}


--------------------------------------------------------------------------------
/backend/web/templates/common/meta.html:
--------------------------------------------------------------------------------
 1 | {% block meta %}
 2 | <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
 3 | <!-- bootstrap & fontawesome -->
 4 | <link rel="stylesheet" href="/static/css/bootstrap.min.css" />
 5 | <link rel="stylesheet" href="/static/font-awesome/4.5.0/css/font-awesome.min.css" />
 6 | <!-- ace styles -->
 7 | <link rel="stylesheet" href="/static/css/ace.min.css" class="ace-main-stylesheet" id="main-ace-style" />
 8 | <!-- basic scripts -->
 9 | <script src="/static/js/jquery-2.1.4.min.js"></script>
10 | <script src="/static/js/ace-extra.min.js"></script>
11 | <script src="/static/js/bootstrap.min.js"></script>
12 | 
13 | <!-- page specific plugin scripts -->
14 | <script src="/static/js/jquery.dataTables.min.js"></script>
15 | <script src="/static/js/jquery.dataTables.bootstrap.min.js"></script>
16 | <script src="/static/js/dataTables.buttons.min.js"></script>
17 | <script src="/static/js/dataTables.select.min.js"></script>
18 | <!-- ace scripts -->
19 | <script src="/static/js/moment.min.js"></script>
20 | <script src="/static/js/bootstrap-datepicker.min.js"></script>
21 | <script src="/static/js/bootstrap-datetimepicker.min.js"></script>
22 | <script src="/static/js/bootstrap-datepicker.zh-CN.js"></script>
23 | 
24 | <link rel="stylesheet" href="/static/css/bootstrap-datepicker3.min.css" />
25 | <link rel="stylesheet" href="/static/css/bootstrap-datetimepicker.min.css" />
26 | 
27 | <script src="/static/js/ace-elements.min.js"></script>
28 | <script src="/static/js/ace.min.js"></script>
29 | <script src="/static/js/jquery-ui.custom.min.js"></script>
30 | <script src="/static/js/bootbox.js"></script>
31 | 
32 | <script src="/static/js/buttons.colVis.min.js"></script>
33 | <script src="/static/js/buttons.print.min.js"></script>
34 | <script src="/static/js/buttons.html5.min.js"></script>
35 | 
36 | 
37 | <script type='text/javascript'>
38 | jQuery(function($) {
39 |     //初始化窗口。
40 |     $('.modal.aside').ace_aside();
41 |     $('#aside-inside-modal').addClass('aside').ace_aside({container: ""});
42 |     $(document).one('ajaxloadstart.page', function(e) {
43 |         $('.modal.aside').remove();
44 |         $(window).off('.aside')
45 |     });
46 | })
47 | $.extend({
48 | 	getUrlVars: function(){
49 | 		var vars = [], hash;
50 | 		var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
51 | 		for(var i = 0; i < hashes.length; i++)
52 | 		{
53 | 			hash = hashes[i].split('=');
54 | 			vars.push(hash[0]);
55 | 			vars[hash[0]] = hash[1];
56 | 		}
57 | 		return vars;
58 | 	},
59 | 	getUrlVar: function(name){
60 | 		return $.getUrlVars()[name];
61 | 	}
62 | });
63 | jQuery(function($) {
64 |     $('[data-rel=tooltip]').tooltip();
65 |     $('[data-rel=popover]').popover({html:true});
66 | });
67 | </script>
68 | <style>
69 | 	#dynamic-table_filter {display: none;}
70 | </style>
71 | {% end %}


--------------------------------------------------------------------------------
/backend/web/templates/index.html:
--------------------------------------------------------------------------------
 1 | {% extends "layout/default.html" %}
 2 | 
 3 | 
 4 | {% block main_content %}
 5 | 
 6 | 	<h3 class="header smaller lighter blue">开源Python全栈股票系统,数据抓取、统计分析、报表展示。</h3>
 7 | 
 8 | 	<div class="clearfix">
 9 | 		<div class="pull-left tableTools-container">
10 | 			<h3>基础库版本</h3>
11 | 			<p>1,pandas使用【 {{ pandasVersion }} 】版本, <a href="https://www.pypandas.cn/docs/" target="_blank">中文文档</a> </p>
12 | 			<p>2,numpy使用【 {{ numpyVersion }} 】版本, <a href="https://www.numpy.org.cn/user/" target="_blank">中文文档</a></p>
13 | 			<p>3,akshare使用【 {{ akshareVersion }} 】版本, <a href="https://www.akshare.xyz/" target="_blank">中文文档</a></p>
14 | 			<p>4,bokeh使用【 {{ bokehVersion }} 】版本, <a href="http://docs.bokeh.org/en/latest/" target="_blank">官方文档</a></p>
15 | 			<p>5,stockstats使用【 {{ stockstatsVersion }} 】版本, <a href="https://github.com/jealous/stockstats/" target="_blank">官方文档</a></p>
16 | 		</div>
17 | 	</div>
18 | 
19 | 	<div class="clearfix">
20 | 		<div class="pull-left tableTools-container">
21 | 			<h3>相关资料信息</h3>
22 | 			<p>1,github项目地址。<a href="https://github.com/pythonstock/stock" target="_blank">pythonstock</a></p>
23 | 			<p>2,博客地址。<a href="https://blog.csdn.net/freewebsys/category_9285317.html" target="_blank">博客地址</a></p>
24 | 		</div>
25 | 	</div>
26 | 
27 | 	<div class="clearfix">
28 | 		<div class="pull-left tableTools-container">
29 | 			<h3>2021年9月20日更新,发布2.0版本</h3>
30 | 			<p>1,修复bokeh的版本升级问题,可以显示趋势数据了。</p>
31 | 			<p>2,使用 stock_zh_ah_name 做每日数据。</p>
32 | 			<p>3,AkShare 升级到 1.1.9 版本。</p>
33 | 		</div>
34 | 	</div>
35 | 
36 | 	<div class="clearfix">
37 | 		<div class="pull-left tableTools-container">
38 | 			<h3>2021年8月31日更新job服务</h3>
39 | 			<p>1,过滤包括:600,6006,601,000,001,002,且不包括ST的股票数据。。</p>
40 | 			<p>2,使用 stock_zh_ah_name 做每日数据。</p>
41 | 			<p>3,AkShare 升级到 1.0.80 版本。</p>
42 | 
43 | 		</div>
44 | 	</div>
45 | 
46 | 	<div class="clearfix">
47 | 		<div class="pull-left tableTools-container">
48 | 			<h3>2021年6月3日使用 AkShare 做数据抓取服务</h3>
49 | 			<p>1,使用 stock_zh_a_spot 做实时行情数据。</p>
50 | 			<p>2,使用 stock_zh_a_daily 做历史数据统计。</p>
51 | 			<p>3,升级基础镜像使用python3.7,AkShare 的 0.9.65 版本。</p>
52 | 
53 | 		</div>
54 | 	</div>
55 | 
56 | {% end %}


--------------------------------------------------------------------------------
/backend/web/templates/layout/default.html:
--------------------------------------------------------------------------------
1 | {% extends "../common/meta.html" %}
2 | {% extends "../common/header.html" %}
3 | {% extends "../common/footer.html" %}
4 | {% extends "../common/left_menu.html" %}
5 | 
6 | {% extends "main.html" %}


--------------------------------------------------------------------------------
/backend/web/templates/layout/indicators-main.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html>
 3 | 	<head>
 4 | 		<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
 5 | 		<meta charset="utf-8" />
 6 | 		<title>股票系统</title>
 7 | 		{% block meta %}{% end %}
 8 | 	</head>
 9 | 
10 | 	<body class="no-skin">
11 | 			<div class="main-content">
12 | 				<div class="main-content-inner">
13 | 					<div class="page-content">
14 | 						<div class="row">
15 | 							<div class="col-xs-12">
16 | 			{% block main_content %}{% end %}
17 | 							</div>
18 | 						</div><!-- /.row -->
19 | 					</div><!-- /.page-content -->
20 | 				</div>
21 | 			</div><!-- /.main-content -->
22 | 	</body>
23 | </html>
24 | 


--------------------------------------------------------------------------------
/backend/web/templates/layout/indicators.html:
--------------------------------------------------------------------------------
1 | {% extends "../common/meta.html" %}
2 | 
3 | {% extends "indicators-main.html" %}


--------------------------------------------------------------------------------
/backend/web/templates/layout/main.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html>
 3 | 	<head>
 4 | 		<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
 5 | 		<meta charset="utf-8" />
 6 | 		<title>股票系统 {{ pythonStockVersion }}</title>
 7 | 		{% block meta %}{% end %}
 8 | 	</head>
 9 | 
10 | 	<body class="no-skin">
11 | 
12 | 		{% block header %}{% end %}
13 | 
14 | 		<div class="main-container ace-save-state" id="main-container">
15 | 			<script type="text/javascript">
16 | 				try{ace.settings.loadState('main-container')}catch(e){}
17 | 			</script>
18 | 
19 | 			{% block left_menu %}{% end %}
20 | 
21 | 			<div class="main-content">
22 | 				<div class="main-content-inner">
23 | 					<div class="page-content">
24 | 						<div class="row">
25 | 							<div class="col-xs-12">
26 | 			{% block main_content %}{% end %}
27 | 							</div>
28 | 						</div><!-- /.row -->
29 | 					</div><!-- /.page-content -->
30 | 				</div>
31 | 			</div><!-- /.main-content -->
32 | 
33 | 			<a href="#" id="btn-scroll-up" class="btn-scroll-up btn btn-sm btn-inverse">
34 | 				<i class="ace-icon fa fa-angle-double-up icon-only bigger-110"></i>
35 | 			</a>
36 | 		</div><!-- /.main-container -->
37 | 	</body>
38 | </html>
39 | 


--------------------------------------------------------------------------------
/backend/web/templates/layout/single_default.html:
--------------------------------------------------------------------------------
1 | {% extends "../common/meta.html" %}
2 | {% extends "../common/header.html" %}
3 | {% extends "../common/footer.html" %}
4 | 
5 | {% extends "single_main.html" %}


--------------------------------------------------------------------------------
/backend/web/templates/layout/single_main.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html>
 3 | 	<head>
 4 | 		<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
 5 | 		<meta charset="utf-8" />
 6 | 		{% block meta %}{% end %}
 7 | 	</head>
 8 | 
 9 | 	<body class="no-skin">
10 | 
11 | 		<div class="main-container ace-save-state" id="main-container">
12 | 			<div class="main-content">
13 | 				<div class="main-content-inner">
14 | 					<div class="page-content">
15 | 						<div class="row">
16 | 							<div class="col-xs-12">
17 | 			{% block main_content %}{% end %}
18 | 							</div>
19 | 						</div><!-- /.row -->
20 | 					</div><!-- /.page-content -->
21 | 				</div>
22 | 			</div><!-- /.main-content -->
23 | 
24 | 			<a href="#" id="btn-scroll-up" class="btn-scroll-up btn btn-sm btn-inverse">
25 | 				<i class="ace-icon fa fa-angle-double-up icon-only bigger-110"></i>
26 | 			</a>
27 | 		</div><!-- /.main-container -->
28 | 	</body>
29 | </html>
30 | 


--------------------------------------------------------------------------------
/backend/web/templates/minst_serving.html:
--------------------------------------------------------------------------------
 1 | {% extends "layout/single_default.html" %}
 2 | 
 3 | 
 4 | {% block main_content %}
 5 | 
 6 | 
 7 | 	<h3 class="header smaller lighter blue">手写图片识别演示</h3>
 8 | 
 9 | 		<div class="row clearfix">
10 |                 <div class="col-md-6 column">
11 | 					<canvas id="the_stage" width="280" height="280">your browser don't support canvas!</canvas>
12 | 					<div>
13 | 						<button type="button" class="btn btn-default butt" onclick="clearCanvas()"><strong>删除</strong></button>
14 | 						<button type="button" class="btn btn-default butt" id="recognize" onclick="processImg()"><strong>识别</strong></button>
15 | 					</div>
16 | 				</div>
17 | 				<div class="col-md-6 column">
18 |                     <h3>result:</h3>
19 |                     <h2 id="rec_result"></h2>
20 |                 </div>
21 | 		</div>
22 | 
23 | 	<h3 class="header smaller lighter blue">测试图片识别演示</h3>
24 | 	<div class="row clearfix" style="width:400px;">
25 | 		{% for image in image_array %}
26 | 				  <div class="col-md-2" style="margin-top: 15px;">
27 | 					  <img src="{{ image }}" alt="" width="50" class="minst_img" height="50" style="1px solid #e1e4e5;cursor: pointer;">
28 | 				  </div>
29 | 		{% end %}
30 | 	</div>
31 | 	<div class="col-md-6 column">
32 | 			<h3>结果:</h3>
33 | 			<h2 id="prediction_text">"-1"</h2>
34 |     </div>
35 | <!-- inline scripts related to this page -->
36 | <script src="/static/js/mnist-draw.js"></script>
37 | <style>
38 | 	#the_stage {
39 |     border: 1px solid #999;
40 |     border-radius: 4px;
41 |     box-shadow: -10px 10px 5px #888888;
42 | }
43 | </style>
44 | <script type="text/javascript">
45 | jQuery(function($) {
46 | 	$("#recognize").click(
47 | 		//提交数据
48 | 		function () {
49 | 			$("#rec_result").html("connecting...");
50 | 			var scribbler = document.getElementById ("the_stage");
51 | 			var imageData =  scribbler.toDataURL('image/png');
52 | 			var dataTemp = imageData.substr(22);
53 | 
54 | 			var sendPackage = {"id": "1", "txt": dataTemp};
55 | 			$.post("/minst_serving/prediction2", sendPackage, function(msg){
56 | 				$("#rec_result").html(msg);
57 | 			});
58 | 		}
59 | 	);
60 | 
61 | 	$(".minst_img").click(
62 | 		//提交数据
63 | 		function () {
64 | 			img_url = $(this).attr("src")
65 | 			console.log("here",);
66 | 			//
67 | 			$.ajax({
68 | 			   type: "GET",
69 | 			   url: "/minst_serving/prediction",
70 | 			   data: "img_url="+img_url,
71 | 			   success: function(msg){
72 | 				 console.log( "prediction : " + msg );
73 | 				 $("#prediction_text").html(msg);
74 | 			   }
75 | 			});
76 | 		}
77 | 	);
78 | });
79 | </script>
80 | {% end %}


--------------------------------------------------------------------------------
/backend/web/templates/stock_chart.html:
--------------------------------------------------------------------------------
 1 | {% extends "layout/default.html" %}
 2 | 
 3 | 
 4 | {% block main_content %}
 5 | 
 6 | 	<h3 class="header smaller lighter blue">欢迎使用股票系统。</h3>
 7 | 
 8 | 	<div class="clearfix">
 9 | 		<div class="pull-right tableTools-container"></div>
10 | 		<img src="/stock/chart/image1" />
11 | 	</div>
12 | {% end %}


--------------------------------------------------------------------------------
/backend/web/templates/stock_indicators.html:
--------------------------------------------------------------------------------
 1 | {% extends "layout/indicators.html" %}
 2 | 
 3 | 
 4 | {% block main_content %}
 5 | 
 6 | <!-- 增加 bokeh 样式。-->
 7 | <link rel="stylesheet" href="/static/css/bokeh.min.css" type="text/css"/>
 8 | <link rel="stylesheet" href="/static/css/bokeh-widgets.min.css" type="text/css"/>
 9 | <link rel="stylesheet" href="/static/css/bokeh-tables.min.css" type="text/css"/>
10 | 
11 | <script type="text/javascript" src="/static/js/bokeh.min.js"></script>
12 | <script type="text/javascript" src="/static/js/bokeh-widgets.min.js"></script>
13 | <script type="text/javascript" src="/static/js/bokeh-tables.min.js"></script>
14 | <script type="text/javascript" src="/static/js/bokeh-api.min.js"></script>
15 | <script type="text/javascript" src="/static/js/bokeh-gl.min.js"></script>
16 | 
17 | 
18 | 
19 | <script type="text/javascript">
20 |     Bokeh.set_log_level("info");
21 | </script>
22 | 
23 | {% for index,element in enumerate(comp_list) %}
24 | <h4 class="header smaller lighter blue">{{ element["title"] }}</h4>
25 | <div class="row">{{ element["desc"] }}</div>
26 | <div class="row" id="_col_{{ index+1 }}" data-column="{{ index }}">
27 |     {% raw element["div"] %}
28 |     {% raw element["script"] %}
29 | </div>
30 | {% end %}
31 | 
32 | {% end %}


--------------------------------------------------------------------------------
/backend/web/templates/test.html:
--------------------------------------------------------------------------------
 1 | {% extends "layout/default.html" %}
 2 | 
 3 | 
 4 | {% block main_content %}
 5 | 
 6 | 	<h3 class="header smaller lighter blue">欢迎使用股票系统。</h3>
 7 | 
 8 | 	<div class="clearfix">
 9 | 		<div class="pull-right tableTools-container"></div>
10 | 	</div>
11 | 
12 | 	<div class="col-sm-1">
13 | 			<button onclick="showDFCFWindow('SZ000001');" role="button" class="btn btn-sm btn-primary" data-toggle="modal">
14 | 				<i class="ace-icon fa fa-plus"></i>
15 | 				测试弹窗东方财富窗口
16 | 			</button>
17 | 	</div><!-- /.col -->
18 | 
19 | 	<!--<iframe src="https://emweb.eastmoney.com/PC_HSF10/ShareholderResearch/Index?type=soft&code=SZ000001" width="700px" height="700px"></iframe>-->
20 | 
21 | 	<!-- 定义一个窗口的Div 东方财富股票数据页面 -->
22 | 	<div id="dfcf-window-modal" class="modal fade">
23 | 		<div class="modal-dialog" style="width:1010px;">
24 | 			<div class="modal-content">
25 | 				<div class="modal-header">
26 | 					<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
27 | 					<h3 class="smaller lighter blue no-margin">东方财富分析</h3>
28 | 				</div>
29 | 				<div class="modal-body" id="dfcf-iframe-body" >
30 | 				</div>
31 | 			</div>
32 | 		</div>
33 | 	</div>
34 | 
35 |     <script type="text/javascript">
36 |         //每次动态加载 东方财富窗口
37 |         function showDFCFWindow(code) {
38 | 
39 |         	var iframe = document.createElement('iframe')
40 |         	var baseUrl = 'https://emweb.eastmoney.com/PC_HSF10/ShareholderResearch/Index?type=soft&code='+code;
41 |         	// var baseUrl = 'https://emweb.eastmoney.com/PC_HSF10/OperationsRequired/Index?type=soft&code=';
42 | 			iframe.src = baseUrl;
43 | 			iframe.width = '980px';
44 | 			iframe.height = '700px';
45 | 			iframe.frameborder = '0';
46 | 			$('#iframe-body').empty(); // 先清空数据
47 | 			document.getElementById("dfcf-iframe-body").appendChild(iframe)
48 |             $('#dfcf-window-modal').modal('show');
49 |         }
50 | 	</script>
51 | 
52 | {% end %}


--------------------------------------------------------------------------------
/backend/web/templates/test2.html:
--------------------------------------------------------------------------------
 1 | {% extends "layout/default.html" %}
 2 | 
 3 | 
 4 | {% block main_content %}
 5 | 
 6 | 	<h3 class="header smaller lighter blue">欢迎使用股票系统。</h3>
 7 | 
 8 | 	<div class="clearfix">
 9 | 		<div class="pull-right tableTools-container"></div>
10 | 	</div>
11 | 
12 | 	<div class="col-sm-1">
13 | 			<button onclick="showIndicatorsWindow('000001');" role="button" class="btn btn-sm btn-primary" data-toggle="modal">
14 | 				<i class="ace-icon fa fa-plus"></i>
15 | 				测试弹窗指标分析窗口
16 | 			</button>
17 | 	</div><!-- /.col -->
18 | 
19 | 	<!--<iframe src="https://emweb.eastmoney.com/PC_HSF10/ShareholderResearch/Index?type=soft&code=SZ000001" width="700px" height="700px"></iframe>-->
20 | 
21 | 	<!-- 定义一个窗口的Div 指标分析数据页面 -->
22 | 	<div id="indicators-window-modal" class="modal fade">
23 | 		<div class="modal-dialog" style="width:1050px;">
24 | 			<div class="modal-content">
25 | 				<div class="modal-header">
26 | 					<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
27 | 					<h3 class="smaller lighter blue no-margin">股票数据详细指标分析</h3>
28 | 				</div>
29 | 				<div class="modal-body" id="indicators-window-body" >
30 | 				</div>
31 | 			</div>
32 | 		</div>
33 | 	</div>
34 | 
35 |     <script type="text/javascript">
36 |         //每次动态加载 指标分析窗口
37 |         function showIndicatorsWindow(code) {
38 |         	var baseUrl = '/data/indicators?code='+code; // 没有跨域问题,直接加载
39 | 			$('#indicators-window-body').context = $("#indicators-window-body").load(baseUrl);
40 |             $('#indicators-window-modal').modal('show');
41 |         }
42 | 	</script>
43 | 
44 | {% end %}


--------------------------------------------------------------------------------
/backend/web/test_thread.py:
--------------------------------------------------------------------------------
  1 | #!/usr/local/bin/python3
  2 | # -*- coding: utf-8 -*-
  3 | import time
  4 | from tornado.httpserver import HTTPServer
  5 | from tornado.ioloop import IOLoop
  6 | from tornado.web import Application, asynchronous, RequestHandler
  7 | from multiprocessing.pool import ThreadPool
  8 | from multiprocessing.pool import ApplyResult
  9 | from tornado import gen
 10 | 
 11 | # https://gist.github.com/methane/2185380 参考
 12 | 
 13 | 
 14 | 
 15 | html_content = """
 16 | <!DOCTYPE html>
 17 | <html lang="zh-CN">
 18 | <head>
 19 |       <script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
 20 | </head>
 21 | <body>
 22 |     <h1>任务测试</h1></br>
 23 |     <button id="job">开始</button>
 24 | </body>
 25 | </html>
 26 | <script type="text/javascript">
 27 |     function job_check(timer,tid) {
 28 |         $.ajax({
 29 |            type: "GET",
 30 |            url: "job_check?tid="+tid,
 31 |            success: function(msg){
 32 |                 console.log(msg);
 33 |                 if(msg != ""){
 34 |                     alert( "任务结果: " + msg );
 35 |                     clearInterval(timer);//结束轮询
 36 |                 }
 37 |             }
 38 |         });
 39 |     }
 40 | 	jQuery(function($) {
 41 | 	    
 42 | 	    $("#job").click( function () {
 43 | 	        $.ajax({
 44 |                type: "GET",
 45 |                url: "add_job",
 46 |                success: function(tid){
 47 |                     alert( "开始任务: " + tid );
 48 |                     timer = setInterval(function(){
 49 |                         console.log("run.");
 50 |                         job_check(timer,tid);
 51 |                     },1000);
 52 |                }
 53 |             });
 54 | 	    });
 55 | 	})
 56 | </script>
 57 | """
 58 | 
 59 | 
 60 | class MainPage(RequestHandler):
 61 |     def get(self):
 62 |         self.write(html_content)
 63 | 
 64 | 
 65 | _workers = ThreadPool(10)
 66 | _result = {}
 67 | 
 68 | 
 69 | # 后台任务。
 70 | def blocking_task(n, tid):
 71 |     time.sleep(n)
 72 |     print(tid)
 73 |     _result[tid] = {"finish"}
 74 | 
 75 | 
 76 | class AddJobHandler(RequestHandler):
 77 |     @gen.coroutine
 78 |     def get(self):
 79 |         tid = str(int(time.time() * 10000))
 80 |         _workers.apply_async(blocking_task, (10, tid))  # 传递参数 10 秒。
 81 |         self.write(tid)
 82 |         self.finish()  # 先finish 掉,然后在后台执行。
 83 | 
 84 | 
 85 | class JobCheckHandler(RequestHandler):
 86 |     def get(self):
 87 |         tid = self.get_argument("tid")
 88 |         if tid in _result.keys():
 89 |             out = _result[tid]  # 结果
 90 |             del _result[tid]  # 删除tid的数据。
 91 |             self.write(str(out))
 92 |         else:
 93 |             self.write("")
 94 | 
 95 | 
 96 | # main 启动。
 97 | if __name__ == "__main__":
 98 |     HTTPServer(Application([
 99 |         ("/", MainPage),
100 |         ("/add_job", AddJobHandler),
101 |         ("/job_check", JobCheckHandler)
102 |     ], debug=True)).listen(9090)
103 |     print("start web .")
104 |     IOLoop.instance().start()
105 | 


--------------------------------------------------------------------------------
/backend/web/test_thread_v2.py:
--------------------------------------------------------------------------------
  1 | #!/usr/local/bin/python3
  2 | # -*- coding: utf-8 -*-
  3 | import time
  4 | from tornado.httpserver import HTTPServer
  5 | from tornado.ioloop import IOLoop
  6 | from tornado.web import Application, asynchronous, RequestHandler
  7 | from tornado import gen
  8 | from tornado.concurrent import run_on_executor
  9 | from concurrent.futures import ThreadPoolExecutor
 10 | 
 11 | # `pip install futures` for python2
 12 | 
 13 | # https://gist.github.com/methane/2185380 参考
 14 | 
 15 | html_content = """
 16 | <!DOCTYPE html>
 17 | <html lang="zh-CN">
 18 | <head>
 19 |       <script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
 20 | </head>
 21 | <body>
 22 |     <h1>任务测试</h1></br>
 23 |     <button id="job">开始</button>
 24 | </body>
 25 | </html>
 26 | <script type="text/javascript">
 27 |     function job_check(timer,tid) {
 28 |         $.ajax({
 29 |            type: "GET",
 30 |            url: "job_check?tid="+tid,
 31 |            success: function(msg){
 32 |                 console.log(msg);
 33 |                 if(msg != ""){
 34 |                     alert( "任务结果: " + msg );
 35 |                     clearInterval(timer);//结束轮询
 36 |                 }
 37 |             }
 38 |         });
 39 |     }
 40 | 	jQuery(function($) {
 41 | 	    
 42 | 	    $("#job").click( function () {
 43 | 	        $.ajax({
 44 |                type: "GET",
 45 |                url: "add_job",
 46 |                success: function(tid){
 47 |                     alert( "开始任务: " + tid );
 48 |                     timer = setInterval(function(){
 49 |                         console.log("run.");
 50 |                         job_check(timer,tid);
 51 |                     },1000);
 52 |                }
 53 |             });
 54 | 	    });
 55 | 	})
 56 | </script>
 57 | """
 58 | 
 59 | 
 60 | class MainPage(RequestHandler):
 61 |     def get(self):
 62 |         self.write(html_content)
 63 | 
 64 | 
 65 | MAX_WORKERS = 4
 66 | _result = {}
 67 | 
 68 | 
 69 | class AddJobHandler(RequestHandler):
 70 |     # 必须定义一个executor的属性,然后run_on_executor 注解才管用。
 71 |     executor = ThreadPoolExecutor(max_workers=MAX_WORKERS)
 72 | 
 73 |     @run_on_executor  # 标记成后台程序执行。
 74 |     def background_task(self, tid):
 75 |         time.sleep(10)  # 传递参数 10 秒。
 76 |         _result[tid] = {"finish"}
 77 | 
 78 |     @gen.coroutine
 79 |     def get(self):
 80 |         tid = str(int(time.time() * 10000))
 81 |         self.background_task(tid)
 82 |         self.write(tid)
 83 | 
 84 | 
 85 | class JobCheckHandler(RequestHandler):
 86 |     def get(self):
 87 |         tid = self.get_argument("tid")
 88 |         if tid in _result.keys():
 89 |             out = _result[tid]  # 结果
 90 |             del _result[tid]  # 删除tid的数据。
 91 |             self.write(str(out))
 92 |         else:
 93 |             self.write("")
 94 | 
 95 | 
 96 | # main 启动。
 97 | if __name__ == "__main__":
 98 |     HTTPServer(Application([
 99 |         ("/", MainPage),
100 |         ("/add_job", AddJobHandler),
101 |         ("/job_check", JobCheckHandler)
102 |     ], debug=True)).listen(9999)
103 |     print("start web .")
104 |     IOLoop.instance().start()
105 | 


--------------------------------------------------------------------------------
/backend/web/tornado_bokeh_embed.py:
--------------------------------------------------------------------------------
 1 | from jinja2 import Environment, FileSystemLoader
 2 | 
 3 | from tornado.web import RequestHandler
 4 | 
 5 | from bokeh.application import Application
 6 | from bokeh.application.handlers import FunctionHandler
 7 | from bokeh.embed import server_document
 8 | from bokeh.layouts import column
 9 | from bokeh.models import ColumnDataSource, Slider
10 | from bokeh.plotting import figure
11 | from bokeh.server.server import Server
12 | from bokeh.themes import Theme
13 | 
14 | from bokeh.sampledata.sea_surface_temperature import sea_surface_temperature
15 | 
16 | env = Environment(loader=FileSystemLoader('templates'))
17 | server_url = "http://localhost:9090/"
18 | 
19 | 
20 | class IndexHandler(RequestHandler):
21 |     def get(self):
22 |         print("index ...")
23 |         template = env.get_template('bokeh_embed.html')
24 |         script = server_document(server_url + 'bkapp')
25 |         print(script)
26 |         self.write(template.render(script=script, template="Tornado"))
27 |         # self.write(html_content)
28 | 
29 | 
30 | def modify_doc(doc):
31 |     df = sea_surface_temperature.copy()
32 |     source = ColumnDataSource(data=df)
33 | 
34 |     plot = figure(x_axis_type='datetime', y_range=(0, 25), y_axis_label='Temperature (Celsius)',
35 |                   title="Sea Surface Temperature at 43.18, -70.43")
36 |     plot.line('time', 'temperature', source=source)
37 | 
38 |     def callback(attr, old, new):
39 |         if new == 0:
40 |             data = df
41 |         else:
42 |             data = df.rolling('{0}D'.format(new)).mean()
43 |         source.data = ColumnDataSource(data=data).data
44 | 
45 |     slider = Slider(start=0, end=30, value=0, step=1, title="Smoothing by N Days")
46 |     slider.on_change('value', callback)
47 | 
48 |     doc.add_root(column(slider, plot))
49 | 
50 |     # doc.theme = Theme(filename="theme.yaml")
51 | 
52 | 
53 | bokeh_app = Application(FunctionHandler(modify_doc))
54 | 
55 | # Setting num_procs here means we can't touch the IOLoop before now, we must
56 | # let Server handle that. If you need to explicitly handle IOLoops then you
57 | # will need to use the lower level BaseServer class.
58 | server = Server(
59 |     {'/bkapp': bokeh_app}, num_procs=1, port=9999,
60 |     extra_patterns=[('/', IndexHandler)]
61 | )
62 | server.start()
63 | 
64 | if __name__ == '__main__':
65 |     from bokeh.util.browser import view
66 | 
67 |     print('Opening Tornado app with embedded Bokeh application on ' + server_url)
68 | 
69 |     server.io_loop.add_callback(view, server_url)
70 |     server.io_loop.start()
71 | 


--------------------------------------------------------------------------------
/docker-compose/.gitignore:
--------------------------------------------------------------------------------
 1 | # C extensions
 2 | *.so
 3 | 
 4 | data
 5 | .idea
 6 | *.iml
 7 | .DS_Store
 8 | *.zip
 9 | *.log
10 | *.pyc
11 | doc
12 | /bin
13 | pkg
14 | *.tmp


--------------------------------------------------------------------------------
/docker-compose/README.md:
--------------------------------------------------------------------------------
 1 | ## 镜像仓库选择
 2 | 
 3 | https://github.com/DaoCloud/public-image-mirror
 4 | 
 5 | ```bash
 6 | 
 7 | # python使用镜像
 8 | docker.m.daocloud.io/library/python:3.11-slim-bullseye
 9 | 
10 | # mysql使用:
11 | docker.m.daocloud.io/library/mysql:8
12 | 
13 | ```
14 | 
15 | 
16 | ## 本地部署方法
17 | 
18 | 
19 | ```
20 | git clone git@gitee.com:pythonstock/docker-compose.git
21 | 
22 | cd docker-compose
23 | 
24 | docker-compose up -d
25 | 
26 | ```
27 | 
28 | ## 访问地址
29 | 
30 | http://localhost:9090/
31 | 
32 | 
33 | 
34 | ## 查看日至,进入项目代码
35 | 
36 | ```
37 | # 查看启动日志:
38 | docker logs -f stock
39 | 
40 | # 进入stock容器
41 | docker exec -it stock bash
42 | ```
43 | 
44 | ## 开发模式,映射stock 代码方法
45 | 
46 | 直接使用 dev yml 即可,会映射stock到/data/stock 然后在外部修改代码容器中运行即可。
47 | 
48 | ```bash
49 | docker-compose  -f dev-docker-compose.yml  up -d
50 | ```
51 | 
52 | ```bash
53 | docker-compose  -f docker-compose-v2.0.yml up -d
54 | ```
55 | 
56 | ## 老镜像还保存一个版本
57 | 
58 | ```
59 | pythonstock/pythonstock:v2021
60 | ```


--------------------------------------------------------------------------------
/docker-compose/build_stock.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/sh
 2 | 
 3 | cd ../stock
 4 | 
 5 | NOW_MONTH=$(date "+%Y-%m")
 6 | 
 7 | DOCKER_TAG=pythonstock/pythonstock:latest
 8 | DOCKER_TAG_MONTH=pythonstock/pythonstock:stock-${NOW_MONTH}
 9 | 
10 | echo " docker build -f Dockerfile -t ${DOCKER_TAG} ."
11 | docker build -f Dockerfile -t ${DOCKER_TAG} .
12 | echo " docker build tag xxx ${DOCKER_TAG_MONTH} "
13 | echo "#################################################################"
14 | echo " docker push ${DOCKER_TAG} "
15 | 
16 | 
17 | 


--------------------------------------------------------------------------------
/docker-compose/dev-docker-compose-restart.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/sh
 2 | 
 3 | 
 4 | git pull
 5 | 
 6 | sleep 1
 7 | docker-compose -f dev-docker-compose.yml down
 8 | 
 9 | sleep 1
10 | docker-compose -f dev-docker-compose.yml up -d
11 | 
12 | echo "restart dev-docker-compose"
13 | 
14 | 


--------------------------------------------------------------------------------
/docker-compose/dev-docker-compose.yml:
--------------------------------------------------------------------------------
 1 | networks:
 2 |   stock-dev-network:
 3 |     driver: bridge
 4 | 
 5 | version: "3"
 6 | services:
 7 |     frontend:
 8 |         image: pythonstock/frontend-dev:latest
 9 |         build:
10 |             context: .
11 |             dockerfile: docker/DevFrontendDockerfile
12 |         container_name: frontend
13 |         ports:
14 |             - "8080:8080"
15 |         volumes:
16 |         # 设置开发目录,方便开发调试
17 |             - "../frontend:/usr/src/app"
18 |         environment:
19 |             LANG: zh_CN.UTF-8
20 |             LC_CTYPE: zh_CN.UTF-8
21 |         restart: always
22 |         networks:
23 |             stock-dev-network: {}
24 |         # 入口写死,手动启动应用。
25 |         entrypoint: /usr/src/app/docker-entrypoint.sh
26 |         depends_on:
27 |             - backend
28 |     backend:
29 |         image: pythonstock/backend-dev:latest
30 |         build:
31 |             context: .
32 |             dockerfile: docker/DevBackendDockerfile
33 |         container_name: backend
34 |         ports:
35 |             - "8888:8888"
36 |             - "9090:9090"
37 |         volumes:
38 |         # 设置开发目录,方便开发调试
39 |             - "../backend/jobs/crontab:/var/spool/cron/crontabs/root"
40 |             - "../backend/jobs/cron.minutely:/etc/cron.minutely"
41 |             - "../backend/jobs/cron.hourly:/etc/cron.hourly"
42 |             - "../backend/jobs/cron.daily:/etc/cron.daily"
43 |             - "../backend/jobs/cron.monthly:/etc/cron.monthly"
44 |             - "../backend:/data/stock"
45 |             - "../backend/supervisor:/data/supervisor"
46 |             - "./data/notebooks:/data/notebooks"
47 |             - "./data/logs:/data/logs"
48 |         environment:
49 |             MYSQL_HOST: mysql-stock
50 |             MYSQL_USER: root
51 |             MYSQL_PWD: mysql-stock
52 |             MYSQL_DB: stock_data
53 |             MYSQL_PORT: 3306
54 |             LANG: zh_CN.UTF-8
55 |             LC_CTYPE: zh_CN.UTF-8
56 |             PYTHONIOENCODING: utf-8
57 |         restart: always
58 |         networks:
59 |             stock-dev-network: {}
60 |         # 入口写死,手动启动应用。
61 |         #entrypoint: sleep 999999d
62 |         depends_on:
63 |             - mysql-stock
64 |     mysql-stock:
65 |         # image: hub.atomgit.com/library/mysql:5.7
66 |         # https://hub.atomgit.com/repos/amd64/mysql
67 |         image: docker.m.daocloud.io/library/mysql:8
68 |         container_name: mysql-stock
69 |         # 执行命令:https://juejin.cn/s/mysql%20healthcheck%20docker-compose
70 |         healthcheck:
71 |             test: ["CMD", "mysqladmin" ,"ping", "-uroot", "-pmysql-stock"]
72 |             interval: 10s
73 |             timeout: 5s
74 |             retries: 5
75 |         ports:
76 |             - "3306:3306"
77 |         networks:
78 |             stock-dev-network: {}
79 |         volumes:
80 |             - "./mysql/my.cnf:/etc/mysql/my.cnf"
81 |             - "./data/mysql-stock/data:/var/lib/mysql"
82 |             - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
83 |         environment:
84 |             MYSQL_ROOT_PASSWORD: mysql-stock
85 |             MYSQL_DATABASE: stock_data
86 |             TZ: Asia/Shanghai
87 |         command: [
88 |             '--character-set-server=utf8mb4',
89 |             '--collation-server=utf8mb4_general_ci',
90 |             '--max_connections=3000'
91 |         ]
92 |         restart: always
93 | 


--------------------------------------------------------------------------------
/docker-compose/docker/DevFrontendDockerfile:
--------------------------------------------------------------------------------
 1 | #使用 node:bullseye-slim 做基础镜像减少大小。
 2 | 
 3 | # FROM docker.m.daocloud.io/library/node:bullseye-slim
 4 | # fixbug 最新node 版本编译不过去,
 5 | #  ERROR  Error: Cannot find module './passes/web-incoming'
 6 | 
 7 | FROM docker.m.daocloud.io/library/node:23.5.0-bullseye-slim
 8 | 
 9 | # https://opsx.alibaba.com/mirror
10 | # 使用阿里云镜像地址。修改debian apt 更新地址,pip 地址,设置时区。
11 | # 设置debian的镜像源
12 | RUN echo "deb http://mirrors.aliyun.com/debian/ bullseye main non-free contrib" > /etc/apt/sources.list && \
13 | echo "deb-src http://mirrors.aliyun.com/debian/ bullseye main non-free contrib" >> /etc/apt/sources.list && \
14 | echo "deb http://mirrors.aliyun.com/debian-security/ bullseye-security main" >> /etc/apt/sources.list && \
15 | echo "deb-src http://mirrors.aliyun.com/debian-security/ bullseye-security main" >> /etc/apt/sources.list && \
16 | echo "deb http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib" >> /etc/apt/sources.list && \
17 | echo "deb-src http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib" >> /etc/apt/sources.list && \
18 | echo "deb http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib" >> /etc/apt/sources.list && \
19 | echo "deb-src http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib" >> /etc/apt/sources.list && \
20 | echo  "[global]\n\
21 | trusted-host=mirrors.aliyun.com\n\
22 | index-url=http://mirrors.aliyun.com/pypi/simple" > /etc/pip.conf && \
23 | ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
24 |     echo "Asia/Shanghai" > /etc/timezone
25 | 
26 | #增加语言utf-8
27 | ENV LANG=zh_CN.UTF-8
28 | ENV LC_CTYPE=zh_CN.UTF-8
29 | ENV LC_ALL=C
30 | 
31 | # 增加 TensorFlow 的支持,使用最新的2.0 编写代码。目前还是使用 1.x 吧,还没有学明白。
32 | # RUN pip3 install tensorflow==2.0.0-rc1 keras
33 | # RUN pip3 install tensorflow keras sklearn
34 | 
35 | # 设置 vim 的语言配置
36 | RUN mkdir -p /etc/vim/ && \
37 |     echo "set fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936" >> /etc/vim/vimrc && \
38 |     echo "set termencoding=utf-8" >> /etc/vim/vimrc && \
39 |     echo "set encoding=utf-8" >> /etc/vim/vimrc
40 | 
41 | # 安装 mysqlclient akshare (pandas ,numpy) tornado bokeh
42 | # 安装 nodejs 库
43 | # apt-get autoremove -y 删除没有用的依赖lib。减少镜像大小。1MB 也要节省。
44 | # apt-get --purge remove 软件包名称 , 删除已安装包(不保留配置文件)。
45 | RUN apt-get update && apt-get install -y python3 make g++ && apt-get clean
46 | 


--------------------------------------------------------------------------------
/docker-compose/docker/ProdFrontendDockerfile:
--------------------------------------------------------------------------------
 1 | #使用 node:bullseye-slim 做基础镜像减少大小。
 2 | 
 3 | FROM docker.m.daocloud.io/library/node:bullseye-slim
 4 | 
 5 | # https://opsx.alibaba.com/mirror
 6 | # 使用阿里云镜像地址。修改debian apt 更新地址,pip 地址,设置时区。
 7 | # 设置debian的镜像源
 8 | RUN echo "deb http://mirrors.aliyun.com/debian/ bullseye main non-free contrib" > /etc/apt/sources.list && \
 9 | echo "deb-src http://mirrors.aliyun.com/debian/ bullseye main non-free contrib" >> /etc/apt/sources.list && \
10 | echo "deb http://mirrors.aliyun.com/debian-security/ bullseye-security main" >> /etc/apt/sources.list && \
11 | echo "deb-src http://mirrors.aliyun.com/debian-security/ bullseye-security main" >> /etc/apt/sources.list && \
12 | echo "deb http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib" >> /etc/apt/sources.list && \
13 | echo "deb-src http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib" >> /etc/apt/sources.list && \
14 | echo "deb http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib" >> /etc/apt/sources.list && \
15 | echo "deb-src http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib" >> /etc/apt/sources.list && \
16 | echo  "[global]\n\
17 | trusted-host=mirrors.aliyun.com\n\
18 | index-url=http://mirrors.aliyun.com/pypi/simple" > /etc/pip.conf && \
19 | ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
20 |     echo "Asia/Shanghai" > /etc/timezone
21 | 
22 | #增加语言utf-8
23 | ENV LANG=zh_CN.UTF-8
24 | ENV LC_CTYPE=zh_CN.UTF-8
25 | ENV LC_ALL=C
26 | 
27 | # 增加 TensorFlow 的支持,使用最新的2.0 编写代码。目前还是使用 1.x 吧,还没有学明白。
28 | # RUN pip3 install tensorflow==2.0.0-rc1 keras
29 | # RUN pip3 install tensorflow keras sklearn
30 | 
31 | # 设置 vim 的语言配置
32 | RUN mkdir -p /etc/vim/ && \
33 |     echo "set fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936" >> /etc/vim/vimrc && \
34 |     echo "set termencoding=utf-8" >> /etc/vim/vimrc && \
35 |     echo "set encoding=utf-8" >> /etc/vim/vimrc
36 | 
37 | # 安装 mysqlclient akshare (pandas ,numpy) tornado bokeh
38 | # 安装 nodejs 库
39 | # apt-get autoremove -y 删除没有用的依赖lib。减少镜像大小。1MB 也要节省。
40 | # apt-get --purge remove 软件包名称 , 删除已安装包(不保留配置文件)。
41 | RUN apt-get update && apt-get install -y python3 make g++ && apt-get clean
42 | 


--------------------------------------------------------------------------------
/docker-compose/docker/README.md:
--------------------------------------------------------------------------------
 1 | 
 2 | # python 基础镜像
 3 | 
 4 | 基础镜像升级到 2020年7月的版本
 5 | 
 6 | 保证运行的最少基础环境,基础环境使用python3.6的版本。安装了超级多的lib库。非常的好用。
 7 | 
 8 | mysqlclient
 9 | sqlalchemy
10 | requests
11 | numpy
12 | tushare
13 | tornado torndb
14 | bokeh
15 | stockstats
16 | ta-lib
17 | jupyter
18 | sklearn
19 | 
20 | # 2021年6月版本,使用 akshare 替换掉 tushare 库
21 | 
22 | akshare 地址:
23 | 
24 | https://www.akshare.xyz/zh_CN/latest/introduction.html
25 | 
26 | AKShare 是基于 Python 的财经数据接口库, 目的是实现对股票、期货、期权、基金、外汇、债券、指数、
27 | 加密货币等金融产品的基本面数据、实时和历史行情数据、衍生数据从数据采集、数据清洗到数据落地的一套工具, 
28 | 主要用于学术研究目的。
29 | 
30 | 
31 | # 2021年 9 月版本,镜像裁剪,supervisor 使用python3
32 | 
33 | supervisor 使用 python3 后好像减少了不少大小。
34 | 同时删除掉一直没有用的 ta-lib 和 jupyter 。升级python基础镜像。
35 | 
36 | 


--------------------------------------------------------------------------------
/docker-compose/docker/build.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/sh
 2 | 
 3 | 
 4 | NOW_MONTH=$(date "+%Y-%m")
 5 | 
 6 | DOCKER_TAG=pythonstock/pythonstock:base-${NOW_MONTH}
 7 | 
 8 | echo " docker build -f Dockerfile -t ${DOCKER_TAG} ."
 9 | docker build -f Dockerfile -t ${DOCKER_TAG} .
10 | echo "#################################################################"
11 | echo " docker push ${DOCKER_TAG} "
12 | 
13 | 
14 | 


--------------------------------------------------------------------------------
/docker-compose/mysql/my.cnf:
--------------------------------------------------------------------------------
 1 | # https://blog.csdn.net/aichogn/article/details/117788275
 2 | # mysql 推荐配置
 3 | [client]
 4 | socket=/var/lib/mysql/mysql.sock
 5 | port=3306
 6 | [mysqld]
 7 |  
 8 | basedir=/var/lib/mysql/
 9 | datadir=/var/lib/mysql/data
10 | socket=/var/lib/mysql/mysql.sock
11 | default-storage-engine=INNODB
12 | character_set_server=utf8mb4
13 | collation_server=utf8mb4_general_ci 
14 |  
15 | port=3306
16 | # Disabling symbolic-links is recommended to prevent assorted security risks
17 | symbolic-links=0
18 | 
19 | server_id=1
20 | 
21 |  ## 最大连接数,MySQL服务器允许的最大连接数16384,连接数越多消耗内存越多
22 | max_connections = 1000
23 | ## 日志过期时间,包括二进制日志(过期自动删除)
24 | # expire_logs_days = 15
25 | ## Enable Per Table Data for InnoDB to shrink ibdata1(innoDB表优化)
26 | innodb_file_per_table = 1
27 | #默认128M,用于存储页面缓存数据外,另外正常情况下还有大约8%的开销,主要用在每个缓存页帧的描述、adaptive hash等数据结构,适当的增加这个参数的大小,可以有效的减少 InnoDB 类型的表的磁盘 I/O 
28 | innodb_buffer_pool_size = 2048M
29 | innodb_log_file_size = 512M
30 | #默认是8MB,InnoDB在写事务日志的时候,为了提高性能,也是先将信息写入Innofb Log Buffer中,当满足innodb_flush_log_trx_commit参数所设置的相应条件(或者日志缓冲区写满)之后,才会将日志写到文件 (或者同步到磁盘)中
31 | innodb_log_buffer_size = 8M
32 | innodb_flush_log_at_trx_commit = 2
33 | #表大小写不敏感
34 | lower_case_table_names=1
35 | #跳过密码  安装完后屏蔽该选项
36 | #skip-grant-tables
37 | #关闭 binlog
38 | skip-log-bin


--------------------------------------------------------------------------------
/docker-compose/nginx.conf:
--------------------------------------------------------------------------------
 1 | # 设置nginx启动
 2 | # systemctl enable nginx
 3 | server {
 4 |     listen       8080;
 5 |     server_name  www.pythonstock.com;
 6 |     root         /usr/share/nginx/html;
 7 | 
 8 |     error_page 404 /404.html;
 9 |     error_page 500 502 503 504 /50x.html;
10 | 
11 |     # 开启gzip
12 |     gzip on;
13 |     # 启用gzip压缩的最小文件;小于设置值的文件将不会被压缩
14 |     gzip_min_length 1k;
15 |     # gzip 压缩级别 1-10
16 |     gzip_comp_level 2;
17 |     # 进行压缩的文件类型。
18 |     gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
19 |     # 是否在http header中添加Vary: Accept-Encoding,建议开启
20 |     gzip_vary on;
21 | 
22 |     access_log /var/log/nginx/stock.access.log main;
23 |     error_log /var/log/nginx/stock.error.log warn;
24 | 
25 |     # https://medium.com/aviabird/413-414-request-url-entity-too-large-error-nginx-b6dcece6f5dd
26 |     # 解决GET 参数过长问题。
27 |     client_max_body_size 10M;
28 |     large_client_header_buffers 4 20k;
29 | 
30 | 
31 |     location ^~ /static/ {
32 |         access_log off;
33 |         # 需要把源代码,再下载一遍。
34 |         root /data/pythonstock/stock/web;
35 |         expires 30d; # 设置30天超时
36 |         # 参考 https://www.cnblogs.com/kevingrace/p/10459429.html
37 |         # add_header    Cache-Control  max-age=360000;
38 |     }
39 | 
40 |     location / {
41 |         proxy_next_upstream     http_500 http_502 http_503 http_504 error timeout invalid_header;
42 |         proxy_set_header        Host  $host;
43 |         proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
44 |         proxy_pass              http://127.0.0.1:9090;
45 |         expires                 0;
46 |     }
47 | }


--------------------------------------------------------------------------------
/docker-compose/nginx/nginx.conf:
--------------------------------------------------------------------------------
 1 | server {
 2 |     listen       8080;
 3 |     server_name  localhost;
 4 |     #access_log  /var/log/nginx/host.access.log  main;
 5 | 
 6 |     location / {
 7 |         root   /usr/share/nginx/html;
 8 |         index  index.html index.htm;
 9 |     }
10 | 
11 |     location /api/v1 {
12 |         proxy_pass http://backend:9090;
13 |     }
14 | 
15 | 
16 |     error_page  404              /404.html;
17 |     error_page   500 502 503 504  /50x.html;
18 |     location = /50x.html {
19 |         root   /usr/share/nginx/html;
20 |     }
21 | 
22 | }
23 | 


--------------------------------------------------------------------------------
/frontend/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | src/assets
3 | public
4 | dist
5 | eslint-disable 


--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
 1 | .DS_Store
 2 | node_modules/
 3 | dist/
 4 | npm-debug.log*
 5 | yarn-debug.log*
 6 | yarn-error.log*
 7 | package-lock.json
 8 | tests/**/coverage/
 9 | 
10 | # Editor directories and files
11 | .idea
12 | .vscode
13 | *.suo
14 | *.ntvs*
15 | *.njsproj
16 | *.sln


--------------------------------------------------------------------------------
/frontend/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2017-present PanJiaChen
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 


--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
 1 | ### 说明,项目迁移到了Gitee 啦,最后一次修改,2023-06-02 执行存档
 2 | 
 3 | 项目迁移到这里了:此项目后续更新访问这里:
 4 | 
 5 | https://gitee.com/pythonstock/stock-ui
 6 | 
 7 | github项目后续就Archives存档了,不再更新了!
 8 | 
 9 | csdn的pythonstock专栏地址,相关资料都在这里有说明:
10 | 
11 | https://blog.csdn.net/freewebsys/category_9285317.html
12 | 
13 | 
14 | ## 1,股票系统前端项目
15 | 
16 | elementUI
17 | 
18 | https://element.eleme.cn/#/zh-CN
19 | 
20 | 使用vue-element-admin的模板进行项目开发:
21 | 
22 | https://panjiachen.github.io/vue-element-admin-site/zh/guide/
23 | 
24 | 在线预览地址:
25 | 
26 | https://panjiachen.github.io/vue-element-admin/#/dashboard
27 | 
28 | 模板源自:
29 | https://gitee.com/panjiachen/vue-admin-template
30 | 
31 | 【相关python stock资料分类】:
32 | http://blog.csdn.net/freewebsys/article/category/7076584
33 | 
34 | 
35 | ## 2,动态展示表格
36 | 
37 | http://localhost:9528/#/example/table
38 | 
39 | 
40 | 在.eslintrc.js 文件中配置 eslint-disable 指令来关闭ESlint语法检测。
41 | 
42 | 
43 | 


--------------------------------------------------------------------------------
/frontend/babel.config.js:
--------------------------------------------------------------------------------
 1 | module.exports = {
 2 |   presets: [
 3 |     // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
 4 |     '@vue/cli-plugin-babel/preset'
 5 |   ],
 6 |   'env': {
 7 |     'development': {
 8 |       // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
 9 |       // This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
10 |       // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html
11 |       'plugins': ['dynamic-import-node']
12 |     }
13 |   }
14 | }
15 | 


--------------------------------------------------------------------------------
/frontend/docker-build.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/sh
 2 | 
 3 | sleep 1
 4 | # 只依赖启动。
 5 | cd /usr/src/app
 6 | 
 7 | #!/bin/bash
 8 | 
 9 | # 定义要检查的文件夹路径
10 | modules_path="/usr/src/app/node_modules"
11 | 
12 | # 使用[ ]检查文件夹是否存在
13 | if [ -d "$modules_path" ]; then
14 |     echo "文件夹 $modules_path 存在"
15 | else
16 |     echo "文件夹 $modules_path 不存在,执行 install 安装"
17 |     npm install --registry=https://registry.npmmirror.com
18 | fi
19 | 
20 | npm run build
21 | # 编译完成之后拷贝 html 资源到 影射目录,等待即可。每次编译前都清空内容。
22 | rm -rf /data/html/*
23 | cp -r ./dist/* /data/html/
24 | echo "######### build finish and cp all html  #########"


--------------------------------------------------------------------------------
/frontend/docker-entrypoint.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/sh
 2 | 
 3 | sleep 1
 4 | # 只依赖启动。
 5 | cd /usr/src/app
 6 | 
 7 | #!/bin/bash
 8 | 
 9 | # 定义要检查的文件夹路径
10 | modules_path="/usr/src/app/node_modules"
11 | 
12 | # 使用[ ]检查文件夹是否存在
13 | if [ -d "$modules_path" ]; then
14 |     echo "文件夹 $modules_path 存在"
15 | else
16 |     echo "文件夹 $modules_path 不存在,执行 install 安装"
17 |     npm install --registry=https://registry.npmmirror.com
18 | fi
19 | 
20 | npm run dev
21 | sleep 999999d
22 | 


--------------------------------------------------------------------------------
/frontend/jest.config.js:
--------------------------------------------------------------------------------
 1 | module.exports = {
 2 |   moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
 3 |   transform: {
 4 |     '^.+\\.vue
#39;: 'vue-jest',
 5 |     '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)
#39;:
 6 |       'jest-transform-stub',
 7 |     '^.+\\.jsx?
#39;: 'babel-jest'
 8 |   },
 9 |   moduleNameMapper: {
10 |     '^@/(.*)
#39;: '<rootDir>/src/$1'
11 |   },
12 |   snapshotSerializers: ['jest-serializer-vue'],
13 |   testMatch: [
14 |     '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
15 |   ],
16 |   collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
17 |   coverageDirectory: '<rootDir>/tests/unit/coverage',
18 |   // 'collectCoverage': true,
19 |   'coverageReporters': [
20 |     'lcov',
21 |     'text-summary'
22 |   ],
23 |   testURL: 'http://localhost/'
24 | }
25 | 


--------------------------------------------------------------------------------
/frontend/jsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "baseUrl": "./",
 4 |     "paths": {
 5 |         "@/*": ["src/*"]
 6 |     }
 7 |   },
 8 |   "exclude": ["node_modules", "dist"]
 9 | }
10 | 


--------------------------------------------------------------------------------
/frontend/mock/index.js:
--------------------------------------------------------------------------------
 1 | const Mock = require('mockjs')
 2 | const { param2Obj } = require('./utils')
 3 | 
 4 | const user = require('./user')
 5 | const table = require('./table')
 6 | 
 7 | const mocks = [
 8 |   ...user,
 9 |   ...table
10 | ]
11 | 
12 | // for front mock
13 | // please use it cautiously, it will redefine XMLHttpRequest,
14 | // which will cause many of your third-party libraries to be invalidated(like progress event).
15 | function mockXHR() {
16 |   // mock patch
17 |   // https://github.com/nuysoft/Mock/issues/300
18 |   Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
19 |   Mock.XHR.prototype.send = function() {
20 |     if (this.custom.xhr) {
21 |       this.custom.xhr.withCredentials = this.withCredentials || false
22 | 
23 |       if (this.responseType) {
24 |         this.custom.xhr.responseType = this.responseType
25 |       }
26 |     }
27 |     this.proxy_send(...arguments)
28 |   }
29 | 
30 |   function XHR2ExpressReqWrap(respond) {
31 |     return function(options) {
32 |       let result = null
33 |       if (respond instanceof Function) {
34 |         const { body, type, url } = options
35 |         // https://expressjs.com/en/4x/api.html#req
36 |         result = respond({
37 |           method: type,
38 |           body: JSON.parse(body),
39 |           query: param2Obj(url)
40 |         })
41 |       } else {
42 |         result = respond
43 |       }
44 |       return Mock.mock(result)
45 |     }
46 |   }
47 | 
48 |   for (const i of mocks) {
49 |     Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
50 |   }
51 | }
52 | 
53 | module.exports = {
54 |   mocks,
55 |   mockXHR
56 | }
57 | 
58 | 


--------------------------------------------------------------------------------
/frontend/mock/mock-server.js:
--------------------------------------------------------------------------------
 1 | const chokidar = require('chokidar')
 2 | const bodyParser = require('body-parser')
 3 | const chalk = require('chalk')
 4 | const path = require('path')
 5 | const Mock = require('mockjs')
 6 | 
 7 | const mockDir = path.join(process.cwd(), 'mock')
 8 | 
 9 | function registerRoutes(app) {
10 |   let mockLastIndex
11 |   const { mocks } = require('./index.js')
12 |   const mocksForServer = mocks.map(route => {
13 |     return responseFake(route.url, route.type, route.response)
14 |   })
15 |   for (const mock of mocksForServer) {
16 |     app[mock.type](mock.url, mock.response)
17 |     mockLastIndex = app._router.stack.length
18 |   }
19 |   const mockRoutesLength = Object.keys(mocksForServer).length
20 |   return {
21 |     mockRoutesLength: mockRoutesLength,
22 |     mockStartIndex: mockLastIndex - mockRoutesLength
23 |   }
24 | }
25 | 
26 | function unregisterRoutes() {
27 |   Object.keys(require.cache).forEach(i => {
28 |     if (i.includes(mockDir)) {
29 |       delete require.cache[require.resolve(i)]
30 |     }
31 |   })
32 | }
33 | 
34 | // for mock server
35 | const responseFake = (url, type, respond) => {
36 |   return {
37 |     url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`),
38 |     type: type || 'get',
39 |     response(req, res) {
40 |       console.log('request invoke:' + req.path)
41 |       res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
42 |     }
43 |   }
44 | }
45 | 
46 | module.exports = app => {
47 |   // parse app.body
48 |   // https://expressjs.com/en/4x/api.html#req.body
49 |   app.use(bodyParser.json())
50 |   app.use(bodyParser.urlencoded({
51 |     extended: true
52 |   }))
53 | 
54 |   const mockRoutes = registerRoutes(app)
55 |   var mockRoutesLength = mockRoutes.mockRoutesLength
56 |   var mockStartIndex = mockRoutes.mockStartIndex
57 | 
58 |   // watch files, hot reload mock server
59 |   chokidar.watch(mockDir, {
60 |     ignored: /mock-server/,
61 |     ignoreInitial: true
62 |   }).on('all', (event, path) => {
63 |     if (event === 'change' || event === 'add') {
64 |       try {
65 |         // remove mock routes stack
66 |         app._router.stack.splice(mockStartIndex, mockRoutesLength)
67 | 
68 |         // clear routes cache
69 |         unregisterRoutes()
70 | 
71 |         const mockRoutes = registerRoutes(app)
72 |         mockRoutesLength = mockRoutes.mockRoutesLength
73 |         mockStartIndex = mockRoutes.mockStartIndex
74 | 
75 |         console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed  ${path}`))
76 |       } catch (error) {
77 |         console.log(chalk.redBright(error))
78 |       }
79 |     }
80 |   })
81 | }
82 | 


--------------------------------------------------------------------------------
/frontend/mock/table.js:
--------------------------------------------------------------------------------
 1 | const Mock = require('mockjs')
 2 | 
 3 | const data = Mock.mock({
 4 |   'items|30': [{
 5 |     id: '@id',
 6 |     title: '@sentence(10, 20)',
 7 |     'status|1': ['published', 'draft', 'deleted'],
 8 |     author: 'name',
 9 |     display_time: '@datetime',
10 |     pageviews: '@integer(300, 5000)'
11 |   }]
12 | })
13 | 
14 | module.exports = [
15 |   {
16 |     url: '/vue-admin-template/table/list',
17 |     type: 'get',
18 |     response: config => {
19 |       const items = data.items
20 |       return {
21 |         code: 20000,
22 |         data: {
23 |           total: items.length,
24 |           items: items
25 |         }
26 |       }
27 |     }
28 |   }
29 | ]
30 | 


--------------------------------------------------------------------------------
/frontend/mock/user.js:
--------------------------------------------------------------------------------
 1 | 
 2 | const tokens = {
 3 |   admin: {
 4 |     token: 'admin-token'
 5 |   },
 6 |   editor: {
 7 |     token: 'editor-token'
 8 |   }
 9 | }
10 | 
11 | const users = {
12 |   'admin-token': {
13 |     roles: ['admin'],
14 |     introduction: 'I am a super administrator',
15 |     avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
16 |     name: 'Super Admin'
17 |   },
18 |   'editor-token': {
19 |     roles: ['editor'],
20 |     introduction: 'I am an editor',
21 |     avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
22 |     name: 'Normal Editor'
23 |   }
24 | }
25 | 
26 | module.exports = [
27 |   // user login
28 |   {
29 |     url: '/vue-admin-template/user/login',
30 |     type: 'post',
31 |     response: config => {
32 |       const { username } = config.body
33 |       const token = tokens[username]
34 | 
35 |       // mock error
36 |       if (!token) {
37 |         return {
38 |           code: 60204,
39 |           message: 'Account and password are incorrect.'
40 |         }
41 |       }
42 | 
43 |       return {
44 |         code: 20000,
45 |         data: token
46 |       }
47 |     }
48 |   },
49 | 
50 |   // get user info
51 |   {
52 |     url: '/vue-admin-template/user/info\.*',
53 |     type: 'get',
54 |     response: config => {
55 |       const { token } = config.query
56 |       const info = users[token]
57 | 
58 |       // mock error
59 |       if (!info) {
60 |         return {
61 |           code: 50008,
62 |           message: 'Login failed, unable to get user details.'
63 |         }
64 |       }
65 | 
66 |       return {
67 |         code: 20000,
68 |         data: info
69 |       }
70 |     }
71 |   },
72 | 
73 |   // user logout
74 |   {
75 |     url: '/vue-admin-template/user/logout',
76 |     type: 'post',
77 |     response: _ => {
78 |       return {
79 |         code: 20000,
80 |         data: 'success'
81 |       }
82 |     }
83 |   }
84 | ]
85 | 


--------------------------------------------------------------------------------
/frontend/mock/utils.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * @param {string} url
 3 |  * @returns {Object}
 4 |  */
 5 | function param2Obj(url) {
 6 |   const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
 7 |   if (!search) {
 8 |     return {}
 9 |   }
10 |   const obj = {}
11 |   const searchArr = search.split('&')
12 |   searchArr.forEach(v => {
13 |     const index = v.indexOf('=')
14 |     if (index !== -1) {
15 |       const name = v.substring(0, index)
16 |       const val = v.substring(index + 1, v.length)
17 |       obj[name] = val
18 |     }
19 |   })
20 |   return obj
21 | }
22 | 
23 | module.exports = {
24 |   param2Obj
25 | }
26 | 


--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "vue-admin-template",
 3 |   "version": "4.4.0",
 4 |   "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
 5 |   "author": "Pan <panfree23@gmail.com>",
 6 |   "scripts": {
 7 |     "dev": "./node_modules/.bin/vue-cli-service serve ",
 8 |     "build": "./node_modules/.bin/vue-cli-service build",
 9 |     "build:stage": "./node_modules/.bin/vue-cli-service build --mode staging",
10 |     "preview": "node build/index.js --preview",
11 |     "svgo": "./node_modules/.bin/svgo -f src/icons/svg --config=src/icons/svgo.yml",
12 |     "lint": "./node_modules/.bin/eslint --ext .js,.vue src",
13 |     "test:unit": "./node_modules/.bin/jest --clearCache && vue-cli-service test:unit",
14 |     "test:ci": "npm run lint && npm run test:unit"
15 |   },
16 |   "dependencies": {
17 |     "axios": "0.28.0",
18 |     "core-js": "^3.26.1",
19 |     "element-ui": "2.15.14",
20 |     "file-saver": "^2.0.5",
21 |     "js-cookie": "2.2.0",
22 |     "normalize.css": "7.0.0",
23 |     "nprogress": "0.2.0",
24 |     "path-to-regexp": "2.4.0",
25 |     "vue": "2.6.10",
26 |     "vue-router": "3.0.6",
27 |     "vuex": "3.1.0",
28 |     "xlsx": "^0.18.5"
29 |   },
30 |   "devDependencies": {
31 |     "@vue/cli-plugin-babel": "4.4.4",
32 |     "@vue/cli-plugin-eslint": "4.4.4",
33 |     "@vue/cli-plugin-unit-jest": "4.4.4",
34 |     "@vue/cli-service": "4.4.4",
35 |     "@vue/test-utils": "1.0.0-beta.29",
36 |     "autoprefixer": "9.5.1",
37 |     "babel-eslint": "10.1.0",
38 |     "babel-jest": "23.6.0",
39 |     "babel-plugin-dynamic-import-node": "2.3.3",
40 |     "chalk": "2.4.2",
41 |     "connect": "3.6.6",
42 |     "eslint": "6.7.2",
43 |     "eslint-plugin-vue": "6.2.2",
44 |     "html-webpack-plugin": "3.2.0",
45 |     "mockjs": "1.0.1-beta3",
46 |     "runjs": "4.3.2",
47 |     "sass": "1.26.8",
48 |     "sass-loader": "8.0.2",
49 |     "script-ext-html-webpack-plugin": "2.1.3",
50 |     "serve-static": "1.16.0",
51 |     "svg-sprite-loader": "4.1.3",
52 |     "svgo": "1.2.2",
53 |     "vue-template-compiler": "2.6.10"
54 |   },
55 |   "browserslist": [
56 |     "> 1%",
57 |     "last 2 versions"
58 |   ],
59 |   "engines": {
60 |     "node": ">=8.9",
61 |     "npm": ">= 3.0.0"
62 |   },
63 |   "license": "MIT"
64 | }
65 | 


--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 | 
3 | module.exports = {
4 |   'plugins': {
5 |     // to edit target browsers: use "browserslist" field in package.json
6 |     'autoprefixer': {}
7 |   }
8 | }
9 | 


--------------------------------------------------------------------------------
/frontend/public/40x.html:
--------------------------------------------------------------------------------
1 | 
2 | <html>
3 | <head><title>404 Not Found</title></head>
4 | <body>
5 | <center><h1>404 Not Found</h1></center>
6 | <hr><center>nginx/1.26.3</center>
7 | </body>
8 | </html>


--------------------------------------------------------------------------------
/frontend/public/50x.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html>
 3 | <head>
 4 | <title>Error</title>
 5 | <style>
 6 | html { color-scheme: light dark; }
 7 | body { width: 35em; margin: 0 auto;
 8 | font-family: Tahoma, Verdana, Arial, sans-serif; }
 9 | </style>
10 | </head>
11 | <body>
12 | <h1>An error occurred.</h1>
13 | <p>Sorry, the page you are looking for is currently unavailable.<br/>
14 | Please try again later.</p>
15 | <p>If you are the system administrator of this resource then you should check
16 | the error log for details.</p>
17 | <p><em>Faithfully yours, nginx.</em></p>
18 | </body>
19 | </html>
20 | 


--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/frontend/public/favicon.ico


--------------------------------------------------------------------------------
/frontend/public/freewebsys-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/frontend/public/freewebsys-logo.jpg


--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html>
 3 |   <head>
 4 |     <meta charset="utf-8">
 5 |     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
 6 |     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
 7 |     <link rel="icon" href="<%= BASE_URL %>favicon.ico">
 8 |     <title><%= webpackConfig.name %></title>
 9 |   </head>
10 |   <body>
11 |     <noscript>
12 |       <strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
13 |     </noscript>
14 |     <div id="app"></div>
15 |     <!-- built files will be auto injected -->
16 |   </body>
17 | </html>
18 | 


--------------------------------------------------------------------------------
/frontend/public/stock-001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/frontend/public/stock-001.png


--------------------------------------------------------------------------------
/frontend/public/stock-002.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/frontend/public/stock-002.png


--------------------------------------------------------------------------------
/frontend/public/stock-003.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/frontend/public/stock-003.png


--------------------------------------------------------------------------------
/frontend/src/App.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div id="app">
 3 |     <router-view />
 4 |   </div>
 5 | </template>
 6 | 
 7 | <script>
 8 | export default {
 9 |   name: 'App'
10 | }
11 | </script>
12 | 


--------------------------------------------------------------------------------
/frontend/src/api/article.js:
--------------------------------------------------------------------------------
 1 | import request from '@/utils/request'
 2 | 
 3 | export function fetchList(query) {
 4 |   return request({
 5 |     url: '/api/v1/api_data',
 6 |     method: 'get',
 7 |     params: query
 8 |   })
 9 | }
10 | 
11 | export function fetchArticle(id) {
12 |   return request({
13 |     url: '/vue-element-admin/article/detail',
14 |     method: 'get',
15 |     params: { id }
16 |   })
17 | }
18 | 
19 | export function fetchPv(pv) {
20 |   return request({
21 |     url: '/vue-element-admin/article/pv',
22 |     method: 'get',
23 |     params: { pv }
24 |   })
25 | }
26 | 
27 | export function createArticle(data) {
28 |   return request({
29 |     url: '/vue-element-admin/article/create',
30 |     method: 'post',
31 |     data
32 |   })
33 | }
34 | 
35 | export function updateArticle(data) {
36 |   return request({
37 |     url: '/vue-element-admin/article/update',
38 |     method: 'post',
39 |     data
40 |   })
41 | }
42 | 


--------------------------------------------------------------------------------
/frontend/src/api/menu.js:
--------------------------------------------------------------------------------
 1 | import request from '@/utils/request'
 2 | 
 3 | // 同步获得菜单相关数据。
 4 | 
 5 | export  function fetchMenuList(query) {
 6 |   return request({
 7 |     url: '/api/v1/menu_list',
 8 |     method: 'get',
 9 |     params: query
10 |   })
11 | }
12 | 
13 | 


--------------------------------------------------------------------------------
/frontend/src/api/package.js:
--------------------------------------------------------------------------------
 1 | import request from '@/utils/request'
 2 | 
 3 | export function fetchPackageVersion(query) {
 4 |   return request({
 5 |     url: '/api/v1/package_verison',
 6 |     method: 'get',
 7 |     params: query
 8 |   })
 9 | }
10 | 
11 | 


--------------------------------------------------------------------------------
/frontend/src/api/table.js:
--------------------------------------------------------------------------------
 1 | import request from '@/utils/request'
 2 | 
 3 | export function getList(params) {
 4 |   return request({
 5 |     url: '/vue-admin-template/table/list',
 6 |     method: 'get',
 7 |     params
 8 |   })
 9 | }
10 | 


--------------------------------------------------------------------------------
/frontend/src/api/user.js:
--------------------------------------------------------------------------------
 1 | import request from '@/utils/request'
 2 | 
 3 | export function login(data) {
 4 |   return request({
 5 |     url: '/vue-admin-template/user/login',
 6 |     method: 'post',
 7 |     data
 8 |   })
 9 | }
10 | 
11 | export function getInfo(token) {
12 |   return request({
13 |     url: '/vue-admin-template/user/info',
14 |     method: 'get',
15 |     params: { token }
16 |   })
17 | }
18 | 
19 | export function logout() {
20 |   return request({
21 |     url: '/vue-admin-template/user/logout',
22 |     method: 'post'
23 |   })
24 | }
25 | 


--------------------------------------------------------------------------------
/frontend/src/assets/404_images/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/frontend/src/assets/404_images/404.png


--------------------------------------------------------------------------------
/frontend/src/assets/404_images/404_cloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/frontend/src/assets/404_images/404_cloud.png


--------------------------------------------------------------------------------
/frontend/src/components/Breadcrumb/index.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <el-breadcrumb class="app-breadcrumb" separator="/">
 3 |     <transition-group name="breadcrumb">
 4 |       <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
 5 |         <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
 6 |         <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
 7 |       </el-breadcrumb-item>
 8 |     </transition-group>
 9 |   </el-breadcrumb>
10 | </template>
11 | 
12 | <script>
13 | import pathToRegexp from 'path-to-regexp'
14 | 
15 | export default {
16 |   data() {
17 |     return {
18 |       levelList: null
19 |     }
20 |   },
21 |   watch: {
22 |     $route() {
23 |       this.getBreadcrumb()
24 |     }
25 |   },
26 |   created() {
27 |     this.getBreadcrumb()
28 |   },
29 |   methods: {
30 |     getBreadcrumb() {
31 |       // only show routes with meta.title
32 |       let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
33 |       const first = matched[0]
34 | 
35 |       if (!this.isDashboard(first)) {
36 |         matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
37 |       }
38 | 
39 |       this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
40 |     },
41 |     isDashboard(route) {
42 |       const name = route && route.name
43 |       if (!name) {
44 |         return false
45 |       }
46 |       return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
47 |     },
48 |     pathCompile(path) {
49 |       // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
50 |       const { params } = this.$route
51 |       var toPath = pathToRegexp.compile(path)
52 |       return toPath(params)
53 |     },
54 |     handleLink(item) {
55 |       const { redirect, path } = item
56 |       if (redirect) {
57 |         this.$router.push(redirect)
58 |         return
59 |       }
60 |       this.$router.push(this.pathCompile(path))
61 |     }
62 |   }
63 | }
64 | </script>
65 | 
66 | <style lang="scss" scoped>
67 | .app-breadcrumb.el-breadcrumb {
68 |   display: inline-block;
69 |   font-size: 14px;
70 |   line-height: 50px;
71 |   margin-left: 8px;
72 | 
73 |   .no-redirect {
74 |     color: #97a8be;
75 |     cursor: text;
76 |   }
77 | }
78 | </style>
79 | 


--------------------------------------------------------------------------------
/frontend/src/components/Hamburger/index.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div style="padding: 0 15px;" @click="toggleClick">
 3 |     <svg
 4 |       :class="{'is-active':isActive}"
 5 |       class="hamburger"
 6 |       viewBox="0 0 1024 1024"
 7 |       xmlns="http://www.w3.org/2000/svg"
 8 |       width="64"
 9 |       height="64"
10 |     >
11 |       <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
12 |     </svg>
13 |   </div>
14 | </template>
15 | 
16 | <script>
17 | export default {
18 |   name: 'Hamburger',
19 |   props: {
20 |     isActive: {
21 |       type: Boolean,
22 |       default: false
23 |     }
24 |   },
25 |   methods: {
26 |     toggleClick() {
27 |       this.$emit('toggleClick')
28 |     }
29 |   }
30 | }
31 | </script>
32 | 
33 | <style scoped>
34 | .hamburger {
35 |   display: inline-block;
36 |   vertical-align: middle;
37 |   width: 20px;
38 |   height: 20px;
39 | }
40 | 
41 | .hamburger.is-active {
42 |   transform: rotate(180deg);
43 | }
44 | </style>
45 | 


--------------------------------------------------------------------------------
/frontend/src/components/Pagination/index.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <div :class="{'hidden':hidden}" class="pagination-container">
  3 |     <el-pagination
  4 |       :background="background"
  5 |       :current-page.sync="currentPage"
  6 |       :page-size.sync="pageSize"
  7 |       :layout="layout"
  8 |       :page-sizes="pageSizes"
  9 |       :total="total"
 10 |       v-bind="$attrs"
 11 |       @size-change="handleSizeChange"
 12 |       @current-change="handleCurrentChange"
 13 |     />
 14 |   </div>
 15 | </template>
 16 | 
 17 | <script>
 18 | import { scrollTo } from '@/utils/scroll-to'
 19 | 
 20 | export default {
 21 |   name: 'Pagination',
 22 |   props: {
 23 |     total: {
 24 |       required: true,
 25 |       type: Number
 26 |     },
 27 |     page: {
 28 |       type: Number,
 29 |       default: 1
 30 |     },
 31 |     limit: {
 32 |       type: Number,
 33 |       default: 20
 34 |     },
 35 |     pageSizes: {
 36 |       type: Array,
 37 |       default() {
 38 |         return [10, 20, 30, 50]
 39 |       }
 40 |     },
 41 |     layout: {
 42 |       type: String,
 43 |       default: 'total, sizes, prev, pager, next, jumper'
 44 |     },
 45 |     background: {
 46 |       type: Boolean,
 47 |       default: true
 48 |     },
 49 |     autoScroll: {
 50 |       type: Boolean,
 51 |       default: true
 52 |     },
 53 |     hidden: {
 54 |       type: Boolean,
 55 |       default: false
 56 |     }
 57 |   },
 58 |   computed: {
 59 |     currentPage: {
 60 |       get() {
 61 |         return this.page
 62 |       },
 63 |       set(val) {
 64 |         this.$emit('update:page', val)
 65 |       }
 66 |     },
 67 |     pageSize: {
 68 |       get() {
 69 |         return this.limit
 70 |       },
 71 |       set(val) {
 72 |         this.$emit('update:limit', val)
 73 |       }
 74 |     }
 75 |   },
 76 |   methods: {
 77 |     handleSizeChange(val) {
 78 |       this.$emit('pagination', { page: this.currentPage, limit: val })
 79 |       if (this.autoScroll) {
 80 |         scrollTo(0, 800)
 81 |       }
 82 |     },
 83 |     handleCurrentChange(val) {
 84 |       this.$emit('pagination', { page: val, limit: this.pageSize })
 85 |       if (this.autoScroll) {
 86 |         scrollTo(0, 800)
 87 |       }
 88 |     }
 89 |   }
 90 | }
 91 | </script>
 92 | 
 93 | <style scoped>
 94 | .pagination-container {
 95 |   background: #fff;
 96 |   padding: 32px 16px;
 97 | }
 98 | .pagination-container.hidden {
 99 |   display: none;
100 | }
101 | </style>
102 | 


--------------------------------------------------------------------------------
/frontend/src/components/SvgIcon/index.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
 3 |   <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
 4 |     <use :xlink:href="iconName" />
 5 |   </svg>
 6 | </template>
 7 | 
 8 | <script>
 9 | // doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
10 | import { isExternal } from '@/utils/validate'
11 | 
12 | export default {
13 |   name: 'SvgIcon',
14 |   props: {
15 |     iconClass: {
16 |       type: String,
17 |       required: true
18 |     },
19 |     className: {
20 |       type: String,
21 |       default: ''
22 |     }
23 |   },
24 |   computed: {
25 |     isExternal() {
26 |       return isExternal(this.iconClass)
27 |     },
28 |     iconName() {
29 |       return `#icon-${this.iconClass}`
30 |     },
31 |     svgClass() {
32 |       if (this.className) {
33 |         return 'svg-icon ' + this.className
34 |       } else {
35 |         return 'svg-icon'
36 |       }
37 |     },
38 |     styleExternalIcon() {
39 |       return {
40 |         mask: `url(${this.iconClass}) no-repeat 50% 50%`,
41 |         '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
42 |       }
43 |     }
44 |   }
45 | }
46 | </script>
47 | 
48 | <style scoped>
49 | .svg-icon {
50 |   width: 1em;
51 |   height: 1em;
52 |   vertical-align: -0.15em;
53 |   fill: currentColor;
54 |   overflow: hidden;
55 | }
56 | 
57 | .svg-external-icon {
58 |   background-color: currentColor;
59 |   mask-size: cover!important;
60 |   display: inline-block;
61 | }
62 | </style>
63 | 


--------------------------------------------------------------------------------
/frontend/src/directive/el-table/adaptive.js:
--------------------------------------------------------------------------------
 1 | import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event'
 2 | 
 3 | /**
 4 |  * How to use
 5 |  * <el-table height="100px" v-el-height-adaptive-table="{bottomOffset: 30}">...</el-table>
 6 |  * el-table height is must be set
 7 |  * bottomOffset: 30(default)   // The height of the table from the bottom of the page.
 8 |  */
 9 | 
10 | const doResize = (el, binding, vnode) => {
11 |   const { componentInstance: $table } = vnode
12 | 
13 |   const { value } = binding
14 | 
15 |   if (!$table.height) {
16 |     throw new Error(`el-$table must set the height. Such as height='100px'`)
17 |   }
18 |   const bottomOffset = (value && value.bottomOffset) || 30
19 | 
20 |   if (!$table) return
21 | 
22 |   const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset
23 |   $table.layout.setHeight(height)
24 |   $table.doLayout()
25 | }
26 | 
27 | export default {
28 |   bind(el, binding, vnode) {
29 |     el.resizeListener = () => {
30 |       doResize(el, binding, vnode)
31 |     }
32 |     // parameter 1 is must be "Element" type
33 |     addResizeListener(window.document.body, el.resizeListener)
34 |   },
35 |   inserted(el, binding, vnode) {
36 |     doResize(el, binding, vnode)
37 |   },
38 |   unbind(el) {
39 |     removeResizeListener(window.document.body, el.resizeListener)
40 |   }
41 | }
42 | 


--------------------------------------------------------------------------------
/frontend/src/directive/el-table/index.js:
--------------------------------------------------------------------------------
 1 | import adaptive from './adaptive'
 2 | 
 3 | const install = function(Vue) {
 4 |   Vue.directive('el-height-adaptive-table', adaptive)
 5 | }
 6 | 
 7 | if (window.Vue) {
 8 |   window['el-height-adaptive-table'] = adaptive
 9 |   Vue.use(install); // eslint-disable-line
10 | }
11 | 
12 | adaptive.install = install
13 | export default adaptive
14 | 


--------------------------------------------------------------------------------
/frontend/src/directive/waves/index.js:
--------------------------------------------------------------------------------
 1 | import waves from './waves'
 2 | 
 3 | const install = function(Vue) {
 4 |   Vue.directive('waves', waves)
 5 | }
 6 | 
 7 | if (window.Vue) {
 8 |   window.waves = waves
 9 |   Vue.use(install); // eslint-disable-line
10 | }
11 | 
12 | waves.install = install
13 | export default waves
14 | 


--------------------------------------------------------------------------------
/frontend/src/directive/waves/waves.css:
--------------------------------------------------------------------------------
 1 | .waves-ripple {
 2 |     position: absolute;
 3 |     border-radius: 100%;
 4 |     background-color: rgba(0, 0, 0, 0.15);
 5 |     background-clip: padding-box;
 6 |     pointer-events: none;
 7 |     -webkit-user-select: none;
 8 |     -moz-user-select: none;
 9 |     -ms-user-select: none;
10 |     user-select: none;
11 |     -webkit-transform: scale(0);
12 |     -ms-transform: scale(0);
13 |     transform: scale(0);
14 |     opacity: 1;
15 | }
16 | 
17 | .waves-ripple.z-active {
18 |     opacity: 0;
19 |     -webkit-transform: scale(2);
20 |     -ms-transform: scale(2);
21 |     transform: scale(2);
22 |     -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
23 |     transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
24 |     transition: opacity 1.2s ease-out, transform 0.6s ease-out;
25 |     transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
26 | }


--------------------------------------------------------------------------------
/frontend/src/directive/waves/waves.js:
--------------------------------------------------------------------------------
 1 | import './waves.css'
 2 | 
 3 | const context = '@@wavesContext'
 4 | 
 5 | function handleClick(el, binding) {
 6 |   function handle(e) {
 7 |     const customOpts = Object.assign({}, binding.value)
 8 |     const opts = Object.assign({
 9 |       ele: el, // 波纹作用元素
10 |       type: 'hit', // hit 点击位置扩散 center中心点扩展
11 |       color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
12 |     },
13 |     customOpts
14 |     )
15 |     const target = opts.ele
16 |     if (target) {
17 |       target.style.position = 'relative'
18 |       target.style.overflow = 'hidden'
19 |       const rect = target.getBoundingClientRect()
20 |       let ripple = target.querySelector('.waves-ripple')
21 |       if (!ripple) {
22 |         ripple = document.createElement('span')
23 |         ripple.className = 'waves-ripple'
24 |         ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
25 |         target.appendChild(ripple)
26 |       } else {
27 |         ripple.className = 'waves-ripple'
28 |       }
29 |       switch (opts.type) {
30 |         case 'center':
31 |           ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px'
32 |           ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px'
33 |           break
34 |         default:
35 |           ripple.style.top =
36 |             (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop ||
37 |               document.body.scrollTop) + 'px'
38 |           ripple.style.left =
39 |             (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft ||
40 |               document.body.scrollLeft) + 'px'
41 |       }
42 |       ripple.style.backgroundColor = opts.color
43 |       ripple.className = 'waves-ripple z-active'
44 |       return false
45 |     }
46 |   }
47 | 
48 |   if (!el[context]) {
49 |     el[context] = {
50 |       removeHandle: handle
51 |     }
52 |   } else {
53 |     el[context].removeHandle = handle
54 |   }
55 | 
56 |   return handle
57 | }
58 | 
59 | export default {
60 |   bind(el, binding) {
61 |     el.addEventListener('click', handleClick(el, binding), false)
62 |   },
63 |   update(el, binding) {
64 |     el.removeEventListener('click', el[context].removeHandle, false)
65 |     el.addEventListener('click', handleClick(el, binding), false)
66 |   },
67 |   unbind(el) {
68 |     el.removeEventListener('click', el[context].removeHandle, false)
69 |     el[context] = null
70 |     delete el[context]
71 |   }
72 | }
73 | 


--------------------------------------------------------------------------------
/frontend/src/icons/index.js:
--------------------------------------------------------------------------------
 1 | import Vue from 'vue'
 2 | import SvgIcon from '@/components/SvgIcon'// svg component
 3 | 
 4 | // register globally
 5 | Vue.component('svg-icon', SvgIcon)
 6 | 
 7 | const req = require.context('./svg', false, /\.svg$/)
 8 | const requireAll = requireContext => requireContext.keys().map(requireContext)
 9 | requireAll(req)
10 | 


--------------------------------------------------------------------------------
/frontend/src/icons/svg/dashboard.svg:
--------------------------------------------------------------------------------
1 | <svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg>


--------------------------------------------------------------------------------
/frontend/src/icons/svg/example.svg:
--------------------------------------------------------------------------------
1 | <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg>


--------------------------------------------------------------------------------
/frontend/src/icons/svg/eye-open.svg:
--------------------------------------------------------------------------------
1 | <svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>


--------------------------------------------------------------------------------
/frontend/src/icons/svg/eye.svg:
--------------------------------------------------------------------------------
1 | <svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>


--------------------------------------------------------------------------------
/frontend/src/icons/svg/form.svg:
--------------------------------------------------------------------------------
1 | <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.068 23.784c-1.02 0-1.877-.32-2.572-.96a8.588 8.588 0 0 1-1.738-2.237 11.524 11.524 0 0 1-1.042-2.621c-.232-.895-.348-1.641-.348-2.238V0h.278c.834 0 1.622.085 2.363.256.742.17 1.645.575 2.711 1.214 1.066.64 2.363 1.535 3.892 2.686 1.53 1.15 3.453 2.664 5.77 4.54 2.502 2.045 4.494 3.771 5.977 5.178 1.483 1.406 2.618 2.6 3.406 3.58.787.98 1.274 1.812 1.46 2.494.185.682.277 1.278.277 1.79v2.046H84.068zM127.3 84.01c.278.682.464 1.535.556 2.558.093 1.023-.37 2.003-1.39 2.94-.463.427-.88.832-1.25 1.215-.372.384-.696.704-.974.96a6.69 6.69 0 0 1-.973.767l-11.816-10.741a44.331 44.331 0 0 0 1.877-1.535 31.028 31.028 0 0 1 1.737-1.406c1.112-.938 2.317-1.343 3.615-1.215 1.297.128 2.363.405 3.197.83.927.427 1.923 1.173 2.989 2.239 1.065 1.065 1.876 2.195 2.432 3.388zM78.23 95.902c2.038 0 3.752-.511 5.143-1.534l-26.969 25.83H18.037c-1.761 0-3.684-.47-5.77-1.407a24.549 24.549 0 0 1-5.838-3.709 21.373 21.373 0 0 1-4.518-5.306c-1.204-2.003-1.807-4.07-1.807-6.202V16.495c0-1.79.44-3.665 1.32-5.626A18.41 18.41 0 0 1 5.04 5.562a21.798 21.798 0 0 1 5.213-3.964C12.198.533 14.237 0 16.37 0h53.24v15.984c0 1.62.278 3.367.834 5.242a16.704 16.704 0 0 0 2.572 5.179c1.159 1.577 2.665 2.898 4.518 3.964 1.853 1.066 4.078 1.598 6.673 1.598h20.295v42.325L85.458 92.45c1.02-1.364 1.529-2.856 1.529-4.476 0-2.216-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1c-2.409 0-4.448.789-6.116 2.366-1.668 1.577-2.502 3.474-2.502 5.69 0 2.217.834 4.092 2.502 5.626 1.668 1.535 3.707 2.302 6.117 2.302h52.13zM26.1 47.951c-2.41 0-4.449.789-6.117 2.366-1.668 1.577-2.502 3.473-2.502 5.69 0 2.216.834 4.092 2.502 5.626 1.668 1.534 3.707 2.302 6.117 2.302h52.13c2.409 0 4.47-.768 6.185-2.302 1.715-1.534 2.572-3.41 2.572-5.626 0-2.217-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1zm52.407 64.063l1.807-1.663 3.476-3.196a479.75 479.75 0 0 0 4.587-4.284 500.757 500.757 0 0 1 5.004-4.667c3.985-3.666 8.48-7.758 13.485-12.276l11.677 10.741-13.485 12.404-5.004 4.603-4.587 4.22a179.46 179.46 0 0 0-3.267 3.068c-.88.853-1.367 1.322-1.46 1.407-.463.341-.973.703-1.529 1.087-.556.383-1.112.703-1.668.959-.556.256-1.413.575-2.572.959a83.5 83.5 0 0 1-3.545 1.087 72.2 72.2 0 0 1-3.475.895c-1.112.256-1.946.426-2.502.511-1.112.17-1.854.043-2.224-.383-.371-.426-.464-1.151-.278-2.174.092-.511.278-1.279.556-2.302.278-1.023.602-2.067.973-3.132l1.042-3.005c.325-.938.58-1.577.765-1.918a10.157 10.157 0 0 1 2.224-2.941z"/></svg>


--------------------------------------------------------------------------------
/frontend/src/icons/svg/link.svg:
--------------------------------------------------------------------------------
1 | <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></svg>


--------------------------------------------------------------------------------
/frontend/src/icons/svg/nested.svg:
--------------------------------------------------------------------------------
1 | <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.002 9.2c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-5.043-3.58-9.132-7.997-9.132S.002 4.157.002 9.2zM31.997.066h95.981V18.33H31.997V.066zm0 45.669c0 5.044 3.58 9.132 7.998 9.132 4.417 0 7.997-4.088 7.997-9.132 0-3.263-1.524-6.278-3.998-7.91-2.475-1.63-5.524-1.63-7.998 0-2.475 1.632-4 4.647-4 7.91zM63.992 36.6h63.986v18.265H63.992V36.6zm-31.995 82.2c0 5.043 3.58 9.132 7.998 9.132 4.417 0 7.997-4.089 7.997-9.132 0-5.044-3.58-9.133-7.997-9.133s-7.998 4.089-7.998 9.133zm31.995-9.131h63.986v18.265H63.992V109.67zm0-27.404c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-3.263-1.524-6.277-3.998-7.909-2.475-1.631-5.524-1.631-7.998 0-2.475 1.632-4 4.646-4 7.91zm31.995-9.13h31.991V91.4H95.987V73.135z"/></svg>


--------------------------------------------------------------------------------
/frontend/src/icons/svg/password.svg:
--------------------------------------------------------------------------------
1 | <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg>


--------------------------------------------------------------------------------
/frontend/src/icons/svg/table.svg:
--------------------------------------------------------------------------------
1 | <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg>


--------------------------------------------------------------------------------
/frontend/src/icons/svg/tree.svg:
--------------------------------------------------------------------------------
1 | <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg>


--------------------------------------------------------------------------------
/frontend/src/icons/svg/user.svg:
--------------------------------------------------------------------------------
1 | <svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>


--------------------------------------------------------------------------------
/frontend/src/icons/svgo.yml:
--------------------------------------------------------------------------------
 1 | # replace default config
 2 | 
 3 | # multipass: true
 4 | # full: true
 5 | 
 6 | plugins:
 7 | 
 8 |   # - name
 9 |   #
10 |   # or:
11 |   # - name: false
12 |   # - name: true
13 |   #
14 |   # or:
15 |   # - name:
16 |   #     param1: 1
17 |   #     param2: 2
18 | 
19 | - removeAttrs:
20 |     attrs:
21 |       - 'fill'
22 |       - 'fill-rule'
23 | 


--------------------------------------------------------------------------------
/frontend/src/layout/components/AppMain.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <section class="app-main">
 3 |     <transition name="fade-transform" mode="out-in">
 4 |       <router-view :key="key" />
 5 |     </transition>
 6 |   </section>
 7 | </template>
 8 | 
 9 | <script>
10 | export default {
11 |   name: 'AppMain',
12 |   computed: {
13 |     key() {
14 |       return this.$route.path
15 |     }
16 |   }
17 | }
18 | </script>
19 | 
20 | <style scoped>
21 | .app-main {
22 |   /*50 = navbar  */
23 |   min-height: calc(100vh - 50px);
24 |   width: 100%;
25 |   position: relative;
26 |   overflow: hidden;
27 | }
28 | .fixed-header+.app-main {
29 |   padding-top: 50px;
30 | }
31 | </style>
32 | 
33 | <style lang="scss">
34 | // fix css style bug in open el-dialog
35 | .el-popup-parent--hidden {
36 |   .fixed-header {
37 |     padding-right: 15px;
38 |   }
39 | }
40 | </style>
41 | 


--------------------------------------------------------------------------------
/frontend/src/layout/components/Sidebar/FixiOSBug.js:
--------------------------------------------------------------------------------
 1 | export default {
 2 |   computed: {
 3 |     device() {
 4 |       return this.$store.state.app.device
 5 |     }
 6 |   },
 7 |   mounted() {
 8 |     // In order to fix the click on menu on the ios device will trigger the mouseleave bug
 9 |     // https://github.com/PanJiaChen/vue-element-admin/issues/1135
10 |     this.fixBugIniOS()
11 |   },
12 |   methods: {
13 |     fixBugIniOS() {
14 |       const $subMenu = this.$refs.subMenu
15 |       if ($subMenu) {
16 |         const handleMouseleave = $subMenu.handleMouseleave
17 |         $subMenu.handleMouseleave = (e) => {
18 |           if (this.device === 'mobile') {
19 |             return
20 |           }
21 |           handleMouseleave(e)
22 |         }
23 |       }
24 |     }
25 |   }
26 | }
27 | 


--------------------------------------------------------------------------------
/frontend/src/layout/components/Sidebar/Item.vue:
--------------------------------------------------------------------------------
 1 | <script>
 2 | export default {
 3 |   name: 'MenuItem',
 4 |   functional: true,
 5 |   props: {
 6 |     icon: {
 7 |       type: String,
 8 |       default: ''
 9 |     },
10 |     title: {
11 |       type: String,
12 |       default: ''
13 |     }
14 |   },
15 |   render(h, context) {
16 |     const { icon, title } = context.props
17 |     const vnodes = []
18 | 
19 |     if (icon) {
20 |       if (icon.includes('el-icon')) {
21 |         vnodes.push(<i class={[icon, 'sub-el-icon']} />)
22 |       } else {
23 |         vnodes.push(<svg-icon icon-class={icon}/>)
24 |       }
25 |     }
26 | 
27 |     if (title) {
28 |       vnodes.push(<span slot='title'>{(title)}</span>)
29 |     }
30 |     return vnodes
31 |   }
32 | }
33 | </script>
34 | 
35 | <style scoped>
36 | .sub-el-icon {
37 |   color: currentColor;
38 |   width: 1em;
39 |   height: 1em;
40 | }
41 | </style>
42 | 


--------------------------------------------------------------------------------
/frontend/src/layout/components/Sidebar/Link.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <component :is="type" v-bind="linkProps(to)">
 3 |     <slot />
 4 |   </component>
 5 | </template>
 6 | 
 7 | <script>
 8 | import { isExternal } from '@/utils/validate'
 9 | 
10 | export default {
11 |   props: {
12 |     to: {
13 |       type: String,
14 |       required: true
15 |     }
16 |   },
17 |   computed: {
18 |     isExternal() {
19 |       return isExternal(this.to)
20 |     },
21 |     type() {
22 |       if (this.isExternal) {
23 |         return 'a'
24 |       }
25 |       return 'router-link'
26 |     }
27 |   },
28 |   methods: {
29 |     linkProps(to) {
30 |       if (this.isExternal) {
31 |         return {
32 |           href: to,
33 |           target: '_blank',
34 |           rel: 'noopener'
35 |         }
36 |       }
37 |       return {
38 |         to: to
39 |       }
40 |     }
41 |   }
42 | }
43 | </script>
44 | 


--------------------------------------------------------------------------------
/frontend/src/layout/components/Sidebar/Logo.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div class="sidebar-logo-container" :class="{'collapse':collapse}">
 3 |     <transition name="sidebarLogoFade">
 4 |       <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
 5 |         <img v-if="logo" :src="logo" class="sidebar-logo">
 6 |         <h1 v-else class="sidebar-title">{{ title }} </h1>
 7 |       </router-link>
 8 |       <router-link v-else key="expand" class="sidebar-logo-link" to="/">
 9 |         <img v-if="logo" :src="logo" class="sidebar-logo">
10 |         <h1 class="sidebar-title">{{ title }} </h1>
11 |       </router-link>
12 |     </transition>
13 |   </div>
14 | </template>
15 | 
16 | <script>
17 | export default {
18 |   name: 'SidebarLogo',
19 |   props: {
20 |     collapse: {
21 |       type: Boolean,
22 |       required: true
23 |     }
24 |   },
25 |   data() {
26 |     return {
27 |       title: 'Vue Admin Template',
28 |       logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png'
29 |     }
30 |   }
31 | }
32 | </script>
33 | 
34 | <style lang="scss" scoped>
35 | .sidebarLogoFade-enter-active {
36 |   transition: opacity 1.5s;
37 | }
38 | 
39 | .sidebarLogoFade-enter,
40 | .sidebarLogoFade-leave-to {
41 |   opacity: 0;
42 | }
43 | 
44 | .sidebar-logo-container {
45 |   position: relative;
46 |   width: 100%;
47 |   height: 50px;
48 |   line-height: 50px;
49 |   background: #2b2f3a;
50 |   text-align: center;
51 |   overflow: hidden;
52 | 
53 |   & .sidebar-logo-link {
54 |     height: 100%;
55 |     width: 100%;
56 | 
57 |     & .sidebar-logo {
58 |       width: 32px;
59 |       height: 32px;
60 |       vertical-align: middle;
61 |       margin-right: 12px;
62 |     }
63 | 
64 |     & .sidebar-title {
65 |       display: inline-block;
66 |       margin: 0;
67 |       color: #fff;
68 |       font-weight: 600;
69 |       line-height: 50px;
70 |       font-size: 14px;
71 |       font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
72 |       vertical-align: middle;
73 |     }
74 |   }
75 | 
76 |   &.collapse {
77 |     .sidebar-logo {
78 |       margin-right: 0px;
79 |     }
80 |   }
81 | }
82 | </style>
83 | 


--------------------------------------------------------------------------------
/frontend/src/layout/components/Sidebar/SidebarItem.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div v-if="!item.hidden">
 3 |     <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
 4 |       <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
 5 |         <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
 6 |           <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
 7 |         </el-menu-item>
 8 |       </app-link>
 9 |     </template>
10 | 
11 |     <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
12 |       <template slot="title">
13 |         <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
14 |       </template>
15 |       <sidebar-item
16 |         v-for="child in item.children"
17 |         :key="child.path"
18 |         :is-nest="true"
19 |         :item="child"
20 |         :base-path="resolvePath(child.path)"
21 |         class="nest-menu"
22 |       />
23 |     </el-submenu>
24 |   </div>
25 | </template>
26 | 
27 | <script>
28 | import path from 'path'
29 | import { isExternal } from '@/utils/validate'
30 | import Item from './Item'
31 | import AppLink from './Link'
32 | import FixiOSBug from './FixiOSBug'
33 | 
34 | export default {
35 |   name: 'SidebarItem',
36 |   components: { Item, AppLink },
37 |   mixins: [FixiOSBug],
38 |   props: {
39 |     // route object
40 |     item: {
41 |       type: Object,
42 |       required: true
43 |     },
44 |     isNest: {
45 |       type: Boolean,
46 |       default: false
47 |     },
48 |     basePath: {
49 |       type: String,
50 |       default: ''
51 |     }
52 |   },
53 |   data() {
54 |     // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
55 |     // TODO: refactor with render function
56 |     this.onlyOneChild = null
57 |     return {}
58 |   },
59 |   methods: {
60 |     hasOneShowingChild(children = [], parent) {
61 |       const showingChildren = children.filter(item => {
62 |         if (item.hidden) {
63 |           return false
64 |         } else {
65 |           // Temp set(will be used if only has one showing child)
66 |           this.onlyOneChild = item
67 |           return true
68 |         }
69 |       })
70 | 
71 |       // When there is only one child router, the child router is displayed by default
72 |       if (showingChildren.length === 1) {
73 |         return true
74 |       }
75 | 
76 |       // Show parent if there are no child router to display
77 |       if (showingChildren.length === 0) {
78 |         this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
79 |         return true
80 |       }
81 | 
82 |       return false
83 |     },
84 |     resolvePath(routePath) {
85 |       if (isExternal(routePath)) {
86 |         return routePath
87 |       }
88 |       if (isExternal(this.basePath)) {
89 |         return this.basePath
90 |       }
91 |       return path.resolve(this.basePath, routePath)
92 |     }
93 |   }
94 | }
95 | </script>
96 | 


--------------------------------------------------------------------------------
/frontend/src/layout/components/Sidebar/index.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div :class="{'has-logo':showLogo}">
 3 |     <logo v-if="showLogo" :collapse="isCollapse" />
 4 |     <el-scrollbar wrap-class="scrollbar-wrapper">
 5 |       <el-menu
 6 |         :default-active="activeMenu"
 7 |         :collapse="isCollapse"
 8 |         :background-color="variables.menuBg"
 9 |         :text-color="variables.menuText"
10 |         :unique-opened="false"
11 |         :active-text-color="variables.menuActiveText"
12 |         :collapse-transition="false"
13 |         mode="vertical"
14 |       >
15 |         <sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />
16 |       </el-menu>
17 |     </el-scrollbar>
18 |   </div>
19 | </template>
20 | 
21 | <script>
22 | import { mapGetters } from 'vuex'
23 | import Logo from './Logo'
24 | import SidebarItem from './SidebarItem'
25 | import variables from '@/styles/variables.scss'
26 | 
27 | export default {
28 |   components: { SidebarItem, Logo },
29 |   computed: {
30 |     ...mapGetters([
31 |       'sidebar'
32 |     ]),
33 |     routes() {
34 |       return this.$router.options.routes
35 |     },
36 |     activeMenu() {
37 |       const route = this.$route
38 |       const { meta, path } = route
39 |       // if set path, the sidebar will highlight the path you set
40 |       if (meta.activeMenu) {
41 |         return meta.activeMenu
42 |       }
43 |       return path
44 |     },
45 |     showLogo() {
46 |       return this.$store.state.settings.sidebarLogo
47 |     },
48 |     variables() {
49 |       return variables
50 |     },
51 |     isCollapse() {
52 |       return !this.sidebar.opened
53 |     }
54 |   }
55 | }
56 | </script>
57 | 


--------------------------------------------------------------------------------
/frontend/src/layout/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Navbar } from './Navbar'
2 | export { default as Sidebar } from './Sidebar'
3 | export { default as AppMain } from './AppMain'
4 | 


--------------------------------------------------------------------------------
/frontend/src/layout/index.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div :class="classObj" class="app-wrapper">
 3 |     <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
 4 |     <sidebar class="sidebar-container" />
 5 |     <div class="main-container">
 6 |       <div :class="{'fixed-header':fixedHeader}">
 7 |         <navbar />
 8 |       </div>
 9 |       <app-main />
10 |     </div>
11 |   </div>
12 | </template>
13 | 
14 | <script>
15 | import { Navbar, Sidebar, AppMain } from './components'
16 | import ResizeMixin from './mixin/ResizeHandler'
17 | 
18 | export default {
19 |   name: 'Layout',
20 |   components: {
21 |     Navbar,
22 |     Sidebar,
23 |     AppMain
24 |   },
25 |   mixins: [ResizeMixin],
26 |   computed: {
27 |     sidebar() {
28 |       return this.$store.state.app.sidebar
29 |     },
30 |     device() {
31 |       return this.$store.state.app.device
32 |     },
33 |     fixedHeader() {
34 |       return this.$store.state.settings.fixedHeader
35 |     },
36 |     classObj() {
37 |       return {
38 |         hideSidebar: !this.sidebar.opened,
39 |         openSidebar: this.sidebar.opened,
40 |         withoutAnimation: this.sidebar.withoutAnimation,
41 |         mobile: this.device === 'mobile'
42 |       }
43 |     }
44 |   },
45 |   methods: {
46 |     handleClickOutside() {
47 |       this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
48 |     }
49 |   }
50 | }
51 | </script>
52 | 
53 | <style lang="scss" scoped>
54 |   @import "~@/styles/mixin.scss";
55 |   @import "~@/styles/variables.scss";
56 | 
57 |   .app-wrapper {
58 |     @include clearfix;
59 |     position: relative;
60 |     height: 100%;
61 |     width: 100%;
62 |     &.mobile.openSidebar{
63 |       position: fixed;
64 |       top: 0;
65 |     }
66 |   }
67 |   .drawer-bg {
68 |     background: #000;
69 |     opacity: 0.3;
70 |     width: 100%;
71 |     top: 0;
72 |     height: 100%;
73 |     position: absolute;
74 |     z-index: 999;
75 |   }
76 | 
77 |   .fixed-header {
78 |     position: fixed;
79 |     top: 0;
80 |     right: 0;
81 |     z-index: 9;
82 |     width: calc(100% - #{$sideBarWidth});
83 |     transition: width 0.28s;
84 |   }
85 | 
86 |   .hideSidebar .fixed-header {
87 |     width: calc(100% - 54px)
88 |   }
89 | 
90 |   .mobile .fixed-header {
91 |     width: 100%;
92 |   }
93 | </style>
94 | 


--------------------------------------------------------------------------------
/frontend/src/layout/mixin/ResizeHandler.js:
--------------------------------------------------------------------------------
 1 | import store from '@/store'
 2 | 
 3 | const { body } = document
 4 | const WIDTH = 992 // refer to Bootstrap's responsive design
 5 | 
 6 | export default {
 7 |   watch: {
 8 |     $route(route) {
 9 |       if (this.device === 'mobile' && this.sidebar.opened) {
10 |         store.dispatch('app/closeSideBar', { withoutAnimation: false })
11 |       }
12 |     }
13 |   },
14 |   beforeMount() {
15 |     window.addEventListener('resize', this.$_resizeHandler)
16 |   },
17 |   beforeDestroy() {
18 |     window.removeEventListener('resize', this.$_resizeHandler)
19 |   },
20 |   mounted() {
21 |     const isMobile = this.$_isMobile()
22 |     if (isMobile) {
23 |       store.dispatch('app/toggleDevice', 'mobile')
24 |       store.dispatch('app/closeSideBar', { withoutAnimation: true })
25 |     }
26 |   },
27 |   methods: {
28 |     // use $_ for mixins properties
29 |     // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
30 |     $_isMobile() {
31 |       const rect = body.getBoundingClientRect()
32 |       return rect.width - 1 < WIDTH
33 |     },
34 |     $_resizeHandler() {
35 |       if (!document.hidden) {
36 |         const isMobile = this.$_isMobile()
37 |         store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
38 | 
39 |         if (isMobile) {
40 |           store.dispatch('app/closeSideBar', { withoutAnimation: true })
41 |         }
42 |       }
43 |     }
44 |   }
45 | }
46 | 


--------------------------------------------------------------------------------
/frontend/src/main.js:
--------------------------------------------------------------------------------
 1 | import Vue from 'vue'
 2 | 
 3 | import 'normalize.css/normalize.css' // A modern alternative to CSS resets
 4 | 
 5 | import ElementUI from 'element-ui'
 6 | import 'element-ui/lib/theme-chalk/index.css'
 7 | import locale from 'element-ui/lib/locale/lang/en' // lang i18n
 8 | 
 9 | import '@/styles/index.scss' // global css
10 | 
11 | import App from './App'
12 | import store from './store'
13 | import router from './router'
14 | 
15 | import '@/icons' // icon
16 | // import '@/permission' // permission control
17 | // 不进行登录校验。
18 | 
19 | /**
20 |  * If you don't want to use mock-server
21 |  * you want to use MockJs for mock api
22 |  * you can execute: mockXHR()
23 |  *
24 |  * Currently MockJs will be used in the production environment,
25 |  * please remove it before going online ! ! !
26 |  */
27 | if (process.env.NODE_ENV === 'production') {
28 |   const { mockXHR } = require('../mock')
29 |   mockXHR()
30 | }
31 | 
32 | // set ElementUI lang to EN
33 | Vue.use(ElementUI, { locale })
34 | // 如果想要中文版 element-ui,按如下方式声明
35 | // Vue.use(ElementUI)
36 | 
37 | Vue.config.productionTip = false
38 | 
39 | new Vue({
40 |   el: '#app',
41 |   router,
42 |   store,
43 |   render: h => h(App)
44 | })
45 | 


--------------------------------------------------------------------------------
/frontend/src/permission.js:
--------------------------------------------------------------------------------
 1 | import router from './router'
 2 | import store from './store'
 3 | import { Message } from 'element-ui'
 4 | import NProgress from 'nprogress' // progress bar
 5 | import 'nprogress/nprogress.css' // progress bar style
 6 | import { getToken } from '@/utils/auth' // get token from cookie
 7 | import getPageTitle from '@/utils/get-page-title'
 8 | 
 9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration
10 | 
11 | const whiteList = ['/login'] // no redirect whitelist
12 | 
13 | router.beforeEach(async(to, from, next) => {
14 |   // start progress bar
15 |   NProgress.start()
16 | 
17 |   // set page title
18 |   document.title = getPageTitle(to.meta.title)
19 | 
20 |   // determine whether the user has logged in
21 |   const hasToken = getToken()
22 | 
23 |   if (hasToken) {
24 |     if (to.path === '/login') {
25 |       // if is logged in, redirect to the home page
26 |       next({ path: '/' })
27 |       NProgress.done()
28 |     } else {
29 |       const hasGetUserInfo = store.getters.name
30 |       if (hasGetUserInfo) {
31 |         next()
32 |       } else {
33 |         try {
34 |           // get user info
35 |           await store.dispatch('user/getInfo')
36 | 
37 |           next()
38 |         } catch (error) {
39 |           // remove token and go to login page to re-login
40 |           await store.dispatch('user/resetToken')
41 |           Message.error(error || 'Has Error')
42 |           next(`/login?redirect=${to.path}`)
43 |           NProgress.done()
44 |         }
45 |       }
46 |     }
47 |   } else {
48 |     /* has no token*/
49 | 
50 |     if (whiteList.indexOf(to.path) !== -1) {
51 |       // in the free login whitelist, go directly
52 |       next()
53 |     } else {
54 |       // other pages that do not have permission to access are redirected to the login page.
55 |       next(`/login?redirect=${to.path}`)
56 |       NProgress.done()
57 |     }
58 |   }
59 | })
60 | 
61 | router.afterEach(() => {
62 |   // finish progress bar
63 |   NProgress.done()
64 | })
65 | 


--------------------------------------------------------------------------------
/frontend/src/settings.js:
--------------------------------------------------------------------------------
 1 | module.exports = {
 2 | 
 3 |   title: 'Vue Admin Template',
 4 | 
 5 |   /**
 6 |    * @type {boolean} true | false
 7 |    * @description Whether fix the header
 8 |    */
 9 |   fixedHeader: false,
10 | 
11 |   /**
12 |    * @type {boolean} true | false
13 |    * @description Whether show the logo in sidebar
14 |    */
15 |   sidebarLogo: false
16 | }
17 | 


--------------------------------------------------------------------------------
/frontend/src/store/getters.js:
--------------------------------------------------------------------------------
1 | const getters = {
2 |   sidebar: state => state.app.sidebar,
3 |   device: state => state.app.device,
4 |   token: state => state.user.token,
5 |   avatar: state => state.user.avatar,
6 |   name: state => state.user.name
7 | }
8 | export default getters
9 | 


--------------------------------------------------------------------------------
/frontend/src/store/index.js:
--------------------------------------------------------------------------------
 1 | import Vue from 'vue'
 2 | import Vuex from 'vuex'
 3 | import getters from './getters'
 4 | import app from './modules/app'
 5 | import settings from './modules/settings'
 6 | import user from './modules/user'
 7 | 
 8 | Vue.use(Vuex)
 9 | 
10 | const store = new Vuex.Store({
11 |   modules: {
12 |     app,
13 |     settings,
14 |     user
15 |   },
16 |   getters
17 | })
18 | 
19 | export default store
20 | 


--------------------------------------------------------------------------------
/frontend/src/store/modules/app.js:
--------------------------------------------------------------------------------
 1 | import Cookies from 'js-cookie'
 2 | 
 3 | const state = {
 4 |   sidebar: {
 5 |     opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
 6 |     withoutAnimation: false
 7 |   },
 8 |   device: 'desktop'
 9 | }
10 | 
11 | const mutations = {
12 |   TOGGLE_SIDEBAR: state => {
13 |     state.sidebar.opened = !state.sidebar.opened
14 |     state.sidebar.withoutAnimation = false
15 |     if (state.sidebar.opened) {
16 |       Cookies.set('sidebarStatus', 1)
17 |     } else {
18 |       Cookies.set('sidebarStatus', 0)
19 |     }
20 |   },
21 |   CLOSE_SIDEBAR: (state, withoutAnimation) => {
22 |     Cookies.set('sidebarStatus', 0)
23 |     state.sidebar.opened = false
24 |     state.sidebar.withoutAnimation = withoutAnimation
25 |   },
26 |   TOGGLE_DEVICE: (state, device) => {
27 |     state.device = device
28 |   }
29 | }
30 | 
31 | const actions = {
32 |   toggleSideBar({ commit }) {
33 |     commit('TOGGLE_SIDEBAR')
34 |   },
35 |   closeSideBar({ commit }, { withoutAnimation }) {
36 |     commit('CLOSE_SIDEBAR', withoutAnimation)
37 |   },
38 |   toggleDevice({ commit }, device) {
39 |     commit('TOGGLE_DEVICE', device)
40 |   }
41 | }
42 | 
43 | export default {
44 |   namespaced: true,
45 |   state,
46 |   mutations,
47 |   actions
48 | }
49 | 


--------------------------------------------------------------------------------
/frontend/src/store/modules/settings.js:
--------------------------------------------------------------------------------
 1 | import defaultSettings from '@/settings'
 2 | 
 3 | const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
 4 | 
 5 | const state = {
 6 |   showSettings: showSettings,
 7 |   fixedHeader: fixedHeader,
 8 |   sidebarLogo: sidebarLogo
 9 | }
10 | 
11 | const mutations = {
12 |   CHANGE_SETTING: (state, { key, value }) => {
13 |     // eslint-disable-next-line no-prototype-builtins
14 |     if (state.hasOwnProperty(key)) {
15 |       state[key] = value
16 |     }
17 |   }
18 | }
19 | 
20 | const actions = {
21 |   changeSetting({ commit }, data) {
22 |     commit('CHANGE_SETTING', data)
23 |   }
24 | }
25 | 
26 | export default {
27 |   namespaced: true,
28 |   state,
29 |   mutations,
30 |   actions
31 | }
32 | 
33 | 


--------------------------------------------------------------------------------
/frontend/src/store/modules/user.js:
--------------------------------------------------------------------------------
 1 | import { login, logout, getInfo } from '@/api/user'
 2 | import { getToken, setToken, removeToken } from '@/utils/auth'
 3 | import { resetRouter } from '@/router'
 4 | 
 5 | const getDefaultState = () => {
 6 |   return {
 7 |     token: getToken(),
 8 |     name: '',
 9 |     avatar: ''
10 |   }
11 | }
12 | 
13 | const state = getDefaultState()
14 | 
15 | const mutations = {
16 |   RESET_STATE: (state) => {
17 |     Object.assign(state, getDefaultState())
18 |   },
19 |   SET_TOKEN: (state, token) => {
20 |     state.token = token
21 |   },
22 |   SET_NAME: (state, name) => {
23 |     state.name = name
24 |   },
25 |   SET_AVATAR: (state, avatar) => {
26 |     state.avatar = avatar
27 |   }
28 | }
29 | 
30 | const actions = {
31 |   // user login
32 |   login({ commit }, userInfo) {
33 |     const { username, password } = userInfo
34 |     return new Promise((resolve, reject) => {
35 |       login({ username: username.trim(), password: password }).then(response => {
36 |         const { data } = response
37 |         commit('SET_TOKEN', data.token)
38 |         setToken(data.token)
39 |         resolve()
40 |       }).catch(error => {
41 |         reject(error)
42 |       })
43 |     })
44 |   },
45 | 
46 |   // get user info
47 |   getInfo({ commit, state }) {
48 |     return new Promise((resolve, reject) => {
49 |       getInfo(state.token).then(response => {
50 |         const { data } = response
51 | 
52 |         if (!data) {
53 |           return reject('Verification failed, please Login again.')
54 |         }
55 | 
56 |         const { name, avatar } = data
57 | 
58 |         commit('SET_NAME', name)
59 |         commit('SET_AVATAR', avatar)
60 |         resolve(data)
61 |       }).catch(error => {
62 |         reject(error)
63 |       })
64 |     })
65 |   },
66 | 
67 |   // user logout
68 |   logout({ commit, state }) {
69 |     return new Promise((resolve, reject) => {
70 |       logout(state.token).then(() => {
71 |         removeToken() // must remove  token  first
72 |         resetRouter()
73 |         commit('RESET_STATE')
74 |         resolve()
75 |       }).catch(error => {
76 |         reject(error)
77 |       })
78 |     })
79 |   },
80 | 
81 |   // remove token
82 |   resetToken({ commit }) {
83 |     return new Promise(resolve => {
84 |       removeToken() // must remove  token  first
85 |       commit('RESET_STATE')
86 |       resolve()
87 |     })
88 |   }
89 | }
90 | 
91 | export default {
92 |   namespaced: true,
93 |   state,
94 |   mutations,
95 |   actions
96 | }
97 | 
98 | 


--------------------------------------------------------------------------------
/frontend/src/styles/element-ui.scss:
--------------------------------------------------------------------------------
 1 | // cover some element-ui styles
 2 | 
 3 | .el-breadcrumb__inner,
 4 | .el-breadcrumb__inner a {
 5 |   font-weight: 400 !important;
 6 | }
 7 | 
 8 | .el-upload {
 9 |   input[type="file"] {
10 |     display: none !important;
11 |   }
12 | }
13 | 
14 | .el-upload__input {
15 |   display: none;
16 | }
17 | 
18 | 
19 | // to fixed https://github.com/ElemeFE/element/issues/2461
20 | .el-dialog {
21 |   transform: none;
22 |   left: 0;
23 |   position: relative;
24 |   margin: 0 auto;
25 | }
26 | 
27 | // refine element ui upload
28 | .upload-container {
29 |   .el-upload {
30 |     width: 100%;
31 | 
32 |     .el-upload-dragger {
33 |       width: 100%;
34 |       height: 200px;
35 |     }
36 |   }
37 | }
38 | 
39 | // dropdown
40 | .el-dropdown-menu {
41 |   a {
42 |     display: block
43 |   }
44 | }
45 | 
46 | // to fix el-date-picker css style
47 | .el-range-separator {
48 |   box-sizing: content-box;
49 | }
50 | 


--------------------------------------------------------------------------------
/frontend/src/styles/index.scss:
--------------------------------------------------------------------------------
 1 | @import './variables.scss';
 2 | @import './mixin.scss';
 3 | @import './transition.scss';
 4 | @import './element-ui.scss';
 5 | @import './sidebar.scss';
 6 | 
 7 | body {
 8 |   height: 100%;
 9 |   -moz-osx-font-smoothing: grayscale;
10 |   -webkit-font-smoothing: antialiased;
11 |   text-rendering: optimizeLegibility;
12 |   font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
13 | }
14 | 
15 | label {
16 |   font-weight: 700;
17 | }
18 | 
19 | html {
20 |   height: 100%;
21 |   box-sizing: border-box;
22 | }
23 | 
24 | #app {
25 |   height: 100%;
26 | }
27 | 
28 | *,
29 | *:before,
30 | *:after {
31 |   box-sizing: inherit;
32 | }
33 | 
34 | a:focus,
35 | a:active {
36 |   outline: none;
37 | }
38 | 
39 | a,
40 | a:focus,
41 | a:hover {
42 |   cursor: pointer;
43 |   color: inherit;
44 |   text-decoration: none;
45 | }
46 | 
47 | div:focus {
48 |   outline: none;
49 | }
50 | 
51 | .clearfix {
52 |   &:after {
53 |     visibility: hidden;
54 |     display: block;
55 |     font-size: 0;
56 |     content: " ";
57 |     clear: both;
58 |     height: 0;
59 |   }
60 | }
61 | 
62 | // main-container global css
63 | .app-container {
64 |   padding: 20px;
65 | }
66 | 


--------------------------------------------------------------------------------
/frontend/src/styles/mixin.scss:
--------------------------------------------------------------------------------
 1 | @mixin clearfix {
 2 |   &:after {
 3 |     content: "";
 4 |     display: table;
 5 |     clear: both;
 6 |   }
 7 | }
 8 | 
 9 | @mixin scrollBar {
10 |   &::-webkit-scrollbar-track-piece {
11 |     background: #d3dce6;
12 |   }
13 | 
14 |   &::-webkit-scrollbar {
15 |     width: 6px;
16 |   }
17 | 
18 |   &::-webkit-scrollbar-thumb {
19 |     background: #99a9bf;
20 |     border-radius: 20px;
21 |   }
22 | }
23 | 
24 | @mixin relative {
25 |   position: relative;
26 |   width: 100%;
27 |   height: 100%;
28 | }
29 | 


--------------------------------------------------------------------------------
/frontend/src/styles/transition.scss:
--------------------------------------------------------------------------------
 1 | // global transition css
 2 | 
 3 | /* fade */
 4 | .fade-enter-active,
 5 | .fade-leave-active {
 6 |   transition: opacity 0.28s;
 7 | }
 8 | 
 9 | .fade-enter,
10 | .fade-leave-active {
11 |   opacity: 0;
12 | }
13 | 
14 | /* fade-transform */
15 | .fade-transform-leave-active,
16 | .fade-transform-enter-active {
17 |   transition: all .5s;
18 | }
19 | 
20 | .fade-transform-enter {
21 |   opacity: 0;
22 |   transform: translateX(-30px);
23 | }
24 | 
25 | .fade-transform-leave-to {
26 |   opacity: 0;
27 |   transform: translateX(30px);
28 | }
29 | 
30 | /* breadcrumb transition */
31 | .breadcrumb-enter-active,
32 | .breadcrumb-leave-active {
33 |   transition: all .5s;
34 | }
35 | 
36 | .breadcrumb-enter,
37 | .breadcrumb-leave-active {
38 |   opacity: 0;
39 |   transform: translateX(20px);
40 | }
41 | 
42 | .breadcrumb-move {
43 |   transition: all .5s;
44 | }
45 | 
46 | .breadcrumb-leave-active {
47 |   position: absolute;
48 | }
49 | 


--------------------------------------------------------------------------------
/frontend/src/styles/variables.scss:
--------------------------------------------------------------------------------
 1 | // sidebar
 2 | $menuText:#bfcbd9;
 3 | $menuActiveText:#409EFF;
 4 | $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
 5 | 
 6 | $menuBg:#304156;
 7 | $menuHover:#263445;
 8 | 
 9 | $subMenuBg:#1f2d3d;
10 | $subMenuHover:#001528;
11 | 
12 | $sideBarWidth: 210px;
13 | 
14 | // the :export directive is the magic sauce for webpack
15 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
16 | :export {
17 |   menuText: $menuText;
18 |   menuActiveText: $menuActiveText;
19 |   subMenuActiveText: $subMenuActiveText;
20 |   menuBg: $menuBg;
21 |   menuHover: $menuHover;
22 |   subMenuBg: $subMenuBg;
23 |   subMenuHover: $subMenuHover;
24 |   sideBarWidth: $sideBarWidth;
25 | }
26 | 


--------------------------------------------------------------------------------
/frontend/src/utils/auth.js:
--------------------------------------------------------------------------------
 1 | import Cookies from 'js-cookie'
 2 | 
 3 | const TokenKey = 'vue_admin_template_token'
 4 | 
 5 | export function getToken() {
 6 |   return Cookies.get(TokenKey)
 7 | }
 8 | 
 9 | export function setToken(token) {
10 |   return Cookies.set(TokenKey, token)
11 | }
12 | 
13 | export function removeToken() {
14 |   return Cookies.remove(TokenKey)
15 | }
16 | 


--------------------------------------------------------------------------------
/frontend/src/utils/get-page-title.js:
--------------------------------------------------------------------------------
 1 | import defaultSettings from '@/settings'
 2 | 
 3 | const title = defaultSettings.title || 'Vue Admin Template'
 4 | 
 5 | export default function getPageTitle(pageTitle) {
 6 |   if (pageTitle) {
 7 |     return `${pageTitle} - ${title}`
 8 |   }
 9 |   return `${title}`
10 | }
11 | 


--------------------------------------------------------------------------------
/frontend/src/utils/index.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Created by PanJiaChen on 16/11/18.
  3 |  */
  4 | 
  5 | /**
  6 |  * Parse the time to string
  7 |  * @param {(Object|string|number)} time
  8 |  * @param {string} cFormat
  9 |  * @returns {string | null}
 10 |  */
 11 | export function parseTime(time, cFormat) {
 12 |   if (arguments.length === 0 || !time) {
 13 |     return null
 14 |   }
 15 |   const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
 16 |   let date
 17 |   if (typeof time === 'object') {
 18 |     date = time
 19 |   } else {
 20 |     if ((typeof time === 'string')) {
 21 |       if ((/^[0-9]+$/.test(time))) {
 22 |         // support "1548221490638"
 23 |         time = parseInt(time)
 24 |       } else {
 25 |         // support safari
 26 |         // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
 27 |         time = time.replace(new RegExp(/-/gm), '/')
 28 |       }
 29 |     }
 30 | 
 31 |     if ((typeof time === 'number') && (time.toString().length === 10)) {
 32 |       time = time * 1000
 33 |     }
 34 |     date = new Date(time)
 35 |   }
 36 |   const formatObj = {
 37 |     y: date.getFullYear(),
 38 |     m: date.getMonth() + 1,
 39 |     d: date.getDate(),
 40 |     h: date.getHours(),
 41 |     i: date.getMinutes(),
 42 |     s: date.getSeconds(),
 43 |     a: date.getDay()
 44 |   }
 45 |   const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
 46 |     const value = formatObj[key]
 47 |     // Note: getDay() returns 0 on Sunday
 48 |     if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
 49 |     return value.toString().padStart(2, '0')
 50 |   })
 51 |   return time_str
 52 | }
 53 | 
 54 | /**
 55 |  * @param {number} time
 56 |  * @param {string} option
 57 |  * @returns {string}
 58 |  */
 59 | export function formatTime(time, option) {
 60 |   if (('' + time).length === 10) {
 61 |     time = parseInt(time) * 1000
 62 |   } else {
 63 |     time = +time
 64 |   }
 65 |   const d = new Date(time)
 66 |   const now = Date.now()
 67 | 
 68 |   const diff = (now - d) / 1000
 69 | 
 70 |   if (diff < 30) {
 71 |     return '刚刚'
 72 |   } else if (diff < 3600) {
 73 |     // less 1 hour
 74 |     return Math.ceil(diff / 60) + '分钟前'
 75 |   } else if (diff < 3600 * 24) {
 76 |     return Math.ceil(diff / 3600) + '小时前'
 77 |   } else if (diff < 3600 * 24 * 2) {
 78 |     return '1天前'
 79 |   }
 80 |   if (option) {
 81 |     return parseTime(time, option)
 82 |   } else {
 83 |     return (
 84 |       d.getMonth() +
 85 |       1 +
 86 |       '月' +
 87 |       d.getDate() +
 88 |       '日' +
 89 |       d.getHours() +
 90 |       '时' +
 91 |       d.getMinutes() +
 92 |       '分'
 93 |     )
 94 |   }
 95 | }
 96 | 
 97 | /**
 98 |  * @param {string} url
 99 |  * @returns {Object}
100 |  */
101 | export function param2Obj(url) {
102 |   const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
103 |   if (!search) {
104 |     return {}
105 |   }
106 |   const obj = {}
107 |   const searchArr = search.split('&')
108 |   searchArr.forEach(v => {
109 |     const index = v.indexOf('=')
110 |     if (index !== -1) {
111 |       const name = v.substring(0, index)
112 |       const val = v.substring(index + 1, v.length)
113 |       obj[name] = val
114 |     }
115 |   })
116 |   return obj
117 | }
118 | 


--------------------------------------------------------------------------------
/frontend/src/utils/request.js:
--------------------------------------------------------------------------------
 1 | import axios from 'axios'
 2 | import { MessageBox, Message } from 'element-ui'
 3 | import store from '@/store'
 4 | import { getToken } from '@/utils/auth'
 5 | 
 6 | // create an axios instance
 7 | const service = axios.create({
 8 |   baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
 9 |    withCredentials: true, // send cookies when cross-domain requests
10 |   timeout: 5000 // request timeout
11 | })
12 | 
13 | // request interceptor
14 | service.interceptors.request.use(
15 |   config => {
16 |     // do something before request is sent
17 | 
18 |     if (store.getters.token) {
19 |       // let each request carry token
20 |       // ['X-Token'] is a custom headers key
21 |       // please modify it according to the actual situation
22 |       config.headers['X-Token'] = getToken()
23 |     }
24 |     return config
25 |   },
26 |   error => {
27 |     // do something with request error
28 |     console.log(error) // for debug
29 |     return Promise.reject(error)
30 |   }
31 | )
32 | 
33 | // response interceptor
34 | service.interceptors.response.use(
35 |   /**
36 |    * If you want to get http information such as headers or status
37 |    * Please return  response => response
38 |   */
39 | 
40 |   /**
41 |    * Determine the request status by custom code
42 |    * Here is just an example
43 |    * You can also judge the status by HTTP Status Code
44 |    */
45 |   response => {
46 |     const res = response.data
47 |     console.log('res.code: ' + res.code) // for debug
48 | 
49 |     // if the custom code is not 20000, it is judged as an error.
50 |     if (res.code !== 20000) {
51 |       Message({
52 |         message: res.message || 'Error',
53 |         type: 'error',
54 |         duration: 5 * 1000
55 |       })
56 | 
57 |       // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
58 |       if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
59 |         // to re-login
60 |         MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
61 |           confirmButtonText: 'Re-Login',
62 |           cancelButtonText: 'Cancel',
63 |           type: 'warning'
64 |         }).then(() => {
65 |           store.dispatch('user/resetToken').then(() => {
66 |             location.reload()
67 |           })
68 |         })
69 |       }
70 |       return Promise.reject(new Error(res.message || 'Error'))
71 |     } else {
72 |       return res
73 |     }
74 |   },
75 |   error => {
76 |     console.log('err' + error) // for debug
77 |     Message({
78 |       message: error.message,
79 |       type: 'error',
80 |       duration: 5 * 1000
81 |     })
82 |     return Promise.reject(error)
83 |   }
84 | )
85 | 
86 | export default service
87 | 


--------------------------------------------------------------------------------
/frontend/src/utils/scroll-to.js:
--------------------------------------------------------------------------------
 1 | Math.easeInOutQuad = function(t, b, c, d) {
 2 |   t /= d / 2
 3 |   if (t < 1) {
 4 |     return c / 2 * t * t + b
 5 |   }
 6 |   t--
 7 |   return -c / 2 * (t * (t - 2) - 1) + b
 8 | }
 9 | 
10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
11 | var requestAnimFrame = (function() {
12 |   return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
13 | })()
14 | 
15 | /**
16 |  * Because it's so fucking difficult to detect the scrolling element, just move them all
17 |  * @param {number} amount
18 |  */
19 | function move(amount) {
20 |   document.documentElement.scrollTop = amount
21 |   document.body.parentNode.scrollTop = amount
22 |   document.body.scrollTop = amount
23 | }
24 | 
25 | function position() {
26 |   return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
27 | }
28 | 
29 | /**
30 |  * @param {number} to
31 |  * @param {number} duration
32 |  * @param {Function} callback
33 |  */
34 | export function scrollTo(to, duration, callback) {
35 |   const start = position()
36 |   const change = to - start
37 |   const increment = 20
38 |   let currentTime = 0
39 |   duration = (typeof (duration) === 'undefined') ? 500 : duration
40 |   var animateScroll = function() {
41 |     // increment the time
42 |     currentTime += increment
43 |     // find the value with the quadratic in-out easing function
44 |     var val = Math.easeInOutQuad(currentTime, start, change, duration)
45 |     // move the document.body
46 |     move(val)
47 |     // do the animation unless its over
48 |     if (currentTime < duration) {
49 |       requestAnimFrame(animateScroll)
50 |     } else {
51 |       if (callback && typeof (callback) === 'function') {
52 |         // the animation is done so lets callback
53 |         callback()
54 |       }
55 |     }
56 |   }
57 |   animateScroll()
58 | }
59 | 


--------------------------------------------------------------------------------
/frontend/src/utils/validate.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Created by PanJiaChen on 16/11/18.
 3 |  */
 4 | 
 5 | /**
 6 |  * @param {string} path
 7 |  * @returns {Boolean}
 8 |  */
 9 | export function isExternal(path) {
10 |   return /^(https?:|mailto:|tel:)/.test(path)
11 | }
12 | 
13 | /**
14 |  * @param {string} str
15 |  * @returns {Boolean}
16 |  */
17 | export function validUsername(str) {
18 |   const valid_map = ['admin', 'editor']
19 |   return valid_map.indexOf(str.trim()) >= 0
20 | }
21 | 


--------------------------------------------------------------------------------
/frontend/src/views/dashboard/index.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <div class="dashboard-container">
  3 | 
  4 | <div class="clearfix">
  5 | 		<div class="pull-left tableTools-container">
  6 | 			<h3>基础库版本</h3>
  7 | 			<p>1,pandas使用【 {{ pandasVersion }} 】版本, <el-link type="primary" href="https://www.runoob.com/pandas/pandas-tutorial.html" target="_blank">中文文档</el-link> </p>
  8 | 			<p>2,numpy使用【 {{ numpyVersion }} 】版本, <el-link type="primary" href="https://www.runoob.com/numpy/numpy-tutorial.html" target="_blank">中文文档</el-link></p>
  9 | 			<p>3,sqlalchemy使用【 {{ sqlalchemyVersion }} 】版本, <el-link type="primary" href="https://docs.sqlalchemy.org/en/20/" target="_blank">英文文档</el-link></p>
 10 | 			<p>4,akshare使用【 {{ akshareVersion }} 】版本, <el-link type="primary" href="https://akshare.akfamily.xyz/data/index/index.html" target="_blank">中文文档</el-link></p>
 11 | 			<p>5,bokeh使用【 {{ bokehVersion }} 】版本, <el-link type="primary" href="http://docs.bokeh.org/en/latest/" target="_blank">官方文档</el-link></p>
 12 | 			<p>6,stockstats使用【 {{ stockstatsVersion }} 】版本, <el-link type="primary" href="https://github.com/jealous/stockstats/" target="_blank">官方文档</el-link></p>
 13 | 		</div>
 14 | 	</div>
 15 | 
 16 | 	<div class="clearfix">
 17 | 		<div class="pull-left tableTools-container">
 18 | 			<h3>相关资料信息</h3>
 19 | 			<p>1,github项目地址。<el-link type="primary" href="https://gitee.com/pythonstock/stock" target="_blank">pythonstock</el-link></p>
 20 | 			<p>2,博客地址。<el-link type="primary" href="https://blog.csdn.net/freewebsys/category_9285317.html" target="_blank">博客地址</el-link></p>
 21 | 		</div>
 22 | 	</div>
 23 | 
 24 | 	<div class="clearfix">
 25 | 		<div class="pull-left tableTools-container">
 26 | 			<h3>2021年9月20日更新,发布2.0版本</h3>
 27 | 			<p>1,修复bokeh的版本升级问题,可以显示趋势数据了。</p>
 28 | 			<p>2,使用 stock_zh_ah_name 做每日数据。</p>
 29 | 			<p>3,AkShare 升级到 1.1.9 版本。</p>
 30 | 		</div>
 31 | 	</div>
 32 | 
 33 | 	<div class="clearfix">
 34 | 		<div class="pull-left tableTools-container">
 35 | 			<h3>2021年8月31日更新job服务</h3>
 36 | 			<p>1,过滤包括:600,6006,601,000,001,002,且不包括ST的股票数据。。</p>
 37 | 			<p>2,使用 stock_zh_ah_name 做每日数据。</p>
 38 | 			<p>3,AkShare 升级到 1.0.80 版本。</p>
 39 | 
 40 | 		</div>
 41 | 	</div>
 42 | 
 43 | 	<div class="clearfix">
 44 | 		<div class="pull-left tableTools-container">
 45 | 			<h3>2021年6月3日使用 AkShare 做数据抓取服务</h3>
 46 | 			<p>1,使用 stock_zh_a_spot 做实时行情数据。</p>
 47 | 			<p>2,使用 stock_zh_a_daily 做历史数据统计。</p>
 48 | 			<p>3,升级基础镜像使用python3.7,AkShare 的 0.9.65 版本。</p>
 49 | 		</div>
 50 | 	</div>
 51 | 
 52 | 
 53 | 
 54 |   </div>
 55 | </template>
 56 | 
 57 | <script>
 58 | import { mapGetters } from 'vuex'
 59 | import { fetchPackageVersion } from '@/api/package'
 60 | 
 61 | export default {
 62 |   name: '首页',
 63 |   data() {
 64 |     return {
 65 | 			pandasVersion: '',
 66 | 			numpyVersion: '',
 67 | 			sqlalchemyVersion: '',
 68 | 			akshareVersion: '',
 69 | 			bokehVersion: '',
 70 | 			stockstatsVersion: '',
 71 | 	}
 72 |   },
 73 |   created() {
 74 |     this.getPackageVersion()
 75 |   },
 76 |   methods: {
 77 |     getPackageVersion() {
 78 | 	console.info("$router.params : ", this.$router.params)
 79 |       fetchPackageVersion().then(response => {
 80 | 
 81 | 		this.pandasVersion = response.pandasVersion
 82 | 		this.numpyVersion = response.numpyVersion
 83 | 		this.sqlalchemyVersion = response.sqlalchemyVersion
 84 | 		this.akshareVersion = response.akshareVersion
 85 | 		this.bokehVersion = response.bokehVersion
 86 | 		this.stockstatsVersion = response.stockstatsVersion
 87 | 
 88 |       })
 89 | 	}
 90 |   }
 91 | }
 92 | </script>
 93 | 
 94 | <style lang="scss" scoped>
 95 | .dashboard {
 96 |   &-container {
 97 |     margin: 30px;
 98 |   }
 99 |   &-text {
100 |     font-size: 30px;
101 |     line-height: 46px;
102 |   }
103 | }
104 | </style>


--------------------------------------------------------------------------------
/frontend/src/views/form/index.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div class="app-container">
 3 |     <el-form ref="form" :model="form" label-width="120px">
 4 |       <el-form-item label="Activity name">
 5 |         <el-input v-model="form.name" />
 6 |       </el-form-item>
 7 |       <el-form-item label="Activity zone">
 8 |         <el-select v-model="form.region" placeholder="please select your zone">
 9 |           <el-option label="Zone one" value="shanghai" />
10 |           <el-option label="Zone two" value="beijing" />
11 |         </el-select>
12 |       </el-form-item>
13 |       <el-form-item label="Activity time">
14 |         <el-col :span="11">
15 |           <el-date-picker v-model="form.date1" type="date" placeholder="Pick a date" style="width: 100%;" />
16 |         </el-col>
17 |         <el-col :span="2" class="line">-</el-col>
18 |         <el-col :span="11">
19 |           <el-time-picker v-model="form.date2" type="fixed-time" placeholder="Pick a time" style="width: 100%;" />
20 |         </el-col>
21 |       </el-form-item>
22 |       <el-form-item label="Instant delivery">
23 |         <el-switch v-model="form.delivery" />
24 |       </el-form-item>
25 |       <el-form-item label="Activity type">
26 |         <el-checkbox-group v-model="form.type">
27 |           <el-checkbox label="Online activities" name="type" />
28 |           <el-checkbox label="Promotion activities" name="type" />
29 |           <el-checkbox label="Offline activities" name="type" />
30 |           <el-checkbox label="Simple brand exposure" name="type" />
31 |         </el-checkbox-group>
32 |       </el-form-item>
33 |       <el-form-item label="Resources">
34 |         <el-radio-group v-model="form.resource">
35 |           <el-radio label="Sponsor" />
36 |           <el-radio label="Venue" />
37 |         </el-radio-group>
38 |       </el-form-item>
39 |       <el-form-item label="Activity form">
40 |         <el-input v-model="form.desc" type="textarea" />
41 |       </el-form-item>
42 |       <el-form-item>
43 |         <el-button type="primary" @click="onSubmit">Create</el-button>
44 |         <el-button @click="onCancel">Cancel</el-button>
45 |       </el-form-item>
46 |     </el-form>
47 |   </div>
48 | </template>
49 | 
50 | <script>
51 | export default {
52 |   data() {
53 |     return {
54 |       form: {
55 |         name: '',
56 |         region: '',
57 |         date1: '',
58 |         date2: '',
59 |         delivery: false,
60 |         type: [],
61 |         resource: '',
62 |         desc: ''
63 |       }
64 |     }
65 |   },
66 |   methods: {
67 |     onSubmit() {
68 |       this.$message('submit!')
69 |     },
70 |     onCancel() {
71 |       this.$message({
72 |         message: 'cancel!',
73 |         type: 'warning'
74 |       })
75 |     }
76 |   }
77 | }
78 | </script>
79 | 
80 | <style scoped>
81 | .line{
82 |   text-align: center;
83 | }
84 | </style>
85 | 
86 | 


--------------------------------------------------------------------------------
/frontend/src/views/nested/menu1/index.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <div style="padding:30px;">
3 |     <el-alert :closable="false" title="menu 1">
4 |       <router-view />
5 |     </el-alert>
6 |   </div>
7 | </template>
8 | 


--------------------------------------------------------------------------------
/frontend/src/views/nested/menu1/menu1-1/index.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <div style="padding:30px;">
3 |     <el-alert :closable="false" title="menu 1-1" type="success">
4 |       <router-view />
5 |     </el-alert>
6 |   </div>
7 | </template>
8 | 


--------------------------------------------------------------------------------
/frontend/src/views/nested/menu1/menu1-2/index.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <div style="padding:30px;">
3 |     <el-alert :closable="false" title="menu 1-2" type="success">
4 |       <router-view />
5 |     </el-alert>
6 |   </div>
7 | </template>
8 | 


--------------------------------------------------------------------------------
/frontend/src/views/nested/menu1/menu1-2/menu1-2-1/index.vue:
--------------------------------------------------------------------------------
1 | <template functional>
2 |   <div style="padding:30px;">
3 |     <el-alert :closable="false" title="menu 1-2-1" type="warning" />
4 |   </div>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/frontend/src/views/nested/menu1/menu1-2/menu1-2-2/index.vue:
--------------------------------------------------------------------------------
1 | <template functional>
2 |   <div style="padding:30px;">
3 |     <el-alert :closable="false" title="menu 1-2-2" type="warning" />
4 |   </div>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/frontend/src/views/nested/menu1/menu1-3/index.vue:
--------------------------------------------------------------------------------
1 | <template functional>
2 |   <div style="padding:30px;">
3 |     <el-alert :closable="false" title="menu 1-3" type="success" />
4 |   </div>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/frontend/src/views/nested/menu2/index.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <div style="padding:30px;">
3 |     <el-alert :closable="false" title="menu 2" />
4 |   </div>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/frontend/src/views/tree/index.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div class="app-container">
 3 |     <el-input v-model="filterText" placeholder="Filter keyword" style="margin-bottom:30px;" />
 4 | 
 5 |     <el-tree
 6 |       ref="tree2"
 7 |       :data="data2"
 8 |       :props="defaultProps"
 9 |       :filter-node-method="filterNode"
10 |       class="filter-tree"
11 |       default-expand-all
12 |     />
13 | 
14 |   </div>
15 | </template>
16 | 
17 | <script>
18 | export default {
19 | 
20 |   data() {
21 |     return {
22 |       filterText: '',
23 |       data2: [{
24 |         id: 1,
25 |         label: 'Level one 1',
26 |         children: [{
27 |           id: 4,
28 |           label: 'Level two 1-1',
29 |           children: [{
30 |             id: 9,
31 |             label: 'Level three 1-1-1'
32 |           }, {
33 |             id: 10,
34 |             label: 'Level three 1-1-2'
35 |           }]
36 |         }]
37 |       }, {
38 |         id: 2,
39 |         label: 'Level one 2',
40 |         children: [{
41 |           id: 5,
42 |           label: 'Level two 2-1'
43 |         }, {
44 |           id: 6,
45 |           label: 'Level two 2-2'
46 |         }]
47 |       }, {
48 |         id: 3,
49 |         label: 'Level one 3',
50 |         children: [{
51 |           id: 7,
52 |           label: 'Level two 3-1'
53 |         }, {
54 |           id: 8,
55 |           label: 'Level two 3-2'
56 |         }]
57 |       }],
58 |       defaultProps: {
59 |         children: 'children',
60 |         label: 'label'
61 |       }
62 |     }
63 |   },
64 |   watch: {
65 |     filterText(val) {
66 |       this.$refs.tree2.filter(val)
67 |     }
68 |   },
69 | 
70 |   methods: {
71 |     filterNode(value, data) {
72 |       if (!value) return true
73 |       return data.label.indexOf(value) !== -1
74 |     }
75 |   }
76 | }
77 | </script>
78 | 
79 | 


--------------------------------------------------------------------------------
/frontend/tests/unit/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |   env: {
3 |     jest: true
4 |   }
5 | }
6 | 


--------------------------------------------------------------------------------
/frontend/tests/unit/components/Breadcrumb.spec.js:
--------------------------------------------------------------------------------
 1 | import { mount, createLocalVue } from '@vue/test-utils'
 2 | import VueRouter from 'vue-router'
 3 | import ElementUI from 'element-ui'
 4 | import Breadcrumb from '@/components/Breadcrumb/index.vue'
 5 | 
 6 | const localVue = createLocalVue()
 7 | localVue.use(VueRouter)
 8 | localVue.use(ElementUI)
 9 | 
10 | const routes = [
11 |   {
12 |     path: '/',
13 |     name: 'home',
14 |     children: [{
15 |       path: 'dashboard',
16 |       name: 'dashboard'
17 |     }]
18 |   },
19 |   {
20 |     path: '/menu',
21 |     name: 'menu',
22 |     children: [{
23 |       path: 'menu1',
24 |       name: 'menu1',
25 |       meta: { title: 'menu1' },
26 |       children: [{
27 |         path: 'menu1-1',
28 |         name: 'menu1-1',
29 |         meta: { title: 'menu1-1' }
30 |       },
31 |       {
32 |         path: 'menu1-2',
33 |         name: 'menu1-2',
34 |         redirect: 'noredirect',
35 |         meta: { title: 'menu1-2' },
36 |         children: [{
37 |           path: 'menu1-2-1',
38 |           name: 'menu1-2-1',
39 |           meta: { title: 'menu1-2-1' }
40 |         },
41 |         {
42 |           path: 'menu1-2-2',
43 |           name: 'menu1-2-2'
44 |         }]
45 |       }]
46 |     }]
47 |   }]
48 | 
49 | const router = new VueRouter({
50 |   routes
51 | })
52 | 
53 | describe('Breadcrumb.vue', () => {
54 |   const wrapper = mount(Breadcrumb, {
55 |     localVue,
56 |     router
57 |   })
58 |   it('dashboard', () => {
59 |     router.push('/dashboard')
60 |     const len = wrapper.findAll('.el-breadcrumb__inner').length
61 |     expect(len).toBe(1)
62 |   })
63 |   it('normal route', () => {
64 |     router.push('/menu/menu1')
65 |     const len = wrapper.findAll('.el-breadcrumb__inner').length
66 |     expect(len).toBe(2)
67 |   })
68 |   it('nested route', () => {
69 |     router.push('/menu/menu1/menu1-2/menu1-2-1')
70 |     const len = wrapper.findAll('.el-breadcrumb__inner').length
71 |     expect(len).toBe(4)
72 |   })
73 |   it('no meta.title', () => {
74 |     router.push('/menu/menu1/menu1-2/menu1-2-2')
75 |     const len = wrapper.findAll('.el-breadcrumb__inner').length
76 |     expect(len).toBe(3)
77 |   })
78 |   // it('click link', () => {
79 |   //   router.push('/menu/menu1/menu1-2/menu1-2-2')
80 |   //   const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
81 |   //   const second = breadcrumbArray.at(1)
82 |   //   console.log(breadcrumbArray)
83 |   //   const href = second.find('a').attributes().href
84 |   //   expect(href).toBe('#/menu/menu1')
85 |   // })
86 |   // it('noRedirect', () => {
87 |   //   router.push('/menu/menu1/menu1-2/menu1-2-1')
88 |   //   const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
89 |   //   const redirectBreadcrumb = breadcrumbArray.at(2)
90 |   //   expect(redirectBreadcrumb.contains('a')).toBe(false)
91 |   // })
92 |   it('last breadcrumb', () => {
93 |     router.push('/menu/menu1/menu1-2/menu1-2-1')
94 |     const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
95 |     const redirectBreadcrumb = breadcrumbArray.at(3)
96 |     expect(redirectBreadcrumb.contains('a')).toBe(false)
97 |   })
98 | })
99 | 


--------------------------------------------------------------------------------
/frontend/tests/unit/components/Hamburger.spec.js:
--------------------------------------------------------------------------------
 1 | import { shallowMount } from '@vue/test-utils'
 2 | import Hamburger from '@/components/Hamburger/index.vue'
 3 | describe('Hamburger.vue', () => {
 4 |   it('toggle click', () => {
 5 |     const wrapper = shallowMount(Hamburger)
 6 |     const mockFn = jest.fn()
 7 |     wrapper.vm.$on('toggleClick', mockFn)
 8 |     wrapper.find('.hamburger').trigger('click')
 9 |     expect(mockFn).toBeCalled()
10 |   })
11 |   it('prop isActive', () => {
12 |     const wrapper = shallowMount(Hamburger)
13 |     wrapper.setProps({ isActive: true })
14 |     expect(wrapper.contains('.is-active')).toBe(true)
15 |     wrapper.setProps({ isActive: false })
16 |     expect(wrapper.contains('.is-active')).toBe(false)
17 |   })
18 | })
19 | 


--------------------------------------------------------------------------------
/frontend/tests/unit/components/SvgIcon.spec.js:
--------------------------------------------------------------------------------
 1 | import { shallowMount } from '@vue/test-utils'
 2 | import SvgIcon from '@/components/SvgIcon/index.vue'
 3 | describe('SvgIcon.vue', () => {
 4 |   it('iconClass', () => {
 5 |     const wrapper = shallowMount(SvgIcon, {
 6 |       propsData: {
 7 |         iconClass: 'test'
 8 |       }
 9 |     })
10 |     expect(wrapper.find('use').attributes().href).toBe('#icon-test')
11 |   })
12 |   it('className', () => {
13 |     const wrapper = shallowMount(SvgIcon, {
14 |       propsData: {
15 |         iconClass: 'test'
16 |       }
17 |     })
18 |     expect(wrapper.classes().length).toBe(1)
19 |     wrapper.setProps({ className: 'test' })
20 |     expect(wrapper.classes().includes('test')).toBe(true)
21 |   })
22 | })
23 | 


--------------------------------------------------------------------------------
/frontend/tests/unit/utils/formatTime.spec.js:
--------------------------------------------------------------------------------
 1 | import { formatTime } from '@/utils/index.js'
 2 | 
 3 | describe('Utils:formatTime', () => {
 4 |   const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
 5 |   const retrofit = 5 * 1000
 6 | 
 7 |   it('ten digits timestamp', () => {
 8 |     expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分')
 9 |   })
10 |   it('test now', () => {
11 |     expect(formatTime(+new Date() - 1)).toBe('刚刚')
12 |   })
13 |   it('less two minute', () => {
14 |     expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前')
15 |   })
16 |   it('less two hour', () => {
17 |     expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前')
18 |   })
19 |   it('less one day', () => {
20 |     expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前')
21 |   })
22 |   it('more than one day', () => {
23 |     expect(formatTime(d)).toBe('7月13日17时54分')
24 |   })
25 |   it('format', () => {
26 |     expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
27 |     expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
28 |     expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
29 |   })
30 | })
31 | 


--------------------------------------------------------------------------------
/frontend/tests/unit/utils/param2Obj.spec.js:
--------------------------------------------------------------------------------
 1 | import { param2Obj } from '@/utils/index.js'
 2 | describe('Utils:param2Obj', () => {
 3 |   const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95'
 4 | 
 5 |   it('param2Obj test', () => {
 6 |     expect(param2Obj(url)).toEqual({
 7 |       name: 'bill',
 8 |       age: '29',
 9 |       sex: '1',
10 |       field: window.btoa('test'),
11 |       key: '测试'
12 |     })
13 |   })
14 | })
15 | 


--------------------------------------------------------------------------------
/frontend/tests/unit/utils/parseTime.spec.js:
--------------------------------------------------------------------------------
 1 | import { parseTime } from '@/utils/index.js'
 2 | 
 3 | describe('Utils:parseTime', () => {
 4 |   const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
 5 |   it('timestamp', () => {
 6 |     expect(parseTime(d)).toBe('2018-07-13 17:54:01')
 7 |   })
 8 |   it('timestamp string', () => {
 9 |     expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01')
10 |   })
11 |   it('ten digits timestamp', () => {
12 |     expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01')
13 |   })
14 |   it('new Date', () => {
15 |     expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01')
16 |   })
17 |   it('format', () => {
18 |     expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
19 |     expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
20 |     expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
21 |   })
22 |   it('get the day of the week', () => {
23 |     expect(parseTime(d, '{a}')).toBe('五') // 星期五
24 |   })
25 |   it('get the day of the week', () => {
26 |     expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日
27 |   })
28 |   it('empty argument', () => {
29 |     expect(parseTime()).toBeNull()
30 |   })
31 | 
32 |   it('null', () => {
33 |     expect(parseTime(null)).toBeNull()
34 |   })
35 | })
36 | 


--------------------------------------------------------------------------------
/frontend/tests/unit/utils/validate.spec.js:
--------------------------------------------------------------------------------
 1 | import { validUsername, isExternal } from '@/utils/validate.js'
 2 | 
 3 | describe('Utils:validate', () => {
 4 |   it('validUsername', () => {
 5 |     expect(validUsername('admin')).toBe(true)
 6 |     expect(validUsername('editor')).toBe(true)
 7 |     expect(validUsername('xxxx')).toBe(false)
 8 |   })
 9 |   it('isExternal', () => {
10 |     expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true)
11 |     expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true)
12 |     expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false)
13 |     expect(isExternal('/dashboard')).toBe(false)
14 |     expect(isExternal('./dashboard')).toBe(false)
15 |     expect(isExternal('dashboard')).toBe(false)
16 |   })
17 | })
18 | 


--------------------------------------------------------------------------------