├── .gitignore ├── LICENSE ├── README.md ├── backend ├── docs │ ├── 1-todo-2020-12-06.md │ └── git-push-tag.md ├── jobs │ ├── 18h_daily_job.py │ ├── README.txt │ ├── aps_job.py │ ├── basic_job.py │ ├── cron.daily │ │ └── run_daily │ ├── cron.hourly │ │ └── run_hourly │ ├── cron.minutely │ │ └── run_1minute │ ├── cron.monthly │ │ └── run_monthly │ ├── crontab │ ├── daily_job.py │ ├── guess_indicators_daily_buy_job.py │ ├── guess_indicators_daily_job.py │ ├── guess_indicators_daily_sell_job.py │ ├── guess_rsrs_daily_job.py │ ├── quarter_job.py │ ├── restart_mnist_serving.sh │ ├── restart_web.sh │ ├── run_cron.sh │ ├── run_init.sh │ ├── run_jupyter.sh │ ├── run_web.sh │ ├── start_mariadb.sh │ └── test_akshare │ │ ├── test_stock_zh_a_daily.py │ │ ├── test_stock_zh_a_spot.py │ │ └── test_stock_zh_index_spot.py ├── libs │ ├── common.py │ ├── stock_web_dic.py │ └── stock_web_dic.py.bk ├── old_jobs │ ├── README.md │ ├── guess_indicators_lite_buy_daily_job.py │ ├── guess_indicators_lite_sell_daily_job.py │ ├── guess_period_daily_job.py │ ├── guess_return_daily_job.py │ └── guess_sklearn_ma_daily_job.py ├── supervisor │ ├── example_supervisord_conf │ └── supervisord.conf └── web │ ├── README.md │ ├── base.py │ ├── chartHandler.py │ ├── dataEditorHandler.py │ ├── dataIndicatorsHandler.py │ ├── dataTableHandler.py │ ├── demo-chart.py │ ├── main.py │ ├── minstServingHandler.py │ ├── static │ ├── css │ │ ├── ace.min.css │ │ ├── bokeh-tables.min.css │ │ ├── bokeh-widgets.min.css │ │ ├── bokeh.min.css │ │ ├── bootstrap-colorpicker.min.css │ │ ├── bootstrap-datepicker3.min.css │ │ ├── bootstrap-datetimepicker.min.css │ │ ├── bootstrap-timepicker.min.css │ │ ├── bootstrap.min.css │ │ ├── buttons.dataTables.min.css │ │ ├── chosen.min.css │ │ ├── daterangepicker.min.css │ │ ├── editor.dataTables.min.css │ │ ├── font-awesome.min.css │ │ ├── fonts.googleapis.com.css │ │ ├── jquery-ui.custom.min.css │ │ ├── jquery-ui.min.css │ │ └── select.dataTables.min.css │ ├── font-awesome │ │ ├── 4.5.0 │ │ │ ├── css │ │ │ │ └── font-awesome.min.css │ │ │ └── fonts │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ ├── fontawesome-webfont.woff │ │ │ │ └── fontawesome-webfont.woff2 │ │ └── opensans │ │ │ └── v13 │ │ │ ├── DXI1ORHCpsQm3Vp6mXoaTXhCUOGz7vYGh680lGh-uXM.woff │ │ │ └── cJZKeOuBrn4kERxqtaUH3T8E0i7KZn-EPnyo3HZu7kw.woff │ ├── img │ │ ├── diff-n-bokeh.png │ │ ├── stock-show-01.jpg │ │ ├── stock2-001.png │ │ ├── stock2-002.png │ │ ├── stock2-003.png │ │ └── 支付宝--微信支付.jpg │ ├── js │ │ ├── ace-elements.min.js │ │ ├── ace-extra.min.js │ │ ├── ace.min.js │ │ ├── autosize.min.js │ │ ├── bokeh-api.min.js │ │ ├── bokeh-gl.min.js │ │ ├── bokeh-tables.min.js │ │ ├── bokeh-widgets.min.js │ │ ├── bokeh.min.js │ │ ├── bootbox.js │ │ ├── bootstrap-datepicker.min.js │ │ ├── bootstrap-datepicker.zh-CN.js │ │ ├── bootstrap-datetimepicker.min.js │ │ ├── bootstrap-timepicker.min.js │ │ ├── bootstrap.min.js │ │ ├── buttons.colVis.min.js │ │ ├── buttons.html5.min.js │ │ ├── buttons.print.min.js │ │ ├── chosen.jquery.min.js │ │ ├── dataTables.buttons.min.js │ │ ├── dataTables.editor.min.js │ │ ├── dataTables.select.min.js │ │ ├── datatables.Chinese.json │ │ ├── daterangepicker.min.js │ │ ├── draw.js │ │ ├── grid.locale-en.js │ │ ├── holder.min.js │ │ ├── jquery-2.1.4.min.js │ │ ├── jquery-ui.custom.min.js │ │ ├── jquery-ui.min.js │ │ ├── jquery.colorbox.min.js │ │ ├── jquery.dataTables.bootstrap.min.js │ │ ├── jquery.dataTables.min.js │ │ ├── jquery.validate.min.js │ │ └── moment.min.js │ └── update_bokeh.sh │ ├── templates │ ├── bokeh_embed.html │ ├── common │ │ ├── footer.html │ │ ├── header.html │ │ ├── left_menu.html │ │ └── meta.html │ ├── data_editor.html │ ├── index.html │ ├── layout │ │ ├── default.html │ │ ├── indicators-main.html │ │ ├── indicators.html │ │ ├── main.html │ │ ├── single_default.html │ │ └── single_main.html │ ├── minst_serving.html │ ├── stock_chart.html │ ├── stock_indicators.html │ ├── stock_web.html │ ├── test.html │ └── test2.html │ ├── test_thread.py │ ├── test_thread_v2.py │ └── tornado_bokeh_embed.py ├── docker-compose ├── .gitignore ├── LICENSE ├── README.md ├── build_stock.sh ├── dev-docker-compose-restart.sh ├── dev-docker-compose.yml ├── docker-compose.yml ├── docker │ ├── DevBackendDockerfile │ ├── DevFrontendDockerfile │ ├── Dockerfile │ ├── ProdBackendDockerfile │ ├── ProdFrontendDockerfile │ ├── README.md │ └── build.sh ├── mysql │ ├── init.sql │ └── my.cnf ├── nginx.conf └── nginx │ └── nginx.conf └── frontend ├── .eslintignore ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── docker-build.sh ├── docker-entrypoint.sh ├── jest.config.js ├── jsconfig.json ├── mock ├── index.js ├── mock-server.js ├── table.js ├── user.js └── utils.js ├── package.json ├── postcss.config.js ├── public ├── 40x.html ├── 50x.html ├── favicon.ico ├── freewebsys-logo.jpg ├── index.html ├── stock-001.png ├── stock-002.png └── stock-003.png ├── src ├── App.vue ├── api │ ├── article.js │ ├── menu.js │ ├── package.js │ ├── table.js │ └── user.js ├── assets │ └── 404_images │ │ ├── 404.png │ │ └── 404_cloud.png ├── components │ ├── Breadcrumb │ │ └── index.vue │ ├── Hamburger │ │ └── index.vue │ ├── Pagination │ │ └── index.vue │ └── SvgIcon │ │ └── index.vue ├── directive │ ├── el-table │ │ ├── adaptive.js │ │ └── index.js │ └── waves │ │ ├── index.js │ │ ├── waves.css │ │ └── waves.js ├── icons │ ├── index.js │ ├── svg │ │ ├── dashboard.svg │ │ ├── example.svg │ │ ├── eye-open.svg │ │ ├── eye.svg │ │ ├── form.svg │ │ ├── link.svg │ │ ├── nested.svg │ │ ├── password.svg │ │ ├── table.svg │ │ ├── tree.svg │ │ └── user.svg │ └── svgo.yml ├── layout │ ├── components │ │ ├── AppMain.vue │ │ ├── Navbar.vue │ │ ├── Sidebar │ │ │ ├── FixiOSBug.js │ │ │ ├── Item.vue │ │ │ ├── Link.vue │ │ │ ├── Logo.vue │ │ │ ├── SidebarItem.vue │ │ │ └── index.vue │ │ └── index.js │ ├── index.vue │ └── mixin │ │ └── ResizeHandler.js ├── main.js ├── permission.js ├── router │ └── index.js ├── settings.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── app.js │ │ ├── settings.js │ │ └── user.js ├── styles │ ├── element-ui.scss │ ├── index.scss │ ├── mixin.scss │ ├── sidebar.scss │ ├── transition.scss │ └── variables.scss ├── utils │ ├── auth.js │ ├── get-page-title.js │ ├── index.js │ ├── request.js │ ├── scroll-to.js │ └── validate.js ├── vendor │ └── Export2Excel.js └── views │ ├── 404.vue │ ├── dashboard │ └── index.vue │ ├── form │ └── index.vue │ ├── login │ └── index.vue │ ├── nested │ ├── menu1 │ │ ├── index.vue │ │ ├── menu1-1 │ │ │ └── index.vue │ │ ├── menu1-2 │ │ │ ├── index.vue │ │ │ ├── menu1-2-1 │ │ │ │ └── index.vue │ │ │ └── menu1-2-2 │ │ │ │ └── index.vue │ │ └── menu1-3 │ │ │ └── index.vue │ └── menu2 │ │ └── index.vue │ ├── table │ ├── complex-table.vue │ └── index.vue │ └── tree │ └── index.vue ├── tests └── unit │ ├── .eslintrc.js │ ├── components │ ├── Breadcrumb.spec.js │ ├── Hamburger.spec.js │ └── SvgIcon.spec.js │ └── utils │ ├── formatTime.spec.js │ ├── param2Obj.spec.js │ ├── parseTime.spec.js │ └── validate.spec.js └── vue.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | data 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | notebooks 31 | nohup.out 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test_akshare / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # dotenv 88 | .env 89 | 90 | # virtualenv 91 | .venv 92 | venv/ 93 | ENV/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | .idea 109 | *.iml 110 | .DS_Store 111 | *.zip 112 | *.log 113 | *.pyc 114 | doc 115 | /bin 116 | pkg 117 | *.tmp -------------------------------------------------------------------------------- /backend/docs/git-push-tag.md: -------------------------------------------------------------------------------- 1 | 2 | ## 创建 tag 并发布到 github 上 3 | 4 | 5 | git tag -a v2.0 -m "v2.0" 6 | 7 | git push origin --tags 8 | -------------------------------------------------------------------------------- /backend/jobs/README.txt: -------------------------------------------------------------------------------- 1 | 1,计算每日买全部推荐买。 2 | 2,计算每日全部推荐卖数据。 3 | 3,设置个人账号,设置购买和卖的数据。进行关联查询。 4 | 4,最重要的沪深300,中正500数据。进行大盘股分析。 -------------------------------------------------------------------------------- /backend/jobs/aps_job.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # -*- coding: utf-8 -*- 3 | from pytz import utc 4 | from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore 5 | from apscheduler.schedulers.blocking import BlockingScheduler 6 | 7 | from apscheduler.executors.pool import ProcessPoolExecutor 8 | import libs.common as common 9 | 10 | # doc : http://apscheduler.readthedocs.io/en/latest/modules/jobstores/sqlalchemy.html 11 | jobstores = { 12 | 'default': SQLAlchemyJobStore(url=common.MYSQL_CONN_URL, tablename='apscheduler_jobs') 13 | } 14 | executors = { 15 | 'default': {'type': 'threadpool', 'max_workers': 20}, 16 | 'processpool': ProcessPoolExecutor(max_workers=5) 17 | } 18 | job_defaults = { 19 | 'coalesce': False, 20 | 'max_instances': 3 21 | } 22 | scheduler = BlockingScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults, timezone=utc) 23 | scheduler.start() 24 | print("start ...") 25 | -------------------------------------------------------------------------------- /backend/jobs/basic_job.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import libs.common as common 5 | import MySQLdb 6 | 7 | # 创建新数据库。 8 | def create_new_database(): 9 | with MySQLdb.connect(common.MYSQL_HOST, common.MYSQL_USER, common.MYSQL_PWD, "mysql", charset="utf8") as db: 10 | try: 11 | create_sql = " CREATE DATABASE IF NOT EXISTS %s CHARACTER SET utf8 COLLATE utf8_general_ci " % common.MYSQL_DB 12 | print(create_sql) 13 | db.autocommit(on=True) 14 | db.cursor().execute(create_sql) 15 | except Exception as e: 16 | print("error CREATE DATABASE :", e) 17 | 18 | 19 | # main函数入口 20 | if __name__ == '__main__': 21 | 22 | # 检查,如果执行 select 1 失败,说明数据库不存在,然后创建一个新的数据库。 23 | try: 24 | with MySQLdb.connect(common.MYSQL_HOST, common.MYSQL_USER, common.MYSQL_PWD, common.MYSQL_DB, 25 | charset="utf8") as db: 26 | db.autocommit(on=True) 27 | db.cursor().execute(" select 1 ") 28 | print("########### db exists ###########") 29 | except Exception as e: 30 | print("check MYSQL_DB error and create new one :", e) 31 | # 检查数据库失败, 32 | create_new_database() 33 | # 执行数据初始化。 34 | -------------------------------------------------------------------------------- /backend/jobs/cron.daily/run_daily: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p /data/logs 4 | DATETIME=`date +%Y-%m-%d:%H:%M:%S` 5 | 6 | DATE=`date +%Y-%m-%d` 7 | 8 | export PYTHONIOENCODING=utf-8 9 | export LANG=zh_CN.UTF-8 10 | export PYTHONPATH=/data/stock 11 | export LC_CTYPE=zh_CN.UTF-8 12 | 13 | 14 | echo "###################"$DATETIME"###################" >> /data/logs/daily.${DATE}.log 15 | #增加获得今日全部数据和大盘数据 16 | /usr/local/bin/python3 /data/stock/jobs/18h_daily_job.py >> /data/logs/daily.${DATE}.log 17 | 18 | 19 | echo "###################"$DATETIME"###################" >> /data/logs/daily.${DATE}.log 20 | #使用股票指标预测。 21 | /usr/local/bin/python3 /data/stock/jobs/guess_indicators_daily_job.py >> /data/logs/daily.${DATE}.log 22 | /usr/local/bin/python3 /data/stock/jobs/guess_indicators_daily_buy_job.py >> /data/logs/daily.${DATE}.log 23 | 24 | #清除前3天数据。 25 | DATE_20=`date -d '-20 days' +%Y-%m-%d` 26 | MONTH_20=`date -d '-20 days' +%Y-%m` 27 | echo "rm -f /data/cache/hist_data_cache/${MONTH_20}/${DATETIME_20}" 28 | rm -f /data/cache/hist_data_cache/${MONTH_20}/${DATETIME_20} -------------------------------------------------------------------------------- /backend/jobs/cron.hourly/run_hourly: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p /data/logs 4 | DATE=`date +%Y-%m-%d:%H:%M:%S` 5 | echo $DATE >> /data/logs/hourly.log 6 | 7 | -------------------------------------------------------------------------------- /backend/jobs/cron.minutely/run_1minute: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p /data/logs 4 | DATE=`date +%Y-%m-%d:%H:%M:%S` 5 | echo $DATE >> /data/logs/1min.log 6 | -------------------------------------------------------------------------------- /backend/jobs/cron.monthly/run_monthly: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p /data/logs 4 | DATE=`date +%Y-%m-%d:%H:%M:%S` 5 | echo $DATE >> /data/logs/monthly.log 6 | 7 | -------------------------------------------------------------------------------- /backend/jobs/crontab: -------------------------------------------------------------------------------- 1 | SHELL=/bin/sh 2 | PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin 3 | */1 * * * * /bin/run-parts /etc/cron.minutely 4 | 10 * * * * /bin/run-parts /etc/cron.hourly 5 | 30 16 * * * /bin/run-parts /etc/cron.daily 6 | 30 17 1,10,20 * * /bin/run-parts /etc/cron.monthly 7 | -------------------------------------------------------------------------------- /backend/jobs/daily_job.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import libs.common as common 6 | import sys 7 | import os 8 | import time 9 | import pandas as pd 10 | import tushare as ts 11 | from sqlalchemy.types import NVARCHAR 12 | from sqlalchemy import inspect 13 | import datetime 14 | import shutil 15 | 16 | 17 | ####### 使用 5.pdf,先做 基本面数据 的数据,然后在做交易数据。 18 | # 19 | def stat_all(tmp_datetime): 20 | datetime_str = (tmp_datetime).strftime("%Y-%m-%d") 21 | datetime_int = (tmp_datetime).strftime("%Y%m%d") 22 | 23 | cache_dir = common.bash_stock_tmp % (datetime_str[0:7], datetime_str) 24 | if os.path.exists(cache_dir): 25 | shutil.rmtree(cache_dir) 26 | print("remove cache dir force :", cache_dir) 27 | 28 | print("datetime_str:", datetime_str) 29 | print("datetime_int:", datetime_int) 30 | data = ts.top_list(datetime_str) 31 | # 处理重复数据,保存最新一条数据。最后一步处理,否则concat有问题。 32 | # 33 | if not data is None and len(data) > 0: 34 | # 插入数据库。 35 | # del data["reason"] 36 | data["date"] = datetime_int # 修改时间成为int类型。 37 | data = data.drop_duplicates(subset="code", keep="last") 38 | data.head(n=1) 39 | common.insert_db(data, "ts_top_list", False, "`date`,`code`") 40 | else: 41 | print("no data .") 42 | 43 | print(datetime_str) 44 | 45 | 46 | # main函数入口 47 | if __name__ == '__main__': 48 | # 使用方法传递。 49 | tmp_datetime = common.run_with_args(stat_all) 50 | -------------------------------------------------------------------------------- /backend/jobs/guess_indicators_daily_buy_job.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import libs.common as common 6 | import pandas as pd 7 | import numpy as np 8 | import math 9 | import datetime 10 | import stockstats 11 | from sqlalchemy import text 12 | 13 | 14 | ### 对每日指标数据,进行筛选。将符合条件的。二次筛选出来。 15 | ### 只是做简单筛选 16 | def stat_all_lite_buy(tmp_datetime): 17 | datetime_str = (tmp_datetime).strftime("%Y-%m-%d") 18 | datetime_int = (tmp_datetime).strftime("%Y%m%d") 19 | print("datetime_str:", datetime_str) 20 | print("datetime_int:", datetime_int) 21 | 22 | # 查询参数 23 | params = {"datetime": datetime_int} 24 | 25 | sql_kdjk = text(" SELECT avg(`kdjk`) as avg_kdjk FROM guess_indicators_daily ") 26 | data_kdjk = pd.read_sql(sql=sql_kdjk, con=common.engine(), params=params) 27 | kdjk = data_kdjk["avg_kdjk"][0] 28 | 29 | sql_kdjd = text(" SELECT avg(`kdjd`) as avg_kdjd FROM guess_indicators_daily ") 30 | data_kdjd = pd.read_sql(sql=sql_kdjd, con=common.engine(), params=params) 31 | kdjd = data_kdjd["avg_kdjd"][0] 32 | 33 | sql_kdjj = text(" SELECT avg(`kdjj`) as avg_kdjj FROM guess_indicators_daily ") 34 | data_kdjj = pd.read_sql(sql=sql_kdjj, con=common.engine(), params=params) 35 | kdjj = data_kdjj["avg_kdjj"][0] 36 | 37 | # K值在80以上,D值在70以上,J值大于90时为超买。 38 | # J大于100时为超买,小于10时为超卖。 39 | # 当六日指标上升到达80时,表示股市已有超买现象 40 | # 当CCI>﹢100 时,表明股价已经进入非常态区间——超买区间,股价的异动现象应多加关注。 41 | params_1 = {"datetime": datetime_int, "kdjk": kdjk, "kdjd": kdjd, "kdjj": kdjj} 42 | sql_1 = text(""" 43 | SELECT `date`,`code`,`name`,`last_price`,`change_percent`,`change_amount`,`volume`,`turnover`, 44 | `amplitude`,`high`,`low`,`open`,`closed`,`volume_ratio`,`turnover_rate`, 45 | `pe_ratio`,`pb_ratio`,`market_cap`,`circulating_market_cap`,`rise_speed`, 46 | `change_5min`,`change_ercent_60day`,`ytd_change_percent`, 47 | `boll`, `boll_lb`, `boll_ub`, `kdjd`, `kdjj`, `kdjk`, `macd`, `macdh`, 48 | `macds`, `pdi`,`trix`, `trix_9_sma`, `vr`, `vr_6_sma`, `wr_10`, `wr_6` 49 | FROM stock_data.guess_indicators_daily WHERE `date` = :datetime 50 | and kdjk >= :kdjk and kdjd >= :kdjd and kdjj >= :kdjj 51 | """) # and kdjj > 100 and rsi_6 > 80 and cci > 100 # 调整参数,提前获得股票增长。 52 | 53 | try: 54 | # 删除老数据。 55 | del_sql = " DELETE FROM `stock_data`.`guess_indicators_lite_buy_daily` WHERE `date`= '%s' " % datetime_int 56 | common.insert(del_sql) 57 | except Exception as e: 58 | print("error :", e) 59 | 60 | print(f"sql_1 : {sql_1}") 61 | data = pd.read_sql(sql=sql_1, con=common.engine(), params=params_1) 62 | data = data.drop_duplicates(subset="code", keep="last") 63 | print("######## stat_all_lite_buy len data ########:", len(data)) 64 | 65 | try: 66 | common.insert_db(data, "guess_indicators_lite_buy_daily", False, "`date`,`code`") 67 | except Exception as e: 68 | print("error :", e) 69 | 70 | 71 | 72 | # main函数入口 73 | if __name__ == '__main__': 74 | # 使用方法传递。 75 | # 二次筛选数据。直接计算买卖股票数据。 76 | tmp_datetime = common.run_with_args(stat_all_lite_buy) 77 | 78 | -------------------------------------------------------------------------------- /backend/jobs/guess_indicators_daily_sell_job.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import libs.common as common 6 | import pandas as pd 7 | import numpy as np 8 | import math 9 | import datetime 10 | import stockstats 11 | from sqlalchemy import text 12 | 13 | # 设置卖出数据。 14 | def stat_all_lite_sell(tmp_datetime): 15 | datetime_str = (tmp_datetime).strftime("%Y-%m-%d") 16 | datetime_int = (tmp_datetime).strftime("%Y%m%d") 17 | print("datetime_str:", datetime_str) 18 | print("datetime_int:", datetime_int) 19 | 20 | # 超卖区:K值在20以下,D值在30以下为超卖区。一般情况下,股价有可能上涨,反弹的可能性增大。局内人不应轻易抛出股票,局外人可寻机入场。 21 | # J大于100时为超买,小于10时为超卖。 22 | # 当六日强弱指标下降至20时,表示股市有超卖现象 23 | # 当CCI<﹣100时,表明股价已经进入另一个非常态区间——超卖区间,投资者可以逢低吸纳股票。 24 | sql_1 = text(""" 25 | SELECT `date`,`code`,`name`,`last_price`,`change_percent`,`change_amount`,`volume`,`turnover`, 26 | `amplitude`,`high`,`low`,`open`,`closed`,`volume_ratio`,`turnover_rate`, 27 | `pe_ratio`,`pb_ratio`,`market_cap`,`circulating_market_cap`,`rise_speed`, 28 | `change_5min`,`change_ercent_60day`,`ytd_change_percent`, 29 | `boll`, `boll_lb`, `boll_ub`, `kdjd`, `kdjj`, `kdjk`, `macd`, `macdh`, 30 | `macds`, `pdi`,`trix`, `trix_9_sma`, `vr`, `vr_6_sma`, `wr_10`, `wr_6` 31 | FROM stock_data.guess_indicators_daily WHERE `date` = :datetime 32 | and kdjk <= 20 and kdjd <= 30 and kdjj <= 10 33 | """) 34 | 35 | try: 36 | # 删除老数据。 37 | del_sql = " DELETE FROM `stock_data`.`guess_indicators_lite_sell_daily` WHERE `date`= '%s' " % datetime_int 38 | common.insert(del_sql) 39 | except Exception as e: 40 | print("error :", e) 41 | 42 | # 查询参数 43 | params = {"datetime": datetime_int} 44 | print(sql_1) 45 | data = pd.read_sql(sql=sql_1, con=common.engine(), params=params) 46 | data = data.drop_duplicates(subset="code", keep="last") 47 | print("######## stat_all_lite_sell len data ########:", len(data)) 48 | 49 | try: 50 | common.insert_db(data, "guess_indicators_lite_sell_daily", False, "`date`,`code`") 51 | except Exception as e: 52 | print("error :", e) 53 | 54 | 55 | 56 | # main函数入口 57 | if __name__ == '__main__': 58 | # 使用方法传递。 59 | # 二次筛选数据。直接计算买卖股票数据。 60 | tmp_datetime = common.run_with_args(stat_all_lite_sell) 61 | 62 | -------------------------------------------------------------------------------- /backend/jobs/quarter_job.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import libs.common as common 6 | import sys 7 | import time 8 | import pandas as pd 9 | import tushare as ts 10 | from sqlalchemy.types import NVARCHAR 11 | from sqlalchemy import inspect 12 | import datetime 13 | 14 | 15 | # 增加一个新quarter列,用来存储季度信息。 16 | def concat_quarter(year, quarter, data_array): 17 | print(len(data_array)) 18 | quarter_str = str(year) + str("%02d" % quarter) # 格式化季度数据。2位。 19 | # 增加到列。 20 | quarter_col = pd.DataFrame([quarter_str for _ in range(len(data_array))], columns=["quarter"]) 21 | return pd.concat([quarter_col, data_array], axis=1) 22 | 23 | 24 | #############################基本面数据 http://tushare.org/fundamental.html 25 | def stat_all(tmp_datetime): 26 | # 返回 31 天前的数据,做上个季度数据统计。 27 | tmp_datetime_1month = tmp_datetime + datetime.timedelta(days=-31) 28 | year = int((tmp_datetime_1month).strftime("%Y")) 29 | quarter = int(pd.Timestamp(tmp_datetime_1month).quarter) # 获得上个季度的数据。 30 | print("############ year %d, quarter %d", year, quarter) 31 | # 业绩报告(主表) 32 | data = ts.get_report_data(year, quarter) 33 | # 增加季度字段。 34 | data = concat_quarter(year, quarter, data) 35 | # 处理重复数据,保存最新一条数据。最后一步处理,否则concat有问题。 36 | data = data.drop_duplicates(subset="code", keep="last") 37 | # 插入数据库。 38 | common.insert_db(data, "ts_report_data", False, "`quarter`,`code`") 39 | 40 | # 盈利能力 41 | data = ts.get_profit_data(year, quarter) 42 | # 增加季度字段。 43 | data = concat_quarter(year, quarter, data) 44 | # 处理重复数据,保存最新一条数据。 45 | data = data.drop_duplicates(subset="code", keep="last") 46 | # 插入数据库。 47 | common.insert_db(data, "ts_profit_data", False, "`quarter`,`code`") 48 | 49 | # 营运能力 50 | data = ts.get_operation_data(year, quarter) 51 | # 增加季度字段。 52 | data = concat_quarter(year, quarter, data) 53 | # 处理重复数据,保存最新一条数据。最后一步处理,否则concat有问题。 54 | data = data.drop_duplicates(subset="code", keep="last") 55 | # 插入数据库。 56 | common.insert_db(data, "ts_operation_data", False, "`quarter`,`code`") 57 | 58 | # 成长能力 59 | data = ts.get_growth_data(year, quarter) 60 | # 增加季度字段。 61 | data = concat_quarter(year, quarter, data) 62 | # 处理重复数据,保存最新一条数据。最后一步处理,否则concat有问题。 63 | data = data.drop_duplicates(subset="code", keep="last") 64 | # 插入数据库。 65 | common.insert_db(data, "ts_growth_data", False, "`quarter`,`code`") 66 | 67 | # 偿债能力 68 | data = ts.get_debtpaying_data(year, quarter) 69 | # 增加季度字段。 70 | data = concat_quarter(year, quarter, data) 71 | # 处理重复数据,保存最新一条数据。最后一步处理,否则concat有问题。 72 | data = data.drop_duplicates(subset="code", keep="last") 73 | # 插入数据库。 74 | common.insert_db(data, "ts_debtpaying_data", False, "`quarter`,`code`") 75 | 76 | # 现金流量 77 | data = ts.get_cashflow_data(year, quarter) 78 | # 增加季度字段。 79 | data = concat_quarter(year, quarter, data) 80 | # 处理重复数据,保存最新一条数据。最后一步处理,否则concat有问题。 81 | data = data.drop_duplicates(subset="code", keep="last") 82 | # 插入数据库。 83 | common.insert_db(data, "ts_cashflow_data", False, "`quarter`,`code`") 84 | 85 | 86 | # main函数入口 87 | if __name__ == '__main__': 88 | # 使用方法传递。 89 | tmp_datetime = common.run_with_args(stat_all) 90 | -------------------------------------------------------------------------------- /backend/jobs/restart_mnist_serving.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ps -ef | grep 'tensorflow_model_server' | grep -v grep | awk '{print$2}' | xargs kill -9 4 | echo "" > /data/logs/mnist_serving.log 5 | nohup tensorflow_model_server --model_name=mnist --model_base_path=/data/mnist_model >> /data/logs/mnist_serving.log & -------------------------------------------------------------------------------- /backend/jobs/restart_web.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ps -ef | grep python3 | grep '/data/stock/web/main.py' | awk '{print$2}' | xargs kill -9 4 | echo "restart web ... " > /data/logs/tornado.log 5 | -------------------------------------------------------------------------------- /backend/jobs/run_cron.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export PYTHONIOENCODING=utf-8 4 | export LANG=zh_CN.UTF-8 5 | export PYTHONPATH=/data/stock 6 | export LC_CTYPE=zh_CN.UTF-8 7 | 8 | mkdir -p /data/logs/tensorflow 9 | 10 | 11 | 12 | DATE=`date +%Y-%m-%d:%H:%M:%S` 13 | 14 | echo $DATE >> /data/logs/run_cron.log 15 | 16 | # 解决定时任务不启动问题,因为权限导致 17 | chmod 755 /etc/cron.minutely/* && chmod 755 /etc/cron.hourly/* 18 | chmod 755 /etc/cron.daily/* && chmod 755 /etc/cron.monthly/* 19 | 20 | # 配置文件每次都设置权限 21 | chmod 600 /var/spool/cron/crontabs/root 22 | chown root:root /var/spool/cron/crontabs/root 23 | 24 | #启动cron服务。在前台 25 | /usr/sbin/cron -f -------------------------------------------------------------------------------- /backend/jobs/run_init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export PYTHONIOENCODING=utf-8 4 | export LANG=zh_CN.UTF-8 5 | export PYTHONPATH=/data/stock 6 | export LC_CTYPE=zh_CN.UTF-8 7 | 8 | mkdir -p /data/logs/tensorflow 9 | 10 | 11 | 12 | DATE=`date +%Y-%m-%d:%H:%M:%S` 13 | 14 | echo $DATE >> /data/logs/run_init.log 15 | 16 | echo "wait 120 second , mysqldb is starting ." >> /data/logs/run_init.log 17 | sleep 120 18 | 19 | /usr/local/bin/python3 /data/stock/jobs/basic_job.py >> /data/logs/run_init.log 20 | 21 | # https://stackoverflow.com/questions/27771781/how-can-i-access-docker-set-environment-variables-from-a-cron-job 22 | # 解决环境变量输出问题。 23 | printenv | grep -v "no_proxy" >> /etc/environment 24 | 25 | # 第一次后台执行日数据。 26 | nohup bash /data/stock/jobs/cron.daily/run_daily & 27 | 28 | #防止 supervisor 重复执行 29 | sleep 999999d -------------------------------------------------------------------------------- /backend/jobs/run_jupyter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p /data/notebooks 4 | 5 | /usr/local/bin/jupyter notebook --NotebookApp.notebook_dir='/data/notebooks' --ip=0.0.0.0 \ 6 | --allow-root >> /data/logs/jupyter-notebook.log 7 | -------------------------------------------------------------------------------- /backend/jobs/run_web.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PYTHONIOENCODING=utf-8 4 | export LANG=zh_CN.UTF-8 5 | export PYTHONPATH=/data/stock 6 | export LC_CTYPE=zh_CN.UTF-8 7 | 8 | echo "" > /data/logs/web.log 9 | /usr/local/bin/python3 /data/stock/web/main.py -log_file_prefix=/data/logs/web.log -------------------------------------------------------------------------------- /backend/jobs/start_mariadb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DATE=`date +%Y-%m-%d:%H:%M:%S` 4 | echo $DATE 5 | 6 | if [ ! -d "/data/mariadb" ]; then 7 | mkdir -p /data/mariadb 8 | /usr/bin/mysql_install_db 9 | fi 10 | 11 | 12 | /usr/bin/mysqld_safe >> /data/logs/start_mariadb.log -------------------------------------------------------------------------------- /backend/jobs/test_akshare/test_stock_zh_a_daily.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import akshare as ak 5 | import libs.common as common 6 | 7 | print(ak.__version__) 8 | 9 | # 历史行情数据 10 | # 日频率 11 | # 接口: stock_zh_a_daily 12 | # 目标地址: https://finance.sina.com.cn/realstock/company/sh600006/nc.shtml(示例) 13 | # 描述: A 股数据是从新浪财经获取的数据, 历史数据按日频率更新; 注意其中的 sh689009 为 CDR, 请 通过 stock_zh_a_cdr_daily 接口获取 14 | # 限量: 单次返回指定 A 股上市公司指定日期间的历史行情日频率数据 15 | # adjust=""; 默认为空: 返回不复权的数据; qfq: 返回前复权后的数据; hfq: 返回后复权后的数据; 16 | 17 | stock_zh_a_daily_qfq_df = ak.stock_zh_a_daily(symbol="sz000002", adjust="") 18 | print(stock_zh_a_daily_qfq_df) 19 | 20 | stock_zh_a_daily_qfq_df = ak.stock_zh_a_daily(symbol="sz000002", start_date="20200101", end_date="20210101", adjust="") 21 | print(stock_zh_a_daily_qfq_df) 22 | 23 | # 插入到 MySQL 数据库中 24 | common.insert_db(stock_zh_a_daily_qfq_df, "stock_zh_a_daily", True, "`symbol`") 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /backend/jobs/test_akshare/test_stock_zh_a_spot.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import akshare as ak 5 | import libs.common as common 6 | 7 | print(ak.__version__) 8 | 9 | # 实时行情数据 10 | # 接口: stock_zh_a_spot 11 | # 目标地址: http://vip.stock.finance.sina.com.cn/mkt/#hs_a 12 | # 描述: A 股数据是从新浪财经获取的数据, 重复运行本函数会被新浪暂时封 IP, 建议增加时间间隔 13 | # 限量: 单次返回所有 A 股上市公司的实时行情数据 14 | 15 | stock_zh_a_spot_df = ak.stock_zh_a_spot() 16 | print(stock_zh_a_spot_df) 17 | 18 | # 插入到 MySQL 数据库中 19 | common.insert_db(stock_zh_a_spot_df, "stock_zh_a_spot", True, "`symbol`") 20 | -------------------------------------------------------------------------------- /backend/jobs/test_akshare/test_stock_zh_index_spot.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import akshare as ak 5 | import libs.common as common 6 | 7 | print(ak.__version__) 8 | 9 | #stock_sse_summary_df = ak.stock_sse_summary() 10 | #print(stock_sse_summary_df) 11 | 12 | # 接口: stock_zh_index_spot 13 | # 目标地址: http://vip.stock.finance.sina.com.cn/mkt/#hs_s 14 | # 描述: 中国股票指数数据, 注意该股票指数指新浪提供的国内股票指数 15 | # 限量: 单次返回所有指数的实时行情数据 16 | stock_zh_index_spot_df = ak.stock_zh_index_spot() 17 | print(stock_zh_index_spot_df) 18 | 19 | # 插入到 MySQL 数据库中 20 | common.insert_db(stock_zh_index_spot_df, "stock_zh_index_spot_df", True, "`symbol`") 21 | -------------------------------------------------------------------------------- /backend/old_jobs/README.md: -------------------------------------------------------------------------------- 1 | ## 说明 2 | 3 | 4 | 之前测试使用的脚本。执行了一段时间,只是用来进行练习使用的。 5 | -------------------------------------------------------------------------------- /backend/supervisor/supervisord.conf: -------------------------------------------------------------------------------- 1 | [unix_http_server] 2 | file=/tmp/supervisor.sock ; the path to the socket file 3 | 4 | [inet_http_server] ; inet (TCP) server disabled by default 5 | port=*:9001 ; ip_address:port specifier, *:port for all iface 6 | ;username=user ; default is no username (open server) 7 | ;password=123 ; default is no password (open server) 8 | 9 | [supervisord] 10 | logfile=/tmp/supervisord.log ; main log file; default $CWD/supervisord.log 11 | logfile_maxbytes=50MB ; max main logfile bytes b4 rotation; default 50MB 12 | logfile_backups=10 ; # of main logfile backups; 0 means none, default 10 13 | loglevel=info ; log level; default info; others: debug,warn,trace 14 | pidfile=/tmp/supervisord.pid ; supervisord pidfile; default supervisord.pid 15 | nodaemon=false ; start in foreground if true; default false 16 | minfds=1024 ; min. avail startup file descriptors; default 1024 17 | minprocs=200 ; min. avail process descriptors;default 200 18 | 19 | [rpcinterface:supervisor] 20 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 21 | 22 | [supervisorctl] 23 | serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket 24 | 25 | [program:init] 26 | command=/data/stock/jobs/run_init.sh 27 | autostart=true 28 | autorestart=true 29 | startsecs=20 30 | priority=1 31 | stopasgroup=true 32 | killasgroup=true 33 | 34 | [program:cron] 35 | command=/data/stock/jobs/run_cron.sh 36 | autostart=true 37 | autorestart=true 38 | startsecs=20 39 | priority=1 40 | stopasgroup=true 41 | killasgroup=true 42 | 43 | 44 | [program:stock-web] 45 | command=/data/stock/jobs/run_web.sh 46 | autostart=true 47 | autorestart=true 48 | startsecs=20 49 | priority=1 50 | stopasgroup=true 51 | killasgroup=true 52 | -------------------------------------------------------------------------------- /backend/web/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/README.md -------------------------------------------------------------------------------- /backend/web/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import tornado.web 5 | import libs.stock_web_dic as stock_web_dic 6 | import libs.common as common 7 | import logging 8 | 9 | #基础handler,主要负责检查mysql的数据库链接。 10 | class BaseHandler(tornado.web.RequestHandler): 11 | def set_default_headers(self): 12 | headers = self.request.headers 13 | # logging.info('head的类型:',type(headers)) 14 | origin = headers.get('origin',None) 15 | logging.info("######################## BaseHandler ########################") 16 | logging.info(origin) 17 | 18 | if origin != None and origin.find("localhost") > 0: 19 | self.set_header("Access-Control-Allow-Credentials", "true") 20 | self.set_header("Access-Control-Allow-Origin",origin) 21 | self.set_header("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS") 22 | self.set_header("Access-Control-Allow-Headers", "x-token, authorization, Authorization, Content-Type, Access-Control-Allow-Origin, Access-Control-Allow-Headers, X-Requested-By, Access-Control-Allow-Methods") 23 | self.set_header("Access-Control-Expose-Headers", "Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma") 24 | # 同时定义一个option方法 25 | def options(self): 26 | self.set_status(204) 27 | self.finish() 28 | 29 | @property 30 | def db(self): 31 | try: 32 | # check every time。 33 | self.application.db.query("SELECT 1 ") 34 | except Exception as e: 35 | print(e) 36 | self.application.db.reconnect() 37 | return self.application.db 38 | 39 | class LeftMenu: 40 | def __init__(self, url): 41 | self.leftMenuList = stock_web_dic.STOCK_WEB_DATA_LIST 42 | self.current_url = url 43 | 44 | # 获得左菜单。 45 | def GetLeftMenu(url): 46 | return LeftMenu(url) 47 | -------------------------------------------------------------------------------- /backend/web/chartHandler.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | from tornado import gen 6 | import libs.stock_web_dic as stock_web_dic 7 | import web.base as webBase 8 | import libs.common as common 9 | import logging 10 | import tornado.web 11 | import matplotlib 12 | matplotlib.use('Agg') 13 | import matplotlib.pyplot as plt 14 | import numpy as np 15 | import io 16 | 17 | def GenImage(freq): 18 | t = np.linspace(0, 10, 500) 19 | y = np.sin(t * freq * 2 * 3.141) 20 | fig1 = plt.figure() 21 | plt.plot(t, y) 22 | plt.xlabel('Time [s]') 23 | memdata = io.BytesIO() 24 | plt.grid(True) 25 | plt.savefig(memdata, format='png') 26 | image = memdata.getvalue() 27 | return image 28 | 29 | 30 | class ImageHandler(tornado.web.RequestHandler): 31 | @gen.coroutine 32 | def get(self): 33 | image = GenImage(0.5) 34 | self.set_header('Content-type', 'image/png') 35 | self.set_header('Content-length', len(image)) 36 | self.write(image) 37 | 38 | # 获得页面数据。 39 | class GetChartHtmlHandler(webBase.BaseHandler): 40 | @gen.coroutine 41 | def get(self): 42 | name = self.get_argument("table_name", default=None, strip=False) 43 | #stockWeb = stock_web_dic.STOCK_WEB_DATA_MAP[name] 44 | # self.uri_ = ("self.request.url:", self.request.uri) 45 | # print self.uri_ 46 | logging.info("chart...") 47 | self.render("stock_chart.html", entries="", 48 | pythonStockVersion=common.__version__, 49 | leftMenu=webBase.GetLeftMenu(self.request.uri)) 50 | 51 | -------------------------------------------------------------------------------- /backend/web/minstServingHandler.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import os.path 4 | import json 5 | import subprocess 6 | import torndb 7 | import tornado.escape 8 | from tornado import gen 9 | import tornado.httpserver 10 | import tornado.ioloop 11 | import tornado.options 12 | import tornado.web 13 | import web.base as webBase 14 | import logging 15 | import numpy as np 16 | from PIL import Image 17 | from PIL import ImageOps 18 | import base64 19 | import io #python2 import StringIO 20 | 21 | work_dir = "/data/stock/tf/minst_serving/input_data" 22 | out_dir = "/static/img/minst_serving/%s.bmp" 23 | 24 | 25 | # 获得页面数据。 26 | class GetMinstServingHtmlHandler(webBase.BaseHandler): 27 | @gen.coroutine 28 | def get(self): 29 | # print self.uri_ 30 | arr = np.arange(30) 31 | image_array = [] 32 | for idx in arr: 33 | out_file = out_dir % ("%05d" % idx) 34 | print(out_file) 35 | image_array.append(out_file) 36 | self.render("minst_serving.html", image_array=image_array) 37 | 38 | 39 | # 获得股票数据内容。 40 | class GetPredictionDataHandler(webBase.BaseHandler): 41 | def get(self): 42 | # 获得分页参数。 43 | img_url = self.get_argument("img_url", default=0, strip=False) 44 | print(img_url) 45 | img_obj = Image.open("/data/stock/web" + img_url) 46 | print("img_obj", img_obj) 47 | server = "0.0.0.0:8500" 48 | prediction = do_inference(server, img_obj) 49 | print('######### prediction : ', prediction) 50 | self.write(json.dumps(prediction)) 51 | 52 | 53 | # 获得股票数据内容。 54 | class GetPrediction2DataHandler(webBase.BaseHandler): 55 | def post(self): 56 | # 获得分页参数。 57 | imgStr = self.get_argument("txt", default="", strip=False) 58 | # imgStr.replace(" ", "+") 59 | imgStr = base64.b64decode(imgStr) 60 | print("imgStr:", type(imgStr)) 61 | image = Image.open(io.StringIO(imgStr)) 62 | image.thumbnail((28, 28), Image.ANTIALIAS) 63 | image = image.convert('L') 64 | image = ImageOps.invert(image) 65 | image.save(work_dir + "/web-tmp.bmp", format="BMP") #保存看看,是否 66 | #print(image) 67 | # img_url = self.get_argument("img_url", default=0, strip=False) 68 | # print(img_url) 69 | server = "0.0.0.0:8500" 70 | prediction = do_inference(server, image) 71 | print('######### prediction : ', prediction) 72 | self.write(json.dumps(prediction)) 73 | 74 | 75 | 76 | # 调用 grpc 代码,将图片转换成数组,让后放到 grpc 调用。 77 | def do_inference(hostport, img_obj): 78 | 79 | print("############", hostport) 80 | 81 | -------------------------------------------------------------------------------- /backend/web/static/css/bootstrap-colorpicker.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Colorpicker 3 | * http://mjolnic.github.io/bootstrap-colorpicker/ 4 | * 5 | * Originally written by (c) 2012 Stefan Petre 6 | * Licensed under the Apache License v2.0 7 | * http://www.apache.org/licenses/LICENSE-2.0.txt 8 | * 9 | */.colorpicker-saturation{float:left;width:100px;height:100px;cursor:crosshair;background-image:url(../images/bootstrap-colorpicker/saturation.png)}.colorpicker-saturation i{position:absolute;top:0;left:0;display:block;width:5px;height:5px;margin:-4px 0 0 -4px;border:1px solid #000;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.colorpicker-saturation i b{display:block;width:5px;height:5px;border:1px solid #fff;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.colorpicker-alpha,.colorpicker-hue{float:left;width:15px;height:100px;margin-bottom:4px;margin-left:4px;cursor:row-resize}.colorpicker-alpha i,.colorpicker-hue i{position:absolute;top:0;left:0;display:block;width:100%;height:1px;margin-top:-1px;background:#000;border-top:1px solid #fff}.colorpicker-hue{background-image:url(../images/bootstrap-colorpicker/hue.png)}.colorpicker-alpha,.colorpicker-color{background-image:url(../images/bootstrap-colorpicker/alpha.png)}.colorpicker-alpha{display:none}.colorpicker:after,.colorpicker:before{position:absolute;display:inline-block;content:''}.colorpicker-alpha,.colorpicker-hue,.colorpicker-saturation{background-size:contain}.colorpicker{top:0;left:0;z-index:2500;min-width:130px;padding:4px;margin-top:1px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*zoom:1}.colorpicker:after,.colorpicker:before{line-height:0}.colorpicker:before{top:-7px;left:6px;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,.2)}.colorpicker:after{clear:both;top:-6px;left:7px;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent}.colorpicker div{position:relative}.colorpicker.colorpicker-with-alpha{min-width:140px}.colorpicker.colorpicker-with-alpha .colorpicker-alpha{display:block}.colorpicker-color{height:10px;margin-top:5px;clear:both;background-position:0 100%}.colorpicker-color div{height:10px}.colorpicker-selectors{display:none;height:10px;margin-top:5px;clear:both}.colorpicker-selectors i{float:left;width:10px;height:10px;cursor:pointer}.colorpicker-selectors i+i{margin-left:3px}.colorpicker-element .add-on i,.colorpicker-element .input-group-addon i{display:inline-block;width:16px;height:16px;vertical-align:text-top;cursor:pointer}.colorpicker.colorpicker-inline{position:relative;z-index:auto;display:inline-block;float:none}.colorpicker.colorpicker-horizontal{width:110px;height:auto;min-width:110px}.colorpicker.colorpicker-horizontal .colorpicker-saturation{margin-bottom:4px}.colorpicker.colorpicker-horizontal .colorpicker-color{width:100px}.colorpicker.colorpicker-horizontal .colorpicker-alpha,.colorpicker.colorpicker-horizontal .colorpicker-hue{float:left;width:100px;height:15px;margin-bottom:4px;margin-left:0;cursor:col-resize}.colorpicker.colorpicker-horizontal .colorpicker-alpha i,.colorpicker.colorpicker-horizontal .colorpicker-hue i{position:absolute;top:0;left:0;display:block;width:1px;height:15px;margin-top:0;background:#fff;border:none}.colorpicker.colorpicker-horizontal .colorpicker-hue{background-image:url(../images/bootstrap-colorpicker/hue-horizontal.png)}.colorpicker.colorpicker-horizontal .colorpicker-alpha{background-image:url(../images/bootstrap-colorpicker/alpha-horizontal.png)}.colorpicker.colorpicker-hidden{display:none}.colorpicker.colorpicker-visible{display:block}.colorpicker-inline.colorpicker-visible{display:inline-block}.colorpicker-right:before{right:6px;left:auto}.colorpicker-right:after{right:7px;left:auto} -------------------------------------------------------------------------------- /backend/web/static/css/bootstrap-timepicker.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Timepicker Component for Twitter Bootstrap 3 | * 4 | * Copyright 2013 Joris de Wit 5 | * 6 | * Contributors https://github.com/jdewit/bootstrap-timepicker/graphs/contributors 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */.bootstrap-timepicker{position:relative}.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu{left:auto;right:0}.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu:before{left:auto;right:12px}.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu:after{left:auto;right:13px}.bootstrap-timepicker .input-group-addon{cursor:pointer}.bootstrap-timepicker .input-group-addon i{display:inline-block;width:16px;height:16px}.bootstrap-timepicker-widget.dropdown-menu{padding:4px}.bootstrap-timepicker-widget.dropdown-menu.open{display:inline-block}.bootstrap-timepicker-widget.dropdown-menu:before{border-bottom:7px solid rgba(0,0,0,.2);border-left:7px solid transparent;border-right:7px solid transparent;content:"";display:inline-block;position:absolute}.bootstrap-timepicker-widget.dropdown-menu:after{border-bottom:6px solid #FFF;border-left:6px solid transparent;border-right:6px solid transparent;content:"";display:inline-block;position:absolute}.bootstrap-timepicker-widget.timepicker-orient-left:before{left:6px}.bootstrap-timepicker-widget.timepicker-orient-left:after{left:7px}.bootstrap-timepicker-widget.timepicker-orient-right:before{right:6px}.bootstrap-timepicker-widget.timepicker-orient-right:after{right:7px}.bootstrap-timepicker-widget.timepicker-orient-top:before{top:-7px}.bootstrap-timepicker-widget.timepicker-orient-top:after{top:-6px}.bootstrap-timepicker-widget.timepicker-orient-bottom:before{bottom:-7px;border-bottom:0;border-top:7px solid #999}.bootstrap-timepicker-widget.timepicker-orient-bottom:after{bottom:-6px;border-bottom:0;border-top:6px solid #fff}.bootstrap-timepicker-widget a.btn,.bootstrap-timepicker-widget input{border-radius:4px}.bootstrap-timepicker-widget table{width:100%;margin:0}.bootstrap-timepicker-widget table td{text-align:center;height:30px;margin:0;padding:2px}.bootstrap-timepicker-widget table td:not(.separator){min-width:30px}.bootstrap-timepicker-widget table td span{width:100%}.bootstrap-timepicker-widget table td a{border:1px solid transparent;width:100%;display:inline-block;margin:0;padding:8px 0;outline:0;color:#333}.bootstrap-timepicker-widget table td a:hover{text-decoration:none;background-color:#eee;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;border-color:#ddd}.bootstrap-timepicker-widget table td a i{margin-top:2px;font-size:18px}.bootstrap-timepicker-widget table td input{width:25px;margin:0;text-align:center}.bootstrap-timepicker-widget .modal-content{padding:4px}@media (min-width:767px){.bootstrap-timepicker-widget.modal{width:200px;margin-left:-100px}}@media (max-width:767px){.bootstrap-timepicker,.bootstrap-timepicker .dropdown-menu{width:100%}} -------------------------------------------------------------------------------- /backend/web/static/css/fonts.googleapis.com.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Open Sans'; 3 | font-style: normal; 4 | font-weight: 300; 5 | src: local('Open Sans Light'), local('OpenSans-Light'), url(/static/font-awesome/opensans/v13/DXI1ORHCpsQm3Vp6mXoaTXhCUOGz7vYGh680lGh-uXM.woff) format('woff'); 6 | } 7 | @font-face { 8 | font-family: 'Open Sans'; 9 | font-style: normal; 10 | font-weight: 400; 11 | src: local('Open Sans'), local('OpenSans'), url(/static/font-awesome/opensans/v13/cJZKeOuBrn4kERxqtaUH3T8E0i7KZn-EPnyo3HZu7kw.woff) format('woff'); 12 | } 13 | -------------------------------------------------------------------------------- /backend/web/static/css/jquery-ui.custom.min.css: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.11.4 - 2015-09-20 2 | * http://jqueryui.com 3 | * Includes: core.css, draggable.css, resizable.css, selectable.css, sortable.css, slider.css 4 | * Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:after,.ui-helper-clearfix:before{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-autohide .ui-resizable-handle,.ui-resizable-disabled .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted #000}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0} -------------------------------------------------------------------------------- /backend/web/static/css/select.dataTables.min.css: -------------------------------------------------------------------------------- 1 | table.dataTable tbody>tr.selected,table.dataTable tbody>tr>.selected{background-color:#B0BED9}table.dataTable.stripe tbody>tr.odd.selected,table.dataTable.stripe tbody>tr.odd>.selected,table.dataTable.display tbody>tr.odd.selected,table.dataTable.display tbody>tr.odd>.selected{background-color:#acbad4}table.dataTable.hover tbody>tr.selected:hover,table.dataTable.hover tbody>tr>.selected:hover,table.dataTable.display tbody>tr.selected:hover,table.dataTable.display tbody>tr>.selected:hover{background-color:#aab7d1}table.dataTable.order-column tbody>tr.selected>.sorting_1,table.dataTable.order-column tbody>tr.selected>.sorting_2,table.dataTable.order-column tbody>tr.selected>.sorting_3,table.dataTable.order-column tbody>tr>.selected,table.dataTable.display tbody>tr.selected>.sorting_1,table.dataTable.display tbody>tr.selected>.sorting_2,table.dataTable.display tbody>tr.selected>.sorting_3,table.dataTable.display tbody>tr>.selected{background-color:#acbad5}table.dataTable.display tbody>tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody>tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody>tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody>tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody>tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody>tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody>tr.odd>.selected,table.dataTable.order-column.stripe tbody>tr.odd>.selected{background-color:#a6b4cd}table.dataTable.display tbody>tr.even>.selected,table.dataTable.order-column.stripe tbody>tr.even>.selected{background-color:#acbad5}table.dataTable.display tbody>tr.selected:hover>.sorting_1,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody>tr.selected:hover>.sorting_2,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody>tr.selected:hover>.sorting_3,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_3{background-color:#a5b2cb}table.dataTable.display tbody>tr:hover>.selected,table.dataTable.display tbody>tr>.selected:hover,table.dataTable.order-column.hover tbody>tr:hover>.selected,table.dataTable.order-column.hover tbody>tr>.selected:hover{background-color:#a2aec7}table.dataTable tbody td.select-checkbox,table.dataTable tbody th.select-checkbox{position:relative}table.dataTable tbody td.select-checkbox:before,table.dataTable tbody td.select-checkbox:after,table.dataTable tbody th.select-checkbox:before,table.dataTable tbody th.select-checkbox:after{display:block;position:absolute;top:1.2em;left:50%;width:12px;height:12px;box-sizing:border-box}table.dataTable tbody td.select-checkbox:before,table.dataTable tbody th.select-checkbox:before{content:' ';margin-top:-6px;margin-left:-6px;border:1px solid black;border-radius:3px}table.dataTable tr.selected td.select-checkbox:after,table.dataTable tr.selected th.select-checkbox:after{content:'\2714';margin-top:-11px;margin-left:-4px;text-align:center;text-shadow:1px 1px #B0BED9, -1px -1px #B0BED9, 1px -1px #B0BED9, -1px 1px #B0BED9}div.dataTables_wrapper span.select-info,div.dataTables_wrapper span.select-item{margin-left:0.5em}@media screen and (max-width: 640px){div.dataTables_wrapper span.select-info,div.dataTables_wrapper span.select-item{margin-left:0;display:block}} 2 | -------------------------------------------------------------------------------- /backend/web/static/font-awesome/4.5.0/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/font-awesome/4.5.0/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /backend/web/static/font-awesome/4.5.0/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/font-awesome/4.5.0/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /backend/web/static/font-awesome/4.5.0/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/font-awesome/4.5.0/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /backend/web/static/font-awesome/opensans/v13/DXI1ORHCpsQm3Vp6mXoaTXhCUOGz7vYGh680lGh-uXM.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/font-awesome/opensans/v13/DXI1ORHCpsQm3Vp6mXoaTXhCUOGz7vYGh680lGh-uXM.woff -------------------------------------------------------------------------------- /backend/web/static/font-awesome/opensans/v13/cJZKeOuBrn4kERxqtaUH3T8E0i7KZn-EPnyo3HZu7kw.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/font-awesome/opensans/v13/cJZKeOuBrn4kERxqtaUH3T8E0i7KZn-EPnyo3HZu7kw.woff -------------------------------------------------------------------------------- /backend/web/static/img/diff-n-bokeh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/img/diff-n-bokeh.png -------------------------------------------------------------------------------- /backend/web/static/img/stock-show-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/img/stock-show-01.jpg -------------------------------------------------------------------------------- /backend/web/static/img/stock2-001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/img/stock2-001.png -------------------------------------------------------------------------------- /backend/web/static/img/stock2-002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/img/stock2-002.png -------------------------------------------------------------------------------- /backend/web/static/img/stock2-003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/img/stock2-003.png -------------------------------------------------------------------------------- /backend/web/static/img/支付宝--微信支付.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/backend/web/static/img/支付宝--微信支付.jpg -------------------------------------------------------------------------------- /backend/web/static/js/autosize.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Autosize 3.0.15 3 | license: MIT 4 | http://www.jacklmoore.com/autosize 5 | */ 6 | !function(a,b){if("function"==typeof define&&define.amd)define(["exports","module"],b);else if("undefined"!=typeof exports&&"undefined"!=typeof module)b(exports,module);else{var c={exports:{}};b(c.exports,c),a.autosize=c.exports}}(this,function(a,b){"use strict";function c(a){function b(){var b=window.getComputedStyle(a,null);n=b.overflowY,"vertical"===b.resize?a.style.resize="none":"both"===b.resize&&(a.style.resize="horizontal"),m="content-box"===b.boxSizing?-(parseFloat(b.paddingTop)+parseFloat(b.paddingBottom)):parseFloat(b.borderTopWidth)+parseFloat(b.borderBottomWidth),isNaN(m)&&(m=0),e()}function c(b){var c=a.style.width;a.style.width="0px",a.offsetWidth,a.style.width=c,n=b,l&&(a.style.overflowY=b),d()}function d(){var b=window.pageYOffset,c=document.body.scrollTop,d=a.style.height;a.style.height="auto";var e=a.scrollHeight+m;return 0===a.scrollHeight?void(a.style.height=d):(a.style.height=e+"px",o=a.clientWidth,document.documentElement.scrollTop=b,void(document.body.scrollTop=c))}function e(){var b=a.style.height;d();var e=window.getComputedStyle(a,null);if(e.height!==a.style.height?"visible"!==n&&c("visible"):"hidden"!==n&&c("hidden"),b!==a.style.height){var f=g("autosize:resized");a.dispatchEvent(f)}}var h=void 0===arguments[1]?{}:arguments[1],i=h.setOverflowX,j=void 0===i?!0:i,k=h.setOverflowY,l=void 0===k?!0:k;if(a&&a.nodeName&&"TEXTAREA"===a.nodeName&&!f.has(a)){var m=null,n=null,o=a.clientWidth,p=function(){a.clientWidth!==o&&e()},q=function(b){window.removeEventListener("resize",p,!1),a.removeEventListener("input",e,!1),a.removeEventListener("keyup",e,!1),a.removeEventListener("autosize:destroy",q,!1),a.removeEventListener("autosize:update",e,!1),f["delete"](a),Object.keys(b).forEach(function(c){a.style[c]=b[c]})}.bind(a,{height:a.style.height,resize:a.style.resize,overflowY:a.style.overflowY,overflowX:a.style.overflowX,wordWrap:a.style.wordWrap});a.addEventListener("autosize:destroy",q,!1),"onpropertychange"in a&&"oninput"in a&&a.addEventListener("keyup",e,!1),window.addEventListener("resize",p,!1),a.addEventListener("input",e,!1),a.addEventListener("autosize:update",e,!1),f.add(a),j&&(a.style.overflowX="hidden",a.style.wordWrap="break-word"),b()}}function d(a){if(a&&a.nodeName&&"TEXTAREA"===a.nodeName){var b=g("autosize:destroy");a.dispatchEvent(b)}}function e(a){if(a&&a.nodeName&&"TEXTAREA"===a.nodeName){var b=g("autosize:update");a.dispatchEvent(b)}}var f="function"==typeof Set?new Set:function(){var a=[];return{has:function(b){return Boolean(a.indexOf(b)>-1)},add:function(b){a.push(b)},"delete":function(b){a.splice(a.indexOf(b),1)}}}(),g=function(a){return new Event(a)};try{new Event("test")}catch(h){g=function(a){var b=document.createEvent("Event");return b.initEvent(a,!0,!1),b}}var i=null;"undefined"==typeof window||"function"!=typeof window.getComputedStyle?(i=function(a){return a},i.destroy=function(a){return a},i.update=function(a){return a}):(i=function(a,b){return a&&Array.prototype.forEach.call(a.length?a:[a],function(a){return c(a,b)}),a},i.destroy=function(a){return a&&Array.prototype.forEach.call(a.length?a:[a],d),a},i.update=function(a){return a&&Array.prototype.forEach.call(a.length?a:[a],e),a}),b.exports=i}); -------------------------------------------------------------------------------- /backend/web/static/js/bootstrap-datepicker.zh-CN.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simplified Chinese translation for bootstrap-datepicker 3 | * Yuan Cheung <advanimal@gmail.com> 4 | */ 5 | ;(function($){ 6 | $.fn.datepicker.dates['zh-CN'] = { 7 | days: ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"], 8 | daysShort: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"], 9 | daysMin: ["日", "一", "二", "三", "四", "五", "六"], 10 | months: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"], 11 | monthsShort: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"], 12 | today: "今日", 13 | clear: "清除", 14 | format: "yyyy年mm月dd日", 15 | titleFormat: "yyyy年mm月", 16 | weekStart: 1 17 | }; 18 | }(jQuery)); -------------------------------------------------------------------------------- /backend/web/static/js/buttons.colVis.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Column visibility buttons for Buttons and DataTables. 3 | * 2015 SpryMedia Ltd - datatables.net/license 4 | */ 5 | !function(a){"function"==typeof define&&define.amd?define(["jquery","datatables.net","datatables.net-buttons"],function(b){return a(b,window,document)}):"object"==typeof exports?module.exports=function(b,c){return b||(b=window),c&&c.fn.dataTable||(c=require("datatables.net")(b,c).$),c.fn.dataTable.Buttons||require("datatables.net-buttons")(b,c),a(c,b,b.document)}:a(jQuery,window,document)}(function(a,b,c,d){"use strict";var e=a.fn.dataTable;return a.extend(e.ext.buttons,{colvis:function(a,b){return{extend:"collection",text:function(a){return a.i18n("buttons.colvis","Column visibility")},className:"buttons-colvis",buttons:[{extend:"columnsToggle",columns:b.columns}]}},columnsToggle:function(a,b){var c=a.columns(b.columns).indexes().map(function(a){return{extend:"columnToggle",columns:a}}).toArray();return c},columnToggle:function(a,b){return{extend:"columnVisibility",columns:b.columns}},columnsVisibility:function(a,b){var c=a.columns(b.columns).indexes().map(function(a){return{extend:"columnVisibility",columns:a,visibility:b.visibility}}).toArray();return c},columnVisibility:{columns:d,text:function(a,b,c){return c._columnText(a,c.columns)},className:"buttons-columnVisibility",action:function(a,b,c,e){var f=b.columns(e.columns),g=f.visible();f.visible(e.visibility!==d?e.visibility:!(g.length?g[0]:!1))},init:function(a,b,c){var d=this,e=a.column(c.columns);a.on("column-visibility.dt"+c.namespace,function(a,b,e,f){b.bDestroying||e!==c.columns||d.active(f)}).on("column-reorder.dt"+c.namespace,function(b,e,f){if(1===a.columns(c.columns).count()){"number"==typeof c.columns&&(c.columns=f.mapping[c.columns]);var g=a.column(c.columns);d.text(c._columnText(a,c.columns)),d.active(g.visible())}}),this.active(e.visible())},destroy:function(a,b,c){a.off("column-visibility.dt"+c.namespace).off("column-reorder.dt"+c.namespace)},_columnText:function(a,b){var c=a.column(b).index();return a.settings()[0].aoColumns[c].sTitle.replace(/\n/g," ").replace(/<.*?>/g,"").replace(/^\s+|\s+$/g,"")}},colvisRestore:{className:"buttons-colvisRestore",text:function(a){return a.i18n("buttons.colvisRestore","Restore visibility")},init:function(a,b,c){c._visOriginal=a.columns().indexes().map(function(b){return a.column(b).visible()}).toArray()},action:function(a,b,c,d){b.columns().every(function(a){var c=b.colReorder&&b.colReorder.transpose?b.colReorder.transpose(a,"toOriginal"):a;this.visible(d._visOriginal[c])})}},colvisGroup:{className:"buttons-colvisGroup",action:function(a,b,c,d){b.columns(d.show).visible(!0),b.columns(d.hide).visible(!1)},show:[],hide:[]}}),e.Buttons}); -------------------------------------------------------------------------------- /backend/web/static/js/buttons.print.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Print button for Buttons and DataTables. 3 | * 2015 SpryMedia Ltd - datatables.net/license 4 | */ 5 | !function(a){"function"==typeof define&&define.amd?define(["jquery","datatables.net","datatables.net-buttons"],function(b){return a(b,window,document)}):"object"==typeof exports?module.exports=function(b,c){return b||(b=window),c&&c.fn.dataTable||(c=require("datatables.net")(b,c).$),c.fn.dataTable.Buttons||require("datatables.net-buttons")(b,c),a(c,b,b.document)}:a(jQuery,window,document)}(function(a,b,c,d){"use strict";var e=a.fn.dataTable,f=c.createElement("a"),g=function(b){var c,d=a(b).clone()[0];return"link"===d.nodeName.toLowerCase()&&(f.href=d.href,c=f.host,-1===c.indexOf("/")&&0!==f.pathname.indexOf("/")&&(c+="/"),d.href=f.protocol+"//"+c+f.pathname+f.search),d.outerHTML};return e.ext.buttons.print={className:"buttons-print",text:function(a){return a.i18n("buttons.print","Print")},action:function(c,d,e,f){var h=d.buttons.exportData(f.exportOptions),i=function(a,b){for(var c="<tr>",d=0,e=a.length;e>d;d++)c+="<"+b+">"+a[d]+"</"+b+">";return c+"</tr>"},j='<table class="'+d.table().node().className+'">';f.header&&(j+="<thead>"+i(h.header,"th")+"</thead>"),j+="<tbody>";for(var k=0,l=h.body.length;l>k;k++)j+=i(h.body[k],"td");j+="</tbody>",f.footer&&(j+="<tfoot>"+i(h.footer,"th")+"</tfoot>");var m=b.open("",""),n=f.title;"function"==typeof n&&(n=n()),-1!==n.indexOf("*")&&(n=n.replace("*",a("title").text())),m.document.close();var o="<title>"+n+"</title>";a("style, link").each(function(){o+=g(this)}),a(m.document.head).html(o),a(m.document.body).html("<h1>"+n+"</h1><div>"+f.message+"</div>"+j),f.customize&&f.customize(m),setTimeout(function(){f.autoPrint&&(m.print(),m.close())},250)},title:"*",message:"",exportOptions:{},header:!0,footer:!1,autoPrint:!0,customize:null},e.Buttons}); -------------------------------------------------------------------------------- /backend/web/static/js/datatables.Chinese.json: -------------------------------------------------------------------------------- 1 | { 2 | "sProcessing": "处理中...", 3 | "sLengthMenu": "显示 _MENU_ 项结果", 4 | "sZeroRecords": "没有匹配结果", 5 | "sInfo": "显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项", 6 | "sInfoEmpty": "显示第 0 至 0 项结果,共 0 项", 7 | "sInfoFiltered": "(由 _MAX_ 项结果过滤)", 8 | "sInfoPostFix": "", 9 | "sSearch": "搜索:", 10 | "sUrl": "", 11 | "sEmptyTable": "表中数据为空", 12 | "sLoadingRecords": "载入中...", 13 | "sInfoThousands": ",", 14 | "oPaginate": { 15 | "sFirst": "首页", 16 | "sPrevious": "上页", 17 | "sNext": "下页", 18 | "sLast": "末页" 19 | }, 20 | "oAria": { 21 | "sSortAscending": ": 以升序排列此列", 22 | "sSortDescending": ": 以降序排列此列" 23 | } 24 | } -------------------------------------------------------------------------------- /backend/web/static/js/draw.js: -------------------------------------------------------------------------------- 1 | 2 | var drawing = false; 3 | 4 | var context; 5 | 6 | var offset_left = 0; 7 | var offset_top = 0; 8 | 9 | 10 | function start_canvas () 11 | { 12 | var scribbler = document.getElementById ("the_stage"); 13 | context = scribbler.getContext ("2d"); 14 | scribbler.onmousedown = function (event) {mousedown(event)}; 15 | scribbler.onmousemove = function (event) {mousemove(event)}; 16 | scribbler.onmouseup = function (event) {mouseup(event)}; 17 | for (var o = scribbler; o ; o = o.offsetParent) { 18 | offset_left += (o.offsetLeft - o.scrollLeft); 19 | offset_top += (o.offsetTop - o.scrollTop); 20 | } 21 | draw(); 22 | } 23 | 24 | function getPosition(evt) 25 | { 26 | evt = (evt) ? evt : ((event) ? event : null); 27 | var left = 0; 28 | var top = 0; 29 | var scribbler = document.getElementById("the_stage"); 30 | 31 | if (evt.pageX) { 32 | left = evt.pageX; 33 | top = evt.pageY; 34 | } else if (document.documentElement.scrollLeft) { 35 | left = evt.clientX + document.documentElement.scrollLeft; 36 | top = evt.clientY + document.documentElement.scrollTop; 37 | } else { 38 | left = evt.clientX + document.body.scrollLeft; 39 | top = evt.clientY + document.body.scrollTop; 40 | } 41 | left -= offset_left; 42 | top -= offset_top; 43 | 44 | return {x : left, y : top}; 45 | } 46 | 47 | function 48 | mousedown(event) 49 | { 50 | drawing = true; 51 | var location = getPosition(event); 52 | context.lineWidth = 20.0; 53 | context.strokeStyle="#000000"; 54 | context.beginPath(); 55 | context.moveTo(location.x,location.y); 56 | } 57 | 58 | 59 | function 60 | mousemove(event) 61 | { 62 | if (!drawing) 63 | return; 64 | var location = getPosition(event); 65 | context.lineTo(location.x,location.y); 66 | context.stroke(); 67 | } 68 | 69 | 70 | 71 | function 72 | mouseup(event) 73 | { 74 | if (!drawing) 75 | return; 76 | mousemove(event); 77 | drawing = false; 78 | } 79 | 80 | function draw() 81 | { 82 | 83 | context.fillStyle = '#ffffff'; 84 | context.fillRect(0, 0, 400, 400); 85 | 86 | } 87 | 88 | function clearCanvas() 89 | { 90 | context.clearRect (0, 0, 400, 400); 91 | draw(); 92 | document.getElementById("rec_result").innerHTML = ""; 93 | } 94 | 95 | onload = start_canvas; 96 | 97 | -------------------------------------------------------------------------------- /backend/web/static/js/jquery.dataTables.bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /* Set the defaults for DataTables initialisation */ 2 | $.extend(!0,$.fn.dataTable.defaults,{sDom:"<'row'<'col-xs-6'l><'col-xs-6'f>r>t<'row'<'col-xs-6'i><'col-xs-6'p>>",oLanguage:{sLengthMenu:"Display _MENU_ records"}}),$.extend($.fn.dataTableExt.oStdClasses,{sWrapper:"dataTables_wrapper form-inline",sFilterInput:"form-control input-sm",sLengthSelect:"form-control input-sm"}),$.fn.dataTable.Api?($.fn.dataTable.defaults.renderer="bootstrap",$.fn.dataTable.ext.renderer.pageButton.bootstrap=function(a,b,c,d,e,f){var g,h,i=new $.fn.dataTable.Api(a),j=a.oClasses,k=a.oLanguage.oPaginate,l=function(b,d){var m,n,o,p,q=function(a){return a.preventDefault(),$(a.target).parent().hasClass("disabled")?!1:void("ellipsis"!==a.data.action&&i.page(a.data.action).draw(!1))};for(m=0,n=d.length;n>m;m++)if(p=d[m],$.isArray(p))l(b,p);else{switch(g="",h="",p){case"ellipsis":g="…",h="disabled";break;case"first":g=k.sFirst,h=p+(e>0?"":" disabled");break;case"previous":g=k.sPrevious,h=p+(e>0?"":" disabled");break;case"next":g=k.sNext,h=p+(f-1>e?"":" disabled");break;case"last":g=k.sLast,h=p+(f-1>e?"":" disabled");break;default:g=p+1,h=e===p?"active":""}g&&(o=$("<li>",{"class":j.sPageButton+" "+h,"aria-controls":a.sTableId,tabindex:a.iTabIndex,id:0===c&&"string"==typeof p?a.sTableId+"_"+p:null}).append($("<a>",{href:"#"}).html(g)).appendTo(b),a.oApi._fnBindAction(o,{action:p},q))}};l($(b).empty().html('<ul class="pagination"/>').children("ul"),d)}):($.fn.dataTable.defaults.sPaginationType="bootstrap",$.fn.dataTableExt.oApi.fnPagingInfo=function(a){return{iStart:a._iDisplayStart,iEnd:a.fnDisplayEnd(),iLength:a._iDisplayLength,iTotal:a.fnRecordsTotal(),iFilteredTotal:a.fnRecordsDisplay(),iPage:-1===a._iDisplayLength?0:Math.ceil(a._iDisplayStart/a._iDisplayLength),iTotalPages:-1===a._iDisplayLength?0:Math.ceil(a.fnRecordsDisplay()/a._iDisplayLength)}},$.extend($.fn.dataTableExt.oPagination,{bootstrap:{fnInit:function(a,b,c){var d=(a.oLanguage.oPaginate,function(b){alert(1),b.preventDefault(),a.oApi._fnPageChange(a,b.data.action)&&c(a)});$(b).append('<ul class="pagination"><li class="prev disabled"><a href="#"><i class="fa fa-angle-double-left"></i></a></li><li class="prev disabled"><a href="#"><i class="fa fa-angle-left"></i></a></li><li class="next disabled"><a href="#"><i class="fa fa-angle-right"></i></a></li><li class="next disabled"><a href="#"><i class="fa fa-angle-double-right"></i></a></li></ul>');var e=$("a",b);$(e[0]).bind("click.DT",{action:"first"},d),$(e[1]).bind("click.DT",{action:"previous"},d),$(e[2]).bind("click.DT",{action:"next"},d),$(e[3]).bind("click.DT",{action:"last"},d)},fnUpdate:function(a,b){var c,d,e,f,g,h,i=5,j=a.oInstance.fnPagingInfo(),k=a.aanFeatures.p,l=Math.floor(i/2);for(j.iTotalPages<i?(g=1,h=j.iTotalPages):j.iPage<=l?(g=1,h=i):j.iPage>=j.iTotalPages-l?(g=j.iTotalPages-i+1,h=j.iTotalPages):(g=j.iPage-l+1,h=g+i-1),c=0,d=k.length;d>c;c++){for($("li:gt(0)",k[c]).filter(":not(.next,.prev)").remove(),e=g;h>=e;e++)f=e==j.iPage+1?'class="active"':"",$("<li "+f+'><a href="#">'+e+"</a></li>").insertBefore($("li.next:eq(0)",k[c])[0]).bind("click",function(c){c.preventDefault(),a._iDisplayStart=(parseInt($("a",this).text(),10)-1)*j.iLength,b(a)});0===j.iPage?$("li.prev",k[c]).addClass("disabled"):$("li.prev",k[c]).removeClass("disabled"),j.iPage===j.iTotalPages-1||0===j.iTotalPages?$("li.next",k[c]).addClass("disabled"):$("li.next",k[c]).removeClass("disabled")}}}})),$.fn.DataTable.TableTools&&($.extend(!0,$.fn.DataTable.TableTools.classes,{container:"DTTT btn-group",buttons:{normal:"btn btn-default",disabled:"disabled"},collection:{container:"DTTT_dropdown dropdown-menu",buttons:{normal:"",disabled:"disabled"}},print:{info:"DTTT_print_info modal"},select:{row:"active"}}),$.extend(!0,$.fn.DataTable.TableTools.DEFAULTS.oTags,{collection:{container:"ul",button:"li",liner:"a"}})); -------------------------------------------------------------------------------- /backend/web/static/update_bokeh.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | rm -f ./js/bokeh.min.js 4 | rm -f ./js/bokeh-api.min.js 5 | rm -f ./js/bokeh-gl.min.js 6 | rm -f ./js/bokeh-tables.min.js 7 | rm -f ./js/bokeh-widgets.min.js 8 | 9 | cp /usr/local/lib/python3.7/site-packages/bokeh/server/static/js/bokeh.min.js ./js/ 10 | cp /usr/local/lib/python3.7/site-packages/bokeh/server/static/js/bokeh-api.min.js ./js/ 11 | cp /usr/local/lib/python3.7/site-packages/bokeh/server/static/js/bokeh-gl.min.js ./js/ 12 | cp /usr/local/lib/python3.7/site-packages/bokeh/server/static/js/bokeh-tables.min.js ./js/ 13 | cp /usr/local/lib/python3.7/site-packages/bokeh/server/static/js/bokeh-widgets.min.js ./js/ -------------------------------------------------------------------------------- /backend/web/templates/bokeh_embed.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | 3 | <html lang="en"> 4 | <head> 5 | <meta charset="utf-8"> 6 | <title>Embedding a Bokeh Server With {{ framework }}</title> 7 | </head> 8 | 9 | <body> 10 | <div> 11 | This Bokeh app below served by a Bokeh server that has been embedded 12 | in another web app framework. For more information see the section 13 | <a target="_blank" 14 | href="https://bokeh.pydata.org/en/latest/docs/user_guide/server.html#embedding-bokeh-server-as-a-library"> 15 | Embedding Bokeh Server as a Library</a> 16 | in the User's Guide. 17 | </div> 18 | {{ script|safe }} 19 | </body> 20 | </html> -------------------------------------------------------------------------------- /backend/web/templates/common/footer.html: -------------------------------------------------------------------------------- 1 | {% block footer %} 2 | {% end %} -------------------------------------------------------------------------------- /backend/web/templates/common/header.html: -------------------------------------------------------------------------------- 1 | {% block header %} 2 | <div id="navbar" class="navbar navbar-default ace-save-state"> 3 | <div class="navbar-container ace-save-state" id="navbar-container"> 4 | <div class="navbar-header pull-left"> 5 | <a href="/stock/" class="navbar-brand"> 6 | <small><i class="fa fa-leaf"></i>开源Python全栈股票系统,数据抓取、统计分析、报表展示 版本:{{ pythonStockVersion }}</small> 7 | </a> 8 | </div> 9 | 10 | </div><!-- /.navbar-container --> 11 | </div> 12 | {% end %} -------------------------------------------------------------------------------- /backend/web/templates/common/left_menu.html: -------------------------------------------------------------------------------- 1 | {% block left_menu %} 2 | <div id="sidebar" class="sidebar responsive ace-save-state"> 3 | <script type="text/javascript"> 4 | try{ace.settings.loadState('sidebar')}catch(e){} 5 | </script> 6 | 7 | <ul class="nav nav-list"> 8 | <li class=""> 9 | <a href="/"> 10 | <i class="menu-icon fa fa-tachometer"></i> 11 | <span class="menu-text"> Dashboard </span> 12 | </a> 13 | <b class="arrow"></b> 14 | </li> 15 | 16 | <li class="active open"> 17 | <!--<a href="#" class="dropdown-toggle">--> 18 | <!--<i class="menu-icon fa fa-desktop"></i>--> 19 | <!--<span class="menu-text">--> 20 | <!--股票原始数据--> 21 | <!--</span>--> 22 | <!--<b class="arrow fa fa-angle-down"></b>--> 23 | <!--</a>--> 24 | <!--<b class="arrow"></b>--> 25 | <li class="submenu"> 26 | <li class="open" style="display:none;"> 27 | {% set loopType = "" %} 28 | {% for leftMenuTmp in leftMenu.leftMenuList %} 29 | 30 | {% if leftMenuTmp.type != loopType %} 31 | </li> 32 | <li class="open"> 33 | {% set loopType = leftMenuTmp.type %} 34 | <a href="#" class="dropdown-toggle"> 35 | <i class="menu-icon fa fa-pencil-square-o"></i> 36 | {{ leftMenuTmp.type }} 37 | <b class="arrow fa fa-angle-down"></b> 38 | </a> 39 | <b class="arrow"></b> 40 | 41 | {% end %} 42 | 43 | <ul class="submenu"> 44 | <li> 45 | <a href="{{ leftMenuTmp.url }}"> 46 | <i class="menu-icon fa fa-caret-right"></i> 47 | {{ leftMenuTmp.name }} 48 | </a> 49 | <b class="arrow"></b> 50 | </li> 51 | </ul> 52 | 53 | {% end %} 54 | </li> 55 | </ul> 56 | 57 | </li> 58 | </ul><!-- /.nav-list --> 59 | 60 | <div class="sidebar-toggle sidebar-collapse" id="sidebar-collapse"> 61 | <i id="sidebar-toggle-icon" class="ace-icon fa fa-angle-double-left ace-save-state" data-icon1="ace-icon fa fa-angle-double-left" data-icon2="ace-icon fa fa-angle-double-right"></i> 62 | </div> 63 | </div> 64 | {% end %} -------------------------------------------------------------------------------- /backend/web/templates/common/meta.html: -------------------------------------------------------------------------------- 1 | {% block meta %} 2 | <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" /> 3 | <!-- bootstrap & fontawesome --> 4 | <link rel="stylesheet" href="/static/css/bootstrap.min.css" /> 5 | <link rel="stylesheet" href="/static/font-awesome/4.5.0/css/font-awesome.min.css" /> 6 | <!-- ace styles --> 7 | <link rel="stylesheet" href="/static/css/ace.min.css" class="ace-main-stylesheet" id="main-ace-style" /> 8 | <!-- basic scripts --> 9 | <script src="/static/js/jquery-2.1.4.min.js"></script> 10 | <script src="/static/js/ace-extra.min.js"></script> 11 | <script src="/static/js/bootstrap.min.js"></script> 12 | 13 | <!-- page specific plugin scripts --> 14 | <script src="/static/js/jquery.dataTables.min.js"></script> 15 | <script src="/static/js/jquery.dataTables.bootstrap.min.js"></script> 16 | <script src="/static/js/dataTables.buttons.min.js"></script> 17 | <script src="/static/js/dataTables.select.min.js"></script> 18 | <!-- ace scripts --> 19 | <script src="/static/js/moment.min.js"></script> 20 | <script src="/static/js/bootstrap-datepicker.min.js"></script> 21 | <script src="/static/js/bootstrap-datetimepicker.min.js"></script> 22 | <script src="/static/js/bootstrap-datepicker.zh-CN.js"></script> 23 | 24 | <link rel="stylesheet" href="/static/css/bootstrap-datepicker3.min.css" /> 25 | <link rel="stylesheet" href="/static/css/bootstrap-datetimepicker.min.css" /> 26 | 27 | <script src="/static/js/ace-elements.min.js"></script> 28 | <script src="/static/js/ace.min.js"></script> 29 | <script src="/static/js/jquery-ui.custom.min.js"></script> 30 | <script src="/static/js/bootbox.js"></script> 31 | 32 | <script src="/static/js/buttons.colVis.min.js"></script> 33 | <script src="/static/js/buttons.print.min.js"></script> 34 | <script src="/static/js/buttons.html5.min.js"></script> 35 | 36 | 37 | <script type='text/javascript'> 38 | jQuery(function($) { 39 | //初始化窗口。 40 | $('.modal.aside').ace_aside(); 41 | $('#aside-inside-modal').addClass('aside').ace_aside({container: ""}); 42 | $(document).one('ajaxloadstart.page', function(e) { 43 | $('.modal.aside').remove(); 44 | $(window).off('.aside') 45 | }); 46 | }) 47 | $.extend({ 48 | getUrlVars: function(){ 49 | var vars = [], hash; 50 | var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); 51 | for(var i = 0; i < hashes.length; i++) 52 | { 53 | hash = hashes[i].split('='); 54 | vars.push(hash[0]); 55 | vars[hash[0]] = hash[1]; 56 | } 57 | return vars; 58 | }, 59 | getUrlVar: function(name){ 60 | return $.getUrlVars()[name]; 61 | } 62 | }); 63 | jQuery(function($) { 64 | $('[data-rel=tooltip]').tooltip(); 65 | $('[data-rel=popover]').popover({html:true}); 66 | }); 67 | </script> 68 | <style> 69 | #dynamic-table_filter {display: none;} 70 | </style> 71 | {% end %} -------------------------------------------------------------------------------- /backend/web/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/default.html" %} 2 | 3 | 4 | {% block main_content %} 5 | 6 | <h3 class="header smaller lighter blue">开源Python全栈股票系统,数据抓取、统计分析、报表展示。</h3> 7 | 8 | <div class="clearfix"> 9 | <div class="pull-left tableTools-container"> 10 | <h3>基础库版本</h3> 11 | <p>1,pandas使用【 {{ pandasVersion }} 】版本, <a href="https://www.pypandas.cn/docs/" target="_blank">中文文档</a> </p> 12 | <p>2,numpy使用【 {{ numpyVersion }} 】版本, <a href="https://www.numpy.org.cn/user/" target="_blank">中文文档</a></p> 13 | <p>3,akshare使用【 {{ akshareVersion }} 】版本, <a href="https://www.akshare.xyz/" target="_blank">中文文档</a></p> 14 | <p>4,bokeh使用【 {{ bokehVersion }} 】版本, <a href="http://docs.bokeh.org/en/latest/" target="_blank">官方文档</a></p> 15 | <p>5,stockstats使用【 {{ stockstatsVersion }} 】版本, <a href="https://github.com/jealous/stockstats/" target="_blank">官方文档</a></p> 16 | </div> 17 | </div> 18 | 19 | <div class="clearfix"> 20 | <div class="pull-left tableTools-container"> 21 | <h3>相关资料信息</h3> 22 | <p>1,github项目地址。<a href="https://github.com/pythonstock/stock" target="_blank">pythonstock</a></p> 23 | <p>2,博客地址。<a href="https://blog.csdn.net/freewebsys/category_9285317.html" target="_blank">博客地址</a></p> 24 | </div> 25 | </div> 26 | 27 | <div class="clearfix"> 28 | <div class="pull-left tableTools-container"> 29 | <h3>2021年9月20日更新,发布2.0版本</h3> 30 | <p>1,修复bokeh的版本升级问题,可以显示趋势数据了。</p> 31 | <p>2,使用 stock_zh_ah_name 做每日数据。</p> 32 | <p>3,AkShare 升级到 1.1.9 版本。</p> 33 | </div> 34 | </div> 35 | 36 | <div class="clearfix"> 37 | <div class="pull-left tableTools-container"> 38 | <h3>2021年8月31日更新job服务</h3> 39 | <p>1,过滤包括:600,6006,601,000,001,002,且不包括ST的股票数据。。</p> 40 | <p>2,使用 stock_zh_ah_name 做每日数据。</p> 41 | <p>3,AkShare 升级到 1.0.80 版本。</p> 42 | 43 | </div> 44 | </div> 45 | 46 | <div class="clearfix"> 47 | <div class="pull-left tableTools-container"> 48 | <h3>2021年6月3日使用 AkShare 做数据抓取服务</h3> 49 | <p>1,使用 stock_zh_a_spot 做实时行情数据。</p> 50 | <p>2,使用 stock_zh_a_daily 做历史数据统计。</p> 51 | <p>3,升级基础镜像使用python3.7,AkShare 的 0.9.65 版本。</p> 52 | 53 | </div> 54 | </div> 55 | 56 | {% end %} -------------------------------------------------------------------------------- /backend/web/templates/layout/default.html: -------------------------------------------------------------------------------- 1 | {% extends "../common/meta.html" %} 2 | {% extends "../common/header.html" %} 3 | {% extends "../common/footer.html" %} 4 | {% extends "../common/left_menu.html" %} 5 | 6 | {% extends "main.html" %} -------------------------------------------------------------------------------- /backend/web/templates/layout/indicators-main.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> 5 | <meta charset="utf-8" /> 6 | <title>股票系统</title> 7 | {% block meta %}{% end %} 8 | </head> 9 | 10 | <body class="no-skin"> 11 | <div class="main-content"> 12 | <div class="main-content-inner"> 13 | <div class="page-content"> 14 | <div class="row"> 15 | <div class="col-xs-12"> 16 | {% block main_content %}{% end %} 17 | </div> 18 | </div><!-- /.row --> 19 | </div><!-- /.page-content --> 20 | </div> 21 | </div><!-- /.main-content --> 22 | </body> 23 | </html> 24 | -------------------------------------------------------------------------------- /backend/web/templates/layout/indicators.html: -------------------------------------------------------------------------------- 1 | {% extends "../common/meta.html" %} 2 | 3 | {% extends "indicators-main.html" %} -------------------------------------------------------------------------------- /backend/web/templates/layout/main.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> 5 | <meta charset="utf-8" /> 6 | <title>股票系统 {{ pythonStockVersion }}</title> 7 | {% block meta %}{% end %} 8 | </head> 9 | 10 | <body class="no-skin"> 11 | 12 | {% block header %}{% end %} 13 | 14 | <div class="main-container ace-save-state" id="main-container"> 15 | <script type="text/javascript"> 16 | try{ace.settings.loadState('main-container')}catch(e){} 17 | </script> 18 | 19 | {% block left_menu %}{% end %} 20 | 21 | <div class="main-content"> 22 | <div class="main-content-inner"> 23 | <div class="page-content"> 24 | <div class="row"> 25 | <div class="col-xs-12"> 26 | {% block main_content %}{% end %} 27 | </div> 28 | </div><!-- /.row --> 29 | </div><!-- /.page-content --> 30 | </div> 31 | </div><!-- /.main-content --> 32 | 33 | <a href="#" id="btn-scroll-up" class="btn-scroll-up btn btn-sm btn-inverse"> 34 | <i class="ace-icon fa fa-angle-double-up icon-only bigger-110"></i> 35 | </a> 36 | </div><!-- /.main-container --> 37 | </body> 38 | </html> 39 | -------------------------------------------------------------------------------- /backend/web/templates/layout/single_default.html: -------------------------------------------------------------------------------- 1 | {% extends "../common/meta.html" %} 2 | {% extends "../common/header.html" %} 3 | {% extends "../common/footer.html" %} 4 | 5 | {% extends "single_main.html" %} -------------------------------------------------------------------------------- /backend/web/templates/layout/single_main.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> 5 | <meta charset="utf-8" /> 6 | {% block meta %}{% end %} 7 | </head> 8 | 9 | <body class="no-skin"> 10 | 11 | <div class="main-container ace-save-state" id="main-container"> 12 | <div class="main-content"> 13 | <div class="main-content-inner"> 14 | <div class="page-content"> 15 | <div class="row"> 16 | <div class="col-xs-12"> 17 | {% block main_content %}{% end %} 18 | </div> 19 | </div><!-- /.row --> 20 | </div><!-- /.page-content --> 21 | </div> 22 | </div><!-- /.main-content --> 23 | 24 | <a href="#" id="btn-scroll-up" class="btn-scroll-up btn btn-sm btn-inverse"> 25 | <i class="ace-icon fa fa-angle-double-up icon-only bigger-110"></i> 26 | </a> 27 | </div><!-- /.main-container --> 28 | </body> 29 | </html> 30 | -------------------------------------------------------------------------------- /backend/web/templates/minst_serving.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/single_default.html" %} 2 | 3 | 4 | {% block main_content %} 5 | 6 | 7 | <h3 class="header smaller lighter blue">手写图片识别演示</h3> 8 | 9 | <div class="row clearfix"> 10 | <div class="col-md-6 column"> 11 | <canvas id="the_stage" width="280" height="280">your browser don't support canvas!</canvas> 12 | <div> 13 | <button type="button" class="btn btn-default butt" onclick="clearCanvas()"><strong>删除</strong></button> 14 | <button type="button" class="btn btn-default butt" id="recognize" onclick="processImg()"><strong>识别</strong></button> 15 | </div> 16 | </div> 17 | <div class="col-md-6 column"> 18 | <h3>result:</h3> 19 | <h2 id="rec_result"></h2> 20 | </div> 21 | </div> 22 | 23 | <h3 class="header smaller lighter blue">测试图片识别演示</h3> 24 | <div class="row clearfix" style="width:400px;"> 25 | {% for image in image_array %} 26 | <div class="col-md-2" style="margin-top: 15px;"> 27 | <img src="{{ image }}" alt="" width="50" class="minst_img" height="50" style="1px solid #e1e4e5;cursor: pointer;"> 28 | </div> 29 | {% end %} 30 | </div> 31 | <div class="col-md-6 column"> 32 | <h3>结果:</h3> 33 | <h2 id="prediction_text">"-1"</h2> 34 | </div> 35 | <!-- inline scripts related to this page --> 36 | <script src="/static/js/mnist-draw.js"></script> 37 | <style> 38 | #the_stage { 39 | border: 1px solid #999; 40 | border-radius: 4px; 41 | box-shadow: -10px 10px 5px #888888; 42 | } 43 | </style> 44 | <script type="text/javascript"> 45 | jQuery(function($) { 46 | $("#recognize").click( 47 | //提交数据 48 | function () { 49 | $("#rec_result").html("connecting..."); 50 | var scribbler = document.getElementById ("the_stage"); 51 | var imageData = scribbler.toDataURL('image/png'); 52 | var dataTemp = imageData.substr(22); 53 | 54 | var sendPackage = {"id": "1", "txt": dataTemp}; 55 | $.post("/minst_serving/prediction2", sendPackage, function(msg){ 56 | $("#rec_result").html(msg); 57 | }); 58 | } 59 | ); 60 | 61 | $(".minst_img").click( 62 | //提交数据 63 | function () { 64 | img_url = $(this).attr("src") 65 | console.log("here",); 66 | // 67 | $.ajax({ 68 | type: "GET", 69 | url: "/minst_serving/prediction", 70 | data: "img_url="+img_url, 71 | success: function(msg){ 72 | console.log( "prediction : " + msg ); 73 | $("#prediction_text").html(msg); 74 | } 75 | }); 76 | } 77 | ); 78 | }); 79 | </script> 80 | {% end %} -------------------------------------------------------------------------------- /backend/web/templates/stock_chart.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/default.html" %} 2 | 3 | 4 | {% block main_content %} 5 | 6 | <h3 class="header smaller lighter blue">欢迎使用股票系统。</h3> 7 | 8 | <div class="clearfix"> 9 | <div class="pull-right tableTools-container"></div> 10 | <img src="/stock/chart/image1" /> 11 | </div> 12 | {% end %} -------------------------------------------------------------------------------- /backend/web/templates/stock_indicators.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/indicators.html" %} 2 | 3 | 4 | {% block main_content %} 5 | 6 | <!-- 增加 bokeh 样式。--> 7 | <link rel="stylesheet" href="/static/css/bokeh.min.css" type="text/css"/> 8 | <link rel="stylesheet" href="/static/css/bokeh-widgets.min.css" type="text/css"/> 9 | <link rel="stylesheet" href="/static/css/bokeh-tables.min.css" type="text/css"/> 10 | 11 | <script type="text/javascript" src="/static/js/bokeh.min.js"></script> 12 | <script type="text/javascript" src="/static/js/bokeh-widgets.min.js"></script> 13 | <script type="text/javascript" src="/static/js/bokeh-tables.min.js"></script> 14 | <script type="text/javascript" src="/static/js/bokeh-api.min.js"></script> 15 | <script type="text/javascript" src="/static/js/bokeh-gl.min.js"></script> 16 | 17 | 18 | 19 | <script type="text/javascript"> 20 | Bokeh.set_log_level("info"); 21 | </script> 22 | 23 | {% for index,element in enumerate(comp_list) %} 24 | <h4 class="header smaller lighter blue">{{ element["title"] }}</h4> 25 | <div class="row">{{ element["desc"] }}</div> 26 | <div class="row" id="_col_{{ index+1 }}" data-column="{{ index }}"> 27 | {% raw element["div"] %} 28 | {% raw element["script"] %} 29 | </div> 30 | {% end %} 31 | 32 | {% end %} -------------------------------------------------------------------------------- /backend/web/templates/test.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/default.html" %} 2 | 3 | 4 | {% block main_content %} 5 | 6 | <h3 class="header smaller lighter blue">欢迎使用股票系统。</h3> 7 | 8 | <div class="clearfix"> 9 | <div class="pull-right tableTools-container"></div> 10 | </div> 11 | 12 | <div class="col-sm-1"> 13 | <button onclick="showDFCFWindow('SZ000001');" role="button" class="btn btn-sm btn-primary" data-toggle="modal"> 14 | <i class="ace-icon fa fa-plus"></i> 15 | 测试弹窗东方财富窗口 16 | </button> 17 | </div><!-- /.col --> 18 | 19 | <!--<iframe src="https://emweb.eastmoney.com/PC_HSF10/ShareholderResearch/Index?type=soft&code=SZ000001" width="700px" height="700px"></iframe>--> 20 | 21 | <!-- 定义一个窗口的Div 东方财富股票数据页面 --> 22 | <div id="dfcf-window-modal" class="modal fade"> 23 | <div class="modal-dialog" style="width:1010px;"> 24 | <div class="modal-content"> 25 | <div class="modal-header"> 26 | <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> 27 | <h3 class="smaller lighter blue no-margin">东方财富分析</h3> 28 | </div> 29 | <div class="modal-body" id="dfcf-iframe-body" > 30 | </div> 31 | </div> 32 | </div> 33 | </div> 34 | 35 | <script type="text/javascript"> 36 | //每次动态加载 东方财富窗口 37 | function showDFCFWindow(code) { 38 | 39 | var iframe = document.createElement('iframe') 40 | var baseUrl = 'https://emweb.eastmoney.com/PC_HSF10/ShareholderResearch/Index?type=soft&code='+code; 41 | // var baseUrl = 'https://emweb.eastmoney.com/PC_HSF10/OperationsRequired/Index?type=soft&code='; 42 | iframe.src = baseUrl; 43 | iframe.width = '980px'; 44 | iframe.height = '700px'; 45 | iframe.frameborder = '0'; 46 | $('#iframe-body').empty(); // 先清空数据 47 | document.getElementById("dfcf-iframe-body").appendChild(iframe) 48 | $('#dfcf-window-modal').modal('show'); 49 | } 50 | </script> 51 | 52 | {% end %} -------------------------------------------------------------------------------- /backend/web/templates/test2.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/default.html" %} 2 | 3 | 4 | {% block main_content %} 5 | 6 | <h3 class="header smaller lighter blue">欢迎使用股票系统。</h3> 7 | 8 | <div class="clearfix"> 9 | <div class="pull-right tableTools-container"></div> 10 | </div> 11 | 12 | <div class="col-sm-1"> 13 | <button onclick="showIndicatorsWindow('000001');" role="button" class="btn btn-sm btn-primary" data-toggle="modal"> 14 | <i class="ace-icon fa fa-plus"></i> 15 | 测试弹窗指标分析窗口 16 | </button> 17 | </div><!-- /.col --> 18 | 19 | <!--<iframe src="https://emweb.eastmoney.com/PC_HSF10/ShareholderResearch/Index?type=soft&code=SZ000001" width="700px" height="700px"></iframe>--> 20 | 21 | <!-- 定义一个窗口的Div 指标分析数据页面 --> 22 | <div id="indicators-window-modal" class="modal fade"> 23 | <div class="modal-dialog" style="width:1050px;"> 24 | <div class="modal-content"> 25 | <div class="modal-header"> 26 | <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> 27 | <h3 class="smaller lighter blue no-margin">股票数据详细指标分析</h3> 28 | </div> 29 | <div class="modal-body" id="indicators-window-body" > 30 | </div> 31 | </div> 32 | </div> 33 | </div> 34 | 35 | <script type="text/javascript"> 36 | //每次动态加载 指标分析窗口 37 | function showIndicatorsWindow(code) { 38 | var baseUrl = '/data/indicators?code='+code; // 没有跨域问题,直接加载 39 | $('#indicators-window-body').context = $("#indicators-window-body").load(baseUrl); 40 | $('#indicators-window-modal').modal('show'); 41 | } 42 | </script> 43 | 44 | {% end %} -------------------------------------------------------------------------------- /backend/web/test_thread.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import time 4 | from tornado.httpserver import HTTPServer 5 | from tornado.ioloop import IOLoop 6 | from tornado.web import Application, asynchronous, RequestHandler 7 | from multiprocessing.pool import ThreadPool 8 | from multiprocessing.pool import ApplyResult 9 | from tornado import gen 10 | 11 | # https://gist.github.com/methane/2185380 参考 12 | 13 | 14 | 15 | html_content = """ 16 | <!DOCTYPE html> 17 | <html lang="zh-CN"> 18 | <head> 19 | <script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script> 20 | </head> 21 | <body> 22 | <h1>任务测试</h1></br> 23 | <button id="job">开始</button> 24 | </body> 25 | </html> 26 | <script type="text/javascript"> 27 | function job_check(timer,tid) { 28 | $.ajax({ 29 | type: "GET", 30 | url: "job_check?tid="+tid, 31 | success: function(msg){ 32 | console.log(msg); 33 | if(msg != ""){ 34 | alert( "任务结果: " + msg ); 35 | clearInterval(timer);//结束轮询 36 | } 37 | } 38 | }); 39 | } 40 | jQuery(function($) { 41 | 42 | $("#job").click( function () { 43 | $.ajax({ 44 | type: "GET", 45 | url: "add_job", 46 | success: function(tid){ 47 | alert( "开始任务: " + tid ); 48 | timer = setInterval(function(){ 49 | console.log("run."); 50 | job_check(timer,tid); 51 | },1000); 52 | } 53 | }); 54 | }); 55 | }) 56 | </script> 57 | """ 58 | 59 | 60 | class MainPage(RequestHandler): 61 | def get(self): 62 | self.write(html_content) 63 | 64 | 65 | _workers = ThreadPool(10) 66 | _result = {} 67 | 68 | 69 | # 后台任务。 70 | def blocking_task(n, tid): 71 | time.sleep(n) 72 | print(tid) 73 | _result[tid] = {"finish"} 74 | 75 | 76 | class AddJobHandler(RequestHandler): 77 | @gen.coroutine 78 | def get(self): 79 | tid = str(int(time.time() * 10000)) 80 | _workers.apply_async(blocking_task, (10, tid)) # 传递参数 10 秒。 81 | self.write(tid) 82 | self.finish() # 先finish 掉,然后在后台执行。 83 | 84 | 85 | class JobCheckHandler(RequestHandler): 86 | def get(self): 87 | tid = self.get_argument("tid") 88 | if tid in _result.keys(): 89 | out = _result[tid] # 结果 90 | del _result[tid] # 删除tid的数据。 91 | self.write(str(out)) 92 | else: 93 | self.write("") 94 | 95 | 96 | # main 启动。 97 | if __name__ == "__main__": 98 | HTTPServer(Application([ 99 | ("/", MainPage), 100 | ("/add_job", AddJobHandler), 101 | ("/job_check", JobCheckHandler) 102 | ], debug=True)).listen(9090) 103 | print("start web .") 104 | IOLoop.instance().start() 105 | -------------------------------------------------------------------------------- /backend/web/test_thread_v2.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import time 4 | from tornado.httpserver import HTTPServer 5 | from tornado.ioloop import IOLoop 6 | from tornado.web import Application, asynchronous, RequestHandler 7 | from tornado import gen 8 | from tornado.concurrent import run_on_executor 9 | from concurrent.futures import ThreadPoolExecutor 10 | 11 | # `pip install futures` for python2 12 | 13 | # https://gist.github.com/methane/2185380 参考 14 | 15 | html_content = """ 16 | <!DOCTYPE html> 17 | <html lang="zh-CN"> 18 | <head> 19 | <script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script> 20 | </head> 21 | <body> 22 | <h1>任务测试</h1></br> 23 | <button id="job">开始</button> 24 | </body> 25 | </html> 26 | <script type="text/javascript"> 27 | function job_check(timer,tid) { 28 | $.ajax({ 29 | type: "GET", 30 | url: "job_check?tid="+tid, 31 | success: function(msg){ 32 | console.log(msg); 33 | if(msg != ""){ 34 | alert( "任务结果: " + msg ); 35 | clearInterval(timer);//结束轮询 36 | } 37 | } 38 | }); 39 | } 40 | jQuery(function($) { 41 | 42 | $("#job").click( function () { 43 | $.ajax({ 44 | type: "GET", 45 | url: "add_job", 46 | success: function(tid){ 47 | alert( "开始任务: " + tid ); 48 | timer = setInterval(function(){ 49 | console.log("run."); 50 | job_check(timer,tid); 51 | },1000); 52 | } 53 | }); 54 | }); 55 | }) 56 | </script> 57 | """ 58 | 59 | 60 | class MainPage(RequestHandler): 61 | def get(self): 62 | self.write(html_content) 63 | 64 | 65 | MAX_WORKERS = 4 66 | _result = {} 67 | 68 | 69 | class AddJobHandler(RequestHandler): 70 | # 必须定义一个executor的属性,然后run_on_executor 注解才管用。 71 | executor = ThreadPoolExecutor(max_workers=MAX_WORKERS) 72 | 73 | @run_on_executor # 标记成后台程序执行。 74 | def background_task(self, tid): 75 | time.sleep(10) # 传递参数 10 秒。 76 | _result[tid] = {"finish"} 77 | 78 | @gen.coroutine 79 | def get(self): 80 | tid = str(int(time.time() * 10000)) 81 | self.background_task(tid) 82 | self.write(tid) 83 | 84 | 85 | class JobCheckHandler(RequestHandler): 86 | def get(self): 87 | tid = self.get_argument("tid") 88 | if tid in _result.keys(): 89 | out = _result[tid] # 结果 90 | del _result[tid] # 删除tid的数据。 91 | self.write(str(out)) 92 | else: 93 | self.write("") 94 | 95 | 96 | # main 启动。 97 | if __name__ == "__main__": 98 | HTTPServer(Application([ 99 | ("/", MainPage), 100 | ("/add_job", AddJobHandler), 101 | ("/job_check", JobCheckHandler) 102 | ], debug=True)).listen(9999) 103 | print("start web .") 104 | IOLoop.instance().start() 105 | -------------------------------------------------------------------------------- /backend/web/tornado_bokeh_embed.py: -------------------------------------------------------------------------------- 1 | from jinja2 import Environment, FileSystemLoader 2 | 3 | from tornado.web import RequestHandler 4 | 5 | from bokeh.application import Application 6 | from bokeh.application.handlers import FunctionHandler 7 | from bokeh.embed import server_document 8 | from bokeh.layouts import column 9 | from bokeh.models import ColumnDataSource, Slider 10 | from bokeh.plotting import figure 11 | from bokeh.server.server import Server 12 | from bokeh.themes import Theme 13 | 14 | from bokeh.sampledata.sea_surface_temperature import sea_surface_temperature 15 | 16 | env = Environment(loader=FileSystemLoader('templates')) 17 | server_url = "http://localhost:9090/" 18 | 19 | 20 | class IndexHandler(RequestHandler): 21 | def get(self): 22 | print("index ...") 23 | template = env.get_template('bokeh_embed.html') 24 | script = server_document(server_url + 'bkapp') 25 | print(script) 26 | self.write(template.render(script=script, template="Tornado")) 27 | # self.write(html_content) 28 | 29 | 30 | def modify_doc(doc): 31 | df = sea_surface_temperature.copy() 32 | source = ColumnDataSource(data=df) 33 | 34 | plot = figure(x_axis_type='datetime', y_range=(0, 25), y_axis_label='Temperature (Celsius)', 35 | title="Sea Surface Temperature at 43.18, -70.43") 36 | plot.line('time', 'temperature', source=source) 37 | 38 | def callback(attr, old, new): 39 | if new == 0: 40 | data = df 41 | else: 42 | data = df.rolling('{0}D'.format(new)).mean() 43 | source.data = ColumnDataSource(data=data).data 44 | 45 | slider = Slider(start=0, end=30, value=0, step=1, title="Smoothing by N Days") 46 | slider.on_change('value', callback) 47 | 48 | doc.add_root(column(slider, plot)) 49 | 50 | # doc.theme = Theme(filename="theme.yaml") 51 | 52 | 53 | bokeh_app = Application(FunctionHandler(modify_doc)) 54 | 55 | # Setting num_procs here means we can't touch the IOLoop before now, we must 56 | # let Server handle that. If you need to explicitly handle IOLoops then you 57 | # will need to use the lower level BaseServer class. 58 | server = Server( 59 | {'/bkapp': bokeh_app}, num_procs=1, port=9999, 60 | extra_patterns=[('/', IndexHandler)] 61 | ) 62 | server.start() 63 | 64 | if __name__ == '__main__': 65 | from bokeh.util.browser import view 66 | 67 | print('Opening Tornado app with embedded Bokeh application on ' + server_url) 68 | 69 | server.io_loop.add_callback(view, server_url) 70 | server.io_loop.start() 71 | -------------------------------------------------------------------------------- /docker-compose/.gitignore: -------------------------------------------------------------------------------- 1 | # C extensions 2 | *.so 3 | 4 | data 5 | .idea 6 | *.iml 7 | .DS_Store 8 | *.zip 9 | *.log 10 | *.pyc 11 | doc 12 | /bin 13 | pkg 14 | *.tmp -------------------------------------------------------------------------------- /docker-compose/README.md: -------------------------------------------------------------------------------- 1 | ## 镜像仓库选择 2 | 3 | https://github.com/DaoCloud/public-image-mirror 4 | 5 | ```bash 6 | 7 | # python使用镜像 8 | docker.m.daocloud.io/library/python:3.11-slim-bullseye 9 | 10 | # mysql使用: 11 | docker.m.daocloud.io/library/mysql:8 12 | 13 | ``` 14 | 15 | 16 | ## 本地部署方法 17 | 18 | 19 | ``` 20 | git clone git@gitee.com:pythonstock/docker-compose.git 21 | 22 | cd docker-compose 23 | 24 | docker-compose up -d 25 | 26 | ``` 27 | 28 | ## 访问地址 29 | 30 | http://localhost:9090/ 31 | 32 | 33 | 34 | ## 查看日至,进入项目代码 35 | 36 | ``` 37 | # 查看启动日志: 38 | docker logs -f stock 39 | 40 | # 进入stock容器 41 | docker exec -it stock bash 42 | ``` 43 | 44 | ## 开发模式,映射stock 代码方法 45 | 46 | 直接使用 dev yml 即可,会映射stock到/data/stock 然后在外部修改代码容器中运行即可。 47 | 48 | ```bash 49 | docker-compose -f dev-docker-compose.yml up -d 50 | ``` 51 | 52 | ```bash 53 | docker-compose -f docker-compose-v2.0.yml up -d 54 | ``` 55 | 56 | ## 老镜像还保存一个版本 57 | 58 | ``` 59 | pythonstock/pythonstock:v2021 60 | ``` -------------------------------------------------------------------------------- /docker-compose/build_stock.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd ../stock 4 | 5 | NOW_MONTH=$(date "+%Y-%m") 6 | 7 | DOCKER_TAG=pythonstock/pythonstock:latest 8 | DOCKER_TAG_MONTH=pythonstock/pythonstock:stock-${NOW_MONTH} 9 | 10 | echo " docker build -f Dockerfile -t ${DOCKER_TAG} ." 11 | docker build -f Dockerfile -t ${DOCKER_TAG} . 12 | echo " docker build tag xxx ${DOCKER_TAG_MONTH} " 13 | echo "#################################################################" 14 | echo " docker push ${DOCKER_TAG} " 15 | 16 | 17 | -------------------------------------------------------------------------------- /docker-compose/dev-docker-compose-restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | git pull 5 | 6 | sleep 1 7 | docker-compose -f dev-docker-compose.yml down 8 | 9 | sleep 1 10 | docker-compose -f dev-docker-compose.yml up -d 11 | 12 | echo "restart dev-docker-compose" 13 | 14 | -------------------------------------------------------------------------------- /docker-compose/dev-docker-compose.yml: -------------------------------------------------------------------------------- 1 | networks: 2 | stock-dev-network: 3 | driver: bridge 4 | 5 | version: "3" 6 | services: 7 | frontend: 8 | image: pythonstock/frontend-dev:latest 9 | build: 10 | context: . 11 | dockerfile: docker/DevFrontendDockerfile 12 | container_name: frontend 13 | ports: 14 | - "8080:8080" 15 | volumes: 16 | # 设置开发目录,方便开发调试 17 | - "../frontend:/usr/src/app" 18 | environment: 19 | LANG: zh_CN.UTF-8 20 | LC_CTYPE: zh_CN.UTF-8 21 | restart: always 22 | networks: 23 | stock-dev-network: {} 24 | # 入口写死,手动启动应用。 25 | entrypoint: /usr/src/app/docker-entrypoint.sh 26 | depends_on: 27 | - backend 28 | backend: 29 | image: pythonstock/backend-dev:latest 30 | build: 31 | context: . 32 | dockerfile: docker/DevBackendDockerfile 33 | container_name: backend 34 | ports: 35 | - "8888:8888" 36 | - "9090:9090" 37 | volumes: 38 | # 设置开发目录,方便开发调试 39 | - "../backend/jobs/crontab:/var/spool/cron/crontabs/root" 40 | - "../backend/jobs/cron.minutely:/etc/cron.minutely" 41 | - "../backend/jobs/cron.hourly:/etc/cron.hourly" 42 | - "../backend/jobs/cron.daily:/etc/cron.daily" 43 | - "../backend/jobs/cron.monthly:/etc/cron.monthly" 44 | - "../backend:/data/stock" 45 | - "../backend/supervisor:/data/supervisor" 46 | - "./data/notebooks:/data/notebooks" 47 | - "./data/logs:/data/logs" 48 | environment: 49 | MYSQL_HOST: mysql-stock 50 | MYSQL_USER: root 51 | MYSQL_PWD: mysql-stock 52 | MYSQL_DB: stock_data 53 | MYSQL_PORT: 3306 54 | LANG: zh_CN.UTF-8 55 | LC_CTYPE: zh_CN.UTF-8 56 | PYTHONIOENCODING: utf-8 57 | restart: always 58 | networks: 59 | stock-dev-network: {} 60 | # 入口写死,手动启动应用。 61 | #entrypoint: sleep 999999d 62 | depends_on: 63 | - mysql-stock 64 | mysql-stock: 65 | # image: hub.atomgit.com/library/mysql:5.7 66 | # https://hub.atomgit.com/repos/amd64/mysql 67 | image: docker.m.daocloud.io/library/mysql:8 68 | container_name: mysql-stock 69 | # 执行命令:https://juejin.cn/s/mysql%20healthcheck%20docker-compose 70 | healthcheck: 71 | test: ["CMD", "mysqladmin" ,"ping", "-uroot", "-pmysql-stock"] 72 | interval: 10s 73 | timeout: 5s 74 | retries: 5 75 | ports: 76 | - "3306:3306" 77 | networks: 78 | stock-dev-network: {} 79 | volumes: 80 | - "./mysql/my.cnf:/etc/mysql/my.cnf" 81 | - "./data/mysql-stock/data:/var/lib/mysql" 82 | - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql 83 | environment: 84 | MYSQL_ROOT_PASSWORD: mysql-stock 85 | MYSQL_DATABASE: stock_data 86 | TZ: Asia/Shanghai 87 | command: [ 88 | '--character-set-server=utf8mb4', 89 | '--collation-server=utf8mb4_general_ci', 90 | '--max_connections=3000' 91 | ] 92 | restart: always 93 | -------------------------------------------------------------------------------- /docker-compose/docker/DevFrontendDockerfile: -------------------------------------------------------------------------------- 1 | #使用 node:bullseye-slim 做基础镜像减少大小。 2 | 3 | # FROM docker.m.daocloud.io/library/node:bullseye-slim 4 | # fixbug 最新node 版本编译不过去, 5 | # ERROR Error: Cannot find module './passes/web-incoming' 6 | 7 | FROM docker.m.daocloud.io/library/node:23.5.0-bullseye-slim 8 | 9 | # https://opsx.alibaba.com/mirror 10 | # 使用阿里云镜像地址。修改debian apt 更新地址,pip 地址,设置时区。 11 | # 设置debian的镜像源 12 | RUN echo "deb http://mirrors.aliyun.com/debian/ bullseye main non-free contrib" > /etc/apt/sources.list && \ 13 | echo "deb-src http://mirrors.aliyun.com/debian/ bullseye main non-free contrib" >> /etc/apt/sources.list && \ 14 | echo "deb http://mirrors.aliyun.com/debian-security/ bullseye-security main" >> /etc/apt/sources.list && \ 15 | echo "deb-src http://mirrors.aliyun.com/debian-security/ bullseye-security main" >> /etc/apt/sources.list && \ 16 | echo "deb http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib" >> /etc/apt/sources.list && \ 17 | echo "deb-src http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib" >> /etc/apt/sources.list && \ 18 | echo "deb http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib" >> /etc/apt/sources.list && \ 19 | echo "deb-src http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib" >> /etc/apt/sources.list && \ 20 | echo "[global]\n\ 21 | trusted-host=mirrors.aliyun.com\n\ 22 | index-url=http://mirrors.aliyun.com/pypi/simple" > /etc/pip.conf && \ 23 | ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ 24 | echo "Asia/Shanghai" > /etc/timezone 25 | 26 | #增加语言utf-8 27 | ENV LANG=zh_CN.UTF-8 28 | ENV LC_CTYPE=zh_CN.UTF-8 29 | ENV LC_ALL=C 30 | 31 | # 增加 TensorFlow 的支持,使用最新的2.0 编写代码。目前还是使用 1.x 吧,还没有学明白。 32 | # RUN pip3 install tensorflow==2.0.0-rc1 keras 33 | # RUN pip3 install tensorflow keras sklearn 34 | 35 | # 设置 vim 的语言配置 36 | RUN mkdir -p /etc/vim/ && \ 37 | echo "set fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936" >> /etc/vim/vimrc && \ 38 | echo "set termencoding=utf-8" >> /etc/vim/vimrc && \ 39 | echo "set encoding=utf-8" >> /etc/vim/vimrc 40 | 41 | # 安装 mysqlclient akshare (pandas ,numpy) tornado bokeh 42 | # 安装 nodejs 库 43 | # apt-get autoremove -y 删除没有用的依赖lib。减少镜像大小。1MB 也要节省。 44 | # apt-get --purge remove 软件包名称 , 删除已安装包(不保留配置文件)。 45 | RUN apt-get update && apt-get install -y python3 make g++ && apt-get clean 46 | -------------------------------------------------------------------------------- /docker-compose/docker/ProdFrontendDockerfile: -------------------------------------------------------------------------------- 1 | #使用 node:bullseye-slim 做基础镜像减少大小。 2 | 3 | FROM docker.m.daocloud.io/library/node:bullseye-slim 4 | 5 | # https://opsx.alibaba.com/mirror 6 | # 使用阿里云镜像地址。修改debian apt 更新地址,pip 地址,设置时区。 7 | # 设置debian的镜像源 8 | RUN echo "deb http://mirrors.aliyun.com/debian/ bullseye main non-free contrib" > /etc/apt/sources.list && \ 9 | echo "deb-src http://mirrors.aliyun.com/debian/ bullseye main non-free contrib" >> /etc/apt/sources.list && \ 10 | echo "deb http://mirrors.aliyun.com/debian-security/ bullseye-security main" >> /etc/apt/sources.list && \ 11 | echo "deb-src http://mirrors.aliyun.com/debian-security/ bullseye-security main" >> /etc/apt/sources.list && \ 12 | echo "deb http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib" >> /etc/apt/sources.list && \ 13 | echo "deb-src http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib" >> /etc/apt/sources.list && \ 14 | echo "deb http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib" >> /etc/apt/sources.list && \ 15 | echo "deb-src http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib" >> /etc/apt/sources.list && \ 16 | echo "[global]\n\ 17 | trusted-host=mirrors.aliyun.com\n\ 18 | index-url=http://mirrors.aliyun.com/pypi/simple" > /etc/pip.conf && \ 19 | ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ 20 | echo "Asia/Shanghai" > /etc/timezone 21 | 22 | #增加语言utf-8 23 | ENV LANG=zh_CN.UTF-8 24 | ENV LC_CTYPE=zh_CN.UTF-8 25 | ENV LC_ALL=C 26 | 27 | # 增加 TensorFlow 的支持,使用最新的2.0 编写代码。目前还是使用 1.x 吧,还没有学明白。 28 | # RUN pip3 install tensorflow==2.0.0-rc1 keras 29 | # RUN pip3 install tensorflow keras sklearn 30 | 31 | # 设置 vim 的语言配置 32 | RUN mkdir -p /etc/vim/ && \ 33 | echo "set fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936" >> /etc/vim/vimrc && \ 34 | echo "set termencoding=utf-8" >> /etc/vim/vimrc && \ 35 | echo "set encoding=utf-8" >> /etc/vim/vimrc 36 | 37 | # 安装 mysqlclient akshare (pandas ,numpy) tornado bokeh 38 | # 安装 nodejs 库 39 | # apt-get autoremove -y 删除没有用的依赖lib。减少镜像大小。1MB 也要节省。 40 | # apt-get --purge remove 软件包名称 , 删除已安装包(不保留配置文件)。 41 | RUN apt-get update && apt-get install -y python3 make g++ && apt-get clean 42 | -------------------------------------------------------------------------------- /docker-compose/docker/README.md: -------------------------------------------------------------------------------- 1 | 2 | # python 基础镜像 3 | 4 | 基础镜像升级到 2020年7月的版本 5 | 6 | 保证运行的最少基础环境,基础环境使用python3.6的版本。安装了超级多的lib库。非常的好用。 7 | 8 | mysqlclient 9 | sqlalchemy 10 | requests 11 | numpy 12 | tushare 13 | tornado torndb 14 | bokeh 15 | stockstats 16 | ta-lib 17 | jupyter 18 | sklearn 19 | 20 | # 2021年6月版本,使用 akshare 替换掉 tushare 库 21 | 22 | akshare 地址: 23 | 24 | https://www.akshare.xyz/zh_CN/latest/introduction.html 25 | 26 | AKShare 是基于 Python 的财经数据接口库, 目的是实现对股票、期货、期权、基金、外汇、债券、指数、 27 | 加密货币等金融产品的基本面数据、实时和历史行情数据、衍生数据从数据采集、数据清洗到数据落地的一套工具, 28 | 主要用于学术研究目的。 29 | 30 | 31 | # 2021年 9 月版本,镜像裁剪,supervisor 使用python3 32 | 33 | supervisor 使用 python3 后好像减少了不少大小。 34 | 同时删除掉一直没有用的 ta-lib 和 jupyter 。升级python基础镜像。 35 | 36 | -------------------------------------------------------------------------------- /docker-compose/docker/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | NOW_MONTH=$(date "+%Y-%m") 5 | 6 | DOCKER_TAG=pythonstock/pythonstock:base-${NOW_MONTH} 7 | 8 | echo " docker build -f Dockerfile -t ${DOCKER_TAG} ." 9 | docker build -f Dockerfile -t ${DOCKER_TAG} . 10 | echo "#################################################################" 11 | echo " docker push ${DOCKER_TAG} " 12 | 13 | 14 | -------------------------------------------------------------------------------- /docker-compose/mysql/my.cnf: -------------------------------------------------------------------------------- 1 | # https://blog.csdn.net/aichogn/article/details/117788275 2 | # mysql 推荐配置 3 | [client] 4 | socket=/var/lib/mysql/mysql.sock 5 | port=3306 6 | [mysqld] 7 | 8 | basedir=/var/lib/mysql/ 9 | datadir=/var/lib/mysql/data 10 | socket=/var/lib/mysql/mysql.sock 11 | default-storage-engine=INNODB 12 | character_set_server=utf8mb4 13 | collation_server=utf8mb4_general_ci 14 | 15 | port=3306 16 | # Disabling symbolic-links is recommended to prevent assorted security risks 17 | symbolic-links=0 18 | 19 | server_id=1 20 | 21 | ## 最大连接数,MySQL服务器允许的最大连接数16384,连接数越多消耗内存越多 22 | max_connections = 1000 23 | ## 日志过期时间,包括二进制日志(过期自动删除) 24 | # expire_logs_days = 15 25 | ## Enable Per Table Data for InnoDB to shrink ibdata1(innoDB表优化) 26 | innodb_file_per_table = 1 27 | #默认128M,用于存储页面缓存数据外,另外正常情况下还有大约8%的开销,主要用在每个缓存页帧的描述、adaptive hash等数据结构,适当的增加这个参数的大小,可以有效的减少 InnoDB 类型的表的磁盘 I/O 28 | innodb_buffer_pool_size = 2048M 29 | innodb_log_file_size = 512M 30 | #默认是8MB,InnoDB在写事务日志的时候,为了提高性能,也是先将信息写入Innofb Log Buffer中,当满足innodb_flush_log_trx_commit参数所设置的相应条件(或者日志缓冲区写满)之后,才会将日志写到文件 (或者同步到磁盘)中 31 | innodb_log_buffer_size = 8M 32 | innodb_flush_log_at_trx_commit = 2 33 | #表大小写不敏感 34 | lower_case_table_names=1 35 | #跳过密码 安装完后屏蔽该选项 36 | #skip-grant-tables 37 | #关闭 binlog 38 | skip-log-bin -------------------------------------------------------------------------------- /docker-compose/nginx.conf: -------------------------------------------------------------------------------- 1 | # 设置nginx启动 2 | # systemctl enable nginx 3 | server { 4 | listen 8080; 5 | server_name www.pythonstock.com; 6 | root /usr/share/nginx/html; 7 | 8 | error_page 404 /404.html; 9 | error_page 500 502 503 504 /50x.html; 10 | 11 | # 开启gzip 12 | gzip on; 13 | # 启用gzip压缩的最小文件;小于设置值的文件将不会被压缩 14 | gzip_min_length 1k; 15 | # gzip 压缩级别 1-10 16 | gzip_comp_level 2; 17 | # 进行压缩的文件类型。 18 | gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; 19 | # 是否在http header中添加Vary: Accept-Encoding,建议开启 20 | gzip_vary on; 21 | 22 | access_log /var/log/nginx/stock.access.log main; 23 | error_log /var/log/nginx/stock.error.log warn; 24 | 25 | # https://medium.com/aviabird/413-414-request-url-entity-too-large-error-nginx-b6dcece6f5dd 26 | # 解决GET 参数过长问题。 27 | client_max_body_size 10M; 28 | large_client_header_buffers 4 20k; 29 | 30 | 31 | location ^~ /static/ { 32 | access_log off; 33 | # 需要把源代码,再下载一遍。 34 | root /data/pythonstock/stock/web; 35 | expires 30d; # 设置30天超时 36 | # 参考 https://www.cnblogs.com/kevingrace/p/10459429.html 37 | # add_header Cache-Control max-age=360000; 38 | } 39 | 40 | location / { 41 | proxy_next_upstream http_500 http_502 http_503 http_504 error timeout invalid_header; 42 | proxy_set_header Host $host; 43 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 44 | proxy_pass http://127.0.0.1:9090; 45 | expires 0; 46 | } 47 | } -------------------------------------------------------------------------------- /docker-compose/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | server_name localhost; 4 | #access_log /var/log/nginx/host.access.log main; 5 | 6 | location / { 7 | root /usr/share/nginx/html; 8 | index index.html index.htm; 9 | } 10 | 11 | location /api/v1 { 12 | proxy_pass http://backend:9090; 13 | } 14 | 15 | 16 | error_page 404 /404.html; 17 | error_page 500 502 503 504 /50x.html; 18 | location = /50x.html { 19 | root /usr/share/nginx/html; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | eslint-disable -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | tests/**/coverage/ 9 | 10 | # Editor directories and files 11 | .idea 12 | .vscode 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln -------------------------------------------------------------------------------- /frontend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present PanJiaChen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | ### 说明,项目迁移到了Gitee 啦,最后一次修改,2023-06-02 执行存档 2 | 3 | 项目迁移到这里了:此项目后续更新访问这里: 4 | 5 | https://gitee.com/pythonstock/stock-ui 6 | 7 | github项目后续就Archives存档了,不再更新了! 8 | 9 | csdn的pythonstock专栏地址,相关资料都在这里有说明: 10 | 11 | https://blog.csdn.net/freewebsys/category_9285317.html 12 | 13 | 14 | ## 1,股票系统前端项目 15 | 16 | elementUI 17 | 18 | https://element.eleme.cn/#/zh-CN 19 | 20 | 使用vue-element-admin的模板进行项目开发: 21 | 22 | https://panjiachen.github.io/vue-element-admin-site/zh/guide/ 23 | 24 | 在线预览地址: 25 | 26 | https://panjiachen.github.io/vue-element-admin/#/dashboard 27 | 28 | 模板源自: 29 | https://gitee.com/panjiachen/vue-admin-template 30 | 31 | 【相关python stock资料分类】: 32 | http://blog.csdn.net/freewebsys/article/category/7076584 33 | 34 | 35 | ## 2,动态展示表格 36 | 37 | http://localhost:9528/#/example/table 38 | 39 | 40 | 在.eslintrc.js 文件中配置 eslint-disable 指令来关闭ESlint语法检测。 41 | 42 | 43 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app 4 | '@vue/cli-plugin-babel/preset' 5 | ], 6 | 'env': { 7 | 'development': { 8 | // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require(). 9 | // This plugin can significantly increase the speed of hot updates, when you have a large number of pages. 10 | // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html 11 | 'plugins': ['dynamic-import-node'] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sleep 1 4 | # 只依赖启动。 5 | cd /usr/src/app 6 | 7 | #!/bin/bash 8 | 9 | # 定义要检查的文件夹路径 10 | modules_path="/usr/src/app/node_modules" 11 | 12 | # 使用[ ]检查文件夹是否存在 13 | if [ -d "$modules_path" ]; then 14 | echo "文件夹 $modules_path 存在" 15 | else 16 | echo "文件夹 $modules_path 不存在,执行 install 安装" 17 | npm install --registry=https://registry.npmmirror.com 18 | fi 19 | 20 | npm run build 21 | # 编译完成之后拷贝 html 资源到 影射目录,等待即可。每次编译前都清空内容。 22 | rm -rf /data/html/* 23 | cp -r ./dist/* /data/html/ 24 | echo "######### build finish and cp all html #########" -------------------------------------------------------------------------------- /frontend/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sleep 1 4 | # 只依赖启动。 5 | cd /usr/src/app 6 | 7 | #!/bin/bash 8 | 9 | # 定义要检查的文件夹路径 10 | modules_path="/usr/src/app/node_modules" 11 | 12 | # 使用[ ]检查文件夹是否存在 13 | if [ -d "$modules_path" ]; then 14 | echo "文件夹 $modules_path 存在" 15 | else 16 | echo "文件夹 $modules_path 不存在,执行 install 安装" 17 | npm install --registry=https://registry.npmmirror.com 18 | fi 19 | 20 | npm run dev 21 | sleep 999999d 22 | -------------------------------------------------------------------------------- /frontend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue#39;: 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)#39;: 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?#39;: 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)#39;: '<rootDir>/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '<rootDir>/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /frontend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /frontend/mock/index.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | const { param2Obj } = require('./utils') 3 | 4 | const user = require('./user') 5 | const table = require('./table') 6 | 7 | const mocks = [ 8 | ...user, 9 | ...table 10 | ] 11 | 12 | // for front mock 13 | // please use it cautiously, it will redefine XMLHttpRequest, 14 | // which will cause many of your third-party libraries to be invalidated(like progress event). 15 | function mockXHR() { 16 | // mock patch 17 | // https://github.com/nuysoft/Mock/issues/300 18 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send 19 | Mock.XHR.prototype.send = function() { 20 | if (this.custom.xhr) { 21 | this.custom.xhr.withCredentials = this.withCredentials || false 22 | 23 | if (this.responseType) { 24 | this.custom.xhr.responseType = this.responseType 25 | } 26 | } 27 | this.proxy_send(...arguments) 28 | } 29 | 30 | function XHR2ExpressReqWrap(respond) { 31 | return function(options) { 32 | let result = null 33 | if (respond instanceof Function) { 34 | const { body, type, url } = options 35 | // https://expressjs.com/en/4x/api.html#req 36 | result = respond({ 37 | method: type, 38 | body: JSON.parse(body), 39 | query: param2Obj(url) 40 | }) 41 | } else { 42 | result = respond 43 | } 44 | return Mock.mock(result) 45 | } 46 | } 47 | 48 | for (const i of mocks) { 49 | Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) 50 | } 51 | } 52 | 53 | module.exports = { 54 | mocks, 55 | mockXHR 56 | } 57 | 58 | -------------------------------------------------------------------------------- /frontend/mock/mock-server.js: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar') 2 | const bodyParser = require('body-parser') 3 | const chalk = require('chalk') 4 | const path = require('path') 5 | const Mock = require('mockjs') 6 | 7 | const mockDir = path.join(process.cwd(), 'mock') 8 | 9 | function registerRoutes(app) { 10 | let mockLastIndex 11 | const { mocks } = require('./index.js') 12 | const mocksForServer = mocks.map(route => { 13 | return responseFake(route.url, route.type, route.response) 14 | }) 15 | for (const mock of mocksForServer) { 16 | app[mock.type](mock.url, mock.response) 17 | mockLastIndex = app._router.stack.length 18 | } 19 | const mockRoutesLength = Object.keys(mocksForServer).length 20 | return { 21 | mockRoutesLength: mockRoutesLength, 22 | mockStartIndex: mockLastIndex - mockRoutesLength 23 | } 24 | } 25 | 26 | function unregisterRoutes() { 27 | Object.keys(require.cache).forEach(i => { 28 | if (i.includes(mockDir)) { 29 | delete require.cache[require.resolve(i)] 30 | } 31 | }) 32 | } 33 | 34 | // for mock server 35 | const responseFake = (url, type, respond) => { 36 | return { 37 | url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`), 38 | type: type || 'get', 39 | response(req, res) { 40 | console.log('request invoke:' + req.path) 41 | res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) 42 | } 43 | } 44 | } 45 | 46 | module.exports = app => { 47 | // parse app.body 48 | // https://expressjs.com/en/4x/api.html#req.body 49 | app.use(bodyParser.json()) 50 | app.use(bodyParser.urlencoded({ 51 | extended: true 52 | })) 53 | 54 | const mockRoutes = registerRoutes(app) 55 | var mockRoutesLength = mockRoutes.mockRoutesLength 56 | var mockStartIndex = mockRoutes.mockStartIndex 57 | 58 | // watch files, hot reload mock server 59 | chokidar.watch(mockDir, { 60 | ignored: /mock-server/, 61 | ignoreInitial: true 62 | }).on('all', (event, path) => { 63 | if (event === 'change' || event === 'add') { 64 | try { 65 | // remove mock routes stack 66 | app._router.stack.splice(mockStartIndex, mockRoutesLength) 67 | 68 | // clear routes cache 69 | unregisterRoutes() 70 | 71 | const mockRoutes = registerRoutes(app) 72 | mockRoutesLength = mockRoutes.mockRoutesLength 73 | mockStartIndex = mockRoutes.mockStartIndex 74 | 75 | console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`)) 76 | } catch (error) { 77 | console.log(chalk.redBright(error)) 78 | } 79 | } 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /frontend/mock/table.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | 3 | const data = Mock.mock({ 4 | 'items|30': [{ 5 | id: '@id', 6 | title: '@sentence(10, 20)', 7 | 'status|1': ['published', 'draft', 'deleted'], 8 | author: 'name', 9 | display_time: '@datetime', 10 | pageviews: '@integer(300, 5000)' 11 | }] 12 | }) 13 | 14 | module.exports = [ 15 | { 16 | url: '/vue-admin-template/table/list', 17 | type: 'get', 18 | response: config => { 19 | const items = data.items 20 | return { 21 | code: 20000, 22 | data: { 23 | total: items.length, 24 | items: items 25 | } 26 | } 27 | } 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /frontend/mock/user.js: -------------------------------------------------------------------------------- 1 | 2 | const tokens = { 3 | admin: { 4 | token: 'admin-token' 5 | }, 6 | editor: { 7 | token: 'editor-token' 8 | } 9 | } 10 | 11 | const users = { 12 | 'admin-token': { 13 | roles: ['admin'], 14 | introduction: 'I am a super administrator', 15 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 16 | name: 'Super Admin' 17 | }, 18 | 'editor-token': { 19 | roles: ['editor'], 20 | introduction: 'I am an editor', 21 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 22 | name: 'Normal Editor' 23 | } 24 | } 25 | 26 | module.exports = [ 27 | // user login 28 | { 29 | url: '/vue-admin-template/user/login', 30 | type: 'post', 31 | response: config => { 32 | const { username } = config.body 33 | const token = tokens[username] 34 | 35 | // mock error 36 | if (!token) { 37 | return { 38 | code: 60204, 39 | message: 'Account and password are incorrect.' 40 | } 41 | } 42 | 43 | return { 44 | code: 20000, 45 | data: token 46 | } 47 | } 48 | }, 49 | 50 | // get user info 51 | { 52 | url: '/vue-admin-template/user/info\.*', 53 | type: 'get', 54 | response: config => { 55 | const { token } = config.query 56 | const info = users[token] 57 | 58 | // mock error 59 | if (!info) { 60 | return { 61 | code: 50008, 62 | message: 'Login failed, unable to get user details.' 63 | } 64 | } 65 | 66 | return { 67 | code: 20000, 68 | data: info 69 | } 70 | } 71 | }, 72 | 73 | // user logout 74 | { 75 | url: '/vue-admin-template/user/logout', 76 | type: 'post', 77 | response: _ => { 78 | return { 79 | code: 20000, 80 | data: 'success' 81 | } 82 | } 83 | } 84 | ] 85 | -------------------------------------------------------------------------------- /frontend/mock/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} url 3 | * @returns {Object} 4 | */ 5 | function param2Obj(url) { 6 | const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') 7 | if (!search) { 8 | return {} 9 | } 10 | const obj = {} 11 | const searchArr = search.split('&') 12 | searchArr.forEach(v => { 13 | const index = v.indexOf('=') 14 | if (index !== -1) { 15 | const name = v.substring(0, index) 16 | const val = v.substring(index + 1, v.length) 17 | obj[name] = val 18 | } 19 | }) 20 | return obj 21 | } 22 | 23 | module.exports = { 24 | param2Obj 25 | } 26 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-admin-template", 3 | "version": "4.4.0", 4 | "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint", 5 | "author": "Pan <panfree23@gmail.com>", 6 | "scripts": { 7 | "dev": "./node_modules/.bin/vue-cli-service serve ", 8 | "build": "./node_modules/.bin/vue-cli-service build", 9 | "build:stage": "./node_modules/.bin/vue-cli-service build --mode staging", 10 | "preview": "node build/index.js --preview", 11 | "svgo": "./node_modules/.bin/svgo -f src/icons/svg --config=src/icons/svgo.yml", 12 | "lint": "./node_modules/.bin/eslint --ext .js,.vue src", 13 | "test:unit": "./node_modules/.bin/jest --clearCache && vue-cli-service test:unit", 14 | "test:ci": "npm run lint && npm run test:unit" 15 | }, 16 | "dependencies": { 17 | "axios": "0.28.0", 18 | "core-js": "^3.26.1", 19 | "element-ui": "2.15.14", 20 | "file-saver": "^2.0.5", 21 | "js-cookie": "2.2.0", 22 | "normalize.css": "7.0.0", 23 | "nprogress": "0.2.0", 24 | "path-to-regexp": "2.4.0", 25 | "vue": "2.6.10", 26 | "vue-router": "3.0.6", 27 | "vuex": "3.1.0", 28 | "xlsx": "^0.18.5" 29 | }, 30 | "devDependencies": { 31 | "@vue/cli-plugin-babel": "4.4.4", 32 | "@vue/cli-plugin-eslint": "4.4.4", 33 | "@vue/cli-plugin-unit-jest": "4.4.4", 34 | "@vue/cli-service": "4.4.4", 35 | "@vue/test-utils": "1.0.0-beta.29", 36 | "autoprefixer": "9.5.1", 37 | "babel-eslint": "10.1.0", 38 | "babel-jest": "23.6.0", 39 | "babel-plugin-dynamic-import-node": "2.3.3", 40 | "chalk": "2.4.2", 41 | "connect": "3.6.6", 42 | "eslint": "6.7.2", 43 | "eslint-plugin-vue": "6.2.2", 44 | "html-webpack-plugin": "3.2.0", 45 | "mockjs": "1.0.1-beta3", 46 | "runjs": "4.3.2", 47 | "sass": "1.26.8", 48 | "sass-loader": "8.0.2", 49 | "script-ext-html-webpack-plugin": "2.1.3", 50 | "serve-static": "1.16.0", 51 | "svg-sprite-loader": "4.1.3", 52 | "svgo": "1.2.2", 53 | "vue-template-compiler": "2.6.10" 54 | }, 55 | "browserslist": [ 56 | "> 1%", 57 | "last 2 versions" 58 | ], 59 | "engines": { 60 | "node": ">=8.9", 61 | "npm": ">= 3.0.0" 62 | }, 63 | "license": "MIT" 64 | } 65 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | 'plugins': { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | 'autoprefixer': {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /frontend/public/40x.html: -------------------------------------------------------------------------------- 1 | 2 | <html> 3 | <head><title>404 Not Found</title></head> 4 | <body> 5 | <center><h1>404 Not Found</h1></center> 6 | <hr><center>nginx/1.26.3</center> 7 | </body> 8 | </html> -------------------------------------------------------------------------------- /frontend/public/50x.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <title>Error</title> 5 | <style> 6 | html { color-scheme: light dark; } 7 | body { width: 35em; margin: 0 auto; 8 | font-family: Tahoma, Verdana, Arial, sans-serif; } 9 | </style> 10 | </head> 11 | <body> 12 | <h1>An error occurred.</h1> 13 | <p>Sorry, the page you are looking for is currently unavailable.<br/> 14 | Please try again later.</p> 15 | <p>If you are the system administrator of this resource then you should check 16 | the error log for details.</p> 17 | <p><em>Faithfully yours, nginx.</em></p> 18 | </body> 19 | </html> 20 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/freewebsys-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/frontend/public/freewebsys-logo.jpg -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <meta charset="utf-8"> 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> 6 | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> 7 | <link rel="icon" href="<%= BASE_URL %>favicon.ico"> 8 | <title><%= webpackConfig.name %></title> 9 | </head> 10 | <body> 11 | <noscript> 12 | <strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> 13 | </noscript> 14 | <div id="app"></div> 15 | <!-- built files will be auto injected --> 16 | </body> 17 | </html> 18 | -------------------------------------------------------------------------------- /frontend/public/stock-001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/frontend/public/stock-001.png -------------------------------------------------------------------------------- /frontend/public/stock-002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/frontend/public/stock-002.png -------------------------------------------------------------------------------- /frontend/public/stock-003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/frontend/public/stock-003.png -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div id="app"> 3 | <router-view /> 4 | </div> 5 | </template> 6 | 7 | <script> 8 | export default { 9 | name: 'App' 10 | } 11 | </script> 12 | -------------------------------------------------------------------------------- /frontend/src/api/article.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchList(query) { 4 | return request({ 5 | url: '/api/v1/api_data', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | 11 | export function fetchArticle(id) { 12 | return request({ 13 | url: '/vue-element-admin/article/detail', 14 | method: 'get', 15 | params: { id } 16 | }) 17 | } 18 | 19 | export function fetchPv(pv) { 20 | return request({ 21 | url: '/vue-element-admin/article/pv', 22 | method: 'get', 23 | params: { pv } 24 | }) 25 | } 26 | 27 | export function createArticle(data) { 28 | return request({ 29 | url: '/vue-element-admin/article/create', 30 | method: 'post', 31 | data 32 | }) 33 | } 34 | 35 | export function updateArticle(data) { 36 | return request({ 37 | url: '/vue-element-admin/article/update', 38 | method: 'post', 39 | data 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/api/menu.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 同步获得菜单相关数据。 4 | 5 | export function fetchMenuList(query) { 6 | return request({ 7 | url: '/api/v1/menu_list', 8 | method: 'get', 9 | params: query 10 | }) 11 | } 12 | 13 | -------------------------------------------------------------------------------- /frontend/src/api/package.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchPackageVersion(query) { 4 | return request({ 5 | url: '/api/v1/package_verison', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | 11 | -------------------------------------------------------------------------------- /frontend/src/api/table.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getList(params) { 4 | return request({ 5 | url: '/vue-admin-template/table/list', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function login(data) { 4 | return request({ 5 | url: '/vue-admin-template/user/login', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | 11 | export function getInfo(token) { 12 | return request({ 13 | url: '/vue-admin-template/user/info', 14 | method: 'get', 15 | params: { token } 16 | }) 17 | } 18 | 19 | export function logout() { 20 | return request({ 21 | url: '/vue-admin-template/user/logout', 22 | method: 'post' 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/frontend/src/assets/404_images/404.png -------------------------------------------------------------------------------- /frontend/src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythonstock/stock/6bc0264e69ba1e93d435c064f8fe83b2598bfe58/frontend/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /frontend/src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <el-breadcrumb class="app-breadcrumb" separator="/"> 3 | <transition-group name="breadcrumb"> 4 | <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path"> 5 | <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span> 6 | <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a> 7 | </el-breadcrumb-item> 8 | </transition-group> 9 | </el-breadcrumb> 10 | </template> 11 | 12 | <script> 13 | import pathToRegexp from 'path-to-regexp' 14 | 15 | export default { 16 | data() { 17 | return { 18 | levelList: null 19 | } 20 | }, 21 | watch: { 22 | $route() { 23 | this.getBreadcrumb() 24 | } 25 | }, 26 | created() { 27 | this.getBreadcrumb() 28 | }, 29 | methods: { 30 | getBreadcrumb() { 31 | // only show routes with meta.title 32 | let matched = this.$route.matched.filter(item => item.meta && item.meta.title) 33 | const first = matched[0] 34 | 35 | if (!this.isDashboard(first)) { 36 | matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched) 37 | } 38 | 39 | this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false) 40 | }, 41 | isDashboard(route) { 42 | const name = route && route.name 43 | if (!name) { 44 | return false 45 | } 46 | return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase() 47 | }, 48 | pathCompile(path) { 49 | // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561 50 | const { params } = this.$route 51 | var toPath = pathToRegexp.compile(path) 52 | return toPath(params) 53 | }, 54 | handleLink(item) { 55 | const { redirect, path } = item 56 | if (redirect) { 57 | this.$router.push(redirect) 58 | return 59 | } 60 | this.$router.push(this.pathCompile(path)) 61 | } 62 | } 63 | } 64 | </script> 65 | 66 | <style lang="scss" scoped> 67 | .app-breadcrumb.el-breadcrumb { 68 | display: inline-block; 69 | font-size: 14px; 70 | line-height: 50px; 71 | margin-left: 8px; 72 | 73 | .no-redirect { 74 | color: #97a8be; 75 | cursor: text; 76 | } 77 | } 78 | </style> 79 | -------------------------------------------------------------------------------- /frontend/src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div style="padding: 0 15px;" @click="toggleClick"> 3 | <svg 4 | :class="{'is-active':isActive}" 5 | class="hamburger" 6 | viewBox="0 0 1024 1024" 7 | xmlns="http://www.w3.org/2000/svg" 8 | width="64" 9 | height="64" 10 | > 11 | <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" /> 12 | </svg> 13 | </div> 14 | </template> 15 | 16 | <script> 17 | export default { 18 | name: 'Hamburger', 19 | props: { 20 | isActive: { 21 | type: Boolean, 22 | default: false 23 | } 24 | }, 25 | methods: { 26 | toggleClick() { 27 | this.$emit('toggleClick') 28 | } 29 | } 30 | } 31 | </script> 32 | 33 | <style scoped> 34 | .hamburger { 35 | display: inline-block; 36 | vertical-align: middle; 37 | width: 20px; 38 | height: 20px; 39 | } 40 | 41 | .hamburger.is-active { 42 | transform: rotate(180deg); 43 | } 44 | </style> 45 | -------------------------------------------------------------------------------- /frontend/src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div :class="{'hidden':hidden}" class="pagination-container"> 3 | <el-pagination 4 | :background="background" 5 | :current-page.sync="currentPage" 6 | :page-size.sync="pageSize" 7 | :layout="layout" 8 | :page-sizes="pageSizes" 9 | :total="total" 10 | v-bind="$attrs" 11 | @size-change="handleSizeChange" 12 | @current-change="handleCurrentChange" 13 | /> 14 | </div> 15 | </template> 16 | 17 | <script> 18 | import { scrollTo } from '@/utils/scroll-to' 19 | 20 | export default { 21 | name: 'Pagination', 22 | props: { 23 | total: { 24 | required: true, 25 | type: Number 26 | }, 27 | page: { 28 | type: Number, 29 | default: 1 30 | }, 31 | limit: { 32 | type: Number, 33 | default: 20 34 | }, 35 | pageSizes: { 36 | type: Array, 37 | default() { 38 | return [10, 20, 30, 50] 39 | } 40 | }, 41 | layout: { 42 | type: String, 43 | default: 'total, sizes, prev, pager, next, jumper' 44 | }, 45 | background: { 46 | type: Boolean, 47 | default: true 48 | }, 49 | autoScroll: { 50 | type: Boolean, 51 | default: true 52 | }, 53 | hidden: { 54 | type: Boolean, 55 | default: false 56 | } 57 | }, 58 | computed: { 59 | currentPage: { 60 | get() { 61 | return this.page 62 | }, 63 | set(val) { 64 | this.$emit('update:page', val) 65 | } 66 | }, 67 | pageSize: { 68 | get() { 69 | return this.limit 70 | }, 71 | set(val) { 72 | this.$emit('update:limit', val) 73 | } 74 | } 75 | }, 76 | methods: { 77 | handleSizeChange(val) { 78 | this.$emit('pagination', { page: this.currentPage, limit: val }) 79 | if (this.autoScroll) { 80 | scrollTo(0, 800) 81 | } 82 | }, 83 | handleCurrentChange(val) { 84 | this.$emit('pagination', { page: val, limit: this.pageSize }) 85 | if (this.autoScroll) { 86 | scrollTo(0, 800) 87 | } 88 | } 89 | } 90 | } 91 | </script> 92 | 93 | <style scoped> 94 | .pagination-container { 95 | background: #fff; 96 | padding: 32px 16px; 97 | } 98 | .pagination-container.hidden { 99 | display: none; 100 | } 101 | </style> 102 | -------------------------------------------------------------------------------- /frontend/src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" /> 3 | <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners"> 4 | <use :xlink:href="iconName" /> 5 | </svg> 6 | </template> 7 | 8 | <script> 9 | // doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage 10 | import { isExternal } from '@/utils/validate' 11 | 12 | export default { 13 | name: 'SvgIcon', 14 | props: { 15 | iconClass: { 16 | type: String, 17 | required: true 18 | }, 19 | className: { 20 | type: String, 21 | default: '' 22 | } 23 | }, 24 | computed: { 25 | isExternal() { 26 | return isExternal(this.iconClass) 27 | }, 28 | iconName() { 29 | return `#icon-${this.iconClass}` 30 | }, 31 | svgClass() { 32 | if (this.className) { 33 | return 'svg-icon ' + this.className 34 | } else { 35 | return 'svg-icon' 36 | } 37 | }, 38 | styleExternalIcon() { 39 | return { 40 | mask: `url(${this.iconClass}) no-repeat 50% 50%`, 41 | '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%` 42 | } 43 | } 44 | } 45 | } 46 | </script> 47 | 48 | <style scoped> 49 | .svg-icon { 50 | width: 1em; 51 | height: 1em; 52 | vertical-align: -0.15em; 53 | fill: currentColor; 54 | overflow: hidden; 55 | } 56 | 57 | .svg-external-icon { 58 | background-color: currentColor; 59 | mask-size: cover!important; 60 | display: inline-block; 61 | } 62 | </style> 63 | -------------------------------------------------------------------------------- /frontend/src/directive/el-table/adaptive.js: -------------------------------------------------------------------------------- 1 | import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event' 2 | 3 | /** 4 | * How to use 5 | * <el-table height="100px" v-el-height-adaptive-table="{bottomOffset: 30}">...</el-table> 6 | * el-table height is must be set 7 | * bottomOffset: 30(default) // The height of the table from the bottom of the page. 8 | */ 9 | 10 | const doResize = (el, binding, vnode) => { 11 | const { componentInstance: $table } = vnode 12 | 13 | const { value } = binding 14 | 15 | if (!$table.height) { 16 | throw new Error(`el-$table must set the height. Such as height='100px'`) 17 | } 18 | const bottomOffset = (value && value.bottomOffset) || 30 19 | 20 | if (!$table) return 21 | 22 | const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset 23 | $table.layout.setHeight(height) 24 | $table.doLayout() 25 | } 26 | 27 | export default { 28 | bind(el, binding, vnode) { 29 | el.resizeListener = () => { 30 | doResize(el, binding, vnode) 31 | } 32 | // parameter 1 is must be "Element" type 33 | addResizeListener(window.document.body, el.resizeListener) 34 | }, 35 | inserted(el, binding, vnode) { 36 | doResize(el, binding, vnode) 37 | }, 38 | unbind(el) { 39 | removeResizeListener(window.document.body, el.resizeListener) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/directive/el-table/index.js: -------------------------------------------------------------------------------- 1 | import adaptive from './adaptive' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('el-height-adaptive-table', adaptive) 5 | } 6 | 7 | if (window.Vue) { 8 | window['el-height-adaptive-table'] = adaptive 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | adaptive.install = install 13 | export default adaptive 14 | -------------------------------------------------------------------------------- /frontend/src/directive/waves/index.js: -------------------------------------------------------------------------------- 1 | import waves from './waves' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('waves', waves) 5 | } 6 | 7 | if (window.Vue) { 8 | window.waves = waves 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | waves.install = install 13 | export default waves 14 | -------------------------------------------------------------------------------- /frontend/src/directive/waves/waves.css: -------------------------------------------------------------------------------- 1 | .waves-ripple { 2 | position: absolute; 3 | border-radius: 100%; 4 | background-color: rgba(0, 0, 0, 0.15); 5 | background-clip: padding-box; 6 | pointer-events: none; 7 | -webkit-user-select: none; 8 | -moz-user-select: none; 9 | -ms-user-select: none; 10 | user-select: none; 11 | -webkit-transform: scale(0); 12 | -ms-transform: scale(0); 13 | transform: scale(0); 14 | opacity: 1; 15 | } 16 | 17 | .waves-ripple.z-active { 18 | opacity: 0; 19 | -webkit-transform: scale(2); 20 | -ms-transform: scale(2); 21 | transform: scale(2); 22 | -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 23 | transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 24 | transition: opacity 1.2s ease-out, transform 0.6s ease-out; 25 | transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out; 26 | } -------------------------------------------------------------------------------- /frontend/src/directive/waves/waves.js: -------------------------------------------------------------------------------- 1 | import './waves.css' 2 | 3 | const context = '@@wavesContext' 4 | 5 | function handleClick(el, binding) { 6 | function handle(e) { 7 | const customOpts = Object.assign({}, binding.value) 8 | const opts = Object.assign({ 9 | ele: el, // 波纹作用元素 10 | type: 'hit', // hit 点击位置扩散 center中心点扩展 11 | color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色 12 | }, 13 | customOpts 14 | ) 15 | const target = opts.ele 16 | if (target) { 17 | target.style.position = 'relative' 18 | target.style.overflow = 'hidden' 19 | const rect = target.getBoundingClientRect() 20 | let ripple = target.querySelector('.waves-ripple') 21 | if (!ripple) { 22 | ripple = document.createElement('span') 23 | ripple.className = 'waves-ripple' 24 | ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px' 25 | target.appendChild(ripple) 26 | } else { 27 | ripple.className = 'waves-ripple' 28 | } 29 | switch (opts.type) { 30 | case 'center': 31 | ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px' 32 | ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px' 33 | break 34 | default: 35 | ripple.style.top = 36 | (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop || 37 | document.body.scrollTop) + 'px' 38 | ripple.style.left = 39 | (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft || 40 | document.body.scrollLeft) + 'px' 41 | } 42 | ripple.style.backgroundColor = opts.color 43 | ripple.className = 'waves-ripple z-active' 44 | return false 45 | } 46 | } 47 | 48 | if (!el[context]) { 49 | el[context] = { 50 | removeHandle: handle 51 | } 52 | } else { 53 | el[context].removeHandle = handle 54 | } 55 | 56 | return handle 57 | } 58 | 59 | export default { 60 | bind(el, binding) { 61 | el.addEventListener('click', handleClick(el, binding), false) 62 | }, 63 | update(el, binding) { 64 | el.removeEventListener('click', el[context].removeHandle, false) 65 | el.addEventListener('click', handleClick(el, binding), false) 66 | }, 67 | unbind(el) { 68 | el.removeEventListener('click', el[context].removeHandle, false) 69 | el[context] = null 70 | delete el[context] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /frontend/src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | <svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg> -------------------------------------------------------------------------------- /frontend/src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg> -------------------------------------------------------------------------------- /frontend/src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | <svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg> -------------------------------------------------------------------------------- /frontend/src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | <svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg> -------------------------------------------------------------------------------- /frontend/src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.068 23.784c-1.02 0-1.877-.32-2.572-.96a8.588 8.588 0 0 1-1.738-2.237 11.524 11.524 0 0 1-1.042-2.621c-.232-.895-.348-1.641-.348-2.238V0h.278c.834 0 1.622.085 2.363.256.742.17 1.645.575 2.711 1.214 1.066.64 2.363 1.535 3.892 2.686 1.53 1.15 3.453 2.664 5.77 4.54 2.502 2.045 4.494 3.771 5.977 5.178 1.483 1.406 2.618 2.6 3.406 3.58.787.98 1.274 1.812 1.46 2.494.185.682.277 1.278.277 1.79v2.046H84.068zM127.3 84.01c.278.682.464 1.535.556 2.558.093 1.023-.37 2.003-1.39 2.94-.463.427-.88.832-1.25 1.215-.372.384-.696.704-.974.96a6.69 6.69 0 0 1-.973.767l-11.816-10.741a44.331 44.331 0 0 0 1.877-1.535 31.028 31.028 0 0 1 1.737-1.406c1.112-.938 2.317-1.343 3.615-1.215 1.297.128 2.363.405 3.197.83.927.427 1.923 1.173 2.989 2.239 1.065 1.065 1.876 2.195 2.432 3.388zM78.23 95.902c2.038 0 3.752-.511 5.143-1.534l-26.969 25.83H18.037c-1.761 0-3.684-.47-5.77-1.407a24.549 24.549 0 0 1-5.838-3.709 21.373 21.373 0 0 1-4.518-5.306c-1.204-2.003-1.807-4.07-1.807-6.202V16.495c0-1.79.44-3.665 1.32-5.626A18.41 18.41 0 0 1 5.04 5.562a21.798 21.798 0 0 1 5.213-3.964C12.198.533 14.237 0 16.37 0h53.24v15.984c0 1.62.278 3.367.834 5.242a16.704 16.704 0 0 0 2.572 5.179c1.159 1.577 2.665 2.898 4.518 3.964 1.853 1.066 4.078 1.598 6.673 1.598h20.295v42.325L85.458 92.45c1.02-1.364 1.529-2.856 1.529-4.476 0-2.216-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1c-2.409 0-4.448.789-6.116 2.366-1.668 1.577-2.502 3.474-2.502 5.69 0 2.217.834 4.092 2.502 5.626 1.668 1.535 3.707 2.302 6.117 2.302h52.13zM26.1 47.951c-2.41 0-4.449.789-6.117 2.366-1.668 1.577-2.502 3.473-2.502 5.69 0 2.216.834 4.092 2.502 5.626 1.668 1.534 3.707 2.302 6.117 2.302h52.13c2.409 0 4.47-.768 6.185-2.302 1.715-1.534 2.572-3.41 2.572-5.626 0-2.217-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1zm52.407 64.063l1.807-1.663 3.476-3.196a479.75 479.75 0 0 0 4.587-4.284 500.757 500.757 0 0 1 5.004-4.667c3.985-3.666 8.48-7.758 13.485-12.276l11.677 10.741-13.485 12.404-5.004 4.603-4.587 4.22a179.46 179.46 0 0 0-3.267 3.068c-.88.853-1.367 1.322-1.46 1.407-.463.341-.973.703-1.529 1.087-.556.383-1.112.703-1.668.959-.556.256-1.413.575-2.572.959a83.5 83.5 0 0 1-3.545 1.087 72.2 72.2 0 0 1-3.475.895c-1.112.256-1.946.426-2.502.511-1.112.17-1.854.043-2.224-.383-.371-.426-.464-1.151-.278-2.174.092-.511.278-1.279.556-2.302.278-1.023.602-2.067.973-3.132l1.042-3.005c.325-.938.58-1.577.765-1.918a10.157 10.157 0 0 1 2.224-2.941z"/></svg> -------------------------------------------------------------------------------- /frontend/src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></svg> -------------------------------------------------------------------------------- /frontend/src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.002 9.2c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-5.043-3.58-9.132-7.997-9.132S.002 4.157.002 9.2zM31.997.066h95.981V18.33H31.997V.066zm0 45.669c0 5.044 3.58 9.132 7.998 9.132 4.417 0 7.997-4.088 7.997-9.132 0-3.263-1.524-6.278-3.998-7.91-2.475-1.63-5.524-1.63-7.998 0-2.475 1.632-4 4.647-4 7.91zM63.992 36.6h63.986v18.265H63.992V36.6zm-31.995 82.2c0 5.043 3.58 9.132 7.998 9.132 4.417 0 7.997-4.089 7.997-9.132 0-5.044-3.58-9.133-7.997-9.133s-7.998 4.089-7.998 9.133zm31.995-9.131h63.986v18.265H63.992V109.67zm0-27.404c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-3.263-1.524-6.277-3.998-7.909-2.475-1.631-5.524-1.631-7.998 0-2.475 1.632-4 4.646-4 7.91zm31.995-9.13h31.991V91.4H95.987V73.135z"/></svg> -------------------------------------------------------------------------------- /frontend/src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg> -------------------------------------------------------------------------------- /frontend/src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg> -------------------------------------------------------------------------------- /frontend/src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | <svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg> -------------------------------------------------------------------------------- /frontend/src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | <svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg> -------------------------------------------------------------------------------- /frontend/src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /frontend/src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <section class="app-main"> 3 | <transition name="fade-transform" mode="out-in"> 4 | <router-view :key="key" /> 5 | </transition> 6 | </section> 7 | </template> 8 | 9 | <script> 10 | export default { 11 | name: 'AppMain', 12 | computed: { 13 | key() { 14 | return this.$route.path 15 | } 16 | } 17 | } 18 | </script> 19 | 20 | <style scoped> 21 | .app-main { 22 | /*50 = navbar */ 23 | min-height: calc(100vh - 50px); 24 | width: 100%; 25 | position: relative; 26 | overflow: hidden; 27 | } 28 | .fixed-header+.app-main { 29 | padding-top: 50px; 30 | } 31 | </style> 32 | 33 | <style lang="scss"> 34 | // fix css style bug in open el-dialog 35 | .el-popup-parent--hidden { 36 | .fixed-header { 37 | padding-right: 15px; 38 | } 39 | } 40 | </style> 41 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | device() { 4 | return this.$store.state.app.device 5 | } 6 | }, 7 | mounted() { 8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug 9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135 10 | this.fixBugIniOS() 11 | }, 12 | methods: { 13 | fixBugIniOS() { 14 | const $subMenu = this.$refs.subMenu 15 | if ($subMenu) { 16 | const handleMouseleave = $subMenu.handleMouseleave 17 | $subMenu.handleMouseleave = (e) => { 18 | if (this.device === 'mobile') { 19 | return 20 | } 21 | handleMouseleave(e) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | <script> 2 | export default { 3 | name: 'MenuItem', 4 | functional: true, 5 | props: { 6 | icon: { 7 | type: String, 8 | default: '' 9 | }, 10 | title: { 11 | type: String, 12 | default: '' 13 | } 14 | }, 15 | render(h, context) { 16 | const { icon, title } = context.props 17 | const vnodes = [] 18 | 19 | if (icon) { 20 | if (icon.includes('el-icon')) { 21 | vnodes.push(<i class={[icon, 'sub-el-icon']} />) 22 | } else { 23 | vnodes.push(<svg-icon icon-class={icon}/>) 24 | } 25 | } 26 | 27 | if (title) { 28 | vnodes.push(<span slot='title'>{(title)}</span>) 29 | } 30 | return vnodes 31 | } 32 | } 33 | </script> 34 | 35 | <style scoped> 36 | .sub-el-icon { 37 | color: currentColor; 38 | width: 1em; 39 | height: 1em; 40 | } 41 | </style> 42 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <component :is="type" v-bind="linkProps(to)"> 3 | <slot /> 4 | </component> 5 | </template> 6 | 7 | <script> 8 | import { isExternal } from '@/utils/validate' 9 | 10 | export default { 11 | props: { 12 | to: { 13 | type: String, 14 | required: true 15 | } 16 | }, 17 | computed: { 18 | isExternal() { 19 | return isExternal(this.to) 20 | }, 21 | type() { 22 | if (this.isExternal) { 23 | return 'a' 24 | } 25 | return 'router-link' 26 | } 27 | }, 28 | methods: { 29 | linkProps(to) { 30 | if (this.isExternal) { 31 | return { 32 | href: to, 33 | target: '_blank', 34 | rel: 'noopener' 35 | } 36 | } 37 | return { 38 | to: to 39 | } 40 | } 41 | } 42 | } 43 | </script> 44 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="sidebar-logo-container" :class="{'collapse':collapse}"> 3 | <transition name="sidebarLogoFade"> 4 | <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/"> 5 | <img v-if="logo" :src="logo" class="sidebar-logo"> 6 | <h1 v-else class="sidebar-title">{{ title }} </h1> 7 | </router-link> 8 | <router-link v-else key="expand" class="sidebar-logo-link" to="/"> 9 | <img v-if="logo" :src="logo" class="sidebar-logo"> 10 | <h1 class="sidebar-title">{{ title }} </h1> 11 | </router-link> 12 | </transition> 13 | </div> 14 | </template> 15 | 16 | <script> 17 | export default { 18 | name: 'SidebarLogo', 19 | props: { 20 | collapse: { 21 | type: Boolean, 22 | required: true 23 | } 24 | }, 25 | data() { 26 | return { 27 | title: 'Vue Admin Template', 28 | logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png' 29 | } 30 | } 31 | } 32 | </script> 33 | 34 | <style lang="scss" scoped> 35 | .sidebarLogoFade-enter-active { 36 | transition: opacity 1.5s; 37 | } 38 | 39 | .sidebarLogoFade-enter, 40 | .sidebarLogoFade-leave-to { 41 | opacity: 0; 42 | } 43 | 44 | .sidebar-logo-container { 45 | position: relative; 46 | width: 100%; 47 | height: 50px; 48 | line-height: 50px; 49 | background: #2b2f3a; 50 | text-align: center; 51 | overflow: hidden; 52 | 53 | & .sidebar-logo-link { 54 | height: 100%; 55 | width: 100%; 56 | 57 | & .sidebar-logo { 58 | width: 32px; 59 | height: 32px; 60 | vertical-align: middle; 61 | margin-right: 12px; 62 | } 63 | 64 | & .sidebar-title { 65 | display: inline-block; 66 | margin: 0; 67 | color: #fff; 68 | font-weight: 600; 69 | line-height: 50px; 70 | font-size: 14px; 71 | font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif; 72 | vertical-align: middle; 73 | } 74 | } 75 | 76 | &.collapse { 77 | .sidebar-logo { 78 | margin-right: 0px; 79 | } 80 | } 81 | } 82 | </style> 83 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div v-if="!item.hidden"> 3 | <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow"> 4 | <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)"> 5 | <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}"> 6 | <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" /> 7 | </el-menu-item> 8 | </app-link> 9 | </template> 10 | 11 | <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body> 12 | <template slot="title"> 13 | <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" /> 14 | </template> 15 | <sidebar-item 16 | v-for="child in item.children" 17 | :key="child.path" 18 | :is-nest="true" 19 | :item="child" 20 | :base-path="resolvePath(child.path)" 21 | class="nest-menu" 22 | /> 23 | </el-submenu> 24 | </div> 25 | </template> 26 | 27 | <script> 28 | import path from 'path' 29 | import { isExternal } from '@/utils/validate' 30 | import Item from './Item' 31 | import AppLink from './Link' 32 | import FixiOSBug from './FixiOSBug' 33 | 34 | export default { 35 | name: 'SidebarItem', 36 | components: { Item, AppLink }, 37 | mixins: [FixiOSBug], 38 | props: { 39 | // route object 40 | item: { 41 | type: Object, 42 | required: true 43 | }, 44 | isNest: { 45 | type: Boolean, 46 | default: false 47 | }, 48 | basePath: { 49 | type: String, 50 | default: '' 51 | } 52 | }, 53 | data() { 54 | // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237 55 | // TODO: refactor with render function 56 | this.onlyOneChild = null 57 | return {} 58 | }, 59 | methods: { 60 | hasOneShowingChild(children = [], parent) { 61 | const showingChildren = children.filter(item => { 62 | if (item.hidden) { 63 | return false 64 | } else { 65 | // Temp set(will be used if only has one showing child) 66 | this.onlyOneChild = item 67 | return true 68 | } 69 | }) 70 | 71 | // When there is only one child router, the child router is displayed by default 72 | if (showingChildren.length === 1) { 73 | return true 74 | } 75 | 76 | // Show parent if there are no child router to display 77 | if (showingChildren.length === 0) { 78 | this.onlyOneChild = { ... parent, path: '', noShowingChildren: true } 79 | return true 80 | } 81 | 82 | return false 83 | }, 84 | resolvePath(routePath) { 85 | if (isExternal(routePath)) { 86 | return routePath 87 | } 88 | if (isExternal(this.basePath)) { 89 | return this.basePath 90 | } 91 | return path.resolve(this.basePath, routePath) 92 | } 93 | } 94 | } 95 | </script> 96 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div :class="{'has-logo':showLogo}"> 3 | <logo v-if="showLogo" :collapse="isCollapse" /> 4 | <el-scrollbar wrap-class="scrollbar-wrapper"> 5 | <el-menu 6 | :default-active="activeMenu" 7 | :collapse="isCollapse" 8 | :background-color="variables.menuBg" 9 | :text-color="variables.menuText" 10 | :unique-opened="false" 11 | :active-text-color="variables.menuActiveText" 12 | :collapse-transition="false" 13 | mode="vertical" 14 | > 15 | <sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" /> 16 | </el-menu> 17 | </el-scrollbar> 18 | </div> 19 | </template> 20 | 21 | <script> 22 | import { mapGetters } from 'vuex' 23 | import Logo from './Logo' 24 | import SidebarItem from './SidebarItem' 25 | import variables from '@/styles/variables.scss' 26 | 27 | export default { 28 | components: { SidebarItem, Logo }, 29 | computed: { 30 | ...mapGetters([ 31 | 'sidebar' 32 | ]), 33 | routes() { 34 | return this.$router.options.routes 35 | }, 36 | activeMenu() { 37 | const route = this.$route 38 | const { meta, path } = route 39 | // if set path, the sidebar will highlight the path you set 40 | if (meta.activeMenu) { 41 | return meta.activeMenu 42 | } 43 | return path 44 | }, 45 | showLogo() { 46 | return this.$store.state.settings.sidebarLogo 47 | }, 48 | variables() { 49 | return variables 50 | }, 51 | isCollapse() { 52 | return !this.sidebar.opened 53 | } 54 | } 55 | } 56 | </script> 57 | -------------------------------------------------------------------------------- /frontend/src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from './Navbar' 2 | export { default as Sidebar } from './Sidebar' 3 | export { default as AppMain } from './AppMain' 4 | -------------------------------------------------------------------------------- /frontend/src/layout/index.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div :class="classObj" class="app-wrapper"> 3 | <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" /> 4 | <sidebar class="sidebar-container" /> 5 | <div class="main-container"> 6 | <div :class="{'fixed-header':fixedHeader}"> 7 | <navbar /> 8 | </div> 9 | <app-main /> 10 | </div> 11 | </div> 12 | </template> 13 | 14 | <script> 15 | import { Navbar, Sidebar, AppMain } from './components' 16 | import ResizeMixin from './mixin/ResizeHandler' 17 | 18 | export default { 19 | name: 'Layout', 20 | components: { 21 | Navbar, 22 | Sidebar, 23 | AppMain 24 | }, 25 | mixins: [ResizeMixin], 26 | computed: { 27 | sidebar() { 28 | return this.$store.state.app.sidebar 29 | }, 30 | device() { 31 | return this.$store.state.app.device 32 | }, 33 | fixedHeader() { 34 | return this.$store.state.settings.fixedHeader 35 | }, 36 | classObj() { 37 | return { 38 | hideSidebar: !this.sidebar.opened, 39 | openSidebar: this.sidebar.opened, 40 | withoutAnimation: this.sidebar.withoutAnimation, 41 | mobile: this.device === 'mobile' 42 | } 43 | } 44 | }, 45 | methods: { 46 | handleClickOutside() { 47 | this.$store.dispatch('app/closeSideBar', { withoutAnimation: false }) 48 | } 49 | } 50 | } 51 | </script> 52 | 53 | <style lang="scss" scoped> 54 | @import "~@/styles/mixin.scss"; 55 | @import "~@/styles/variables.scss"; 56 | 57 | .app-wrapper { 58 | @include clearfix; 59 | position: relative; 60 | height: 100%; 61 | width: 100%; 62 | &.mobile.openSidebar{ 63 | position: fixed; 64 | top: 0; 65 | } 66 | } 67 | .drawer-bg { 68 | background: #000; 69 | opacity: 0.3; 70 | width: 100%; 71 | top: 0; 72 | height: 100%; 73 | position: absolute; 74 | z-index: 999; 75 | } 76 | 77 | .fixed-header { 78 | position: fixed; 79 | top: 0; 80 | right: 0; 81 | z-index: 9; 82 | width: calc(100% - #{$sideBarWidth}); 83 | transition: width 0.28s; 84 | } 85 | 86 | .hideSidebar .fixed-header { 87 | width: calc(100% - 54px) 88 | } 89 | 90 | .mobile .fixed-header { 91 | width: 100%; 92 | } 93 | </style> 94 | -------------------------------------------------------------------------------- /frontend/src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 11 | } 12 | } 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeDestroy() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile') 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import 'normalize.css/normalize.css' // A modern alternative to CSS resets 4 | 5 | import ElementUI from 'element-ui' 6 | import 'element-ui/lib/theme-chalk/index.css' 7 | import locale from 'element-ui/lib/locale/lang/en' // lang i18n 8 | 9 | import '@/styles/index.scss' // global css 10 | 11 | import App from './App' 12 | import store from './store' 13 | import router from './router' 14 | 15 | import '@/icons' // icon 16 | // import '@/permission' // permission control 17 | // 不进行登录校验。 18 | 19 | /** 20 | * If you don't want to use mock-server 21 | * you want to use MockJs for mock api 22 | * you can execute: mockXHR() 23 | * 24 | * Currently MockJs will be used in the production environment, 25 | * please remove it before going online ! ! ! 26 | */ 27 | if (process.env.NODE_ENV === 'production') { 28 | const { mockXHR } = require('../mock') 29 | mockXHR() 30 | } 31 | 32 | // set ElementUI lang to EN 33 | Vue.use(ElementUI, { locale }) 34 | // 如果想要中文版 element-ui,按如下方式声明 35 | // Vue.use(ElementUI) 36 | 37 | Vue.config.productionTip = false 38 | 39 | new Vue({ 40 | el: '#app', 41 | router, 42 | store, 43 | render: h => h(App) 44 | }) 45 | -------------------------------------------------------------------------------- /frontend/src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import store from './store' 3 | import { Message } from 'element-ui' 4 | import NProgress from 'nprogress' // progress bar 5 | import 'nprogress/nprogress.css' // progress bar style 6 | import { getToken } from '@/utils/auth' // get token from cookie 7 | import getPageTitle from '@/utils/get-page-title' 8 | 9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration 10 | 11 | const whiteList = ['/login'] // no redirect whitelist 12 | 13 | router.beforeEach(async(to, from, next) => { 14 | // start progress bar 15 | NProgress.start() 16 | 17 | // set page title 18 | document.title = getPageTitle(to.meta.title) 19 | 20 | // determine whether the user has logged in 21 | const hasToken = getToken() 22 | 23 | if (hasToken) { 24 | if (to.path === '/login') { 25 | // if is logged in, redirect to the home page 26 | next({ path: '/' }) 27 | NProgress.done() 28 | } else { 29 | const hasGetUserInfo = store.getters.name 30 | if (hasGetUserInfo) { 31 | next() 32 | } else { 33 | try { 34 | // get user info 35 | await store.dispatch('user/getInfo') 36 | 37 | next() 38 | } catch (error) { 39 | // remove token and go to login page to re-login 40 | await store.dispatch('user/resetToken') 41 | Message.error(error || 'Has Error') 42 | next(`/login?redirect=${to.path}`) 43 | NProgress.done() 44 | } 45 | } 46 | } 47 | } else { 48 | /* has no token*/ 49 | 50 | if (whiteList.indexOf(to.path) !== -1) { 51 | // in the free login whitelist, go directly 52 | next() 53 | } else { 54 | // other pages that do not have permission to access are redirected to the login page. 55 | next(`/login?redirect=${to.path}`) 56 | NProgress.done() 57 | } 58 | } 59 | }) 60 | 61 | router.afterEach(() => { 62 | // finish progress bar 63 | NProgress.done() 64 | }) 65 | -------------------------------------------------------------------------------- /frontend/src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | title: 'Vue Admin Template', 4 | 5 | /** 6 | * @type {boolean} true | false 7 | * @description Whether fix the header 8 | */ 9 | fixedHeader: false, 10 | 11 | /** 12 | * @type {boolean} true | false 13 | * @description Whether show the logo in sidebar 14 | */ 15 | sidebarLogo: false 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | device: state => state.app.device, 4 | token: state => state.user.token, 5 | avatar: state => state.user.avatar, 6 | name: state => state.user.name 7 | } 8 | export default getters 9 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | import app from './modules/app' 5 | import settings from './modules/settings' 6 | import user from './modules/user' 7 | 8 | Vue.use(Vuex) 9 | 10 | const store = new Vuex.Store({ 11 | modules: { 12 | app, 13 | settings, 14 | user 15 | }, 16 | getters 17 | }) 18 | 19 | export default store 20 | -------------------------------------------------------------------------------- /frontend/src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const state = { 4 | sidebar: { 5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, 6 | withoutAnimation: false 7 | }, 8 | device: 'desktop' 9 | } 10 | 11 | const mutations = { 12 | TOGGLE_SIDEBAR: state => { 13 | state.sidebar.opened = !state.sidebar.opened 14 | state.sidebar.withoutAnimation = false 15 | if (state.sidebar.opened) { 16 | Cookies.set('sidebarStatus', 1) 17 | } else { 18 | Cookies.set('sidebarStatus', 0) 19 | } 20 | }, 21 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 22 | Cookies.set('sidebarStatus', 0) 23 | state.sidebar.opened = false 24 | state.sidebar.withoutAnimation = withoutAnimation 25 | }, 26 | TOGGLE_DEVICE: (state, device) => { 27 | state.device = device 28 | } 29 | } 30 | 31 | const actions = { 32 | toggleSideBar({ commit }) { 33 | commit('TOGGLE_SIDEBAR') 34 | }, 35 | closeSideBar({ commit }, { withoutAnimation }) { 36 | commit('CLOSE_SIDEBAR', withoutAnimation) 37 | }, 38 | toggleDevice({ commit }, device) { 39 | commit('TOGGLE_DEVICE', device) 40 | } 41 | } 42 | 43 | export default { 44 | namespaced: true, 45 | state, 46 | mutations, 47 | actions 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const { showSettings, fixedHeader, sidebarLogo } = defaultSettings 4 | 5 | const state = { 6 | showSettings: showSettings, 7 | fixedHeader: fixedHeader, 8 | sidebarLogo: sidebarLogo 9 | } 10 | 11 | const mutations = { 12 | CHANGE_SETTING: (state, { key, value }) => { 13 | // eslint-disable-next-line no-prototype-builtins 14 | if (state.hasOwnProperty(key)) { 15 | state[key] = value 16 | } 17 | } 18 | } 19 | 20 | const actions = { 21 | changeSetting({ commit }, data) { 22 | commit('CHANGE_SETTING', data) 23 | } 24 | } 25 | 26 | export default { 27 | namespaced: true, 28 | state, 29 | mutations, 30 | actions 31 | } 32 | 33 | -------------------------------------------------------------------------------- /frontend/src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { login, logout, getInfo } from '@/api/user' 2 | import { getToken, setToken, removeToken } from '@/utils/auth' 3 | import { resetRouter } from '@/router' 4 | 5 | const getDefaultState = () => { 6 | return { 7 | token: getToken(), 8 | name: '', 9 | avatar: '' 10 | } 11 | } 12 | 13 | const state = getDefaultState() 14 | 15 | const mutations = { 16 | RESET_STATE: (state) => { 17 | Object.assign(state, getDefaultState()) 18 | }, 19 | SET_TOKEN: (state, token) => { 20 | state.token = token 21 | }, 22 | SET_NAME: (state, name) => { 23 | state.name = name 24 | }, 25 | SET_AVATAR: (state, avatar) => { 26 | state.avatar = avatar 27 | } 28 | } 29 | 30 | const actions = { 31 | // user login 32 | login({ commit }, userInfo) { 33 | const { username, password } = userInfo 34 | return new Promise((resolve, reject) => { 35 | login({ username: username.trim(), password: password }).then(response => { 36 | const { data } = response 37 | commit('SET_TOKEN', data.token) 38 | setToken(data.token) 39 | resolve() 40 | }).catch(error => { 41 | reject(error) 42 | }) 43 | }) 44 | }, 45 | 46 | // get user info 47 | getInfo({ commit, state }) { 48 | return new Promise((resolve, reject) => { 49 | getInfo(state.token).then(response => { 50 | const { data } = response 51 | 52 | if (!data) { 53 | return reject('Verification failed, please Login again.') 54 | } 55 | 56 | const { name, avatar } = data 57 | 58 | commit('SET_NAME', name) 59 | commit('SET_AVATAR', avatar) 60 | resolve(data) 61 | }).catch(error => { 62 | reject(error) 63 | }) 64 | }) 65 | }, 66 | 67 | // user logout 68 | logout({ commit, state }) { 69 | return new Promise((resolve, reject) => { 70 | logout(state.token).then(() => { 71 | removeToken() // must remove token first 72 | resetRouter() 73 | commit('RESET_STATE') 74 | resolve() 75 | }).catch(error => { 76 | reject(error) 77 | }) 78 | }) 79 | }, 80 | 81 | // remove token 82 | resetToken({ commit }) { 83 | return new Promise(resolve => { 84 | removeToken() // must remove token first 85 | commit('RESET_STATE') 86 | resolve() 87 | }) 88 | } 89 | } 90 | 91 | export default { 92 | namespaced: true, 93 | state, 94 | mutations, 95 | actions 96 | } 97 | 98 | -------------------------------------------------------------------------------- /frontend/src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | 19 | // to fixed https://github.com/ElemeFE/element/issues/2461 20 | .el-dialog { 21 | transform: none; 22 | left: 0; 23 | position: relative; 24 | margin: 0 auto; 25 | } 26 | 27 | // refine element ui upload 28 | .upload-container { 29 | .el-upload { 30 | width: 100%; 31 | 32 | .el-upload-dragger { 33 | width: 100%; 34 | height: 200px; 35 | } 36 | } 37 | } 38 | 39 | // dropdown 40 | .el-dropdown-menu { 41 | a { 42 | display: block 43 | } 44 | } 45 | 46 | // to fix el-date-picker css style 47 | .el-range-separator { 48 | box-sizing: content-box; 49 | } 50 | -------------------------------------------------------------------------------- /frontend/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './mixin.scss'; 3 | @import './transition.scss'; 4 | @import './element-ui.scss'; 5 | @import './sidebar.scss'; 6 | 7 | body { 8 | height: 100%; 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-font-smoothing: antialiased; 11 | text-rendering: optimizeLegibility; 12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 13 | } 14 | 15 | label { 16 | font-weight: 700; 17 | } 18 | 19 | html { 20 | height: 100%; 21 | box-sizing: border-box; 22 | } 23 | 24 | #app { 25 | height: 100%; 26 | } 27 | 28 | *, 29 | *:before, 30 | *:after { 31 | box-sizing: inherit; 32 | } 33 | 34 | a:focus, 35 | a:active { 36 | outline: none; 37 | } 38 | 39 | a, 40 | a:focus, 41 | a:hover { 42 | cursor: pointer; 43 | color: inherit; 44 | text-decoration: none; 45 | } 46 | 47 | div:focus { 48 | outline: none; 49 | } 50 | 51 | .clearfix { 52 | &:after { 53 | visibility: hidden; 54 | display: block; 55 | font-size: 0; 56 | content: " "; 57 | clear: both; 58 | height: 0; 59 | } 60 | } 61 | 62 | // main-container global css 63 | .app-container { 64 | padding: 20px; 65 | } 66 | -------------------------------------------------------------------------------- /frontend/src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .5s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all .5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // sidebar 2 | $menuText:#bfcbd9; 3 | $menuActiveText:#409EFF; 4 | $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951 5 | 6 | $menuBg:#304156; 7 | $menuHover:#263445; 8 | 9 | $subMenuBg:#1f2d3d; 10 | $subMenuHover:#001528; 11 | 12 | $sideBarWidth: 210px; 13 | 14 | // the :export directive is the magic sauce for webpack 15 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 16 | :export { 17 | menuText: $menuText; 18 | menuActiveText: $menuActiveText; 19 | subMenuActiveText: $subMenuActiveText; 20 | menuBg: $menuBg; 21 | menuHover: $menuHover; 22 | subMenuBg: $subMenuBg; 23 | subMenuHover: $subMenuHover; 24 | sideBarWidth: $sideBarWidth; 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'vue_admin_template_token' 4 | 5 | export function getToken() { 6 | return Cookies.get(TokenKey) 7 | } 8 | 9 | export function setToken(token) { 10 | return Cookies.set(TokenKey, token) 11 | } 12 | 13 | export function removeToken() { 14 | return Cookies.remove(TokenKey) 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || 'Vue Admin Template' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * Parse the time to string 7 | * @param {(Object|string|number)} time 8 | * @param {string} cFormat 9 | * @returns {string | null} 10 | */ 11 | export function parseTime(time, cFormat) { 12 | if (arguments.length === 0 || !time) { 13 | return null 14 | } 15 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 16 | let date 17 | if (typeof time === 'object') { 18 | date = time 19 | } else { 20 | if ((typeof time === 'string')) { 21 | if ((/^[0-9]+$/.test(time))) { 22 | // support "1548221490638" 23 | time = parseInt(time) 24 | } else { 25 | // support safari 26 | // https://stackoverflow.com/questions/4310953/invalid-date-in-safari 27 | time = time.replace(new RegExp(/-/gm), '/') 28 | } 29 | } 30 | 31 | if ((typeof time === 'number') && (time.toString().length === 10)) { 32 | time = time * 1000 33 | } 34 | date = new Date(time) 35 | } 36 | const formatObj = { 37 | y: date.getFullYear(), 38 | m: date.getMonth() + 1, 39 | d: date.getDate(), 40 | h: date.getHours(), 41 | i: date.getMinutes(), 42 | s: date.getSeconds(), 43 | a: date.getDay() 44 | } 45 | const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { 46 | const value = formatObj[key] 47 | // Note: getDay() returns 0 on Sunday 48 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } 49 | return value.toString().padStart(2, '0') 50 | }) 51 | return time_str 52 | } 53 | 54 | /** 55 | * @param {number} time 56 | * @param {string} option 57 | * @returns {string} 58 | */ 59 | export function formatTime(time, option) { 60 | if (('' + time).length === 10) { 61 | time = parseInt(time) * 1000 62 | } else { 63 | time = +time 64 | } 65 | const d = new Date(time) 66 | const now = Date.now() 67 | 68 | const diff = (now - d) / 1000 69 | 70 | if (diff < 30) { 71 | return '刚刚' 72 | } else if (diff < 3600) { 73 | // less 1 hour 74 | return Math.ceil(diff / 60) + '分钟前' 75 | } else if (diff < 3600 * 24) { 76 | return Math.ceil(diff / 3600) + '小时前' 77 | } else if (diff < 3600 * 24 * 2) { 78 | return '1天前' 79 | } 80 | if (option) { 81 | return parseTime(time, option) 82 | } else { 83 | return ( 84 | d.getMonth() + 85 | 1 + 86 | '月' + 87 | d.getDate() + 88 | '日' + 89 | d.getHours() + 90 | '时' + 91 | d.getMinutes() + 92 | '分' 93 | ) 94 | } 95 | } 96 | 97 | /** 98 | * @param {string} url 99 | * @returns {Object} 100 | */ 101 | export function param2Obj(url) { 102 | const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') 103 | if (!search) { 104 | return {} 105 | } 106 | const obj = {} 107 | const searchArr = search.split('&') 108 | searchArr.forEach(v => { 109 | const index = v.indexOf('=') 110 | if (index !== -1) { 111 | const name = v.substring(0, index) 112 | const val = v.substring(index + 1, v.length) 113 | obj[name] = val 114 | } 115 | }) 116 | return obj 117 | } 118 | -------------------------------------------------------------------------------- /frontend/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { MessageBox, Message } from 'element-ui' 3 | import store from '@/store' 4 | import { getToken } from '@/utils/auth' 5 | 6 | // create an axios instance 7 | const service = axios.create({ 8 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url 9 | withCredentials: true, // send cookies when cross-domain requests 10 | timeout: 5000 // request timeout 11 | }) 12 | 13 | // request interceptor 14 | service.interceptors.request.use( 15 | config => { 16 | // do something before request is sent 17 | 18 | if (store.getters.token) { 19 | // let each request carry token 20 | // ['X-Token'] is a custom headers key 21 | // please modify it according to the actual situation 22 | config.headers['X-Token'] = getToken() 23 | } 24 | return config 25 | }, 26 | error => { 27 | // do something with request error 28 | console.log(error) // for debug 29 | return Promise.reject(error) 30 | } 31 | ) 32 | 33 | // response interceptor 34 | service.interceptors.response.use( 35 | /** 36 | * If you want to get http information such as headers or status 37 | * Please return response => response 38 | */ 39 | 40 | /** 41 | * Determine the request status by custom code 42 | * Here is just an example 43 | * You can also judge the status by HTTP Status Code 44 | */ 45 | response => { 46 | const res = response.data 47 | console.log('res.code: ' + res.code) // for debug 48 | 49 | // if the custom code is not 20000, it is judged as an error. 50 | if (res.code !== 20000) { 51 | Message({ 52 | message: res.message || 'Error', 53 | type: 'error', 54 | duration: 5 * 1000 55 | }) 56 | 57 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; 58 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) { 59 | // to re-login 60 | MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', { 61 | confirmButtonText: 'Re-Login', 62 | cancelButtonText: 'Cancel', 63 | type: 'warning' 64 | }).then(() => { 65 | store.dispatch('user/resetToken').then(() => { 66 | location.reload() 67 | }) 68 | }) 69 | } 70 | return Promise.reject(new Error(res.message || 'Error')) 71 | } else { 72 | return res 73 | } 74 | }, 75 | error => { 76 | console.log('err' + error) // for debug 77 | Message({ 78 | message: error.message, 79 | type: 'error', 80 | duration: 5 * 1000 81 | }) 82 | return Promise.reject(error) 83 | } 84 | ) 85 | 86 | export default service 87 | -------------------------------------------------------------------------------- /frontend/src/utils/scroll-to.js: -------------------------------------------------------------------------------- 1 | Math.easeInOutQuad = function(t, b, c, d) { 2 | t /= d / 2 3 | if (t < 1) { 4 | return c / 2 * t * t + b 5 | } 6 | t-- 7 | return -c / 2 * (t * (t - 2) - 1) + b 8 | } 9 | 10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts 11 | var requestAnimFrame = (function() { 12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) } 13 | })() 14 | 15 | /** 16 | * Because it's so fucking difficult to detect the scrolling element, just move them all 17 | * @param {number} amount 18 | */ 19 | function move(amount) { 20 | document.documentElement.scrollTop = amount 21 | document.body.parentNode.scrollTop = amount 22 | document.body.scrollTop = amount 23 | } 24 | 25 | function position() { 26 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop 27 | } 28 | 29 | /** 30 | * @param {number} to 31 | * @param {number} duration 32 | * @param {Function} callback 33 | */ 34 | export function scrollTo(to, duration, callback) { 35 | const start = position() 36 | const change = to - start 37 | const increment = 20 38 | let currentTime = 0 39 | duration = (typeof (duration) === 'undefined') ? 500 : duration 40 | var animateScroll = function() { 41 | // increment the time 42 | currentTime += increment 43 | // find the value with the quadratic in-out easing function 44 | var val = Math.easeInOutQuad(currentTime, start, change, duration) 45 | // move the document.body 46 | move(val) 47 | // do the animation unless its over 48 | if (currentTime < duration) { 49 | requestAnimFrame(animateScroll) 50 | } else { 51 | if (callback && typeof (callback) === 'function') { 52 | // the animation is done so lets callback 53 | callback() 54 | } 55 | } 56 | } 57 | animateScroll() 58 | } 59 | -------------------------------------------------------------------------------- /frontend/src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * @param {string} path 7 | * @returns {Boolean} 8 | */ 9 | export function isExternal(path) { 10 | return /^(https?:|mailto:|tel:)/.test(path) 11 | } 12 | 13 | /** 14 | * @param {string} str 15 | * @returns {Boolean} 16 | */ 17 | export function validUsername(str) { 18 | const valid_map = ['admin', 'editor'] 19 | return valid_map.indexOf(str.trim()) >= 0 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="dashboard-container"> 3 | 4 | <div class="clearfix"> 5 | <div class="pull-left tableTools-container"> 6 | <h3>基础库版本</h3> 7 | <p>1,pandas使用【 {{ pandasVersion }} 】版本, <el-link type="primary" href="https://www.runoob.com/pandas/pandas-tutorial.html" target="_blank">中文文档</el-link> </p> 8 | <p>2,numpy使用【 {{ numpyVersion }} 】版本, <el-link type="primary" href="https://www.runoob.com/numpy/numpy-tutorial.html" target="_blank">中文文档</el-link></p> 9 | <p>3,sqlalchemy使用【 {{ sqlalchemyVersion }} 】版本, <el-link type="primary" href="https://docs.sqlalchemy.org/en/20/" target="_blank">英文文档</el-link></p> 10 | <p>4,akshare使用【 {{ akshareVersion }} 】版本, <el-link type="primary" href="https://akshare.akfamily.xyz/data/index/index.html" target="_blank">中文文档</el-link></p> 11 | <p>5,bokeh使用【 {{ bokehVersion }} 】版本, <el-link type="primary" href="http://docs.bokeh.org/en/latest/" target="_blank">官方文档</el-link></p> 12 | <p>6,stockstats使用【 {{ stockstatsVersion }} 】版本, <el-link type="primary" href="https://github.com/jealous/stockstats/" target="_blank">官方文档</el-link></p> 13 | </div> 14 | </div> 15 | 16 | <div class="clearfix"> 17 | <div class="pull-left tableTools-container"> 18 | <h3>相关资料信息</h3> 19 | <p>1,github项目地址。<el-link type="primary" href="https://gitee.com/pythonstock/stock" target="_blank">pythonstock</el-link></p> 20 | <p>2,博客地址。<el-link type="primary" href="https://blog.csdn.net/freewebsys/category_9285317.html" target="_blank">博客地址</el-link></p> 21 | </div> 22 | </div> 23 | 24 | <div class="clearfix"> 25 | <div class="pull-left tableTools-container"> 26 | <h3>2021年9月20日更新,发布2.0版本</h3> 27 | <p>1,修复bokeh的版本升级问题,可以显示趋势数据了。</p> 28 | <p>2,使用 stock_zh_ah_name 做每日数据。</p> 29 | <p>3,AkShare 升级到 1.1.9 版本。</p> 30 | </div> 31 | </div> 32 | 33 | <div class="clearfix"> 34 | <div class="pull-left tableTools-container"> 35 | <h3>2021年8月31日更新job服务</h3> 36 | <p>1,过滤包括:600,6006,601,000,001,002,且不包括ST的股票数据。。</p> 37 | <p>2,使用 stock_zh_ah_name 做每日数据。</p> 38 | <p>3,AkShare 升级到 1.0.80 版本。</p> 39 | 40 | </div> 41 | </div> 42 | 43 | <div class="clearfix"> 44 | <div class="pull-left tableTools-container"> 45 | <h3>2021年6月3日使用 AkShare 做数据抓取服务</h3> 46 | <p>1,使用 stock_zh_a_spot 做实时行情数据。</p> 47 | <p>2,使用 stock_zh_a_daily 做历史数据统计。</p> 48 | <p>3,升级基础镜像使用python3.7,AkShare 的 0.9.65 版本。</p> 49 | </div> 50 | </div> 51 | 52 | 53 | 54 | </div> 55 | </template> 56 | 57 | <script> 58 | import { mapGetters } from 'vuex' 59 | import { fetchPackageVersion } from '@/api/package' 60 | 61 | export default { 62 | name: '首页', 63 | data() { 64 | return { 65 | pandasVersion: '', 66 | numpyVersion: '', 67 | sqlalchemyVersion: '', 68 | akshareVersion: '', 69 | bokehVersion: '', 70 | stockstatsVersion: '', 71 | } 72 | }, 73 | created() { 74 | this.getPackageVersion() 75 | }, 76 | methods: { 77 | getPackageVersion() { 78 | console.info("$router.params : ", this.$router.params) 79 | fetchPackageVersion().then(response => { 80 | 81 | this.pandasVersion = response.pandasVersion 82 | this.numpyVersion = response.numpyVersion 83 | this.sqlalchemyVersion = response.sqlalchemyVersion 84 | this.akshareVersion = response.akshareVersion 85 | this.bokehVersion = response.bokehVersion 86 | this.stockstatsVersion = response.stockstatsVersion 87 | 88 | }) 89 | } 90 | } 91 | } 92 | </script> 93 | 94 | <style lang="scss" scoped> 95 | .dashboard { 96 | &-container { 97 | margin: 30px; 98 | } 99 | &-text { 100 | font-size: 30px; 101 | line-height: 46px; 102 | } 103 | } 104 | </style> -------------------------------------------------------------------------------- /frontend/src/views/form/index.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="app-container"> 3 | <el-form ref="form" :model="form" label-width="120px"> 4 | <el-form-item label="Activity name"> 5 | <el-input v-model="form.name" /> 6 | </el-form-item> 7 | <el-form-item label="Activity zone"> 8 | <el-select v-model="form.region" placeholder="please select your zone"> 9 | <el-option label="Zone one" value="shanghai" /> 10 | <el-option label="Zone two" value="beijing" /> 11 | </el-select> 12 | </el-form-item> 13 | <el-form-item label="Activity time"> 14 | <el-col :span="11"> 15 | <el-date-picker v-model="form.date1" type="date" placeholder="Pick a date" style="width: 100%;" /> 16 | </el-col> 17 | <el-col :span="2" class="line">-</el-col> 18 | <el-col :span="11"> 19 | <el-time-picker v-model="form.date2" type="fixed-time" placeholder="Pick a time" style="width: 100%;" /> 20 | </el-col> 21 | </el-form-item> 22 | <el-form-item label="Instant delivery"> 23 | <el-switch v-model="form.delivery" /> 24 | </el-form-item> 25 | <el-form-item label="Activity type"> 26 | <el-checkbox-group v-model="form.type"> 27 | <el-checkbox label="Online activities" name="type" /> 28 | <el-checkbox label="Promotion activities" name="type" /> 29 | <el-checkbox label="Offline activities" name="type" /> 30 | <el-checkbox label="Simple brand exposure" name="type" /> 31 | </el-checkbox-group> 32 | </el-form-item> 33 | <el-form-item label="Resources"> 34 | <el-radio-group v-model="form.resource"> 35 | <el-radio label="Sponsor" /> 36 | <el-radio label="Venue" /> 37 | </el-radio-group> 38 | </el-form-item> 39 | <el-form-item label="Activity form"> 40 | <el-input v-model="form.desc" type="textarea" /> 41 | </el-form-item> 42 | <el-form-item> 43 | <el-button type="primary" @click="onSubmit">Create</el-button> 44 | <el-button @click="onCancel">Cancel</el-button> 45 | </el-form-item> 46 | </el-form> 47 | </div> 48 | </template> 49 | 50 | <script> 51 | export default { 52 | data() { 53 | return { 54 | form: { 55 | name: '', 56 | region: '', 57 | date1: '', 58 | date2: '', 59 | delivery: false, 60 | type: [], 61 | resource: '', 62 | desc: '' 63 | } 64 | } 65 | }, 66 | methods: { 67 | onSubmit() { 68 | this.$message('submit!') 69 | }, 70 | onCancel() { 71 | this.$message({ 72 | message: 'cancel!', 73 | type: 'warning' 74 | }) 75 | } 76 | } 77 | } 78 | </script> 79 | 80 | <style scoped> 81 | .line{ 82 | text-align: center; 83 | } 84 | </style> 85 | 86 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/index.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div style="padding:30px;"> 3 | <el-alert :closable="false" title="menu 1"> 4 | <router-view /> 5 | </el-alert> 6 | </div> 7 | </template> 8 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/menu1-1/index.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div style="padding:30px;"> 3 | <el-alert :closable="false" title="menu 1-1" type="success"> 4 | <router-view /> 5 | </el-alert> 6 | </div> 7 | </template> 8 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/menu1-2/index.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div style="padding:30px;"> 3 | <el-alert :closable="false" title="menu 1-2" type="success"> 4 | <router-view /> 5 | </el-alert> 6 | </div> 7 | </template> 8 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/menu1-2/menu1-2-1/index.vue: -------------------------------------------------------------------------------- 1 | <template functional> 2 | <div style="padding:30px;"> 3 | <el-alert :closable="false" title="menu 1-2-1" type="warning" /> 4 | </div> 5 | </template> 6 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/menu1-2/menu1-2-2/index.vue: -------------------------------------------------------------------------------- 1 | <template functional> 2 | <div style="padding:30px;"> 3 | <el-alert :closable="false" title="menu 1-2-2" type="warning" /> 4 | </div> 5 | </template> 6 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu1/menu1-3/index.vue: -------------------------------------------------------------------------------- 1 | <template functional> 2 | <div style="padding:30px;"> 3 | <el-alert :closable="false" title="menu 1-3" type="success" /> 4 | </div> 5 | </template> 6 | -------------------------------------------------------------------------------- /frontend/src/views/nested/menu2/index.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div style="padding:30px;"> 3 | <el-alert :closable="false" title="menu 2" /> 4 | </div> 5 | </template> 6 | -------------------------------------------------------------------------------- /frontend/src/views/tree/index.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="app-container"> 3 | <el-input v-model="filterText" placeholder="Filter keyword" style="margin-bottom:30px;" /> 4 | 5 | <el-tree 6 | ref="tree2" 7 | :data="data2" 8 | :props="defaultProps" 9 | :filter-node-method="filterNode" 10 | class="filter-tree" 11 | default-expand-all 12 | /> 13 | 14 | </div> 15 | </template> 16 | 17 | <script> 18 | export default { 19 | 20 | data() { 21 | return { 22 | filterText: '', 23 | data2: [{ 24 | id: 1, 25 | label: 'Level one 1', 26 | children: [{ 27 | id: 4, 28 | label: 'Level two 1-1', 29 | children: [{ 30 | id: 9, 31 | label: 'Level three 1-1-1' 32 | }, { 33 | id: 10, 34 | label: 'Level three 1-1-2' 35 | }] 36 | }] 37 | }, { 38 | id: 2, 39 | label: 'Level one 2', 40 | children: [{ 41 | id: 5, 42 | label: 'Level two 2-1' 43 | }, { 44 | id: 6, 45 | label: 'Level two 2-2' 46 | }] 47 | }, { 48 | id: 3, 49 | label: 'Level one 3', 50 | children: [{ 51 | id: 7, 52 | label: 'Level two 3-1' 53 | }, { 54 | id: 8, 55 | label: 'Level two 3-2' 56 | }] 57 | }], 58 | defaultProps: { 59 | children: 'children', 60 | label: 'label' 61 | } 62 | } 63 | }, 64 | watch: { 65 | filterText(val) { 66 | this.$refs.tree2.filter(val) 67 | } 68 | }, 69 | 70 | methods: { 71 | filterNode(value, data) { 72 | if (!value) return true 73 | return data.label.indexOf(value) !== -1 74 | } 75 | } 76 | } 77 | </script> 78 | 79 | -------------------------------------------------------------------------------- /frontend/tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /frontend/tests/unit/components/Breadcrumb.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, createLocalVue } from '@vue/test-utils' 2 | import VueRouter from 'vue-router' 3 | import ElementUI from 'element-ui' 4 | import Breadcrumb from '@/components/Breadcrumb/index.vue' 5 | 6 | const localVue = createLocalVue() 7 | localVue.use(VueRouter) 8 | localVue.use(ElementUI) 9 | 10 | const routes = [ 11 | { 12 | path: '/', 13 | name: 'home', 14 | children: [{ 15 | path: 'dashboard', 16 | name: 'dashboard' 17 | }] 18 | }, 19 | { 20 | path: '/menu', 21 | name: 'menu', 22 | children: [{ 23 | path: 'menu1', 24 | name: 'menu1', 25 | meta: { title: 'menu1' }, 26 | children: [{ 27 | path: 'menu1-1', 28 | name: 'menu1-1', 29 | meta: { title: 'menu1-1' } 30 | }, 31 | { 32 | path: 'menu1-2', 33 | name: 'menu1-2', 34 | redirect: 'noredirect', 35 | meta: { title: 'menu1-2' }, 36 | children: [{ 37 | path: 'menu1-2-1', 38 | name: 'menu1-2-1', 39 | meta: { title: 'menu1-2-1' } 40 | }, 41 | { 42 | path: 'menu1-2-2', 43 | name: 'menu1-2-2' 44 | }] 45 | }] 46 | }] 47 | }] 48 | 49 | const router = new VueRouter({ 50 | routes 51 | }) 52 | 53 | describe('Breadcrumb.vue', () => { 54 | const wrapper = mount(Breadcrumb, { 55 | localVue, 56 | router 57 | }) 58 | it('dashboard', () => { 59 | router.push('/dashboard') 60 | const len = wrapper.findAll('.el-breadcrumb__inner').length 61 | expect(len).toBe(1) 62 | }) 63 | it('normal route', () => { 64 | router.push('/menu/menu1') 65 | const len = wrapper.findAll('.el-breadcrumb__inner').length 66 | expect(len).toBe(2) 67 | }) 68 | it('nested route', () => { 69 | router.push('/menu/menu1/menu1-2/menu1-2-1') 70 | const len = wrapper.findAll('.el-breadcrumb__inner').length 71 | expect(len).toBe(4) 72 | }) 73 | it('no meta.title', () => { 74 | router.push('/menu/menu1/menu1-2/menu1-2-2') 75 | const len = wrapper.findAll('.el-breadcrumb__inner').length 76 | expect(len).toBe(3) 77 | }) 78 | // it('click link', () => { 79 | // router.push('/menu/menu1/menu1-2/menu1-2-2') 80 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 81 | // const second = breadcrumbArray.at(1) 82 | // console.log(breadcrumbArray) 83 | // const href = second.find('a').attributes().href 84 | // expect(href).toBe('#/menu/menu1') 85 | // }) 86 | // it('noRedirect', () => { 87 | // router.push('/menu/menu1/menu1-2/menu1-2-1') 88 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 89 | // const redirectBreadcrumb = breadcrumbArray.at(2) 90 | // expect(redirectBreadcrumb.contains('a')).toBe(false) 91 | // }) 92 | it('last breadcrumb', () => { 93 | router.push('/menu/menu1/menu1-2/menu1-2-1') 94 | const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 95 | const redirectBreadcrumb = breadcrumbArray.at(3) 96 | expect(redirectBreadcrumb.contains('a')).toBe(false) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /frontend/tests/unit/components/Hamburger.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import Hamburger from '@/components/Hamburger/index.vue' 3 | describe('Hamburger.vue', () => { 4 | it('toggle click', () => { 5 | const wrapper = shallowMount(Hamburger) 6 | const mockFn = jest.fn() 7 | wrapper.vm.$on('toggleClick', mockFn) 8 | wrapper.find('.hamburger').trigger('click') 9 | expect(mockFn).toBeCalled() 10 | }) 11 | it('prop isActive', () => { 12 | const wrapper = shallowMount(Hamburger) 13 | wrapper.setProps({ isActive: true }) 14 | expect(wrapper.contains('.is-active')).toBe(true) 15 | wrapper.setProps({ isActive: false }) 16 | expect(wrapper.contains('.is-active')).toBe(false) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /frontend/tests/unit/components/SvgIcon.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import SvgIcon from '@/components/SvgIcon/index.vue' 3 | describe('SvgIcon.vue', () => { 4 | it('iconClass', () => { 5 | const wrapper = shallowMount(SvgIcon, { 6 | propsData: { 7 | iconClass: 'test' 8 | } 9 | }) 10 | expect(wrapper.find('use').attributes().href).toBe('#icon-test') 11 | }) 12 | it('className', () => { 13 | const wrapper = shallowMount(SvgIcon, { 14 | propsData: { 15 | iconClass: 'test' 16 | } 17 | }) 18 | expect(wrapper.classes().length).toBe(1) 19 | wrapper.setProps({ className: 'test' }) 20 | expect(wrapper.classes().includes('test')).toBe(true) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /frontend/tests/unit/utils/formatTime.spec.js: -------------------------------------------------------------------------------- 1 | import { formatTime } from '@/utils/index.js' 2 | 3 | describe('Utils:formatTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | const retrofit = 5 * 1000 6 | 7 | it('ten digits timestamp', () => { 8 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') 9 | }) 10 | it('test now', () => { 11 | expect(formatTime(+new Date() - 1)).toBe('刚刚') 12 | }) 13 | it('less two minute', () => { 14 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') 15 | }) 16 | it('less two hour', () => { 17 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') 18 | }) 19 | it('less one day', () => { 20 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') 21 | }) 22 | it('more than one day', () => { 23 | expect(formatTime(d)).toBe('7月13日17时54分') 24 | }) 25 | it('format', () => { 26 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 27 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 28 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /frontend/tests/unit/utils/param2Obj.spec.js: -------------------------------------------------------------------------------- 1 | import { param2Obj } from '@/utils/index.js' 2 | describe('Utils:param2Obj', () => { 3 | const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95' 4 | 5 | it('param2Obj test', () => { 6 | expect(param2Obj(url)).toEqual({ 7 | name: 'bill', 8 | age: '29', 9 | sex: '1', 10 | field: window.btoa('test'), 11 | key: '测试' 12 | }) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /frontend/tests/unit/utils/parseTime.spec.js: -------------------------------------------------------------------------------- 1 | import { parseTime } from '@/utils/index.js' 2 | 3 | describe('Utils:parseTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | it('timestamp', () => { 6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01') 7 | }) 8 | it('timestamp string', () => { 9 | expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01') 10 | }) 11 | it('ten digits timestamp', () => { 12 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') 13 | }) 14 | it('new Date', () => { 15 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') 16 | }) 17 | it('format', () => { 18 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 19 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 20 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 21 | }) 22 | it('get the day of the week', () => { 23 | expect(parseTime(d, '{a}')).toBe('五') // 星期五 24 | }) 25 | it('get the day of the week', () => { 26 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 27 | }) 28 | it('empty argument', () => { 29 | expect(parseTime()).toBeNull() 30 | }) 31 | 32 | it('null', () => { 33 | expect(parseTime(null)).toBeNull() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /frontend/tests/unit/utils/validate.spec.js: -------------------------------------------------------------------------------- 1 | import { validUsername, isExternal } from '@/utils/validate.js' 2 | 3 | describe('Utils:validate', () => { 4 | it('validUsername', () => { 5 | expect(validUsername('admin')).toBe(true) 6 | expect(validUsername('editor')).toBe(true) 7 | expect(validUsername('xxxx')).toBe(false) 8 | }) 9 | it('isExternal', () => { 10 | expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) 11 | expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) 12 | expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false) 13 | expect(isExternal('/dashboard')).toBe(false) 14 | expect(isExternal('./dashboard')).toBe(false) 15 | expect(isExternal('dashboard')).toBe(false) 16 | }) 17 | }) 18 | --------------------------------------------------------------------------------