├── .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 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="",d=0,e=a.length;e>d;d++)c+="<"+b+">"+a[d]+"";return c+""},j='';f.header&&(j+=""+i(h.header,"th")+""),j+="";for(var k=0,l=h.body.length;l>k;k++)j+=i(h.body[k],"td");j+="",f.footer&&(j+=""+i(h.footer,"th")+"");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=""+n+"";a("style, link").each(function(){o+=g(this)}),a(m.document.head).html(o),a(m.document.body).html("

"+n+"

"+f.message+"
"+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="…",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=$("
  • ",{"class":j.sPageButton+" "+h,"aria-controls":a.sTableId,tabindex:a.iTabIndex,id:0===c&&"string"==typeof p?a.sTableId+"_"+p:null}).append($("",{href:"#"}).html(g)).appendTo(b),a.oApi._fnBindAction(o,{action:p},q))}};l($(b).empty().html(' 59 | 60 | 63 | 64 | {% end %} -------------------------------------------------------------------------------- /backend/web/templates/common/meta.html: -------------------------------------------------------------------------------- 1 | {% block meta %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 68 | 71 | {% end %} -------------------------------------------------------------------------------- /backend/web/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/default.html" %} 2 | 3 | 4 | {% block main_content %} 5 | 6 |

    开源Python全栈股票系统,数据抓取、统计分析、报表展示。

    7 | 8 |
    9 |
    10 |

    基础库版本

    11 |

    1,pandas使用【 {{ pandasVersion }} 】版本, 中文文档

    12 |

    2,numpy使用【 {{ numpyVersion }} 】版本, 中文文档

    13 |

    3,akshare使用【 {{ akshareVersion }} 】版本, 中文文档

    14 |

    4,bokeh使用【 {{ bokehVersion }} 】版本, 官方文档

    15 |

    5,stockstats使用【 {{ stockstatsVersion }} 】版本, 官方文档

    16 |
    17 |
    18 | 19 |
    20 |
    21 |

    相关资料信息

    22 |

    1,github项目地址。pythonstock

    23 |

    2,博客地址。博客地址

    24 |
    25 |
    26 | 27 |
    28 |
    29 |

    2021年9月20日更新,发布2.0版本

    30 |

    1,修复bokeh的版本升级问题,可以显示趋势数据了。

    31 |

    2,使用 stock_zh_ah_name 做每日数据。

    32 |

    3,AkShare 升级到 1.1.9 版本。

    33 |
    34 |
    35 | 36 |
    37 |
    38 |

    2021年8月31日更新job服务

    39 |

    1,过滤包括:600,6006,601,000,001,002,且不包括ST的股票数据。。

    40 |

    2,使用 stock_zh_ah_name 做每日数据。

    41 |

    3,AkShare 升级到 1.0.80 版本。

    42 | 43 |
    44 |
    45 | 46 |
    47 |
    48 |

    2021年6月3日使用 AkShare 做数据抓取服务

    49 |

    1,使用 stock_zh_a_spot 做实时行情数据。

    50 |

    2,使用 stock_zh_a_daily 做历史数据统计。

    51 |

    3,升级基础镜像使用python3.7,AkShare 的 0.9.65 版本。

    52 | 53 |
    54 |
    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 | 2 | 3 | 4 | 5 | 6 | 股票系统 7 | {% block meta %}{% end %} 8 | 9 | 10 | 11 |
    12 |
    13 |
    14 |
    15 |
    16 | {% block main_content %}{% end %} 17 |
    18 |
    19 |
    20 |
    21 |
    22 | 23 | 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 | 2 | 3 | 4 | 5 | 6 | 股票系统 {{ pythonStockVersion }} 7 | {% block meta %}{% end %} 8 | 9 | 10 | 11 | 12 | {% block header %}{% end %} 13 | 14 |
    15 | 18 | 19 | {% block left_menu %}{% end %} 20 | 21 |
    22 |
    23 |
    24 |
    25 |
    26 | {% block main_content %}{% end %} 27 |
    28 |
    29 |
    30 |
    31 |
    32 | 33 | 34 | 35 | 36 |
    37 | 38 | 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 | 2 | 3 | 4 | 5 | 6 | {% block meta %}{% end %} 7 | 8 | 9 | 10 | 11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 | {% block main_content %}{% end %} 18 |
    19 |
    20 |
    21 |
    22 |
    23 | 24 | 25 | 26 | 27 |
    28 | 29 | 30 | -------------------------------------------------------------------------------- /backend/web/templates/minst_serving.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/single_default.html" %} 2 | 3 | 4 | {% block main_content %} 5 | 6 | 7 |

    手写图片识别演示

    8 | 9 |
    10 |
    11 | your browser don't support canvas! 12 |
    13 | 14 | 15 |
    16 |
    17 |
    18 |

    result:

    19 |

    20 |
    21 |
    22 | 23 |

    测试图片识别演示

    24 |
    25 | {% for image in image_array %} 26 |
    27 | 28 |
    29 | {% end %} 30 |
    31 |
    32 |

    结果:

    33 |

    "-1"

    34 |
    35 | 36 | 37 | 44 | 80 | {% end %} -------------------------------------------------------------------------------- /backend/web/templates/stock_chart.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/default.html" %} 2 | 3 | 4 | {% block main_content %} 5 | 6 |

    欢迎使用股票系统。

    7 | 8 |
    9 |
    10 | 11 |
    12 | {% end %} -------------------------------------------------------------------------------- /backend/web/templates/stock_indicators.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/indicators.html" %} 2 | 3 | 4 | {% block main_content %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | {% for index,element in enumerate(comp_list) %} 24 |

    {{ element["title"] }}

    25 |
    {{ element["desc"] }}
    26 |
    27 | {% raw element["div"] %} 28 | {% raw element["script"] %} 29 |
    30 | {% end %} 31 | 32 | {% end %} -------------------------------------------------------------------------------- /backend/web/templates/test.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/default.html" %} 2 | 3 | 4 | {% block main_content %} 5 | 6 |

    欢迎使用股票系统。

    7 | 8 |
    9 |
    10 |
    11 | 12 |
    13 | 17 |
    18 | 19 | 20 | 21 | 22 | 34 | 35 | 51 | 52 | {% end %} -------------------------------------------------------------------------------- /backend/web/templates/test2.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/default.html" %} 2 | 3 | 4 | {% block main_content %} 5 | 6 |

    欢迎使用股票系统。

    7 | 8 |
    9 |
    10 |
    11 | 12 |
    13 | 17 |
    18 | 19 | 20 | 21 | 22 | 34 | 35 | 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 | 17 | 18 | 19 | 20 | 21 | 22 |

    任务测试


    23 | 24 | 25 | 26 | 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 | 17 | 18 | 19 | 20 | 21 | 22 |

    任务测试


    23 | 24 | 25 | 26 | 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$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/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: '/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 ", 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 | 3 | 404 Not Found 4 | 5 |

    404 Not Found

    6 |
    nginx/1.26.3
    7 | 8 | -------------------------------------------------------------------------------- /frontend/public/50x.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Error 5 | 10 | 11 | 12 |

    An error occurred.

    13 |

    Sorry, the page you are looking for is currently unavailable.
    14 | Please try again later.

    15 |

    If you are the system administrator of this resource then you should check 16 | the error log for details.

    17 |

    Faithfully yours, nginx.

    18 | 19 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= webpackConfig.name %> 9 | 10 | 11 | 14 |
    15 | 16 | 17 | 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 | 6 | 7 | 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 | 11 | 12 | 65 | 66 | 79 | -------------------------------------------------------------------------------- /frontend/src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /frontend/src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 92 | 93 | 102 | -------------------------------------------------------------------------------- /frontend/src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 47 | 48 | 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 | * ... 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 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | 8 | 9 | 19 | 20 | 32 | 33 | 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 | 34 | 35 | 42 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | 34 | 83 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 96 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 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 | 13 | 14 | 52 | 53 | 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 | 56 | 57 | 93 | 94 | -------------------------------------------------------------------------------- /frontend/src/views/form/index.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 79 | 80 | 85 | 86 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/menu1-1/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/menu1-2/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/menu1-2/menu1-2-1/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/menu1-2/menu1-2-2/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/menu1-3/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu2/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /frontend/src/views/tree/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 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 | --------------------------------------------------------------------------------