Browse Source

import: 第一次提交

SongZihuan 3 years ago
commit
ab3b9f1c63
51 changed files with 2521 additions and 0 deletions
  1. 142 0
      .gitignore
  2. 48 0
      configure/__init__.py
  3. 0 0
      core/__init__.py
  4. 70 0
      core/blog.py
  5. 29 0
      core/comment.py
  6. 34 0
      core/file.py
  7. 30 0
      core/msg.py
  8. 127 0
      core/user.py
  9. 9 0
      main.py
  10. 9 0
      send_email/__init__.py
  11. 6 0
      sql/__init__.py
  12. 103 0
      sql/base.py
  13. 78 0
      sql/blog.py
  14. 27 0
      sql/comment.py
  15. 36 0
      sql/file.py
  16. 39 0
      sql/msg.py
  17. 158 0
      sql/mysql.py
  18. 48 0
      sql/user.py
  19. BIN
      static/images/hello.jpg
  20. 0 0
      static/styles/about_me/about_me.css
  21. 6 0
      static/styles/auth/login.css
  22. 6 0
      static/styles/auth/register.css
  23. 9 0
      static/styles/base.css
  24. 12 0
      static/styles/docx/article.css
  25. 38 0
      static/styles/docx/docx.css
  26. 15 0
      static/styles/file/file.css
  27. 106 0
      static/styles/index/hello.css
  28. 44 0
      static/styles/index/index.css
  29. 12 0
      static/styles/msg/msg.css
  30. 70 0
      templates/about_me/about_me.html
  31. 35 0
      templates/auth/login.html
  32. 38 0
      templates/auth/register.html
  33. 29 0
      templates/auth/yours.html
  34. 129 0
      templates/base.html
  35. 62 0
      templates/docx/article.html
  36. 80 0
      templates/docx/docx.html
  37. 4 0
      templates/email-msg/register.txt
  38. 48 0
      templates/file/file.html
  39. 19 0
      templates/index/error.html
  40. 27 0
      templates/index/hello.html
  41. 88 0
      templates/index/index.html
  42. 9 0
      templates/macro.html
  43. 53 0
      templates/msg/msg.html
  44. 11 0
      view/__init__.py
  45. 28 0
      view/about_me.py
  46. 140 0
      view/auth.py
  47. 70 0
      view/base.py
  48. 146 0
      view/docx.py
  49. 57 0
      view/file.py
  50. 65 0
      view/index.py
  51. 72 0
      view/msg.py

+ 142 - 0
.gitignore

@@ -0,0 +1,142 @@
+.idea
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+conf.json

+ 48 - 0
configure/__init__.py

@@ -0,0 +1,48 @@
+import json
+
+with open("conf.json", encoding='utf-8') as f:
+    json_str = f.read()
+    _conf: dict = json.loads(json_str)
+
+
+conf = dict()
+
+_mysql = _conf["mysql"]
+conf["mysql_url"] = str(_mysql["url"])
+conf["mysql_name"] = str(_mysql["name"])
+conf["mysql_passwd"] = str(_mysql["passwd"])
+conf["mysql_port"] = int(_mysql.get("port", 3306))
+
+_email = _conf["email"]
+conf["email_server"] = str(_email["server"])
+conf["email_port"] = int(_email["port"])
+conf["email_tls"] = bool(_email.get("tls", False))
+conf["email_ssl"] = bool(_email.get("ssl", False))
+conf["email_name"] = str(_email["name"])
+conf["email_passwd"] = str(_email["passwd"])
+conf["email_prefix"] = str(_email.get("prefix", "[HBlog]"))
+conf["email_sender"] = str(_email["sender"])
+
+conf["secret-key"] = str(_conf.get("secret-key", "HBlog-R-Salt"))
+
+introduce = _conf["info"]["introduce"]
+introduce_list = []
+for i in introduce:
+    describe: str = introduce[i]
+    describe = " ".join([f"<p>{i}</p>" for i in describe.split('\n')])
+    introduce_list.append((i, describe))
+
+conf["describe-link"] = _conf["info"]["link"]
+conf["describe-info"] = introduce_list
+
+conf["blog-name"] = _conf["info"]["blog-name"]
+conf["blog-describe"] = _conf["info"]["blog-describe"]
+
+conf["about-me-name"] = _conf["info"]["about-me"]["name"]
+conf["about-me-describe"] = _conf["info"]["about-me"]["describe"]
+
+conf["about-me-project"] = _conf["info"]["project"]
+conf["about-me-skill"] = _conf["info"]["skill"]
+conf["about-me-read"] = _conf["info"]["read"]
+
+conf["foot-info"] = _conf["info"]["foot-info"]

+ 0 - 0
core/__init__.py


+ 70 - 0
core/blog.py

@@ -0,0 +1,70 @@
+from sql.blog import (get_blog_list,
+                      get_blog_count,
+                      get_blog_list_with_file,
+                      get_blog_with_file_count,
+                      get_blog_list_not_top,
+                      read_blog,
+                      write_blog,
+                      get_blog_user_count)
+import core.user
+import core.file
+import core.comment
+
+
+class LoadBlogError(Exception):
+    pass
+
+
+def load_blog_by_id(blog_id) -> "BlogArticle":
+    blog_id = blog_id
+    blog = read_blog(blog_id)
+    if len(blog) == 0:
+        raise LoadBlogError
+
+    try:
+        auth = core.user.User.load_user_by_id(blog[0])
+    except core.user.LoaderUserError:
+        raise LoadBlogError
+
+    title = blog[1]
+    subtitle = blog[2]
+    context = blog[3]
+    update_time = blog[6]
+    top = blog[7]
+    comment = core.comment.load_comment_list(blog_id)
+    file = core.file.File.get_blog_file(blog_id)
+    return BlogArticle(blog_id, auth, title, subtitle, context, update_time, top, comment, file)
+
+
+class BlogArticle:
+    def __init__(self, blog_id, auth, title, subtitle, context, update_time=None, top=False, comment=None, file=None):
+        self.blog_id = blog_id
+        self.user = auth
+        self.title = title
+        self.subtitle = subtitle
+        self.context = context
+        self.update_time = update_time
+        self.top = top
+        self.comment = [] if comment is None else comment
+        self.file = [] if file is None else file
+
+    @staticmethod
+    def get_blog_list(file_id=None, limit=None, offset=None, not_top=False):
+        if file_id is None:
+            if not_top:
+                return get_blog_list_not_top(limit=limit, offset=offset)
+            return get_blog_list(limit=limit, offset=offset)
+        return get_blog_list_with_file(file_id, limit=limit, offset=offset)
+
+    @staticmethod
+    def get_blog_count(file_id=None, auth=None):
+        if file_id is None:
+            return get_blog_count()
+        if auth is None:
+            return get_blog_with_file_count(file_id)
+        return get_blog_user_count(auth.get_user_id())
+
+    def write_blog(self):
+        if self.blog_id is not None:  # 只有 blog_id为None时才使用
+            return False
+        return write_blog(self.user.get_user_id(), self.title, self.subtitle, self.context, self.file)

+ 29 - 0
core/comment.py

@@ -0,0 +1,29 @@
+from sql.comment import get_comment_list, write_comment, get_user_comment_count
+import core.user
+
+
+class LoadCommentError(Exception):
+    pass
+
+
+def load_comment_list(blog_id: int):
+    comment_list = get_comment_list(blog_id)
+    ret = []
+    for comment in comment_list:
+        ret.append(Comment(blog_id, core.user.User(comment[1], None, None, comment[0]), comment[2], comment[3]))
+    return ret
+
+
+class Comment:
+    def __init__(self, blog_id: int, auth: "core.user.User", context: str, update_time=None):
+        self.blog_id = blog_id
+        self.auth = auth
+        self.context = context
+        self.update_time = update_time
+
+    @staticmethod
+    def get_user_comment_count(auth: "core.user"):
+        return get_user_comment_count(auth.get_user_id())
+
+    def create_comment(self):
+        return write_comment(self.blog_id, self.auth.get_user_id(), self.context)

+ 34 - 0
core/file.py

@@ -0,0 +1,34 @@
+from sql.file import get_file_id, create_file, get_file_list, get_blog_file
+
+
+class LoadFileError(Exception):
+    ...
+
+
+def load_file_by_name(name: str) -> "File":
+    file_id, describe = get_file_id(name)
+    if file_id is None:
+        raise LoadFileError
+    return File(name, describe, file_id)
+
+
+class File:
+    def __init__(self, name, describe, file_id):
+        self.name = name
+        self.describe = describe
+        self.id = file_id
+
+    @staticmethod
+    def get_file_list():
+        return get_file_list()
+
+    def create_file(self):
+        return create_file(self.name, self.describe)
+
+    @staticmethod
+    def get_blog_file(blog_id: int):
+        file = get_blog_file(blog_id)
+        file_list = []
+        for i in file:
+            file_list.append(File(i[1], i[2], i[0]))
+        return file_list

+ 30 - 0
core/msg.py

@@ -0,0 +1,30 @@
+from typing import Optional
+
+from sql.msg import get_msg_list, get_msg_count, write_msg, get_user_msg_count
+import core.user
+
+
+def load_message_list(limit: Optional[int] = None, offset: Optional[int] = None, show_secret: bool = False):
+    msg = get_msg_list(limit=limit, offset=offset, show_secret=show_secret)
+    ret = []
+    for i in msg:
+        ret.append(Message(i[0], core.user.User(i[2], None, None, i[1]), i[3], i[5], i[4]))
+    return ret
+
+
+class Message:
+    def __init__(self, msg_id, auth: "core.user.User", context, secret, update_time):
+        self.msg_id = msg_id
+        self.auth = auth
+        self.context = context
+        self.secret = secret
+        self.update_time = update_time
+
+    @staticmethod
+    def get_msg_count(auth: "core.user" = None):
+        if auth is None:
+            return get_msg_count()
+        return get_user_msg_count(auth.get_user_id())
+
+    def create_msg(self):
+        return write_msg(self.auth.get_user_id(), self.context, self.secret)

+ 127 - 0
core/user.py

@@ -0,0 +1,127 @@
+from flask_login import UserMixin, AnonymousUserMixin
+from werkzeug.security import generate_password_hash, check_password_hash
+from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
+
+from configure import conf
+from sql.user import read_user, check_role, get_user_email, add_user, get_role_name
+import core.blog
+import core.comment
+import core.msg
+
+
+class LoaderUserError(Exception):
+    pass
+
+
+class AnonymousUser(AnonymousUserMixin):
+    def __init__(self):
+        super(AnonymousUser, self).__init__()
+        self.role = 3  # 默认角色
+        self.email = ""  # 无邮箱
+        self.passwd_hash = ""  # 无密码
+
+    def check_role(self, operate: str):
+        return check_role(self.role, operate)
+
+    @staticmethod
+    def get_user_id():
+        return 0
+
+
+def load_user_by_email(email: str) -> "User":
+    user = read_user(email)
+    if len(user) == 0:
+        raise LoaderUserError
+    passwd_hash = user[0]
+    role = user[1]
+    user_id = user[2]
+    return User(email, passwd_hash, role, user_id)
+
+
+class User(UserMixin):
+    def __init__(self, email, passwd_hash, role, user_id):
+        self.email = email
+        self.passwd_hash = passwd_hash
+        self.role = role
+        if role is not None:
+            self.role_name = get_role_name(role)
+        else:
+            self.role_name = None
+        self.id = user_id
+
+    def count_info(self):
+        msg = core.msg.Message.get_msg_count(self)
+        comment = core.comment.Comment.get_user_comment_count(self)
+        blog = core.blog.BlogArticle.get_blog_count(None, self)
+        return msg, comment, blog
+
+    @property
+    def s_email(self):
+        if len(self.email) <= 4:
+            return f"{self.email[0]}****"
+        else:
+            email = f"{self.email[0]}****{self.email[5:]}"
+            return email
+
+    @staticmethod
+    def load_user_by_id(user_id):
+        email = get_user_email(user_id)
+        if email is None:
+            raise LoaderUserError
+        return load_user_by_email(email)
+
+    @property
+    def comment_count(self):
+        return 0
+
+    @property
+    def blog_count(self):
+        return 0
+
+    @property
+    def msg_count(self):
+        return 0
+
+    @property
+    def is_active(self):
+        """Flask要求的属性, 表示用户是否激活(可登录), HGSSystem没有封禁用户系统, 所有用户都是被激活的"""
+        return True
+
+    @property
+    def is_authenticated(self):
+        """Flask要求的属性, 表示登录的凭据是否正确, 这里检查是否能 load_user_by_id"""
+        return True
+
+    def get_id(self):
+        """Flask要求的方法"""
+        return self.email
+
+    def get_user_id(self):
+        return self.id
+
+    @staticmethod
+    def creat_token(email: str, passwd_hash: str):
+        s = Serializer(conf["secret-key"], expires_in=3600)
+        return s.dumps({"email": email, "passwd_hash": passwd_hash})
+
+    @staticmethod
+    def load_token(token: str):
+        s = Serializer(conf["secret-key"], expires_in=3600)
+        try:
+            token = s.loads(token)
+            return token['email'], token['passwd_hash']
+        except Exception:
+            return None
+
+    @staticmethod
+    def get_passwd_hash(passwd: str):
+        return generate_password_hash(passwd)
+
+    def check_passwd(self, passwd: str):
+        return check_password_hash(self.passwd_hash, passwd)
+
+    def check_role(self, operate: str):
+        return check_role(self.role, operate)
+
+    def create_user(self):
+        return add_user(self.email, self.passwd_hash)

+ 9 - 0
main.py

@@ -0,0 +1,9 @@
+from view import WebApp
+
+
+web = WebApp(__name__)
+app = web.get_app()
+
+
+if __name__ == '__main__':
+    app.run()

+ 9 - 0
send_email/__init__.py

@@ -0,0 +1,9 @@
+from flask import render_template
+from flask_mail import Mail, Message
+from configure import conf
+
+
+def send_msg(title: str, mail: Mail, to, template, **kwargs):
+    message = Message(conf['email_prefix'] + title, sender=f"HBlog Admin <{conf['email_sender']}>", recipients=[to])
+    message.body = render_template("email-msg/" + f"{template}.txt", **kwargs)
+    mail.send(message)

+ 6 - 0
sql/__init__.py

@@ -0,0 +1,6 @@
+from sql.mysql import MysqlDB
+from configure import conf
+
+
+DB = MysqlDB
+db = DB(host=conf["mysql_url"], name=conf["mysql_name"], passwd=conf["mysql_passwd"], port=conf["mysql_port"])

+ 103 - 0
sql/base.py

@@ -0,0 +1,103 @@
+import abc
+from typing import List, Dict, Tuple, Union, Optional
+
+
+class DBException(Exception):
+    ...
+
+
+class DBDoneException(DBException):
+    ...
+
+
+class DBCloseException(DBException):
+    ...
+
+
+class DBBit:
+    BIT_0 = b'\x00'
+    BIT_1 = b'\x01'
+
+
+class Database(metaclass=abc.ABCMeta):
+    @abc.abstractmethod
+    def __init__(self, host: str, name: str, passwd: str, port: str):
+        self._host = str(host)
+        self._name = str(name)
+        self._passwd = str(passwd)
+        if port is None:
+            self._port = 3306
+        else:
+            self._port = int(port)
+
+    @abc.abstractmethod
+    def close(self):
+        """
+        关闭数据库, 此代码执行后任何成员函数再被调用其行为是未定义的
+        :return:
+        """
+        ...
+
+    @abc.abstractmethod
+    def is_connect(self) -> bool:
+        """
+        :return: 是否处于连接状态
+        """
+        ...
+
+    @abc.abstractmethod
+    def get_cursor(self) -> any:
+        """
+        :return: 返回数据库游标
+        """
+        ...
+
+    @abc.abstractmethod
+    def search(self, columns: List[str], table: str,
+               where: Union[str, List[str]] = None,
+               limit: Optional[int] = None,
+               offset: Optional[int] = None,
+               order_by: Optional[List[Tuple[str, str]]] = None):
+        """
+        执行 查询 SQL语句
+        :param columns: 列名称
+        :param table: 表
+        :param where: 条件
+        :param limit: 限制行数
+        :param offset: 偏移
+        :param order_by: 排序方式
+        :return:
+        """
+        ...
+
+    @abc.abstractmethod
+    def insert(self, table: str, columns: list, values: Union[str, List[str]]):
+        """
+        执行 插入 SQL语句, 并提交
+        :param table: 表
+        :param columns: 列名称
+        :param values: 数据
+        :return:
+        """
+        ...
+
+    @abc.abstractmethod
+    def delete(self, table: str, where: Union[str, List[str]] = None):
+        """
+        执行 删除 SQL语句, 并提交
+        :param table: 表
+        :param where: 条件
+        :return:
+        """
+        ...
+
+    @abc.abstractmethod
+    def update(self, table: str, kw: "Dict[str:str]", where: Union[str, List[str]] = None):
+        """
+        执行 更新 SQL语句, 并提交
+        :param table: 表
+        :param kw: 键值对
+        :param where: 条件
+        :return:
+        """
+        ...

+ 78 - 0
sql/blog.py

@@ -0,0 +1,78 @@
+from sql import db
+from typing import Optional, List
+import core.file
+
+
+def write_blog(auth_id: int, title: str, subtitle:str, context: str, file_list: List[core.file.File]) -> bool:
+    cur = db.insert(table="blog", columns=["Auth", "Title", "SubTitle", "Context"],
+                    values=f"{auth_id}, '{title}', '{subtitle}', '{context}'")
+    if cur is None or cur.rowcount == 0:
+        return False
+    blog_id = cur.lastrowid
+    for file in file_list:
+        cur = db.insert(table="blog_file", columns=["BlogID", "FileID"],
+                        values=f"{blog_id}, {file.id}")
+        if cur is None or cur.rowcount == 0:
+            return False
+    return True
+
+
+def read_blog(blog_id: int) -> list:
+    cur = db.search(columns=["Auth", "Title", "SubTitle", "Context", "Quote", "Spider", "UpdateTime", "Top"],
+                    table="blog",
+                    where=f"ID={blog_id}")
+    if cur is None or cur.rowcount == 0:
+        return []
+    return cur.fetchone()
+
+
+def get_blog_list(limit: Optional[int] = None, offset: Optional[int] = None) -> list:
+    cur = db.search(columns=["ID", "Title", "SubTitle", "UpdateTime", "Top"], table="blog_with_top",
+                    limit=limit,
+                    offset=offset)
+    if cur is None or cur.rowcount == 0:
+        return []
+    return cur.fetchall()
+
+
+def get_blog_list_not_top(limit: Optional[int] = None, offset: Optional[int] = None) -> list:
+    cur = db.search(columns=["ID", "Title", "SubTitle", "UpdateTime"], table="blog",
+                    order_by=[("UpdateTime", "DESC")],
+                    limit=limit,
+                    offset=offset)
+    if cur is None or cur.rowcount == 0:
+        return []
+    return cur.fetchall()
+
+
+def get_blog_count() -> int:
+    cur = db.search(columns=["count(ID)"], table="blog")
+    if cur is None or cur.rowcount == 0:
+        return 0
+    return cur.fetchone()[0]
+
+
+def get_blog_list_with_file(file_id, limit: Optional[int] = None, offset: Optional[int] = None) -> list:
+    cur = db.search(columns=["BlogID", "Title", "SubTitle", "UpdateTime", "Top"], table="blog_with_file",
+                    where=f"FileID={file_id}",
+                    limit=limit,
+                    offset=offset)
+    if cur is None or cur.rowcount == 0:
+        return []
+    return cur.fetchall()
+
+
+def get_blog_with_file_count(file_id) -> int:
+    cur = db.search(columns=["count(ID)"], table="blog_with_file",
+                    where=f"FileID={file_id}")
+    if cur is None or cur.rowcount == 0:
+        return 0
+    return cur.fetchone()[0]
+
+
+def get_blog_user_count(user_id: int) -> int:
+    cur = db.search(columns=["count(ID)"], table="blog",
+                    where=f"Auth={user_id}")
+    if cur is None or cur.rowcount == 0:
+        return 0
+    return cur.fetchone()[0]

+ 27 - 0
sql/comment.py

@@ -0,0 +1,27 @@
+from sql import db
+
+
+def get_comment_list(blog_id: int):
+    cur = db.search(columns=["Auth", "Email", "Context", "UpdateTime"],
+                    table="comment_user",
+                    where=f"BlogID={blog_id}")
+    if cur is None or cur.rowcount == 0:
+        return []
+    return cur.fetchall()
+
+
+def write_comment(blog_id: int, user_id: int, context: str):
+    cur = db.insert(table="comment",
+                    columns=["BlogID", "Auth", "Context"],
+                    values=f"{blog_id}, {user_id}, '{context}'")
+    if cur is None or cur.rowcount == 0:
+        return False
+    return True
+
+
+def get_user_comment_count(user_id: int):
+    cur = db.search(columns=["count(ID)"], table="comment",
+                    where=f"Auth={user_id}")
+    if cur is None or cur.rowcount == 0:
+        return 0
+    return cur.fetchone()[0]

+ 36 - 0
sql/file.py

@@ -0,0 +1,36 @@
+from sql import db
+from typing import Optional
+
+
+def create_file(name: str, describe: str):
+    cur = db.insert(table="file",
+                    columns=["Name", "DescribeText"],
+                    values=f"'{name}', '{describe}'")
+    if cur is None or cur.rowcount == 0:
+        return False
+    return True
+
+
+def get_blog_file(blog_id: int):
+    cur = db.search(columns=["FileID", "FileName", "DescribeText"], table="blog_file_with_name",
+                    where=f"BlogID={blog_id}")
+    if cur is None or cur.rowcount == 0:
+        return []
+    return cur.fetchall()
+
+
+def get_file_list(limit: Optional[int] = None, offset: Optional[int] = None):
+    cur = db.search(columns=["ID", "Name", "DescribeText", "Count"], table="file_with_count",
+                    limit=limit,
+                    offset=offset)
+    if cur is None or cur.rowcount == 0:
+        return []
+    return cur.fetchall()
+
+
+def get_file_id(name: str):
+    cur = db.search(columns=["ID", "DescribeText"], table="file",
+                    where=f"Name='{name}'")
+    if cur is None or cur.rowcount == 0:
+        return None, None
+    return cur.fetchone()

+ 39 - 0
sql/msg.py

@@ -0,0 +1,39 @@
+from sql import db
+from typing import Optional
+
+
+def get_msg_list(limit: Optional[int] = None, offset: Optional[int] = None, show_secret: bool = False):
+    if show_secret:
+        where = None
+    else:
+        where = "Secret=0"
+
+    cur = db.search(columns=["MsgID", "Auth", "Email", "Context", "UpdateTime", "Secret"], table="message_user",
+                    limit=limit,
+                    where=where,
+                    offset=offset)
+    if cur is None or cur.rowcount == 0:
+        return []
+    return cur.fetchall()
+
+
+def get_msg_count():
+    cur = db.search(columns=["count(ID)"], table="message")
+    if cur is None or cur.rowcount == 0:
+        return 0
+    return cur.fetchone()[0]
+
+
+def get_user_msg_count(user_id: int):
+    cur = db.search(columns=["count(ID)"], table="message",
+                    where=f"Auth={user_id}")
+    if cur is None or cur.rowcount == 0:
+        return 0
+    return cur.fetchone()[0]
+
+
+def write_msg(auth: int, context: str, secret: bool = False):
+    cur = db.insert(table="message",
+                    columns=["Auth", "Context", "Secret"],
+                    values=f"{auth}, '{context}', {1 if secret else 0}")
+    return cur is not None and cur.rowcount == 1

+ 158 - 0
sql/mysql.py

@@ -0,0 +1,158 @@
+import pymysql
+import threading
+import traceback
+from sql.base import Database, DBException, DBCloseException
+from typing import Optional, Union, List, Tuple, Dict
+
+
+class MysqlDB(Database):
+    def __init__(self,
+                 host: Optional[str],
+                 name: Optional[str],
+                 passwd: Optional[str],
+                 port: Optional[str]):
+        if host is None or name is None:
+            raise DBException
+        super(MysqlDB, self).__init__(host=host, name=name, passwd=passwd, port=port)
+        try:
+            self._db = pymysql.connect(user=self._name,
+                                       password=self._passwd,
+                                       host=self._host,
+                                       port=self._port,
+                                       database="HBlog")
+        except pymysql.err.OperationalError:
+            raise
+        self._cursor = self._db.cursor()
+        self._lock = threading.RLock()
+
+    def close(self):
+        if self._cursor is not None:
+            self._cursor.close()
+        if self._db is not None:
+            self._db.close()
+        self._db = None
+        self._cursor = None
+        self._lock = None
+
+    def is_connect(self) -> bool:
+        if self._cursor is None or self._db is None:
+            return False
+        return True
+
+    def get_cursor(self) -> pymysql.cursors.Cursor:
+        if self._cursor is None or self._db is None:
+            raise DBCloseException
+        return self._cursor
+
+    def search(self, columns: List[str], table: str,
+               where: Union[str, List[str]] = None,
+               limit: Optional[int] = None,
+               offset: Optional[int] = None,
+               order_by: Optional[List[Tuple[str, str]]] = None,
+               group_by: Optional[List[str]] = None,
+               for_update: bool = False):
+        if type(where) is list and len(where) > 0:
+            where: str = " WHERE " + " AND ".join(f"({w})" for w in where)
+        elif type(where) is str and len(where) > 0:
+            where = " WHERE " + where
+        else:
+            where: str = ""
+
+        if order_by is None:
+            order_by: str = ""
+        else:
+            by = [f" {i[0]} {i[1]} " for i in order_by]
+            order_by: str = " ORDER BY" + ", ".join(by)
+
+        if limit is None or limit == 0:
+            limit: str = ""
+        else:
+            limit = f" LIMIT {limit}"
+
+        if offset is None:
+            offset: str = ""
+        else:
+            offset = f" OFFSET {offset}"
+
+        if group_by is None:
+            group_by: str = ""
+        else:
+            group_by = "GROUP BY " + ", ".join(group_by)
+
+        columns: str = ", ".join(columns)
+        if for_update:
+            for_update = "FOR UPDATE"
+        else:
+            for_update = ""
+        return self.__search(f"SELECT {columns} "
+                             f"FROM {table} "
+                             f"{where} {group_by} {order_by} {limit} {offset} {for_update};")
+
+    def insert(self, table: str, columns: list, values: Union[str, List[str]], not_commit: bool = False):
+        columns: str = ", ".join(columns)
+        if type(values) is str:
+            values: str = f"({values})"
+        else:
+            values: str = ", ".join(f"{v}" for v in values)
+        return self.__done(f"INSERT INTO {table}({columns}) VALUES {values};", not_commit=not_commit)
+
+    def delete(self, table: str, where: Union[str, List[str]] = None, not_commit: bool = False):
+        if type(where) is list and len(where) > 0:
+            where: str = " AND ".join(f"({w})" for w in where)
+        elif type(where) is not str or len(where) == 0:  # 必须指定条件
+            return None
+
+        return self.__done(f"DELETE FROM {table} WHERE {where};", not_commit=not_commit)
+
+    def update(self, table: str, kw: "Dict[str:str]", where: Union[str, List[str]] = None, not_commit: bool = False):
+        if len(kw) == 0:
+            return None
+
+        if type(where) is list and len(where) > 0:
+            where: str = " AND ".join(f"({w})" for w in where)
+        elif type(where) is not str or len(where) == 0:  # 必须指定条件
+            return None
+
+        kw_list = [f"{key} = {kw[key]}" for key in kw]
+        kw_str = ", ".join(kw_list)
+        return self.__done(f"UPDATE {table} SET {kw_str} WHERE {where};", not_commit=not_commit)
+
+    def __search(self, sql) -> Union[None, pymysql.cursors.Cursor]:
+        if self._cursor is None or self._db is None:
+            raise DBCloseException
+
+        try:
+            self._lock.acquire()  # 上锁
+            self._cursor.execute(sql)
+        except pymysql.MySQLError:
+            print(f"sql='{sql}'")
+            traceback.print_exc()
+            return None
+        finally:
+            self._lock.release()  # 释放锁
+        return self._cursor
+
+    def __done(self, sql, not_commit: bool = False) -> Union[None, pymysql.cursors.Cursor]:
+        if self._cursor is None or self._db is None:
+            raise DBCloseException
+
+        try:
+            self._lock.acquire()
+            self._cursor.execute(sql)
+        except pymysql.MySQLError:
+            self._db.rollback()
+            print(f"sql={sql}")
+            traceback.print_exc()
+            return None
+        finally:
+            if not not_commit:
+                self._db.commit()
+            self._lock.release()
+        return self._cursor
+
+    def commit(self):
+        try:
+            self._lock.acquire()
+            self._db.commit()
+        finally:
+            self._lock.release()

+ 48 - 0
sql/user.py

@@ -0,0 +1,48 @@
+from sql import db
+from sql.base import DBBit
+import core.user
+
+
+def get_user_email(user_id):
+    cur = db.search(columns=["Email"], table="user", where=f"ID='{user_id}'")
+    if cur is None or cur.rowcount == 0:
+        return None
+    return cur.fetchone()[0]
+
+
+def read_user(email: str):
+    cur = db.search(columns=["PasswdHash", "Role", "ID"], table="user", where=f"Email='{email}'")
+    if cur is None or cur.rowcount == 0:
+        return []
+    assert cur.rowcount == 1
+    return cur.fetchone()
+
+
+def add_user(email: str, passwd: str):
+    cur = db.search(columns=["count(Email)"], table="user")  # 统计个数
+    passwd = core.user.User.get_passwd_hash(passwd)
+    if cur is None or cur.rowcount == 0 or cur.fetchone()[0] == 0:
+        db.insert(table='user', columns=['Email', 'PasswdHash', 'Role'], values=f"'{email}', '{passwd}', 1")  # 创建为管理员用户
+    else:
+        db.insert(table='user', columns=['Email', 'PasswdHash'], values=f"'{email}', '{passwd}'")
+
+
+def get_role_name(role: int):
+    cur = db.search(columns=["RoleName"], table="role", where=f"RoleID={role}")
+    if cur is None or cur.rowcount == 0:
+        return None
+    return cur.fetchone()[0]
+
+
+def check_role(role: int, operate: str):
+    cur = db.search(columns=[operate], table="role", where=f"RoleID={role}")
+    if cur is None or cur.rowcount == 0:
+        return False
+    return cur.fetchone()[0] == DBBit.BIT_1
+
+
+def check_role_by_name(role: str, operate: str):
+    cur = db.search(columns=[operate], table="role", where=f"RoleName='{role}")
+    if cur is None or cur.rowcount == 0:
+        return False
+    return cur.fetchone()[0] == DBBit.BIT_1

BIN
static/images/hello.jpg


+ 0 - 0
static/styles/about_me/about_me.css


+ 6 - 0
static/styles/auth/login.css

@@ -0,0 +1,6 @@
+.login-form {
+    background-color: white;
+    border-radius: 10px;
+    border: 2px solid #6b6882;
+    padding: 15px;
+}

+ 6 - 0
static/styles/auth/register.css

@@ -0,0 +1,6 @@
+.register-form {
+    background-color: white;
+    border-radius: 10px;
+    border: 2px solid #6b6882;
+    padding: 15px;
+}

+ 9 - 0
static/styles/base.css

@@ -0,0 +1,9 @@
+body {
+    overflow-y: scroll;
+    overflow-x: hidden;
+}
+
+#foot {
+    text-align: center;
+    padding-bottom: 10px;
+}

+ 12 - 0
static/styles/docx/article.css

@@ -0,0 +1,12 @@
+.comment {
+    margin-bottom: 20px;
+    border-radius: 10px;
+    border: 2px solid #FFDC00;
+    padding: 15px;
+    background-color: white;
+}
+
+.comment .comment-title {
+    color: black;
+    text-decoration: none;
+}

+ 38 - 0
static/styles/docx/docx.css

@@ -0,0 +1,38 @@
+.docx, .docx-top {
+    margin-bottom: 20px;
+    border-radius: 10px;
+    border-width: 2px;
+    border-style: solid;
+
+    min-width: 30em;
+    height: 4em;
+    line-height: 4em;
+}
+
+.docx {
+    border-color: #0074D9;
+    background-color: white;
+}
+
+.docx-top {
+    border-color: #dc3023;
+    background-color: white;
+}
+
+.docx a, .docx-top a{
+    padding: 15px;
+    color: black;
+    text-decoration: none;
+}
+
+.docx a:hover, .docx-top a:hover {
+    color: darkblue;
+    text-decoration: none;
+}
+
+.markdown {
+    background-color: white;
+    padding: 15px;
+    border: 2px solid #0074D9;
+    border-radius: 10px;
+}

+ 15 - 0
static/styles/file/file.css

@@ -0,0 +1,15 @@
+.file {
+    background-color: white;
+    border-radius: 10px;
+    border: 2px solid #39CCCC;
+    min-width: 30%;
+    min-height: 20%;
+    padding: 15px;
+}
+
+.create {
+    background-color: white;
+    border-radius: 10px;
+    border: 2px solid #1685a9;
+    padding: 15px;
+}

+ 106 - 0
static/styles/index/hello.css

@@ -0,0 +1,106 @@
+body {
+    background-image: url("../../images/hello.jpg");
+}
+
+#base {
+    position: relative;
+}
+
+@keyframes shaking {
+    from {
+        transform: rotate(0deg);
+    }
+
+    20% {
+        transform: rotate(-3deg);
+    }
+
+    40% {
+        transform: rotate(0deg);
+    }
+
+    60% {
+        transform: rotate(3deg);
+    }
+
+    80% {
+        transform: rotate(0deg);
+    }
+
+    to {
+        transform: rotate(0deg);
+    }
+}
+
+#title-1 {
+    text-decoration: none;
+    border-bottom: black solid 3px;
+    padding-bottom: 15px;
+
+    width: 96%;
+    margin: auto;
+
+    font-weight: bold;
+    text-align: center;
+    color: black;
+    font-size: 50px;
+    padding-top: 5%;
+}
+
+#title-1:hover {
+    animation: shaking 300ms linear 0s 1;
+}
+
+#btn {
+    position: relative;
+    display: block;
+    text-align: center;
+
+    border-radius: 10px;
+    border-width: 3px;
+    border-style: ridge;
+    border-color: rgb(123, 104, 238);
+
+    background-image: linear-gradient(to right, #F0FFF0 0%, #FFDC00 30%, #FFDC00 100%);
+
+    font-size: 30px;
+    height: 100px;
+
+    box-shadow: 5px 5px 2px 0 black;
+}
+
+@media all and (max-width: 992px) {
+    #btn {
+        top: 120px;
+        left: 3%;
+        width: 94%;
+    }
+}
+
+@media not all and (max-width: 992px) {
+    #btn {
+        top: 120px;
+        left: 25%;
+        width: 50%;
+        transition:
+                transform 1s,
+                background-image 1s;
+    }
+
+    #btn:hover {
+        background-image: linear-gradient(to right, #F0FFF0 0%, #F0FFF0 70%, #FFDC00 100%);
+        transform: scale(1.5);
+    }
+}
+
+#main {
+    text-align: center;
+}
+
+#title-section {
+    height: 50vh;
+    width: 50%;
+    margin: 10% auto;
+    background-color: rgb(255, 255, 255, 20%);
+    border-radius: 10px;
+}

+ 44 - 0
static/styles/index/index.css

@@ -0,0 +1,44 @@
+.docx {
+    margin-bottom: 20px;
+    border-radius: 10px;
+    border: 2px solid #0074D9;
+    background-color: white;
+
+    min-width: 30em;
+    height: 4em;
+    line-height: 4em;
+}
+
+.docx a{
+    padding: 15px;
+}
+
+.docx a, .msg .msg-title {
+    color: black;
+    text-decoration: none;
+}
+
+.docx a:hover {
+    color: darkblue;
+    text-decoration: none;
+}
+
+.msg {
+    margin-bottom: 20px;
+    border-radius: 10px;
+    border: 2px solid #FFDC00;
+    padding: 15px;
+    background-color: white;
+}
+
+.introduce {
+    margin-bottom: 20px;
+    border-radius: 10px;
+    border: 2px solid #3D9970;
+    padding: 15px;
+    background-color: white;
+}
+
+.introduce p{
+    text-indent: 2em;
+}

+ 12 - 0
static/styles/msg/msg.css

@@ -0,0 +1,12 @@
+.msg {
+    margin-bottom: 20px;
+    border-radius: 10px;
+    border: 2px solid #FFDC00;
+    padding: 15px;
+    background-color: white;
+}
+
+.msg .msg-title {
+    color: black;
+    text-decoration: none;
+}

+ 70 - 0
templates/about_me/about_me.html

@@ -0,0 +1,70 @@
+{% extends "base.html" %}
+
+{% block title %} 关于我 {% endblock %}
+
+{% block style %}
+    {{ super() }}
+    <link href="{{ url_for('static', filename='styles/about_me/about_me.css') }}" rel="stylesheet">
+{% endblock %}
+
+{% block context %}
+    <section id="base" class="container mt-3">
+    <div class="row">
+        <div class="col-12 col-lg-6">
+            <div class="card mr-lg-2 mb-3">
+                <div class="card-header"> 关于我 </div>
+                <div class="card-body">
+                    <h4 class="card-title"> {{ conf['about-me-name'] }} </h4>
+                    <h5 class="card-subtitle"> 博主 </h5>
+                    <p class="card-text"> {{ conf['about-me-describe'] }} </p>
+
+
+                    {% for link in conf['describe-link'] %}
+                        <a class="card-link" href="{{ conf['describe-link'][link] }}"> {{ link }} </a>
+                    {% endfor %}
+                </div>
+            </div>
+
+            <div class="card mr-lg-2 mb-3 mb-lg-0">
+                <div class="card-header"> 项目 </div>
+                <div class="card-body">
+                    {% for p in conf["about-me-project"] %}
+                        <h4 class="card-title"> {{ p }}-项目 </h4>
+                        <ul>
+                            {% for i in conf["about-me-project"][p] %}
+                                <li class="card-text"> {{ i }} </li>
+                            {% endfor %}
+                        </ul>
+                    {% endfor %}
+                </div>
+            </div>
+        </div>
+
+        <div class="col-12 col-lg-6">
+            <div class="card ml-lg-2 mb-3">
+                <div class="card-header"> 技术栈 </div>
+                <div class="card-body">
+                    {% for p in conf["about-me-skill"] %}
+                        <h4 class="card-title"> {{ p }}-项目 </h4>
+                        <ul>
+                            {% for i in conf["about-me-skill"][p] %}
+                                <li class="card-text"> {{ i }} </li>
+                            {% endfor %}
+                        </ul>
+                    {% endfor %}
+                </div>
+            </div>
+            <div class="card ml-lg-2">
+                <div class="card-header"> 阅读栈 </div>
+                <div class="card-body">
+                    <ul>
+                        {% for i in conf["about-me-read"] %}
+                            <li class="card-text"> 《{{ i }}》 </li>
+                        {% endfor %}
+                    </ul>
+                </div>
+            </div>
+        </div>
+    </div>
+    </section>
+{% endblock %}

+ 35 - 0
templates/auth/login.html

@@ -0,0 +1,35 @@
+{% extends "base.html" %}
+
+{% block title %} 登录 {% endblock %}
+
+{% block style %}
+    {{ super() }}
+    <link href="{{ url_for('static', filename='styles/auth/login.css') }}" rel="stylesheet">
+{% endblock %}
+
+{% block context %}
+    <section id="base" class="container mt-3">
+    <div class="row">
+        <div class="col-12 col-lg-6 offset-lg-3">
+            <form method="post" action="{{ url_for("auth.login_page") }}" class="login-form">
+                {{ form.hidden_tag() }}
+
+                <div class="form-group">
+                    {{ form.email.label }}
+                    {{ form.email(class="form-control") }}
+                </div>
+
+                <div class="form-group">
+                    {{ form.passwd.label }}
+                    {{ form.passwd(class="form-control") }}
+                </div>
+
+                <a class="btn btn-info mr-2" href="{{ url_for("auth.register_page") }}"> 前往注册 </a>
+                {{ form.submit(class='btn btn-info mr-2') }}
+                {{ form.remember() }} {{ form.remember.label }}
+
+            </form>
+        </div>
+    </div>
+    </section>
+{% endblock %}

+ 38 - 0
templates/auth/register.html

@@ -0,0 +1,38 @@
+{% extends "base.html" %}
+
+{% block title %} 注册 {% endblock %}
+
+{% block style %}
+    {{ super() }}
+    <link href="{{ url_for('static', filename='styles/auth/register.css') }}" rel="stylesheet">
+{% endblock %}
+
+{% block context %}
+    <section id="base" class="container mt-3">
+    <div class="row">
+        <div class="col-12 col-lg-6 offset-lg-3">
+            <form method="post" action="{{ url_for("auth.register_page") }}" class="register-form">
+                {{ RegisterForm.hidden_tag() }}
+
+                <div class="form-group">
+                    {{ RegisterForm.email.label }}
+                    {{ RegisterForm.email(class="form-control") }}
+                </div>
+
+                <div class="form-group">
+                    {{ RegisterForm.passwd.label }}
+                    {{ RegisterForm.passwd(class="form-control") }}
+                </div>
+
+                <div class="form-group">
+                    {{ RegisterForm.passwd_again.label }}
+                    {{ RegisterForm.passwd_again(class="form-control") }}
+                </div>
+
+                {{ RegisterForm.submit(class='btn btn-info mr-2') }}
+                <a class="btn btn-info mr-2" href="{{ url_for("auth.login_page") }}"> 前往登录 </a>
+            </form>
+        </div>
+    </div>
+    </section>
+{% endblock %}

+ 29 - 0
templates/auth/yours.html

@@ -0,0 +1,29 @@
+{% extends "base.html" %}
+
+{% block title %} 关于你 {% endblock %}
+
+{% block style %}
+    {{ super() }}
+    <link href="{{ url_for('static', filename='styles/about_me/about_me.css') }}" rel="stylesheet">
+{% endblock %}
+
+{% block context %}
+    <section id="base" class="container mt-3">
+    <div class="row">
+        <div class="col-12 col-lg-6 offset-lg-3">
+            <div class="card mr-lg-2 mb-3">
+                <div class="card-header"> 你的 </div>
+                <div class="card-body">
+                    <h4 class="card-title mb-2"> {{ current_user.email }} </h4>
+                    <p class="card-text mb-2"> 用户组:{{ current_user.role_name }} </p>
+                    <p class="card-text mb-2"> 评论条数:{{ comment_count }} </p>
+                    <p class="card-text mb-2"> 留言条数:{{ msg_count }} </p>
+                    <p class="card-text mb-2"> 博客:{{ blog_count }} </p>
+
+                    <a class="card-link" href="{{ url_for('auth.logout_page') }}"> 退出登录 </a>
+                </div>
+            </div>
+        </div>
+    </div>
+    </section>
+{% endblock %}

+ 129 - 0
templates/base.html

@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<html lang="zh">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+
+    {% block font %}
+        <link rel="preconnect" href="https://fonts.googleapis.com">
+        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+        <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@100;400&display=swap" rel="stylesheet">
+    {% endblock %}
+
+    {% block style %}
+        <link href="https://cdn.staticfile.org/bootstrap/4.6.1/css/bootstrap.min.css" rel="stylesheet">
+        <link href="{{ url_for('static', filename='styles/base.css') }}" rel="stylesheet">
+        <style>
+            html {
+                font-family: 'Noto Sans SC', sans-serif;
+            }
+            body{
+                background-color: #f0fcff;
+            }
+    </style>
+    {% endblock %}
+
+    <title>{% block title %} {% endblock %} - {{ blog_name }} </title>
+</head>
+<body>
+{% block nav %}
+    <h1 class="text-center mt-2"> 《{{ blog_name }}》—— <small>{{ blog_describe }}</small> </h1>
+
+    <header>
+        <nav>
+            <ul class="nav nav-tabs justify-content-center mt-3" id="top-nav">
+                <li class="nav-item">
+                    <a href="{{ url_for('base.index_page') }}" class="nav-link {{ top_nav[0] }}">
+                        首页
+                    </a>
+                </li>
+
+                {% if current_user.check_role("ReadBlog") %}
+                <li class="nav-item">
+                    <a href="{{ url_for('file.file_page') }}" class="nav-link {{ top_nav[1] }}">
+                        归档
+                    </a>
+                </li>
+
+                <li class="nav-item">
+                    <a href="{{ url_for('docx.docx_page', page=1) }}" class="nav-link {{ top_nav[2] }}">
+                        文章
+                    </a>
+                </li>
+                {% endif %}
+
+                <li class="nav-item">
+                    <a href="{{ url_for('msg.msg_page', page=1) }}" class="nav-link {{ top_nav[3] }}">
+                        留言
+                    </a>
+                </li>
+
+                <li class="nav-item">
+                    <a href="{{ url_for('about_me.about_me_page') }}" class="nav-link {{ top_nav[4] }}">
+                        关于我
+                    </a>
+                </li>
+
+                <li class="nav-item">
+                    {% if current_user.is_authenticated %}
+                        <a href="{{ url_for('auth.yours_page') }}" class="nav-link {{ top_nav[5] }}">
+                            你的
+                        </a>
+                    {% else %}
+                        <a href="{{ url_for('auth.login_page') }}" class="nav-link {{ top_nav[5] }}">
+                            登录
+                        </a>
+                    {% endif %}
+                </li>
+            </ul>
+        </nav>
+
+        <section class="container mt-2 mb-2">
+        {% for message in get_flashed_messages() %}
+            <div class="alert alert-info">
+                <button type="button" class="close" data-dismiss="alert">&times;</button>
+                {{ message }}
+            </div>
+        {% endfor %}
+        </section>
+    </header>
+{% endblock %}
+
+<div id="context" class="mb-2">
+    {% block context %} {% endblock %}
+</div>
+
+{% block footer %}
+    <footer id="foot">
+        <hr>
+        {{ conf['foot-info'] }}
+    </footer>
+    <script>
+        let context = document.getElementById('context')
+        let foot = document.getElementById('foot')
+        let context_height = context.getBoundingClientRect().bottom
+        let win_height = 0
+        if (window.innerHeight)
+            win_height = window.innerHeight;
+        else if ((document.body) && (document.body.clientHeight))
+            win_height = document.body.clientHeight;
+
+        console.log(context_height, win_height, foot.clientHeight, (win_height - context_height - foot.clientHeight).toString())
+        if (win_height - context_height - foot.clientHeight <= 0)
+            foot.style.marginTop = "0"
+        else {
+            foot.style.marginTop = (win_height - context_height - foot.clientHeight).toString() + "px"
+            console.log(foot.style.marginTop)
+            console.log("HAHAH")
+        }
+    </script>
+{% endblock %}
+
+{% block javascript %}
+    <script src="https://cdn.staticfile.org/popper.js/0.2.0/popper.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/1.10.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/bootstrap/4.6.1/js/bootstrap.min.js"></script>
+{% endblock %}
+
+</body>
+</html>

+ 62 - 0
templates/docx/article.html

@@ -0,0 +1,62 @@
+{% extends "base.html" %}
+{% import "macro.html" as macro %}
+
+{% block title %} 文档 {% endblock %}
+
+{% block style %}
+    {{ super() }}
+    <link href="{{ url_for('static', filename='styles/docx/article.css') }}" rel="stylesheet">
+{% endblock %}
+
+{% block context %}
+    <section id="base" class="container mt-3">
+        {% if current_user.check_role("ReadBlog") %}
+            {# 检查是否具有读取权限 #}
+            <div class="row">
+                <article class="col-12">
+                    <h1> {{ article.title }} <small> {{ article.subtitle }} <small> {{ article.update_time}} </small> </small> </h1>
+                    {% for file in article.file %}
+                        <span class="badge badge-info"> {{ file.name }} </span>
+                    {% endfor %}
+                    <hr>
+
+                    {{ article.context | safe }}
+                </article>
+            </div>
+        {% endif %}
+
+        {% if current_user.check_role("ReadComment") %}
+            <div class="row">
+                <article class="col-12">
+                    <h1 class="mt-3"> 评论 </h1>
+
+                    <section class="col-12 text-right">
+                        <form action="{{ url_for('docx.comment_page', blog=article.blog_id) }}" method="post">
+                            {{ form.hidden_tag() }}
+                            {{ form.context(class="form-control mb-2", rows="3") }}
+                            {{ form.submit(class="btn btn-info mb-2", value="评论") }}
+                        </form>
+                    </section>
+                    <hr>
+
+                    {% for comment in article.comment %}
+                        <section class="col-12">
+                            <div class="comment">
+                                <p class="comment-title h5">
+                                    {% if current_user.check_role("ReadUserInfo") %}  {# 判断是否可读取用户信息 #}
+                                        {{ comment.auth.email }}
+                                    {% else %}
+                                        {{ comment.auth.s_email }}
+                                    {% endif %}
+                                    <br>
+                                    <small> {{ comment.update_time }} </small>
+                                </p>
+                                <p> {{ comment.context.replace('\n', '<br>') | safe  }} </p>
+                            </div>
+                        </section>
+                    {% endfor %}
+                </article>
+            </div>
+        {% endif %}
+    </section>
+{% endblock %}

+ 80 - 0
templates/docx/docx.html

@@ -0,0 +1,80 @@
+{% extends "base.html" %}
+{% import "macro.html" as macro %}
+
+{% block title %} 博客 {% endblock %}
+
+{% block style %}
+    {{ super() }}
+    <link href="{{ url_for('static', filename='styles/docx/docx.css') }}" rel="stylesheet">
+{% endblock %}
+
+{% block context %}
+    <section id="base" class="container mt-3">
+        {% if form and current_user.check_role("WriteBlog") %}
+            {# 判断是否有权限写博客 #}
+            <div class="row">
+                <div class="col-12">
+                    <div class="markdown">
+                        <form action="{{ url_for('docx.create_docx_page') }}" method="post">
+                            {{ form.hidden_tag() }}
+                            <div class="form-group">
+                                {{ form.title.label }}
+                                {{ form.title(class="form-control") }}
+                            </div>
+
+                            <div class="form-group">
+                                {{ form.subtitle.label }}
+                                {{ form.subtitle(class="form-control") }}
+                            </div>
+
+                            <div class="form-group">
+                                {{ form.file.label }}
+                                {{ form.file(class="form-control") }}
+                            </div>
+
+                            {{ form.context(class="form-control mb-2", rows="15") }}
+
+                            <div class="text-right">
+                                {{ form.submit(class="btn btn-info mb-2", value="发表") }}
+                            </div>
+                        </form>
+                    </div>
+                </div>
+            </div>
+            <hr>
+        {% endif %}
+
+        {% if current_user.check_role("ReadBlog") %}
+            {# 检查是否具有读取权限 #}
+            <div class="row">
+                <article class="col-12">
+                {% for blog in blog_list %}
+                    {% if blog[4] == is_top %}
+                        <div class="docx-top"> <a class="h4"
+                                                          href="{{ url_for("docx.article_page", blog_id=blog[0]) }}">
+                            {{ blog[1] }}
+                                <small> {{ blog[2] }}
+                                    <small> {{ blog[3] }} </small> </small> </a></div>
+                    {% else %}
+                        <div class="docx"> <a class="h4"
+                                                      href="{{ url_for("docx.article_page", blog_id=blog[0]) }}">
+                            {{ blog[1] }}
+                                <small> {{ blog[2] }}
+                                    <small> {{ blog[3] }} </small> </small> </a></div>
+                    {% endif %}
+                {% endfor %}
+                </article>
+            </div>
+
+            <ul class="pagination col-12 justify-content-center">
+                {{ macro.get_page_list(page_list) }}
+            </ul>
+
+        {% endif %}
+    </section>
+{% endblock %}
+
+{% block javascript %}
+    {{ super() }}
+    {{ pagedown.include_pagedown() }}
+{% endblock %}

+ 4 - 0
templates/email-msg/register.txt

@@ -0,0 +1,4 @@
+欢迎注册HBlog用户,请点击以下链接完成认证。
+{{ register_url }}
+
+若您未进行过注册操作,请忽略此邮件。

+ 48 - 0
templates/file/file.html

@@ -0,0 +1,48 @@
+{% extends "base.html" %}
+
+{% block title %} 归档 {% endblock %}
+
+{% block style %}
+    {{ super() }}
+    <link href="{{ url_for('static', filename='styles/file/file.css') }}" rel="stylesheet">
+{% endblock %}
+
+{% block context %}
+    {% if form and current_user.check_role("WriteBlog") %}
+        <div class="container">
+            <div class="row">
+                <section class="col-12 col-lg-6 offset-lg-3 text-right">
+                    <div class="create">
+                        <form class="writer clearfix" action="{{ url_for('file.create_file_page') }}" method="post">
+                        {{ form.hidden_tag() }}
+                        <div class="form-group text-left">
+                            {{ form.name.label }}
+                            {{ form.name(class="form-control") }}
+                        </div>
+
+                        <div class="form-group text-left">
+                            {{ form.describe.label }}
+                            {{ form.describe(class="form-control") }}
+                        </div>
+
+                        {{ form.submit(class="btn btn-info mb-2", value="创建归档") }}
+                    </form>
+                    </div>
+                </section>
+            </div>
+            <hr>
+        </div>
+    {% endif %}
+
+    <section id="base" class="d-flex mt-3 container justify-content-center flex-wrap">
+        {% for file in file_list %}
+            <div class="file mb-3 mr-2 ml-2">
+                <h3> {{ file[1] }} </h3>
+                <hr>
+                <p> {{ file[2] }} </p>
+                <p> 篇数: {{ file[3] }} </p>
+                <a class="btn btn-info" href="{{ url_for("docx.file_page", file=file[0], page=1) }}"> 进入 </a>
+            </div>
+        {% endfor %}
+    </section>
+{% endblock %}

+ 19 - 0
templates/index/error.html

@@ -0,0 +1,19 @@
+{% extends "base.html" %}
+
+{% block title %} 错误 {% endblock %}
+
+{% block style %}
+    {{ super() }}
+    <link href="{{ url_for('static', filename='styles/index/index.css') }}" rel="stylesheet">
+{% endblock %}
+
+{% block context %}
+    <section id="base" class="container mt-3">
+        <div class="row">
+            <div class="col-12 text-center">
+                <p><strong> {{ error_info }} </strong></p>
+                <a class="btn btn-info" href="{{ url_for("base.index_page") }}"> 回到主页 </a>
+            </div>
+        </div>
+    </section>
+{% endblock %}

+ 27 - 0
templates/index/hello.html

@@ -0,0 +1,27 @@
+{% extends "base.html" %}
+
+{% block title %} 欢迎 {% endblock %}
+
+{% block style %}
+    {{ super() }}
+    <link href="{{ url_for('static', filename='styles/index/hello.css') }}" rel="stylesheet">
+    <style>
+        body {
+            overflow-y: hidden;
+            overflow-x: hidden;
+        }
+    </style>
+{% endblock %}
+
+{% block nav %} {% endblock %}
+
+{% block context %}
+    <section id="title-section">
+        <h1 id="title-1"> 欢迎,这里是{{ blog_name }} </h1>
+        <form method="get" action=" {{ url_for('base.index_page') }} ">
+            <input id="btn" type="submit" value="进入">
+        </form>
+    </section>
+{% endblock %}
+
+{% block footer %} {% endblock %}

+ 88 - 0
templates/index/index.html

@@ -0,0 +1,88 @@
+{% extends "base.html" %}
+
+{% block title %} 主页 {% endblock %}
+
+{% block style %}
+    {{ super() }}
+    <link href="{{ url_for('static', filename='styles/index/index.css') }}" rel="stylesheet">
+{% endblock %}
+
+{% block context %}
+    <section id="base" class="container mt-3">
+        <div class="row">
+            <article class="col-12">
+                <div class="introduce mr-lg-2 ml-lg-2">
+                    {% for info in conf['describe-info'] %}
+                        <h2> {{ info[0] }} </h2>
+                        {{ info[1] | safe }}
+                    {% endfor %}
+
+                    {% for link in conf['describe-link'] %}
+                        <a class="btn btn-info" href="{{ conf['describe-link'][link] }}"> {{ link }} </a>
+                    {% endfor %}
+                </div>
+            </article>
+        </div>
+        <div class="row">
+            {% if current_user.check_role("ReadBlog") %}  {# 检查是否具有读取权限 #}
+                {% if current_user.check_role("ReadMsg") %}
+                    <article class="col-12 col-lg-8">
+                    {% for blog in blog_list %}
+                        <div class="docx ml-lg-2"> <a class="h4"
+                                                      href="{{ url_for("docx.article_page", blog_id=blog[0]) }}">
+                            {{ blog[1] }}
+                                <small> {{ blog[2] }}
+                                    <small> {{ blog[3] }} </small> </small> </a></div>
+                    {% endfor %}
+                    </article>
+                {% else %}
+                    <article class="col-12">
+                    {% for blog in blog_list %}
+                        <div class="docx"> <a class="h4"
+                                              href="{{ url_for("docx.article_page", blog_id=blog[0]) }}">
+                            {{ blog[1] }}
+                                <small> {{ blog[2] }}
+                                    <small> {{ blog[3] }} </small> </small> </a></div>
+                    {% endfor %}
+                    </article>
+                {% endif %}
+            {% endif %}
+
+            {% if current_user.check_role("ReadMsg") %}  {# 检查是否具有读取权限 #}
+                {% if current_user.check_role("ReadBlog") %}
+                    <aside class="col-12 col-lg-4">
+                    {% for msg in msg_list %}
+                        <div class="msg mr-0 mr-lg-2">
+                            <p class="msg-title h5">
+                                {% if current_user.check_role("ReadUserInfo") %}  {# 判断是否可读取用户信息 #}
+                                    {{ msg.auth.email }}
+                                {% else %}
+                                    {{ msg.auth.s_email }}
+                                {% endif %}
+                                <br> <small>
+                                {{ msg.update_time }} </small> </p>
+                            <p> {{ msg.context.replace('\n', '<br>') | safe }} </p>
+                        </div>
+                    {% endfor %}
+                    </aside>
+                {% else %}
+                    <aside class="col-12">
+                    {% for msg in msg_list %}
+                        <div class="msg">
+                            <p class="msg-title h5">
+                                {% if current_user.check_role("ReadUserInfo") %}  {# 判断是否可读取用户信息 #}
+                                    {{ msg.auth.email }}
+                                {% else %}
+                                    {{ msg.auth.s_email }}
+                                {% endif %}
+                                <br> <small>
+                                {{ msg.update_time }} </small> </p>
+                            <p> {{ msg.context.replace('\n', '<br>') | safe }} </p>
+                        </div>
+                    {% endfor %}
+                    </aside>
+                {% endif %}
+            {% endif %}
+        </div>
+    </section>
+{% endblock %}

+ 9 - 0
templates/macro.html

@@ -0,0 +1,9 @@
+{% macro get_page_list(info_lines) %}
+    {% for line in info_lines %}
+        {% if line %}
+            <li class="page-item"><a class="page-link" href="{{ line[1] }}"> {{ line[0] }} </a></li>
+        {% else %}
+            <li class="page-item"><a class="page-link" href="#"> ... </a></li>
+        {% endif %}
+    {% endfor %}
+{% endmacro %}

+ 53 - 0
templates/msg/msg.html

@@ -0,0 +1,53 @@
+{% extends "base.html" %}
+{% import "macro.html" as macro %}
+
+{% block title %} 留言 {% endblock %}
+
+{% block style %}
+    {{ super() }}
+    <link href="{{ url_for('static', filename='styles/msg/msg.css') }}" rel="stylesheet">
+{% endblock %}
+
+{% block context %}
+    <section id="base" class="container mt-3">
+        <div class="row">
+            <section class="col-12 text-right">
+                <form class="writer clearfix" action="{{ url_for('msg.write_page') }}" method="post">
+                    {{ form.hidden_tag() }}
+                    {{ form.context(class="form-control mb-2", rows="5") }}
+                    {{ form.secret() }} {{ form.secret.label }}
+                    {{ form.submit(class="btn btn-info mb-2", value="留言") }}
+                </form>
+            </section>
+        </div>
+        <hr>
+        {% if current_user.check_role("ReadMsg") %}  {# 检查是否具有读取权限 #}
+            <div class="row">
+                <section class="col-12">
+                    {% for msg in msg_list %}
+                        <div class="msg mr-0">
+                            <p class="msg-title h5">
+                                {% if current_user.check_role("ReadUserInfo") %}  {# 判断是否可读取用户信息 #}
+                                    {{ msg.auth.email }}
+                                {% else %}
+                                    {{ msg.auth.s_email }}
+                                {% endif %}
+                                <br>
+                                <small> {{ msg.update_time }}
+                                    {% if msg.secret == is_secret %}
+                                        <small> [私密留言] </small>
+                                    {% endif %}
+                                </small>
+                            </p>
+                            <p> {{ msg.context.replace('\n', '<br>') | safe }} </p>
+                        </div>
+                    {% endfor %}
+                </section>
+            </div>
+
+            <ul class="pagination col-12 justify-content-center">
+                {{ macro.get_page_list(page_list) }}
+            </ul>
+        {% endif %}
+    </section>
+{% endblock %}

+ 11 - 0
view/__init__.py

@@ -0,0 +1,11 @@
+from view.index import IndexApp
+from view.docx import DocxApp
+from view.file import FileApp
+from view.msg import MsgApp
+from view.about_me import AboutMeApp
+from view.auth import AuthApp
+
+
+class WebApp(IndexApp, DocxApp, FileApp, MsgApp, AboutMeApp, AuthApp):
+    def __init__(self, import_name):
+        super(WebApp, self).__init__(import_name)

+ 28 - 0
view/about_me.py

@@ -0,0 +1,28 @@
+from flask import Flask, Blueprint, render_template
+from typing import Optional
+
+from configure import conf
+from view.base import App
+
+
+about_me = Blueprint("about_me", __name__)
+app: Optional[Flask] = None
+
+
+@about_me.route('/')
+def about_me_page():
+    return render_template("about_me/about_me.html")
+
+
+@about_me.context_processor
+def inject_base():
+    return {"top_nav": ["", "", "", "", "active", ""]}
+
+
+class AboutMeApp(App):
+    def __init__(self, import_name):
+        super(AboutMeApp, self).__init__(import_name)
+
+        global app
+        app = self._app
+        app.register_blueprint(about_me, url_prefix="/about-me")

+ 140 - 0
view/auth.py

@@ -0,0 +1,140 @@
+from flask import Flask, Blueprint, render_template, redirect, flash, url_for, request, abort
+from flask_login import login_required, login_user, current_user, logout_user
+from flask_mail import Mail
+from wtforms import StringField, PasswordField, BooleanField, SubmitField, ValidationError
+from wtforms.validators import DataRequired, Length, EqualTo
+from typing import Optional
+
+from view.base import App
+from core.user import User, LoaderUserError, load_user_by_email
+from flask_wtf import FlaskForm
+from send_email import send_msg
+
+auth = Blueprint("auth", __name__)
+app: Optional[Flask] = None
+mail: Optional[Mail] = None
+
+
+class LoginForm(FlaskForm):
+    email = StringField("邮箱", validators=[DataRequired(), Length(1, 32)])
+    passwd = PasswordField("密码", validators=[DataRequired(), Length(8, 32)])
+    remember = BooleanField("记住我")
+    submit = SubmitField("登录")
+
+
+class RegisterForm(FlaskForm):
+    email = StringField("邮箱", validators=[DataRequired(), Length(1, 32)])
+    passwd = PasswordField("密码", validators=[DataRequired(),
+                                             EqualTo("passwd_again", message="两次输入密码不相同"),
+                                             Length(8, 32)])
+    passwd_again = PasswordField("重复密码", validators=[DataRequired()])
+    submit = SubmitField("注册")
+
+    def validate_email(self, field):
+        try:
+            load_user_by_email(field.data)
+        except LoaderUserError:
+            return
+        else:
+            raise ValidationError("Email already register")
+
+
+@auth.route('/yours')
+@login_required
+def yours_page():
+    msg_count, comment_count, blog_count = current_user.count_info()
+    return render_template("auth/yours.html", msg_count=msg_count, comment_count=comment_count, blog_count=blog_count)
+
+
+@auth.route('/login', methods=["GET", "POST"])
+def login_page():
+    if current_user.is_authenticated:
+        return redirect(url_for("auth.yours_page"))
+
+    form = LoginForm()
+    if form.validate_on_submit():
+        try:
+            user = load_user_by_email(form.email.data)
+        except LoaderUserError:
+            user = None
+
+        if user is not None and user.check_passwd(form.passwd.data):
+            login_user(user, form.remember.data)
+            next_page = request.args.get("next")
+            if next_page is None or not next_page.startswith('/'):
+                next_page = url_for('base.index_page')
+            flash("登陆成功")
+            return redirect(next_page)
+        flash("账号或密码错误")
+        return redirect(url_for("auth.login_page"))
+    return render_template("auth/login.html", form=form)
+
+
+@auth.route('/register', methods=["GET", "POST"])
+def register_page():
+    if current_user.is_authenticated:
+        return redirect(url_for("auth.yours_page"))
+
+    form = RegisterForm()
+    if form.validate_on_submit():
+        token = User.creat_token(form.email.data, form.passwd.data)
+        register_url = url_for("auth.confirm_page", token=token, _external=True)
+        send_msg("注册确认", mail, form.email.data, "register", register_url=register_url)
+        flash("注册提交成功, 请进入邮箱点击确认注册链接")
+        return redirect(url_for("base.index_page"))
+    return render_template("auth/register.html", RegisterForm=form)
+
+
+@auth.route('/confirm')
+def confirm_page():
+    token = request.args.get("token", None)
+    if token is None:
+        abort(404)
+        return
+
+    token = User.load_token(token)
+    if token is None:
+        abort(404)
+        return
+
+    try:
+        load_user_by_email(token[0])
+    except LoaderUserError:
+        pass
+    else:
+        abort(404)
+        return
+
+    User(token[0], token[1], None, None).create_user()
+    flash(f"用户{token[0]}认证完成")
+    return redirect(url_for("base.index_page"))
+
+
+@auth.route('/logout')
+def logout_page():
+    logout_user()
+    flash("退出登录成功")
+    return redirect(url_for("base.index_page"))
+
+
+@auth.context_processor
+def inject_base():
+    return {"top_nav": ["", "", "", "", "", "active"]}
+
+
+class AuthApp(App):
+    def __init__(self, import_name):
+        super(AuthApp, self).__init__(import_name)
+
+        global app, mail
+        app = self._app
+        mail = self.mail
+        app.register_blueprint(auth, url_prefix="/auth")
+        self.login_manager.login_view = "auth.login_page"
+
+        @self.login_manager.user_loader
+        def user_loader(email: str):
+            try:
+                return load_user_by_email(email)
+            except LoaderUserError:
+                return None

+ 70 - 0
view/base.py

@@ -0,0 +1,70 @@
+from flask import Flask, url_for
+from flask_mail import Mail
+from flask_login import LoginManager
+from typing import Optional
+
+from configure import conf
+from core.user import AnonymousUser
+
+
+class App:
+    def __init__(self, import_name: str):
+        self._app = Flask(import_name)
+        self._app.config["SECRET_KEY"] = conf['secret-key']
+
+        self.login_manager = LoginManager()
+        self.login_manager.init_app(self._app)
+        self.login_manager.anonymous_user = AnonymousUser  # 设置未登录的匿名对象
+
+        self._app.config["MAIL_SERVER"] = conf['email_server']
+        self._app.config["MAIL_PORT"] = conf['email_port']
+        self._app.config["MAIL_USE_TLS"] = conf['email_tls']
+        self._app.config["MAIL_USE_SSL"] = conf['email_ssl']
+        self._app.config["MAIL_USERNAME"] = conf['email_name']
+        self._app.config["MAIL_PASSWORD"] = conf['email_passwd']
+
+        self.mail = Mail(self._app)
+
+    def get_app(self) -> Flask:
+        return self._app
+
+    def run(self):
+        self.run()
+
+    @staticmethod
+    def get_max_page(count: int, count_page: int):
+        return (count // count_page) + (0 if count % count_page == 0 else 1)
+
+    @staticmethod
+    def get_page(url, page: int, count: int):
+        if count <= 9:
+            page_list = [[f"{i + 1}", url_for(url, page=i + 1)] for i in range(count)]
+        elif page <= 5:
+            """
+            [1][2][3][4][5][6][...][count - 1][count]
+            """
+            page_list = [[f"{i + 1}", url_for(url, page=i + 1)] for i in range(6)]
+
+            page_list += [None,
+                          [f"{count - 1}", url_for(url, page=count - 1)],
+                          [f"{count}", url_for(url, page=count)]]
+        elif page >= count - 5:
+            """
+            [1][2][...][count - 5][count - 4][count - 3][count - 2][count - 1][count]
+            """
+            page_list: Optional[list] = [["1", url_for(url, page=1)],
+                                         ["2", url_for(url, page=2)],
+                                         None]
+            page_list += [[f"{count - 5 + i}", url_for(url, page=count - 5 + i), False] for i in range(6)]
+        else:
+            """
+            [1][2][...][page - 2][page - 1][page][page + 1][page + 2][...][count - 1][count]
+            """
+            page_list: Optional[list] = [["1", url_for(url, page=1)],
+                                         ["2", url_for(url, page=2)],
+                                         None]
+            page_list += [[f"{page - 2 + i}", url_for(url, page=page - 2 + i)] for i in range(5)]
+            page_list += [None,
+                          [f"{count - 1}", url_for(url, page=count - 1)],
+                          [f"{count}", url_for(url, page=count)]]
+        return page_list

+ 146 - 0
view/docx.py

@@ -0,0 +1,146 @@
+from flask import Flask, Blueprint, render_template, abort, redirect, url_for, flash
+from flask_wtf import FlaskForm
+from flask_pagedown import PageDown
+from flask_pagedown.fields import PageDownField
+from flask_login import login_required, current_user
+from wtforms import TextAreaField, StringField, SubmitField
+from wtforms.validators import DataRequired, Length
+from typing import Optional
+import bleach
+from markdown import markdown
+
+from view.base import App
+from sql.base import DBBit
+from core.blog import BlogArticle, load_blog_by_id
+from core.user import User
+from core.comment import Comment
+from core.file import load_file_by_name, LoadFileError
+
+docx = Blueprint("docx", __name__)
+app: Optional[Flask] = None
+allow_tag = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'small',
+             'ul', 'h1', 'h2', 'h3', 'h4', 'h5' 'h6', 'p']
+
+
+class WriteBlogForm(FlaskForm):
+    title = StringField("标题", validators=[DataRequired(), Length(1, 10)])
+    subtitle = StringField("副标题", validators=[DataRequired(), Length(1, 10)])
+    file = StringField("归档", validators=[DataRequired(), Length(1, 10)])
+    context = PageDownField("博客内容", validators=[DataRequired()])
+    submit = SubmitField("提交博客")
+
+
+class WriteCommentForm(FlaskForm):
+    context = TextAreaField(validators=[DataRequired()])
+    submit = SubmitField("评论")
+
+
+@docx.route('/<int:page>')
+def docx_page(page: int = 1):
+    if page < 1:
+        abort(404)
+        return
+
+    blog_list = BlogArticle.get_blog_list(limit=20, offset=(page - 1) * 20)
+    max_page = App.get_max_page(BlogArticle.get_blog_count(), 20)
+    page_list = App.get_page("docx.docx_page", page, max_page)
+    return render_template("docx/docx.html",
+                           blog_list=blog_list,
+                           is_top=DBBit.BIT_1,
+                           page_list=page_list,
+                           form=WriteBlogForm())
+
+
+@docx.route('/<int:file>/<int:page>')
+def file_page(file: int, page: int = 1):
+    if page < 1:
+        abort(404)
+        return
+
+    blog_list = BlogArticle.get_blog_list(file_id=file, limit=20, offset=(page - 1) * 20)
+    max_page = App.get_max_page(BlogArticle.get_blog_count(file_id=file), 20)
+    page_list = App.get_page("docx.file_page", page, max_page)
+    return render_template("docx/docx.html",
+                           blog_list=blog_list,
+                           is_top=DBBit.BIT_1,
+                           page_list=page_list,
+                           form=None)
+
+
+@docx.route('/article/<int:blog_id>')
+def article_page(blog_id: int):
+    article = load_blog_by_id(blog_id)
+    return render_template("docx/article.html",
+                           article=article,
+                           file_list=article.file,
+                           form=WriteCommentForm())
+
+
+@docx.route('/comment/<int:blog>', methods=["POST"])
+@login_required
+def comment_page(blog: int):
+    form = WriteCommentForm()
+    if form.validate_on_submit():
+        auth: User = current_user
+        if not auth.check_role("WriteComment"):  # 检查是否具有权限
+            abort(403)
+            return
+
+        context = form.context.data
+        if Comment(blog, auth, context).create_comment():
+            flash("评论成功")
+        else:
+            flash("评论失败")
+
+        return redirect(url_for("docx.article_page", blog_id=blog))
+    abort(404)
+
+
+@docx.route('/create-docx', methods=["POST"])
+@login_required
+def create_docx_page():
+    form = WriteBlogForm()
+    if form.validate_on_submit():
+        auth: User = current_user
+        if not auth.check_role("WriteBlog"):  # 检查是否具有写入权限
+            abort(403)
+            return
+
+        title = form.title.data
+        subtitle = form.subtitle.data
+
+        file = set(str(form.file.data).replace(" ", "").split(";"))
+        file_list = []
+        for f in file:
+            try:
+                file_list.append(load_file_by_name(f))
+            except LoadFileError:
+                pass
+
+        context = bleach.linkify(
+            bleach.clean(
+                markdown(form.context.data, output_format='html'), tags=allow_tag, strip=True))
+
+        if BlogArticle(None, current_user, title, subtitle, context, file=file_list).write_blog():
+            flash(f"博客 {title} 发表成功")
+        else:
+            flash(f"博客 {title} 发表失败")
+
+        return redirect(url_for("docx.docx_page", page=1))
+    abort(404)
+
+
+@docx.context_processor
+def inject_base():
+    return {"top_nav": ["", "", "active", "", "", ""]}
+
+
+class DocxApp(App):
+    def __init__(self, import_name):
+        super(DocxApp, self).__init__(import_name)
+
+        global app
+        app = self._app
+        self.pagedown = PageDown()
+        self.pagedown.init_app(app)
+        app.register_blueprint(docx, url_prefix="/docx")

+ 57 - 0
view/file.py

@@ -0,0 +1,57 @@
+from flask import Flask, Blueprint, render_template, abort, redirect, url_for, flash
+from typing import Optional
+from flask_login import login_required, current_user
+from flask_wtf import FlaskForm
+from wtforms import StringField, SubmitField
+from wtforms.validators import DataRequired, Length
+
+from view.base import App
+from core.user import User
+from core.file import File
+
+file = Blueprint("file", __name__)
+app: Optional[Flask] = None
+
+
+class CreateFileForm(FlaskForm):
+    name = StringField("名字", validators=[DataRequired(), Length(1, 10)])
+    describe = StringField("描述", validators=[DataRequired(), Length(1, 30)])
+    submit = SubmitField("创建归档")
+
+
+@file.route('/')
+def file_page():
+    file_list = File.get_file_list()
+    return render_template("file/file.html", file_list=file_list, form=CreateFileForm())
+
+
+@file.route("create-file", methods=["POST"])
+@login_required
+def create_file_page():
+    form = CreateFileForm()
+    if form.validate_on_submit():
+        auth: User = current_user
+        if not auth.check_role("WriteBlog"):  # 检查相应的权限
+            abort(403)
+            return
+
+        if File(form.name.data, form.describe.data, None).create_file():
+            flash(f"创建归档 {form.name.data} 成功")
+        else:
+            flash(f"创建归档 {form.name.data} 失败")
+        return redirect(url_for("file.file_page"))
+    abort(404)
+
+
+@file.context_processor
+def inject_base():
+    return {"top_nav": ["", "active", "", "", "", ""]}
+
+
+class FileApp(App):
+    def __init__(self, import_name):
+        super(FileApp, self).__init__(import_name)
+
+        global app
+        app = self._app
+        app.register_blueprint(file, url_prefix="/file")

+ 65 - 0
view/index.py

@@ -0,0 +1,65 @@
+from flask import Flask, Blueprint, render_template
+from typing import Optional
+
+from configure import conf
+from view.base import App
+from core.blog import BlogArticle
+from core.msg import load_message_list
+
+index = Blueprint("base", __name__)
+app: Optional[Flask] = None
+
+
+@index.route('/')
+def hello_page():
+    return render_template("index/hello.html")
+
+
+@index.route('/index')
+def index_page():
+    blog_list = BlogArticle.get_blog_list(limit=5, offset=0, not_top=True)
+    msg_list = load_message_list(limit=6, offset=0, show_secret=False)
+    return render_template("index/index.html", blog_list=blog_list, msg_list=msg_list)
+
+
+@index.app_errorhandler(404)
+def error_404(e):
+    return render_template("index/error.html", error_code="404", error_info=f"你似乎来到一片荒漠:{e}"), 404
+
+
+@index.app_errorhandler(405)
+def error_404(e):
+    return render_template("index/error.html", error_code="404", error_info=f"请求错误:{e}"), 405
+
+
+@index.app_errorhandler(403)
+def error_403(e):
+    return render_template("index/error.html", error_code="404", error_info=f"权限不足:{e}"), 403
+
+
+@index.app_errorhandler(500)
+def error_500(e):
+    return render_template("index/error.html", error_code="404", error_info=f"服务器出问题啦:{e}"), 500
+
+
+@index.context_processor
+def inject_base():
+    return {"top_nav": ["active", "", "", "", "", ""]}
+
+
+@index.app_context_processor
+def inject_base():
+    return {"blog_name": conf['blog-name'],
+            "top_nav": ["", "", "", "", "", ""],
+            "blog_describe": conf['blog-describe'],
+            "conf": conf}
+
+
+class IndexApp(App):
+    def __init__(self, import_name):
+        super(IndexApp, self).__init__(import_name)
+
+        global app
+        app = self._app
+        app.register_blueprint(index, url_prefix="/")
+

+ 72 - 0
view/msg.py

@@ -0,0 +1,72 @@
+from flask import Flask, Blueprint, render_template, abort, redirect, url_for, flash
+from flask_wtf import FlaskForm
+from flask_login import login_required, current_user
+from wtforms import TextAreaField, BooleanField, SubmitField
+from wtforms.validators import DataRequired
+from typing import Optional
+
+from view.base import App
+from sql.base import DBBit
+from core.user import User
+from core.msg import Message, load_message_list
+
+msg = Blueprint("msg", __name__)
+app: Optional[Flask] = None
+
+
+class WriteForm(FlaskForm):
+    """
+    写新内容表单
+    """
+    context = TextAreaField(validators=[DataRequired()])
+    secret = BooleanField("私密留言")
+    submit = SubmitField("留言")
+
+
+@msg.route('/<int:page>')
+def msg_page(page: int = 1):
+    if page < 1:
+        abort(404)
+        return
+
+    msg_list = load_message_list(20, (page - 1) * 20,
+                                 show_secret=current_user.check_role("ReadSecretMsg"))  # 判断是否可读取私密内容
+    max_page = App.get_max_page(Message.get_msg_count(), 20)
+    page_list = App.get_page("docx.docx_page", page, max_page)
+    return render_template("msg/msg.html", msg_list=msg_list, page_list=page_list, form=WriteForm(),
+                           is_secret=DBBit.BIT_1)
+
+
+@msg.route('/write', methods=["POST"])
+@login_required
+def write_page():
+    form = WriteForm()
+    if form.validate_on_submit():
+        auth: User = current_user
+        if not auth.check_role("WriteMsg"):  # 检查相应权限
+            abort(403)
+            return
+
+        context = form.context.data
+        secret = form.secret.data
+        if Message(None, auth, context, secret, None).create_msg():
+            flash("留言成功")
+        else:
+            flash("留言失败")
+
+        return redirect(url_for("msg.msg_page", page=1))
+    abort(404)
+
+
+@msg.context_processor
+def inject_base():
+    return {"top_nav": ["", "", "", "active", "", ""]}
+
+
+class MsgApp(App):
+    def __init__(self, import_name):
+        super(MsgApp, self).__init__(import_name)
+
+        global app
+        app = self._app
+        app.register_blueprint(msg, url_prefix="/msg")