├── .flaskenv ├── .gitattributes ├── .github └── workflows │ ├── codeql-analysis.yml │ └── ossar-analysis.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── back ├── README.md ├── __init__.py ├── api_1_0 │ ├── __init__.py │ ├── api_tasks.py │ ├── archives.py │ ├── auth.py │ ├── books.py │ ├── categories.py │ ├── comments.py │ ├── decorators.py │ ├── errors.py │ ├── posts.py │ ├── tags.py │ ├── uploads.py │ ├── users.py │ └── utils.py ├── celery_components │ ├── __init__.py │ ├── celery_config.py │ └── tasks.py ├── config.py ├── controller │ ├── __init__.py │ ├── archives.py │ ├── authctrl.py │ ├── categories.py │ ├── posts.py │ └── tags.py ├── data │ ├── sql │ │ ├── blog-data.sql │ │ └── blog-schema.sql │ └── test_data │ │ └── iyblog_dev.sql ├── docs │ ├── data.md │ └── separate_frontend_and_backend.md ├── exception.py ├── logs │ └── .gitkeep ├── main │ ├── __init__.py │ ├── errors.py │ └── views.py ├── mains.py ├── migrations │ ├── README │ ├── alembic.ini │ ├── env.py │ ├── script.py.mako │ └── versions │ │ └── 37b32fa5627f_add_iy_article_slug.py ├── models.py ├── requirements.txt ├── setting.py └── utils │ ├── __init__.py │ ├── bd_trans.py │ ├── captcha.py │ ├── date.py │ ├── flask_logger.py │ ├── mail.py │ └── redis_util.py ├── celery_worker.py ├── confs ├── nginx │ ├── conf.d │ │ └── app.conf │ ├── nginx.conf │ └── readme.md ├── redis │ ├── 6379.conf │ └── redisd └── supervisord.d │ ├── app.ini │ ├── celery_beat.ini │ ├── celery_work.ini │ └── readme.md ├── document ├── README.MD ├── TODOlist.md ├── deploy.md ├── huawei_cloud.md ├── install_CentOS.md └── src │ ├── idealyard-qq-group.png │ ├── img_20190910153859.jpg │ ├── overview.gif │ ├── overview.jpg │ ├── reset_password.jpg │ └── tags.jpg ├── dot.env ├── front ├── .babelrc ├── .editorconfig ├── .gitignore ├── .postcssrc.js ├── README.md ├── build │ ├── build.js │ ├── check-versions.js │ ├── logo.png │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── config │ ├── dev.env.js │ ├── index.js │ ├── prod.env.js │ └── test.env.js ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── App.vue │ ├── Home.vue │ ├── api │ │ ├── article.js │ │ ├── category.js │ │ ├── comment.js │ │ ├── index.js │ │ ├── login.js │ │ ├── tag.js │ │ └── upload.js │ ├── assets │ │ ├── css │ │ │ └── style.css │ │ ├── img │ │ │ ├── bg.png │ │ │ ├── default_avatar.png │ │ │ ├── link_bg.png │ │ │ ├── logo.png │ │ │ ├── logo1.png │ │ │ ├── panfish.png │ │ │ ├── profile_bg.jpg │ │ │ ├── sea.png │ │ │ └── spray.png │ │ ├── iyicon │ │ │ ├── demo.css │ │ │ ├── demo_index.html │ │ │ ├── iconfont.css │ │ │ ├── iconfont.eot │ │ │ ├── iconfont.js │ │ │ ├── iconfont.svg │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ └── iconfont.woff2 │ │ └── theme │ │ │ ├── alert.css │ │ │ ├── aside.css │ │ │ ├── autocomplete.css │ │ │ ├── badge.css │ │ │ ├── base.css │ │ │ ├── breadcrumb-item.css │ │ │ ├── breadcrumb.css │ │ │ ├── button-group.css │ │ │ ├── button.css │ │ │ ├── card.css │ │ │ ├── carousel-item.css │ │ │ ├── carousel.css │ │ │ ├── cascader.css │ │ │ ├── checkbox-button.css │ │ │ ├── checkbox-group.css │ │ │ ├── checkbox.css │ │ │ ├── col.css │ │ │ ├── collapse-item.css │ │ │ ├── collapse.css │ │ │ ├── color-picker.css │ │ │ ├── container.css │ │ │ ├── date-picker.css │ │ │ ├── dialog.css │ │ │ ├── display.css │ │ │ ├── dropdown-item.css │ │ │ ├── dropdown-menu.css │ │ │ ├── dropdown.css │ │ │ ├── fonts │ │ │ ├── element-icons.ttf │ │ │ └── element-icons.woff │ │ │ ├── footer.css │ │ │ ├── form-item.css │ │ │ ├── form.css │ │ │ ├── header.css │ │ │ ├── icon.css │ │ │ ├── index.css │ │ │ ├── input-number.css │ │ │ ├── input.css │ │ │ ├── loading.css │ │ │ ├── main.css │ │ │ ├── menu-item-group.css │ │ │ ├── menu-item.css │ │ │ ├── menu.css │ │ │ ├── message-box.css │ │ │ ├── message.css │ │ │ ├── notification.css │ │ │ ├── option-group.css │ │ │ ├── option.css │ │ │ ├── pagination.css │ │ │ ├── popover.css │ │ │ ├── popper.css │ │ │ ├── progress.css │ │ │ ├── radio-button.css │ │ │ ├── radio-group.css │ │ │ ├── radio.css │ │ │ ├── rate.css │ │ │ ├── reset.css │ │ │ ├── row.css │ │ │ ├── scrollbar.css │ │ │ ├── select-dropdown.css │ │ │ ├── select.css │ │ │ ├── slider.css │ │ │ ├── spinner.css │ │ │ ├── step.css │ │ │ ├── steps.css │ │ │ ├── submenu.css │ │ │ ├── switch.css │ │ │ ├── tab-pane.css │ │ │ ├── table-column.css │ │ │ ├── table.css │ │ │ ├── tabs.css │ │ │ ├── tag.css │ │ │ ├── time-picker.css │ │ │ ├── time-select.css │ │ │ ├── tooltip.css │ │ │ ├── transfer.css │ │ │ ├── tree.css │ │ │ └── upload.css │ ├── components │ │ ├── BaseFooter.vue │ │ ├── article │ │ │ └── ArticleItem.vue │ │ ├── card │ │ │ ├── CardArchive.vue │ │ │ ├── CardArticle.vue │ │ │ ├── CardCategory.vue │ │ │ ├── CardFooter.vue │ │ │ ├── CardMe.vue │ │ │ └── CardTag.vue │ │ ├── comment │ │ │ └── CommentItem.vue │ │ ├── gotop │ │ │ └── GoTop.vue │ │ ├── markdown │ │ │ └── MarkdownEditor.vue │ │ └── scrollpage │ │ │ └── index.vue │ ├── main.js │ ├── request │ │ ├── index.js │ │ └── token.js │ ├── router │ │ └── index.js │ ├── store │ │ ├── index.js │ │ └── mutation-types.js │ ├── utils │ │ ├── crypto.js │ │ ├── regexr.js │ │ └── time.js │ └── views │ │ ├── About.vue │ │ ├── BaseHeader.vue │ │ ├── Index.vue │ │ ├── Link.vue │ │ ├── Log.vue │ │ ├── Login.vue │ │ ├── MessageBoard.vue │ │ ├── NotFound.vue │ │ ├── Profile.vue │ │ ├── Register.vue │ │ ├── ResetPassword.vue │ │ ├── blog │ │ ├── BlogAllCategoryTag.vue │ │ ├── BlogArchive.vue │ │ ├── BlogCategoryTag.vue │ │ ├── BlogView.vue │ │ ├── BlogWrite.vue │ │ └── Tag.vue │ │ └── common │ │ └── ArticleScrollPage.vue ├── static │ ├── .gitkeep │ ├── category │ │ ├── back.png │ │ ├── database.png │ │ ├── front.png │ │ ├── language.png │ │ └── lift.jpg │ ├── favicon.ico │ ├── tag │ │ ├── css.png │ │ ├── hibernate.svg │ │ ├── html.png │ │ ├── java.png │ │ ├── js.png │ │ ├── maven.png │ │ └── vue.png │ └── user │ │ ├── admin.png │ │ ├── user_1.png │ │ ├── user_2.png │ │ ├── user_3.png │ │ ├── user_4.png │ │ ├── user_5.png │ │ └── user_6.png └── test │ ├── e2e │ ├── custom-assertions │ │ └── elementCount.js │ ├── nightwatch.conf.js │ ├── runner.js │ └── specs │ │ └── test.js │ └── unit │ ├── .eslintrc │ ├── jest.conf.js │ ├── setup.js │ └── specs │ └── HelloWorld.spec.js ├── gun.py ├── runserver.py └── wsgi.py /.flaskenv: -------------------------------------------------------------------------------- 1 | # 用来保存 flask 相关的环境变量 2 | FLASK_APP = runserver 3 | # 开启DEBUG模式 **注意**:生产模式下必须关闭 4 | FLASK_DEBUG = True 5 | # 配置工作模式(此处默认开发模式) 6 | FLASK_ENV = 'development' 7 | # 使用的配置环境(默认使用生产配置) 8 | FLASK_CONFIG = 'default' 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | front/static/* linguist-vendored 2 | front/test/* linguist-vendored 3 | *.html linguist-vendored 4 | *.sql linguist-vendored 5 | *.css linguist-vendored 6 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 15 * * 2' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['python', 'javascript'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | # If you wish to specify custom queries, you can do so here or in a config file. 45 | # By default, queries listed here will override any specified in a config file. 46 | # Prefix the list here with "+" to use these queries and those in the config file. 47 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 48 | 49 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 50 | # If this step fails, then you should remove it and run the build manually (see below) 51 | - name: Autobuild 52 | uses: github/codeql-action/autobuild@v1 53 | 54 | # ℹ️ Command-line programs to run using the OS shell. 55 | # 📚 https://git.io/JvXDl 56 | 57 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 58 | # and modify them (or add more) to build your code if your project 59 | # uses a compiled language 60 | 61 | #- run: | 62 | # make bootstrap 63 | # make release 64 | 65 | - name: Perform CodeQL Analysis 66 | uses: github/codeql-action/analyze@v1 67 | -------------------------------------------------------------------------------- /.github/workflows/ossar-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow integrates a collection of open source static analysis tools 2 | # with GitHub code scanning. For documentation, or to provide feedback, visit 3 | # https://github.com/github/ossar-action 4 | name: OSSAR 5 | 6 | on: 7 | push: 8 | pull_request: 9 | 10 | jobs: 11 | OSSAR-Scan: 12 | # OSSAR runs on windows-latest. 13 | # ubuntu-latest and macos-latest support coming soon 14 | runs-on: windows-latest 15 | 16 | steps: 17 | # Checkout your code repository to scan 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | with: 21 | # We must fetch at least the immediate parents so that if this is 22 | # a pull request then we can checkout the head. 23 | fetch-depth: 2 24 | 25 | # If this run was triggered by a pull request event, then checkout 26 | # the head of the pull request instead of the merge commit. 27 | - run: git checkout HEAD^2 28 | if: ${{ github.event_name == 'pull_request' }} 29 | 30 | # Ensure a compatible version of dotnet is installed. 31 | # The [Microsoft Security Code Analysis CLI](https://aka.ms/mscadocs) is built with dotnet v3.1.201. 32 | # A version greater than or equal to v3.1.201 of dotnet must be installed on the agent in order to run this action. 33 | # Remote agents already have a compatible version of dotnet installed and this step may be skipped. 34 | # For local agents, ensure dotnet version 3.1.201 or later is installed by including this action: 35 | # - name: Install .NET 36 | # uses: actions/setup-dotnet@v1 37 | # with: 38 | # dotnet-version: '3.1.x' 39 | 40 | # Run open source static analysis tools 41 | - name: Run OSSAR 42 | uses: github/ossar-action@v1 43 | id: ossar 44 | 45 | # Upload results to the Security tab 46 | - name: Upload OSSAR results 47 | uses: github/codeql-action/upload-sarif@v1 48 | with: 49 | sarif_file: ${{ steps.ossar.outputs.sarifFile }} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | develop-eggs/ 12 | dist/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | MANIFEST 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | .hypothesis/ 47 | .pytest_cache/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | db.sqlite3 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # Environments 84 | .env 85 | .venv 86 | env/ 87 | venv/ 88 | ENV/ 89 | env.bak/ 90 | venv.bak/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | 105 | .DS_Store 106 | node_modules/ 107 | 108 | .idea/ 109 | front/dist.zip 110 | front/config/dev.env.js 111 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7 2 | ENV PYTHONUNBUFFERED 1 3 | RUN mkdir /idealyard 4 | WORKDIR /idealyard 5 | COPY . /idealyard 6 | RUN pip install -p Pipfile -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019, imoyao 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://mirrors.aliyun.com/pypi/simple" 4 | # just for china 5 | # url = "https://pypi.org/simple" 6 | verify_ssl = true 7 | 8 | [dev-packages] 9 | ipython = "*" 10 | 11 | [packages] 12 | aniso8601 = "==7.0.0" 13 | click = "==7.0" 14 | flask-httpauth = "==3.3.0" 15 | itsdangerous = "==1.1.0" 16 | passlib = "==1.7.1" 17 | pytz = "==2019.1" 18 | six = "==1.12.0" 19 | Flask = "==1.0.3" 20 | Flask-Cors = "==3.0.8" 21 | Flask-RESTful = "==0.3.7" 22 | Flask-SQLAlchemy = "==2.4.0" 23 | Jinja2 = "==2.10.1" 24 | MarkupSafe = "==1.1.1" 25 | PyMySQL = "==0.9.3" 26 | SQLAlchemy = "==1.3.4" 27 | Werkzeug = "==0.15.4" 28 | flask-migrate = "*" 29 | gevent = "*" 30 | gunicorn = "*" 31 | flask-mail = "*" 32 | redis = "*" 33 | flask-redis = "*" 34 | flask-uploads = "*" 35 | python-dotenv = "*" 36 | celery = "*" 37 | supervisor = "*" 38 | 39 | [requires] 40 | python_version = "3.7" 41 | -------------------------------------------------------------------------------- /back/README.md: -------------------------------------------------------------------------------- 1 | ## 安装环境依赖 2 | 3 | ### 后端 4 | ```shell 5 | pip install -r requirments.txt 6 | ``` 7 | 8 | ## 启动服务 9 | 10 | ### 命令行启动 11 | ```shell 12 | export FLASK_APP=runserver.py 13 | export FLASK_ENV=default # 设置env 14 | flask run --host=0.0.0.0 # 外部访问 15 | ``` 16 | ### PyCharm启动 17 | 18 | 配置`Run/Debug Configuration`,如下 19 | ```shell 20 | FLASK_APP = runserver.py # default script path,if module name: FLASK_APP = runserver 21 | FLASK_ENV = development 22 | FLASK_DEBUG = 1 23 | In folder XXX/idealyard/back 24 | ssh://root@192.168.*.*:22/home/*/envs/*/bin/python -u -m flask run --host=0.0.0.0 25 | ``` 26 | ## TODO 27 | 每次清空数据库后再次重启服务,报错: 28 | ```bash 29 | AssertionError: View function mapping is overwriting an existing endpoint function: auth 30 | ``` 31 | - http://www.voidcn.com/article/p-zkaupalv-bve.html 32 | -------------------------------------------------------------------------------- /back/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import Flask 5 | from flask_cors import CORS 6 | from flask_migrate import Migrate 7 | from flask_uploads import configure_uploads, patch_request_class 8 | 9 | from back.api_1_0 import api, auth, posts, users, tags, archives, categories, comments, users, uploads, api_tasks 10 | from back.config import config 11 | from .api_1_0.books import Books, Test 12 | from .models import db 13 | from back.api_1_0 import api_bp 14 | from back.main import main_bp 15 | from back.utils.flask_logger import register_logger 16 | from back.utils.mail import mail 17 | from back.utils.redis_util import redis_store 18 | 19 | cors = CORS(resources={r"/api/*": {"origins": "*"}}) 20 | 21 | 22 | def add_api(): 23 | """ 24 | 添加 api 接口 25 | :return: 26 | """ 27 | # api.add_resource(Test, '/api/tests', '/api/books/') 28 | 29 | api.add_resource(auth.Auth, '/api/signin', '/api/token') 30 | api.add_resource(auth.ResetPassword, '/api/password') 31 | api.add_resource(auth.EmailApi, '/api/emails') 32 | api.add_resource(auth.Verification, '/api/verifications') 33 | api.add_resource(users.UserApi, '/api/register', '/api/users', '/api/users/') 34 | 35 | api.add_resource(posts.PostApi, '/api/articles') 36 | api.add_resource(posts.PostDetail, '/api/articles/') 37 | api.add_resource(posts.IdentifyPostDetail, '/api/identifiers/') 38 | api.add_resource(posts.SlugApi, '/api/slugs') 39 | api.add_resource(posts.IdApi, '/api/identifiers') 40 | api.add_resource(tags.TagApi, '/api/tags', '/api/tags/') 41 | api.add_resource(categories.CategoryApi, '/api/categories', '/api/categories/') 42 | # api.add_resource(tags.TagDetail, '/api/tags/') 43 | api.add_resource(archives.Archives, '/api/archives') 44 | api.add_resource(comments.Comments, '/api/comments', '/api/tags/') 45 | 46 | # api.add_resource(archives.ArchivesDetail, '/api/archives/') 47 | api.add_resource(uploads.UploadImage, '/api/images') 48 | api.add_resource(api_tasks.TaskStatus, '/api/tasks//') 49 | 50 | 51 | def add_blueprints(app): 52 | """ 53 | 添加蓝图 54 | :param app: 55 | :return: 56 | """ 57 | app.register_blueprint(api_bp) 58 | app.register_blueprint(main_bp) 59 | 60 | 61 | def create_app(config_name): 62 | # app = Flask(__name__, static_folder="../static", template_folder="..") 63 | app = Flask(__name__, static_folder="../dist/static", template_folder="../dist") 64 | app.config.from_object(config[config_name]) 65 | config[config_name].init_app(app) 66 | # Load extensions 67 | register_logger(__name__) 68 | cors.init_app(app) 69 | db.init_app(app) 70 | migrate = Migrate(app, db) # 在db对象创建之后调用! 71 | configure_uploads(app, uploads.image_upload) # configure_uploads(app, [files, photos]) 72 | mail.init_app(app) 73 | redis_store.init_app(app) 74 | patch_request_class(app, size=None) # 防止用户上传过大文件导致服务器空间不足,加此自动引发HTTP错误。 75 | add_api() 76 | # api.init_app需要写在add_api()之后 77 | api.init_app(app) 78 | # Load blueprints 79 | add_blueprints(app) 80 | return app 81 | -------------------------------------------------------------------------------- /back/api_1_0/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/6/4 15:10 4 | 5 | from flask import Blueprint 6 | from flask_restful import Api 7 | 8 | api_bp = Blueprint('api', __name__) 9 | # 注意此处命名,上为bp,下为api 10 | api = Api() 11 | -------------------------------------------------------------------------------- /back/api_1_0/api_tasks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by Administrator at 2019/9/8 12:21 4 | from flask import jsonify 5 | from flask_restful import Resource 6 | 7 | from back.celery_components import tasks as celery_tasks 8 | 9 | 10 | class TaskStatus(Resource): 11 | """ 12 | 获取状态 13 | see also:http://www.pythondoc.com/flask-celery/first.html 14 | """ 15 | 16 | def __init__(self): 17 | self.response_obj = {'success': True, 'code': 0, 'info': {}, 'msg': ''} 18 | 19 | def get(self, task_id, name): 20 | task = None 21 | if name == 'mail': 22 | task = celery_tasks.send_reset_password_mail_long_task.AsyncResult(task_id) 23 | else: 24 | pass 25 | if task.state == 'PENDING': 26 | # job did not start yet 27 | response = { 28 | 'state': task.state, 29 | 'current': 0, 30 | 'total': 1, 31 | 'status': 'Pending...' 32 | } 33 | elif task.state != 'FAILURE': 34 | response = { 35 | 'state': task.state, 36 | 'result': task.result, 37 | } 38 | else: 39 | # something went wrong in the background job 40 | response = { 41 | 'state': task.state, 42 | 'current': 1, 43 | 'total': 1, 44 | 'status': str(task.get()), # this is the exception raised 45 | } 46 | 47 | self.response_obj['info'] = response 48 | return jsonify(self.response_obj) 49 | -------------------------------------------------------------------------------- /back/api_1_0/archives.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/6/29 22:16 4 | """ 5 | 定义所有跟归档相关的api接口 6 | """ 7 | 8 | from flask import jsonify, request 9 | from flask_restful import Resource 10 | 11 | from back.controller.archives import GetArchiveCtrl 12 | 13 | Archives_getter = GetArchiveCtrl() 14 | 15 | 16 | class Archives(Resource): 17 | """ 18 | 文章归档 19 | """ 20 | 21 | def __init__(self): 22 | self.response_obj = {'success': True, 'code': 0, 'data': None, 'msg': ''} 23 | 24 | def get(self): 25 | # 请求数据 26 | order_desc = True 27 | args = request.args 28 | if args: 29 | order = args.get('order') 30 | # 默认降序,如果传值,则判断传值是否为desc,否? >> False 31 | order_desc = order and order == 'desc' 32 | data = Archives_getter.extract_post_with_year_and_month(order_desc) 33 | self.response_obj['data'] = data 34 | return jsonify(self.response_obj) 35 | 36 | 37 | class ArchivesDetail(Resource): 38 | pass 39 | -------------------------------------------------------------------------------- /back/api_1_0/books.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/6/4 15:11 4 | 5 | from flask import jsonify, request, g 6 | from flask_restful import Resource 7 | 8 | from back.controller import tags 9 | from back.controller.authctrl import basic_auth, multi_auth 10 | 11 | BOOKS = [ 12 | { 13 | 'title': 'On the Road', 14 | 'author': 'Jack Kerouac', 15 | 'read': True 16 | }, 17 | { 18 | 'title': 'Harry Potter and the Philosopher\'s Stone', 19 | 'author': 'J. K. Rowling', 20 | 'read': False 21 | }, 22 | { 23 | 'title': 'Green Eggs and Ham', 24 | 'author': 'Dr. Seuss', 25 | 'read': True 26 | } 27 | ] 28 | 29 | 30 | class Books(Resource): 31 | BOOKS = [ 32 | { 33 | 'title': 'On the Road', 34 | 'author': 'Jack Kerouac', 35 | 'read': True 36 | }, 37 | { 38 | 'title': 'Harry Potter and the Philosopher\'s Stone', 39 | 'author': 'J. K. Rowling', 40 | 'read': False 41 | }, 42 | { 43 | 'title': 'Green Eggs and Ham', 44 | 'author': 'Dr. Seuss', 45 | 'read': True 46 | } 47 | ] 48 | 49 | def __init__(self): 50 | self.response_obj = {'success': True, 'code': 0, 'data': None, 'msg': ''} 51 | 52 | def get(self, book_id=None): 53 | if book_id: 54 | self.response_obj['data'] = self.BOOKS[0] # just for test 55 | else: 56 | self.response_obj['data'] = self.BOOKS 57 | return jsonify(self.response_obj) 58 | 59 | def post(self): 60 | post_data = request.get_json() 61 | self.BOOKS.append({ # 此处为自写逻辑 62 | 'title': post_data.get('title'), 63 | 'author': post_data.get('author'), 64 | 'read': post_data.get('read'), 65 | }) 66 | self.response_obj['msg'] = 'Books added!' 67 | return self.response_obj, 201 68 | 69 | def put(self, book_id): 70 | put_data = request.get_json() 71 | data = put_data['data'] 72 | if book_id and self.BOOKS[0]: # just for test 73 | self.BOOKS[0] = data 74 | return jsonify(put_data) 75 | 76 | def delete(self, book_id): 77 | if book_id: 78 | self.BOOKS.pop(0) 79 | return jsonify(self.response_obj) 80 | 81 | 82 | class Test(Resource): 83 | """ 84 | 用于快速测试的一个借口 85 | """ 86 | decorators = [basic_auth.login_required] 87 | 88 | def __init__(self): 89 | self.response_obj = {'success': True, 'code': 0, 'data': None, 'msg': ''} 90 | 91 | def get(self): 92 | t = tags.GetTagCtrl() 93 | data = {} 94 | self.response_obj['data'] = data 95 | return jsonify(self.response_obj) 96 | 97 | def post(self): 98 | args = request.json 99 | token = g.user.generate_auth_token() 100 | if token: 101 | username = g.user.username 102 | return jsonify({'code': 0, 'msg': "success", 'token': token.decode('ascii'), 'username': username}) 103 | else: 104 | return jsonify({'code': 1, 'msg': "请检查输入"}) 105 | -------------------------------------------------------------------------------- /back/api_1_0/categories.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/7/1 11:05 4 | """ 5 | 归档API 6 | """ 7 | 8 | from flask import request 9 | from flask_restful import Resource 10 | 11 | from back.controller.categories import GetCategoryCtrl 12 | from .utils import jsonify_with_args 13 | 14 | category_getter = GetCategoryCtrl() 15 | 16 | 17 | class CategoryApi(Resource): 18 | """ 19 | 避免重名,起名*Api 20 | """ 21 | 22 | def __init__(self): 23 | self.response_obj = {'success': True, 'code': 0, 'data': None, 'msg': ''} 24 | 25 | def get(self, category_id=None): 26 | # 请求数据 27 | args = request.args 28 | if category_id: 29 | data = category_getter.posts_for_category(category_id) 30 | self.response_obj['data'] = data 31 | return jsonify_with_args(self.response_obj) 32 | 33 | if args: 34 | # **注意**:args这里获取参数最好用dict.get() 而不是dict['key'],否则可能导致出错而程序不报错!!! 35 | pass 36 | self.response_obj['code'] = 1 37 | self.response_obj['success'] = False 38 | return jsonify_with_args(self.response_obj, 400) 39 | else: 40 | self.response_obj['data'] = category_getter.show_categories() 41 | return jsonify_with_args(self.response_obj) 42 | -------------------------------------------------------------------------------- /back/api_1_0/comments.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/7/4 11:04 4 | 5 | """ 6 | 定义所有跟评论相关的api接口 7 | """ 8 | 9 | from flask import jsonify, request 10 | from flask_restful import Resource 11 | 12 | 13 | class Comments(Resource): 14 | """ 15 | 文章归档 16 | """ 17 | 18 | def __init__(self): 19 | self.response_obj = {'success': True, 'code': 0, 'data': None, 'msg': ''} 20 | 21 | def get(self, comment_id=None): 22 | # 请求数据 23 | if comment_id: 24 | pass 25 | else: 26 | args = request.args 27 | if args: 28 | post_id = args.get('post_id') 29 | if post_id: 30 | data = None 31 | self.response_obj['data'] = data 32 | else: 33 | self.response_obj['code'] = 1 34 | self.response_obj['success'] = False 35 | self.response_obj['msg'] = 'Args required.' 36 | return jsonify(self.response_obj) 37 | -------------------------------------------------------------------------------- /back/api_1_0/decorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from functools import wraps 4 | from flask import g 5 | 6 | from .errors import forbidden 7 | 8 | 9 | def permission_required(permission): 10 | """ 11 | 用于权限检测(rbac?) 12 | :param permission: 13 | :return: 14 | """ 15 | def decorator(f): 16 | @wraps(f) 17 | def decorator_function(*args, **kwargs): 18 | if not g.current_user.can(permission): 19 | return forbidden('Insufficient permissions') 20 | return f(*args, **kwargs) 21 | return decorator_function 22 | return decorator 23 | -------------------------------------------------------------------------------- /back/api_1_0/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask import jsonify 4 | 5 | from . import api_bp 6 | from back.exception import ValidationError 7 | 8 | 9 | def bad_request(message): 10 | response = jsonify({'error': 'bad request', 'message': message}) 11 | response.status_code = 400 12 | return response 13 | 14 | 15 | def unauthorized_error(message): 16 | response = jsonify({'error': 'unauthorized', 'message': message}) 17 | response.status_code = 401 18 | return response 19 | 20 | 21 | def forbidden(message): 22 | response = jsonify({'error': 'forbidden', 'message': message}) 23 | response.status_code = 403 24 | return response 25 | 26 | 27 | def method_not_allowed(message): 28 | response = jsonify({'error': 'method not allowed', 'message': message}) 29 | response.status_code = 405 30 | return response 31 | 32 | 33 | @api_bp.errorhandler(ValidationError) 34 | def validation_error(e): 35 | return bad_request(e.args[0]) 36 | -------------------------------------------------------------------------------- /back/api_1_0/tags.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/6/29 15:09 4 | """ 5 | 定义所有跟标签相关的api接口 6 | """ 7 | 8 | from flask import jsonify, request 9 | from flask_restful import Resource 10 | 11 | from back import setting 12 | from back.controller.tags import GetTagCtrl 13 | from back.models import Tag 14 | from .utils import jsonify_with_args 15 | 16 | tag_getter = GetTagCtrl() 17 | 18 | 19 | class TagApi(Resource): 20 | """ 21 | 避免重名,起名*Api TODO: 这个函数里面获取的结构需要调整!!! 22 | """ 23 | 24 | def __init__(self): 25 | self.response_obj = {'success': True, 'code': 0, 'data': None, 'msg': ''} 26 | 27 | def get(self, tag_id=None): 28 | args = request.args 29 | query_by = args.get('query', None, type=str) 30 | order_by = args.get('order_by', 'id', type=str) 31 | order = args.get('order') # 默认降序 32 | order_by_desc = order and order == 'desc' if order else True 33 | query_key, limit_count = (None,) * 2 34 | # 最新最热走limit逻辑,截取而不是分页 TODO: 标签暂时应该没有这个必要 35 | page, per_page = (None,) * 2 36 | if args.get('limit') and args['limit']: 37 | limit_count = int(args.get('limit')) 38 | hot = args.get('hot', False, type=bool) 39 | if tag_id: # 查单个 40 | # /api/tags/id 41 | query_key = tag_id 42 | query_by = 'tag_id' # 只传id时给默认值 43 | 44 | elif not hot: 45 | # TODO:默认按照id排,后续可以添加按照名字排(index >> name) 46 | order_by = args.get('order_by', 'id', type=str) 47 | # **注意**:args这里获取参数最好用dict.get() 而不是dict['key'],否则可能导致出错而程序不报错!!! 48 | query_key = args.get('name') and args['name'] or args.get('id') and args['id'] 49 | 50 | else: 51 | # 总数少于设定值则全返回,否则返回设定值 52 | limit_count = Tag.query.count() if Tag.query.count() < setting.LIMIT_HOT_TAG_COUNT else \ 53 | setting.LIMIT_HOT_TAG_COUNT 54 | if not args: 55 | # 没有请求参数,则返回全部 56 | limit_count = None 57 | data = tag_getter.get_tag_detail_by_args(query_key, query_by=query_by, order_by=order_by, hot=hot, 58 | order_by_desc=order_by_desc, 59 | limit_count=limit_count) 60 | if data: 61 | self.response_obj['data'] = data 62 | self.response_obj['total'] = len(data) 63 | return jsonify(self.response_obj) 64 | else: 65 | # 数据为空,还没来得及初始化! 66 | self.response_obj['code'] = 1 67 | self.response_obj['msg'] = 'Please make initialization first.' 68 | self.response_obj['success'] = False 69 | return jsonify_with_args(self.response_obj, 417) 70 | 71 | 72 | class TagDetail(Resource): 73 | pass 74 | -------------------------------------------------------------------------------- /back/api_1_0/uploads.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/7/15 9:59 4 | import os 5 | 6 | from flask import request, jsonify, current_app 7 | from flask_uploads import UploadSet, IMAGES, UploadNotAllowed 8 | from flask_restful import Resource 9 | 10 | from .utils import jsonify_with_args 11 | 12 | image_upload = UploadSet('images', IMAGES) 13 | 14 | 15 | class UploadImage(Resource): 16 | def __init__(self): 17 | self.response_obj = {'success': True, 'code': 0, 'data': None, 'msg': ''} 18 | 19 | def post(self): 20 | image_data = request.files.get('image') 21 | if image_data: 22 | base_filename = image_data.filename 23 | full_name = os.path.join(current_app.config['UPLOADED_IMAGES_DEST'], base_filename) 24 | filename = image_upload.resolve_conflict(current_app.config['UPLOADED_IMAGES_DEST'], 25 | base_filename) if os.path.exists(full_name) else base_filename 26 | # 保存上传文件 27 | try: 28 | image_obj = image_upload.save(image_data, name=filename) 29 | except UploadNotAllowed as e: 30 | self.response_obj['code'] = 1 31 | self.response_obj['success'] = False 32 | return jsonify_with_args(self.response_obj, 415) 33 | # 获取上传图片的URL 34 | img_url = image_upload.url(image_obj) 35 | data = {'url': img_url} 36 | self.response_obj['data'] = data 37 | return jsonify(self.response_obj) 38 | 39 | else: 40 | self.response_obj['code'] = 1 41 | self.response_obj['success'] = False 42 | return jsonify_with_args(self.response_obj, 400) 43 | -------------------------------------------------------------------------------- /back/api_1_0/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/6/27 22:34 4 | from flask import make_response, jsonify 5 | 6 | 7 | def jsonify_with_args(data, code=200, *args): 8 | """ 9 | 返回json数据同时修改返回状态码,etc 10 | :param data: 11 | :param code: 12 | :param args: 13 | :return: 14 | """ 15 | assert isinstance(data, dict) 16 | return make_response(jsonify(data), code, *args) 17 | -------------------------------------------------------------------------------- /back/celery_components/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/6/19 9:45 4 | """ 5 | 分离出来的原因:https://www.v2ex.com/t/471458 6 | """ 7 | from celery import Celery 8 | from back.celery_components import celery_config 9 | 10 | 11 | celery_app = Celery(__name__, broker=celery_config.broker_url, backend=celery_config.result_backend) 12 | 13 | 14 | def init_celery(app=None): 15 | """ 16 | 初始化celery 17 | 参见:http://docs.jinkan.org/docs/flask/patterns/celery.html 18 | :return: 19 | """ 20 | 21 | celery_app.config_from_object('back.celery_components.celery_config') 22 | TaskBase = celery_app.Task 23 | 24 | class ContextTask(TaskBase): 25 | abstract = True 26 | 27 | def __call__(self, *args, **kwargs): 28 | with app.app_context(): 29 | return TaskBase.__call__(self, *args, **kwargs) 30 | 31 | celery_app.Task = ContextTask 32 | return celery_app 33 | -------------------------------------------------------------------------------- /back/celery_components/celery_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/6/19 9:46 4 | """ 5 | Celery 从 4.0 开始启用新的小写配置名,某些配置被新的名称替换。 6 | 虽然旧的大写配置仍然支持,但如果你打算使用小写配置名,或是打算在未来进行迁移, 7 | 这里的配置加载方式就会失效,因为 Flask 在从文件或对象导入配置时只会导入大写形式的配置变量。 8 | """ 9 | from kombu import Exchange, Queue 10 | broker_url = 'redis://localhost:6379/1' 11 | result_backend = 'redis://localhost:6379/1' 12 | task_serializer = 'json' 13 | result_serializer = 'json' 14 | accept_content = ['application/json'] 15 | timezone = 'Asia/Shanghai' 16 | enable_utc = True 17 | celery_imports = ("tasks",) 18 | broker_transport_options = {'visibility_timeout': 43200} 19 | # 创建exchange 20 | # see also: https://www.cnblogs.com/julyluo/p/6265775.html 21 | default_exchange = Exchange('default', type='direct') 22 | mail_exchange = Exchange('mail', type='direct') 23 | 24 | task_queues = ( 25 | Queue('default', default_exchange, routing_key='default'), 26 | Queue('mail', mail_exchange, routing_key="mail") 27 | ) 28 | 29 | task_default_queue = "default" 30 | # see also:https://www.jianshu.com/p/b3d2c5871bec 31 | # task_routes = { 32 | # 'tasks.mem_test_long_task': { 33 | # 'queue': 'mail', 34 | # 'routing_key': 'mail', 35 | # }, 36 | # 'tasks.mem_bench_long_task': { 37 | # 'queue': 'mail', 38 | # 'routing_key': 'mail', 39 | # }, 40 | # } 41 | -------------------------------------------------------------------------------- /back/celery_components/tasks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/6/18 16:24 4 | """ 5 | 存放需要异步执行的耗时任务 6 | https://segmentfault.com/a/1190000008022050 7 | """ 8 | 9 | import random 10 | import time 11 | 12 | # from celery.signals import task_postrun 13 | from celery.utils.log import get_task_logger 14 | 15 | from back.controller.authctrl import PostUserCtrl 16 | 17 | from back.celery_components import celery_app 18 | 19 | auth_ctrl = PostUserCtrl() 20 | # https://blog.csdn.net/qq_27437781/article/details/83507110 21 | logger = get_task_logger(__name__) 22 | 23 | 24 | @celery_app.task(bind=True) 25 | def long_task(self): 26 | """Background task that runs a long function with progress reports.""" 27 | verb = ['Starting up', 'Booting', 'Repairing', 'Loading', 'Checking'] 28 | adjective = ['master', 'radiant', 'silent', 'harmonic', 'fast'] 29 | noun = ['solar array', 'particle reshaper', 'cosmic ray', 'orbiter', 'bit'] 30 | message = '' 31 | total = random.randint(10, 50) 32 | for i in range(total): 33 | if not message or random.random() < 0.25: 34 | message = '{0} {1} {2}...'.format(random.choice(verb), 35 | random.choice(adjective), 36 | random.choice(noun)) 37 | self.update_state(state='PROGRESS', 38 | meta={'current': i, 'total': total, 39 | 'status': message}) 40 | time.sleep(1) 41 | return {'current': 100, 'total': 100, 'status': 'Task completed!', 42 | 'result': 42} 43 | 44 | 45 | # 添加了 bind=True 参数。这个参数告诉 Celery 发送一个 self 参数到我的函数,我能够使用它(self)来记录状态更新。 46 | @celery_app.task(bind=True) 47 | def send_reset_password_mail_long_task(self, req_ip, email): 48 | """ 49 | 发送验证邮件 50 | :param self: 51 | :param req_ip: 52 | :param email: 53 | :return: 54 | """ 55 | ret = auth_ctrl.reset_pw_action(req_ip, email) 56 | return ret 57 | 58 | 59 | @celery_app.task 60 | def log(message): 61 | """Print some log messages""" 62 | logger.debug(message) 63 | logger.info(message) 64 | logger.warning(message) 65 | logger.error(message) 66 | logger.critical(message) 67 | 68 | 69 | # @task_postrun.connect 70 | # def close_session(*args, **kwargs): 71 | # # Flask SQLAlchemy will automatically create new sessions for you from 72 | # # a scoped session factory, given that we are maintaining the same app 73 | # # context, this ensures tasks have a fresh session (e.g. session errors 74 | # # won't propagate across tasks) 75 | # db.session.remove() 76 | 77 | 78 | @celery_app.task() 79 | def log_it(num1, num2): 80 | msg = num1 + num2 81 | print(msg) 82 | logger.debug("in log_test()") 83 | return msg 84 | 85 | 86 | if __name__ == '__main__': 87 | task = log_it.apply_async(args=[10, 20], countdown=10) 88 | -------------------------------------------------------------------------------- /back/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | http://www.pythondoc.com/flask/config.html#id6 5 | """ 6 | import os 7 | 8 | basedir = os.path.abspath(os.path.dirname(__file__)) 9 | 10 | 11 | class Config: 12 | SECRET_KEY = os.getenv('SECRET_KEY') or 'MPk2WlUArcLeeU_iohzT' 13 | 14 | ''' 15 | # 旧版本 16 | import random 17 | import string 18 | ''.join(random.choices(string.ascii_letters + string.digits, k=15)) 19 | # py3.6+ 20 | import secrets 21 | secrets.token_urlsafe(nbytes=15) 22 | ''' 23 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True 24 | SQLALCHEMY_TRACK_MODIFICATIONS = False 25 | SQLALCHEMY_RECORD_QUERIES = True 26 | # 分页 27 | FLASKY_POSTS_PER_PAGE = 10 28 | # 上传图片 29 | UPLOADED_IMAGES_DEST = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static/images') 30 | MAX_CONTENT_LENGTH = 10 * 1024 * 1024 31 | # 邮件服务器设置 32 | MAIL_SERVER = os.getenv('MAIL_SERVER') 33 | # 163不支持STARTTLS 34 | MAIL_PORT = 465 35 | MAIL_USE_SSL = True 36 | MAIL_USERNAME = os.getenv('MAIL_USERNAME') 37 | MAIL_PASSWORD = os.getenv('MAIL_PASSWORD') 38 | MAIL_DEFAULT_SENDER = ('别院牧志', os.getenv('MAIL_USERNAME')) 39 | # redis 配置 40 | # REDIS_URL = "redis://:password@localhost:6379/0" 41 | REDIS_URL = "redis://localhost:6379/0" 42 | 43 | def __init__(self): 44 | pass 45 | 46 | @staticmethod 47 | def init_app(app): 48 | pass 49 | 50 | 51 | class MySQLConfig: 52 | MYSQL_USERNAME = os.getenv('MYSQL_USER') 53 | MYSQL_PASSWORD = os.getenv('MYSQL_PASSWORD') 54 | MYSQL_DB = os.getenv('MYSQL_DB') 55 | MYSQL_HOST = 'localhost:3306' 56 | MYSQL_CHARSET = 'utf8mb4' # 为了支持 emoji 显示,需要设置为 utf8mb4 编码 57 | 58 | 59 | class DevelopmentConfig(Config): 60 | DEBUG = True 61 | database = MySQLConfig.MYSQL_DB or 'iyblog_dev' 62 | SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{MySQLConfig.MYSQL_USERNAME}:{MySQLConfig.MYSQL_PASSWORD}' \ 63 | f'@{MySQLConfig.MYSQL_HOST}/{database}?charset={MySQLConfig.MYSQL_CHARSET}' 64 | 65 | 66 | class TestingConfig(Config): 67 | TESTING = True 68 | database = MySQLConfig.MYSQL_DB or 'iyblog_test' 69 | SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{MySQLConfig.MYSQL_USERNAME}:{MySQLConfig.MYSQL_PASSWORD}' \ 70 | f'@{MySQLConfig.MYSQL_HOST}/{database}?charset={MySQLConfig.MYSQL_CHARSET}' 71 | 72 | 73 | class ProductionConfig(Config): 74 | database = MySQLConfig.MYSQL_DB or 'iyblog_product' 75 | SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{MySQLConfig.MYSQL_USERNAME}:{MySQLConfig.MYSQL_PASSWORD}' \ 76 | f'@{MySQLConfig.MYSQL_HOST}/{database}?charset={MySQLConfig.MYSQL_CHARSET}' 77 | 78 | 79 | config = { 80 | 'development': DevelopmentConfig, 81 | 'testing': TestingConfig, 82 | 'production': ProductionConfig, 83 | 'default': DevelopmentConfig 84 | } 85 | -------------------------------------------------------------------------------- /back/controller/archives.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/6/30 8:38 4 | 5 | """ 6 | 归档: 7 | 先找出年份最大和最小值 >>> 按年划分 8 | 再找出每一年的按月划分 >> range(0,13) 9 | """ 10 | from sqlalchemy import func 11 | 12 | from back.controller import MakeQuery 13 | from back.models import db, Article 14 | from back.utils.date import DateTime 15 | 16 | date_maker = DateTime() 17 | query_maker = MakeQuery() 18 | 19 | 20 | class GetArchiveCtrl: 21 | @staticmethod 22 | def last_create_time(): 23 | """ 24 | 最晚创建时间 25 | :return: 26 | """ 27 | last_time = db.session.query(func.max(Article.create_date).label('max_time')).one().max_time 28 | year = date_maker.year(last_time) 29 | return year 30 | 31 | @staticmethod 32 | def first_create_time(): 33 | """ 34 | 最早创建时间 35 | :return: 36 | """ 37 | first_time = db.session.query(func.min(Article.create_date).label('min_time')).one().min_time 38 | first_year = date_maker.year(first_time) 39 | return first_year 40 | 41 | def extract_post_with_year_and_month(self, order_desc=True): 42 | """ 43 | 按照年、月筛选博文 44 | :param order_desc: bool,排序方式,默认按照时间倒序 45 | :return: list,文章信息列表 46 | """ 47 | first = self.first_create_time() 48 | last = self.last_create_time() 49 | post_info_by_ct = [] 50 | if all([first, last]): 51 | for year in range(first, last + 1): 52 | year_data = self.extract_post_with_month(year) 53 | post_info_by_ct.extend(year_data) 54 | return post_info_by_ct[::-1] if order_desc else post_info_by_ct 55 | 56 | @staticmethod 57 | def extract_post_with_month(year): 58 | """ 59 | 归档是否有必要也做一个新页面(按说没有) 60 | :param year: 61 | :return: 62 | """ 63 | same_year_data = [] 64 | # 一年12个月 65 | for mon in range(1, 13): 66 | ym = dict() 67 | data = query_maker.query_post_year_month(year, mon) 68 | post_obj = data.all() 69 | same_month_posts = [] 70 | if post_obj: 71 | for post in post_obj: 72 | data_item = dict() 73 | data_item['post_id'] = post.post_id 74 | str_date = '' 75 | create_date = post.create_date 76 | if create_date: 77 | str_date = date_maker.make_strftime(create_date) 78 | data_item['create_date'] = str_date 79 | same_month_posts.append(data_item) 80 | # 没有博文,跳出本次循环 81 | else: 82 | continue 83 | ym['year'] = year 84 | ym['month'] = mon 85 | ym['posts'] = same_month_posts 86 | ym['counts'] = len(same_month_posts) 87 | same_year_data.append(ym) 88 | return same_year_data 89 | -------------------------------------------------------------------------------- /back/controller/categories.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/6/29 22:28 4 | from back.controller import QueryComponent 5 | from back.models import Article, Category 6 | from back.models import db 7 | 8 | """ 9 | 分类 按照类别分组 10 | """ 11 | 12 | 13 | class GetCategoryCtrl: 14 | @staticmethod 15 | def query_all_category(): 16 | return Category.query.all() 17 | 18 | @staticmethod 19 | def posts_for_category(category_id): 20 | """ 21 | 根据分类 id 查找文章 22 | :param category_id: 23 | :return: 24 | """ 25 | category_data = QueryComponent.category_for_post(category_id) 26 | post_data = Article.query.filter_by(category_id=category_id).all() 27 | # 如果需要返回文章详情,此处扩写 28 | articles = [data.post_id for data in post_data] 29 | category_data['articles'] = articles 30 | category_data['article_counts'] = len(post_data) 31 | return category_data 32 | 33 | def show_categories(self): 34 | """ 35 | 显示所有的分类信息 36 | :return: 37 | """ 38 | categories_data = Category.query.order_by(Category.id).all() 39 | all_data = [] 40 | for category in categories_data: 41 | category_item = {} 42 | # hint:查询之后拿结果直接A.b 而不是 A[b] 43 | category_id = category.id 44 | articles = self.posts_for_category(category_id) 45 | category_item['id'] = category_id 46 | category_item['article_counts'] = articles['article_counts'] 47 | category_item['articles'] = articles 48 | category_item['description'] = category.description 49 | category_item['categoryname'] = category.category_name 50 | all_data.append(category_item) 51 | return all_data 52 | 53 | 54 | # POST 55 | class PostCategoryCtrl: 56 | 57 | @staticmethod 58 | def new_or_query_category(category_name, description=''): 59 | """ 60 | 如果已存在,则查找id,否则新建分类 61 | :param category_name: 62 | :param description: 63 | :return: 64 | """ 65 | assert category_name 66 | already_exist_category = {category.category_name: category.id for category in 67 | GetCategoryCtrl.query_all_category()} 68 | if already_exist_category and category_name in already_exist_category.keys(): 69 | category_id = already_exist_category[category_name] 70 | else: 71 | category = Category(category_name=category_name, description=description) 72 | db.session.add(category) 73 | db.session.commit() 74 | category_id = category.id 75 | return category_id 76 | -------------------------------------------------------------------------------- /back/docs/separate_frontend_and_backend.md: -------------------------------------------------------------------------------- 1 | # 前后端分离技术 2 | 3 | - [基于Vue与flask的前后端分离开发](https://github.com/Mcbai/Blog/issues/5) 4 | - [Web开发实战:聊一聊前后端分离架构](https://www.jianshu.com/p/9e17fe1de0d4) 5 | - [使用vue+flask做全栈开发的全过程(实现前后端分离)](https://www.cnblogs.com/yingqml/p/7205147.html) 6 | - [Flask & Vue 构建前后端分离的应用](https://www.cnblogs.com/brifuture/p/10186844.html) ** 7 | - [Python构建RESTful网络服务[Flask篇:用Flask+Vue.js打造全栈单页面应用]](https://zhuanlan.zhihu.com/p/58609388) ** 8 | - [RESTful API的前后端分离实现](http://geocld.github.io/2016/08/27/restful-api/) 9 | 10 | - [Developing a Single Page App with Flask and Vue.js](https://testdriven.io/blog/developing-a-single-page-app-with-flask-and-vuejs/) 11 | - [中文版](https://juejin.im/post/5c1f7289f265da612e28a214) 12 | - [Single Page Apps with Vue.js and Flask](https://stackabuse.com/single-page-apps-with-vue-js-and-flask-setting-up-vue-js/) 13 | 14 | # RESTful 15 | - [用 Flask 来写个轻博客](https://blog.csdn.net/jmilk/article/category/6518106/1?) 16 | - [理解RESTful架构](http://www.ruanyifeng.com/blog/2011/09/restful.html) 17 | - [RESTful API 最佳实践](http://www.ruanyifeng.com/blog/2018/10/restful-api-best-practices.html) 18 | - [RESTful API 设计指南](http://www.ruanyifeng.com/blog/2014/05/restful_api.html) 19 | - [RESETful API 设计规范](https://godruoyi.com/posts/the-resetful-api-design-specification) -------------------------------------------------------------------------------- /back/exception.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class ValidationError(ValueError): 5 | pass 6 | -------------------------------------------------------------------------------- /back/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/back/logs/.gitkeep -------------------------------------------------------------------------------- /back/main/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import Blueprint 5 | 6 | main_bp = Blueprint('main', __name__) 7 | 8 | from . import views, errors 9 | -------------------------------------------------------------------------------- /back/main/errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from flask import request, jsonify 4 | from flask import render_template 5 | 6 | from . import main_bp 7 | 8 | 9 | @main_bp.app_errorhandler(404) 10 | def page_not_found(e): 11 | response = jsonify({'error': 'not found'}) 12 | response.status_code = 404 13 | return response 14 | -------------------------------------------------------------------------------- /back/main/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from flask import abort, current_app, render_template, jsonify 5 | from jinja2 import TemplateNotFound 6 | 7 | from . import main_bp 8 | 9 | 10 | @main_bp.route('/hello') 11 | def hello_world(): 12 | return 'Hello World!' 13 | 14 | 15 | @main_bp.route('/favicon.ico') 16 | def favicon(): 17 | return current_app.send_static_file('favicon.ico') 18 | 19 | 20 | @main_bp.route('/') 21 | @main_bp.route('/index/') 22 | def index(): 23 | try: 24 | return 'This is Index Page.' 25 | # return render_template('index.html') 26 | except TemplateNotFound: 27 | abort(404) 28 | 29 | 30 | @main_bp.route('/ping') 31 | def ping_pong(): 32 | return jsonify('pong!') 33 | -------------------------------------------------------------------------------- /back/mains.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/6/29 8:01 4 | 5 | 6 | from flask import Blueprint 7 | from flask import abort, current_app, jsonify 8 | from jinja2 import TemplateNotFound 9 | 10 | bp = Blueprint('main', __name__) 11 | 12 | 13 | @bp.route('/favicon.ico') 14 | def favicon(): 15 | return current_app.send_static_file('favicon.ico') 16 | 17 | 18 | @bp.route('/') 19 | @bp.route('/index/') 20 | def index(): 21 | try: 22 | return 'This is Index Page.' 23 | except TemplateNotFound: 24 | abort(404) 25 | 26 | 27 | @bp.route('/ping') 28 | def ping_pong(): 29 | return jsonify('pong!') 30 | 31 | 32 | @bp.app_errorhandler(404) 33 | def page_not_found(e): 34 | response = jsonify({'error': 'not found'}) 35 | response.status_code = 404 36 | return response 37 | 38 | 39 | @bp.app_errorhandler(405) 40 | def method_not_allowed(e): 41 | response = jsonify({'error': 'method not allowed'}) 42 | response.status_code = 405 43 | return response 44 | -------------------------------------------------------------------------------- /back/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /back/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /back/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy import pool 8 | 9 | from alembic import context 10 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | fileConfig(config.config_file_name) 18 | logger = logging.getLogger('alembic.env') 19 | 20 | # add your model's MetaData object here 21 | # for 'autogenerate' support 22 | # from myapp import mymodel 23 | # target_metadata = mymodel.Base.metadata 24 | from flask import current_app 25 | config.set_main_option( 26 | 'sqlalchemy.url', current_app.config.get( 27 | 'SQLALCHEMY_DATABASE_URI').replace('%', '%%')) 28 | target_metadata = current_app.extensions['migrate'].db.metadata 29 | 30 | # other values from the config, defined by the needs of env.py, 31 | # can be acquired: 32 | # my_important_option = config.get_main_option("my_important_option") 33 | # ... etc. 34 | 35 | 36 | def run_migrations_offline(): 37 | """Run migrations in 'offline' mode. 38 | 39 | This configures the context with just a URL 40 | and not an Engine, though an Engine is acceptable 41 | here as well. By skipping the Engine creation 42 | we don't even need a DBAPI to be available. 43 | 44 | Calls to context.execute() here emit the given string to the 45 | script output. 46 | 47 | """ 48 | url = config.get_main_option("sqlalchemy.url") 49 | context.configure( 50 | url=url, target_metadata=target_metadata, literal_binds=True 51 | ) 52 | 53 | with context.begin_transaction(): 54 | context.run_migrations() 55 | 56 | 57 | def run_migrations_online(): 58 | """Run migrations in 'online' mode. 59 | 60 | In this scenario we need to create an Engine 61 | and associate a connection with the context. 62 | 63 | """ 64 | 65 | # this callback is used to prevent an auto-migration from being generated 66 | # when there are no changes to the schema 67 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 68 | def process_revision_directives(context, revision, directives): 69 | if getattr(config.cmd_opts, 'autogenerate', False): 70 | script = directives[0] 71 | if script.upgrade_ops.is_empty(): 72 | directives[:] = [] 73 | logger.info('No changes in schema detected.') 74 | 75 | connectable = engine_from_config( 76 | config.get_section(config.config_ini_section), 77 | prefix='sqlalchemy.', 78 | poolclass=pool.NullPool, 79 | ) 80 | 81 | with connectable.connect() as connection: 82 | context.configure( 83 | connection=connection, 84 | target_metadata=target_metadata, 85 | process_revision_directives=process_revision_directives, 86 | **current_app.extensions['migrate'].configure_args 87 | ) 88 | 89 | with context.begin_transaction(): 90 | context.run_migrations() 91 | 92 | 93 | if context.is_offline_mode(): 94 | run_migrations_offline() 95 | else: 96 | run_migrations_online() 97 | -------------------------------------------------------------------------------- /back/migrations/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 | -------------------------------------------------------------------------------- /back/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.1.0 2 | aniso8601==7.0.0 3 | backcall==0.1.0 4 | blinker==1.4 5 | Click==7.0 6 | decorator==4.4.0 7 | Flask==1.0.3 8 | Flask-Cors==3.0.8 9 | Flask-HTTPAuth==3.3.0 10 | Flask-Mail==0.9.1 11 | Flask-Migrate==2.5.2 12 | flask-redis==0.4.0 13 | Flask-RESTful==0.3.7 14 | Flask-SQLAlchemy==2.4.0 15 | Flask-Uploads==0.2.1 16 | gevent==1.4.0 17 | greenlet==0.4.15 18 | gunicorn==19.9.0 19 | ipython==7.6.1 20 | ipython-genutils==0.2.0 21 | itsdangerous==1.1.0 22 | jedi==0.14.1 23 | Jinja2==2.10.1 24 | Mako==1.1.0 25 | MarkupSafe==1.1.1 26 | parso==0.5.1 27 | passlib==1.7.1 28 | pexpect==4.7.0 29 | pickleshare==0.7.5 30 | prompt-toolkit==2.0.9 31 | ptyprocess==0.6.0 32 | Pygments==2.4.2 33 | PyMySQL==0.9.3 34 | python-dateutil==2.8.0 35 | python-dotenv==0.10.3 36 | python-editor==1.0.4 37 | pytz==2019.1 38 | redis==3.3.8 39 | six==1.12.0 40 | SQLAlchemy==1.3.4 41 | traitlets==4.3.2 42 | wcwidth==0.1.7 43 | Werkzeug==0.15.4 44 | -------------------------------------------------------------------------------- /back/setting.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 该文件设置所有全局变量 4 | """ 5 | import os 6 | 7 | LIMIT_NEW_POST_COUNT = 5 8 | LIMIT_HOT_POST_COUNT = 5 9 | LIMIT_HOT_TAG_COUNT = 10 10 | INITIAL_VIEW_COUNTS = 0 11 | INITIAL_POST_IDENTIFIER = 19930126 12 | # 临时密码有效期 13 | TEMPORARY_PW_EXPIRE_MINUTES = 10 # min 14 | TEMPORARY_PW_EXPIRE_SECONDS = 60 * TEMPORARY_PW_EXPIRE_MINUTES # second 15 | # log 16 | APP_LOG_FP = 'logs/app.log' 17 | # flask 18 | FLASK_HOST = '0.0.0.0' 19 | FLASK_PORT = '5000' 20 | HOST_IP = os.getenv('FLASK_HOST', FLASK_HOST) 21 | # baidu_trans 22 | BD_TRANS_API_URL = '/api/trans/vip/translate' 23 | # regex 24 | RE_SYMBOL = r'[\,\,\.\。\?\?\:\:\'\‘\’\"\“\”\、\/\*\&\$\#\@\!\(\(\)\)\[\【\]\】\{\}\|\-"]' 25 | RE_EXCLUDE_CHINESE = r'[A-Za-z0-9\!\%\[\]\,\。]' 26 | -------------------------------------------------------------------------------- /back/utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/7/16 10:04 4 | import hashlib 5 | 6 | 7 | def md5_encrypt(origin_str): 8 | """ 9 | md5加密 10 | :param origin_str: 11 | :return: 12 | """ 13 | _m1 = hashlib.md5() 14 | # 此处必须声明encode,若写法为hl.update(str) 报错为: Unicode-objects must be encoded before hashing 15 | _m1.update(origin_str.encode(encoding='utf-8')) 16 | _rtn = _m1.hexdigest() 17 | return _rtn 18 | -------------------------------------------------------------------------------- /back/utils/captcha.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/8/31 0:50 4 | """用于生成随机验证码""" 5 | import string 6 | import random 7 | 8 | 9 | class CaptchaCreator: 10 | 11 | @staticmethod 12 | def random_seq(choice_seq, count=6, repeatable=True): 13 | """ 14 | 生成随机数列表 15 | :param choice_seq:list, 16 | :param count: int,随机数长度 17 | :param repeatable: bool,是否可重复 18 | :return: list 19 | """ 20 | if repeatable: 21 | return [random.choice(choice_seq) for _ in range(count)] 22 | return random.sample(choice_seq, count) 23 | 24 | def shuffle(self): 25 | digits = self.random_seq(string.digits) 26 | random.shuffle(digits) 27 | return ''.join(digits) 28 | 29 | 30 | if __name__ == '__main__': 31 | c = CaptchaCreator() 32 | print(c.shuffle()) 33 | -------------------------------------------------------------------------------- /back/utils/date.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/6/30 9:00 4 | import datetime 5 | 6 | 7 | class DateTime: 8 | @staticmethod 9 | def is_datetime(time_obj): 10 | assert isinstance(time_obj, datetime.datetime) 11 | 12 | def make_strftime(self, time_obj): 13 | """ 14 | make to str 15 | :param time_obj: 16 | :return: 17 | """ 18 | self.is_datetime(time_obj) 19 | return time_obj.strftime('%Y-%m-%d %H:%M:%S') 20 | 21 | def year(self, time_obj): 22 | if time_obj: 23 | self.is_datetime(time_obj) 24 | return time_obj.year 25 | -------------------------------------------------------------------------------- /back/utils/flask_logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import os 3 | import sys 4 | import logging 5 | from logging.handlers import RotatingFileHandler 6 | from back import setting 7 | 8 | 9 | def register_logger(name): 10 | # 改进:http://maqiangthunder.github.io/2016/04/18/python/flask/flask%E7%B3%BB%E7%BB%9F%E6%97%A5%E5%BF%97%E5%86%99%E5%88%B0%E6%96%87%E4%BB%B6/ 11 | logger = logging.getLogger(name) 12 | dir_name, _ = os.path.split(setting.APP_LOG_FP) 13 | if not os.path.exists(dir_name): 14 | os.mkdir(dir_name) 15 | logger.setLevel(logging.INFO) 16 | # 指定logger输出格式 17 | formatter = logging.Formatter('%(asctime)s - %(levelname)-2s[%(name)s]: %(message)s') 18 | # 文件日志 19 | file_handler = RotatingFileHandler(setting.APP_LOG_FP, maxBytes=10 * 1024 * 1024, backupCount=7) 20 | file_handler.setFormatter(formatter) # 可以通过setFormatter指定输出格式 21 | file_handler.setLevel(logging.INFO) 22 | 23 | logger.addHandler(file_handler) 24 | # 控制台日志 25 | console_handler = logging.StreamHandler(sys.stdout) 26 | console_handler.formatter = formatter # 也可以直接给formatter赋值 27 | # 为logger添加的日志处理器,可以自定义日志处理器让其输出到其他地方 28 | # logger.addHandler(file_handler) 29 | logger.addHandler(console_handler) 30 | # 指定日志的最低输出级别,默认为WARN级别 31 | # logger.setLevel(logging.DEBUG) 32 | return logger 33 | -------------------------------------------------------------------------------- /back/utils/mail.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/8/31 19:06 4 | 5 | from flask_mail import Mail, Message 6 | 7 | mail = Mail() 8 | 9 | 10 | class MailSender: 11 | """ 12 | 发送邮件 13 | """ 14 | @staticmethod 15 | def send_mail(subject, to, body): 16 | message = Message(subject, recipients=[to], body=body) 17 | mail.send(message) 18 | return 0 19 | -------------------------------------------------------------------------------- /back/utils/redis_util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/8/31 21:00 4 | # https://stackoverflow.com/questions/30040159/why-use-flasks-redis-extension 5 | from flask_redis import FlaskRedis 6 | 7 | from redis import StrictRedis 8 | 9 | redis_store = FlaskRedis.from_custom_provider(StrictRedis) 10 | -------------------------------------------------------------------------------- /celery_worker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/7/8 18:30 4 | """ 5 | 调度任务 6 | cd CURRENT_APP_PATH 7 | celery -A celery_worker:celery worker --loglevel=DEBUG -f /var/log/celery/celery.log 8 | 主要参考: 9 | https://github.com/nebularazer/flask-celery-example 10 | https://github.com/chiqj/flask-with-celery-example 11 | https://github.com/zenyui/celery-flask-factory 12 | """ 13 | import os 14 | 15 | from celery.schedules import crontab 16 | 17 | from back import create_app 18 | from back.celery_components import tasks 19 | from back.celery_components import init_celery 20 | 21 | flask_app = create_app(os.getenv('FLASK_CONFIG', 'default')) 22 | celery = init_celery(flask_app) 23 | 24 | 25 | @celery.on_after_configure.connect 26 | def setup_periodic_tasks(sender, **kwargs): 27 | """ 28 | 周期性任务 29 | # 添加定时任务参见:https://www.cnblogs.com/linxiyue/archive/2017/12/21/8082102.html 30 | :param sender: 31 | :param kwargs: 32 | :return: 33 | """ 34 | # sender.add_periodic_task(10.0, tasks.write_bmc_power_state_to_db, 35 | # name='write_bmc_power_state_to_db every 10s') 36 | 37 | # sender.add_periodic_task(3000.0, tasks.my_add(2, 3), name='my_add every 5min') 38 | 39 | # Calls log('Logging Stuff') every 30 seconds 40 | sender.add_periodic_task(300.0, tasks.log.s('Logging Stuff'), name='Log every 300s') 41 | # sender.add_periodic_task(30.0, tasks.send_reset_password_mail_long_task('0.0.0.0', 'emailme8@163.com'), 42 | # name='my_add every 5min') 43 | 44 | # Executes every Monday morning at 7:30 a.m. 45 | sender.add_periodic_task( 46 | crontab(hour=7, minute=30, day_of_week=1), 47 | tasks.log.s('Monday morning log!'), 48 | ) 49 | -------------------------------------------------------------------------------- /confs/nginx/conf.d/app.conf: -------------------------------------------------------------------------------- 1 | # 配置flask应用代理 2 | server { 3 | listen 80 default_server; 4 | listen [::]:80 default_server; 5 | server_name localhost; # TODO:填写域名 6 | charset utf-8; 7 | access_log /var/log/nginx/access.log; 8 | error_log /var/log/nginx/error.log; 9 | client_max_body_size 100M; 10 | location / { 11 | root /usr/local/idealyard/front/dist; # 按需修改为dist目录 12 | try_files $uri $uri/ /index.html last; 13 | index index.html index.htm; 14 | } 15 | location ~ \.(gif|jpg|jpeg|png|bmp|ico)$ { 16 | root /home/imoyao/iyblog/front/dist; 17 | expires 30d; 18 | } 19 | location /api { 20 | proxy_pass http://127.0.0.1:5000; 21 | proxy_http_version 1.1; 22 | proxy_connect_timeout 300; 23 | proxy_send_timeout 300; 24 | proxy_read_timeout 300; 25 | send_timeout 300; 26 | proxy_set_header Upgrade $http_upgrade; 27 | proxy_set_header Connection "upgrade"; 28 | proxy_set_header Host $http_host; 29 | proxy_set_header X-Scheme $scheme; 30 | proxy_set_header X-Forwarded-For $remote_addr; 31 | proxy_set_header X-Forwarded-Port $server_port; 32 | proxy_set_header X-Request-Start $msec; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /confs/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | # For more information on configuration, see: 2 | # * Official English Documentation: http://nginx.org/en/docs/ 3 | # * Official Russian Documentation: http://nginx.org/ru/docs/ 4 | user nginx; 5 | worker_processes auto; 6 | error_log /var/log/nginx/error.log; 7 | pid /run/nginx.pid; 8 | 9 | # Load dynamic modules. See /usr/share/nginx/README.dynamic. 10 | include /usr/share/nginx/modules/*.conf; 11 | 12 | events { 13 | worker_connections 1024; 14 | } 15 | 16 | http { 17 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 18 | '$status $body_bytes_sent "$http_referer" ' 19 | '"$http_user_agent" "$http_x_forwarded_for"'; 20 | 21 | access_log /var/log/nginx/access.log main; 22 | 23 | sendfile on; 24 | tcp_nopush on; 25 | tcp_nodelay on; 26 | keepalive_timeout 65; 27 | types_hash_max_size 2048; 28 | 29 | include /etc/nginx/mime.types; 30 | default_type application/octet-stream; 31 | 32 | # Load modular configuration files from the /etc/nginx/conf.d directory. 33 | # See http://nginx.org/en/docs/ngx_core_module.html#include 34 | # for more information. 35 | include /etc/nginx/conf.d/*.conf; 36 | 37 | server { 38 | listen 81 default_server; # 注意此处默认80被占用的话可以直接使用其他端口 39 | listen [::]:81 default_server; 40 | server_name _; 41 | root /usr/share/nginx/html; 42 | 43 | # Load configuration files for the default server block. 44 | include /etc/nginx/default.d/*.conf; 45 | 46 | location / { 47 | } 48 | 49 | error_page 404 /404.html; 50 | location = /40x.html { 51 | } 52 | 53 | error_page 500 502 503 504 /50x.html; 54 | location = /50x.html { 55 | } 56 | } 57 | 58 | # Settings for a TLS enabled server. 59 | # 60 | # server { 61 | # listen 443 ssl http2 default_server; 62 | # listen [::]:443 ssl http2 default_server; 63 | # server_name _; 64 | # root /usr/share/nginx/html; 65 | # 66 | # ssl_certificate "/etc/pki/nginx/server.crt"; 67 | # ssl_certificate_key "/etc/pki/nginx/private/server.key"; 68 | # ssl_session_cache shared:SSL:1m; 69 | # ssl_session_timeout 10m; 70 | # ssl_ciphers HIGH:!aNULL:!MD5; 71 | # ssl_prefer_server_ciphers on; 72 | # 73 | # # Load configuration files for the default server block. 74 | # include /etc/nginx/default.d/*.conf; 75 | # 76 | # location / { 77 | # } 78 | # 79 | # error_page 404 /404.html; 80 | # location = /40x.html { 81 | # } 82 | # 83 | # error_page 500 502 503 504 /50x.html; 84 | # location = /50x.html { 85 | # } 86 | # } 87 | 88 | } 89 | 90 | -------------------------------------------------------------------------------- /confs/nginx/readme.md: -------------------------------------------------------------------------------- 1 | 此处配置用于nginx实现反向代理 2 | 默认路径:`/etc/nginx` 3 | -------------------------------------------------------------------------------- /confs/redis/redisd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # chkconfig: 2345 90 10 3 | # Simple Redis init.d script conceived to work on Linux systems 4 | # as it does use of the /proc filesystem. 5 | 6 | ### BEGIN INIT INFO 7 | # Provides: redis_6379 8 | # Default-Start: 2 3 4 5 9 | # Default-Stop: 0 1 6 10 | # Short-Description: Redis data structure server 11 | # Description: Redis data structure server. See https://redis.io 12 | ### END INIT INFO 13 | 14 | REDISPORT=6379 15 | EXEC=/usr/local/bin/redis-server 16 | CLIEXEC=/usr/local/bin/redis-cli 17 | 18 | PIDFILE=/var/run/redis_${REDISPORT}.pid 19 | CONF="/etc/redis/${REDISPORT}.conf" 20 | 21 | case "$1" in 22 | start) 23 | if [ -f $PIDFILE ] 24 | then 25 | echo "$PIDFILE exists, process is already running or crashed" 26 | else 27 | echo "Starting Redis server..." 28 | $EXEC $CONF 29 | fi 30 | ;; 31 | stop) 32 | if [ ! -f $PIDFILE ] 33 | then 34 | echo "$PIDFILE does not exist, process is not running" 35 | else 36 | PID=$(cat $PIDFILE) 37 | echo "Stopping ..." 38 | $CLIEXEC -p $REDISPORT shutdown 39 | while [ -x /proc/${PID} ] 40 | do 41 | echo "Waiting for Redis to shutdown ..." 42 | sleep 1 43 | done 44 | echo "Redis stopped" 45 | fi 46 | ;; 47 | *) 48 | echo "Please use start or stop as first argument" 49 | ;; 50 | esac 51 | -------------------------------------------------------------------------------- /confs/supervisord.d/app.ini: -------------------------------------------------------------------------------- 1 | [program:app] 2 | command=/root/.virtualenvs/iyblog-_wyXyExX/bin/gunicorn -c gun.py runserver:app 3 | ;项目根目录路径 4 | directory=/root/iyblog 5 | startsecs=0 6 | stopwaitsecs=0 7 | autostart=true 8 | autorestart=true 9 | stopasgroup=true 10 | killasgroup=true 11 | stderr_logfile=/var/log/app/app_err.log 12 | stdout_logfile=/var/log/app/app_out.log 13 | -------------------------------------------------------------------------------- /confs/supervisord.d/celery_beat.ini: -------------------------------------------------------------------------------- 1 | [program:celery_beat] 2 | command=/root/.virtualenvs/iyblog-_wyXyExX/bin/celery -A celery_worker:celery beat -l info 3 | directory=/root/iyblog 4 | startsecs=0 5 | stopwaitsecs=0 6 | autostart=true 7 | autorestart=true 8 | stderr_logfile=/var/log/celery/beat_err.log 9 | stdout_logfile=/var/log/celery/beat_out.log 10 | stopasgroup=true 11 | killasgroup=true 12 | -------------------------------------------------------------------------------- /confs/supervisord.d/celery_work.ini: -------------------------------------------------------------------------------- 1 | [program:celery_work] 2 | command=/root/.virtualenvs/iyblog-_wyXyExX/bin/celery -A celery_worker:celery worker -Q default,mail -c 10 -l info 3 | directory=/root/iyblog 4 | startsecs=0 5 | stopwaitsecs=0 6 | autostart=true 7 | autorestart=true 8 | stderr_logfile=/var/log/celery/worker_err.log 9 | stdout_logfile=/var/log/celery/worker_out.log 10 | stopasgroup=true 11 | killasgroup=true 12 | -------------------------------------------------------------------------------- /confs/supervisord.d/readme.md: -------------------------------------------------------------------------------- 1 | 2 | 此处配置用于管理web应用和celery任务 3 | 默认路径:`/etc/supervisord.d` 4 | 关于环境变量的设置请参阅此[问题](https://stackoverflow.com/questions/12900402/supervisor-and-environment-variables) -------------------------------------------------------------------------------- /document/src/idealyard-qq-group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/document/src/idealyard-qq-group.png -------------------------------------------------------------------------------- /document/src/img_20190910153859.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/document/src/img_20190910153859.jpg -------------------------------------------------------------------------------- /document/src/overview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/document/src/overview.gif -------------------------------------------------------------------------------- /document/src/overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/document/src/overview.jpg -------------------------------------------------------------------------------- /document/src/reset_password.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/document/src/reset_password.jpg -------------------------------------------------------------------------------- /document/src/tags.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/document/src/tags.jpg -------------------------------------------------------------------------------- /dot.env: -------------------------------------------------------------------------------- 1 | # 用来存放项目中相关敏感信息 2 | 3 | # 百度翻译API 4 | # 你的appid 5 | BD_APP_ID = '' 6 | # 你的密钥 7 | BD_SECRET_KEY = '' 8 | 9 | # mail 10 | MAIL_SERVER = '' 11 | MAIL_USERNAME = '' 12 | MAIL_PASSWORD = '' 13 | MYSQL_USER = '' 14 | MYSQL_PASSWORD = '' 15 | 16 | # 用来保存 flask 相关的环境变量 17 | 18 | FLASK_APP = runserver 19 | # 开启DEBUG模式 **注意**:生产模式下必须关闭 20 | FLASK_DEBUG = True 21 | # 配置工作模式(此处默认开发模式) 22 | FLASK_ENV = 'development' 23 | # 使用的配置环境(默认使用生产配置) 24 | FLASK_CONFIG = 'default' -------------------------------------------------------------------------------- /front/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /front/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /front/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | /test/unit/coverage/ 8 | /test/e2e/reports/ 9 | selenium-debug.log 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | -------------------------------------------------------------------------------- /front/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /front/README.md: -------------------------------------------------------------------------------- 1 | ## 学习 2 | [Vue 2.0 的建议学习顺序](https://zhuanlan.zhihu.com/p/23134551) 3 | [ECMAScript 6 入门](http://es6.ruanyifeng.com/) 4 | 5 | ### 登录 6 | https://segmentfault.com/a/1190000015201803 7 | https://www.cnblogs.com/herozhou/p/7469667.html 8 | https://www.cnblogs.com/FarmanKKK/p/8136483.html 9 | https://www.jianshu.com/p/1210a281b40f 10 | -------------------------------------------------------------------------------- /front/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /front/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /front/build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/build/logo.png -------------------------------------------------------------------------------- /front/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | // generate loader string to be used with extract text plugin 33 | function generateLoaders (loader, loaderOptions) { 34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 35 | 36 | if (loader) { 37 | loaders.push({ 38 | loader: loader + '-loader', 39 | options: Object.assign({}, loaderOptions, { 40 | sourceMap: options.sourceMap 41 | }) 42 | }) 43 | } 44 | 45 | // Extract CSS when that option is specified 46 | // (which is the case during production build) 47 | if (options.extract) { 48 | return ExtractTextPlugin.extract({ 49 | use: loaders, 50 | fallback: 'vue-style-loader' 51 | }) 52 | } else { 53 | return ['vue-style-loader'].concat(loaders) 54 | } 55 | } 56 | 57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 58 | return { 59 | css: generateLoaders(), 60 | postcss: generateLoaders(), 61 | less: generateLoaders('less'), 62 | sass: generateLoaders('sass', { indentedSyntax: true }), 63 | scss: generateLoaders('sass'), 64 | stylus: generateLoaders('stylus'), 65 | styl: generateLoaders('stylus') 66 | } 67 | } 68 | 69 | // Generate loaders for standalone style files (outside of .vue) 70 | exports.styleLoaders = function (options) { 71 | const output = [] 72 | const loaders = exports.cssLoaders(options) 73 | 74 | for (const extension in loaders) { 75 | const loader = loaders[extension] 76 | output.push({ 77 | test: new RegExp('\\.' + extension + '$'), 78 | use: loader 79 | }) 80 | } 81 | 82 | return output 83 | } 84 | 85 | exports.createNotifierCallback = () => { 86 | const notifier = require('node-notifier') 87 | 88 | return (severity, errors) => { 89 | if (severity !== 'error') return 90 | 91 | const error = errors[0] 92 | const filename = error.file && error.file.split('!').pop() 93 | 94 | notifier.notify({ 95 | title: packageConfig.name, 96 | message: severity + ': ' + error.name, 97 | subtitle: filename || '', 98 | icon: path.join(__dirname, 'logo.png') 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /front/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /front/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | 12 | 13 | module.exports = { 14 | context: path.resolve(__dirname, '../'), 15 | entry: { 16 | app: './src/main.js' 17 | }, 18 | output: { 19 | path: config.build.assetsRoot, 20 | filename: '[name].js', 21 | publicPath: process.env.NODE_ENV === 'production' 22 | ? config.build.assetsPublicPath 23 | : config.dev.assetsPublicPath 24 | }, 25 | resolve: { 26 | extensions: ['.js', '.vue', '.json'], 27 | alias: { 28 | 'vue$': 'vue/dist/vue.esm.js', 29 | '@': resolve('src'), 30 | } 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.vue$/, 36 | loader: 'vue-loader', 37 | options: vueLoaderConfig 38 | }, 39 | { 40 | test: /\.js$/, 41 | loader: 'babel-loader', 42 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 43 | }, 44 | { 45 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 46 | loader: 'url-loader', 47 | options: { 48 | limit: 10000, 49 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 50 | } 51 | }, 52 | { 53 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 54 | loader: 'url-loader', 55 | options: { 56 | limit: 10000, 57 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 58 | } 59 | }, 60 | { 61 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 62 | loader: 'url-loader', 63 | options: { 64 | limit: 10000, 65 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 66 | } 67 | } 68 | ] 69 | }, 70 | node: { 71 | // prevent webpack from injecting useless setImmediate polyfill because Vue 72 | // source contains it (although only uses it if it's native). 73 | setImmediate: false, 74 | // prevent webpack from injecting mocks to Node native modules 75 | // that does not make sense for the client 76 | dgram: 'empty', 77 | fs: 'empty', 78 | net: 'empty', 79 | tls: 'empty', 80 | child_process: 'empty' 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /front/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const path = require('path') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | 13 | const HOST = process.env.HOST 14 | const PORT = process.env.PORT && Number(process.env.PORT) 15 | 16 | const devWebpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: config.dev.devtool, 22 | 23 | // these devServer options should be customized in /config/index.js 24 | devServer: { 25 | clientLogLevel: 'warning', 26 | historyApiFallback: { 27 | rewrites: [ 28 | { from: /.*/, to: path.join(config.dev.assetsPublicPath, 'index.html') }, 29 | ], 30 | }, 31 | hot: true, 32 | contentBase: false, // since we use CopyWebpackPlugin. 33 | compress: true, 34 | host: HOST || config.dev.host, 35 | port: PORT || config.dev.port, 36 | open: config.dev.autoOpenBrowser, 37 | overlay: config.dev.errorOverlay 38 | ? { warnings: false, errors: true } 39 | : false, 40 | publicPath: config.dev.assetsPublicPath, 41 | proxy: config.dev.proxyTable, 42 | quiet: true, // necessary for FriendlyErrorsPlugin 43 | watchOptions: { 44 | poll: config.dev.poll, 45 | } 46 | }, 47 | plugins: [ 48 | new webpack.DefinePlugin({ 49 | 'process.env': require('../config/dev.env') 50 | }), 51 | new webpack.HotModuleReplacementPlugin(), 52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 53 | new webpack.NoEmitOnErrorsPlugin(), 54 | // https://github.com/ampedandwired/html-webpack-plugin 55 | new HtmlWebpackPlugin({ 56 | filename: 'index.html', 57 | template: 'index.html', 58 | inject: true 59 | }), 60 | // copy custom static assets 61 | new CopyWebpackPlugin([ 62 | { 63 | from: path.resolve(__dirname, '../static'), 64 | to: config.dev.assetsSubDirectory, 65 | ignore: ['.*'] 66 | } 67 | ]) 68 | ] 69 | }) 70 | 71 | module.exports = new Promise((resolve, reject) => { 72 | portfinder.basePort = process.env.PORT || config.dev.port 73 | portfinder.getPort((err, port) => { 74 | if (err) { 75 | reject(err) 76 | } else { 77 | // publish the new Port, necessary for e2e tests 78 | process.env.PORT = port 79 | // add port to devServer config 80 | devWebpackConfig.devServer.port = port 81 | 82 | // Add FriendlyErrorsPlugin 83 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 84 | compilationSuccessInfo: { 85 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 86 | }, 87 | onErrors: config.dev.notifyOnErrors 88 | ? utils.createNotifierCallback() 89 | : undefined 90 | })) 91 | 92 | resolve(devWebpackConfig) 93 | } 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /front/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"', 7 | // BASE_API: '"http://localhost:8888/"' 8 | BASE_API: '"http://localhost:5000/api"' 9 | }) 10 | -------------------------------------------------------------------------------- /front/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | 24 | /** 25 | * Source Maps 26 | */ 27 | 28 | // https://webpack.js.org/configuration/devtool/#development 29 | devtool: 'cheap-module-eval-source-map', 30 | 31 | // If you have problems debugging vue-files in devtools, 32 | // set this to false - it *may* help 33 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 34 | cacheBusting: true, 35 | 36 | cssSourceMap: true 37 | }, 38 | 39 | build: { 40 | // Template for index.html 41 | index: path.resolve(__dirname, '../dist/index.html'), 42 | 43 | // Paths 44 | assetsRoot: path.resolve(__dirname, '../dist'), 45 | assetsSubDirectory: 'static', 46 | assetsPublicPath: '/', 47 | 48 | /** 49 | * Source Maps 50 | */ 51 | 52 | productionSourceMap: true, 53 | // https://webpack.js.org/configuration/devtool/#production 54 | devtool: '#source-map', 55 | 56 | // Gzip off by default as many popular static hosts such as 57 | // Surge or Netlify already gzip all static assets for you. 58 | // Before setting to `true`, make sure to: 59 | // npm install --save-dev compression-webpack-plugin 60 | productionGzip: true, // 开启gzip压缩 61 | productionGzipExtensions: ['js', 'css'], 62 | 63 | // Run the build command with an extra argument to 64 | // View the bundle analyzer report after build finishes: 65 | // `npm run build --report` 66 | // Set to `true` or `false` to always turn it on or off 67 | bundleAnalyzerReport: process.env.npm_config_report 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /front/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"', 4 | BASE_API: '"http://localhost:5000/api"' 5 | } 6 | -------------------------------------------------------------------------------- /front/config/test.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const devEnv = require('./dev.env') 4 | 5 | module.exports = merge(devEnv, { 6 | NODE_ENV: '"testing"' 7 | }) 8 | -------------------------------------------------------------------------------- /front/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 别院牧志 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "front", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "imoyao ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "unit": "jest --config test/unit/jest.conf.js --coverage", 11 | "e2e": "node test/e2e/runner.js", 12 | "test": "npm run unit && npm run e2e", 13 | "build": "node build/build.js" 14 | }, 15 | "dependencies": { 16 | "axios": "^0.19.0", 17 | "chance": "^1.0.18", 18 | "compression-webpack-plugin": "^1.1.11", 19 | "crypto-js": "^3.1.9-1", 20 | "element-ui": "^2.10.1", 21 | "lint-md": "^0.2.0", 22 | "mavon-editor": "^2.7.5", 23 | "vue": "^2.6.10", 24 | "vue-router": "^3.0.1", 25 | "vuewordcloud": "^18.7.11", 26 | "vuex": "^3.1.1", 27 | "webpack-dev-server": "^2.9.1" 28 | }, 29 | "devDependencies": { 30 | "autoprefixer": "^7.1.2", 31 | "babel-core": "^6.22.1", 32 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 33 | "babel-jest": "^21.0.2", 34 | "babel-loader": "^7.1.1", 35 | "babel-plugin-dynamic-import-node": "^1.2.0", 36 | "babel-plugin-syntax-jsx": "^6.18.0", 37 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", 38 | "babel-plugin-transform-runtime": "^6.22.0", 39 | "babel-plugin-transform-vue-jsx": "^3.5.0", 40 | "babel-preset-env": "^1.3.2", 41 | "babel-preset-stage-2": "^6.22.0", 42 | "babel-register": "^6.22.0", 43 | "chalk": "^2.0.1", 44 | "chromedriver": "^2.27.2", 45 | "copy-webpack-plugin": "^4.0.1", 46 | "cross-spawn": "^5.0.1", 47 | "css-loader": "^0.28.0", 48 | "extract-text-webpack-plugin": "^3.0.0", 49 | "file-loader": "^1.1.4", 50 | "friendly-errors-webpack-plugin": "^1.6.1", 51 | "html-webpack-plugin": "^2.30.1", 52 | "jest": "^22.0.4", 53 | "jest-serializer-vue": "^0.3.0", 54 | "nightwatch": "^0.9.12", 55 | "node-notifier": "^5.1.2", 56 | "optimize-css-assets-webpack-plugin": "^3.2.0", 57 | "ora": "^1.2.0", 58 | "portfinder": "^1.0.13", 59 | "postcss-import": "^11.0.0", 60 | "postcss-loader": "^2.0.8", 61 | "postcss-url": "^7.2.1", 62 | "rimraf": "^2.6.0", 63 | "selenium-server": "^3.0.1", 64 | "semver": "^5.3.0", 65 | "shelljs": "^0.7.6", 66 | "uglifyjs-webpack-plugin": "^1.1.1", 67 | "url-loader": "^0.5.8", 68 | "vue-jest": "^1.0.2", 69 | "vue-loader": "^13.7.3", 70 | "vue-style-loader": "^3.0.1", 71 | "vue-template-compiler": "^2.5.2", 72 | "webpack": "^3.6.0", 73 | "webpack-bundle-analyzer": "^2.9.0", 74 | "webpack-merge": "^4.1.0" 75 | }, 76 | "engines": { 77 | "node": ">= 6.0.0", 78 | "npm": ">= 3.0.0" 79 | }, 80 | "browserslist": [ 81 | "> 1%", 82 | "last 2 versions", 83 | "not ie <= 8" 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /front/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | 17 | 104 | -------------------------------------------------------------------------------- /front/src/Home.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 47 | 48 | 54 | -------------------------------------------------------------------------------- /front/src/api/category.js: -------------------------------------------------------------------------------- 1 | import request from '@/request' 2 | 3 | // 4 | export function reqAllCategories() { 5 | return request({ 6 | url: '/categories', 7 | method: 'get', 8 | }) 9 | } 10 | 11 | export function reqCategoryDetail(id) { 12 | return request({ 13 | url: `/categories/${id}`, 14 | method: 'get', 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /front/src/api/comment.js: -------------------------------------------------------------------------------- 1 | import request from '@/request' 2 | 3 | 4 | export function reqCommentsByArticle(id) { 5 | let paramsObj ={ 6 | 'post_id':id 7 | } 8 | return request({ 9 | url: `/comments`, 10 | method: 'get', 11 | params:paramsObj 12 | }) 13 | } 14 | 15 | export function publishComment(comment) { 16 | return request({ 17 | url: '/comments/create/change', 18 | method: 'post', 19 | data: comment 20 | }) 21 | } 22 | 23 | -------------------------------------------------------------------------------- /front/src/api/index.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /front/src/api/login.js: -------------------------------------------------------------------------------- 1 | import request from '@/request' 2 | import {getToken} from '@/request/token' 3 | 4 | export function requestLogin(account, password) { 5 | const data = { 6 | username: account, 7 | password: password 8 | } 9 | return request({ 10 | url: '/signin', 11 | method: 'post', 12 | auth: data, 13 | // 这里是重点,因为对配置不熟悉搞了好久。`auth` 表示应该使用 HTTP 基础验证,并提供凭据 14 | // 将设置一个 `Authorization` 头,覆写掉现有的任意使用 `headers` 设置的自定义 `Authorization`头 15 | // auth: { 16 | // username: 'janedoe', 17 | // password: 's00pers3cret' 18 | // }, 19 | }) 20 | } 21 | 22 | export function logout() { 23 | let _this = this 24 | this.$confirm('确认退出吗?', '提示', { 25 | type: 'warning' 26 | }).then(() => { 27 | localStorage.removeItem('token') 28 | _this.$router.push('/') 29 | }).catch(() => { 30 | 31 | }) 32 | } 33 | 34 | export function reqUserInfo() { 35 | const data = { 36 | token: getToken() 37 | } 38 | return request({ 39 | url: '/users', 40 | method: 'get', 41 | data 42 | }) 43 | } 44 | 45 | export function register(userInfo) { 46 | let account = userInfo.account 47 | let email = userInfo.email 48 | let password = userInfo.password 49 | let rePassword = userInfo.rePassword 50 | const data = { 51 | account, 52 | email, 53 | password, 54 | rePassword 55 | } 56 | return request({ 57 | url: '/register', 58 | method: 'post', 59 | data 60 | }) 61 | } 62 | 63 | export function fetchCheckEmail(email) { 64 | return request({ 65 | url: '/emails', 66 | method: 'get', 67 | params: email 68 | }) 69 | } 70 | 71 | export function sendCaptcha(email) { 72 | const data = { 73 | email, 74 | } 75 | return request({ 76 | url: '/emails', 77 | method: 'post', 78 | data 79 | }) 80 | } 81 | 82 | export function verificateCaptcha(email, captcha) { 83 | const data = { 84 | email, 85 | captcha 86 | } 87 | return request({ 88 | url: '/verifications', 89 | method: 'post', 90 | data 91 | }) 92 | } 93 | 94 | export function resetPassword(email, password) { 95 | const data = { 96 | email, 97 | password 98 | } 99 | return request({ 100 | url: '/password', 101 | method: 'post', 102 | data 103 | }) 104 | } 105 | 106 | export function reqTaskStatus(name, location) { 107 | console.log(location) 108 | const data = { 109 | name 110 | } 111 | return request({ 112 | url: location, 113 | method: 'get', 114 | data 115 | }) 116 | } 117 | -------------------------------------------------------------------------------- /front/src/api/tag.js: -------------------------------------------------------------------------------- 1 | import request from '@/request' 2 | 3 | export function reqAllTags() { 4 | return request({ 5 | url: '/tags', 6 | method: 'get', 7 | }) 8 | } 9 | 10 | export function reqHotTags() { 11 | return request({ 12 | url: '/tags', 13 | method: 'get', 14 | params: { 15 | hot: true, 16 | } 17 | }) 18 | } 19 | 20 | export function reqMostTags() { 21 | return request({ 22 | url: '/tags', 23 | method: 'get', 24 | params: { 25 | hot: true, 26 | limit:5 27 | } 28 | }) 29 | } 30 | 31 | export function reqTagDetail(id) { 32 | return request({ 33 | url: `/tags/${id}`, 34 | method: 'get', 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /front/src/api/upload.js: -------------------------------------------------------------------------------- 1 | import request from '@/request' 2 | 3 | export function upload(formdata) { 4 | return request({ 5 | headers: {'Content-Type': 'multipart/form-data'}, 6 | url: '/images', 7 | method: 'post', 8 | data: formdata 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /front/src/assets/css/style.css: -------------------------------------------------------------------------------- 1 | /*自定义样式*/ 2 | 3 | /*文章详情页*/ 4 | .me-view-body { 5 | margin: 100px auto 140px; 6 | } 7 | 8 | .me-view-container { 9 | width: 700px; 10 | } 11 | 12 | .el-main { 13 | overflow: hidden; 14 | } 15 | 16 | .me-view-title { 17 | font-size: 34px; 18 | font-weight: 700; 19 | line-height: 1.3; 20 | } 21 | 22 | .me-view-author { 23 | margin-top: 30px; 24 | vertical-align: middle; 25 | } 26 | 27 | .me-view-picture { 28 | width: 40px; 29 | height: 40px; 30 | border: 1px solid #ddd; 31 | border-radius: 50%; 32 | vertical-align: middle; 33 | background-color: #5fb878; 34 | } 35 | 36 | .me-view-info { 37 | display: inline-block; 38 | vertical-align: middle; 39 | margin-left: 8px; 40 | } 41 | 42 | .me-view-meta { 43 | font-size: 12px; 44 | color: #969696; 45 | } 46 | 47 | .me-view-end { 48 | margin-top: 20px; 49 | } 50 | 51 | .me-view-tag { 52 | margin-top: 20px; 53 | padding-left: 6px; 54 | border-left: 4px solid #c5cac3; 55 | } 56 | 57 | .me-view-tag-item { 58 | margin: 0 4px; 59 | } 60 | 61 | .me-view-comment { 62 | margin-top: 60px; 63 | } 64 | 65 | .me-view-comment-title { 66 | font-weight: 600; 67 | border-bottom: 1px solid #f0f0f0; 68 | padding-bottom: 20px; 69 | } 70 | 71 | .me-view-comment-write { 72 | margin-top: 20px; 73 | } 74 | 75 | .me-view-comment-text { 76 | font-size: 16px; 77 | } 78 | 79 | .v-show-content { 80 | padding: 8px 25px 15px 0px !important; 81 | } 82 | 83 | .v-note-wrapper .v-note-panel { 84 | box-shadow: none !important; 85 | } 86 | 87 | .v-note-wrapper .v-note-panel .v-note-show .v-show-content, .v-note-wrapper .v-note-panel .v-note-show .v-show-content-html { 88 | background: #fff !important; 89 | } 90 | 91 | .el-divider__text { 92 | position: absolute; 93 | background-color: #fff; 94 | padding: 0 20px; 95 | font-weight: 500; 96 | color: #cc2a41; 97 | font-size: 14px; 98 | } 99 | 100 | figcaption { 101 | text-align: center; 102 | margin-top: .66667em; 103 | margin-bottom: .66667em; 104 | padding: 0 1em; 105 | font-size: .9em; 106 | line-height: 1.5; 107 | color: #999; 108 | } 109 | -------------------------------------------------------------------------------- /front/src/assets/img/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/img/bg.png -------------------------------------------------------------------------------- /front/src/assets/img/default_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/img/default_avatar.png -------------------------------------------------------------------------------- /front/src/assets/img/link_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/img/link_bg.png -------------------------------------------------------------------------------- /front/src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/img/logo.png -------------------------------------------------------------------------------- /front/src/assets/img/logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/img/logo1.png -------------------------------------------------------------------------------- /front/src/assets/img/panfish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/img/panfish.png -------------------------------------------------------------------------------- /front/src/assets/img/profile_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/img/profile_bg.jpg -------------------------------------------------------------------------------- /front/src/assets/img/sea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/img/sea.png -------------------------------------------------------------------------------- /front/src/assets/img/spray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/img/spray.png -------------------------------------------------------------------------------- /front/src/assets/iyicon/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/iyicon/iconfont.eot -------------------------------------------------------------------------------- /front/src/assets/iyicon/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/iyicon/iconfont.ttf -------------------------------------------------------------------------------- /front/src/assets/iyicon/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/iyicon/iconfont.woff -------------------------------------------------------------------------------- /front/src/assets/iyicon/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/iyicon/iconfont.woff2 -------------------------------------------------------------------------------- /front/src/assets/theme/alert.css: -------------------------------------------------------------------------------- 1 | .el-alert{width:100%;padding:8px 16px;margin:0;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;position:relative;background-color:#fff;overflow:hidden;opacity:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-transition:opacity .2s;transition:opacity .2s}.el-alert.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-alert--success{background-color:#f0f9eb;color:#67c23a}.el-alert--success .el-alert__description{color:#67c23a}.el-alert--info{background-color:#f4f4f5;color:#909399}.el-alert--info .el-alert__description{color:#909399}.el-alert--warning{background-color:#fdf6ec;color:#e6a23c}.el-alert--warning .el-alert__description{color:#e6a23c}.el-alert--error{background-color:#fef0f0;color:#f56c6c}.el-alert--error .el-alert__description{color:#f56c6c}.el-alert__content{display:table-cell;padding:0 8px}.el-alert__icon{font-size:16px;width:16px}.el-alert__icon.is-big{font-size:28px;width:28px}.el-alert__title{font-size:13px;line-height:18px}.el-alert__title.is-bold{font-weight:700}.el-alert .el-alert__description{font-size:12px;margin:5px 0 0}.el-alert__closebtn{font-size:12px;color:#c0c4cc;opacity:1;position:absolute;top:12px;right:15px;cursor:pointer}.el-alert__closebtn.is-customed{font-style:normal;font-size:13px;top:9px}.el-alert-fade-enter,.el-alert-fade-leave-active{opacity:0} -------------------------------------------------------------------------------- /front/src/assets/theme/aside.css: -------------------------------------------------------------------------------- 1 | .el-aside{overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box} -------------------------------------------------------------------------------- /front/src/assets/theme/badge.css: -------------------------------------------------------------------------------- 1 | .el-badge{position:relative;vertical-align:middle;display:inline-block}.el-badge__content{background-color:#f56c6c;border-radius:10px;color:#fff;display:inline-block;font-size:12px;height:18px;line-height:18px;padding:0 6px;text-align:center;white-space:nowrap;border:1px solid #fff}.el-badge__content.is-fixed{position:absolute;top:0;right:10px;-webkit-transform:translateY(-50%) translateX(100%);transform:translateY(-50%) translateX(100%)}.el-badge__content.is-fixed.is-dot{right:5px}.el-badge__content.is-dot{height:8px;width:8px;padding:0;right:0;border-radius:50%} -------------------------------------------------------------------------------- /front/src/assets/theme/breadcrumb-item.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/theme/breadcrumb-item.css -------------------------------------------------------------------------------- /front/src/assets/theme/breadcrumb.css: -------------------------------------------------------------------------------- 1 | .el-breadcrumb{font-size:14px;line-height:1}.el-breadcrumb::after,.el-breadcrumb::before{display:table;content:""}.el-breadcrumb::after{clear:both}.el-breadcrumb__separator{margin:0 9px;font-weight:700;color:#c0c4cc}.el-breadcrumb__separator[class*=icon]{margin:0 6px;font-weight:400}.el-breadcrumb__item{float:left}.el-breadcrumb__inner,.el-breadcrumb__inner a{font-weight:700;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1);color:#303133}.el-breadcrumb__inner a:hover,.el-breadcrumb__inner:hover{color:#5FB878;cursor:pointer}.el-breadcrumb__item:last-child .el-breadcrumb__inner,.el-breadcrumb__item:last-child .el-breadcrumb__inner a,.el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover,.el-breadcrumb__item:last-child .el-breadcrumb__inner:hover{font-weight:400;color:#606266;cursor:text}.el-breadcrumb__item:last-child .el-breadcrumb__separator{display:none} -------------------------------------------------------------------------------- /front/src/assets/theme/button-group.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/theme/button-group.css -------------------------------------------------------------------------------- /front/src/assets/theme/card.css: -------------------------------------------------------------------------------- 1 | .el-card{border-radius:4px;border:1px solid #ebeef5;background-color:#fff;overflow:hidden;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);color:#303133}.el-card__header{padding:18px 20px;border-bottom:1px solid #ebeef5;-webkit-box-sizing:border-box;box-sizing:border-box}.el-card__body{padding:20px} -------------------------------------------------------------------------------- /front/src/assets/theme/carousel-item.css: -------------------------------------------------------------------------------- 1 | .el-carousel__item,.el-carousel__mask{position:absolute;height:100%;top:0;left:0}.el-carousel__item{width:100%;display:inline-block;overflow:hidden;z-index:0}.el-carousel__item.is-active{z-index:2}.el-carousel__item.is-animating{-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.el-carousel__item--card{width:50%;-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.el-carousel__item--card.is-in-stage{cursor:pointer;z-index:1}.el-carousel__item--card.is-in-stage.is-hover .el-carousel__mask,.el-carousel__item--card.is-in-stage:hover .el-carousel__mask{opacity:.12}.el-carousel__item--card.is-active{z-index:2}.el-carousel__mask{width:100%;background-color:#fff;opacity:.24;-webkit-transition:.2s;transition:.2s} -------------------------------------------------------------------------------- /front/src/assets/theme/carousel.css: -------------------------------------------------------------------------------- 1 | .el-carousel{overflow-x:hidden;position:relative}.el-carousel__container{position:relative;height:300px}.el-carousel__arrow{border:none;outline:0;padding:0;margin:0;height:36px;width:36px;cursor:pointer;-webkit-transition:.3s;transition:.3s;border-radius:50%;background-color:rgba(31,45,61,.11);color:#fff;position:absolute;top:50%;z-index:10;-webkit-transform:translateY(-50%);transform:translateY(-50%);text-align:center;font-size:12px}.el-carousel__arrow--left{left:16px}.el-carousel__arrow--right{right:16px}.el-carousel__arrow:hover{background-color:rgba(31,45,61,.23)}.el-carousel__arrow i{cursor:pointer}.el-carousel__indicators{position:absolute;list-style:none;bottom:0;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);margin:0;padding:0;z-index:2}.el-carousel__indicators--outside{bottom:26px;text-align:center;position:static;-webkit-transform:none;transform:none}.el-carousel__indicators--outside .el-carousel__indicator:hover button{opacity:.64}.el-carousel__indicators--outside button{background-color:#c0c4cc;opacity:.24}.el-carousel__indicators--labels{left:0;right:0;-webkit-transform:none;transform:none;text-align:center}.el-carousel__indicators--labels .el-carousel__button{height:auto;width:auto;padding:2px 18px;font-size:12px}.el-carousel__indicators--labels .el-carousel__indicator{padding:6px 4px}.el-carousel__indicator{display:inline-block;background-color:transparent;padding:12px 4px;cursor:pointer}.el-carousel__indicator:hover button{opacity:.72}.el-carousel__indicator.is-active button{opacity:1}.el-carousel__button{display:block;opacity:.48;width:30px;height:2px;background-color:#fff;border:none;outline:0;padding:0;margin:0;cursor:pointer;-webkit-transition:.3s;transition:.3s}.carousel-arrow-left-enter,.carousel-arrow-left-leave-active{-webkit-transform:translateY(-50%) translateX(-10px);transform:translateY(-50%) translateX(-10px);opacity:0}.carousel-arrow-right-enter,.carousel-arrow-right-leave-active{-webkit-transform:translateY(-50%) translateX(10px);transform:translateY(-50%) translateX(10px);opacity:0} -------------------------------------------------------------------------------- /front/src/assets/theme/checkbox-button.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/theme/checkbox-button.css -------------------------------------------------------------------------------- /front/src/assets/theme/checkbox-group.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/theme/checkbox-group.css -------------------------------------------------------------------------------- /front/src/assets/theme/collapse-item.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/theme/collapse-item.css -------------------------------------------------------------------------------- /front/src/assets/theme/collapse.css: -------------------------------------------------------------------------------- 1 | .el-collapse-item__header,.el-collapse-item__wrap{background-color:#fff;border-bottom:1px solid #ebeef5}.el-collapse,.el-collapse-item__header,.el-collapse-item__wrap{border-bottom:1px solid #ebeef5}.el-collapse{border-top:1px solid #ebeef5}.el-collapse-item__header{height:48px;line-height:48px;color:#303133;cursor:pointer;font-size:13px;font-weight:500;-webkit-transition:border-bottom-color .3s;transition:border-bottom-color .3s;outline:0}.el-collapse-item__arrow{margin-right:8px;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;float:right;line-height:48px;font-weight:300}.el-collapse-item__header.focusing:focus:not(:hover){color:#5FB878}.el-collapse-item__wrap{will-change:height;overflow:hidden;-webkit-box-sizing:border-box;box-sizing:border-box}.el-collapse-item__content{padding-bottom:25px;font-size:13px;color:#303133;line-height:1.769230769230769}.el-collapse-item.is-active .el-collapse-item__header{border-bottom-color:transparent}.el-collapse-item.is-active .el-collapse-item__header .el-collapse-item__arrow{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.el-collapse-item:last-child{margin-bottom:-1px} -------------------------------------------------------------------------------- /front/src/assets/theme/container.css: -------------------------------------------------------------------------------- 1 | .el-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-sizing:border-box;box-sizing:border-box;min-width:0}.el-container.is-vertical{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column} -------------------------------------------------------------------------------- /front/src/assets/theme/dialog.css: -------------------------------------------------------------------------------- 1 | .v-modal-enter{-webkit-animation:v-modal-in .2s ease;animation:v-modal-in .2s ease}.v-modal-leave{-webkit-animation:v-modal-out .2s ease forwards;animation:v-modal-out .2s ease forwards}@-webkit-keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-in{0%{opacity:0}}@-webkit-keyframes v-modal-out{100%{opacity:0}}@keyframes v-modal-out{100%{opacity:0}}.v-modal{position:fixed;left:0;top:0;width:100%;height:100%;opacity:.5;background:#000}.el-dialog{position:relative;margin:0 auto 50px;background:#fff;border-radius:2px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.3);box-shadow:0 1px 3px rgba(0,0,0,.3);-webkit-box-sizing:border-box;box-sizing:border-box;width:50%}.el-dialog.is-fullscreen{width:100%;margin-top:0;margin-bottom:0;height:100%;overflow:auto}.el-dialog__wrapper{position:fixed;top:0;right:0;bottom:0;left:0;overflow:auto;margin:0}.el-dialog__header{padding:15px 15px 10px}.el-dialog__headerbtn{position:absolute;top:15px;right:15px;padding:0;background:0 0;border:none;outline:0;cursor:pointer;font-size:16px}.el-dialog__headerbtn .el-dialog__close{color:#909399}.el-dialog__headerbtn:focus .el-dialog__close,.el-dialog__headerbtn:hover .el-dialog__close{color:#5FB878}.el-dialog__title{line-height:24px;font-size:18px;color:#303133}.el-dialog__body{padding:30px 20px;color:#606266;line-height:24px;font-size:14px}.el-dialog__footer{padding:10px 15px 15px;text-align:right;-webkit-box-sizing:border-box;box-sizing:border-box}.el-dialog--center{text-align:center}.el-dialog--center .el-dialog__header{padding-top:30px}.el-dialog--center .el-dialog__body{text-align:initial;padding:25px 27px 30px}.el-dialog--center .el-dialog__footer{text-align:inherit;padding-bottom:30px}.dialog-fade-enter-active{-webkit-animation:dialog-fade-in .3s;animation:dialog-fade-in .3s}.dialog-fade-leave-active{-webkit-animation:dialog-fade-out .3s;animation:dialog-fade-out .3s}@-webkit-keyframes dialog-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@keyframes dialog-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes dialog-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}@keyframes dialog-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}} -------------------------------------------------------------------------------- /front/src/assets/theme/display.css: -------------------------------------------------------------------------------- 1 | @media only screen and (max-width:767px){.hidden-xs-only{display:none!important}}@media only screen and (min-width:768px){.hidden-sm-and-up{display:none!important}}@media only screen and (min-width:768px) and (max-width:991px){.hidden-sm-only{display:none!important}}@media only screen and (max-width:991px){.hidden-sm-and-down{display:none!important}}@media only screen and (min-width:992px){.hidden-md-and-up{display:none!important}}@media only screen and (min-width:992px) and (max-width:1199px){.hidden-md-only{display:none!important}}@media only screen and (max-width:1199px){.hidden-md-and-down{display:none!important}}@media only screen and (min-width:1200px){.hidden-lg-and-up{display:none!important}}@media only screen and (min-width:1200px) and (max-width:1919px){.hidden-lg-only{display:none!important}}@media only screen and (max-width:1919px){.hidden-lg-and-down{display:none!important}}@media only screen and (min-width:1920px){.hidden-xl-only{display:none!important}} -------------------------------------------------------------------------------- /front/src/assets/theme/dropdown-item.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/theme/dropdown-item.css -------------------------------------------------------------------------------- /front/src/assets/theme/dropdown-menu.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/theme/dropdown-menu.css -------------------------------------------------------------------------------- /front/src/assets/theme/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/theme/fonts/element-icons.ttf -------------------------------------------------------------------------------- /front/src/assets/theme/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/theme/fonts/element-icons.woff -------------------------------------------------------------------------------- /front/src/assets/theme/footer.css: -------------------------------------------------------------------------------- 1 | .el-footer{padding:0 20px;-webkit-box-sizing:border-box;box-sizing:border-box} -------------------------------------------------------------------------------- /front/src/assets/theme/form-item.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/theme/form-item.css -------------------------------------------------------------------------------- /front/src/assets/theme/form.css: -------------------------------------------------------------------------------- 1 | .el-form--inline .el-form-item,.el-form--inline .el-form-item__content{display:inline-block;vertical-align:top}.el-form-item::after,.el-form-item__content::after{clear:both}.el-form--label-left .el-form-item__label{text-align:left}.el-form--label-top .el-form-item__label{float:none;display:inline-block;text-align:left;padding:0 0 10px}.el-form--inline .el-form-item{margin-right:10px}.el-form--inline .el-form-item__label{float:none;display:inline-block}.el-form--inline.el-form--label-top .el-form-item__content{display:block}.el-form-item{margin-bottom:22px}.el-form-item::after,.el-form-item::before{display:table;content:""}.el-form-item .el-form-item{margin-bottom:0}.el-form-item--mini.el-form-item,.el-form-item--small.el-form-item{margin-bottom:18px}.el-form-item .el-input__validateIcon{display:none}.el-form-item--medium .el-form-item__content,.el-form-item--medium .el-form-item__label{line-height:36px}.el-form-item--small .el-form-item__content,.el-form-item--small .el-form-item__label{line-height:32px}.el-form-item--small .el-form-item__error{padding-top:2px}.el-form-item--mini .el-form-item__content,.el-form-item--mini .el-form-item__label{line-height:28px}.el-form-item--mini .el-form-item__error{padding-top:1px}.el-form-item__label{text-align:right;vertical-align:middle;float:left;font-size:14px;color:#606266;line-height:40px;padding:0 12px 0 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-form-item__content{line-height:40px;position:relative;font-size:14px}.el-form-item__content::after,.el-form-item__content::before{display:table;content:""}.el-form-item__error{color:#f56c6c;font-size:12px;line-height:1;padding-top:4px;position:absolute;top:100%;left:0}.el-form-item__error--inline{position:relative;top:auto;left:auto;display:inline-block;margin-left:10px}.el-form-item.is-required .el-form-item__label:before{content:'*';color:#f56c6c;margin-right:4px}.el-form-item.is-error .el-input__inner,.el-form-item.is-error .el-input__inner:focus,.el-form-item.is-error .el-textarea__inner,.el-form-item.is-error .el-textarea__inner:focus{border-color:#f56c6c}.el-form-item.is-error .el-input-group__append .el-input__inner,.el-form-item.is-error .el-input-group__prepend .el-input__inner{border-color:transparent}.el-form-item.is-error .el-input__validateIcon{color:#f56c6c}.el-form-item.is-success .el-input__inner,.el-form-item.is-success .el-input__inner:focus,.el-form-item.is-success .el-textarea__inner,.el-form-item.is-success .el-textarea__inner:focus{border-color:#67c23a}.el-form-item.is-success .el-input-group__append .el-input__inner,.el-form-item.is-success .el-input-group__prepend .el-input__inner{border-color:transparent}.el-form-item.is-success .el-input__validateIcon{color:#67c23a}.el-form-item--feedback .el-input__validateIcon{display:inline-block} -------------------------------------------------------------------------------- /front/src/assets/theme/header.css: -------------------------------------------------------------------------------- 1 | .el-header{padding:0 20px;-webkit-box-sizing:border-box;box-sizing:border-box} -------------------------------------------------------------------------------- /front/src/assets/theme/loading.css: -------------------------------------------------------------------------------- 1 | .el-loading-parent--relative{position:relative!important}.el-loading-parent--hidden{overflow:hidden!important}.el-loading-mask{position:absolute;z-index:10000;background-color:rgba(255,255,255,.9);margin:0;top:0;right:0;bottom:0;left:0;-webkit-transition:opacity .3s;transition:opacity .3s}.el-loading-mask.is-fullscreen{position:fixed}.el-loading-mask.is-fullscreen .el-loading-spinner{margin-top:-25px}.el-loading-mask.is-fullscreen .el-loading-spinner .circular{height:50px;width:50px}.el-loading-spinner{top:50%;margin-top:-21px;width:100%;text-align:center;position:absolute}.el-loading-spinner .el-loading-text{color:#5FB878;margin:3px 0;font-size:14px}.el-loading-spinner .circular{height:42px;width:42px;-webkit-animation:loading-rotate 2s linear infinite;animation:loading-rotate 2s linear infinite}.el-loading-spinner .path{-webkit-animation:loading-dash 1.5s ease-in-out infinite;animation:loading-dash 1.5s ease-in-out infinite;stroke-dasharray:90,150;stroke-dashoffset:0;stroke-width:2;stroke:#5FB878;stroke-linecap:round}.el-loading-spinner i{color:#5FB878}.el-loading-fade-enter,.el-loading-fade-leave-active{opacity:0}@-webkit-keyframes loading-rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes loading-rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}100%{stroke-dasharray:90,150;stroke-dashoffset:-120px}}@keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}100%{stroke-dasharray:90,150;stroke-dashoffset:-120px}} -------------------------------------------------------------------------------- /front/src/assets/theme/main.css: -------------------------------------------------------------------------------- 1 | .el-main{-webkit-box-flex:1;-ms-flex:1;flex:1;overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box;padding:20px} -------------------------------------------------------------------------------- /front/src/assets/theme/menu-item-group.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/theme/menu-item-group.css -------------------------------------------------------------------------------- /front/src/assets/theme/menu-item.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/theme/menu-item.css -------------------------------------------------------------------------------- /front/src/assets/theme/message.css: -------------------------------------------------------------------------------- 1 | .el-message__closeBtn:focus,.el-message__content:focus{outline-width:0}.el-message{min-width:380px;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;border-width:1px;border-style:solid;border-color:#ebeef5;position:fixed;left:50%;top:20px;-webkit-transform:translateX(-50%);transform:translateX(-50%);background-color:#edf2fc;-webkit-transition:opacity .3s,-webkit-transform .4s;transition:opacity .3s,-webkit-transform .4s;transition:opacity .3s,transform .4s;transition:opacity .3s,transform .4s,-webkit-transform .4s;overflow:hidden;padding:15px 15px 15px 20px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-message.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-message p{margin:0}.el-message--info .el-message__content{color:#909399}.el-message--success{background-color:#f0f9eb;border-color:#e1f3d8}.el-message--success .el-message__content{color:#67c23a}.el-message--warning{background-color:#fdf6ec;border-color:#faecd8}.el-message--warning .el-message__content{color:#e6a23c}.el-message--error{background-color:#fef0f0;border-color:#fde2e2}.el-message--error .el-message__content{color:#f56c6c}.el-message__icon{margin-right:10px}.el-message__content{padding:0;font-size:14px;line-height:1}.el-message__closeBtn{position:absolute;top:50%;right:15px;-webkit-transform:translateY(-50%);transform:translateY(-50%);cursor:pointer;color:#c0c4cc;font-size:16px}.el-message__closeBtn:hover{color:#909399}.el-message .el-icon-success{color:#67c23a}.el-message .el-icon-error{color:#f56c6c}.el-message .el-icon-info{color:#909399}.el-message .el-icon-warning{color:#e6a23c}.el-message-fade-enter,.el-message-fade-leave-active{opacity:0;-webkit-transform:translate(-50%,-100%);transform:translate(-50%,-100%)} -------------------------------------------------------------------------------- /front/src/assets/theme/notification.css: -------------------------------------------------------------------------------- 1 | .el-notification{display:-webkit-box;display:-ms-flexbox;display:flex;width:330px;padding:14px 26px 14px 13px;border-radius:8px;-webkit-box-sizing:border-box;box-sizing:border-box;border:1px solid #ebeef5;position:fixed;background-color:#fff;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);-webkit-transition:opacity .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;transition:opacity .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;transition:opacity .3s,transform .3s,left .3s,right .3s,top .4s,bottom .3s;transition:opacity .3s,transform .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;overflow:hidden}.el-notification.right{right:16px}.el-notification.left{left:16px}.el-notification__group{margin-left:13px}.el-notification__title{font-weight:700;font-size:16px;color:#303133;margin:0}.el-notification__content{font-size:14px;line-height:21px;margin:6px 0 0;color:#606266;text-align:justify}.el-notification__content p{margin:0}.el-notification__icon{height:24px;width:24px;font-size:24px}.el-notification__closeBtn{position:absolute;top:18px;right:15px;cursor:pointer;color:#909399;font-size:16px}.el-notification__closeBtn:hover{color:#606266}.el-notification .el-icon-success{color:#67c23a}.el-notification .el-icon-error{color:#f56c6c}.el-notification .el-icon-info{color:#909399}.el-notification .el-icon-warning{color:#e6a23c}.el-notification-fade-enter.right{right:0;-webkit-transform:translateX(100%);transform:translateX(100%)}.el-notification-fade-enter.left{left:0;-webkit-transform:translateX(-100%);transform:translateX(-100%)}.el-notification-fade-leave-active{opacity:0} -------------------------------------------------------------------------------- /front/src/assets/theme/option-group.css: -------------------------------------------------------------------------------- 1 | .el-select-group{margin:0;padding:0}.el-select-group__wrap{position:relative;list-style:none;margin:0;padding:0}.el-select-group__wrap:not(:last-of-type){padding-bottom:24px}.el-select-group__wrap:not(:last-of-type)::after{content:'';position:absolute;display:block;left:20px;right:20px;bottom:12px;height:1px;background:#e4e7ed}.el-select-group__title{padding-left:20px;font-size:12px;color:#909399;line-height:30px}.el-select-group .el-select-dropdown__item{padding-left:20px} -------------------------------------------------------------------------------- /front/src/assets/theme/option.css: -------------------------------------------------------------------------------- 1 | .el-select-dropdown__item{font-size:14px;padding:0 20px;position:relative;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#606266;height:34px;line-height:34px;-webkit-box-sizing:border-box;box-sizing:border-box;cursor:pointer}.el-select-dropdown__item.is-disabled{color:#c0c4cc;cursor:not-allowed}.el-select-dropdown__item.is-disabled:hover{background-color:#fff}.el-select-dropdown__item.hover,.el-select-dropdown__item:hover{background-color:#f5f7fa}.el-select-dropdown__item.selected{color:#5FB878;font-weight:700}.el-select-dropdown__item span{line-height:34px!important} -------------------------------------------------------------------------------- /front/src/assets/theme/popover.css: -------------------------------------------------------------------------------- 1 | .el-popper .popper__arrow,.el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.el-popper .popper__arrow::after{content:" ";border-width:6px}.el-popper[x-placement^=top]{margin-bottom:12px}.el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#ebeef5;border-bottom-width:0}.el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.el-popper[x-placement^=bottom]{margin-top:12px}.el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#ebeef5}.el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.el-popper[x-placement^=right]{margin-left:12px}.el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#ebeef5;border-left-width:0}.el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.el-popper[x-placement^=left]{margin-right:12px}.el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#ebeef5}.el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff}.el-popover{position:absolute;background:#fff;min-width:150px;border-radius:4px;border:1px solid #ebeef5;padding:12px;z-index:2000;color:#606266;line-height:1.4;text-align:justify;font-size:14px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-popover--plain{padding:18px 20px}.el-popover__title{color:#303133;font-size:16px;line-height:1;margin-bottom:12px}.el-popover__reference:focus:hover,.el-popover__reference:focus:not(.focusing){outline-width:0} -------------------------------------------------------------------------------- /front/src/assets/theme/popper.css: -------------------------------------------------------------------------------- 1 | .el-popper .popper__arrow,.el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.el-popper .popper__arrow::after{content:" ";border-width:6px}.el-popper[x-placement^=top]{margin-bottom:12px}.el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#ebeef5;border-bottom-width:0}.el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.el-popper[x-placement^=bottom]{margin-top:12px}.el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#ebeef5}.el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.el-popper[x-placement^=right]{margin-left:12px}.el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#ebeef5;border-left-width:0}.el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.el-popper[x-placement^=left]{margin-right:12px}.el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#ebeef5}.el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff} -------------------------------------------------------------------------------- /front/src/assets/theme/progress.css: -------------------------------------------------------------------------------- 1 | .el-progress{position:relative;line-height:1}.el-progress__text{font-size:14px;color:#606266;display:inline-block;vertical-align:middle;margin-left:10px;line-height:1}.el-progress__text i{vertical-align:middle;display:block}.el-progress--circle{display:inline-block}.el-progress--circle .el-progress__text{position:absolute;top:50%;left:0;width:100%;text-align:center;margin:0;-webkit-transform:translate(0,-50%);transform:translate(0,-50%)}.el-progress--circle .el-progress__text i{vertical-align:middle;display:inline-block}.el-progress--without-text .el-progress__text{display:none}.el-progress--without-text .el-progress-bar{padding-right:0;margin-right:0;display:block}.el-progress-bar,.el-progress-bar__inner::after,.el-progress-bar__innerText{display:inline-block;vertical-align:middle}.el-progress--text-inside .el-progress-bar{padding-right:0;margin-right:0}.el-progress.is-success .el-progress-bar__inner{background-color:#67c23a}.el-progress.is-success .el-progress__text{color:#67c23a}.el-progress.is-exception .el-progress-bar__inner{background-color:#f56c6c}.el-progress.is-exception .el-progress__text{color:#f56c6c}.el-progress-bar{padding-right:50px;width:100%;margin-right:-55px;-webkit-box-sizing:border-box;box-sizing:border-box}.el-progress-bar__outer{height:6px;border-radius:100px;background-color:#ebeef5;overflow:hidden;position:relative;vertical-align:middle}.el-progress-bar__inner{position:absolute;left:0;top:0;height:100%;background-color:#5FB878;text-align:right;border-radius:100px;line-height:1;white-space:nowrap}.el-progress-bar__inner::after{content:"";height:100%}.el-progress-bar__innerText{color:#fff;font-size:12px;margin:0 5px}@-webkit-keyframes progress{0%{background-position:0 0}100%{background-position:32px 0}}@keyframes progress{0%{background-position:0 0}100%{background-position:32px 0}} -------------------------------------------------------------------------------- /front/src/assets/theme/radio-button.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";.el-radio-button,.el-radio-button__inner{display:inline-block;position:relative;outline:0}.el-radio-button__inner{line-height:1;white-space:nowrap;vertical-align:middle;background:#fff;border:1px solid #dcdfe6;font-weight:500;border-left:0;color:#606266;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;cursor:pointer;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);padding:12px 20px;font-size:14px;border-radius:0}.el-radio-button__inner.is-round{padding:12px 20px}.el-radio-button__inner:hover{color:#5FB878}.el-radio-button__inner [class*=el-icon-]{line-height:.9}.el-radio-button__inner [class*=el-icon-]+span{margin-left:5px}.el-radio-button__orig-radio{opacity:0;outline:0;position:absolute;z-index:-1;left:-999px}.el-radio-button__orig-radio:checked+.el-radio-button__inner{color:#fff;background-color:#5FB878;border-color:#5FB878;-webkit-box-shadow:-1px 0 0 0 #5FB878;box-shadow:-1px 0 0 0 #5FB878}.el-radio-button__orig-radio:disabled+.el-radio-button__inner{color:#c0c4cc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#ebeef5;-webkit-box-shadow:none;box-shadow:none}.el-radio-button__orig-radio:disabled:checked+.el-radio-button__inner{background-color:#f2f6fc}.el-radio-button:first-child .el-radio-button__inner{border-left:1px solid #dcdfe6;border-radius:4px 0 0 4px;-webkit-box-shadow:none!important;box-shadow:none!important}.el-radio-button:last-child .el-radio-button__inner{border-radius:0 4px 4px 0}.el-radio-button:first-child:last-child .el-radio-button__inner{border-radius:4px}.el-radio-button--medium .el-radio-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.el-radio-button--medium .el-radio-button__inner.is-round{padding:10px 20px}.el-radio-button--small .el-radio-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.el-radio-button--small .el-radio-button__inner.is-round{padding:9px 15px}.el-radio-button--mini .el-radio-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.el-radio-button--mini .el-radio-button__inner.is-round{padding:7px 15px}.el-radio-button:focus:not(.is-focus):not(:active){-webkit-box-shadow:0 0 2px 2px #5FB878;box-shadow:0 0 2px 2px #5FB878} -------------------------------------------------------------------------------- /front/src/assets/theme/radio-group.css: -------------------------------------------------------------------------------- 1 | .el-radio-group{display:inline-block;line-height:1;vertical-align:middle;font-size:0} -------------------------------------------------------------------------------- /front/src/assets/theme/radio.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";.el-radio,.el-radio--medium.is-bordered .el-radio__label{font-size:14px}.el-radio,.el-radio__input{white-space:nowrap;line-height:1;outline:0}.el-radio,.el-radio__inner,.el-radio__input{position:relative;display:inline-block}.el-radio{color:#606266;font-weight:500;cursor:pointer;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.el-radio.is-bordered{padding:12px 20px 0 10px;border-radius:4px;border:1px solid #dcdfe6;-webkit-box-sizing:border-box;box-sizing:border-box;height:40px}.el-radio.is-bordered.is-checked{border-color:#5FB878}.el-radio.is-bordered.is-disabled{cursor:not-allowed;border-color:#ebeef5}.el-radio__input.is-disabled .el-radio__inner,.el-radio__input.is-disabled.is-checked .el-radio__inner{background-color:#f5f7fa;border-color:#e4e7ed}.el-radio.is-bordered+.el-radio.is-bordered{margin-left:10px}.el-radio--medium.is-bordered{padding:10px 20px 0 10px;border-radius:4px;height:36px}.el-radio--mini.is-bordered .el-radio__label,.el-radio--small.is-bordered .el-radio__label{font-size:12px}.el-radio--medium.is-bordered .el-radio__inner{height:14px;width:14px}.el-radio--small.is-bordered{padding:8px 15px 0 10px;border-radius:3px;height:32px}.el-radio--small.is-bordered .el-radio__inner{height:12px;width:12px}.el-radio--mini.is-bordered{padding:6px 15px 0 10px;border-radius:3px;height:28px}.el-radio--mini.is-bordered .el-radio__inner{height:12px;width:12px}.el-radio+.el-radio{margin-left:30px}.el-radio__input{cursor:pointer;vertical-align:middle}.el-radio__input.is-disabled .el-radio__inner{cursor:not-allowed}.el-radio__input.is-disabled .el-radio__inner::after{cursor:not-allowed;background-color:#f5f7fa}.el-radio__input.is-disabled .el-radio__inner+.el-radio__label{cursor:not-allowed}.el-radio__input.is-disabled.is-checked .el-radio__inner::after{background-color:#c0c4cc}.el-radio__input.is-disabled+span.el-radio__label{color:#c0c4cc;cursor:not-allowed}.el-radio__input.is-checked .el-radio__inner{border-color:#5FB878;background:#5FB878}.el-radio__input.is-checked .el-radio__inner::after{-webkit-transform:translate(-50%,-50%) scale(1);transform:translate(-50%,-50%) scale(1)}.el-radio__input.is-checked+.el-radio__label{color:#5FB878}.el-radio__input.is-focus .el-radio__inner{border-color:#5FB878}.el-radio__inner{border:1px solid #dcdfe6;border-radius:100%;width:14px;height:14px;background-color:#fff;cursor:pointer;-webkit-box-sizing:border-box;box-sizing:border-box}.el-radio__inner:hover{border-color:#5FB878}.el-radio__inner::after{width:4px;height:4px;border-radius:100%;background-color:#fff;content:"";position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%) scale(0);transform:translate(-50%,-50%) scale(0);-webkit-transition:-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6);transition:-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6);transition:transform .15s cubic-bezier(.71,-.46,.88,.6);transition:transform .15s cubic-bezier(.71,-.46,.88,.6),-webkit-transform .15s cubic-bezier(.71,-.46,.88,.6)}.el-radio__original{opacity:0;outline:0;position:absolute;z-index:-1;top:0;left:0;right:0;bottom:0;margin:0}.el-radio:focus:not(.is-focus):not(:active) .el-radio__inner{-webkit-box-shadow:0 0 2px 2px #5FB878;box-shadow:0 0 2px 2px #5FB878}.el-radio__label{font-size:14px;padding-left:10px} -------------------------------------------------------------------------------- /front/src/assets/theme/rate.css: -------------------------------------------------------------------------------- 1 | .el-rate__icon,.el-rate__item{position:relative;display:inline-block}.el-rate{height:20px;line-height:1}.el-rate:active,.el-rate:focus{outline-width:0}.el-rate__item{font-size:0;vertical-align:middle}.el-rate__icon{font-size:18px;margin-right:6px;color:#c0c4cc;-webkit-transition:.3s;transition:.3s}.el-rate__decimal,.el-rate__icon .path2{position:absolute;top:0;left:0}.el-rate__icon.hover{-webkit-transform:scale(1.15);transform:scale(1.15)}.el-rate__decimal{display:inline-block;overflow:hidden}.el-rate__text{font-size:14px;vertical-align:middle} -------------------------------------------------------------------------------- /front/src/assets/theme/reset.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";body{font-family:"Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;font-weight:400;font-size:14px;color:#000}a{color:#5FB878;text-decoration:none}a:focus,a:hover{color:rgb(127, 198, 147)}a:active{color:rgb(86, 166, 108)}h1,h2,h3,h4,h5,h6{color:#606266;font-weight:inherit}h1:first-child,h2:first-child,h3:first-child,h4:first-child,h5:first-child,h6:first-child,p:first-child{margin-top:0}h1:last-child,h2:last-child,h3:last-child,h4:last-child,h5:last-child,h6:last-child,p:last-child{margin-bottom:0}h1{font-size:20px}h2{font-size:18px}h3{font-size:16px}h4,h5,h6,p{font-size:inherit}p{line-height:1.8}sub,sup{font-size:13px}small{font-size:12px}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee} -------------------------------------------------------------------------------- /front/src/assets/theme/row.css: -------------------------------------------------------------------------------- 1 | .el-row{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box}.el-row::after,.el-row::before{display:table;content:""}.el-row::after{clear:both}.el-row--flex{display:-webkit-box;display:-ms-flexbox;display:flex}.el-row--flex:after,.el-row--flex:before{display:none}.el-row--flex.is-justify-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-row--flex.is-justify-end{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.el-row--flex.is-justify-space-between{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.el-row--flex.is-justify-space-around{-ms-flex-pack:distribute;justify-content:space-around}.el-row--flex.is-align-middle{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-row--flex.is-align-bottom{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end} -------------------------------------------------------------------------------- /front/src/assets/theme/scrollbar.css: -------------------------------------------------------------------------------- 1 | .el-scrollbar{overflow:hidden;position:relative}.el-scrollbar:active>.el-scrollbar__bar,.el-scrollbar:focus>.el-scrollbar__bar,.el-scrollbar:hover>.el-scrollbar__bar{opacity:1;-webkit-transition:opacity 340ms ease-out;transition:opacity 340ms ease-out}.el-scrollbar__wrap{overflow:scroll;height:100%}.el-scrollbar__wrap--hidden-default::-webkit-scrollbar{width:0;height:0}.el-scrollbar__thumb{position:relative;display:block;width:0;height:0;cursor:pointer;border-radius:inherit;background-color:rgba(144,147,153,.3);-webkit-transition:.3s background-color;transition:.3s background-color}.el-scrollbar__thumb:hover{background-color:rgba(144,147,153,.5)}.el-scrollbar__bar{position:absolute;right:2px;bottom:2px;z-index:1;border-radius:4px;opacity:0;-webkit-transition:opacity 120ms ease-out;transition:opacity 120ms ease-out}.el-scrollbar__bar.is-vertical{width:6px;top:2px}.el-scrollbar__bar.is-vertical>div{width:100%}.el-scrollbar__bar.is-horizontal{height:6px;left:2px}.el-scrollbar__bar.is-horizontal>div{height:100%} -------------------------------------------------------------------------------- /front/src/assets/theme/select-dropdown.css: -------------------------------------------------------------------------------- 1 | .el-popper .popper__arrow,.el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.el-popper .popper__arrow::after{content:" ";border-width:6px}.el-popper[x-placement^=top]{margin-bottom:12px}.el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#ebeef5;border-bottom-width:0}.el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.el-popper[x-placement^=bottom]{margin-top:12px}.el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#ebeef5}.el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.el-popper[x-placement^=right]{margin-left:12px}.el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#ebeef5;border-left-width:0}.el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.el-popper[x-placement^=left]{margin-right:12px}.el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#ebeef5}.el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff}.el-select-dropdown{position:absolute;z-index:1001;border:1px solid #e4e7ed;border-radius:4px;background-color:#fff;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);-webkit-box-sizing:border-box;box-sizing:border-box;margin:5px 0}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected{color:#5FB878;background-color:#fff}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected.hover{background-color:#f5f7fa}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected::after{position:absolute;right:20px;font-family:element-icons;content:"\E611";font-size:12px;font-weight:700;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.el-select-dropdown .el-scrollbar.is-empty .el-select-dropdown__list{padding:0}.el-select-dropdown .popper__arrow{-webkit-transform:translateX(-400%);transform:translateX(-400%)}.el-select-dropdown.is-arrow-fixed .popper__arrow{-webkit-transform:translateX(-200%);transform:translateX(-200%)}.el-select-dropdown__empty{padding:10px 0;margin:0;text-align:center;color:#999;font-size:14px}.el-select-dropdown__wrap{max-height:274px}.el-select-dropdown__list{list-style:none;padding:6px 0;margin:0;-webkit-box-sizing:border-box;box-sizing:border-box} -------------------------------------------------------------------------------- /front/src/assets/theme/spinner.css: -------------------------------------------------------------------------------- 1 | .el-time-spinner{width:100%;white-space:nowrap}.el-spinner{display:inline-block;vertical-align:middle}.el-spinner-inner{-webkit-animation:rotate 2s linear infinite;animation:rotate 2s linear infinite;width:50px;height:50px}.el-spinner-inner .path{stroke:#ececec;stroke-linecap:round;-webkit-animation:dash 1.5s ease-in-out infinite;animation:dash 1.5s ease-in-out infinite}@-webkit-keyframes rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}100%{stroke-dasharray:90,150;stroke-dashoffset:-124}}@keyframes dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}100%{stroke-dasharray:90,150;stroke-dashoffset:-124}} -------------------------------------------------------------------------------- /front/src/assets/theme/steps.css: -------------------------------------------------------------------------------- 1 | .el-steps{display:-webkit-box;display:-ms-flexbox;display:flex}.el-steps--simple{padding:13px 8%;border-radius:4px;background:#f5f7fa}.el-steps--horizontal{white-space:nowrap}.el-steps--vertical{height:100%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-flow:column;flex-flow:column} -------------------------------------------------------------------------------- /front/src/assets/theme/submenu.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/theme/submenu.css -------------------------------------------------------------------------------- /front/src/assets/theme/switch.css: -------------------------------------------------------------------------------- 1 | .el-switch{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;position:relative;font-size:14px;line-height:20px;height:20px;vertical-align:middle}.el-switch.is-disabled .el-switch__core,.el-switch.is-disabled .el-switch__label{cursor:not-allowed}.el-switch__core,.el-switch__label{display:inline-block;cursor:pointer;vertical-align:middle}.el-switch__label{-webkit-transition:.2s;transition:.2s;height:20px;font-size:14px;font-weight:500;color:#303133}.el-switch__label.is-active{color:#5FB878}.el-switch__label--left{margin-right:10px}.el-switch__label--right{margin-left:10px}.el-switch__label *{line-height:1;font-size:14px;display:inline-block}.el-switch__input{position:absolute;width:0;height:0;opacity:0;margin:0}.el-switch__input:focus~.el-switch__core{outline:#5FB878 solid 1px}.el-switch__core{margin:0;position:relative;width:40px;height:20px;border:1px solid #dcdfe6;outline:0;border-radius:10px;-webkit-box-sizing:border-box;box-sizing:border-box;background:#dcdfe6;-webkit-transition:border-color .3s,background-color .3s;transition:border-color .3s,background-color .3s}.el-switch__core .el-switch__button{position:absolute;top:1px;left:1px;border-radius:100%;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;width:16px;height:16px;background-color:#fff}.el-switch.is-checked .el-switch__core{border-color:#5FB878;background-color:#5FB878}.el-switch.is-disabled{opacity:.6}.el-switch--wide .el-switch__label.el-switch__label--left span{left:10px}.el-switch--wide .el-switch__label.el-switch__label--right span{right:10px}.el-switch .label-fade-enter,.el-switch .label-fade-leave-active{opacity:0} -------------------------------------------------------------------------------- /front/src/assets/theme/tab-pane.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/src/assets/theme/tab-pane.css -------------------------------------------------------------------------------- /front/src/assets/theme/tag.css: -------------------------------------------------------------------------------- 1 | .el-tag{background-color:rgba(64,158,255,.1);display:inline-block;padding:0 10px;height:32px;line-height:30px;font-size:12px;color:#5FB878;border-radius:4px;-webkit-box-sizing:border-box;box-sizing:border-box;border:1px solid rgba(64,158,255,.2);white-space:nowrap}.el-tag .el-icon-close{border-radius:50%;text-align:center;position:relative;cursor:pointer;font-size:12px;height:16px;width:16px;line-height:16px;vertical-align:middle;top:-1px;right:-5px;color:#5FB878}.el-tag .el-icon-close::before{display:block}.el-tag .el-icon-close:hover{background-color:#5FB878;color:#fff}.el-tag--info,.el-tag--info .el-tag__close{color:#909399}.el-tag--info{background-color:rgba(144,147,153,.1);border-color:rgba(144,147,153,.2)}.el-tag--info.is-hit{border-color:#909399}.el-tag--info .el-tag__close:hover{background-color:#909399;color:#fff}.el-tag--success{background-color:rgba(103,194,58,.1);border-color:rgba(103,194,58,.2);color:#67c23a}.el-tag--success.is-hit{border-color:#67c23a}.el-tag--success .el-tag__close{color:#67c23a}.el-tag--success .el-tag__close:hover{background-color:#67c23a;color:#fff}.el-tag--warning{background-color:rgba(230,162,60,.1);border-color:rgba(230,162,60,.2);color:#e6a23c}.el-tag--warning.is-hit{border-color:#e6a23c}.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag--warning .el-tag__close:hover{background-color:#e6a23c;color:#fff}.el-tag--danger{background-color:rgba(245,108,108,.1);border-color:rgba(245,108,108,.2);color:#f56c6c}.el-tag--danger.is-hit{border-color:#f56c6c}.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag--danger .el-tag__close:hover{background-color:#f56c6c;color:#fff}.el-tag--medium{height:28px;line-height:26px}.el-tag--medium .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.el-tag--small{height:24px;padding:0 8px;line-height:22px}.el-tag--small .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.el-tag--mini{height:20px;padding:0 5px;line-height:19px}.el-tag--mini .el-icon-close{margin-left:-3px;-webkit-transform:scale(.7);transform:scale(.7)} -------------------------------------------------------------------------------- /front/src/assets/theme/tooltip.css: -------------------------------------------------------------------------------- 1 | .el-tooltip:focus:hover,.el-tooltip:focus:not(.focusing){outline-width:0}.el-tooltip__popper{position:absolute;border-radius:4px;padding:10px;z-index:2000;font-size:12px;line-height:1.2}.el-tooltip__popper .popper__arrow,.el-tooltip__popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-tooltip__popper .popper__arrow{border-width:6px}.el-tooltip__popper .popper__arrow::after{content:" ";border-width:5px}.el-tooltip__popper[x-placement^=top]{margin-bottom:12px}.el-tooltip__popper[x-placement^=top] .popper__arrow{bottom:-6px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-5px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=bottom]{margin-top:12px}.el-tooltip__popper[x-placement^=bottom] .popper__arrow{top:-6px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-5px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=right]{margin-left:12px}.el-tooltip__popper[x-placement^=right] .popper__arrow{left:-6px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=right] .popper__arrow::after{bottom:-5px;left:1px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=left]{margin-right:12px}.el-tooltip__popper[x-placement^=left] .popper__arrow{right:-6px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-5px;margin-left:-5px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper.is-dark{background:#303133;color:#fff}.el-tooltip__popper.is-light{background:#fff;border:1px solid #303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow{border-top-color:#303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow::after{border-top-color:#fff}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow{border-bottom-color:#303133}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow::after{border-bottom-color:#fff}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow{border-left-color:#303133}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow::after{border-left-color:#fff}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow{border-right-color:#303133}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow::after{border-right-color:#fff} -------------------------------------------------------------------------------- /front/src/assets/theme/tree.css: -------------------------------------------------------------------------------- 1 | .el-tree{cursor:default;background:#fff;color:#606266}.el-tree-node:focus>.el-tree-node__content,.el-tree-node__content:hover{background-color:#f5f7fa}.el-tree__empty-block{position:relative;min-height:60px;text-align:center;width:100%;height:100%}.el-tree__empty-text{position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);color:#6f7180}.el-tree-node{white-space:nowrap;outline:0}.el-tree-node__content{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:26px;cursor:pointer}.el-tree-node__content>.el-tree-node__expand-icon{padding:6px}.el-tree-node__content>.el-checkbox{margin-right:8px}.el-tree-node__expand-icon{cursor:pointer;color:#c0c4cc;font-size:12px;-webkit-transform:rotate(0);transform:rotate(0);-webkit-transition:-webkit-transform .3s ease-in-out;transition:-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out;transition:transform .3s ease-in-out,-webkit-transform .3s ease-in-out}.el-tree-node__expand-icon.expanded{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.el-tree-node__expand-icon.is-leaf{color:transparent;cursor:default}.el-tree-node__label{font-size:14px}.el-tree-node__loading-icon{margin-right:8px;font-size:14px;color:#c0c4cc}.el-tree-node>.el-tree-node__children{overflow:hidden;background-color:transparent}.el-tree-node.is-expanded>.el-tree-node__children{display:block}.el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content{background-color:#f0f7ff} -------------------------------------------------------------------------------- /front/src/components/BaseFooter.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 26 | 27 | 51 | -------------------------------------------------------------------------------- /front/src/components/article/ArticleItem.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 71 | 72 | 125 | -------------------------------------------------------------------------------- /front/src/components/card/CardArchive.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 59 | 60 | 102 | -------------------------------------------------------------------------------- /front/src/components/card/CardArticle.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 39 | 40 | 59 | -------------------------------------------------------------------------------- /front/src/components/card/CardCategory.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | 27 | 46 | -------------------------------------------------------------------------------- /front/src/components/card/CardFooter.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 42 | 43 | 116 | -------------------------------------------------------------------------------- /front/src/components/card/CardMe.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 83 | 84 | 107 | -------------------------------------------------------------------------------- /front/src/components/card/CardTag.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 61 | 62 | 88 | -------------------------------------------------------------------------------- /front/src/components/gotop/GoTop.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 51 | 52 | 91 | -------------------------------------------------------------------------------- /front/src/components/markdown/MarkdownEditor.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 54 | 64 | -------------------------------------------------------------------------------- /front/src/components/scrollpage/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 80 | 81 | 84 | -------------------------------------------------------------------------------- /front/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App' 3 | 4 | import router from './router' 5 | import store from './store' 6 | 7 | import lodash from 'lodash' 8 | 9 | import ElementUI, {Message} from 'element-ui' 10 | import '@/assets/theme/index.css' 11 | import '@/assets/iyicon/iconfont.css' 12 | import '@/assets/css/style.css' 13 | 14 | import {formatTime} from "./utils/time"; 15 | 16 | 17 | Vue.config.productionTip = false 18 | 19 | 20 | // 为了实现Class的私有属性 21 | const showMessage = Symbol('showMessage') 22 | 23 | 24 | // 重写ElementUI的Message 25 | // 如何让Element UI的Message消息提示每次只弹出一个:https://segmentfault.com/a/1190000020173021 26 | // single默认值true,因为项目需求,默认只弹出一个,可以根据实际需要设置 27 | 28 | class DonMessage { 29 | success(options, single = true) { 30 | this[showMessage]('success', options, single) 31 | } 32 | 33 | warning(options, single = true) { 34 | this[showMessage]('warning', options, single) 35 | } 36 | 37 | info(options, single = true) { 38 | this[showMessage]('info', options, single) 39 | } 40 | 41 | error(options, single = true) { 42 | this[showMessage]('error', options, single) 43 | } 44 | 45 | [showMessage](type, options, single) { 46 | if (single) { 47 | // 判断是否已存在Message 48 | if (document.getElementsByClassName('el-message').length === 0) { 49 | Message[type](options) 50 | } 51 | } else { 52 | Message[type](options) 53 | } 54 | } 55 | } 56 | 57 | Vue.use(ElementUI) 58 | Vue.prototype.$message = new DonMessage() 59 | Object.defineProperty(Vue.prototype, '$_', {value: lodash}) 60 | 61 | 62 | Vue.directive('title', function (el, binding) { 63 | document.title = el.dataset.title 64 | }) 65 | // 格式化时间 66 | Vue.filter('format', formatTime) 67 | 68 | new Vue({ 69 | el: '#app', 70 | router, 71 | store, 72 | template: '', 73 | components: {App} 74 | }) 75 | -------------------------------------------------------------------------------- /front/src/request/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import {Message} from 'element-ui' 3 | import store from '@/store' 4 | import {getToken} from '@/request/token' 5 | 6 | const service = axios.create({ 7 | baseURL: process.env.BASE_API, 8 | timeout: 10000 9 | }) 10 | 11 | service.interceptors.request.use(function (config) { 12 | // Do something before request is sent 13 | const token = getToken() 14 | if (token) { 15 | config.headers.Authorization = `Bearer ${token}` 16 | } 17 | return config 18 | }, function (error) { 19 | // Do something with request error 20 | return Promise.reject(error) 21 | }) 22 | 23 | // 响应拦截器 24 | // Add a response interceptor 25 | service.interceptors.response.use(function (response) { 26 | // Do something with response data 27 | return response.data 28 | }, function (error) { 29 | // Do something with response error 30 | if (error.response) { 31 | console.log(error.response,'error.response') 32 | // 匹配不同的响应码 33 | switch (error.response.status) { 34 | case 401: 35 | // 清除 Token 及 已认证 等状态 36 | store.dispatch('fedLogOut').then(data => { //获取用户信息 37 | next() 38 | }).catch(() => { 39 | console.log(error.response) 40 | }) 41 | break 42 | case 403: 43 | console.log(error) 44 | Message({ 45 | type: 'error', 46 | showClose: true, 47 | message: '你没有权限进行该项操作!' 48 | }) 49 | break 50 | case 404: 51 | Message({ 52 | type: 'warning', 53 | showClose: true, 54 | message: '404: Not Found' 55 | }) 56 | // 页面跳转 57 | window.location.href="/" 58 | break 59 | 60 | case 500: // 根本拿不到 500 错误,因为 CORs 不会过来 61 | Message({ 62 | type: 'warning', 63 | showClose: true, 64 | message: '500: Oops... INTERNAL SERVER ERROR' 65 | }) 66 | break 67 | } 68 | } else if (error.request) { 69 | console.log('不好意思,我挂了。😕') 70 | // Message({ 71 | // type: 'warning', 72 | // showClose: true, 73 | // message: '不好意思,我挂了。😕' 74 | // }) 75 | 76 | // Vue.toasted.error('The request has not been sent to Flask API,because OPTIONS get error', { icon: 'fingerprint' }) 77 | } else { 78 | console.log('Error: ', error.message) 79 | } 80 | return Promise.reject(error) 81 | }) 82 | 83 | export default service 84 | -------------------------------------------------------------------------------- /front/src/request/token.js: -------------------------------------------------------------------------------- 1 | /* 2 | 1、生命周期: 3 | localStorage的生命周期是永久的,关闭页面或浏览器之后localStorage中的数据也不会消失。localStorage除非主动删除数据,否则数据永远不会消失。 4 | 5 | sessionStorage的生命周期是在仅在当前会话下有效。sessionStorage引入了一个“浏览器窗口”的概念,sessionStorage是在同源的窗口中始终存在的数据。只要这个浏览器窗口没有关闭,即使刷新页面或者进入同源另一个页面,数据依然存在。但是sessionStorage在关闭了浏览器窗口后就会被销毁。同时独立的打开同一个窗口同一个页面,sessionStorage也是不一样的。 6 | 7 | 2、存储大小:localStorage和sessionStorage的存储数据大小一般都是:5MB 8 | 9 | 3、存储位置:localStorage和sessionStorage都保存在客户端,不与服务器进行交互通信。 10 | 11 | 4、存储内容类型:localStorage和sessionStorage只能存储字符串类型,对于复杂的对象可以使用ECMAScript提供的JSON对象的stringify和parse来处理 12 | 13 | 5、获取方式:localStorage:window.localStorage;;sessionStorage:window.sessionStorage;。 14 | 15 | 6、应用场景:localStoragese:常用于长期登录(+判断用户是否已登录),适合长期保存在本地的数据。sessionStorage:敏感账号一次性登录; 16 | 17 | */ 18 | export function getToken() { 19 | return localStorage.token 20 | } 21 | 22 | export function sessionToken() { 23 | return localStorage.token 24 | } 25 | 26 | export function setToken(token) { 27 | return localStorage.token = token 28 | } 29 | 30 | export function removeToken() { 31 | return localStorage.removeItem('token') 32 | } 33 | -------------------------------------------------------------------------------- /front/src/store/mutation-types.js: -------------------------------------------------------------------------------- 1 | export const SET_ACCOUNT = 'SET_ACCOUNT' 2 | export const SET_NAME = 'SET_NAME' 3 | export const SET_AVATAR = 'SET_AVATAR' 4 | export const SET_TOKEN = 'SET_TOKEN' -------------------------------------------------------------------------------- /front/src/utils/crypto.js: -------------------------------------------------------------------------------- 1 | import CryptoJS from "crypto-js"; 2 | 3 | const secretKey = '8M0jMkkyqkNrZGCZ3bL' 4 | 5 | export function encryptFunc(rawStr) { 6 | return CryptoJS.AES.encrypt(rawStr, secretKey).toString(); 7 | } 8 | 9 | export function decryptFunc(secretStr) { 10 | return CryptoJS.AES.decrypt(secretStr, secretKey).toString(CryptoJS.enc.Utf8) 11 | } 12 | -------------------------------------------------------------------------------- /front/src/utils/regexr.js: -------------------------------------------------------------------------------- 1 | export function isUrl(testStr) { 2 | // let reg = /^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(([A-Za-z0-9-~]+)\.)+([A-Za-z0-9-~\/])+$/; 3 | let reg = /^([--:\w?@%&+~#=]*\.[a-z]{2,4}\/{0,2})((?:[?&](?:\w+)=(?:\w+))+|[--:\w?@%&+~#=]+)?$/; 4 | return reg.test(testStr) 5 | } 6 | -------------------------------------------------------------------------------- /front/src/utils/time.js: -------------------------------------------------------------------------------- 1 | export function formatTime(time) { 2 | const d = new Date(time) 3 | const now = Date.now() 4 | 5 | const diff = (now - d) / 1000 6 | 7 | if (diff < 30) { 8 | return '刚刚' 9 | } else if (diff < 3600) { // less 1 hour 10 | return Math.ceil(diff / 60) + '分钟前' 11 | } else if (diff < 3600 * 24) { 12 | return Math.ceil(diff / 3600) + '小时前' 13 | } else if (diff < 3600 * 24 * 2) { 14 | return '1天前' 15 | } 16 | 17 | return time 18 | } 19 | -------------------------------------------------------------------------------- /front/src/views/MessageBoard.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 32 | 33 | 52 | -------------------------------------------------------------------------------- /front/src/views/NotFound.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | 24 | 79 | -------------------------------------------------------------------------------- /front/src/views/blog/BlogArchive.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 89 | 90 | 132 | -------------------------------------------------------------------------------- /front/src/views/blog/Tag.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 28 | 29 | 69 | 70 | 85 | -------------------------------------------------------------------------------- /front/src/views/common/ArticleScrollPage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 114 | 115 | 124 | -------------------------------------------------------------------------------- /front/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/.gitkeep -------------------------------------------------------------------------------- /front/static/category/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/category/back.png -------------------------------------------------------------------------------- /front/static/category/database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/category/database.png -------------------------------------------------------------------------------- /front/static/category/front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/category/front.png -------------------------------------------------------------------------------- /front/static/category/language.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/category/language.png -------------------------------------------------------------------------------- /front/static/category/lift.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/category/lift.jpg -------------------------------------------------------------------------------- /front/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/favicon.ico -------------------------------------------------------------------------------- /front/static/tag/css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/tag/css.png -------------------------------------------------------------------------------- /front/static/tag/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/tag/html.png -------------------------------------------------------------------------------- /front/static/tag/java.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/tag/java.png -------------------------------------------------------------------------------- /front/static/tag/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/tag/js.png -------------------------------------------------------------------------------- /front/static/tag/maven.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/tag/maven.png -------------------------------------------------------------------------------- /front/static/tag/vue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/tag/vue.png -------------------------------------------------------------------------------- /front/static/user/admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/user/admin.png -------------------------------------------------------------------------------- /front/static/user/user_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/user/user_1.png -------------------------------------------------------------------------------- /front/static/user/user_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/user/user_2.png -------------------------------------------------------------------------------- /front/static/user/user_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/user/user_3.png -------------------------------------------------------------------------------- /front/static/user/user_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/user/user_4.png -------------------------------------------------------------------------------- /front/static/user/user_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/user/user_5.png -------------------------------------------------------------------------------- /front/static/user/user_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imoyao/idealyard/7faec2cd705659062a05787d26c307ff8ff2c055/front/static/user/user_6.png -------------------------------------------------------------------------------- /front/test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // The assertion name is the filename. 3 | // Example usage: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // For more information on custom assertions see: 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | 10 | exports.assertion = function (selector, count) { 11 | this.message = 'Testing if element <' + selector + '> has count: ' + count 12 | this.expected = count 13 | this.pass = function (val) { 14 | return val === this.expected 15 | } 16 | this.value = function (res) { 17 | return res.value 18 | } 19 | this.command = function (cb) { 20 | var self = this 21 | return this.api.execute(function (selector) { 22 | return document.querySelectorAll(selector).length 23 | }, [selector], function (res) { 24 | cb.call(self, res) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /front/test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | var config = require('../../config') 3 | 4 | // http://nightwatchjs.org/gettingstarted#settings-file 5 | module.exports = { 6 | src_folders: ['test/e2e/specs'], 7 | output_folder: 'test/e2e/reports', 8 | custom_assertions_path: ['test/e2e/custom-assertions'], 9 | 10 | selenium: { 11 | start_process: true, 12 | server_path: require('selenium-server').path, 13 | host: '127.0.0.1', 14 | port: 4444, 15 | cli_args: { 16 | 'webdriver.chrome.driver': require('chromedriver').path 17 | } 18 | }, 19 | 20 | test_settings: { 21 | default: { 22 | selenium_port: 4444, 23 | selenium_host: 'localhost', 24 | silent: true, 25 | globals: { 26 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port) 27 | } 28 | }, 29 | 30 | chrome: { 31 | desiredCapabilities: { 32 | browserName: 'chrome', 33 | javascriptEnabled: true, 34 | acceptSslCerts: true 35 | } 36 | }, 37 | 38 | firefox: { 39 | desiredCapabilities: { 40 | browserName: 'firefox', 41 | javascriptEnabled: true, 42 | acceptSslCerts: true 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /front/test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing' 3 | 4 | const webpack = require('webpack') 5 | const DevServer = require('webpack-dev-server') 6 | 7 | const webpackConfig = require('../../build/webpack.prod.conf') 8 | const devConfigPromise = require('../../build/webpack.dev.conf') 9 | 10 | let server 11 | 12 | devConfigPromise.then(devConfig => { 13 | const devServerOptions = devConfig.devServer 14 | const compiler = webpack(webpackConfig) 15 | server = new DevServer(compiler, devServerOptions) 16 | const port = devServerOptions.port 17 | const host = devServerOptions.host 18 | return server.listen(port, host) 19 | }) 20 | .then(() => { 21 | // 2. run the nightwatch test suite against it 22 | // to run in additional browsers: 23 | // 1. add an entry in test/e2e/nightwatch.conf.js under "test_settings" 24 | // 2. add it to the --env flag below 25 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 26 | // For more information on Nightwatch's config file, see 27 | // http://nightwatchjs.org/guide#settings-file 28 | let opts = process.argv.slice(2) 29 | if (opts.indexOf('--config') === -1) { 30 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']) 31 | } 32 | if (opts.indexOf('--env') === -1) { 33 | opts = opts.concat(['--env', 'chrome']) 34 | } 35 | 36 | const spawn = require('cross-spawn') 37 | const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 38 | 39 | runner.on('exit', function (code) { 40 | server.close() 41 | process.exit(code) 42 | }) 43 | 44 | runner.on('error', function (err) { 45 | server.close() 46 | throw err 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /front/test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': function (browser) { 6 | // automatically uses dev Server port from /config.index.js 7 | // default: http://localhost:8080 8 | // see nightwatch.conf.js 9 | const devServer = browser.globals.devServerURL 10 | 11 | browser 12 | .url(devServer) 13 | .waitForElementVisible('#app', 5000) 14 | .assert.elementPresent('.hello') 15 | .assert.containsText('h1', 'Welcome to Your Vue.js App') 16 | .assert.elementCount('img', 1) 17 | .end() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /front/test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "globals": { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /front/test/unit/jest.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | rootDir: path.resolve(__dirname, '../../'), 5 | moduleFileExtensions: [ 6 | 'js', 7 | 'json', 8 | 'vue' 9 | ], 10 | moduleNameMapper: { 11 | '^@/(.*)$': '/src/$1' 12 | }, 13 | transform: { 14 | '^.+\\.js$': '/node_modules/babel-jest', 15 | '.*\\.(vue)$': '/node_modules/vue-jest' 16 | }, 17 | testPathIgnorePatterns: [ 18 | '/test/e2e' 19 | ], 20 | snapshotSerializers: ['/node_modules/jest-serializer-vue'], 21 | setupFiles: ['/test/unit/setup'], 22 | mapCoverage: true, 23 | coverageDirectory: '/test/unit/coverage', 24 | collectCoverageFrom: [ 25 | 'src/**/*.{js,vue}', 26 | '!src/main.js', 27 | '!src/router/index.js', 28 | '!**/node_modules/**' 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /front/test/unit/setup.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.config.productionTip = false 4 | -------------------------------------------------------------------------------- /front/test/unit/specs/HelloWorld.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import HelloWorld from '@/components/HelloWorld' 3 | 4 | describe('HelloWorld.vue', () => { 5 | it('should render correct contents', () => { 6 | const Constructor = Vue.extend(HelloWorld) 7 | const vm = new Constructor().$mount() 8 | expect(vm.$el.querySelector('.hello h1').textContent) 9 | .toEqual('Welcome to Your Vue.js App') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /gun.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by imoyao at 2019/8/7 15:40 4 | 5 | import os 6 | import gevent.monkey 7 | 8 | gevent.monkey.patch_all() 9 | 10 | import multiprocessing 11 | 12 | from back import setting 13 | 14 | if not os.path.exists('logs'): 15 | os.mkdir('logs') 16 | debug = True 17 | loglevel = 'debug' 18 | # 绑定的ip及端口号 19 | app_host = setting.FLASK_HOST 20 | app_port = setting.FLASK_PORT 21 | bind = ':'.join([app_host, app_port]) 22 | 23 | pidfile = 'gunicorn.pid' 24 | logfile = '/var/log/app/debug.log' 25 | 26 | # 启动的进程数 27 | workers = multiprocessing.cpu_count() * 2 + 1 28 | # 使用gevent模式 29 | worker_class = 'gunicorn.workers.ggevent.GeventWorker' 30 | # 环境变量 31 | # TODO: 环境变量只有部分可以正常识别,原因未知 32 | raw_env = [ 33 | # 用来保存 flask 相关的环境变量 34 | 'FLASK_APP=runserver', 35 | # 开启DEBUG模式 **注意**:生产模式下必须关闭 36 | 'FLASK_DEBUG=True', 37 | # 配置工作模式(此处默认开发模式) 38 | 'FLASK_ENV=production', 39 | # 使用的配置环境(默认使用生产配置) 40 | 'FLASK_CONFIG=production' 41 | ] 42 | # 项目的根目录,比如你的app.py文件在/home/ubuntu/app目录下,就填写'/home/ubuntu/app' 43 | # chdir = '' 44 | 45 | x_forwarded_for_header = 'X-FORWARDED-FOR' 46 | -------------------------------------------------------------------------------- /runserver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | 5 | from back import create_app, setting 6 | 7 | app = create_app(os.getenv('FLASK_CONFIG', 'default')) 8 | 9 | if __name__ == '__main__': 10 | host_ip = setting.HOST_IP 11 | app.run(host=host_ip) # TODO:此处需要写进环境变量 12 | -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | dotenv_path = os.path.join(os.path.dirname(__file__), '.env') 5 | if os.path.exists(dotenv_path): 6 | load_dotenv(dotenv_path) 7 | 8 | from back import create_app # noqa 9 | 10 | app = create_app('production') 11 | --------------------------------------------------------------------------------