├── .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 |
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 | [
](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 | 
94 |
95 | ## Browsers support
96 |
97 | Modern browsers and Internet Explorer 10+.
98 |
99 | | [
](http://godban.github.io/browsers-support-badges/)IE / Edge | [
](http://godban.github.io/browsers-support-badges/)Firefox | [
](http://godban.github.io/browsers-support-badges/)Chrome | [
](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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
5 | {{ item.meta.title }}
6 | {{ item.meta.title }}
7 |
8 |
9 |
10 |
11 |
12 |
65 |
66 |
79 |
--------------------------------------------------------------------------------
/frontend/src/components/Hamburger/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
32 |
33 |
45 |
--------------------------------------------------------------------------------
/frontend/src/components/MarkdownEditor/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
52 |
53 |
128 |
--------------------------------------------------------------------------------
/frontend/src/components/SvgIcon/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
47 |
48 |
63 |
--------------------------------------------------------------------------------
/frontend/src/components/Tinymce/components/EditorImage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | upload
5 |
6 |
7 |
19 |
20 | Click upload
21 |
22 |
23 |
24 | Cancel
25 |
26 |
27 | Confirm
28 |
29 |
30 |
31 |
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 |
2 | wangEditor
3 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
25 |
26 |
38 |
39 |
47 |
--------------------------------------------------------------------------------
/frontend/src/layout/components/Navbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
30 |
31 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
44 |
--------------------------------------------------------------------------------
/frontend/src/layout/components/Sidebar/Logo.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
33 |
34 |
83 |
--------------------------------------------------------------------------------
/frontend/src/layout/components/Sidebar/SidebarItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
23 |
24 |
25 |
26 |
27 |
96 |
--------------------------------------------------------------------------------
/frontend/src/layout/components/Sidebar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
57 |
--------------------------------------------------------------------------------
/frontend/src/layout/components/TagsView/ScrollPane.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
2 |
3 |
购物车
4 |
5 |
6 |
7 |
15 |
16 |
19 |
--------------------------------------------------------------------------------
/frontend/src/views/dashboard/component/CardView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
{{ desc }}
8 |
9 |
10 |
11 |
12 |
57 |
58 |
82 |
--------------------------------------------------------------------------------
/frontend/src/views/dashboard/component/HistogramChart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
114 |
115 |
118 |
--------------------------------------------------------------------------------
/frontend/src/views/dashboard/component/LineChart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
111 |
112 |
115 |
--------------------------------------------------------------------------------
/frontend/src/views/dashboard/component/PieChart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
85 |
86 |
89 |
--------------------------------------------------------------------------------
/frontend/src/views/dashboard/component/RadarChart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
125 |
--------------------------------------------------------------------------------
/frontend/src/views/dashboard/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
81 |
82 |
109 |
--------------------------------------------------------------------------------
/frontend/src/views/goods/attribute/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 新增分类
9 |
10 |
11 |
12 |
13 |
14 | 返回列表
15 |
16 |
17 |
18 |
19 |
20 | ccc
21 |
22 |
23 |
24 |
25 |
26 |
27 |
56 |
57 |
70 |
--------------------------------------------------------------------------------
/frontend/src/views/goods/display/component/GoodsListView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 列表展示
7 |
8 |
9 |
10 |
11 |
19 |
20 |
23 |
--------------------------------------------------------------------------------
/frontend/src/views/goods/display/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 新增商品
7 |
8 |
9 |
10 |
11 |
12 | 返回列表
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
43 |
44 |
57 |
--------------------------------------------------------------------------------
/frontend/src/views/orders/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 待付款
5 | 待发货
6 | 待收货
7 | 已收获
8 | 已关闭
9 | 全部订单
10 |
11 |
12 |
13 |
14 |
24 |
25 |
38 |
--------------------------------------------------------------------------------
/frontend/src/views/profile/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
个人中心
4 |
5 |
6 |
7 |
19 |
20 |
22 |
23 |
--------------------------------------------------------------------------------
/frontend/src/views/shop/ad/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
广告
4 |
5 |
6 |
7 |
15 |
16 |
19 |
--------------------------------------------------------------------------------
/frontend/src/views/shop/display/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
显示设置
4 |
5 |
6 |
7 |
15 |
16 |
19 |
--------------------------------------------------------------------------------
/frontend/src/views/shop/freight/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
运费模版
4 |
5 |
6 |
7 |
15 |
16 |
19 |
--------------------------------------------------------------------------------
/frontend/src/views/shop/notice/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
公告管理
4 |
5 |
6 |
7 |
15 |
16 |
19 |
--------------------------------------------------------------------------------
/frontend/src/views/shop/shipper/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
快递设置
4 |
5 |
6 |
7 |
15 |
16 |
19 |
--------------------------------------------------------------------------------
/frontend/src/views/user/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
用户
4 |
5 |
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 |
--------------------------------------------------------------------------------