├── .gitignore ├── README.md ├── backend ├── .gitignore ├── Pipfile ├── Pipfile.lock ├── README.md ├── alembic.ini ├── alembic │ ├── README │ ├── env.py │ ├── script.py.mako │ └── versions │ │ └── 3a8b7c8118a7_init_commit.py ├── app │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── api_v1 │ │ │ ├── __init__.py │ │ │ ├── api.py │ │ │ ├── auth │ │ │ │ ├── __init__.py │ │ │ │ ├── crud │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── role.py │ │ │ │ │ └── user.py │ │ │ │ ├── schemas │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── role_schema.py │ │ │ │ │ ├── token_schema.py │ │ │ │ │ └── user_schema.py │ │ │ │ └── views.py │ │ │ ├── goods │ │ │ │ ├── __init__.py │ │ │ │ ├── crud │ │ │ │ │ ├── category.py │ │ │ │ │ └── goods.py │ │ │ │ ├── schemas │ │ │ │ │ ├── category_schema.py │ │ │ │ │ └── goods_schema.py │ │ │ │ └── views.py │ │ │ ├── user │ │ │ │ └── views.py │ │ │ └── utils │ │ │ │ ├── __init__.py │ │ │ │ └── views.py │ │ ├── common │ │ │ ├── __init__.py │ │ │ ├── curd_base.py │ │ │ ├── deps.py │ │ │ ├── logger.py │ │ │ └── schemas_base.py │ │ ├── db │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── base_class.py │ │ │ ├── init_db.py │ │ │ └── session.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── auth.py │ │ │ ├── express.py │ │ │ ├── goods.py │ │ │ └── orders.py │ │ └── utils │ │ │ ├── custom_exc.py │ │ │ └── response_code.py │ ├── assets │ │ ├── tmp15x208xz.jpeg │ │ ├── tmp6uubdhg3.png │ │ ├── tmp__t5cbki.jpeg │ │ ├── tmp_h7ij9kw.jpg │ │ ├── tmpobf4p7ke.jpeg │ │ ├── tmpp0gbn5qp.png │ │ └── tmpv_85251f.jpg │ ├── core │ │ ├── __init__.py │ │ ├── config │ │ │ ├── __init__.py │ │ │ ├── development_config.py │ │ │ └── production_config.py │ │ └── security.py │ ├── initial_data.py │ ├── main.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_aioredis.py │ │ └── test_websocket.py │ └── utils.py ├── region.sql └── requirements.txt └── frontend ├── .editorconfig ├── .env.development ├── .env.production ├── .env.staging ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── DEV-LOG.md ├── LICENSE ├── README-zh.md ├── README.md ├── babel.config.js ├── jest.config.js ├── jsconfig.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── api │ ├── goods.js │ ├── table.js │ ├── user.js │ └── utils.js ├── assets │ └── 404_images │ │ ├── 404.png │ │ └── 404_cloud.png ├── components │ ├── Breadcrumb │ │ └── index.vue │ ├── Hamburger │ │ └── index.vue │ ├── MarkdownEditor │ │ └── index.vue │ ├── SvgIcon │ │ └── index.vue │ ├── Tinymce │ │ ├── components │ │ │ └── EditorImage.vue │ │ ├── dynamicLoadScript.js │ │ ├── index.vue │ │ ├── plugins.js │ │ └── toolbar.js │ └── WangEditor │ │ └── index.vue ├── 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 │ │ ├── TagsView │ │ │ ├── ScrollPane.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 │ │ ├── tagsView.js │ │ └── user.js ├── styles │ ├── element-ui.scss │ ├── index.scss │ ├── mixin.scss │ ├── sidebar.scss │ ├── transition.scss │ └── variables.scss ├── utils │ ├── auth.js │ ├── common.js │ ├── get-page-title.js │ ├── index.js │ ├── request.js │ ├── textarea-line.js │ └── validate.js └── views │ ├── 404.vue │ ├── cart │ └── index.vue │ ├── dashboard │ ├── component │ │ ├── CardView.vue │ │ ├── HistogramChart.vue │ │ ├── LineChart.vue │ │ ├── PieChart.vue │ │ └── RadarChart.vue │ └── index.vue │ ├── goods │ ├── attribute │ │ ├── component │ │ │ ├── CategoryListView.vue │ │ │ └── CategoryView.vue │ │ └── index.vue │ └── display │ │ ├── component │ │ ├── GoodsListView.vue │ │ └── GoodsView.vue │ │ └── index.vue │ ├── login │ └── index.vue │ ├── orders │ └── index.vue │ ├── profile │ └── index.vue │ ├── shop │ ├── ad │ │ └── index.vue │ ├── display │ │ └── index.vue │ ├── freight │ │ └── index.vue │ ├── notice │ │ └── index.vue │ └── shipper │ │ └── index.vue │ └── user │ └── 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 | .idea 6 | # C extensions 7 | *.so 8 | # Distribution / packaging 9 | .Python 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | pip-wheel-metadata/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | node_modules 30 | 31 | /dist 32 | /assets 33 | 34 | # local env files 35 | .env.local 36 | .env.*.local 37 | 38 | # Log files 39 | npm-debug.log* 40 | yarn-debug.log* 41 | yarn-error.log* 42 | 43 | 44 | # Editor directories and files 45 | .idea 46 | .vscode 47 | *.suo 48 | *.ntvs* 49 | *.njsproj 50 | *.sln 51 | *.sw* 52 | 53 | # PyInstaller 54 | # Usually these files are written by a python script from a template 55 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 56 | *.manifest 57 | *.spec 58 | 59 | # Installer logs 60 | pip-log.txt 61 | pip-delete-this-directory.txt 62 | 63 | # Unit test / coverage reports 64 | htmlcov/ 65 | .tox/ 66 | .nox/ 67 | .coverage 68 | .coverage.* 69 | .cache 70 | nosetests.xml 71 | coverage.xml 72 | *.cover 73 | *.py,cover 74 | .hypothesis/ 75 | .pytest_cache/ 76 | 77 | # Translations 78 | *.mo 79 | *.pot 80 | 81 | # Django stuff: 82 | *.log 83 | local_settings.py 84 | db.sqlite3 85 | db.sqlite3-journal 86 | 87 | # Flask stuff: 88 | instance/ 89 | .webassets-cache 90 | 91 | # Scrapy stuff: 92 | .scrapy 93 | 94 | # Sphinx documentation 95 | docs/_build/ 96 | 97 | # PyBuilder 98 | target/ 99 | 100 | # Jupyter Notebook 101 | .ipynb_checkpoints 102 | 103 | # IPython 104 | profile_default/ 105 | ipython_config.py 106 | 107 | # pyenv 108 | .python-version 109 | 110 | # pipenv 111 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 112 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 113 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 114 | # install all needed dependencies. 115 | #Pipfile.lock 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | 155 | .DS_Store 156 | node_modules/ 157 | dist/ 158 | npm-debug.log* 159 | yarn-debug.log* 160 | yarn-error.log* 161 | package-lock.json 162 | tests/**/coverage/ 163 | 164 | # Editor directories and files 165 | .idea 166 | .vscode 167 | *.suo 168 | *.ntvs* 169 | *.njsproj 170 | *.sln -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mall项目后台管理 2 | 3 | > 前段时间学习Vue写了一个移动端项目 https://www.charmcode.cn/app/mall/home 然后教程到此就结束了, 我就总感觉少点什么,计划自己着手写一套后台管理。 4 | 5 | ## 相关项目 6 | 7 | - 移动端Mall项目源码(Vue构建): https://github.com/CoderCharm/Mall 8 | - 目前已部署上线: https://www.charmcode.cn/app/mall/home 9 | 10 | - 移动端Mall项目API服务源码(Python FastAPI构建): https://github.com/CoderCharm/MallAPI 11 | - 线上文档地址: 12 | 13 | 14 | ## 技术栈 15 | - 前端 Vue2 使用模版 https://github.com/PanJiaChen/vue-admin-template 16 | - ElementUI 17 | - 后端 Python3.7 FastAPI框架 18 | - [x] SQLAlchemy+MySql 19 | - [ ] Celery 队列,定时任务等 20 | - [ ] WebSocket 站内消息 21 | 22 | ## 个人博客地址 23 | > 后续会着手自定义 https://github.com/PanJiaChen/vue-admin-template 模版,然后记录一步步修改开发的过程。 24 | 25 | 可以持续关注个人博客Vue后台管理系列博客 26 | https://www.charmcode.cn/tags/Vue%E5%90%8E%E5%8F%B0%E7%AE%A1%E7%90%86/ 27 | 28 | 博客园地址会同步发布(个人站点备份) 29 | https://www.cnblogs.com/CharmCode/tag/Vue%E5%90%8E%E5%8F%B0%E7%AE%A1%E7%90%86/ 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .idea 6 | # C extensions 7 | *.so 8 | # Distribution / packaging 9 | .Python 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | pip-wheel-metadata/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | node_modules 30 | 31 | /dist 32 | /assets 33 | 34 | # local env files 35 | .env.local 36 | .env.*.local 37 | 38 | # Log files 39 | npm-debug.log* 40 | yarn-debug.log* 41 | yarn-error.log* 42 | 43 | 44 | # Editor directories and files 45 | .idea 46 | .vscode 47 | *.suo 48 | *.ntvs* 49 | *.njsproj 50 | *.sln 51 | *.sw* 52 | 53 | # PyInstaller 54 | # Usually these files are written by a python script from a template 55 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 56 | *.manifest 57 | *.spec 58 | 59 | # Installer logs 60 | pip-log.txt 61 | pip-delete-this-directory.txt 62 | 63 | # Unit test / coverage reports 64 | htmlcov/ 65 | .tox/ 66 | .nox/ 67 | .coverage 68 | .coverage.* 69 | .cache 70 | nosetests.xml 71 | coverage.xml 72 | *.cover 73 | *.py,cover 74 | .hypothesis/ 75 | .pytest_cache/ 76 | 77 | # Translations 78 | *.mo 79 | *.pot 80 | 81 | # Django stuff: 82 | *.log 83 | local_settings.py 84 | db.sqlite3 85 | db.sqlite3-journal 86 | 87 | # Flask stuff: 88 | instance/ 89 | .webassets-cache 90 | 91 | # Scrapy stuff: 92 | .scrapy 93 | 94 | # Sphinx documentation 95 | docs/_build/ 96 | 97 | # PyBuilder 98 | target/ 99 | 100 | # Jupyter Notebook 101 | .ipynb_checkpoints 102 | 103 | # IPython 104 | profile_default/ 105 | ipython_config.py 106 | 107 | # pyenv 108 | .python-version 109 | 110 | # pipenv 111 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 112 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 113 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 114 | # install all needed dependencies. 115 | #Pipfile.lock 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | -------------------------------------------------------------------------------- /backend/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | 10 | [requires] 11 | python_version = "3.7" 12 | -------------------------------------------------------------------------------- /backend/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "7f7606f08e0544d8d012ef4d097dabdd6df6843a28793eb6551245d4b2db4242" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": {}, 19 | "develop": {} 20 | } 21 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | ## 后台API服务 2 | 3 | ## 项目文件组织 4 | > 参考Django文件组织,FastAPI官方推荐项目生成,Flask工厂函数。 5 | 6 |
7 | 项目文件结构 8 | 9 | ``` 10 | /alembic // alembic 自动生成的迁移配置文件夹,迁移不正确时 产看其中的env.py文件 11 | alembic.ini // alembic 自动生成的迁移配置文件 12 | app 13 | |____core 14 | | |______init__.py 15 | | |____config // 配置文件 16 | | | |______init__.py // 根据虚拟环境导入不同配置 17 | | | |____development_config.py // 开发配置 18 | | | |____production_config.py // 生成配置 19 | | |____security.py // token password验证 20 | |____tests 21 | | |______init__.py 22 | |______init__.py 23 | |____api // API文件夹 24 | | |____api_v1 // 版本区分 25 | | | |____auth // auth模块 26 | | | | |______init__.py 27 | | | | |____schemas // 验证model文件夹 28 | | | | | |____user.py // user验证 29 | | | | | |______init__.py 30 | | | | |____curd // curd 文件夹 31 | | | | | |____user.py // user curd操作 32 | | | | | |______init__.py 33 | | | | |____views.py // 各模块视图函数 34 | | | |______init__.py 35 | | | |____api.py // 路由函数 36 | | |______init__.py 37 | | |____utils // 工具类文件夹 38 | | | |____custom_exc.py // 自定义异常 39 | | | |____response_code.py // 统一自定义响应状态 40 | | |____models // 项目models 文件(我没像django那样放到各模块下面,单独抽出来了) 41 | | | |______init__.py 42 | | | |____auth.py // 用户权限相关的 43 | | | |____goods.py // 商品相关 44 | | | |____shop.py // 店铺相关 45 | | |____extensions // 扩展文件夹 46 | | | |______init__.py 47 | | | |____logger.py // 扩展日志 loguru 简单配置 48 | | |____common // 通用文件夹 49 | | | |______init__.py 50 | | | |____deps.py // 通用依赖文件,如数据库操作对象,权限验证对象 51 | | | |____curd_base.py // curd_base对象 52 | | | |____model_base.py // model继承base对象(报错暂时不用) 53 | | |____logs 54 | | |____db // 数据库 55 | | | |______init__.py 56 | | | |____base_class.py 57 | | | |____session.py // 58 | | | |____base.py // 导出全部models 给alembic迁移用 59 | | | |____init_db.py // 初始化数据 60 | |____utils.py 61 | |____main.py 62 | |____initial_data.py 63 | 64 | 65 | 66 | ``` 67 | 68 |
69 | 70 | 71 | ## alembic 生成表 72 | 73 | #### 自动生成迁移文件 74 | > 删除/alembic/versions/文件夹下的文件, 然后在重新提交 75 | 76 | ```shell 77 | alembic revision --autogenerate -m "init commit" 78 | ``` 79 | 80 | #### 路径问题 81 | 82 | ```python 83 | import os,sys 84 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 85 | 86 | print(f"当前路径:{BASE_DIR}") 87 | # /Users/xxxx/MyFile/python_code/FastAdmin/backend 88 | 89 | sys.path.insert(0, BASE_DIR) 90 | # 如果还不行,那就简单直接点 直接写固定 91 | # sys.path.insert(0, "/你的路径/FastAdmin/backend") 92 | 93 | ``` 94 | 95 | 96 | #### 生成表 97 | > alembic upgrade head 98 | 99 | 100 | ## 初始化账号密码 101 | 102 | #### 生成初始化账号密码 103 | 104 | ```shell 105 | cd app 106 | python initial_data.py 107 | 108 | ``` 109 | 110 | 111 | ```shell 112 | username: wg_python@163.com 113 | password: admin12345 114 | ``` 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | ## 参考 123 | 124 | 125 | - [alembic导入app noqa](https://stackoverflow.com/questions/32032940/how-to-import-the-own-model-into-myproject-alembic-env-py):https://stackoverflow.com/questions/32032940/how-to-import-the-own-model-into-myproject-alembic-env-py 126 | - [alembic教程](https://alembic.sqlalchemy.org/en/latest/tutorial.html) https://alembic.sqlalchemy.org/en/latest/tutorial.html 127 | - [alembic迁移](https://alembic.sqlalchemy.org/en/latest/tutorial.html#running-our-first-migration): https://alembic.sqlalchemy.org/en/latest/tutorial.html#running-our-first-migration 128 | - [海风小店](https://raw.githubusercontent.com/iamdarcy/hioshop-server/master/hiolabsDB.sql) https://raw.githubusercontent.com/iamdarcy/hioshop-server/master/hiolabsDB.sql 129 | - [SqlAlchemy官网](https://docs.sqlalchemy.org/en/13/orm/query.html) https://docs.sqlalchemy.org/en/13/orm/query.html 130 | - [SqlAlchemy过滤操作](https://www.tutorialspoint.com/sqlalchemy/sqlalchemy_orm_filter_operators.htm) https://www.tutorialspoint.com/sqlalchemy/sqlalchemy_orm_filter_operators.htm 131 | - [SqlAlchemy包含操作](https://stackoverflow.com/questions/4926757/sqlalchemy-query-where-a-column-contains-a-substring) https://stackoverflow.com/questions/4926757/sqlalchemy-query-where-a-column-contains-a-substring 132 | -------------------------------------------------------------------------------- /backend/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # timezone to use when rendering the date 11 | # within the migration file as well as the filename. 12 | # string value is passed to dateutil.tz.gettz() 13 | # leave blank for localtime 14 | # timezone = 15 | 16 | # max length of characters to apply to the 17 | # "slug" field 18 | #truncate_slug_length = 40 19 | 20 | # set to 'true' to run the environment during 21 | # the 'revision' command, regardless of autogenerate 22 | # revision_environment = false 23 | 24 | # set to 'true' to allow .pyc and .pyo files without 25 | # a source .py file to be detected as revisions in the 26 | # versions/ directory 27 | # sourceless = false 28 | 29 | # version location specification; this defaults 30 | # to alembic/versions. When using multiple version 31 | # directories, initial revisions must be specified with --version-path 32 | # version_locations = %(here)s/bar %(here)s/bat alembic/versions 33 | 34 | # the output encoding used when revision files 35 | # are written from script.py.mako 36 | # output_encoding = utf-8 37 | 38 | # Logging configuration 39 | [loggers] 40 | keys = root,sqlalchemy,alembic 41 | 42 | [handlers] 43 | keys = console 44 | 45 | [formatters] 46 | keys = generic 47 | 48 | [logger_root] 49 | level = WARN 50 | handlers = console 51 | qualname = 52 | 53 | [logger_sqlalchemy] 54 | level = WARN 55 | handlers = 56 | qualname = sqlalchemy.engine 57 | 58 | [logger_alembic] 59 | level = INFO 60 | handlers = 61 | qualname = alembic 62 | 63 | [handler_console] 64 | class = StreamHandler 65 | args = (sys.stderr,) 66 | level = NOTSET 67 | formatter = generic 68 | 69 | [formatter_generic] 70 | format = %(levelname)-5.5s [%(name)s] %(message)s 71 | datefmt = %H:%M:%S 72 | -------------------------------------------------------------------------------- /backend/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /backend/alembic/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | from alembic import context 4 | from sqlalchemy import engine_from_config, pool 5 | from logging.config import fileConfig 6 | 7 | # this is the Alembic Config object, which provides 8 | # access to the values within the .ini file in use. 9 | 10 | config = context.config 11 | 12 | # Interpret the config file for Python logging. 13 | # This line sets up loggers basically. 14 | fileConfig(config.config_file_name) 15 | 16 | # add your model's MetaData object here 17 | # for 'autogenerate' support 18 | # from myapp import mymodel 19 | # target_metadata = mymodel.Base.metadata 20 | # target_metadata = None 21 | 22 | import os 23 | import sys 24 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 25 | 26 | # print(f"当前路径:{BASE_DIR}") 27 | # /Users/xxxx/python_code/FastAdmin/backend 28 | 29 | sys.path.insert(0, BASE_DIR) 30 | 31 | from api.db.base import Base # noqa 32 | 33 | 34 | target_metadata = Base.metadata 35 | 36 | 37 | # other values from the config, defined by the needs of env.py, 38 | # can be acquired: 39 | # my_important_option = config.get_main_option("my_important_option") 40 | # ... etc. 41 | 42 | 43 | def get_url(): 44 | from core.config import settings 45 | return settings.SQLALCHEMY_DATABASE_URL 46 | 47 | 48 | def run_migrations_offline(): 49 | """Run migrations in 'offline' mode. 50 | 51 | This configures the context with just a URL 52 | and not an Engine, though an Engine is acceptable 53 | here as well. By skipping the Engine creation 54 | we don't even need a DBAPI to be available. 55 | 56 | Calls to context.execute() here emit the given string to the 57 | script output. 58 | 59 | """ 60 | url = get_url() 61 | context.configure( 62 | url=url, target_metadata=target_metadata, literal_binds=True, compare_type=True 63 | ) 64 | 65 | with context.begin_transaction(): 66 | context.run_migrations() 67 | 68 | 69 | def run_migrations_online(): 70 | """Run migrations in 'online' mode. 71 | 72 | In this scenario we need to create an Engine 73 | and associate a connection with the context. 74 | 75 | """ 76 | configuration = config.get_section(config.config_ini_section) 77 | configuration["sqlalchemy.url"] = get_url() 78 | connectable = engine_from_config( 79 | configuration, prefix="sqlalchemy.", poolclass=pool.NullPool, 80 | ) 81 | 82 | with connectable.connect() as connection: 83 | context.configure( 84 | connection=connection, target_metadata=target_metadata, compare_type=True 85 | ) 86 | 87 | with context.begin_transaction(): 88 | context.run_migrations() 89 | 90 | 91 | if context.is_offline_mode(): 92 | run_migrations_offline() 93 | else: 94 | run_migrations_online() 95 | -------------------------------------------------------------------------------- /backend/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /backend/app/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/6/22 15:13 4 | # @Author : CoderCharm 5 | # @File : __init__.py.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | """ 11 | 12 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/17 15:19 4 | # @Author : CoderCharm 5 | # @File : __init__.py.py 6 | # @Software: PyCharm 7 | # @Github : github/CoderCharm 8 | # @Email : wg_python@163.com 9 | # @Desc : 10 | """ 11 | 12 | """ 13 | 14 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from api.api_v1 import auth, goods, utils 4 | 5 | 6 | api_v1_router = APIRouter() 7 | api_v1_router.include_router(auth.router, prefix="/admin/auth", tags=["用户"]) 8 | api_v1_router.include_router(goods.router, prefix="/admin/goods", tags=["商品"]) 9 | api_v1_router.include_router(utils.router, prefix="/admin/utils", tags=["工具类"]) 10 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/auth/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/7 15:18 4 | # @Author : CoderCharm 5 | # @File : __init__.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | """ 11 | 12 | from .views import router 13 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/auth/crud/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/7 16:47 4 | # @Author : CoderCharm 5 | # @File : __init__.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 这个模块的curd操作都写在这个文件夹内 10 | 11 | """ 12 | 13 | from .user import curd_user 14 | from .role import curd_role 15 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/auth/crud/role.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/11 11:00 4 | # @Author : CoderCharm 5 | # @File : role.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 角色表crud操作 10 | """ 11 | 12 | from typing import Optional 13 | from sqlalchemy.orm import Session 14 | 15 | from api.common.curd_base import CRUDBase 16 | from api.models.auth import AdminRole 17 | from ..schemas import role_schema 18 | 19 | 20 | class CRUDRole(CRUDBase[AdminRole, role_schema.RoleCreate, role_schema.RoleUpdate]): 21 | 22 | @staticmethod 23 | def query_role(db: Session, *, role_id: int) -> Optional[AdminRole]: 24 | """ 25 | 此role_id是否存在 26 | :param db: 27 | :param role_id: 28 | :return: 29 | """ 30 | return db.query(AdminRole).filter(AdminRole.role_id == role_id).first() 31 | 32 | def create(self, db: Session, *, obj_in: role_schema.RoleCreate) -> AdminRole: 33 | db_obj = AdminRole( 34 | role_id=obj_in.role_id, 35 | role_name=obj_in.role_name, 36 | permission_id=obj_in.permission_id, 37 | re_mark=obj_in.re_mark 38 | ) 39 | db.add(db_obj) 40 | db.commit() 41 | db.refresh(db_obj) 42 | return db_obj 43 | 44 | 45 | curd_role = CRUDRole(AdminRole) 46 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/auth/crud/user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/7 16:38 4 | # @Author : CoderCharm 5 | # @File : curd_user.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | """ 11 | 12 | from typing import Optional 13 | 14 | from sqlalchemy.orm import Session 15 | 16 | from core.security import get_password_hash, verify_password 17 | from api.common.curd_base import CRUDBase 18 | from api.models.auth import AdminUser 19 | from ..schemas import user_schema 20 | 21 | 22 | class CRUDUser(CRUDBase[AdminUser, user_schema.UserCreate, user_schema.UserUpdate]): 23 | 24 | @staticmethod 25 | def get_by_email(db: Session, *, email: str) -> Optional[AdminUser]: 26 | """ 27 | 通过email获取用户 28 | 参数里面的* 表示 后面调用的时候 要用指定参数的方法调用 29 | 正确调用方式 30 | curd_user.get_by_email(db, email="xxx") 31 | 错误调用方式 32 | curd_user.get_by_email(db, "xxx") 33 | :param db: 34 | :param email: 35 | :return: 36 | """ 37 | return db.query(AdminUser).filter(AdminUser.email == email).first() 38 | 39 | def create(self, db: Session, *, obj_in: user_schema.UserCreate) -> AdminUser: 40 | db_obj = AdminUser( 41 | nickname=obj_in.nickname, 42 | email=obj_in.email, 43 | hashed_password=get_password_hash(obj_in.password), 44 | avatar=obj_in.avatar, 45 | role_id=obj_in.role_id, 46 | is_active=obj_in.is_active 47 | ) 48 | db.add(db_obj) 49 | db.commit() 50 | db.refresh(db_obj) 51 | return db_obj 52 | 53 | def authenticate(self, db: Session, *, email: str, password: str) -> Optional[AdminUser]: 54 | user = self.get_by_email(db, email=email) 55 | if not user: 56 | return None 57 | if not verify_password(password, user.hashed_password): 58 | return None 59 | return user 60 | 61 | @staticmethod 62 | def is_active(user: AdminUser) -> bool: 63 | return user.is_active == 1 64 | 65 | 66 | curd_user = CRUDUser(AdminUser) 67 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/auth/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/7 14:12 4 | # @Author : CoderCharm 5 | # @File : __init__.py.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 验证 model 字段格式,验证错误的会自动抛出 10 | """ 11 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/auth/schemas/role_schema.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/11 10:57 4 | # @Author : CoderCharm 5 | # @File : role_schema.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 权限表 10 | """ 11 | from typing import Optional 12 | 13 | from pydantic import BaseModel 14 | 15 | 16 | class RoleCreate(BaseModel): 17 | """ 18 | 创建角色字段 19 | """ 20 | role_id: int 21 | role_name: str 22 | permission_id: int 23 | re_mark: Optional[str] = None 24 | 25 | 26 | class RoleUpdate(BaseModel): 27 | """ 28 | 角色更新字段 29 | """ 30 | role_name: Optional[str] = None 31 | re_mark: Optional[str] = None 32 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/auth/schemas/token_schema.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/10 17:01 4 | # @Author : CoderCharm 5 | # @File : token_schema.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | """ 11 | 12 | from typing import Optional 13 | from pydantic import BaseModel 14 | 15 | from api.common.schemas_base import RespBase 16 | 17 | 18 | class Token(BaseModel): 19 | token: str 20 | 21 | 22 | class TokenPayload(BaseModel): 23 | sub: Optional[int] = None 24 | 25 | 26 | class RespToken(RespBase): 27 | # 认证响应模型 28 | data: Token 29 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/auth/schemas/user_schema.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/7 16:23 4 | # @Author : CoderCharm 5 | # @File : user_schema.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | """ 11 | 12 | from typing import Optional, Union 13 | 14 | from pydantic import BaseModel, EmailStr, AnyHttpUrl 15 | 16 | 17 | from api.common.schemas_base import RespBase 18 | 19 | 20 | # Shared properties 21 | class UserBase(BaseModel): 22 | email: Optional[EmailStr] = None 23 | phone: int = None 24 | is_active: Optional[bool] = True 25 | 26 | 27 | class UserAuth(BaseModel): 28 | password: str 29 | 30 | 31 | # 邮箱登录认证 验证数据字段都叫username 32 | class UserEmailAuth(UserAuth): 33 | username: EmailStr 34 | 35 | 36 | # 手机号登录认证 验证数据字段都叫username 37 | class UserPhoneAuth(UserAuth): 38 | username: int 39 | 40 | 41 | # 创建账号需要验证的条件 42 | class UserCreate(UserBase): 43 | nickname: str 44 | email: EmailStr 45 | password: str 46 | role_id: int 47 | avatar: AnyHttpUrl 48 | 49 | 50 | # Properties to receive via API on update 51 | class UserUpdate(UserBase): 52 | password: Optional[str] = None 53 | 54 | 55 | class UserInDBBase(UserBase): 56 | id: Optional[int] = None 57 | 58 | class Config: 59 | orm_mode = True 60 | 61 | 62 | class UserInDB(UserInDBBase): 63 | hashed_password: str 64 | 65 | 66 | # 返回的用户信息 67 | class UserInfo(BaseModel): 68 | role_id: int 69 | role: str 70 | nickname: str 71 | avatar: AnyHttpUrl 72 | 73 | 74 | class RespUserInfo(RespBase): 75 | # 响应用户信息 76 | data: UserInfo 77 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/auth/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/7 14:11 4 | # @Author : CoderCharm 5 | # @File : views.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | """ 11 | from datetime import timedelta 12 | from typing import Any, Union 13 | 14 | from sqlalchemy.orm import Session 15 | from fastapi import APIRouter, Depends 16 | from core import security 17 | 18 | from api.common import deps 19 | from api.utils import response_code 20 | from api.common.logger import logger 21 | 22 | from api.models import auth 23 | from core.config import settings 24 | 25 | from .schemas import user_schema, token_schema 26 | 27 | from .crud import curd_user, curd_role 28 | 29 | router = APIRouter() 30 | 31 | 32 | @router.post("/login/access-token", summary="用户登录认证", response_model=token_schema.RespToken) 33 | async def login_access_token( 34 | *, 35 | db: Session = Depends(deps.get_db), 36 | user_info: user_schema.UserEmailAuth, 37 | ) -> Any: 38 | """ 39 | 用户登录 40 | :param db: 41 | :param user_info: 42 | :return: 43 | """ 44 | 45 | # 验证用户 46 | user = curd_user.authenticate(db, email=user_info.username, password=user_info.password) 47 | if not user: 48 | logger.info(f"用户邮箱认证错误: email{user_info.username} password:{user_info.password}") 49 | return response_code.resp_500(message="用户名或者密码错误") 50 | elif not curd_user.is_active(user): 51 | return response_code.resp_500(message="用户邮箱未激活") 52 | 53 | access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) 54 | 55 | # 登录token 只存放了user.id 56 | return response_code.resp_200(data={ 57 | "token": security.create_access_token(user.id, expires_delta=access_token_expires), 58 | }) 59 | 60 | 61 | @router.get("/user/info", summary="获取用户信息", response_model=user_schema.RespUserInfo) 62 | async def get_user_info( 63 | *, 64 | db: Session = Depends(deps.get_db), 65 | current_user: auth.AdminUser = Depends(deps.get_current_user) 66 | ) -> Any: 67 | """ 68 | 获取用户信息 69 | :param db: 70 | :param current_user: 71 | :return: 72 | """ 73 | role_info = curd_role.query_role(db, role_id=current_user.role_id) 74 | 75 | return response_code.resp_200(data={ 76 | "role_id": current_user.role_id, 77 | "role": role_info.role_name, 78 | "nickname": current_user.nickname, 79 | "avatar": current_user.avatar 80 | }) 81 | 82 | 83 | @router.post("/user/logout", summary="用户退出", response_model=user_schema.RespBase) 84 | async def user_logout(token_data: Union[str, Any] = Depends(deps.check_jwt_token),): 85 | """ 86 | 用户退出 87 | :param token_data: 88 | :return: 89 | """ 90 | logger.info(f"用户退出->用户id:{token_data.sub}") 91 | return response_code.resp_200(message="logout success") 92 | 93 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/goods/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/7 17:27 4 | # @Author : CoderCharm 5 | # @File : __init__.py.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | """ 11 | 12 | from .views import router 13 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/goods/crud/goods.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/17 10:21 4 | # @Author : CoderCharm 5 | # @File : goods.py 6 | # @Software: PyCharm 7 | # @Github : github/CoderCharm 8 | # @Email : wg_python@163.com 9 | # @Desc : 10 | """ 11 | 12 | """ 13 | 14 | 15 | # from sqlalchemy.ext.serializer import dumps 16 | 17 | 18 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/goods/schemas/category_schema.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/25 09:00 4 | # @Author : CoderCharm 5 | # @File : category_schema.py 6 | # @Software: PyCharm 7 | # @Github : github/CoderCharm 8 | # @Email : wg_python@163.com 9 | # @Desc : 10 | """ 11 | 12 | """ 13 | from typing import Union, List 14 | from pydantic import BaseModel, AnyHttpUrl, conint 15 | 16 | 17 | class CategoryCreate(BaseModel): 18 | """ 19 | 新增分类 20 | """ 21 | name: str 22 | front_desc: str 23 | sort_order: int 24 | icon_url: AnyHttpUrl 25 | enabled: int = 1 26 | 27 | 28 | class CategoryUpdate(CategoryCreate): 29 | """ 30 | 更新分类 31 | """ 32 | id: Union[int, str] 33 | 34 | 35 | class CategoryDel(BaseModel): 36 | """ 37 | 逻辑删除 38 | """ 39 | ids: List[int] 40 | 41 | 42 | class CategoryEnable(CategoryDel): 43 | """ 44 | 批量操作开启开关 继承 ids:List[int] 45 | """ 46 | enabled: int 47 | 48 | 49 | class CategorySearch(BaseModel): 50 | """ 51 | 搜索分类 52 | """ 53 | key_world: str 54 | page: conint(ge=1) = 1 55 | page_size: conint(le=50) = 10 56 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/goods/schemas/goods_schema.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/17 14:34 4 | # @Author : CoderCharm 5 | # @File : goods_schema.py 6 | # @Software: PyCharm 7 | # @Github : github/CoderCharm 8 | # @Email : wg_python@163.com 9 | # @Desc : 10 | """ 11 | 验证 goods 的模型 12 | """ 13 | from typing import Union, List 14 | from pydantic import BaseModel, AnyHttpUrl, conint 15 | 16 | 17 | class GoodsBase(BaseModel): 18 | pass 19 | 20 | 21 | class GoodsCreate(GoodsBase): 22 | """ 23 | 新增商品 24 | """ 25 | goods_name: str 26 | goods_brief: str # 商品简介 27 | category_id: int # 分类 28 | is_on_sale: int = 1 # 是在售卖 29 | pic_banner: str = None # 封面 没有取轮播图第一张 30 | list_pic_url: str # 轮播图 31 | goods_unit: str # 商品单位 32 | sell_volume: int # 销量 33 | goods_desc_type: int = 1 # 详情内容1=富文本 2=MarkDown 34 | goods_desc: str # 商品详情 35 | specification_id: int # 商品规格 重量 长度 颜色 尺码 36 | specification_name: str # 商品规格名称 如 37 | specification_unit: str # 规格单位 如斤 克 38 | specification_memo: str # 补充说明 如每袋5个装 39 | goods_number: float # 库存数量 40 | retail_price: float # 零售价 41 | min_retail_price: float # 最低零售价 42 | is_new: int = 0 # 是否新品 默认0 43 | freight_template_id: int # 运费模版id 44 | sort_order: int = 1 # 排序 45 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/goods/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/13 13:37 4 | # @Author : CoderCharm 5 | # @File : views.py 6 | # @Software: PyCharm 7 | # @Github : github/CoderCharm 8 | # @Email : wg_python@163.com 9 | # @Desc : 10 | """ 11 | 12 | """ 13 | from typing import Union, Any 14 | from fastapi import APIRouter, Depends, Query 15 | from sqlalchemy.orm import Session 16 | 17 | from api.common import deps 18 | from api.common.logger import logger 19 | from api.utils import response_code 20 | 21 | from .schemas import goods_schema, category_schema 22 | from .crud.category import curd_category 23 | 24 | router = APIRouter() 25 | 26 | 27 | @router.post("/add/goods", summary="添加商品") 28 | async def goods_add( 29 | goods_info: goods_schema.GoodsCreate, 30 | db: Session = Depends(deps.get_db), 31 | token_data: Union[str, Any] = Depends(deps.check_jwt_token), 32 | ): 33 | logger.info(goods_info) 34 | return response_code.resp_200(data="ok") 35 | 36 | 37 | @router.get("/query/category/list", summary="查询分类列表") 38 | async def query_category_list( 39 | db: Session = Depends(deps.get_db), 40 | token_data: Union[str, Any] = Depends(deps.check_jwt_token), 41 | page: int = Query(1, ge=1, title="当前页"), 42 | page_size: int = Query(10, le=50, title="页码长度") 43 | ): 44 | logger.info(f"查询分类列表->用户id:{token_data.sub}当前页{page}长度{page_size}") 45 | response_result = curd_category.query_all(db, page=page, page_size=page_size) 46 | return response_code.resp_200(data=response_result) 47 | 48 | 49 | @router.get("/query/category", summary="查询分类") 50 | async def query_category( 51 | db: Session = Depends(deps.get_db), 52 | token_data: Union[str, Any] = Depends(deps.check_jwt_token), 53 | cate_id: int = Query(..., title="查询当前分类"), 54 | ): 55 | logger.info(f"查询分类->用户id:{token_data.sub}分类:{cate_id}") 56 | response_result = curd_category.query_obj(db, cate_id=cate_id) 57 | return response_code.resp_200(data=response_result) 58 | 59 | 60 | @router.post("/add/category", summary="添加分类") 61 | async def add_category( 62 | category_info: category_schema.CategoryCreate, 63 | db: Session = Depends(deps.get_db), 64 | token_data: Union[str, Any] = Depends(deps.check_jwt_token), 65 | ): 66 | logger.info(f"添加分类->用户id:{token_data.sub}分类名:{category_info.name}") 67 | curd_category.create(db=db, obj_in=category_info) 68 | return response_code.resp_200(message="分类添加成功") 69 | 70 | 71 | @router.post("/modify/category", summary="修改分类") 72 | async def modify_category( 73 | cate_info: category_schema.CategoryUpdate, 74 | db: Session = Depends(deps.get_db), 75 | token_data: Union[str, Any] = Depends(deps.check_jwt_token), 76 | ): 77 | logger.info(f"修改分类->用户id:{token_data.sub}分类id:{cate_info.id}") 78 | curd_category.update_cate(db=db, obj_in=cate_info) 79 | 80 | return response_code.resp_200(message="修改成功") 81 | 82 | 83 | @router.post("/del/category", summary="删除分类") 84 | async def modify_category( 85 | cate_ids: category_schema.CategoryDel, 86 | db: Session = Depends(deps.get_db), 87 | token_data: Union[str, Any] = Depends(deps.check_jwt_token), 88 | ): 89 | logger.info(f"修改分类->用户id:{token_data.sub}分类id:{cate_ids.ids}") 90 | for cate_id in cate_ids.ids: 91 | curd_category.remove(db, id=cate_id) 92 | return response_code.resp_200(message="删除成功") 93 | 94 | 95 | @router.post("/enabled/category", summary="分类开启或关闭") 96 | async def enabled_category( 97 | cate_info: category_schema.CategoryEnable, 98 | db: Session = Depends(deps.get_db), 99 | token_data: Union[str, Any] = Depends(deps.check_jwt_token), 100 | ): 101 | logger.info(f"开启分类操作->用户id:{token_data.sub}分类id:{cate_info.ids}操作:{cate_info.enabled}") 102 | for cate_id in cate_info.ids: 103 | curd_category.update_enabled(db, id=cate_id, enabled=cate_info.enabled) 104 | return response_code.resp_200(message="操作成功") 105 | 106 | 107 | @router.post("/search/category", summary="搜索分类") 108 | async def search_category( 109 | cate_info: category_schema.CategorySearch, 110 | db: Session = Depends(deps.get_db), 111 | token_data: Union[str, Any] = Depends(deps.check_jwt_token), 112 | ): 113 | logger.info(f"搜索分类操作->用户id:{token_data.sub}搜索{cate_info.key_world}:{cate_info.key_world}" 114 | f"页码:{cate_info.page}长度{cate_info.page_size}") 115 | response_result = curd_category.search_field(db, cate_info=cate_info) 116 | return response_code.resp_200(data=response_result) 117 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/user/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/15 13:51 4 | # @Author : CoderCharm 5 | # @File : views.py 6 | # @Software: PyCharm 7 | # @Github : github/CoderCharm 8 | # @Email : wg_python@163.com 9 | # @Desc : 10 | """ 11 | 12 | 用户视图 13 | 14 | """ 15 | 16 | from fastapi import APIRouter, Depends 17 | 18 | router = APIRouter() 19 | 20 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/utils/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .views import router 3 | 4 | -------------------------------------------------------------------------------- /backend/app/api/api_v1/utils/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/17 17:30 4 | # @Author : CoderCharm 5 | # @File : views.py 6 | # @Software: PyCharm 7 | # @Github : github/CoderCharm 8 | # @Email : wg_python@163.com 9 | # @Desc : 10 | """ 11 | 12 | https://fastapi.tiangolo.com/tutorial/request-forms-and-files/ 13 | 14 | 上传文件一定要安装这个库(粗心没注意第一行,卡了俩小时) 15 | pip install python-multipart 16 | 17 | https://github.com/tiangolo/fastapi/issues/426#issuecomment-542828790 18 | 19 | """ 20 | import os 21 | import shutil 22 | from pathlib import Path 23 | from typing import Union, Any 24 | from tempfile import NamedTemporaryFile 25 | from fastapi import APIRouter, Depends, File, UploadFile 26 | 27 | from api.common import deps 28 | from api.common.logger import logger 29 | from core.config import settings 30 | from api.utils import response_code 31 | 32 | router = APIRouter() 33 | 34 | 35 | @router.post("/upload/file", summary="上传图片") 36 | async def upload_image( 37 | token_data: Union[str, Any] = Depends(deps.check_jwt_token), 38 | file: UploadFile = File(...) 39 | ): 40 | logger.info(f"用户{token_data.sub}->上传文件:{file.filename}") 41 | 42 | # 本地存储临时方案,一般生产都是使用第三方云存储OSS(如七牛云, 阿里云) 43 | save_dir = f"{settings.BASE_DIR}/assets" 44 | if not os.path.exists(save_dir): 45 | os.mkdir(save_dir) 46 | 47 | try: 48 | suffix = Path(file.filename).suffix 49 | 50 | with NamedTemporaryFile(delete=False, suffix=suffix, dir=save_dir) as tmp: 51 | shutil.copyfileobj(file.file, tmp) 52 | tmp_file_name = Path(tmp.name).name 53 | finally: 54 | file.file.close() 55 | 56 | return response_code.resp_200(data={"image": f"http://127.0.0.1:8010/assets/{tmp_file_name}"}) 57 | -------------------------------------------------------------------------------- /backend/app/api/common/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/7 14:07 4 | # @Author : CoderCharm 5 | # @File : __init__.py.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | 通用文件 11 | 12 | """ 13 | 14 | -------------------------------------------------------------------------------- /backend/app/api/common/curd_base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/7 14:08 4 | # @Author : CoderCharm 5 | # @File : curd_base.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | """ 11 | 12 | from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union 13 | 14 | from fastapi.encoders import jsonable_encoder 15 | from pydantic import BaseModel 16 | from sqlalchemy.orm import Session 17 | 18 | from api.db.base_class import Base 19 | 20 | ModelType = TypeVar("ModelType", bound=Base) 21 | CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) 22 | UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) 23 | 24 | 25 | class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): 26 | def __init__(self, model: Type[ModelType]): 27 | """ 28 | CRUD object with default methods to Create, Read, Update, Delete (CRUD). 29 | 30 | **Parameters** 31 | 32 | * `model`: A SQLAlchemy model class 33 | * `schema`: A Pydantic model (schema) class 34 | """ 35 | self.model = model 36 | 37 | def get(self, db: Session, id: Any) -> Optional[ModelType]: 38 | return db.query(self.model).filter(self.model.id == id, self.model.is_delete == 0).first() 39 | 40 | def get_multi( 41 | self, db: Session, *, page: int = 0, page_size: int = 100 42 | ) -> List[ModelType]: 43 | temp_page = (page - 1) * page_size 44 | return db.query(self.model).filter(self.model.is_delete == 0).offset(temp_page).limit(page_size).all() 45 | 46 | def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType: 47 | obj_in_data = jsonable_encoder(obj_in) 48 | db_obj = self.model(**obj_in_data) # type: ignore 49 | db.add(db_obj) 50 | db.commit() 51 | db.refresh(db_obj) 52 | return db_obj 53 | 54 | def update( 55 | self, 56 | db: Session, 57 | *, 58 | db_obj: ModelType, 59 | obj_in: Union[UpdateSchemaType, Dict[str, Any]] 60 | ) -> ModelType: 61 | obj_data = jsonable_encoder(db_obj) 62 | if isinstance(obj_in, dict): 63 | update_data = obj_in 64 | else: 65 | update_data = obj_in.dict(exclude_unset=True) 66 | for field in obj_data: 67 | if field in update_data: 68 | setattr(db_obj, field, update_data[field]) 69 | db.add(db_obj) 70 | db.commit() 71 | db.refresh(db_obj) 72 | return db_obj 73 | 74 | def remove(self, db: Session, *, id: int) -> ModelType: 75 | obj = db.query(self.model).filter(self.model.id == id).update({self.model.is_delete: 1}) 76 | # db.delete(obj) 77 | db.commit() 78 | return obj 79 | -------------------------------------------------------------------------------- /backend/app/api/common/deps.py: -------------------------------------------------------------------------------- 1 | from typing import Generator, Optional, Union, Any 2 | 3 | from fastapi import Depends, Header 4 | # from fastapi.security import OAuth2PasswordBearer 5 | from jose import jwt 6 | from pydantic import ValidationError 7 | from sqlalchemy.orm import Session 8 | 9 | # from api import models, crud, schemas 10 | from core import security 11 | from core.config import settings 12 | from api.db.session import SessionLocal 13 | from api.models.auth import AdminUser 14 | from api.api_v1.auth.schemas import token_schema 15 | from api.api_v1.auth.crud import curd_user 16 | 17 | from api.utils import custom_exc 18 | 19 | 20 | def get_db() -> Generator: 21 | try: 22 | db = SessionLocal() 23 | yield db 24 | finally: 25 | db.close() 26 | 27 | 28 | def check_jwt_token( 29 | token: Optional[str] = Header(None) 30 | ) -> Union[str, Any]: 31 | """ 32 | 只解析验证token 33 | :param token: 34 | :return: 35 | """ 36 | 37 | try: 38 | payload = jwt.decode( 39 | token, 40 | settings.SECRET_KEY, algorithms=settings.JWT_ALGORITHM 41 | ) 42 | return token_schema.TokenPayload(**payload) 43 | except (jwt.JWTError, jwt.ExpiredSignatureError, ValidationError): 44 | raise custom_exc.UserTokenError(err_desc="access token fail") 45 | 46 | 47 | def get_current_user( 48 | db: Session = Depends(get_db), token: Optional[str] = Header(None) 49 | ) -> AdminUser: 50 | """ 51 | 根据header中token 获取当前用户 52 | :param db: 53 | :param token: 54 | :return: 55 | """ 56 | if not token: 57 | raise custom_exc.UserTokenError(err_desc='headers not found token') 58 | 59 | token_data = check_jwt_token(token) 60 | user = curd_user.get(db, id=token_data.sub) 61 | if not user: 62 | raise custom_exc.UserNotFound(err_desc="user not found") 63 | return user 64 | -------------------------------------------------------------------------------- /backend/app/api/common/logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/6/17 15:50 4 | # @Author : CoderCharm 5 | # @File : logger.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | 日志文件配置 11 | 12 | # 本来是想 像flask那样把日志对象挂载到app对象上,作者建议直接使用全局对象 13 | https://github.com/tiangolo/fastapi/issues/81#issuecomment-473677039 14 | pip install loguru 15 | 16 | """ 17 | 18 | import os 19 | import time 20 | from loguru import logger 21 | from core.config import settings 22 | 23 | # 定位到log日志文件 24 | log_path = os.path.join(settings.BASE_DIR, 'logs') 25 | 26 | if not os.path.exists(log_path): 27 | os.mkdir(log_path) 28 | 29 | log_path_error = os.path.join(log_path, f'{time.strftime("%Y-%m-%d")}_error.log') 30 | 31 | # 日志简单配置 32 | # 具体其他配置 可自行参考 https://github.com/Delgan/loguru 33 | logger.add(log_path_error, rotation="12:00", retention="5 days", enqueue=True) 34 | 35 | 36 | # 只导出 logger 37 | __all__ = ["logger"] 38 | -------------------------------------------------------------------------------- /backend/app/api/common/schemas_base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/24 17:04 4 | # @Author : CoderCharm 5 | # @File : schemas_base.py 6 | # @Software: PyCharm 7 | # @Github : github/CoderCharm 8 | # @Email : wg_python@163.com 9 | # @Desc : 10 | """ 11 | 12 | """ 13 | from typing import Union 14 | from pydantic import BaseModel 15 | 16 | 17 | class RespBase(BaseModel): 18 | code: int 19 | message: str 20 | data: Union[dict, list, str] 21 | -------------------------------------------------------------------------------- /backend/app/api/db/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/6/22 15:15 4 | # @Author : CoderCharm 5 | # @File : __init__.py.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | """ 11 | 12 | -------------------------------------------------------------------------------- /backend/app/api/db/base.py: -------------------------------------------------------------------------------- 1 | # Import all the models, so that Base has them before being 2 | # imported by Alembic 3 | 4 | # imported by Alembic # 方便在Alembic导入,迁移用 5 | 6 | from api.db.base_class import Base # noqa 7 | 8 | from api.models.auth import AdminUser, AdminRole, MallUser, MallAddress, MallSearchHistory, MallSiteNotice 9 | 10 | from api.models.express import MallFreightTemplate, MallRegion, MallShipper 11 | 12 | from api.models.goods import MallGoods, MallBanner, MallGoodsGallery, MallGoodsKeywords, MallGoodsSpecification 13 | 14 | from api.models.orders import MallOrder, MallCart, MallOrderExpress, MallOrderGoods -------------------------------------------------------------------------------- /backend/app/api/db/base_class.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from datetime import datetime 3 | 4 | from sqlalchemy import Column, Integer, DateTime 5 | 6 | from sqlalchemy.sql import func 7 | from sqlalchemy.ext.declarative import as_declarative, declared_attr 8 | 9 | 10 | @as_declarative() 11 | class Base: 12 | # 通用的字段 13 | id = Column(Integer, primary_key=True, index=True, autoincrement=True) 14 | create_time = Column(DateTime, default=datetime.now, server_default=func.now(), comment="创建时间") 15 | update_time = Column(DateTime, default=datetime.now, onupdate=datetime.now, server_default=func.now(), 16 | server_onupdate=func.now(), comment="更新时间") 17 | is_delete = Column(Integer, default=0, comment="逻辑删除:0=未删除,1=删除", server_default='0') 18 | __name__: str 19 | 20 | # Generate __tablename__ automatically 21 | @declared_attr 22 | def __tablename__(cls) -> str: 23 | import re 24 | # 如果没有指定__tablename__ 则默认使用model类名转换表名字 25 | name_list = re.findall(r"[A-Z][a-z\d]*", cls.__name__) 26 | # 表名格式替换成 下划线_格式 如 MallUser 替换成 mall_user 27 | return "_".join(name_list).lower() 28 | 29 | 30 | def gen_uuid() -> str: 31 | # 生成uuid 32 | # https://stackoverflow.com/questions/183042/how-can-i-use-uuids-in-sqlalchemy?rq=1 33 | return uuid.uuid4().hex 34 | -------------------------------------------------------------------------------- /backend/app/api/db/init_db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from api.api_v1.auth.schemas import user as user_schemas, role as role_schemas 4 | 5 | from api.api_v1.auth.crud import curd_user, curd_role 6 | from core.config import settings 7 | 8 | 9 | # make sure all SQL Alchemy models are imported (db.base) before initializing DB 10 | # otherwise, SQL Alchemy might fail to initialize relationships properly 11 | # for more details: https://github.com/tiangolo/full-stack-fastapi-postgresql/issues/28 12 | 13 | 14 | def init_db(db: Session) -> None: 15 | # Tables should be created with Alembic migrations 16 | # But if you don't want to use migrations, create 17 | # the tables un-commenting the next line 18 | # Base.metadata.create_all(bind=engine) 19 | for temp_role in settings.DEFAULT_ROLE: 20 | role = curd_role.query_role(db, role_id=temp_role.get("role_id")) 21 | if not role: 22 | role_in = role_schemas.RoleCreate( 23 | role_id=temp_role.get("role_id"), 24 | role_name=temp_role.get("role_name"), 25 | permission_id=temp_role.get("permission_id"), 26 | re_mark=temp_role.get("re_mark") 27 | ) 28 | role = curd_role.create(db, obj_in=role_in) 29 | print(f"角色创建成功:{role.role_name}") 30 | else: 31 | print(f"此角色id已存在:{role.role_id}") 32 | 33 | user = curd_user.get_by_email(db, email=settings.FIRST_MALL) 34 | 35 | if not user: 36 | user_in = user_schemas.UserCreate( 37 | nickname=settings.FIRST_SUPERUSER, 38 | email=settings.FIRST_MALL, 39 | password=settings.FIRST_SUPERUSER_PASSWORD, 40 | role_id=settings.FIRST_ROLE, 41 | avatar=settings.FIRST_AVATAR 42 | ) 43 | user = curd_user.create(db, obj_in=user_in) # noqa: F841 44 | print(f"{user.nickname}用户创建成功 角色id:{user.role_id}") 45 | else: 46 | print(f"{user.nickname}此用户邮箱:{user.email}已经注册过了") 47 | -------------------------------------------------------------------------------- /backend/app/api/db/session.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker 3 | 4 | from core.config import settings 5 | 6 | engine = create_engine(settings.SQLALCHEMY_DATABASE_URL, pool_pre_ping=True) 7 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 8 | -------------------------------------------------------------------------------- /backend/app/api/models/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/6/22 15:15 4 | # @Author : CoderCharm 5 | # @File : __init__.py.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | """ 11 | 12 | # from .item import Item 13 | # from .user import User 14 | -------------------------------------------------------------------------------- /backend/app/api/models/auth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/7 14:16 4 | # @Author : CoderCharm 5 | # @File : auth.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | 用户模块 11 | 12 | """ 13 | from datetime import datetime 14 | 15 | from sqlalchemy import Boolean, Column, Integer, String, VARCHAR, BIGINT, SmallInteger, DateTime 16 | from api.db.base_class import Base, gen_uuid 17 | 18 | 19 | class AdminUser(Base): 20 | """ 21 | 管理员表 22 | """ 23 | __tablename__ = "admin_user" 24 | user_id = Column(VARCHAR(32), default=gen_uuid, unique=True, comment="用户id") 25 | email = Column(VARCHAR(128), unique=True, index=True, nullable=False, comment="邮箱") 26 | phone = Column(VARCHAR(16), unique=True, index=True, nullable=True, comment="手机号") 27 | nickname = Column(VARCHAR(128), comment="管理员昵称") 28 | avatar = Column(VARCHAR(256), comment="管理员头像") 29 | hashed_password = Column(VARCHAR(128), nullable=False, comment="密码") 30 | is_active = Column(Integer, default=False, comment="邮箱是否激活 0=未激活 1=激活", server_default="0") 31 | role_id = Column(Integer, comment="角色表") 32 | __table_args__ = ({'comment': '管理员表'}) 33 | 34 | 35 | class AdminRole(Base): 36 | """ 37 | 简单的用户角色表设计 38 | """ 39 | __tablename__ = "admin_role" 40 | role_id = Column(Integer, primary_key=True, index=True, comment="角色Id") 41 | role_name = Column(VARCHAR(64), comment="角色名字") 42 | permission_id = Column(BIGINT, comment="权限ID") 43 | re_mark = Column(VARCHAR(128), comment="备注信息") 44 | __table_args__ = ({'comment': '管理员角色'}) 45 | 46 | 47 | class MallUser(Base): 48 | """ 49 | 用户表 50 | """ 51 | __tablename__ = "mall_user" 52 | user_id = Column(VARCHAR(32), default=gen_uuid, index=True, unique=True, comment="用户id") 53 | nickname = Column(VARCHAR(128), comment="用户昵称(显示用可更改)") 54 | username = Column(VARCHAR(128), comment="用户名(不可更改)") 55 | avatar = Column(VARCHAR(256), nullable=True, comment="用户头像") 56 | hashed_password = Column(VARCHAR(128), nullable=False, comment="密码") 57 | phone = Column(VARCHAR(16), unique=True, index=True, nullable=True, comment="手机号") 58 | gender = Column(SmallInteger, default=0, comment="性别 0=未知 1=男 2=女", server_default="0") 59 | register_time = Column(DateTime, default=datetime.now, comment="注册事件") 60 | last_login_time = Column(DateTime, default=datetime.now, comment="上次登录时间") 61 | last_login_ip = Column(VARCHAR(64), nullable=True, comment="上次登录IP") 62 | register_ip = Column(VARCHAR(64), nullable=True, comment="注册IP") 63 | weixin_openid = Column(VARCHAR(64), nullable=True, comment="微信openId") 64 | country = Column(VARCHAR(64), nullable=True, comment="国家") 65 | province = Column(VARCHAR(64), nullable=True, comment="省") 66 | city = Column(VARCHAR(64), nullable=True, comment="市") 67 | __table_args__ = ({'comment': '用户表'}) 68 | 69 | 70 | class MallAddress(Base): 71 | """ 72 | 用户地址列表 73 | """ 74 | name = Column(VARCHAR(64), comment="用户昵称") 75 | user_id = Column(VARCHAR(32), comment="用户id") 76 | country_id = Column(Integer, comment="国家Id") 77 | province_id = Column(Integer, comment="省id") 78 | city_id = Column(Integer, comment="市id") 79 | district_id = Column(Integer, comment="区id") 80 | address = Column(VARCHAR(128), comment="详细地址") 81 | phone = Column(VARCHAR(64), comment="手机号") 82 | is_default = Column(SmallInteger, default=0, comment="是否默认地址", server_default="0") 83 | __table_args__ = ({'comment': '地址表'}) 84 | 85 | 86 | class MallSearchHistory(Base): 87 | """ 88 | 搜索记录 89 | """ 90 | keyword = Column(VARCHAR(64), comment="搜索关键词") 91 | search_origin = Column(SmallInteger, default=1, comment="搜索来源 1=小程序 2=APP 3=PC", server_default="1") 92 | user_id = Column(VARCHAR(32), index=True, comment="用户id") 93 | __table_args__ = ({'comment': '搜索记录'}) 94 | 95 | 96 | class MallSiteNotice(Base): 97 | """ 98 | 站点消息 99 | """ 100 | enabled = Column(SmallInteger, default=1, comment="是否开启 0=为开启 1=开启", server_default="1") 101 | content = Column(VARCHAR(256), comment="全局消息通知") 102 | start_time = Column(DateTime, comment="开始时间") 103 | end_time = Column(DateTime, comment="结束时间") 104 | 105 | __table_args__ = ({'comment': '站点消息'}) 106 | 107 | -------------------------------------------------------------------------------- /backend/app/api/models/express.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/14 15:08 4 | # @Author : CoderCharm 5 | # @File : express.py 6 | # @Software: PyCharm 7 | # @Github : github/CoderCharm 8 | # @Email : wg_python@163.com 9 | # @Desc : 10 | """ 11 | 物流模块 12 | """ 13 | 14 | from sqlalchemy import Column, Integer, VARCHAR, SmallInteger, DECIMAL 15 | from api.db.base_class import Base 16 | 17 | 18 | class MallShipper(Base): 19 | """ 20 | 快递信息 21 | """ 22 | shipper_name = Column(VARCHAR(128), comment="物流公司名称") 23 | shipper_code = Column(VARCHAR(128), comment="物流公司代码") 24 | sort_order = Column(Integer, default=10, comment="排序") 25 | month_code = Column(VARCHAR(16), comment="月份") 26 | customer_name = Column(VARCHAR(16), comment="用户名") 27 | enabled = Column(SmallInteger, default=1, comment="是否开启 0=为开启 1=开启", server_default="1") 28 | __table_args__ = ({'comment': '快递信息'}) 29 | 30 | 31 | class MallRegion(Base): 32 | """ 33 | 地区表 34 | """ 35 | parent_id = Column(SmallInteger, default=0, index=True, comment="父id") 36 | region_name = Column(VARCHAR(128), default="", comment="地区名称") 37 | region_type = Column(SmallInteger, default=2, comment="地区类型") 38 | agency_id = Column(SmallInteger, default=0, index=True) 39 | area = Column(VARCHAR(64), default="", comment="方位,根据这个定运费") 40 | area_code = Column(VARCHAR(8), default="0", comment="方位代码") 41 | far_area = Column(SmallInteger, default=0, comment="偏远地区") 42 | __table_args__ = ({'comment': '地区'}) 43 | 44 | 45 | class MallExceptArea(Base): 46 | """ 47 | 偏远地区表 48 | """ 49 | content = Column(VARCHAR(256), comment="地区名称") 50 | area = Column(VARCHAR(256), comment="地区地区id ,隔开如 6,30,31,32") 51 | 52 | 53 | class MallFreightTemplate(Base): 54 | """ 55 | 运费模版 56 | """ 57 | name = Column(VARCHAR(128), nullable=True, comment="运费模版名称") 58 | package_price = Column(DECIMAL(10, 2), default=0.00, comment="包装费用") 59 | freight_type = Column(SmallInteger, default=0, comment="0=按件 1=按重量") 60 | 61 | __table_args__ = ({'comment': '运费模版'}) 62 | -------------------------------------------------------------------------------- /backend/app/api/utils/custom_exc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/6/21 11:13 4 | # @Author : CoderCharm 5 | # @File : custom_exc.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | 自定义异常 11 | 12 | """ 13 | 14 | 15 | class UserTokenError(Exception): 16 | def __init__(self, err_desc: str = "用户认证异常"): 17 | self.err_desc = err_desc 18 | 19 | 20 | class UserNotFound(Exception): 21 | def __init__(self, err_desc: str = "没有此用户"): 22 | self.err_desc = err_desc 23 | 24 | 25 | class PostParamsError(Exception): 26 | def __init__(self, err_desc: str = "POST请求参数错误"): 27 | self.err_desc = err_desc 28 | -------------------------------------------------------------------------------- /backend/app/api/utils/response_code.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/6/19 16:12 4 | # @Author : CoderCharm 5 | # @File : response_code.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 定义返回的状态 10 | 11 | # 看到文档说这个orjson 能压缩性能(squeezing performance) 12 | https://fastapi.tiangolo.com/advanced/custom-response/#use-orjsonresponse 13 | 14 | It's possible that ORJSONResponse might be a faster alternative. 15 | 16 | # 安装 17 | pip install --upgrade orjson 18 | 19 | 测试了下,序列化某些特殊的字段不友好,比如小数 20 | TypeError: Type is not JSON serializable: decimal.Decimal 21 | """ 22 | from fastapi import status 23 | from fastapi.responses import JSONResponse, Response # , ORJSONResponse 24 | 25 | from typing import Union 26 | 27 | 28 | def resp_200(*, data: Union[list, dict, str]=None, message: str="Success"): 29 | 30 | return JSONResponse( 31 | status_code=status.HTTP_200_OK, 32 | content={ 33 | 'code': 200, 34 | 'message': message, 35 | 'data': data, 36 | } 37 | ) 38 | 39 | 40 | def resp_400(*, data: str = None, message: str="BAD REQUEST") -> Response: 41 | return JSONResponse( 42 | status_code=status.HTTP_400_BAD_REQUEST, 43 | content={ 44 | 'code': 400, 45 | 'message': message, 46 | 'data': data, 47 | } 48 | ) 49 | 50 | 51 | def resp_403(*, data: str = None, message: str="Forbidden") -> Response: 52 | return JSONResponse( 53 | status_code=status.HTTP_403_FORBIDDEN, 54 | content={ 55 | 'code': 403, 56 | 'message': message, 57 | 'data': data, 58 | } 59 | ) 60 | 61 | 62 | def resp_404(*, data: str = None, message: str="Page Not Found") -> Response: 63 | return JSONResponse( 64 | status_code=status.HTTP_404_NOT_FOUND, 65 | content={ 66 | 'code': 404, 67 | 'message': message, 68 | 'data': data, 69 | } 70 | ) 71 | 72 | 73 | def resp_422(*, data: str = None, message: Union[list, dict, str]="UNPROCESSABLE_ENTITY") -> Response: 74 | return JSONResponse( 75 | status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, 76 | content={ 77 | 'code': 422, 78 | 'message': message, 79 | 'data': data, 80 | } 81 | ) 82 | 83 | 84 | def resp_500(*, data: str = None, message: Union[list, dict, str]="Server Internal Error") -> Response: 85 | return JSONResponse( 86 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 87 | content={ 88 | 'code': "500", 89 | 'message': message, 90 | 'data': data, 91 | } 92 | ) 93 | 94 | 95 | # 自定义 96 | def resp_5000(*, data: Union[list, dict, str]=None, message: str="Token failure") -> Response: 97 | return JSONResponse( 98 | status_code=status.HTTP_200_OK, 99 | content={ 100 | 'code': 5000, 101 | 'message': message, 102 | 'data': data, 103 | } 104 | ) 105 | 106 | 107 | def resp_5001(*, data: Union[list, dict, str]=None, message: str="User Not Found") -> Response: 108 | return JSONResponse( 109 | status_code=status.HTTP_200_OK, 110 | content={ 111 | 'code': 5001, 112 | 'message': message, 113 | 'data': data, 114 | } 115 | ) 116 | -------------------------------------------------------------------------------- /backend/app/assets/tmp15x208xz.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxy2077/FastAdmin/9d7039ae263aa6e1b93dc52c00ea20b54b19d2f6/backend/app/assets/tmp15x208xz.jpeg -------------------------------------------------------------------------------- /backend/app/assets/tmp6uubdhg3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxy2077/FastAdmin/9d7039ae263aa6e1b93dc52c00ea20b54b19d2f6/backend/app/assets/tmp6uubdhg3.png -------------------------------------------------------------------------------- /backend/app/assets/tmp__t5cbki.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxy2077/FastAdmin/9d7039ae263aa6e1b93dc52c00ea20b54b19d2f6/backend/app/assets/tmp__t5cbki.jpeg -------------------------------------------------------------------------------- /backend/app/assets/tmp_h7ij9kw.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxy2077/FastAdmin/9d7039ae263aa6e1b93dc52c00ea20b54b19d2f6/backend/app/assets/tmp_h7ij9kw.jpg -------------------------------------------------------------------------------- /backend/app/assets/tmpobf4p7ke.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxy2077/FastAdmin/9d7039ae263aa6e1b93dc52c00ea20b54b19d2f6/backend/app/assets/tmpobf4p7ke.jpeg -------------------------------------------------------------------------------- /backend/app/assets/tmpp0gbn5qp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxy2077/FastAdmin/9d7039ae263aa6e1b93dc52c00ea20b54b19d2f6/backend/app/assets/tmpp0gbn5qp.png -------------------------------------------------------------------------------- /backend/app/assets/tmpv_85251f.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxy2077/FastAdmin/9d7039ae263aa6e1b93dc52c00ea20b54b19d2f6/backend/app/assets/tmpv_85251f.jpg -------------------------------------------------------------------------------- /backend/app/core/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/6/22 15:15 4 | # @Author : CoderCharm 5 | # @File : __init__.py.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | """ 11 | 12 | -------------------------------------------------------------------------------- /backend/app/core/config/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/6/22 15:34 4 | # @Author : CoderCharm 5 | # @File : __init__.py.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 配置文件 10 | 11 | 我这种是一种方式,简单直观 12 | 13 | 还有一种是服务一个固定路径放一个配置文件如 /etc/conf 下 xxx.ini 或者 xxx.py文件 14 | 然后项目默认读取 /etc/conf 目录下的配置文件,能读取则为生产环境, 15 | 读取不到则为开发环境,开发环境配置可以直接写在代码里面(或者配置ide环境变量) 16 | 17 | 服务器上设置 ENV 环境变量 18 | 19 | 更具环境变量 区分生产开发 20 | 21 | """ 22 | 23 | import os 24 | 25 | # 获取环境变量 26 | env = os.getenv("ENV", "") 27 | if env: 28 | # 如果有虚拟环境 则是 生产环境 29 | print("----------生产环境启动------------") 30 | from .production_config import settings 31 | else: 32 | # 没有则是开发环境 33 | print("----------开发环境启动------------") 34 | from .development_config import settings 35 | 36 | -------------------------------------------------------------------------------- /backend/app/core/config/development_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/6/9 14:47 4 | # @Author : CoderCharm 5 | # @File : development_config.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 开发环境配置 10 | 11 | 12 | """ 13 | 14 | import os 15 | from typing import List, Union 16 | 17 | from pydantic import AnyHttpUrl, BaseSettings, IPvAnyAddress, EmailStr 18 | 19 | 20 | class Settings(BaseSettings): 21 | 22 | DEBUG: bool = True 23 | # 24 | API_V1_STR: str = "/api/mall/v1" 25 | # SECRET_KEY 记得保密生产环境 不要直接写在代码里面 26 | SECRET_KEY: str = "(-ASp+_)-Ulhw0848hnvVG-iqKyJSD&*&^-H3C9mqEqSl8KN-YRzRE" 27 | 28 | # jwt加密算法 29 | JWT_ALGORITHM: str = "HS256" 30 | # jwt token过期时间 60 minutes * 24 hours * 8 days = 8 days 31 | ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 32 | 33 | # 根路径 34 | BASE_DIR: str = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 35 | 36 | # 项目信息 37 | PROJECT_NAME: str = "FastAdmin" 38 | DESCRIPTION: str = "更多信息查看 https://www.charmcode.cn/" 39 | SERVER_NAME: str = "API_V1" 40 | SERVER_HOST: AnyHttpUrl = "http://127.0.0.1:8020" 41 | # BACKEND_CORS_ORIGINS is a JSON-formatted list of origins 42 | # e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", \ 43 | # "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]' 44 | # 跨域 45 | BACKEND_CORS_ORIGINS: List[str] = ['*'] 46 | 47 | # mysql 配置 48 | MYSQL_USERNAME: str = 'root' 49 | MYSQL_PASSWORD: str = "Admin12345-" 50 | MYSQL_HOST: Union[AnyHttpUrl, IPvAnyAddress] = "172.16.137.129" 51 | MYSQL_DATABASE: str = 'FastAdmin' 52 | 53 | # mysql地址 54 | SQLALCHEMY_DATABASE_URL = f"mysql+pymysql://{MYSQL_USERNAME}:{MYSQL_PASSWORD}@" \ 55 | f"{MYSQL_HOST}/{MYSQL_DATABASE}?charset=utf8mb4" 56 | 57 | # redis配置 58 | REDIS_HOST: str = "172.16.137.129" 59 | REDIS_PASSWORD: str = "root12345" 60 | REDIS_DB: int = 0 61 | REDIS_PORT: int = 6379 62 | REDIS_URL: str = f"redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}?encoding=utf-8" 63 | 64 | # 基本角色权限 个人没做过权限设置 但是也看过一些开源项目就这样设计吧 65 | DEFAULT_ROLE: List[dict] = [ 66 | {"role_id": 100, "role_name": "普通员工", "permission_id": 100}, 67 | {"role_id": 500, "role_name": "主管", "permission_id": 500}, 68 | {"role_id": 999, "role_name": "超级管理员", "permission_id": 999, "re_mark": "最高权限的超级管理员"}, 69 | ] 70 | 71 | # 默认生成用户数据 72 | FIRST_SUPERUSER: str = "王小右" 73 | FIRST_MALL: EmailStr = "wg_python@163.com" 74 | FIRST_SUPERUSER_PASSWORD: str = "admin12345" 75 | FIRST_ROLE: int = 999 # 超级管理员 76 | FIRST_AVATAR: AnyHttpUrl = "https://avatar-static.segmentfault.com/106/603/1066030767-5d396cc440024_huge256" 77 | 78 | class Config: 79 | case_sensitive = True 80 | 81 | 82 | settings = Settings() 83 | -------------------------------------------------------------------------------- /backend/app/core/config/production_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/6/16 10:15 4 | # @Author : CoderCharm 5 | # @File : production_config.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | 生产环境 11 | 12 | """ 13 | import os 14 | # import secrets 15 | from typing import List, Union 16 | 17 | from pydantic import AnyHttpUrl, BaseSettings, IPvAnyAddress, EmailStr 18 | 19 | 20 | class Settings(BaseSettings): 21 | DEBUG: bool = False 22 | 23 | API_V1_STR: str = "/api/mall/v1" 24 | SECRET_KEY: str = os.getenv("SECRET_KEY") 25 | 26 | # jwt加密算法 27 | JWT_ALGORITHM: str = "HS256" 28 | # 60 minutes * 24 hours * 8 days = 8 days 29 | ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 30 | # 根路径 31 | BASE_DIR: str = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 32 | 33 | # 项目信息 34 | PROJECT_NAME: str = "FastAdmin" 35 | DESCRIPTION: str = "更多信息查看 https://www.charmcode.cn/" 36 | SERVER_NAME: str = "API_V1" 37 | SERVER_HOST: AnyHttpUrl = "http://domain.com" 38 | 39 | # 跨域配置 40 | BACKEND_CORS_ORIGINS: List[AnyHttpUrl, str] = ['*'] 41 | 42 | # mysql 配置 43 | MYSQL_USERNAME: str = os.getenv("MYSQL_USER", "root") 44 | MYSQL_PASSWORD: str = os.getenv("MYSQL_PASSWORD", "admin") 45 | MYSQL_HOST: Union[AnyHttpUrl, IPvAnyAddress] = os.getenv("MYSQL_HOST", "127.0.0.1") 46 | MYSQL_DATABASE: str = 'FastAdmin' 47 | 48 | SQLALCHEMY_DATABASE_URL = f"mysql+pymysql://{MYSQL_USERNAME}:{MYSQL_PASSWORD}@" \ 49 | f"{MYSQL_HOST}/{MYSQL_DATABASE}?charset=utf8mb4" 50 | 51 | # 基本角色权限 个人没做过权限设置 但是也看过一些开源项目就这样设计吧 52 | DEFAULT_ROLE: List[dict] = [ 53 | {"role_id": 100, "role_name": "普通员工", "permission_id": 100}, 54 | {"role_id": 500, "role_name": "主管", "permission_id": 500}, 55 | {"role_id": 999, "role_name": "超级管理员", "permission_id": 999, "re_mark": "最高权限的超级管理员"}, 56 | ] 57 | 58 | # 默认生成用户数据 59 | FIRST_SUPERUSER: str = "王小右" 60 | FIRST_MALL: EmailStr = "wg_python@163.com" 61 | FIRST_SUPERUSER_PASSWORD: str = "admin12345" 62 | FIRST_ROLE: int = 999 # 超级管理员 63 | FIRST_AVATAR: AnyHttpUrl = "https://avatar-static.segmentfault.com/106/603/1066030767-5d396cc440024_huge256" 64 | 65 | class Config: 66 | case_sensitive = True 67 | 68 | 69 | settings = Settings() 70 | -------------------------------------------------------------------------------- /backend/app/core/security.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/6/22 15:15 4 | # @Author : CoderCharm 5 | # @File : __init__.py.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | token password 验证 11 | 12 | pip install python-jose 13 | 14 | pip install passlib 15 | 16 | 17 | """ 18 | 19 | from datetime import datetime, timedelta 20 | from typing import Any, Union 21 | 22 | from jose import jwt 23 | from passlib.context import CryptContext 24 | 25 | from core.config import settings 26 | 27 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 28 | 29 | 30 | def create_access_token( 31 | subject: Union[str, Any], expires_delta: timedelta = None 32 | ) -> str: 33 | if expires_delta: 34 | expire = datetime.utcnow() + expires_delta 35 | else: 36 | expire = datetime.utcnow() + timedelta( 37 | minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES 38 | ) 39 | to_encode = {"exp": expire, "sub": str(subject)} 40 | encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM) 41 | return encoded_jwt 42 | 43 | 44 | def verify_password(plain_password: str, hashed_password: str) -> bool: 45 | return pwd_context.verify(plain_password, hashed_password) 46 | 47 | 48 | def get_password_hash(password: str) -> str: 49 | return pwd_context.hash(password) 50 | -------------------------------------------------------------------------------- /backend/app/initial_data.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | 4 | 创建初始化 5 | 6 | 角色以及用户 7 | 8 | 初始化配置信息在 app/core/config/ 9 | 10 | """ 11 | import logging 12 | 13 | from api.db.init_db import init_db 14 | from api.db.session import SessionLocal 15 | 16 | logging.basicConfig(level=logging.INFO) 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | def init() -> None: 21 | db = SessionLocal() 22 | init_db(db) 23 | 24 | 25 | def main() -> None: 26 | logger.info("Creating initial data") 27 | init() 28 | logger.info("Initial data created") 29 | 30 | 31 | if __name__ == "__main__": 32 | main() 33 | -------------------------------------------------------------------------------- /backend/app/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/6/22 15:17 4 | # @Author : CoderCharm 5 | # @File : main.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | pip install uvicorn 11 | 12 | # 推荐启动方式 main指当前文件名字 app指FastAPI对象名称 13 | uvicorn main:app --host=127.0.0.1 --port=8010 --reload 14 | 15 | 16 | 类似flask 工厂模式创建 17 | 18 | 19 | # 生产启动命令 去掉热重载 (可用supervisor托管后台运行) 20 | 在main.py同文件下下启动 21 | uvicorn main:app --host=127.0.0.1 --port=8010 --workers=4 22 | 23 | # 同样可以也可以配合gunicorn多进程启动 main.py同文件下下启动 默认127.0.0.1:8000端口 24 | gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker -b 127.0.0.1:8020 25 | 26 | """ 27 | 28 | 29 | from api import create_app 30 | 31 | 32 | app = create_app() 33 | 34 | if __name__ == "__main__": 35 | import uvicorn 36 | uvicorn.run(app='main:app', host="127.0.0.1", port=8010, reload=True, debug=True) 37 | 38 | -------------------------------------------------------------------------------- /backend/app/tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/6/22 15:16 4 | # @Author : CoderCharm 5 | # @File : __init__.py.py 6 | # @Software: PyCharm 7 | # @Desc : 8 | """ 9 | 10 | """ 11 | -------------------------------------------------------------------------------- /backend/app/tests/test_aioredis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/25 10:30 4 | # @Author : CoderCharm 5 | # @File : test_aioredis.py 6 | # @Software: PyCharm 7 | # @Github : github/CoderCharm 8 | # @Email : wg_python@163.com 9 | # @Desc : 10 | """ 11 | 12 | 测试aioredis 13 | 14 | 本来是想直接使用redsi的, 但是查阅资料都是使用aioredis 15 | 16 | 看了很多在FastAPI中使用redis的demo 17 | 18 | 所以参照着写,把redsi挂载到fastapi app上 在/app/api/__init__.py register_redis函数 19 | 20 | 使用,从request对象中拿到redis对象 21 | request.app.state.redis 22 | 23 | 24 | https://github.com/tiangolo/fastapi/issues/1694 25 | https://github.com/tiangolo/fastapi/issues/1742 26 | https://github.com/leonh/redis-streams-fastapi-chat/blob/master/chat.py 27 | 28 | 29 | import aioredis 30 | 31 | async def gen_redis(): 32 | try: 33 | redis_cli = await aioredis.create_redis_pool( 34 | ("172.16.137.129", 6379), password="root12345", encoding='utf-8') 35 | print("返回cli") 36 | yield redis_cli 37 | finally: 38 | print("关闭执行了") 39 | # https://aioredis.readthedocs.io/en/v1.3.0/api_reference.html#aioredis.RedisConnection.wait_closed 40 | await redis_cli.wait_closed() 41 | 42 | 43 | async def a(): 44 | print("开始") 45 | r = await gen_redis().__anext__() 46 | print("收到了") 47 | await r.set("a", 123) 48 | print("结束了") 49 | 50 | 51 | asyncio.run(a()) 52 | 53 | """ 54 | 55 | import asyncio 56 | 57 | from aioredis import create_redis_pool, Redis 58 | 59 | from fastapi import FastAPI, Request, Query 60 | 61 | app = FastAPI() 62 | 63 | 64 | async def get_redis_pool() -> Redis: 65 | redis = await create_redis_pool(f"redis://:root12345@172.16.137.129:6379/0?encoding=utf-8") 66 | return redis 67 | 68 | 69 | @app.on_event('startup') 70 | async def startup_event(): 71 | """ 72 | 获取链接 73 | :return: 74 | """ 75 | app.state.redis = await get_redis_pool() 76 | 77 | 78 | @app.on_event('shutdown') 79 | async def shutdown_event(): 80 | """ 81 | 关闭 82 | :return: 83 | """ 84 | app.state.redis.close() 85 | await app.state.redis.wait_closed() 86 | 87 | 88 | @app.get("/test", summary="测试redis") 89 | async def test_redis(request: Request, num: int=Query(123, title="参数num")): 90 | # redis写入 await异步变同步 91 | await request.app.state.redis.set("aa", num) 92 | # redis读取 93 | v = await request.app.state.redis.get("aa") 94 | print(v, type(v)) 95 | return {"msg": v} 96 | 97 | 98 | if __name__ == '__main__': 99 | import uvicorn 100 | uvicorn.run(app='test_aioredis:app', host="127.0.0.1", port=8080, reload=True, debug=True) 101 | 102 | -------------------------------------------------------------------------------- /backend/app/tests/test_websocket.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/7/24 17:48 4 | # @Author : CoderCharm 5 | # @File : test_websocket.py.py 6 | # @Software: PyCharm 7 | # @Github : github/CoderCharm 8 | # @Email : wg_python@163.com 9 | # @Desc : 10 | """ 11 | 12 | """ 13 | 14 | 15 | from fastapi import FastAPI, WebSocket 16 | from fastapi.responses import HTMLResponse 17 | 18 | app = FastAPI() 19 | 20 | html = """ 21 | 22 | 23 | 24 | Chat 25 | 26 | 27 |

WebSocket Chat

28 |
29 | 30 | 31 |
32 | 34 | 50 | 51 | 52 | """ 53 | 54 | 55 | @app.get("/") 56 | async def get(): 57 | return HTMLResponse(html) 58 | 59 | 60 | @app.websocket("/ws") 61 | async def websocket_endpoint(websocket: WebSocket): 62 | await websocket.accept() 63 | while True: 64 | data = await websocket.receive_text() 65 | await websocket.send_text(f"Message text was: {data}") 66 | 67 | 68 | if __name__ == "__main__": 69 | import uvicorn 70 | uvicorn.run(app='test_websocket:app', host="127.0.0.1", port=8090, reload=True, debug=True) 71 | -------------------------------------------------------------------------------- /backend/app/utils.py: -------------------------------------------------------------------------------- 1 | # import logging 2 | # from datetime import datetime, timedelta 3 | # from pathlib import Path 4 | # from typing import Any, Dict, Optional 5 | # 6 | # 7 | # from jose import jwt 8 | # 9 | # from core.config import settings 10 | # 11 | # 12 | # def send_email( 13 | # email_to: str, 14 | # subject_template: str = "", 15 | # html_template: str = "", 16 | # environment: Dict[str, Any] = {}, 17 | # ) -> None: 18 | # print("发送邮件") 19 | # logging.info(f"send email result:") 20 | # 21 | # 22 | # def send_test_email(email_to: str) -> None: 23 | # project_name = settings.PROJECT_NAME 24 | # subject = f"{project_name} - Test email" 25 | # with open(Path(settings.EMAIL_TEMPLATES_DIR) / "test_email.html") as f: 26 | # template_str = f.read() 27 | # send_email( 28 | # email_to=email_to, 29 | # subject_template=subject, 30 | # html_template=template_str, 31 | # environment={"project_name": settings.PROJECT_NAME, "email": email_to}, 32 | # ) 33 | # 34 | # 35 | # def send_reset_password_email(email_to: str, email: str, token: str) -> None: 36 | # project_name = settings.PROJECT_NAME 37 | # subject = f"{project_name} - Password recovery for user {email}" 38 | # with open(Path(settings.EMAIL_TEMPLATES_DIR) / "reset_password.html") as f: 39 | # template_str = f.read() 40 | # server_host = settings.SERVER_HOST 41 | # link = f"{server_host}/reset-password?token={token}" 42 | # send_email( 43 | # email_to=email_to, 44 | # subject_template=subject, 45 | # html_template=template_str, 46 | # environment={ 47 | # "project_name": settings.PROJECT_NAME, 48 | # "username": email, 49 | # "email": email_to, 50 | # "valid_hours": settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS, 51 | # "link": link, 52 | # }, 53 | # ) 54 | # 55 | # 56 | # def send_new_account_email(email_to: str, username: str, password: str) -> None: 57 | # project_name = settings.PROJECT_NAME 58 | # subject = f"{project_name} - New account for user {username}" 59 | # with open(Path(settings.EMAIL_TEMPLATES_DIR) / "new_account.html") as f: 60 | # template_str = f.read() 61 | # link = settings.SERVER_HOST 62 | # send_email( 63 | # email_to=email_to, 64 | # subject_template=subject, 65 | # html_template=template_str, 66 | # environment={ 67 | # "project_name": settings.PROJECT_NAME, 68 | # "username": username, 69 | # "password": password, 70 | # "email": email_to, 71 | # "link": link, 72 | # }, 73 | # ) 74 | # 75 | # def generate_password_reset_token(email: str) -> str: 76 | # delta = timedelta(hours=settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS) 77 | # now = datetime.utcnow() 78 | # expires = now + delta 79 | # exp = expires.timestamp() 80 | # encoded_jwt = jwt.encode( 81 | # {"exp": exp, "nbf": now, "sub": email}, settings.SECRET_KEY, algorithm="HS256", 82 | # ) 83 | # return encoded_jwt 84 | # 85 | # 86 | # def verify_password_reset_token(token: str) -> Optional[str]: 87 | # try: 88 | # decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) 89 | # return decoded_token["email"] 90 | # except jwt.JWTError: 91 | # return None 92 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==0.5.0 2 | aioredis==1.3.1 3 | alembic==1.4.2 4 | async-timeout==3.0.1 5 | bcrypt==3.1.7 6 | cffi==1.14.0 7 | click==7.1.2 8 | dnspython==1.16.0 9 | ecdsa==0.15 10 | email-validator==1.1.1 11 | fastapi==0.60.1 12 | gunicorn==20.0.4 13 | h11==0.9.0 14 | hiredis==1.1.0 15 | httptools==0.1.1 16 | idna==2.9 17 | loguru==0.5.1 18 | Mako==1.1.3 19 | MarkupSafe==1.1.1 20 | passlib==1.7.2 21 | pyasn1==0.4.8 22 | pycparser==2.20 23 | pydantic==1.5.1 24 | PyMySQL==0.9.3 25 | python-dateutil==2.8.1 26 | python-editor==1.0.4 27 | python-jose==3.1.0 28 | python-multipart==0.0.5 29 | rsa==4.6 30 | six==1.15.0 31 | SQLAlchemy==1.3.17 32 | starlette==0.13.6 33 | uvicorn==0.11.7 34 | uvloop==0.14.0 35 | websockets==8.1 36 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = 'http://127.0.0.1:8010/api/mall/v1/admin' 6 | -------------------------------------------------------------------------------- /frontend/.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/api/admin/v1' 6 | 7 | -------------------------------------------------------------------------------- /frontend/.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | 3 | # just a flag 4 | ENV = 'staging' 5 | 6 | # base api 7 | VUE_APP_BASE_API = '/stage-api' 8 | 9 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /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 17 | -------------------------------------------------------------------------------- /frontend/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 10 3 | script: npm run test 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /frontend/DEV-LOG.md: -------------------------------------------------------------------------------- 1 | # Vue Mall后台开发日志 2 | 3 | 像写博客那样记录自己是如何一步步写出来的。 4 | 5 | ## 一 初始化 6 | 7 | #### 克隆代码 8 | 9 | ```shell 10 | git clone https://github.com/PanJiaChen/vue-admin-template.git forntend 11 | 12 | ``` 13 | #### 修改`package.json`文件 14 | ``` 15 | # 依赖文件安装修改 16 | # 个人学习vue较晚,更喜欢npm run serve启动。 17 | "dev": "vue-cli-service serve", 改为 "serve": "vue-cli-service serve", 18 | 19 | # 由于我用的webstorm eslint一直提示报错,之前搜过是版本太高 20 | 21 | "eslint": "^6.7.2", 改为 "eslint": "^5.6.0", 22 | 23 | ``` 24 | 25 | #### 添加tagsview 快捷导航(标签栏导航) 26 | 27 | 参考 https://github.com/PanJiaChen/vue-admin-template/issues/349 28 | 29 | 也可以直接clone我这个 v1_init 版本 30 | 31 | https://github.com/CoderCharm/FastAdmin/tree/v1_init 32 | 33 | ## 二 首页 echarts 集成 34 | 35 | 搜Github上面 Vue echarts 插件, star最高的就是这俩 36 | 37 | https://github.com/ecomfe/vue-echarts (4.9k star 百度家开源的) 38 | https://github.com/ElemeFE/v-charts (5.8k star 饿了么开源的) 39 | 40 | [v-charts文档](https://v-charts.js.org/#/) https://v-charts.js.org/#/ 41 | 42 | 还是直接用`echarts`,这个库没人维护了, https://github.com/ElemeFE/v-charts/issues/842 43 | 44 | https://echarts.apache.org/zh/api.html#echarts 45 | 46 | -------------------------------------------------------------------------------- /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-zh.md: -------------------------------------------------------------------------------- 1 | # vue-admin-template 2 | 3 | > 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。 4 | 5 | [线上地址](http://panjiachen.github.io/vue-admin-template) 6 | 7 | [国内访问](https://panjiachen.gitee.io/vue-admin-template) 8 | 9 | 目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`。 10 | 11 | ## Extra 12 | 13 | 如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control) 14 | 15 | ## GitAds 16 | 17 | [GitAds](https://tracking.gitads.io/?repo=PanJiaChen/vue-admin-template) 18 | 19 | 20 | ## 相关项目 21 | 22 | - [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) 23 | 24 | - [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) 25 | 26 | - [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) 27 | 28 | - [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312) 29 | 30 | 写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目: 31 | 32 | - [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2) 33 | - [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac) 34 | - [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35) 35 | - [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56) 36 | - [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836) 37 | 38 | ## Build Setup 39 | 40 | ```bash 41 | # 克隆项目 42 | git clone https://github.com/PanJiaChen/vue-admin-template.git 43 | 44 | # 进入项目目录 45 | cd vue-admin-template 46 | 47 | # 安装依赖 48 | npm install 49 | 50 | # 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 51 | npm install --registry=https://registry.npm.taobao.org 52 | 53 | # 启动服务 54 | npm run dev 55 | ``` 56 | 57 | 浏览器访问 [http://localhost:9528](http://localhost:9528) 58 | 59 | ## 发布 60 | 61 | ```bash 62 | # 构建测试环境 63 | npm run build:stage 64 | 65 | # 构建生产环境 66 | npm run build:prod 67 | ``` 68 | 69 | ## 其它 70 | 71 | ```bash 72 | # 预览发布环境效果 73 | npm run preview 74 | 75 | # 预览发布环境效果 + 静态资源分析 76 | npm run preview -- --report 77 | 78 | # 代码格式检查 79 | npm run lint 80 | 81 | # 代码格式检查并自动修复 82 | npm run lint -- --fix 83 | ``` 84 | 85 | 更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/) 86 | 87 | ## 购买贴纸 88 | 89 | 你也可以通过 购买[官方授权的贴纸](https://smallsticker.com/product/vue-element-admin) 的方式来支持 vue-element-admin - 每售出一张贴纸,我们将获得 2 元的捐赠。 90 | 91 | ## Demo 92 | 93 | ![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif) 94 | 95 | ## Browsers support 96 | 97 | Modern browsers and Internet Explorer 10+. 98 | 99 | | [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | 100 | | --------- | --------- | --------- | --------- | 101 | | IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions 102 | 103 | ## License 104 | 105 | [MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license. 106 | 107 | Copyright (c) 2017-present PanJiaChen 108 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | ## Mall 项目后台 2 | 3 | > 这是配套 个人练习的 https://github.com/CoderCharm/Mall 而写的后台管理。 4 | > 基于花裤衩的开源 https://github.com/PanJiaChen/vue-admin-template 后台模版构建,感谢花裤衩。 5 | 6 | ## 安装启动 7 | 8 | ``` 9 | # 安装依赖 10 | npm install 11 | 12 | # 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 13 | npm install --registry=https://registry.npm.taobao.org 14 | 15 | # 启动服务 16 | npm run serve 17 | 18 | ``` 19 | 20 | 21 | ## 开发日志 22 | 23 | [记录自己学习Vue的过程](./DEV-LOG.md) 24 | 25 | ## 参考 26 | - [vue-element-admin文档](https://panjiachen.github.io/vue-element-admin-site/zh/guide/) https://panjiachen.github.io/vue-element-admin-site/zh/guide/ 27 | - [js开源项目大佬 bailicangdu](https://github.com/bailicangdu) https://github.com/bailicangdu 28 | - [开箱即用的CRUD后台管理](https://github.com/flipped-aurora/gin-vue-admin/tree/master/web) https://github.com/flipped-aurora/gin-vue-admin/tree/master/web -------------------------------------------------------------------------------- /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/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /frontend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-admin-template", 3 | "version": "4.4.0", 4 | "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint", 5 | "author": "Pan ", 6 | "scripts": { 7 | "serve": "vue-cli-service serve", 8 | "build:prod": "vue-cli-service build", 9 | "build:stage": "vue-cli-service build --mode staging", 10 | "preview": "node build/index.js --preview", 11 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml", 12 | "lint": "eslint --ext .js,.vue src", 13 | "test:unit": "jest --clearCache && vue-cli-service test:unit", 14 | "test:ci": "npm run lint && npm run test:unit" 15 | }, 16 | "dependencies": { 17 | "@kangc/v-md-editor": "^1.4.8", 18 | "axios": "0.18.1", 19 | "core-js": "3.6.5", 20 | "echarts": "^4.8.0", 21 | "element-ui": "2.13.2", 22 | "js-cookie": "2.2.0", 23 | "lodash.debounce": "^4.0.8", 24 | "markdown-it": "^11.0.0", 25 | "mavon-editor": "^2.9.0", 26 | "normalize.css": "7.0.0", 27 | "nprogress": "0.2.0", 28 | "path-to-regexp": "2.4.0", 29 | "vue": "2.6.10", 30 | "vue-count-to": "^1.0.13", 31 | "vue-markdown": "^2.2.4", 32 | "vue-router": "3.0.6", 33 | "vuex": "3.1.0", 34 | "wangeditor": "^3.1.1" 35 | }, 36 | "devDependencies": { 37 | "@vue/cli-plugin-babel": "4.4.4", 38 | "@vue/cli-plugin-eslint": "4.4.4", 39 | "@vue/cli-plugin-unit-jest": "4.4.4", 40 | "@vue/cli-service": "4.4.4", 41 | "@vue/test-utils": "1.0.0-beta.29", 42 | "autoprefixer": "9.5.1", 43 | "babel-eslint": "10.1.0", 44 | "babel-jest": "23.6.0", 45 | "babel-plugin-dynamic-import-node": "2.3.3", 46 | "chalk": "2.4.2", 47 | "connect": "3.6.6", 48 | "eslint": "^5.6.0", 49 | "eslint-plugin-vue": "6.2.2", 50 | "html-webpack-plugin": "3.2.0", 51 | "mockjs": "1.0.1-beta3", 52 | "node-sass": "^4.14.1", 53 | "runjs": "4.3.2", 54 | "sass": "1.26.8", 55 | "sass-loader": "^7.1.0", 56 | "script-ext-html-webpack-plugin": "2.1.3", 57 | "serve-static": "1.13.2", 58 | "style-loader": "^1.2.1", 59 | "svg-sprite-loader": "4.1.3", 60 | "svgo": "1.2.2", 61 | "vue-template-compiler": "2.6.10" 62 | }, 63 | "browserslist": [ 64 | "> 1%", 65 | "last 2 versions" 66 | ], 67 | "engines": { 68 | "node": ">=8.9", 69 | "npm": ">= 3.0.0" 70 | }, 71 | "license": "MIT" 72 | } 73 | -------------------------------------------------------------------------------- /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/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxy2077/FastAdmin/9d7039ae263aa6e1b93dc52c00ea20b54b19d2f6/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= webpackConfig.name %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /frontend/src/api/goods.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 添加分类 4 | export function addCategory(data) { 5 | return request( 6 | '/goods/add/category', 7 | 'post', 8 | data 9 | ) 10 | } 11 | 12 | // 删除分类 13 | export function delCategory(data) { 14 | return request( 15 | '/goods/del/category', 16 | 'post', 17 | data 18 | ) 19 | } 20 | 21 | // 修改分类 22 | export function modifyCategory(data) { 23 | return request( 24 | '/goods/modify/category', 25 | 'post', 26 | data 27 | ) 28 | } 29 | 30 | // 开启分类 31 | export function enabledCategory(data) { 32 | return request( 33 | '/goods/enabled/category', 34 | 'post', 35 | data 36 | ) 37 | } 38 | 39 | // 获取分类 40 | export function getCategory(data) { 41 | return request( 42 | '/goods/query/category', 43 | 'get', 44 | data 45 | ) 46 | } 47 | 48 | // 获取全部分类 49 | export function getCategoryList(data) { 50 | return request( 51 | '/goods/query/category/list', 52 | 'get', 53 | data 54 | ) 55 | } 56 | 57 | // 通过name和front_desc查询分类 58 | export function searchCategoryList(data) { 59 | return request( 60 | '/goods/search/category', 61 | 'post', 62 | data 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /frontend/src/api/table.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getList(params) { 4 | return request( 5 | '/auth/table/list', 6 | '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 | '/auth/login/access-token', 6 | 'post', 7 | data 8 | ) 9 | } 10 | 11 | export function getInfo(token) { 12 | return request( 13 | '/auth/user/info', 14 | 'get', 15 | token 16 | ) 17 | } 18 | 19 | export function logout() { 20 | return request( 21 | '/auth/user/logout', 22 | 'post' 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/api/utils.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 上传图片 4 | export function UpLoadImg(data) { 5 | return request( 6 | '/utils/upload/file', 7 | 'post', 8 | data 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxy2077/FastAdmin/9d7039ae263aa6e1b93dc52c00ea20b54b19d2f6/frontend/src/assets/404_images/404.png -------------------------------------------------------------------------------- /frontend/src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxy2077/FastAdmin/9d7039ae263aa6e1b93dc52c00ea20b54b19d2f6/frontend/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /frontend/src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 65 | 66 | 79 | -------------------------------------------------------------------------------- /frontend/src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /frontend/src/components/MarkdownEditor/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 52 | 53 | 128 | -------------------------------------------------------------------------------- /frontend/src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 47 | 48 | 63 | -------------------------------------------------------------------------------- /frontend/src/components/Tinymce/components/EditorImage.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 113 | 114 | 122 | -------------------------------------------------------------------------------- /frontend/src/components/Tinymce/dynamicLoadScript.js: -------------------------------------------------------------------------------- 1 | let callbacks = [] 2 | 3 | function loadedTinymce() { 4 | // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144 5 | // check is successfully downloaded script 6 | return window.tinymce 7 | } 8 | 9 | const dynamicLoadScript = (src, callback) => { 10 | const existingScript = document.getElementById(src) 11 | const cb = callback || function() {} 12 | 13 | if (!existingScript) { 14 | const script = document.createElement('script') 15 | script.src = src // src url for the third-party library being loaded. 16 | script.id = src 17 | document.body.appendChild(script) 18 | callbacks.push(cb) 19 | const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd 20 | onEnd(script) 21 | } 22 | 23 | if (existingScript && cb) { 24 | if (loadedTinymce()) { 25 | cb(null, existingScript) 26 | } else { 27 | callbacks.push(cb) 28 | } 29 | } 30 | 31 | function stdOnEnd(script) { 32 | script.onload = function() { 33 | // this.onload = null here is necessary 34 | // because even IE9 works not like others 35 | this.onerror = this.onload = null 36 | for (const cb of callbacks) { 37 | cb(null, script) 38 | } 39 | callbacks = null 40 | } 41 | script.onerror = function() { 42 | this.onerror = this.onload = null 43 | cb(new Error('Failed to load ' + src), script) 44 | } 45 | } 46 | 47 | function ieOnEnd(script) { 48 | script.onreadystatechange = function() { 49 | if (this.readyState !== 'complete' && this.readyState !== 'loaded') return 50 | this.onreadystatechange = null 51 | for (const cb of callbacks) { 52 | cb(null, script) // there is no way to catch loading errors in IE8 53 | } 54 | callbacks = null 55 | } 56 | } 57 | } 58 | 59 | export default dynamicLoadScript 60 | -------------------------------------------------------------------------------- /frontend/src/components/Tinymce/plugins.js: -------------------------------------------------------------------------------- 1 | // Any plugins you want to use has to be imported 2 | // Detail plugins list see https://www.tinymce.com/docs/plugins/ 3 | // Custom builds see https://www.tinymce.com/download/custom-builds/ 4 | 5 | const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount'] 6 | 7 | export default plugins 8 | -------------------------------------------------------------------------------- /frontend/src/components/Tinymce/toolbar.js: -------------------------------------------------------------------------------- 1 | // Here is a list of the toolbar 2 | // Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols 3 | 4 | const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen'] 5 | 6 | export default toolbar 7 | -------------------------------------------------------------------------------- /frontend/src/components/WangEditor/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 27 | -------------------------------------------------------------------------------- /frontend/src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /frontend/src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | 26 | 38 | 39 | 47 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 60 | 61 | 139 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | device() { 4 | return this.$store.state.app.device 5 | } 6 | }, 7 | mounted() { 8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug 9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135 10 | this.fixBugIniOS() 11 | }, 12 | methods: { 13 | fixBugIniOS() { 14 | const $subMenu = this.$refs.subMenu 15 | if ($subMenu) { 16 | const handleMouseleave = $subMenu.handleMouseleave 17 | $subMenu.handleMouseleave = (e) => { 18 | if (this.device === 'mobile') { 19 | return 20 | } 21 | handleMouseleave(e) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 42 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | 34 | 83 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 96 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 57 | -------------------------------------------------------------------------------- /frontend/src/layout/components/TagsView/ScrollPane.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 71 | 72 | 88 | -------------------------------------------------------------------------------- /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 | export { default as TagsView } from './TagsView' 5 | -------------------------------------------------------------------------------- /frontend/src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 54 | 55 | 96 | -------------------------------------------------------------------------------- /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 | import mavonEditor from 'mavon-editor' 19 | import 'mavon-editor/dist/css/index.css' 20 | // use 21 | Vue.use(mavonEditor) 22 | 23 | // set ElementUI lang to EN 24 | // Vue.use(ElementUI, { locale }) 25 | // 如果想要中文版 element-ui,按如下方式声明 26 | Vue.use(ElementUI) 27 | 28 | Vue.config.productionTip = false 29 | 30 | new Vue({ 31 | el: '#app', 32 | router, 33 | store, 34 | render: h => h(App) 35 | }) 36 | 37 | -------------------------------------------------------------------------------- /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 后台管理', 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 | visitedViews: state => state.tagsView.visitedViews, 8 | cachedViews: state => state.tagsView.cachedViews 9 | } 10 | export default getters 11 | -------------------------------------------------------------------------------- /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 | import tagsView from './modules/tagsView' 8 | 9 | Vue.use(Vuex) 10 | 11 | const store = new Vuex.Store({ 12 | modules: { 13 | app, 14 | settings, 15 | user, 16 | tagsView 17 | }, 18 | getters 19 | }) 20 | 21 | export default store 22 | -------------------------------------------------------------------------------- /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 | console.log('vuex登录', username, password) 37 | console.log(response) 38 | const { data } = response 39 | commit('SET_TOKEN', data.token) 40 | setToken(data.token) 41 | resolve() 42 | }).catch(error => { 43 | reject(error) 44 | }) 45 | }) 46 | }, 47 | 48 | // get user info 49 | getInfo({ commit, state }) { 50 | return new Promise((resolve, reject) => { 51 | getInfo(state.token).then(response => { 52 | const { data } = response 53 | 54 | if (!data) { 55 | return reject('Verification failed, please Login again.') 56 | } 57 | 58 | const { nickname, avatar } = data 59 | 60 | commit('SET_NAME', nickname) 61 | commit('SET_AVATAR', avatar) 62 | resolve(data) 63 | }).catch(error => { 64 | reject(error) 65 | }) 66 | }) 67 | }, 68 | 69 | // user logout 70 | logout({ commit, state }) { 71 | return new Promise((resolve, reject) => { 72 | logout(state.token).then(() => { 73 | removeToken() // must remove token first 74 | resetRouter() 75 | commit('RESET_STATE') 76 | resolve() 77 | }).catch(error => { 78 | reject(error) 79 | }) 80 | }) 81 | }, 82 | 83 | // remove token 84 | resetToken({ commit }) { 85 | return new Promise(resolve => { 86 | removeToken() // must remove token first 87 | commit('RESET_STATE') 88 | resolve() 89 | }) 90 | } 91 | } 92 | 93 | export default { 94 | namespaced: true, 95 | state, 96 | mutations, 97 | actions 98 | } 99 | 100 | -------------------------------------------------------------------------------- /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/common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 放一些全局的变量 3 | * */ 4 | export const commonSetting = { 5 | uploadUrl: 'http://127.0.0.1:8010/api/mall/v1/admin/utils/upload/file' 6 | } 7 | -------------------------------------------------------------------------------- /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') { 49 | return ['日', '一', '二', '三', '四', '五', '六'][value] 50 | } 51 | return value.toString().padStart(2, '0') 52 | }) 53 | return time_str 54 | } 55 | 56 | /** 57 | * @param {number} time 58 | * @param {string} option 59 | * @returns {string} 60 | */ 61 | export function formatTime(time, option) { 62 | if (('' + time).length === 10) { 63 | time = parseInt(time) * 1000 64 | } else { 65 | time = +time 66 | } 67 | const d = new Date(time) 68 | const now = Date.now() 69 | 70 | const diff = (now - d) / 1000 71 | 72 | if (diff < 30) { 73 | return '刚刚' 74 | } else if (diff < 3600) { 75 | // less 1 hour 76 | return Math.ceil(diff / 60) + '分钟前' 77 | } else if (diff < 3600 * 24) { 78 | return Math.ceil(diff / 3600) + '小时前' 79 | } else if (diff < 3600 * 24 * 2) { 80 | return '1天前' 81 | } 82 | if (option) { 83 | return parseTime(time, option) 84 | } else { 85 | return ( 86 | d.getMonth() + 87 | 1 + 88 | '月' + 89 | d.getDate() + 90 | '日' + 91 | d.getHours() + 92 | '时' + 93 | d.getMinutes() + 94 | '分' 95 | ) 96 | } 97 | } 98 | 99 | /** 100 | * @param {string} url 101 | * @returns {Object} 102 | */ 103 | export function param2Obj(url) { 104 | const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') 105 | if (!search) { 106 | return {} 107 | } 108 | const obj = {} 109 | const searchArr = search.split('&') 110 | searchArr.forEach(v => { 111 | const index = v.indexOf('=') 112 | if (index !== -1) { 113 | const name = v.substring(0, index) 114 | const val = v.substring(index + 1, v.length) 115 | obj[name] = val 116 | } 117 | }) 118 | return obj 119 | } 120 | -------------------------------------------------------------------------------- /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['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 | 48 | // if the custom code is not 20000, it is judged as an error. 49 | if (res.code !== 200) { 50 | Message({ 51 | message: res.message || 'Error', 52 | type: 'error', 53 | duration: 5 * 1000 54 | }) 55 | 56 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; 57 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) { 58 | // to re-login 59 | MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', { 60 | confirmButtonText: 'Re-Login', 61 | cancelButtonText: 'Cancel', 62 | type: 'warning' 63 | }).then(() => { 64 | store.dispatch('user/resetToken').then(() => { 65 | location.reload() 66 | }) 67 | }) 68 | } 69 | return Promise.reject(new Error(res.message || 'Error')) 70 | } else { 71 | return res 72 | } 73 | }, 74 | error => { 75 | // console.log('err' + error) // for debug 76 | Message({ 77 | message: error.message, 78 | type: 'error', 79 | duration: 5 * 1000 80 | }) 81 | return Promise.reject(error) 82 | } 83 | ) 84 | 85 | export const request = (url, method = 'POST', data = {}) => { 86 | // 参数配置 如果是get方法就 设置成params 参数, 其他则设置成data参数 87 | const reConfig = method.toLocaleUpperCase() === 'GET' ? { 88 | url, 89 | method, 90 | params: data 91 | } : { 92 | url, 93 | method, 94 | data 95 | } 96 | return service(reConfig) 97 | } 98 | 99 | export default request 100 | -------------------------------------------------------------------------------- /frontend/src/utils/textarea-line.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 文本框添加行序 3 | * */ 4 | 5 | export const TLN = { 6 | eventList: {}, 7 | update_line_numbers: function(ta, el) { 8 | const line_count = ta.value.split('\n').length 9 | const child_count = el.children.length 10 | let difference = line_count - child_count 11 | if (difference > 0) { 12 | const frag = document.createDocumentFragment() 13 | while (difference > 0) { 14 | const line_number = document.createElement('span') 15 | line_number.className = 'tln-line' 16 | frag.appendChild(line_number) 17 | difference-- 18 | } 19 | el.appendChild(frag) 20 | } 21 | while (difference < 0) { 22 | el.removeChild(el.lastChild) 23 | difference++ 24 | } 25 | }, 26 | append_line_numbers: function(id) { 27 | const ta = document.getElementById(id) 28 | if (ta == null) { 29 | return console.warn('[tln.js] Couldn\'t find textarea of id \'' + id + '\'') 30 | } 31 | if (ta.className.indexOf('tln-active') !== -1) { 32 | return console.warn('[tln.js] textarea of id \'' + id + '\' is already numbered') 33 | } 34 | ta.classList.add('tln-active') 35 | ta.style = {} 36 | const el = document.createElement('div') 37 | el.className = 'tln-wrapper' 38 | ta.parentNode.insertBefore(el, ta) 39 | TLN.update_line_numbers(ta, el) 40 | TLN.eventList[id] = [] 41 | const __change_evts = [ 42 | 'propertychange', 'input', 'keydown', 'keyup' 43 | ] 44 | const __change_hdlr = (function(ta, el) { 45 | return function(e) { 46 | if ((+ta.scrollLeft === 10 && (e.keyCode === 37 || e.which === 37 || 47 | e.code === 'ArrowLeft' || e.key === 'ArrowLeft')) || 48 | e.keyCode === 36 || e.which === 36 || e.code === 'Home' || e.key === 'Home' || 49 | e.keyCode === 13 || e.which === 13 || e.code === 'Enter' || e.key === 'Enter' || 50 | e.code === 'NumpadEnter') { 51 | ta.scrollLeft = 0 52 | } 53 | TLN.update_line_numbers(ta, el) 54 | } 55 | }(ta, el)) 56 | for (let i = __change_evts.length - 1; i >= 0; i--) { 57 | ta.addEventListener(__change_evts[i], __change_hdlr) 58 | TLN.eventList[id].push({ 59 | evt: __change_evts[i], 60 | hdlr: __change_hdlr 61 | }) 62 | } 63 | const __scroll_evts = ['change', 'mousewheel', 'scroll'] 64 | const __scroll_hdlr = (function(ta, el) { 65 | return function() { 66 | el.scrollTop = ta.scrollTop 67 | } 68 | }(ta, el)) 69 | for (let i = __scroll_evts.length - 1; i >= 0; i--) { 70 | ta.addEventListener(__scroll_evts[i], __scroll_hdlr) 71 | TLN.eventList[id].push({ 72 | evt: __scroll_evts[i], 73 | hdlr: __scroll_hdlr 74 | }) 75 | } 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /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 | /** 22 | * @param {string} str 23 | * @returns {Boolean} 24 | * */ 25 | export function validEmail(str) { 26 | const emailReg = /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/ 27 | return emailReg.test(str) 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/views/cart/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /frontend/src/views/dashboard/component/CardView.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 57 | 58 | 82 | -------------------------------------------------------------------------------- /frontend/src/views/dashboard/component/HistogramChart.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 114 | 115 | 118 | -------------------------------------------------------------------------------- /frontend/src/views/dashboard/component/LineChart.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 111 | 112 | 115 | -------------------------------------------------------------------------------- /frontend/src/views/dashboard/component/PieChart.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 85 | 86 | 89 | -------------------------------------------------------------------------------- /frontend/src/views/dashboard/component/RadarChart.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 125 | -------------------------------------------------------------------------------- /frontend/src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 81 | 82 | 109 | -------------------------------------------------------------------------------- /frontend/src/views/goods/attribute/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 56 | 57 | 70 | -------------------------------------------------------------------------------- /frontend/src/views/goods/display/component/GoodsListView.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /frontend/src/views/goods/display/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 43 | 44 | 57 | -------------------------------------------------------------------------------- /frontend/src/views/orders/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | 25 | 38 | -------------------------------------------------------------------------------- /frontend/src/views/profile/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 22 | 23 | -------------------------------------------------------------------------------- /frontend/src/views/shop/ad/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /frontend/src/views/shop/display/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /frontend/src/views/shop/freight/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /frontend/src/views/shop/notice/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /frontend/src/views/shop/shipper/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /frontend/src/views/user/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const defaultSettings = require('./src/settings.js') 4 | 5 | function resolve(dir) { 6 | return path.join(__dirname, dir) 7 | } 8 | 9 | const name = defaultSettings.title || 'vue Admin Template' // page title 10 | 11 | // If your port is set to 80, 12 | // use administrator privileges to execute the command line. 13 | // For example, Mac: sudo npm run 14 | // You can change the port by the following methods: 15 | // port = 9528 npm run dev OR npm run dev --port = 9528 16 | const port = process.env.port || process.env.npm_config_port || 9528 // dev port 17 | 18 | // All configuration item explanations can be find in https://cli.vuejs.org/config/ 19 | module.exports = { 20 | /** 21 | * You will need to set publicPath if you plan to deploy your site under a sub path, 22 | * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/, 23 | * then publicPath should be set to "/bar/". 24 | * In most cases please use '/' !!! 25 | * Detail: https://cli.vuejs.org/config/#publicpath 26 | */ 27 | publicPath: '/', 28 | outputDir: 'dist', 29 | assetsDir: 'static', 30 | lintOnSave: process.env.NODE_ENV === 'development', 31 | productionSourceMap: false, 32 | configureWebpack: { 33 | // provide the app's title in webpack's name field, so that 34 | // it can be accessed in index.html to inject the correct title. 35 | name: name, 36 | resolve: { 37 | alias: { 38 | '@': resolve('src') 39 | } 40 | } 41 | }, 42 | chainWebpack(config) { 43 | // it can improve the speed of the first screen, it is recommended to turn on preload 44 | config.plugin('preload').tap(() => [ 45 | { 46 | rel: 'preload', 47 | // to ignore runtime.js 48 | // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171 49 | fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/], 50 | include: 'initial' 51 | } 52 | ]) 53 | 54 | // when there are many pages, it will cause too many meaningless requests 55 | config.plugins.delete('prefetch') 56 | 57 | // set svg-sprite-loader 58 | config.module 59 | .rule('svg') 60 | .exclude.add(resolve('src/icons')) 61 | .end() 62 | config.module 63 | .rule('icons') 64 | .test(/\.svg$/) 65 | .include.add(resolve('src/icons')) 66 | .end() 67 | .use('svg-sprite-loader') 68 | .loader('svg-sprite-loader') 69 | .options({ 70 | symbolId: 'icon-[name]' 71 | }) 72 | .end() 73 | 74 | config 75 | .when(process.env.NODE_ENV !== 'development', 76 | config => { 77 | config 78 | .plugin('ScriptExtHtmlWebpackPlugin') 79 | .after('html') 80 | .use('script-ext-html-webpack-plugin', [{ 81 | // `runtime` must same as runtimeChunk name. default is `runtime` 82 | inline: /runtime\..*\.js$/ 83 | }]) 84 | .end() 85 | config 86 | .optimization.splitChunks({ 87 | chunks: 'all', 88 | cacheGroups: { 89 | libs: { 90 | name: 'chunk-libs', 91 | test: /[\\/]node_modules[\\/]/, 92 | priority: 10, 93 | chunks: 'initial' // only package third parties that are initially dependent 94 | }, 95 | elementUI: { 96 | name: 'chunk-elementUI', // split elementUI into a single package 97 | priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app 98 | test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm 99 | }, 100 | commons: { 101 | name: 'chunk-commons', 102 | test: resolve('src/components'), // can customize your rules 103 | minChunks: 3, // minimum common number 104 | priority: 5, 105 | reuseExistingChunk: true 106 | } 107 | } 108 | }) 109 | // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk 110 | config.optimization.runtimeChunk('single') 111 | } 112 | ) 113 | } 114 | } 115 | --------------------------------------------------------------------------------