├── .gitattributes ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── app.py ├── backup.sh ├── config.py ├── config.yaml.tmpl ├── convert.py ├── custom ├── README.md └── __init__.py ├── dev_requirements.txt ├── docker-compose.yml ├── docs ├── .nojekyll ├── README.md ├── _sidebar.md ├── changelog.md ├── configuration.md ├── deploying.md ├── docker-compose.md ├── features.md ├── front-dev.md ├── index.html ├── kubernetes.md ├── pycharm.svg ├── python-dev.md ├── quickstart.md └── widget.md ├── ext.py ├── forms.py ├── hexo-exporter.py ├── k8s ├── app.yaml ├── arq.yaml ├── config.yaml ├── memcached.yaml ├── nginx.yaml ├── optional │ ├── mariadb.yaml │ └── redis.yaml └── sentinel │ ├── Dockerfile │ ├── redis-master.conf │ ├── redis-slave.conf │ └── run.sh ├── libs ├── __init__.py └── extracted.py ├── manage.py ├── models ├── __init__.py ├── activity.py ├── base.py ├── blog.py ├── comment.py ├── consts.py ├── markdown.py ├── mc.py ├── mention.py ├── mixin.py ├── react.py ├── signals.py ├── toc.py ├── user.py ├── utils.py └── var.py ├── package.json ├── pyproject.toml ├── requirements.txt ├── screenshot ├── admin.png └── index.png ├── setup.cfg ├── setup.sh ├── src ├── activities │ ├── index.html │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── App.vue │ │ ├── api.js │ │ ├── assets │ │ │ ├── base.css │ │ │ └── logo.svg │ │ ├── components │ │ │ ├── Paginator.vue │ │ │ └── UploadForm.vue │ │ ├── main.js │ │ ├── router │ │ │ └── index.js │ │ ├── store │ │ │ ├── getters.js │ │ │ ├── index.js │ │ │ └── modules │ │ │ │ ├── app.js │ │ │ │ ├── errorLog.js │ │ │ │ ├── tagsView.js │ │ │ │ └── user.js │ │ ├── utils │ │ │ ├── auth.js │ │ │ └── request.js │ │ └── views │ │ │ └── Activity.vue │ └── vite.config.js ├── admin │ ├── .eslintrc-auto-import.json │ ├── .gitignore │ ├── .vscode │ │ └── extensions.json │ ├── LICENSE │ ├── auto-imports.d.ts │ ├── components.d.ts │ ├── index.html │ ├── jsconfig.json │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── App.vue │ │ ├── api.js │ │ ├── assets │ │ │ ├── 401_images │ │ │ │ └── 401.gif │ │ │ ├── 404_images │ │ │ │ ├── 404.png │ │ │ │ └── 404_cloud.png │ │ │ ├── base.css │ │ │ ├── image │ │ │ │ ├── animation-image.gif │ │ │ │ └── logo.png │ │ │ ├── layout │ │ │ │ ├── animation-image.gif │ │ │ │ └── logo.png │ │ │ └── logo.svg │ │ ├── components │ │ │ ├── Breadcrumb │ │ │ │ └── index.vue │ │ │ ├── DndList │ │ │ │ └── index.vue │ │ │ ├── Dropdown │ │ │ │ └── index.vue │ │ │ ├── ElSvgIcon.vue │ │ │ ├── Hamburger │ │ │ │ └── index.vue │ │ │ ├── ImageCropper │ │ │ │ ├── index.vue │ │ │ │ └── utils │ │ │ │ │ ├── data2blob.js │ │ │ │ │ ├── effectRipple.js │ │ │ │ │ ├── language.js │ │ │ │ │ └── mimes.js │ │ │ ├── MDinput │ │ │ │ └── index.vue │ │ │ ├── MarkdownEditor │ │ │ │ └── index.vue │ │ │ ├── Pagination │ │ │ │ └── index.vue │ │ │ ├── Sticky │ │ │ │ └── index.vue │ │ │ └── SvgIcon │ │ │ │ └── index.vue │ │ ├── icons │ │ │ ├── SvgIcon.vue │ │ │ ├── common │ │ │ │ ├── 404.svg │ │ │ │ ├── bug.svg │ │ │ │ ├── lock.svg │ │ │ │ └── zip.svg │ │ │ └── nav-bar │ │ │ │ ├── collection.svg │ │ │ │ ├── dashboard.svg │ │ │ │ ├── delete.svg │ │ │ │ ├── documentation.svg │ │ │ │ ├── edit.svg │ │ │ │ ├── example.svg │ │ │ │ ├── eye-open.svg │ │ │ │ ├── eye.svg │ │ │ │ ├── form.svg │ │ │ │ ├── link.svg │ │ │ │ ├── list.svg │ │ │ │ ├── nested.svg │ │ │ │ ├── password.svg │ │ │ │ ├── table.svg │ │ │ │ ├── tree.svg │ │ │ │ └── user.svg │ │ ├── layout │ │ │ ├── Layout.vue │ │ │ ├── components │ │ │ │ ├── AppMain.vue │ │ │ │ ├── Breadcrumb │ │ │ │ │ ├── Breadcrumb.vue │ │ │ │ │ └── index.js │ │ │ │ ├── Hamburger │ │ │ │ │ ├── Hamburger.vue │ │ │ │ │ └── index.js │ │ │ │ ├── Navbar.vue │ │ │ │ ├── Sidebar │ │ │ │ │ ├── ElSvgItem.vue │ │ │ │ │ ├── Item.jsx │ │ │ │ │ ├── Link.vue │ │ │ │ │ ├── Logo.vue │ │ │ │ │ ├── Sidebar.vue │ │ │ │ │ ├── SidebarItem.vue │ │ │ │ │ └── index.js │ │ │ │ ├── TagsView │ │ │ │ │ ├── TagsView.vue │ │ │ │ │ └── index.js │ │ │ │ └── index.js │ │ │ ├── hook │ │ │ │ └── useResizeHandler.js │ │ │ └── index.js │ │ ├── main.js │ │ ├── mixins │ │ │ ├── commonMixin.js │ │ │ ├── elementMixin.js │ │ │ ├── routerMixin.js │ │ │ └── websocketMixin.js │ │ ├── router │ │ │ └── index.js │ │ ├── settings.js │ │ ├── store │ │ │ ├── getters.js │ │ │ ├── index.js │ │ │ └── modules │ │ │ │ ├── app.js │ │ │ │ ├── tagsView.js │ │ │ │ └── user.js │ │ ├── styles │ │ │ ├── btn.scss │ │ │ ├── detail.scss │ │ │ ├── elemenet-style-overflow.scss │ │ │ ├── index.scss │ │ │ ├── mixin.scss │ │ │ ├── reset-style.scss │ │ │ ├── scss-suger.scss │ │ │ ├── sidebar.scss │ │ │ ├── transition.scss │ │ │ ├── variables-to-js.scss │ │ │ └── variables.scss │ │ ├── utils │ │ │ ├── auth.js │ │ │ ├── bus.js │ │ │ ├── comentUtil.js │ │ │ ├── filter.js │ │ │ ├── getPageTitle.js │ │ │ ├── request.js │ │ │ ├── scrollTo.js │ │ │ ├── useElement.js │ │ │ └── validate.js │ │ └── views │ │ │ ├── dashboard.vue │ │ │ ├── error-page │ │ │ ├── 401.vue │ │ │ └── 404.vue │ │ │ ├── favorite.vue │ │ │ ├── login.vue │ │ │ ├── post │ │ │ ├── components │ │ │ │ └── post-detail.vue │ │ │ ├── create.vue │ │ │ ├── edit.vue │ │ │ └── list.vue │ │ │ ├── redirect │ │ │ └── index.jsx │ │ │ ├── topic │ │ │ ├── components │ │ │ │ └── topic-detail.vue │ │ │ ├── create.vue │ │ │ ├── edit.vue │ │ │ └── list.vue │ │ │ └── user │ │ │ ├── components │ │ │ └── user-detail.vue │ │ │ ├── create.vue │ │ │ ├── edit.vue │ │ │ └── list.vue │ ├── tsconfig.json │ ├── typings │ │ ├── common.d.ts │ │ ├── env.d.ts │ │ ├── global.d.ts │ │ └── shims-vue.d.ts │ └── vite.config.js └── blog │ ├── blog.js │ ├── index.js │ ├── search.js │ └── social-sharer.js ├── srv ├── .hosts ├── ansible.cfg ├── deploy.yml └── templates │ ├── nginx.conf │ └── supervisord.conf ├── static ├── css │ ├── admin │ │ ├── 401.css │ │ ├── 404.css │ │ ├── dashboard.css │ │ ├── favorite.css │ │ ├── index.css │ │ ├── index2.css │ │ ├── index3.css │ │ ├── list.css │ │ ├── login.css │ │ ├── post-detail.css │ │ ├── topic-detail.css │ │ └── user-detail.css │ ├── balloon.min.css │ ├── base.css │ ├── dracula.css │ ├── favorites.css │ ├── favorites.min.css │ ├── font-awesome.css │ ├── gitment.css │ ├── iconfont.css │ ├── iconfont.eot │ ├── iconfont.svg │ ├── iconfont.ttf │ ├── iconfont.woff │ ├── iconfont.woff2 │ ├── index.css │ ├── index.min.css │ ├── lightbox.css │ ├── main.min.css │ ├── post.css │ ├── post.min.css │ ├── pure-min.css │ ├── react.css │ ├── social-sharer.css │ ├── tomorrow.min.css │ ├── topic.css │ ├── topic.min.css │ ├── uikit.min.css │ └── widget.css ├── dist │ └── blog │ │ ├── blog.js │ │ ├── index.js │ │ ├── search.js │ │ └── social-sharer.js ├── fonts │ ├── element-icons.2fad952a.woff │ ├── element-icons.6f0a7632.ttf │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff ├── gif │ └── admin │ │ ├── 401.gif │ │ └── animation-image.gif ├── html │ └── github-card.html ├── img │ ├── default-avatar.jpg │ ├── funny.png │ ├── love.png │ ├── sad.png │ ├── surprised.png │ ├── tui-editor-2x.b4361244.png │ ├── tui-editor.30dd0f52.png │ └── upvote.png ├── js │ ├── admin │ │ ├── 401.js │ │ ├── 404.js │ │ ├── create.js │ │ ├── create2.js │ │ ├── create3.js │ │ ├── dashboard.js │ │ ├── edit.js │ │ ├── edit2.js │ │ ├── edit3.js │ │ ├── favorite.js │ │ ├── filter.js │ │ ├── index.js │ │ ├── index2.js │ │ ├── index3.js │ │ ├── index4.js │ │ ├── index5.js │ │ ├── list.js │ │ ├── list2.js │ │ ├── list3.js │ │ ├── login.js │ │ ├── post-detail.js │ │ ├── topic-detail.js │ │ └── user-detail.js │ ├── github-widget.js │ ├── highlight.min.js │ ├── index.js │ └── vendor.js ├── png │ └── admin │ │ ├── 404.png │ │ ├── 404_cloud.png │ │ └── logo.png └── upload │ └── README ├── tasks.py ├── templates ├── activities.html ├── admin.html ├── archives.html ├── base.html ├── email │ └── mention.html ├── favorites.html ├── index.html ├── partials │ └── sidebar │ │ ├── about_me.html │ │ ├── blogroll.html │ │ ├── favorite │ │ ├── book.html │ │ ├── game.html │ │ ├── movie.html │ │ └── subject.html │ │ ├── feed.html │ │ ├── html.html │ │ ├── latest_comments.html │ │ ├── latest_notes.html │ │ ├── most_viewed.html │ │ └── tagcloud.html ├── post.html ├── search.html ├── sitemap.xml ├── tag.html ├── tags.html ├── topic.html ├── topics.html └── utils.html ├── views ├── __init__.py ├── admin.py ├── blog.py ├── index.py ├── j.py ├── request.py └── utils.py └── webpack.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | static/css/gitment.css linguist-vendored 2 | static/css/social-sharer.css linguist-vendored 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> Python 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | local_settings.py 62 | node_modules 63 | venv 64 | *~ 65 | yarn.lock 66 | static/upload/ 67 | profile.json 68 | custom 69 | package-lock.json 70 | .DS_Store 71 | deploy 72 | srv/deploy.retry 73 | .idea/ 74 | # K8s 75 | k8s/config.yaml 76 | config.yaml 77 | .mypy_cache 78 | static/jpg 79 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: 3 | directories: 4 | - $HOME/.cache/pip 5 | env: TOXENV=Python3.10 6 | matrix: 7 | include: 8 | - python: 3.10 9 | dist: focal 10 | install: pip install -r dev_requirements.txt 11 | script: make lint 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine AS build 2 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories \ 3 | && apk update \ 4 | && apk add git gcc musl-dev libffi-dev openssl-dev make libxml2-dev libxslt-dev 5 | WORKDIR /install 6 | COPY requirements.txt /requirements.txt 7 | RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple \ 8 | --trusted-host pypi.tuna.tsinghua.edu.cn -r /requirements.txt \ 9 | && mkdir -p /install/lib/python3.8/site-packages \ 10 | && cp -rp /usr/local/lib/python3.8/site-packages /install/lib/python3.8 11 | 12 | FROM python:3.8-alpine 13 | COPY --from=build /install/lib /usr/local/lib 14 | COPY --from=build /install/src /usr/local/src 15 | WORKDIR /app 16 | COPY . /app 17 | COPY --from=build /usr/local/bin/gunicorn /app/gunicorn 18 | COPY --from=build /usr/local/bin/arq /app/arq -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | checkfiles = views models *.py 2 | 3 | help: 4 | @echo "Lyanna development makefile" 5 | @echo 6 | @echo "usage: make " 7 | @echo "Targets:" 8 | @echo " deps Ensure dev/test dependencies are installed" 9 | @echo " lint Reports all linter violations" 10 | @echo " style Auto-formats the code" 11 | 12 | deps: 13 | @pip install -r dev_requirements.txt 14 | 15 | lint: deps 16 | flake8 $(checkfiles) || (echo "Please run 'make style' to try fix style issues" && false) 17 | bandit -r $(checkfiles) -s B403,B301,B104,B105,B311,B602,B404 18 | mypy --allow-untyped-calls $(checkfiles) 19 | 20 | style: deps 21 | isort -rc $(checkfiles) 22 | autopep8 -i -r $(checkfiles) 23 | -------------------------------------------------------------------------------- /backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo cp /var/lib/redis/dump.rdb ~/lyanna 4 | sudo chown ubuntu:ubuntu ~/lyanna/dump.rdb 5 | 6 | mysqldump -u root lyanna > /home/ubuntu/lyanna/db_backup.sql 7 | -------------------------------------------------------------------------------- /custom/README.md: -------------------------------------------------------------------------------- 1 | # 自定义视图/模型/模板 -------------------------------------------------------------------------------- /custom/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/custom/__init__.py -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | bandit==1.7.4 2 | flake8==4.0.1 3 | isort==5.10.1 4 | autopep8==1.6.0 5 | mypy==0.941 6 | types-PyYAML==6.0.5 7 | types-redis==4.1.18 8 | pycodestyle==2.8.0 9 | pyflakes==2.4.0 10 | ipython==8.10.0 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | db: 4 | image: mysql 5 | restart: always 6 | environment: 7 | MYSQL_DATABASE: 'test' 8 | MYSQL_USER: 'root' 9 | MYSQL_PASSWORD: '' 10 | MYSQL_ROOT_PASSWORD: '' 11 | MYSQL_ALLOW_EMPTY_PASSWORD: 'true' 12 | ports: 13 | - '3306:3306' 14 | volumes: 15 | - my-datavolume:/var/lib/mysql 16 | networks: 17 | - app-network 18 | redis: 19 | image: redis:alpine 20 | networks: 21 | - app-network 22 | memcached: 23 | image: memcached:1.5-alpine 24 | networks: 25 | - app-network 26 | web: 27 | networks: 28 | - app-network 29 | image: dongweiming/lyanna:latest 30 | ports: 31 | - '8000:8000' 32 | volumes: 33 | - .:/app 34 | depends_on: 35 | - db 36 | - redis 37 | - memcached 38 | environment: 39 | DEBUG: 1 40 | REDIS_URL: 'redis://redis:6379' 41 | MEMCACHED_HOST: 'memcached' 42 | DB_URL: 'mysql://root:@db:3306/test?charset=utf8' 43 | PYTHONPATH: $PYTHONPATH:/usr/local/src/aiomcache:/usr/local/src/aiomysql:/usr/local/src/tortoise:/usr/local/src/arq:/usr/local/src 44 | command: sh -c './setup.sh && python app.py' 45 | volumes: 46 | my-datavolume: 47 | networks: 48 | app-network: 49 | driver: bridge 50 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Lyanna 2 | 3 | > You are a Targaryen but you’re also a Stark. You’re fire and ice. 4 | 5 | `Lyanna` My Blog Using Sanic 6 | 7 | ![Lyanna](https://user-images.githubusercontent.com/841395/51306517-7900bd00-1a78-11e9-8e4d-96d840c8cc99.gif) 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - 入门 2 | - [快速开始](quickstart.md) 3 | - [功能](features.md) 4 | - [配置项](configuration.md) 5 | - [Widget](widget.md) 6 | - [部署](deploying.md) 7 | - [使用Docker Compose开发](docker-compose.md) 8 | - [在kubernetes上运行](kubernetes.md) 9 | 10 | - 高级 11 | 12 | - [前端开发](front-dev.md) 13 | - [后端开发](python-dev.md) 14 | 15 | - 相关课程 16 | 17 | - [Python项目实战](https://www.bytedemy.com/article/python-project/) 18 | - [Vue.js入门到进阶](https://www.bytedemy.com/article/vue-course/) 19 | - [爱湃森店铺](https://appv72m4Msi7516.h5.xiaoeknow.com) 20 | 21 | - [Changelog](changelog.md) 22 | -------------------------------------------------------------------------------- /docs/deploying.md: -------------------------------------------------------------------------------- 1 | # 部署 2 | 3 | ```bash 4 | ansible-playbook deploy.yml --ask-sudo-pass 5 | ``` 6 | 7 | Python应用服务器用了gunicorn(木有用ASGI因为不支持): 8 | 9 | ```bash 10 | gunicorn app:app --bind unix:/XXX/lyanna.sock --worker-class sanic.worker.GunicornWorker 11 | ``` 12 | 13 | 部署方案是使用 Ansbile: 14 | 15 | ```bash 16 | ansible-playbook deploy.yml --ask-sudo-pass 17 | ``` 18 | 19 | 通过 Ansbile 剧本把 local_settings.py, nginx.conf, supervisor.conf 部署到服务器上。如果要回滚到某个commit,可以这样: 20 | 21 | ```bash 22 | ansible-playbook deploy.yml --ask-sudo-pass --extra-vars="git_commit_version=THE_COMMIT_VERSION" 23 | ``` 24 | 25 | 具体详见 [srv目录](https://github.com/dongweiming/lyanna/tree/master/srv) 26 | -------------------------------------------------------------------------------- /docs/docker-compose.md: -------------------------------------------------------------------------------- 1 | 为了方便本地开发和起demo体验效果,可以使用[Docker Compose](https://docs.docker.com/compose/)在Docker里面开发。在这种模式下数据库、Nginx、Memcached、Redis、lyanna应用等都在独立的容器中,一个命令全部启动: 2 | 3 | ```bash 4 | ❯ docker-compose up # 可以加 -d 后台运行 5 | Starting lyanna_memcached_1 ... 6 | Starting lyanna_memcached_1 ... done 7 | Starting lyanna_redis_1 ... done 8 | Starting lyanna_web_1 ... done 9 | Attaching to lyanna_db_1, lyanna_redis_1, lyanna_memcached_1, lyanna_web_1 10 | ... 11 | web_1 | Init Finished! 12 | web_1 | User admin created!!! ID: 1 13 | web_1 | [2019-11-25 20:59:26 +0000] [1] [DEBUG] 14 | web_1 | 15 | web_1 | Sanic 16 | web_1 | Build Fast. Run Fast. 17 | web_1 | 18 | web_1 | 19 | web_1 | [2019-11-25 12:59:26 +0000] [1] [INFO] Goin' Fast @ http://0.0.0.0:8000 20 | web_1 | [2019-11-25 12:59:27 +0000] [19] [INFO] Starting worker [19] 21 | ``` 22 | 23 | 容器可以随意创建和销毁,不会对本机环境有影响。这样访问 http://localhost:8000 就可以访问开发服务器了,本地修改代码会同步进容器直接生效。 24 | 25 | ## 相关文章 26 | 27 | 文章不是Docker和Docker compose的教程,你需要看官方文档,也推荐看下面列出的文章: 28 | 29 | 1. [Python项目容器化实践(一) - Docker Compose](https://www.dongwm.com/post/use-docker-compose/) 30 | -------------------------------------------------------------------------------- /docs/features.md: -------------------------------------------------------------------------------- 1 | # 功能 2 | 3 | 支持如下功能: 4 | 5 | * 可以通过后台对文章、标签等做增删改查 6 | * 后台支持Markdown编辑/预览 7 | * 支持代码语法高亮 8 | * 支持TOC 9 | * 支持文章搜索 10 | * 支持Github登录评论 11 | * 支持Github登录对文章和平台表态 12 | * 可以分享文章到微信/微博/豆瓣/印象笔记/Linkedin 13 | * 支持Hexo等其他Markdown源文件的导入 14 | * 支持文章的语法高亮 15 | * 支持个人设置(如设置头像,个人介绍) 16 | * 支持定制导航栏 17 | * 支持RSS/Sitemap 18 | * 相关文章推荐(根据相似标签) 19 | * 响应式设计 20 | * 支持评论提及邮件 21 | * 支持 Github Cards. 具体用法请看 [这里](#github-cards) 22 | * 文章内容(除代码部分之外)自动「盘古之白」 23 | * 支持「文章专题」 24 | * 支持「动态」 25 | * 可对评论回应 26 | * 支持用Docker Compose本地开发 27 | * 支持kubernetes上运行 28 | * Widget系统,内置aboutme、blogroll、most\_viewed、latest\_comments、tagcloud、html等widget 29 | * 导航栏项可以设置icon和颜色(如RSS) 30 | * 支持配置CDN域名服务静态文件 31 | * 支持日记类型文章,可以在右侧显示条目(读书/电影/游戏)信息 32 | * 显示收藏的条目(读书/电影/游戏)列表 33 | * 收藏的条目(读书/电影/游戏)列表可以从豆瓣同步进来 34 | -------------------------------------------------------------------------------- /docs/front-dev.md: -------------------------------------------------------------------------------- 1 | # 前端开发 2 | 3 | 博客的前端分3部分。首先确保已经安装了`yarn` 4 | 5 | ## 博客的Javascript 6 | 7 | 使用 Webpack+ES6+Sass ,首先需要安装依赖: 8 | 9 | ```bash 10 | ❯ yarn install 11 | ``` 12 | 13 | 接着启动开发环境: 14 | 15 | ```bash 16 | ❯ yarn start 17 | ``` 18 | 19 | 修改src目录下代码即可看到效果 20 | 21 | 生产环境需要构建: 22 | 23 | ```bash 24 | ❯ yarn build 25 | ``` 26 | 27 | ## 后台 28 | 29 | 使用 ElementUI+Vue-CLI+Vue-Router+Vuex+Webpack+ES6+Sass ,需要安装依赖: 30 | 31 | ```bash 32 | ❯ cd admin # 在 admin 子目录下 33 | ❯ yarn install 34 | ``` 35 | 36 | 接着启动开发环境: 37 | 38 | ```bash 39 | ❯ yarn serve 40 | ``` 41 | 42 | 修改src目录下代码即可看到效果 43 | 44 | 生产环境需要构建: 45 | 46 | ``` 47 | ❯ yarn build 48 | ❯ cp -rp dist/static/* ../static/ # 把新生成的文件拷贝到static目录下 49 | ``` 50 | 51 | ## 博客的CSS 52 | 53 | 修改static/css下非`min.css`后缀的CSS文件,然后执行如下命令合并和压缩: 54 | 55 | ``` 56 | python manage.py build-css 57 | ``` 58 | 59 | ## 「动态」页面(/activities) 60 | 61 | 使用 Vue-CLI+Vue-Router+Vuex+Webpack+ES6+Sass ,需要安装依赖: 62 | 63 | ```bash 64 | ❯ cd activity # 在 activity 子目录下 65 | ❯ yarn install 66 | ``` 67 | 68 | 接着启动开发环境: 69 | 70 | ```bash 71 | ❯ yarn serve 72 | ``` 73 | 74 | 修改src目录下代码即可看到效果 75 | 76 | 生产环境需要构建: 77 | 78 | ``` 79 | ❯ yarn build 80 | ❯ cp -rp dist/static/* ../static/ # 把新生成的文件拷贝到static目录下 81 | ``` 82 | 83 | ## 其他说明 84 | 85 | 管理后台和动态使用了一些通用组件和功能,放在了common目录下: 86 | 87 | ```bash 88 | ❯ tree common -L 2 89 | common 90 | └── src 91 | ├── api.js # API统一在这里 92 | ├── store # Vuex 93 | └── utils # 功能函数 94 | 95 | 3 directories, 1 file 96 | ``` 97 | 98 | 为了优化网页打开速度,Vue等库使用Webpack的externals参数项(在vue.config.js中)直接在模板中引入外部CDN(cdn.jsdelivr.net)的内容 99 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lyanna Document 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/kubernetes.md: -------------------------------------------------------------------------------- 1 | 除了使用[Docker Compose](docker-compose.md)以及常规的[Ansible部署](deploying.md)还可以在本地或者真实服务器的Kubernetes上运行。 2 | 3 | ## 安装Kubernetes 4 | 5 | 请参考相关文章链接1「在 macOS 上安装 Kubernetes」部分。 6 | 7 | ## 部署lyanna 8 | 9 | 请参考相关文章链接2「安装 NGINX Ingress」和「本地部署 lyanna」部分。 10 | 11 | ## 重要说明 12 | 13 | 暂时不推荐在生产环境中用Kubernetes来运行。原因见相关文章链接3「Kubernetes 版本的问题」部分。 14 | 15 | ## 相关文章 16 | 17 | 文章不是Docker和Docker compose的教程,你需要看官方文档,也推荐看下面列出的文章: 18 | 19 | 1. [Python项目容器化实践(四) - Kubernetes基础篇](https://www.dongwm.com/post/use-kubernetes-1/) 20 | 2. [Python项目容器化实践(八) - 将lyanna应用部署在本地Kubernetes上运行](https://www.dongwm.com/post/use-kubernetes-5/) 21 | 3. [Python项目容器化实践(一) - Docker Compose](https://www.dongwm.com/post/use-docker-compose/) 22 | -------------------------------------------------------------------------------- /docs/python-dev.md: -------------------------------------------------------------------------------- 1 | # 后端开发 2 | 3 | 首先需要新建一个 local_settings.py 文件,至少设置`DEBUG=True`,可以实现autoreload: 4 | 5 | ```bash 6 | ➜ cat local_settings.py 7 | DEBUG = True 8 | ``` 9 | 10 | 启动后调试应用即可: 11 | 12 | ```bash 13 | python app.py 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | ## 安装 4 | 5 | 首先确保要使用的CPython版本>=3.6,接着克隆项目: 6 | 7 | ```bash 8 | git clone https://github.com/dongweiming/lyanna 9 | cd lyanna 10 | ``` 11 | 12 | 安装依赖: 13 | 14 | ```bash 15 | python3 -m venv venv 16 | source venv/bin/activate 17 | pip install -r requirements.txt 18 | ``` 19 | 20 | ## 准备环境 21 | 22 | [重要]你需要确保安装并启动如下软件: 23 | 24 | 1. MySQL 25 | 2. Memcached 26 | 3. Redis 27 | 28 | ### 初始化 29 | 30 | 安装依赖后,需要做一些初始化工作: 31 | 32 | ```bash 33 | python manage.py initdb # 初始化数据库,创建表结构和索引 34 | python manage.py adduser --name YOUR_NAME --password YOUR_PASS --email YOUR_EMAIL # 创建可登录后台的管理员账号 35 | # 可选,如果你之前已经有博客,想把之前写的Markdown文章导入,如果是Hexo,灰常好, 36 | # 用下面脚本就可以,如果是其他系统可以按对应逻辑自己写写 37 | python hexo-exporter.py Markdown文件目录1 Markdown文件目录1 --uid=1 # uid是上面创建的管理员账号ID 38 | ``` 39 | 40 | 添加自己的配置项到local_settings.py文件中,具体选项可以看👉 [配置项](configuration.md) 41 | 42 | 最后启动应用就好啦: 43 | 44 | ```bash 45 | python app.py # 启动应用 46 | arq tasks.WorkerSettings # 启动arq支持异步任务 47 | ``` 48 | 49 | 如果你要部署到自己的服务器上,可以参考 [部署](deploying.md) 50 | 51 | ## 管理后台 52 | 53 | 这个博客系统内置了强大的管理后台,可以登录`/admin`子路径访问它(是本地环境就是访问`localhost:8000/admin`,如果是线上的话就是`DOMAIN:PORT/admin`。 54 | 55 | 可以通过后台添加用户(User)、文章(Post)、专题(Topic)等内容。 56 | 57 | ## v4不兼容问题 58 | 59 | 从v4开始,支持条目卡片、收藏等功能,可以通过如下命令完成迁移: 60 | 61 | ```bash 62 | python manage.py migrate-for-v4 63 | ``` 64 | 65 | 66 | ## v3.5不兼容问题 67 | 68 | 从v3.5开始,删除文章会同时删除对应的动态,可以通过如下命令完成迁移: 69 | 70 | ```bash 71 | python manage.py migrate-for-v35 72 | ``` 73 | 74 | 如果版本<=3.0看下面👇 75 | 76 | ## v3.0不兼容问题 77 | 78 | 在v3.0添加了动态功能,如果之前你已经使用的版本>2.7,可以通过如下命令完成迁移: 79 | 80 | ```bash 81 | python manage.py migrate-for-v30 82 | ``` 83 | 84 | 如果版本<=2.7看下面👇 85 | 86 | ## v2.7不兼容问题 87 | 88 | 在v2.5时有做了很多修改,如果之前你已经使用的版本>=2.5,可以通过如下命令完成迁移: 89 | 90 | ```bash 91 | python manage.py migrate-for-v27 92 | ``` 93 | 94 | 如果版本<2.5看下面👇 95 | 96 | ## v2.5不兼容问题 97 | 98 | 在v2.5时修改了`posts`表结构,如果之前你已经使用了lyanna,升级后会找不到新加的字段,报错如下: 99 | 100 | ```python 101 | ... 102 | tortoise.exceptions.OperationalError: (1054, "Unknown column 'pageview' in 'field list'") 103 | ``` 104 | 105 | 可以通过如下命令修改: 106 | 107 | ```bash 108 | python manage.py migrate-for-v25 109 | ``` 110 | 111 | ## 联系我 112 | 113 | > - [GitHub](https://github.com/dongweiming "github") 114 | > - [ciici123@gmail.com](mailto:ciici123@gmail.com) 115 | 116 | [![QQ群](https://img.shields.io/badge/QQ%E7%BE%A4-522012167-yellowgreen.svg)](https://jq.qq.com/?_wv=1027&k=5RS89BW) 117 | -------------------------------------------------------------------------------- /ext.py: -------------------------------------------------------------------------------- 1 | from sanic_mako import SanicMako 2 | from tortoise import Tortoise 3 | 4 | from config import DB_URL, SENTRY_DSN 5 | 6 | mako = SanicMako() 7 | 8 | 9 | async def init_db(create_db: bool = False) -> None: 10 | await Tortoise.init( 11 | db_url=DB_URL, 12 | modules={'models': ['models']}, 13 | _create_db=create_db 14 | ) 15 | 16 | if SENTRY_DSN: 17 | from sanic_sentry import SanicSentry 18 | sentry = SanicSentry() 19 | else: 20 | sentry = None 21 | -------------------------------------------------------------------------------- /hexo-exporter.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import glob 3 | from typing import List 4 | 5 | import yaml 6 | from tortoise import run_async 7 | from tortoise.exceptions import IntegrityError 8 | 9 | from ext import init_db 10 | from models import Post 11 | 12 | parser = argparse.ArgumentParser(description='Hexo Exporter') 13 | parser.add_argument('source_dirs', metavar='DIR', type=str, nargs='+', 14 | help='Markdown file directories') 15 | parser.add_argument('--uid', type=int, help='Author ID') 16 | args = parser.parse_args() 17 | source_dirs = args.source_dirs 18 | 19 | if not args.uid: 20 | print('the `uid` argument are required!') 21 | exit(1) 22 | 23 | files: List[str] = [] 24 | 25 | for dir in source_dirs: 26 | files.extend(sum([glob.glob(f'{dir}/*.{pattern}') 27 | for pattern in ('md', 'markdown')], [])) 28 | 29 | files = sorted(files, reverse=False) 30 | 31 | 32 | async def write_post(file: str) -> None: 33 | flag = False 34 | meta_info = '' 35 | 36 | with open(file) as f: 37 | for i in f: 38 | i = i.strip() 39 | if i == '---' and flag: 40 | break 41 | if i == '---': 42 | flag = True 43 | continue 44 | meta_info += i + '\n' 45 | 46 | meta_dict = yaml.safe_load(meta_info) 47 | title = meta_dict['title'] 48 | date = meta_dict.get('date') or meta_dict.get('created') 49 | tags = meta_dict.get('tags', []) 50 | if not (title and date): 51 | print(f"[Fail] Parse meta failed in {f.name}") 52 | return 53 | content = ''.join(f.readlines()) 54 | 55 | try: 56 | await Post.create(title=title, content=content, 57 | tags=tags, author_id=args.uid, slug='', 58 | summary='', status=Post.STATUS_ONLINE, 59 | created_at=str(date)) 60 | print(f"[Success] Load post: {f.name}") 61 | except IntegrityError: 62 | ... 63 | 64 | 65 | async def main() -> None: 66 | await init_db() 67 | for f in files: 68 | await write_post(f) 69 | 70 | 71 | if __name__ == '__main__': 72 | run_async(main()) 73 | -------------------------------------------------------------------------------- /k8s/arq.yaml: -------------------------------------------------------------------------------- 1 | kind: DaemonSet 2 | apiVersion: apps/v1 3 | metadata: 4 | name: lyanna-arq 5 | labels: 6 | app.kubernetes.io/name: lyanna-arq 7 | spec: 8 | selector: 9 | matchLabels: 10 | app.kubernetes.io/name: lyanna-arq 11 | template: 12 | metadata: 13 | labels: 14 | app.kubernetes.io/name: lyanna-arq 15 | spec: 16 | containers: 17 | - image: dongweiming/lyanna:latest 18 | #imagePullPolicy: Always 19 | name: lyanna-web 20 | command: ['sh', '-c', './arq tasks.WorkerSettings'] 21 | resources: 22 | limits: 23 | cpu: 300m 24 | memory: 100Mi 25 | requests: 26 | cpu: 100m 27 | memory: 50Mi 28 | env: 29 | - name: MEMCACHED_HOST 30 | valueFrom: 31 | configMapKeyRef: 32 | name: lyanna-cfg 33 | key: memcached_host 34 | - name: DB_URL 35 | valueFrom: 36 | configMapKeyRef: 37 | name: lyanna-cfg 38 | key: db_url 39 | - name: REDIS_SENTINEL_SVC_HOST 40 | valueFrom: 41 | configMapKeyRef: 42 | name: lyanna-cfg 43 | key: redis_sentinel_host 44 | - name: REDIS_SENTINEL_SVC_POST 45 | valueFrom: 46 | configMapKeyRef: 47 | name: lyanna-cfg 48 | key: redis_sentinel_port 49 | - name: REDIS_URL 50 | valueFrom: 51 | configMapKeyRef: 52 | name: lyanna-cfg 53 | key: redis_url 54 | - name: PYTHONPATH 55 | value: $PYTHONPATH:/usr/local/src/aiomysql:/usr/local/src/aiomcache:/usr/local/src/tortoise:/usr/local/src/arq:/usr/local/src 56 | -------------------------------------------------------------------------------- /k8s/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: lyanna-cfg 5 | data: 6 | db_port: "3306" 7 | database: test 8 | db_user: lyanna 9 | db_password: lyanna 10 | memcached_host: lyanna-memcached 11 | replication-password: lyanna 12 | redis_sentinel_host: redis-sentinel 13 | redis_sentinel_port: "26379" 14 | db_url: mysql://lyanna:lyanna@lyanna-mariadb:3306/test?charset=utf8 15 | local_settings.py: | 16 | SITE_TITLE = '小明明s à domicile(Kubernetes版)' 17 | BLOG_URL = 'https://blog.dongwm.com' 18 | --- 19 | apiVersion: extensions/v1beta1 20 | kind: Ingress 21 | metadata: 22 | name: lyanna-ing-static 23 | annotations: 24 | kubernetes.io/ingress.class: "nginx" 25 | nginx.ingress.kubernetes.io/use-regex: "true" 26 | nginx.ingress.kubernetes.io/proxy-buffering: "on" 27 | nginx.ingress.kubernetes.io/configuration-snippet: | 28 | include /etc/nginx/mime.types; 29 | proxy_cache static-cache; 30 | proxy_ignore_headers Cache-Control; 31 | proxy_cache_valid any 30m; 32 | add_header X-Cache-Status $upstream_cache_status; 33 | spec: 34 | rules: 35 | - host: lyanna.local 36 | http: 37 | paths: 38 | - path: /static 39 | backend: 40 | serviceName: lyanna-svc 41 | servicePort: 80 42 | - path: /img 43 | backend: 44 | serviceName: lyanna-svc 45 | servicePort: 80 46 | - path: /fonts 47 | backend: 48 | serviceName: lyanna-svc 49 | servicePort: 80 50 | --- 51 | apiVersion: extensions/v1beta1 52 | kind: Ingress 53 | metadata: 54 | name: lyanna-ing-admin 55 | annotations: 56 | kubernetes.io/ingress.class: "nginx" 57 | nginx.ingress.kubernetes.io/use-regex: "true" 58 | nginx.ingress.kubernetes.io/configuration-snippet: | 59 | allow "1.2.3.4"; 60 | deny all; 61 | spec: 62 | rules: 63 | - host: lyanna.local 64 | http: 65 | paths: 66 | - path: /admin 67 | backend: 68 | serviceName: lyanna-svc 69 | servicePort: 80 70 | --- 71 | apiVersion: extensions/v1beta1 72 | kind: Ingress 73 | metadata: 74 | name: lyanna-ing 75 | annotations: 76 | kubernetes.io/ingress.class: "nginx" 77 | spec: 78 | rules: 79 | - host: lyanna.local 80 | http: 81 | paths: 82 | - path: / 83 | backend: 84 | serviceName: lyanna-svc 85 | servicePort: 80 86 | -------------------------------------------------------------------------------- /k8s/memcached.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: lyanna-memcached 5 | labels: 6 | app.kubernetes.io/name: memcached 7 | spec: 8 | podManagementPolicy: OrderedReady 9 | replicas: 1 10 | revisionHistoryLimit: 10 11 | selector: 12 | matchLabels: 13 | app.kubernetes.io/name: memcached 14 | serviceName: lyanna-memcached 15 | template: 16 | metadata: 17 | labels: 18 | app.kubernetes.io/name: memcached 19 | spec: 20 | containers: 21 | - command: 22 | - memcached 23 | - -o 24 | - modern 25 | - -v 26 | - -I 27 | - 20m 28 | image: memcached:latest 29 | imagePullPolicy: IfNotPresent 30 | resources: 31 | limits: 32 | cpu: 200m 33 | memory: 100Mi 34 | requests: 35 | cpu: 100m 36 | memory: 50Mi 37 | livenessProbe: 38 | failureThreshold: 3 39 | initialDelaySeconds: 10 40 | periodSeconds: 10 41 | successThreshold: 1 42 | tcpSocket: 43 | port: memcache 44 | timeoutSeconds: 5 45 | name: lyanna-memcached 46 | ports: 47 | - containerPort: 11211 48 | name: memcache 49 | protocol: TCP 50 | readinessProbe: 51 | failureThreshold: 3 52 | initialDelaySeconds: 5 53 | periodSeconds: 10 54 | successThreshold: 1 55 | tcpSocket: 56 | port: memcache 57 | timeoutSeconds: 1 58 | resources: 59 | requests: 60 | cpu: 50m 61 | memory: 64Mi 62 | securityContext: 63 | runAsUser: 1001 64 | dnsPolicy: ClusterFirst 65 | restartPolicy: Always 66 | securityContext: 67 | fsGroup: 1001 68 | terminationGracePeriodSeconds: 30 69 | updateStrategy: 70 | type: RollingUpdate 71 | --- 72 | apiVersion: v1 73 | kind: Service 74 | metadata: 75 | labels: 76 | app.kubernetes.io/name: memcached 77 | name: lyanna-memcached 78 | spec: 79 | clusterIP: None 80 | ports: 81 | - name: memcache 82 | port: 11211 83 | protocol: TCP 84 | targetPort: memcache 85 | selector: 86 | app.kubernetes.io/name: memcached 87 | sessionAffinity: ClientIP 88 | -------------------------------------------------------------------------------- /k8s/nginx.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | keep-alive: "5" 4 | http-snippet: | 5 | proxy_cache_path /tmp/nginx-cache levels=1:2 keys_zone=static-cache:2m max_size=100m inactive=7d use_temp_path=off; 6 | proxy_cache_key $scheme$proxy_host$request_uri; 7 | proxy_cache_lock on; 8 | proxy_cache_use_stale updating; 9 | kind: ConfigMap 10 | metadata: 11 | name: nginx-configuration 12 | namespace: ingress-nginx 13 | labels: 14 | app.kubernetes.io/name: ingress-nginx 15 | app.kubernetes.io/part-of: ingress-nginx 16 | -------------------------------------------------------------------------------- /k8s/sentinel/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2016 The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM alpine:3.8 16 | 17 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories \ 18 | && apk update \ 19 | && apk add --no-cache redis sed bash 20 | 21 | COPY redis-master.conf /redis-master/redis.conf 22 | COPY redis-slave.conf /redis-slave/redis.conf 23 | COPY run.sh /run.sh 24 | 25 | CMD [ "/run.sh" ] 26 | 27 | ENTRYPOINT [ "bash", "-c" ] 28 | -------------------------------------------------------------------------------- /libs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/libs/__init__.py -------------------------------------------------------------------------------- /libs/extracted.py: -------------------------------------------------------------------------------- 1 | from extraction import Extracted 2 | from extraction.techniques import Technique 3 | 4 | 5 | class DoubanGameExtracted(Extracted): 6 | @property 7 | def image(self): 8 | if self.images: 9 | return self.images[1] 10 | 11 | @property 12 | def description(self): 13 | if self.descriptions: 14 | return self.descriptions[3] 15 | 16 | 17 | class MetacriticExtracted(Extracted): 18 | @property 19 | def description(self): 20 | return None 21 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | from sanic_jwt import exceptions # noqa 2 | 3 | from .activity import Activity, Status # noqa 4 | from .blog import (Post, PostTag, SpecialItem, SpecialTopic, # noqa 5 | Tag, Subject, Favorite) 6 | from .comment import Comment # noqa 7 | from .react import ReactItem, ReactStats # noqa 8 | from .user import GithubUser, User, create_user, validate_login # noqa 9 | 10 | 11 | async def jwt_authenticate(request, *args, **kwargs): 12 | username = request.json.get('username', None) 13 | password = request.json.get('password', None) 14 | 15 | if not username or not password: 16 | raise exceptions.AuthenticationFailed('Missing username or password.') 17 | 18 | ok, user = await validate_login(username, password) 19 | if not ok: 20 | raise exceptions.AuthenticationFailed('User or Password is incorrect.') 21 | 22 | if not user.active: # type: ignore 23 | raise exceptions.AuthenticationFailed( 24 | 'The account has been deactivated!') 25 | return {'user_id': user.id} # type: ignore 26 | -------------------------------------------------------------------------------- /models/consts.py: -------------------------------------------------------------------------------- 1 | K_POST = 1001 2 | K_COMMENT = 1002 3 | K_STATUS = 1003 4 | K_ACTIVITY = 1004 5 | K_CARD = 1005 6 | K_FAVORITE = 1006 7 | 8 | K_DOUBAN = 0 9 | K_METACRITIC = 1 10 | K_OTHER = 9 11 | 12 | ONE_MINUTE = 60 13 | ONE_HOUR = ONE_MINUTE * 60 14 | ONE_DAY = ONE_HOUR * 24 15 | 16 | T_MOVIE = 'movie' 17 | T_BOOK = 'book' 18 | T_GAME = 'game' 19 | 20 | SUBDOMAIN_MAP = { 21 | T_MOVIE: 'movie', 22 | T_BOOK: 'book', 23 | T_GAME: 'www' 24 | } 25 | 26 | PERMALINK_TYPES = ('id', 'slug', 'title') 27 | STATIC_FILE_TYPES = ('jpg', 'png', 'webp', 'gif', 'mp4', 'css', 'js') 28 | 29 | UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36' # noqa 30 | -------------------------------------------------------------------------------- /models/mention.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import List 3 | 4 | from tortoise.expressions import Q 5 | 6 | from models.user import GithubUser 7 | 8 | MENTION_LIMIT = 20 9 | _METION_UID_RE = re.compile(r'@([a-zA-Z0-9][-.\w]*?)(?![-.\w])') 10 | EMAIL_SUBJECT = '有人在 {title} 的评论中提到了你!' 11 | 12 | 13 | class Mention: 14 | @staticmethod 15 | def _parse_mention_names(author: GithubUser, content: str = '') -> List[str]: 16 | names = set(name for name in _METION_UID_RE.findall(content) 17 | if name and name != author.username) 18 | return list(names)[:MENTION_LIMIT] 19 | 20 | @classmethod 21 | async def get_mention_users(cls, content: str, author_id: int) -> List[GithubUser]: 22 | if not (author := await GithubUser.cache(author_id)): 23 | return [] 24 | mention_names = cls._parse_mention_names(author, content) 25 | return await GithubUser.filter(Q(username__in=mention_names)) 26 | -------------------------------------------------------------------------------- /models/mixin.py: -------------------------------------------------------------------------------- 1 | import markupsafe 2 | from aioredis.commands import Redis 3 | 4 | from .markdown import markdown 5 | from .utils import get_redis 6 | 7 | 8 | class ContentMixin: 9 | id: int 10 | 11 | @property 12 | async def redis(self) -> Redis: 13 | return await get_redis() 14 | 15 | def get_db_key(self, key: str) -> str: 16 | return f'{self.__class__.__name__}/{self.id}/props/{key}' 17 | 18 | async def set_props_by_key(self, key: str, value: bytes) -> bool: 19 | key = self.get_db_key(key) 20 | return await (await self.redis).set(key, value) # noqa: W606 21 | 22 | async def get_props_by_key(self, key: str) -> bytes: 23 | key = self.get_db_key(key) 24 | return await (await self.redis).get(key) or b'' # noqa: W606 25 | 26 | async def set_content(self, content: bytes) -> bool: 27 | return await self.set_props_by_key('content', content) 28 | 29 | async def save(self, *args, **kwargs): 30 | if (content := kwargs.pop('content', None)) is not None: 31 | await self.set_content(content) 32 | return await super().save(*args, **kwargs) # type: ignore 33 | 34 | @property 35 | async def content(self) -> str: 36 | if (rv := await self.get_props_by_key('content')): 37 | return rv.decode('utf-8') 38 | return '' 39 | 40 | @property 41 | async def html_content(self): 42 | if not (content := str(markupsafe.escape(await self.content))): 43 | return '' 44 | return markdown(content.replace('>', '>')) 45 | -------------------------------------------------------------------------------- /models/signals.py: -------------------------------------------------------------------------------- 1 | from asyncblink import signal 2 | 3 | comment_reacted = signal('comment_reacted') 4 | post_created = signal('post_created') 5 | -------------------------------------------------------------------------------- /models/user.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Tuple, Union 2 | 3 | from tortoise import fields 4 | from werkzeug.security import check_password_hash, generate_password_hash 5 | 6 | from .base import BaseModel 7 | 8 | 9 | class User(BaseModel): 10 | email = fields.CharField(max_length=100) 11 | name = fields.CharField(max_length=100, unique=True) 12 | avatar = fields.CharField(max_length=100, default='') 13 | password = fields.TextField() 14 | active = fields.BooleanField(default=True) 15 | 16 | class Meta: 17 | table = 'users' 18 | 19 | def to_dict(self) -> Dict[str, Any]: 20 | rv = super().to_dict() 21 | rv.pop('password') 22 | return rv 23 | 24 | 25 | class GithubUser(BaseModel): 26 | gid = fields.IntField(unique=True) 27 | email = fields.CharField(max_length=100, default='', unique=True) 28 | username = fields.CharField(max_length=100, unique=True) 29 | picture = fields.CharField(max_length=100, default='') 30 | link = fields.CharField(max_length=100, default='') 31 | 32 | class Meta: 33 | table = 'github_users' 34 | 35 | def to_dict(self) -> Dict[str, Any]: 36 | rv = super().to_dict() 37 | rv.pop('created_at') 38 | return rv 39 | 40 | 41 | def generate_password(password: str) -> str: 42 | return generate_password_hash( 43 | password, method='pbkdf2:sha256') 44 | 45 | 46 | async def create_user(**data) -> User: 47 | if 'name' not in data or 'password' not in data: 48 | raise ValueError('username and password are required.') 49 | 50 | data['password'] = generate_password(data.pop('password')) 51 | 52 | user = await User.create(**data) 53 | return user 54 | 55 | 56 | async def validate_login(name: str, password: str) -> Tuple[bool, Union[User, None]]: 57 | if not (user := await User.filter(name=name).first()): 58 | return False, None 59 | if check_password_hash(user.password, password): 60 | return True, user 61 | return False, User() 62 | 63 | 64 | async def create_github_user(user_info) -> GithubUser: 65 | user = await GithubUser.filter(gid=user_info.id).first() 66 | kwargs = { 67 | 'gid': user_info.id, 68 | 'link': user_info.link, 69 | 'picture': user_info.picture, 70 | 'username': user_info.username, 71 | 'email': user_info.email or user_info.username, 72 | } 73 | user = await (user.update(**kwargs) if user 74 | else GithubUser.create(**kwargs)) 75 | return user 76 | -------------------------------------------------------------------------------- /models/var.py: -------------------------------------------------------------------------------- 1 | import contextvars 2 | from typing import Any 3 | 4 | redis_var: contextvars.ContextVar[Any] = contextvars.ContextVar('redis') 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lyanna", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "start": "webpack --progress --colors --watch --mode development", 8 | "build": "webpack --mode production" 9 | }, 10 | "author": "Dongweiming", 11 | "devDependencies": { 12 | "@babel/core": "^7.1.5", 13 | "@babel/preset-env": "^7.1.5", 14 | "babel-core": "^6.26.3", 15 | "babel-loader": "^8.0.4", 16 | "babel-preset-es2015": "^6.24.1", 17 | "codemirror": "^5.42.0", 18 | "css-loader": "^1.0.1", 19 | "file-loader": "^2.0.0", 20 | "glob": "^7.1.3", 21 | "highlight.js": "^9.13.1", 22 | "marked": "^1.0.0", 23 | "node-sass": "^4.13.0", 24 | "qrcode": "^1.3.2", 25 | "sass-loader": "^7.1.0", 26 | "select2": "^4.0.6-rc.1", 27 | "style-loader": "^0.23.1", 28 | "url-loader": "^1.1.2", 29 | "webpack": "^4.25.1", 30 | "webpack-cli": "^3.1.2" 31 | }, 32 | "dependencies": { 33 | "jquery": "^3.3.1", 34 | "tributejs": "^4.1.1", 35 | "uikit": "^3.0.0-rc.25" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 88 3 | py38 = true 4 | skip-string-normalization = true 5 | 6 | [tool.isort] 7 | known_third_party = "sanic" 8 | not_skip = "__init__.py" 9 | multi_line_output = 5 10 | include_trailing_comma = true 11 | force_grid_wrap = 0 12 | force_single_line = false 13 | balanced_wrapping = true 14 | use_parentheses = true -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==24.1.0 2 | aiohappyeyeballs==2.6.1 3 | aiohttp==3.11.14 4 | aiomysql==0.0.22 5 | aioredis==1.3.1 6 | aiosignal==1.3.2 7 | aiosmtplib==1.1.6 8 | aiosqlite==0.17.0 9 | annotated-types==0.7.0 10 | arq==0.22 11 | async-timeout==5.0.1 12 | asyncblink==0.3.2 13 | attrs==25.3.0 14 | beautifulsoup4==4.13.3 15 | blinker==1.6.3 16 | click==8.0.4 17 | cssmin==0.2.0 18 | Cython==0.29.28 19 | extraction==0.3 20 | frozenlist==1.5.0 21 | gunicorn==20.1.0 22 | hiredis==3.1.0 23 | html5lib==1.1 24 | httptools==0.6.4 25 | idna==3.10 26 | iso8601==0.1.16 27 | lxml==5.3.1 28 | Mako==1.3.9 29 | MarkupSafe==3.0.2 30 | mistune==0.8.4 31 | multidict==5.2.0 32 | pangu==4.0.6.1 33 | propcache==0.3.1 34 | pydantic==1.10.21 35 | pydantic_core==2.33.0 36 | Pygments==2.11.2 37 | PyJWT==2.1.0 38 | PyMySQL==0.9.3 39 | pypika-tortoise==0.1.6 40 | pytz==2025.2 41 | PyYAML==6.0.2 42 | raven==6.10.0 43 | raven-aiohttp==0.7.0 44 | redis==4.4.4 45 | sanic==21.12.2 46 | sanic-jwt==1.7.0 47 | Sanic-Mako==0.7.0 48 | -e git+https://github.com/dongweiming/sanic-oauth.git@3cc63a9ab0dad56b9bca9a70b4788dec0ca0dae6#egg=sanic_oauth 49 | sanic-routing==0.7.2 50 | sanic-sentry==0.1.7 51 | sanic-session==0.8.0 52 | Sanic-WTF==0.6.0 53 | setuptools==78.1.0 54 | six==1.17.0 55 | soupsieve==2.6 56 | tortoise-orm==0.18.1 57 | typing-inspection==0.4.0 58 | typing_extensions==4.13.0 59 | ujson==5.10.0 60 | uvloop==0.21.0 61 | webencodings==0.5.1 62 | websockets==10.0 63 | Werkzeug==0.16.1 64 | WTForms==3.0.1 65 | yarl==1.18.3 66 | -------------------------------------------------------------------------------- /screenshot/admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/screenshot/admin.png -------------------------------------------------------------------------------- /screenshot/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/screenshot/index.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [mypy] 2 | pretty = True 3 | ignore_missing_imports = True 4 | check_untyped_defs = True 5 | disallow_untyped_calls = True 6 | disallow_untyped_defs = False 7 | disallow_incomplete_defs = False 8 | disallow_untyped_decorators = False 9 | no_implicit_optional = True 10 | warn_redundant_casts = True 11 | warn_unused_ignores = True 12 | warn_return_any = False 13 | warn_no_return = False 14 | warn_unused_configs = True 15 | allow_redefinition = True 16 | strict_equality = True 17 | show_error_context = True 18 | 19 | [flake8] 20 | max_line_length = 88 -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | # Init DB 4 | python manage.py initdb 5 | # Create default superuser 6 | python manage.py adduser --name admin --password admin123 --email admin@admin.com 7 | -------------------------------------------------------------------------------- /src/activities/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/activities/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "src", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview --port 5050" 8 | }, 9 | "dependencies": { 10 | "axios": "^0.26.1", 11 | "js-cookie": "^3.0.1", 12 | "moment": "^2.29.1", 13 | "plyr": "^3.6.12", 14 | "qs": "^6.10.3", 15 | "sass": "^1.49.9", 16 | "sass-loader": "^12.6.0", 17 | "vue": "^3.2.31", 18 | "vue-plyr": "^7.0.0", 19 | "vue-router": "^4.0.14", 20 | "vue-toastification": "^2.0.0-rc.5", 21 | "vue-upload-component": "^3.1.2", 22 | "vuex": "^4.0.2" 23 | }, 24 | "devDependencies": { 25 | "@rollup/plugin-babel": "^5.3.1", 26 | "@vitejs/plugin-vue": "^2.2.2", 27 | "vite": "^2.8.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/activities/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/src/activities/public/favicon.ico -------------------------------------------------------------------------------- /src/activities/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /src/activities/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | position: relative; 59 | font-weight: normal; 60 | } 61 | 62 | body { 63 | min-height: 100vh; 64 | color: var(--color-text); 65 | background: var(--color-background); 66 | transition: color 0.5s, background-color 0.5s; 67 | line-height: 1.6; 68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 70 | font-size: 15px; 71 | text-rendering: optimizeLegibility; 72 | -webkit-font-smoothing: antialiased; 73 | -moz-osx-font-smoothing: grayscale; 74 | } 75 | -------------------------------------------------------------------------------- /src/activities/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/activities/src/components/Paginator.vue: -------------------------------------------------------------------------------- 1 | 16 | 37 | 38 | 71 | -------------------------------------------------------------------------------- /src/activities/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | 3 | import Toast from "vue-toastification" 4 | import VuePlyr from 'vue-plyr' 5 | import VueUploadComponent from 'vue-upload-component' 6 | import "vue-toastification/dist/index.css" 7 | import 'vue-plyr/dist/vue-plyr.css' 8 | 9 | import router from './router' 10 | import App from './App.vue' 11 | import store from './store' 12 | 13 | const app = createApp(App) 14 | 15 | app.use(router) 16 | app.use(store) 17 | app.use(Toast) 18 | app.use(VuePlyr, { 19 | plyr: { 20 | captions: { 21 | defaultActive: false 22 | }, 23 | controls: ["play", "progress", "current-time", "duration", "mute", "volume", "fullscreen"] 24 | } 25 | }) 26 | app.component('file-upload', VueUploadComponent) 27 | 28 | app.mount('#app') 29 | -------------------------------------------------------------------------------- /src/activities/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | 3 | import Activity from '../views/Activity.vue' 4 | 5 | const router = createRouter({ 6 | history: createWebHistory(import.meta.env.BASE_URL), 7 | routes: [ 8 | { 9 | path: '/activities', 10 | name: 'activities', 11 | component: Activity 12 | } 13 | ] 14 | }) 15 | 16 | export default router 17 | -------------------------------------------------------------------------------- /src/activities/src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | token: state => state.user.token, 3 | avatar: state => state.user.avatar, 4 | name: state => state.user.name, 5 | sidebar: state => state.app.sidebar, 6 | device: state => state.app.device, 7 | visitedViews: state => state.tagsView.visitedViews, 8 | cachedViews: state => state.tagsView.cachedViews, 9 | size: state => state.app.size, 10 | } 11 | 12 | export default getters 13 | -------------------------------------------------------------------------------- /src/activities/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | 3 | import app from './modules/app' 4 | import user from './modules/user' 5 | import getters from './getters' 6 | import tagsView from './modules/tagsView' 7 | 8 | const store = createStore({ 9 | modules: { 10 | app, 11 | user, 12 | tagsView 13 | }, 14 | getters 15 | }) 16 | 17 | export default store 18 | -------------------------------------------------------------------------------- /src/activities/src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const app = { 4 | state: { 5 | sidebar: { 6 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, 7 | withoutAnimation: false 8 | }, 9 | device: 'desktop', 10 | language: Cookies.get('language') || 'en', 11 | size: Cookies.get('size') || 'medium' 12 | }, 13 | mutations: { 14 | TOGGLE_SIDEBAR: state => { 15 | state.sidebar.opened = !state.sidebar.opened 16 | state.sidebar.withoutAnimation = false 17 | if (state.sidebar.opened) { 18 | Cookies.set('sidebarStatus', 1) 19 | } else { 20 | Cookies.set('sidebarStatus', 0) 21 | } 22 | }, 23 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 24 | Cookies.set('sidebarStatus', 0) 25 | state.sidebar.opened = false 26 | state.sidebar.withoutAnimation = withoutAnimation 27 | }, 28 | TOGGLE_DEVICE: (state, device) => { 29 | state.device = device 30 | }, 31 | SET_LANGUAGE: (state, language) => { 32 | state.language = language 33 | Cookies.set('language', language) 34 | }, 35 | SET_SIZE: (state, size) => { 36 | state.size = size 37 | Cookies.set('size', size) 38 | } 39 | }, 40 | actions: { 41 | toggleSideBar({ commit }) { 42 | commit('TOGGLE_SIDEBAR') 43 | }, 44 | closeSideBar({ commit }, { withoutAnimation }) { 45 | commit('CLOSE_SIDEBAR', withoutAnimation) 46 | }, 47 | toggleDevice({ commit }, device) { 48 | commit('TOGGLE_DEVICE', device) 49 | }, 50 | setLanguage({ commit }, language) { 51 | commit('SET_LANGUAGE', language) 52 | }, 53 | setSize({ commit }, size) { 54 | commit('SET_SIZE', size) 55 | } 56 | } 57 | } 58 | 59 | export default app 60 | -------------------------------------------------------------------------------- /src/activities/src/store/modules/errorLog.js: -------------------------------------------------------------------------------- 1 | const errorLog = { 2 | state: { 3 | logs: [] 4 | }, 5 | mutations: { 6 | ADD_ERROR_LOG: (state, log) => { 7 | state.logs.push(log) 8 | } 9 | }, 10 | actions: { 11 | addErrorLog({ commit }, log) { 12 | commit('ADD_ERROR_LOG', log) 13 | } 14 | } 15 | } 16 | 17 | export default errorLog 18 | -------------------------------------------------------------------------------- /src/activities/src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { loginByUsername, getUserInfo } from '@/api' 2 | import { getToken, setToken, removeToken } from '@/utils/auth' 3 | 4 | const user = { 5 | state: { 6 | user: '', 7 | status: '', 8 | code: '', 9 | token: getToken(), 10 | name: '', 11 | avatar: '' 12 | }, 13 | 14 | mutations: { 15 | SET_CODE: (state, code) => { 16 | state.code = code 17 | }, 18 | SET_TOKEN: (state, token) => { 19 | state.token = token 20 | }, 21 | SET_STATUS: (state, status) => { 22 | state.status = status 23 | }, 24 | SET_NAME: (state, name) => { 25 | state.name = name 26 | }, 27 | SET_AVATAR: (state, avatar) => { 28 | state.avatar = avatar 29 | } 30 | }, 31 | 32 | actions: { 33 | // 用户名登录 34 | LoginByUsername({ commit }, userInfo) { 35 | const username = userInfo.username.trim() 36 | return new Promise((resolve, reject) => { 37 | loginByUsername(username, userInfo.password).then(response => { 38 | const data = response.data 39 | commit('SET_TOKEN', data.access_token) 40 | setToken(response.data.access_token) 41 | resolve() 42 | }).catch(error => { 43 | reject(error) 44 | }) 45 | }) 46 | }, 47 | 48 | // 获取用户信息 49 | GetUserInfo({ commit, state }) { 50 | return new Promise((resolve, reject) => { 51 | getUserInfo(state.token).then(response => { 52 | // 由于mockjs 不支持自定义状态码只能这样hack 53 | if (!response.data) { 54 | reject('Verification failed, please login again.') 55 | } 56 | const data = response.data 57 | 58 | commit('SET_NAME', data.name) 59 | commit('SET_AVATAR', data.avatar) 60 | resolve(response) 61 | }).catch(error => { 62 | reject(error) 63 | }) 64 | }) 65 | }, 66 | 67 | // 设置新的头像 68 | SetNewAvatar({ commit, state }, args) { 69 | if (state.name == args[0]) { 70 | commit('SET_AVATAR', args[1]) 71 | } 72 | }, 73 | 74 | // 前端 登出 75 | FedLogOut({ commit }) { 76 | return new Promise(resolve => { 77 | commit('SET_TOKEN', '') 78 | removeToken() 79 | resolve() 80 | }) 81 | } 82 | } 83 | } 84 | 85 | export default user 86 | -------------------------------------------------------------------------------- /src/activities/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | const TokenKey = 'Lyanna-Token' 2 | 3 | export function getToken() { 4 | return localStorage.getItem(TokenKey) 5 | } 6 | 7 | export function setToken(token) { 8 | return localStorage.setItem(TokenKey, token) 9 | } 10 | 11 | export function removeToken() { 12 | return localStorage.removeItem(TokenKey) 13 | } 14 | -------------------------------------------------------------------------------- /src/activities/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import store from '@/store' 3 | import { getToken } from './auth' 4 | 5 | // create an axios instance 6 | const service = axios.create({ 7 | baseURL: import.meta.env.BASE_URL, 8 | timeout: 5000 // request timeout 9 | }) 10 | 11 | // request interceptor 12 | service.interceptors.request.use( 13 | config => { 14 | // Do something before request is sent 15 | if (store.getters.token) { 16 | config.headers['authorization'] = `Bearer ${getToken()}` 17 | } 18 | return config 19 | }, 20 | error => { 21 | // Do something with request error 22 | console.log(error) // for debug 23 | Promise.reject(error) 24 | } 25 | ) 26 | 27 | // response interceptor 28 | service.interceptors.response.use( 29 | response => response, 30 | error => { 31 | console.log('err' + error) // for debug 32 | return Promise.reject(error) 33 | } 34 | ) 35 | 36 | export default service 37 | -------------------------------------------------------------------------------- /src/activities/vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | 6 | export default defineConfig({ 7 | plugins: [vue()], 8 | resolve: { 9 | alias: { 10 | '@': fileURLToPath(new URL('./src', import.meta.url)), 11 | } 12 | }, 13 | build: { 14 | rollupOptions: { 15 | output: { 16 | entryFileNames: '[name].js', 17 | chunkFileNames: '[name].js', 18 | assetFileNames: '[name].[ext]' 19 | } 20 | } 21 | }, 22 | server: { 23 | proxy: { 24 | ...['/api', '/static', '/j'].reduce( 25 | (acc, ctx) => ({ 26 | ...acc, 27 | [ctx]: { 28 | target: 'http://127.0.0.1:8000', 29 | changeOrigin: true 30 | }, 31 | }), 32 | {} 33 | ), 34 | } 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /src/admin/.eslintrc-auto-import.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "computed": true, 4 | "createApp": true, 5 | "createLogger": true, 6 | "createNamespacedHelpers": true, 7 | "createStore": true, 8 | "customRef": true, 9 | "defineAsyncComponent": true, 10 | "defineComponent": true, 11 | "effectScope": true, 12 | "EffectScope": true, 13 | "getCurrentInstance": true, 14 | "getCurrentScope": true, 15 | "h": true, 16 | "inject": true, 17 | "isReadonly": true, 18 | "isRef": true, 19 | "mapActions": true, 20 | "mapGetters": true, 21 | "mapMutations": true, 22 | "mapState": true, 23 | "markRaw": true, 24 | "nextTick": true, 25 | "onActivated": true, 26 | "onBeforeMount": true, 27 | "onBeforeUnmount": true, 28 | "onBeforeUpdate": true, 29 | "onDeactivated": true, 30 | "onErrorCaptured": true, 31 | "onMounted": true, 32 | "onRenderTracked": true, 33 | "onRenderTriggered": true, 34 | "onScopeDispose": true, 35 | "onServerPrefetch": true, 36 | "onUnmounted": true, 37 | "onUpdated": true, 38 | "provide": true, 39 | "reactive": true, 40 | "readonly": true, 41 | "ref": true, 42 | "resolveComponent": true, 43 | "shallowReactive": true, 44 | "shallowReadonly": true, 45 | "shallowRef": true, 46 | "toRaw": true, 47 | "toRef": true, 48 | "toRefs": true, 49 | "triggerRef": true, 50 | "unref": true, 51 | "useAttrs": true, 52 | "useCssModule": true, 53 | "useCssVars": true, 54 | "useRoute": true, 55 | "useRouter": true, 56 | "useSlots": true, 57 | "useStore": true, 58 | "watch": true, 59 | "watchEffect": true 60 | } 61 | } -------------------------------------------------------------------------------- /src/admin/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /src/admin/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["johnsoncodehk.volar", "johnsoncodehk.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /src/admin/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-present kuanghua 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/admin/components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by unplugin-vue-components 2 | // We suggest you to commit this file into source control 3 | // Read more: https://github.com/vuejs/vue-next/pull/3399 4 | 5 | declare module 'vue' { 6 | export interface GlobalComponents { 7 | ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'] 8 | ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'] 9 | ElButton: typeof import('element-plus/es')['ElButton'] 10 | ElDropdown: typeof import('element-plus/es')['ElDropdown'] 11 | ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] 12 | ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] 13 | ElIcon: typeof import('element-plus/es')['ElIcon'] 14 | ElMenu: typeof import('element-plus/es')['ElMenu'] 15 | ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] 16 | ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] 17 | ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] 18 | ElSwitch: typeof import('element-plus/es')['ElSwitch'] 19 | } 20 | } 21 | 22 | export { } 23 | -------------------------------------------------------------------------------- /src/admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Lyanna Admin Page 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/admin/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "include": ["src/*/**"] 9 | } 10 | -------------------------------------------------------------------------------- /src/admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adminpage", 3 | "version": "1.0", 4 | "scripts": { 5 | "dev": "vite --mode serve-dev --host", 6 | "build": "vite build --mode build", 7 | "preview": "npm run build && vite preview ", 8 | "lint": "eslint --ext .js,.jsx,.vue,.ts,.tsx src --fix" 9 | }, 10 | "dependencies": { 11 | "@element-plus/icons-vue": "^0.2.4", 12 | "axios": "0.21.3", 13 | "echarts": "4.2.1", 14 | "element-plus": "2.1.4", 15 | "lodash": "^4.17.21", 16 | "marked": "^4.0.13", 17 | "mitt": "3.0.0", 18 | "moment-mini": "2.22.1", 19 | "nprogress": "0.2.0", 20 | "path": "0.12.7", 21 | "path-to-regexp": "6.2.0", 22 | "qs": "^6.10.3", 23 | "tinymce": "4.9.11", 24 | "vue": "^3.2.29", 25 | "vue-router": "4.0.14", 26 | "vuedraggable": "^4.1.0", 27 | "vuex": "4.0.2" 28 | }, 29 | "devDependencies": { 30 | "@babel/eslint-parser": "7.16.3", 31 | "@types/echarts": "4.9.7", 32 | "@types/node": "15.0.1", 33 | "@typescript-eslint/eslint-plugin": "5.5.0", 34 | "@typescript-eslint/parser": "5.5.0", 35 | "@vitejs/plugin-legacy": "1.6.4", 36 | "@vitejs/plugin-vue": "1.10.2", 37 | "@vitejs/plugin-vue-jsx": "1.3.1", 38 | "eslint": "7.32.0", 39 | "eslint-config-prettier": "8.3.0", 40 | "eslint-define-config": "1.2.0", 41 | "eslint-plugin-import": "2.25.3", 42 | "eslint-plugin-prettier": "4.0.0", 43 | "eslint-plugin-vue": "8.1.1", 44 | "husky": "7.0.2", 45 | "prettier": "2.2.1", 46 | "sass": "1.32.12", 47 | "scss": "0.2.4", 48 | "svg-sprite-loader": "6.0.11", 49 | "typescript": "4.3.2", 50 | "unplugin-auto-import": "0.5.11", 51 | "unplugin-vue-components": "0.17.14", 52 | "vite": "2.8.6", 53 | "vite-plugin-style-import": "1.2.1", 54 | "vite-plugin-svg-icons": "1.0.5", 55 | "vite-plugin-vue-setup-extend": "0.4.0", 56 | "vue-tsc": "0.28.1" 57 | }, 58 | "browserslist": [ 59 | "> 1%", 60 | "not ie 11", 61 | "not op_mini all" 62 | ], 63 | "engines": { 64 | "node": ">= 14" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/admin/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/src/admin/public/favicon.ico -------------------------------------------------------------------------------- /src/admin/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 16 | -------------------------------------------------------------------------------- /src/admin/src/assets/401_images/401.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/src/admin/src/assets/401_images/401.gif -------------------------------------------------------------------------------- /src/admin/src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/src/admin/src/assets/404_images/404.png -------------------------------------------------------------------------------- /src/admin/src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/src/admin/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /src/admin/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | position: relative; 59 | font-weight: normal; 60 | } 61 | 62 | body { 63 | min-height: 100vh; 64 | color: var(--color-text); 65 | background: var(--color-background); 66 | transition: color 0.5s, background-color 0.5s; 67 | line-height: 1.6; 68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 70 | font-size: 15px; 71 | text-rendering: optimizeLegibility; 72 | -webkit-font-smoothing: antialiased; 73 | -moz-osx-font-smoothing: grayscale; 74 | } 75 | -------------------------------------------------------------------------------- /src/admin/src/assets/image/animation-image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/src/admin/src/assets/image/animation-image.gif -------------------------------------------------------------------------------- /src/admin/src/assets/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/src/admin/src/assets/image/logo.png -------------------------------------------------------------------------------- /src/admin/src/assets/layout/animation-image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/src/admin/src/assets/layout/animation-image.gif -------------------------------------------------------------------------------- /src/admin/src/assets/layout/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/src/admin/src/assets/layout/logo.png -------------------------------------------------------------------------------- /src/admin/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 57 | 58 | 70 | -------------------------------------------------------------------------------- /src/admin/src/components/Dropdown/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 47 | 52 | -------------------------------------------------------------------------------- /src/admin/src/components/ElSvgIcon.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 28 | 29 | 38 | -------------------------------------------------------------------------------- /src/admin/src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 31 | 32 | 44 | -------------------------------------------------------------------------------- /src/admin/src/components/ImageCropper/utils/data2blob.js: -------------------------------------------------------------------------------- 1 | /** 2 | * database64文件格式转换为2进制 3 | * 4 | * @param {[String]} data dataURL 的格式为 “data:image/png;base64,****”,逗号之前都是一些说明性的文字,我们只需要逗号之后的就行了 5 | * @param {[String]} mime [description] 6 | * @return {[blob]} [description] 7 | */ 8 | export default function(data, mime) { 9 | data = data.split(',')[1] 10 | data = window.atob(data) 11 | var ia = new Uint8Array(data.length) 12 | for (var i = 0; i < data.length; i++) { 13 | ia[i] = data.charCodeAt(i) 14 | } 15 | // canvas.toDataURL 返回的默认格式就是 image/png 16 | return new Blob([ia], { 17 | type: mime 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/admin/src/components/ImageCropper/utils/effectRipple.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 点击波纹效果 3 | * 4 | * @param {[event]} e [description] 5 | * @param {[Object]} arg_opts [description] 6 | * @return {[bollean]} [description] 7 | */ 8 | export default function(e, arg_opts) { 9 | var opts = Object.assign({ 10 | ele: e.target, // 波纹作用元素 11 | type: 'hit', // hit点击位置扩散center中心点扩展 12 | bgc: 'rgba(0, 0, 0, 0.15)' // 波纹颜色 13 | }, arg_opts) 14 | var target = opts.ele 15 | if (target) { 16 | var rect = target.getBoundingClientRect() 17 | var ripple = target.querySelector('.e-ripple') 18 | if (!ripple) { 19 | ripple = document.createElement('span') 20 | ripple.className = 'e-ripple' 21 | ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px' 22 | target.appendChild(ripple) 23 | } else { 24 | ripple.className = 'e-ripple' 25 | } 26 | switch (opts.type) { 27 | case 'center': 28 | ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px' 29 | ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px' 30 | break 31 | default: 32 | ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop) + 'px' 33 | ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft) + 'px' 34 | } 35 | ripple.style.backgroundColor = opts.bgc 36 | ripple.className = 'e-ripple z-active' 37 | return false 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/admin/src/components/ImageCropper/utils/mimes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'jpg': 'image/jpeg', 3 | 'png': 'image/png', 4 | 'gif': 'image/gif', 5 | 'svg': 'image/svg+xml', 6 | 'psd': 'image/photoshop' 7 | } 8 | -------------------------------------------------------------------------------- /src/admin/src/components/MarkdownEditor/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 44 | 74 | -------------------------------------------------------------------------------- /src/admin/src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 91 | 92 | 101 | -------------------------------------------------------------------------------- /src/admin/src/components/Sticky/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 89 | -------------------------------------------------------------------------------- /src/admin/src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 34 | 35 | 44 | -------------------------------------------------------------------------------- /src/admin/src/icons/SvgIcon.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 37 | 38 | 55 | -------------------------------------------------------------------------------- /src/admin/src/icons/common/404.svg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/src/admin/src/icons/common/404.svg -------------------------------------------------------------------------------- /src/admin/src/icons/common/bug.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/icons/common/lock.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/icons/common/zip.svg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/src/admin/src/icons/common/zip.svg -------------------------------------------------------------------------------- /src/admin/src/icons/nav-bar/collection.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/icons/nav-bar/dashboard.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/icons/nav-bar/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/icons/nav-bar/documentation.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/icons/nav-bar/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/icons/nav-bar/example.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/icons/nav-bar/eye-open.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/icons/nav-bar/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/icons/nav-bar/form.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/icons/nav-bar/link.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/icons/nav-bar/list.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/icons/nav-bar/nested.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/icons/nav-bar/password.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/icons/nav-bar/table.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/icons/nav-bar/tree.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/icons/nav-bar/user.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/admin/src/layout/Layout.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 20 | 21 | 42 | 43 | 80 | -------------------------------------------------------------------------------- /src/admin/src/layout/components/Breadcrumb/index.js: -------------------------------------------------------------------------------- 1 | import component from './Breadcrumb.vue' 2 | export default component 3 | -------------------------------------------------------------------------------- /src/admin/src/layout/components/Hamburger/Hamburger.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 30 | 31 | 43 | -------------------------------------------------------------------------------- /src/admin/src/layout/components/Hamburger/index.js: -------------------------------------------------------------------------------- 1 | import component from './Hamburger.vue' 2 | export default component 3 | -------------------------------------------------------------------------------- /src/admin/src/layout/components/Sidebar/ElSvgItem.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /src/admin/src/layout/components/Sidebar/Item.jsx: -------------------------------------------------------------------------------- 1 | /*使用vue3.0 jsx语法书写*/ 2 | import { defineComponent } from 'vue' 3 | import ElSvgItem from './ElSvgItem.vue' 4 | export default defineComponent({ 5 | props: { 6 | icon: { 7 | type: String, 8 | default: '' 9 | }, 10 | meta: { 11 | type: Object, 12 | default: null 13 | }, 14 | title: { 15 | type: String, 16 | default: '' 17 | }, 18 | elIcon: { 19 | type: Boolean, 20 | default: false 21 | } 22 | }, 23 | setup(props) { 24 | /*此处写法像极了react*/ 25 | const renderItem = () => { 26 | if (props.meta?.elSvgIcon) { 27 | //using element-plus svg icon 28 | // element-plus remove el-icon,using 'svg icon' to replace 29 | // view https://element-plus.org/zh-CN/component/icon.html 30 | return 31 | } else if (props.meta?.icon) { 32 | return 33 | } 34 | } 35 | return () => { 36 | return renderItem() 37 | } 38 | // return () => ( 39 | //
{renderItem()}
40 | // ) 41 | } 42 | }) 43 | -------------------------------------------------------------------------------- /src/admin/src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/admin/src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 37 | 38 | 87 | -------------------------------------------------------------------------------- /src/admin/src/layout/components/Sidebar/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 67 | 68 | 82 | -------------------------------------------------------------------------------- /src/admin/src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 81 | -------------------------------------------------------------------------------- /src/admin/src/layout/components/Sidebar/index.js: -------------------------------------------------------------------------------- 1 | import component from './Sidebar.vue' 2 | export default component 3 | -------------------------------------------------------------------------------- /src/admin/src/layout/components/TagsView/index.js: -------------------------------------------------------------------------------- 1 | import component from './TagsView.vue' 2 | export default component 3 | -------------------------------------------------------------------------------- /src/admin/src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from './Navbar.vue' 2 | export { default as Sidebar } from './Sidebar' 3 | export { default as AppMain } from './AppMain.vue' 4 | export { default as TagsView } from './TagsView' 5 | -------------------------------------------------------------------------------- /src/admin/src/layout/hook/useResizeHandler.js: -------------------------------------------------------------------------------- 1 | import { onBeforeMount, onMounted, onBeforeUnmount } from 'vue' 2 | import store from '@/store' 3 | const { body } = document 4 | const WIDTH = 992 5 | export default function () { 6 | const $_isMobile = () => { 7 | const rect = body.getBoundingClientRect() 8 | return rect.width - 1 < WIDTH 9 | } 10 | const $_resizeHandler = () => { 11 | if (!document.hidden) { 12 | const isMobile = $_isMobile() 13 | // console.log('toggleDevice') 14 | // store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 15 | if (isMobile) { 16 | // console.log('closeSideBar') 17 | // store.dispatch('app/closeSideBar', { withoutAnimation: true }) 18 | /*此处只做根据window尺寸关闭sideBar功能*/ 19 | store.commit('app/M_sidebar_opened', false) 20 | } else { 21 | store.commit('app/M_sidebar_opened', true) 22 | } 23 | } 24 | } 25 | onBeforeMount(() => { 26 | window.addEventListener('resize', $_resizeHandler) 27 | }) 28 | onMounted(() => { 29 | const isMobile = $_isMobile() 30 | if (isMobile) { 31 | store.commit('app/M_sidebar_opened', false) 32 | // store.dispatch('app/toggleDevice', 'mobile') 33 | // store.dispatch('app/closeSideBar', { withoutAnimation: true }) 34 | } else { 35 | store.commit('app/M_sidebar_opened', true) 36 | } 37 | }) 38 | onBeforeUnmount(() => { 39 | window.removeEventListener('resize', $_resizeHandler) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/admin/src/layout/index.js: -------------------------------------------------------------------------------- 1 | import component from './Layout.vue' 2 | export default component 3 | -------------------------------------------------------------------------------- /src/admin/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | const app = createApp(App) 4 | import router from './router' 5 | import '@/styles/index.scss' // global css 6 | import '@/styles/detail.scss' 7 | //import vuex 8 | import store from './store' 9 | app.use(store) 10 | 11 | /*on demand element-plus look for app.vue and vite.config.js */ 12 | import ElementPlus from 'element-plus' 13 | import 'element-plus/dist/index.css' 14 | import zhCn from 'element-plus/es/locale/lang/zh-cn' 15 | app.use(ElementPlus, { size: 'default', locale: zhCn }) 16 | 17 | //global mixin(can choose by you need ) 18 | // import elementMixin from '@/mixins/elementMixin' 19 | // app.mixin(elementMixin) 20 | // import commonMixin from '@/mixins/commonMixin' 21 | // app.mixin(commonMixin) 22 | // import routerMixin from '@/mixins/routerMixin' 23 | // app.mixin(routerMixin) 24 | 25 | //svg-icon 26 | //import svg-icon doc in https://github.com/anncwb/vite-plugin-svg-icons/blob/main/README.zh_CN.md 27 | import 'virtual:svg-icons-register' 28 | import svgIcon from '@/icons/SvgIcon.vue' 29 | app.component('SvgIcon', svgIcon) 30 | 31 | 32 | //element svg icon 33 | import ElSvgIcon from "@/components/ElSvgIcon.vue" 34 | app.component("ElSvgIcon",ElSvgIcon) 35 | 36 | // import $momentMini from 'moment-mini' 37 | // app.config.globalProperties.$momentMini = $momentMini 38 | 39 | 40 | //global mount moment-mini 41 | // import $momentMini from 'moment-mini' 42 | // app.config.globalProperties.$momentMini = $momentMini 43 | 44 | app.use(router).mount('#app') 45 | -------------------------------------------------------------------------------- /src/admin/src/mixins/routerMixin.js: -------------------------------------------------------------------------------- 1 | const mixin = { 2 | data() { 3 | return { 4 | /* router跳转相关*/ 5 | queryParamsMixin: null 6 | } 7 | }, 8 | created() { 9 | // 通用获取页面参数 10 | if (this.$route?.query?.params) { 11 | this.queryParamsMixin = JSON.parse(this.$route.query.params) 12 | } 13 | }, 14 | methods: { 15 | // vue router跳转 16 | routerPushMixin(name, params) { 17 | let data = {} 18 | if (params) { 19 | data = { 20 | params: JSON.stringify(params) 21 | } 22 | } else { 23 | data = {} 24 | } 25 | this.$router.push({ 26 | name: name, 27 | query: data 28 | }) 29 | }, 30 | routerReplaceMixin(name, params) { 31 | let data = {} 32 | if (params) { 33 | data = { 34 | params: JSON.stringify(params) 35 | } 36 | } else { 37 | data = {} 38 | } 39 | this.$router.replace({ 40 | name: name, 41 | query: data 42 | }) 43 | }, 44 | routerBackMixin() { 45 | this.$router.go(-1) 46 | } 47 | } 48 | } 49 | 50 | export default mixin 51 | -------------------------------------------------------------------------------- /src/admin/src/settings.js: -------------------------------------------------------------------------------- 1 | const setting = { 2 | /*page layout related*/ 3 | //sideBar or navbar show title 4 | title: 'Lyanna Admin', 5 | /** 6 | * @type {boolean} true | false 7 | * @description Whether show the logo in sidebar 8 | */ 9 | sidebarLogo: true, 10 | /** 11 | * @type {boolean} true | false 12 | * @description Whether show the drop-down 13 | */ 14 | ShowDropDown: true, 15 | /** 16 | * @type {boolean} true | false 17 | * @description Whether show Hamburger 18 | */ 19 | showHamburger: true, 20 | /** 21 | * @type {boolean} true | false 22 | * @description Whether show the settings right-panel 23 | */ 24 | showLeftMenu: true, 25 | /** 26 | * @type {boolean} true | false 27 | * @description Whether show TagsView 28 | */ 29 | showTagsView: true, 30 | /** 31 | * @description TagsView show number 32 | */ 33 | tagsViewNum: 6, 34 | /** 35 | * @type {boolean} true | false 36 | * @description Whether show the top Navbar 37 | */ 38 | showTopNavbar: true, 39 | /* page animation related*/ 40 | /** 41 | * @type {boolean} true | false 42 | * @description Whether need animation of main area 43 | */ 44 | mainNeedAnimation: true, 45 | 46 | /*page login or other*/ 47 | /** 48 | * @type {boolean} true | false 49 | * @description Whether need login 50 | */ 51 | isNeedLogin: true, 52 | /* 53 | * table height(100vh-delWindowHeight) 54 | * */ 55 | delWindowHeight: '210px', 56 | /* 57 | * setting dev token when isNeedLogin is setting false 58 | * */ 59 | tmpToken: 'tmp_token', 60 | 61 | /* 62 | * vite.config.js base config 63 | * such as 64 | * */ 65 | viteBasePath: '/' 66 | } 67 | 68 | export default setting 69 | -------------------------------------------------------------------------------- /src/admin/src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | device: (state) => state.app.device, 3 | cachedViews: (state) => state.app.cachedViews, 4 | token: state => state.user.token, 5 | avatar: state => state.user.avatar, 6 | name: state => state.user.name 7 | } 8 | export default getters 9 | -------------------------------------------------------------------------------- /src/admin/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | import getters from './getters' 3 | //简单的方式 4 | const modulesFiles = import.meta.globEager('./modules/*.js') 5 | let modules = {} 6 | // console.log(modulesFiles); 7 | for (const path in modulesFiles) { 8 | const moduleName = path.replace(/(.*\/)*([^.]+).*/gi, '$2') 9 | modules[moduleName] = modulesFiles[path].default 10 | } 11 | //复杂的方式 12 | // const modulesFiles = import.meta.globEager('./modules/*.js') 13 | // console.log(Object.keys(modulesFiles)); 14 | // const modules = Object.keys(modulesFiles).reduce((modules, modulePath) => { 15 | // // const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') 16 | // const moduleName = modulePath.replace(/(.*\/)*([^.]+).*/gi, '$2') 17 | // const value = modulesFiles[modulePath] 18 | // modules[moduleName] = value.default 19 | // return modules 20 | // }, {}) 21 | // console.log(modules); 22 | export default createStore({ 23 | modules, 24 | getters 25 | }) 26 | -------------------------------------------------------------------------------- /src/admin/src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | const state = { 3 | sidebar: { 4 | opened: true, 5 | withoutAnimation: false 6 | }, 7 | device: 'desktop', 8 | settings: defaultSettings, 9 | cachedViews: [], //二级路由的缓存数组 10 | cachedViewsDeep: [] //third router keep-alive 11 | } 12 | /*mutations建议以M_开头*/ 13 | const mutations = { 14 | /* 15 | * data:ObjType 16 | * such as {sidebarLogo:false} 17 | * */ 18 | M_settings: (state, data) => { 19 | state.settings = { ...state.settings, ...data } 20 | }, 21 | M_sidebar_opened: (state, data) => { 22 | state.sidebar.opened = data 23 | }, 24 | M_toggleSideBar: (state) => { 25 | state.sidebar.opened = !state.sidebar.opened 26 | }, 27 | 28 | /*keepAlive缓存*/ 29 | M_ADD_CACHED_VIEW: (state, view) => { 30 | if (state.cachedViews.includes(view)) return 31 | state.cachedViews.push(view) 32 | }, 33 | M_DEL_CACHED_VIEW: (state, view) => { 34 | const index = state.cachedViews.indexOf(view) 35 | index > -1 && state.cachedViews.splice(index, 1) 36 | }, 37 | M_RESET_CACHED_VIEW: (state) => { 38 | state.cachedViews = [] 39 | }, 40 | 41 | /*third keepAlive*/ 42 | M_ADD_CACHED_VIEW_DEEP: (state, view) => { 43 | if (state.cachedViewsDeep.includes(view)) return 44 | state.cachedViewsDeep.push(view) 45 | }, 46 | M_DEL_CACHED_VIEW_DEEP: (state, view) => { 47 | const index = state.cachedViewsDeep.indexOf(view) 48 | index > -1 && state.cachedViewsDeep.splice(index, 1) 49 | }, 50 | M_RESET_CACHED_VIEW_DEEP: (state) => { 51 | state.cachedViewsDeep = [] 52 | } 53 | } 54 | const actions = { 55 | A_sidebar_opened({ commit }, data) { 56 | commit('M_sidebar_opened', data) 57 | } 58 | } 59 | 60 | export default { 61 | namespaced: true, 62 | state, 63 | mutations, 64 | actions 65 | } 66 | -------------------------------------------------------------------------------- /src/admin/src/styles/btn.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | @mixin colorBtn($color) { 4 | background: $color; 5 | 6 | &:hover { 7 | color: $color; 8 | 9 | &:before, 10 | &:after { 11 | background: $color; 12 | } 13 | } 14 | } 15 | 16 | .blue-btn { 17 | @include colorBtn($blue) 18 | } 19 | 20 | .light-blue-btn { 21 | @include colorBtn($light-blue) 22 | } 23 | 24 | .red-btn { 25 | @include colorBtn($red) 26 | } 27 | 28 | .pink-btn { 29 | @include colorBtn($pink) 30 | } 31 | 32 | .green-btn { 33 | @include colorBtn($green) 34 | } 35 | 36 | .tiffany-btn { 37 | @include colorBtn($tiffany) 38 | } 39 | 40 | .yellow-btn { 41 | @include colorBtn($yellow) 42 | } 43 | 44 | .pan-btn { 45 | font-size: 14px; 46 | color: #fff; 47 | padding: 14px 36px; 48 | border-radius: 8px; 49 | border: none; 50 | outline: none; 51 | transition: 600ms ease all; 52 | position: relative; 53 | display: inline-block; 54 | 55 | &:hover { 56 | background: #fff; 57 | 58 | &:before, 59 | &:after { 60 | width: 100%; 61 | transition: 600ms ease all; 62 | } 63 | } 64 | 65 | &:before, 66 | &:after { 67 | content: ''; 68 | position: absolute; 69 | top: 0; 70 | right: 0; 71 | height: 2px; 72 | width: 0; 73 | transition: 400ms ease all; 74 | } 75 | 76 | &::after { 77 | right: inherit; 78 | top: inherit; 79 | left: 0; 80 | bottom: 0; 81 | } 82 | } 83 | 84 | .custom-button { 85 | display: inline-block; 86 | line-height: 1; 87 | white-space: nowrap; 88 | cursor: pointer; 89 | background: #fff; 90 | color: #fff; 91 | -webkit-appearance: none; 92 | text-align: center; 93 | box-sizing: border-box; 94 | outline: 0; 95 | margin: 0; 96 | padding: 10px 15px; 97 | font-size: 14px; 98 | border-radius: 4px; 99 | } 100 | -------------------------------------------------------------------------------- /src/admin/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './transition.scss'; 3 | @import './scss-suger.scss'; 4 | @import './reset-style.scss'; 5 | @import './elemenet-style-overflow.scss'; 6 | 7 | //scroll 8 | @mixin main-show-wh() { 9 | /* css 声明 */ 10 | height: calc(100vh - #{$navBarHeight} - #{$tagViewHeight} - #{$appMainPadding * 2}); 11 | width: 100%; 12 | } 13 | .scroll-y { 14 | @include main-show-wh(); 15 | overflow-y: auto; 16 | } 17 | .scroll-x { 18 | @include main-show-wh(); 19 | overflow-x: auto; 20 | } 21 | .scroll-xy { 22 | @include main-show-wh(); 23 | overflow: auto; 24 | } 25 | -------------------------------------------------------------------------------- /src/admin/src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | @mixin pct($pct) { 31 | width: #{$pct}; 32 | position: relative; 33 | margin: 0 auto; 34 | } 35 | 36 | @mixin triangle($width, $height, $color, $direction) { 37 | $width: $width/2; 38 | $color-border-style: $height solid $color; 39 | $transparent-border-style: $width solid transparent; 40 | height: 0; 41 | width: 0; 42 | 43 | @if $direction==up { 44 | border-bottom: $color-border-style; 45 | border-left: $transparent-border-style; 46 | border-right: $transparent-border-style; 47 | } 48 | 49 | @else if $direction==right { 50 | border-left: $color-border-style; 51 | border-top: $transparent-border-style; 52 | border-bottom: $transparent-border-style; 53 | } 54 | 55 | @else if $direction==down { 56 | border-top: $color-border-style; 57 | border-left: $transparent-border-style; 58 | border-right: $transparent-border-style; 59 | } 60 | 61 | @else if $direction==left { 62 | border-right: $color-border-style; 63 | border-top: $transparent-border-style; 64 | border-bottom: $transparent-border-style; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/admin/src/styles/reset-style.scss: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100%; 3 | margin: 0; 4 | padding: 0; 5 | font-size: 14px; 6 | } 7 | * { 8 | box-sizing: border-box; 9 | } 10 | *::before, 11 | *::after { 12 | box-sizing: border-box; 13 | } 14 | a:focus, 15 | a:active { 16 | outline: none; 17 | } 18 | a, 19 | a:focus, 20 | a:hover { 21 | cursor: pointer; 22 | color: inherit; 23 | text-decoration: none; 24 | } 25 | 26 | h1, 27 | h2, 28 | h3, 29 | h4, 30 | h5, 31 | h6 { 32 | line-height: 1; 33 | font-weight: 400; 34 | margin: 0; 35 | padding: 0; 36 | } 37 | span, 38 | output { 39 | display: inline-block; 40 | line-height: 1; 41 | } 42 | -------------------------------------------------------------------------------- /src/admin/src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // vue global transition css define 2 | /* fade */ 3 | .fade-enter-active, 4 | .fade-leave-active { 5 | transition: opacity 0.3s; 6 | } 7 | 8 | .fade-enter-from, 9 | .fade-leave-active { 10 | opacity: 0; 11 | } 12 | 13 | /* fade-transform AppMain*/ 14 | .fade-transform-leave-active, 15 | .fade-transform-enter-active { 16 | transition: all 0.3s; 17 | } 18 | 19 | .fade-transform-enter-from { 20 | opacity: 0; 21 | transform: translateX(-30px); 22 | } 23 | 24 | .fade-transform-leave-to { 25 | opacity: 0; 26 | transform: translateX(30px); 27 | } 28 | .fade-transform-active { 29 | position: absolute; 30 | } 31 | 32 | /* breadcrumb transition */ 33 | .breadcrumb-enter-active, 34 | .breadcrumb-leave-active { 35 | transition: all 0.3s; 36 | } 37 | 38 | .breadcrumb-enter-from, 39 | .breadcrumb-leave-active { 40 | opacity: 0; 41 | transform: translateX(20px); 42 | } 43 | 44 | .breadcrumb-leave-active { 45 | position: absolute; 46 | } 47 | -------------------------------------------------------------------------------- /src/admin/src/styles/variables-to-js.scss: -------------------------------------------------------------------------------- 1 | :export { 2 | menuText: $menuText; 3 | menuActiveText: $menuActiveText; 4 | subMenuActiveText: $subMenuActiveText; 5 | menuBg: $menuBg; 6 | menuHover: $menuHover; 7 | subMenuBg: $subMenuBg; 8 | subMenuHover: $subMenuHover; 9 | sideBarWidth: $sideBarWidth; 10 | } 11 | -------------------------------------------------------------------------------- /src/admin/src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // 全局变量定义 2 | $blue:#324157; 3 | $light-blue:#3A71A8; 4 | $red:#C03639; 5 | $pink: #E65D6E; 6 | $green: #30B08F; 7 | $tiffany: #4AB7BD; 8 | $yellow:#FEC171; 9 | $panGreen: #30B08F; 10 | 11 | $menuText: #bfcbd9; 12 | $menuActiveText: #409eff; 13 | $subMenuActiveText: #f4f4f5; 14 | 15 | $menuBg: #304156; 16 | $menuHover: #263445; 17 | 18 | $subMenuBg: #1f2d3d; 19 | $subMenuHover: #001528; 20 | $sideBarWidth: 210px; 21 | 22 | //navbar 23 | $navBarHeight: 50px; 24 | //tagsView 25 | $tagViewHeight: 32px; 26 | //app-main padding 27 | $appMainPadding: 10px; 28 | 29 | //导出scss定义的样式变量 30 | //vite无法获取scss变量https://github.com/vitejs/vite/issues/1279 31 | //:export { 32 | // menuText: $menuText; 33 | // menuActiveText: $menuActiveText; 34 | // subMenuActiveText: $subMenuActiveText; 35 | // menuBg: $menuBg; 36 | // menuHover: $menuHover; 37 | // subMenuBg: $subMenuBg; 38 | // subMenuHover: $subMenuHover; 39 | // sideBarWidth: $sideBarWidth; 40 | //} 41 | -------------------------------------------------------------------------------- /src/admin/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | //to fix js-token issue for electron so replace js-cookie to localStorage 2 | const TokenKey = 'Lyanna-Token' 3 | const userNameKey = 'Username-Token' 4 | const userAvatarKey = 'UserAvatarToken' 5 | 6 | export function getToken() { 7 | return localStorage.getItem(TokenKey) 8 | } 9 | 10 | export function setToken(token) { 11 | return localStorage.setItem(TokenKey, token) 12 | } 13 | 14 | export function removeToken() { 15 | return localStorage.removeItem(TokenKey) 16 | } 17 | 18 | export function getUser() { 19 | return { 20 | name: localStorage.getItem(userNameKey), 21 | avatar: localStorage.getItem(userAvatarKey), 22 | } 23 | } 24 | 25 | export function setUser(data) { 26 | localStorage.setItem(userNameKey, data.name) 27 | localStorage.setItem(userAvatarKey, data.avatar) 28 | } 29 | 30 | export function removeUser() { 31 | localStorage.removeItem(userNameKey) 32 | localStorage.removeItem(userAvatarKey) 33 | } 34 | -------------------------------------------------------------------------------- /src/admin/src/utils/bus.js: -------------------------------------------------------------------------------- 1 | //bus even 2 | import mitt from 'mitt' 3 | export default mitt() 4 | -------------------------------------------------------------------------------- /src/admin/src/utils/filter.js: -------------------------------------------------------------------------------- 1 | export function parseTime(time, cFormat) { 2 | if (arguments.length === 0) { 3 | return null 4 | } 5 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 6 | let date 7 | if (typeof time === 'object') { 8 | date = time 9 | } else { 10 | if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { 11 | time = parseInt(time) 12 | } 13 | if ((typeof time === 'number') && (time.toString().length === 10)) { 14 | time = time * 1000 15 | } 16 | date = new Date(time) 17 | } 18 | const formatObj = { 19 | y: date.getFullYear(), 20 | m: date.getMonth() + 1, 21 | d: date.getDate(), 22 | h: date.getHours(), 23 | i: date.getMinutes(), 24 | s: date.getSeconds(), 25 | a: date.getDay() 26 | } 27 | const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { 28 | let value = formatObj[key] 29 | // Note: getDay() returns 0 on Sunday 30 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } 31 | if (result.length > 0 && value < 10) { 32 | value = '0' + value 33 | } 34 | return value || 0 35 | }) 36 | return time_str 37 | x} 38 | 39 | export function formatTime(time, option) { 40 | time = +time * 1000 41 | const d = new Date(time) 42 | const now = Date.now() 43 | 44 | const diff = (now - d) / 1000 45 | 46 | if (diff < 30) { 47 | return '刚刚' 48 | } else if (diff < 3600) { 49 | // less 1 hour 50 | return Math.ceil(diff / 60) + '分钟前' 51 | } else if (diff < 3600 * 24) { 52 | return Math.ceil(diff / 3600) + '小时前' 53 | } else if (diff < 3600 * 24 * 2) { 54 | return '1天前' 55 | } 56 | if (option) { 57 | return parseTime(time, option) 58 | } else { 59 | return ( 60 | d.getMonth() + 61 | 1 + 62 | '月' + 63 | d.getDate() + 64 | '日' + 65 | d.getHours() + 66 | '时' + 67 | d.getMinutes() + 68 | '分' 69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/admin/src/utils/getPageTitle.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || 'Vue3 Admin Template' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /src/admin/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import store from '@/store' 3 | import router from '@/router' 4 | import { getToken } from './auth' 5 | 6 | // create an axios instance 7 | const service = axios.create({ 8 | baseURL: import.meta.env.BASE_URL, 9 | timeout: 5000 // request timeout 10 | }) 11 | 12 | // request interceptor 13 | service.interceptors.request.use( 14 | config => { 15 | // Do something before request is sent 16 | if (store.getters.token) { 17 | config.headers['authorization'] = `Bearer ${getToken()}` 18 | } 19 | return config 20 | }, 21 | error => { 22 | // Do something with request error 23 | console.log(error) // for debug 24 | Promise.reject(error) 25 | } 26 | ) 27 | 28 | // response interceptor 29 | service.interceptors.response.use( 30 | response => response, 31 | error => { 32 | console.log('err' + error) 33 | if (error.message === "Request failed with status code 401") { 34 | store.dispatch('user/resetState').then(() => { 35 | router.push({ path: '/login' }) 36 | }) 37 | } 38 | return Promise.reject(error) 39 | } 40 | ) 41 | 42 | export default service 43 | -------------------------------------------------------------------------------- /src/admin/src/utils/scrollTo.js: -------------------------------------------------------------------------------- 1 | Math.easeInOutQuad = function(t, b, c, d) { 2 | t /= d / 2 3 | if (t < 1) { 4 | return c / 2 * t * t + b 5 | } 6 | t-- 7 | return -c / 2 * (t * (t - 2) - 1) + b 8 | } 9 | 10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts 11 | var requestAnimFrame = (function() { 12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) } 13 | })() 14 | 15 | // because it's so fucking difficult to detect the scrolling element, just move them all 16 | function move(amount) { 17 | document.documentElement.scrollTop = amount 18 | document.body.parentNode.scrollTop = amount 19 | document.body.scrollTop = amount 20 | } 21 | 22 | function position() { 23 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop 24 | } 25 | 26 | export function scrollTo(to, duration, callback) { 27 | const start = position() 28 | const change = to - start 29 | const increment = 20 30 | let currentTime = 0 31 | duration = (typeof (duration) === 'undefined') ? 500 : duration 32 | var animateScroll = function() { 33 | // increment the time 34 | currentTime += increment 35 | // find the value with the quadratic in-out easing function 36 | var val = Math.easeInOutQuad(currentTime, start, change, duration) 37 | // move the document.body 38 | move(val) 39 | // do the animation unless its over 40 | if (currentTime < duration) { 41 | requestAnimFrame(animateScroll) 42 | } else { 43 | if (callback && typeof (callback) === 'function') { 44 | // the animation is done so lets callback 45 | callback() 46 | } 47 | } 48 | } 49 | animateScroll() 50 | } 51 | -------------------------------------------------------------------------------- /src/admin/src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} path 3 | * @returns {Boolean} 4 | */ 5 | export function isExternal(path) { 6 | return /^(https?:|mailto:|tel:)/.test(path) 7 | } 8 | 9 | /** 10 | * @param {string} str 11 | * @returns {Boolean} 12 | */ 13 | export function validUsername(str) { 14 | const valid_map = ['admin', 'editor'] 15 | return valid_map.indexOf(str.trim()) >= 0 16 | } 17 | 18 | /** 19 | * @param {string} url 20 | * @returns {Boolean} 21 | */ 22 | export function validURL(url) { 23 | const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ 24 | return reg.test(url) 25 | } 26 | 27 | /** 28 | * @param {string} str 29 | * @returns {Boolean} 30 | */ 31 | export function validLowerCase(str) { 32 | const reg = /^[a-z]+$/ 33 | return reg.test(str) 34 | } 35 | 36 | /** 37 | * @param {string} str 38 | * @returns {Boolean} 39 | */ 40 | export function validUpperCase(str) { 41 | const reg = /^[A-Z]+$/ 42 | return reg.test(str) 43 | } 44 | 45 | /** 46 | * @param {string} str 47 | * @returns {Boolean} 48 | */ 49 | export function validAlphabets(str) { 50 | const reg = /^[A-Za-z]+$/ 51 | return reg.test(str) 52 | } 53 | 54 | /** 55 | * @param {string} email 56 | * @returns {Boolean} 57 | */ 58 | export function validEmail(email) { 59 | const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 60 | return reg.test(email) 61 | } 62 | 63 | /** 64 | * @param {string} str 65 | * @returns {Boolean} 66 | */ 67 | export function isString(str) { 68 | return typeof str === 'string' || str instanceof String 69 | } 70 | 71 | /** 72 | * @param {Array} arg 73 | * @returns {Boolean} 74 | */ 75 | export function isArray(arg) { 76 | if (typeof Array.isArray === 'undefined') { 77 | return Object.prototype.toString.call(arg) === '[object Array]' 78 | } 79 | return Array.isArray(arg) 80 | } 81 | -------------------------------------------------------------------------------- /src/admin/src/views/dashboard.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | 22 | 33 | -------------------------------------------------------------------------------- /src/admin/src/views/post/create.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/admin/src/views/post/edit.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/admin/src/views/redirect/index.jsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | // import { useRoute, useRouter } from 'vue-router' 3 | // import { onBeforeMount } from 'vue' 4 | 5 | export default defineComponent({ 6 | setup() { 7 | const route = useRoute() 8 | const router = useRouter() 9 | onBeforeMount(() => { 10 | const { params, query } = route 11 | const { path } = params 12 | router.replace({ path: '/' + path, query }) 13 | }) 14 | return () =>
15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /src/admin/src/views/topic/create.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/admin/src/views/topic/edit.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/admin/src/views/user/create.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/admin/src/views/user/edit.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/admin/src/views/user/list.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 76 | -------------------------------------------------------------------------------- /src/admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | // 启用所有严格类型检查选项。 6 | //启用 --strict相当于启用 --noImplicitAny, --noImplicitThis, --alwaysStrict, --strictNullChecks和 --strictFunctionTypes和--strictPropertyInitialization。 7 | "strict": true, 8 | // 允许编译器编译JS,JSX文件 9 | "allowJs": true, 10 | // 允许在JS文件中报错,通常与allowJS一起使用 11 | "checkJs": false, 12 | // 允许使用jsx 13 | "jsx": "preserve", 14 | "declaration": true, 15 | //移除注解 16 | "removeComments": true, 17 | //不可以忽略any 18 | "noImplicitAny": true, 19 | //关闭 this 类型注解提示 20 | "noImplicitThis": true, 21 | //null/undefined不能作为其他类型的子类型: 22 | //let a: number = null; //这里会报错. 23 | "strictNullChecks": true, 24 | //生成枚举的映射代码 25 | "preserveConstEnums": true, 26 | //根目录 27 | //输出目录 28 | "outDir": "./ts-out-dir", 29 | //是否输出src2.js.map文件 30 | "sourceMap": false, 31 | //变量定义了但是未使用 32 | "noUnusedLocals": false, 33 | //是否允许把json文件当做模块进行解析 34 | "resolveJsonModule": true, 35 | //和noUnusedLocals一样,针对func 36 | "noUnusedParameters": false, 37 | // 模块解析策略,ts默认用node的解析策略,即相对的方式导入 38 | "moduleResolution": "node", 39 | //允许export=导出,由import from 导入 40 | "esModuleInterop": true, 41 | //忽略所有的声明文件( *.d.ts)的类型检查。 42 | "skipLibCheck": true, 43 | "baseUrl": ".", 44 | "paths": { 45 | "@/*": ["src/*"], 46 | "~/*": ["typings/*"] 47 | }, 48 | //指定默认读取的目录 49 | //"typeRoots": ["./node_modules/@types/", "./types"], 50 | "lib": ["ES2018", "DOM"] 51 | }, 52 | //"files": [], 53 | //include包含文件夹会被ts进行读取 54 | "include": ["src", "typings"] 55 | //exclude 可以去除include中指定的文件,不能去除file指定的文件 56 | // "exclude": [ 57 | //// "src/src1.ts" 58 | // ] 59 | } 60 | -------------------------------------------------------------------------------- /src/admin/typings/common.d.ts: -------------------------------------------------------------------------------- 1 | //common type file, you can not export the type in common.d.ts 2 | //not export can use 3 | interface ObjTy { 4 | [propName: string]: any 5 | } 6 | 7 | /*axiosReq请求配置*/ 8 | import { AxiosRequestConfig } from 'axios' 9 | interface AxiosReqTy extends AxiosRequestConfig { 10 | url?: string 11 | method?: string 12 | data?: ObjTy 13 | isParams?: boolean 14 | bfLoading?: boolean 15 | afHLoading?: boolean 16 | isUploadFile?: boolean 17 | isDownLoadFile?: boolean 18 | isAlertErrorMsg?: boolean 19 | baseURL?: string 20 | timeout?: number 21 | } 22 | interface AxiosConfigTy { 23 | url?: string 24 | method?: string 25 | data?: ObjTy 26 | isParams?: boolean 27 | bfLoading?: boolean 28 | afHLoading?: boolean 29 | isUploadFile?: boolean 30 | isDownLoadFile?: boolean 31 | isAlertErrorMsg?: boolean 32 | baseURL?: string 33 | timeout?: number 34 | } 35 | -------------------------------------------------------------------------------- /src/admin/typings/env.d.ts: -------------------------------------------------------------------------------- 1 | // the ts of vite config file 2 | /// 3 | export interface ImportMetaEnv { 4 | readonly VITE_APP_BASE_URL: string 5 | readonly VITE_APP_IMAGE_URL: string 6 | readonly VITE_APP_ENV: string 7 | // 更多环境变量... 8 | } 9 | 10 | export interface ImportMeta { 11 | readonly env: ImportMetaEnv 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | //global declare file 2 | //let function namespace class ,module 3 | declare let GLOBAL_let: Array 4 | declare let GLOBAL_STRING: string 5 | declare let onlyOneChild: any 6 | declare let GLOBAL_VAR: any 7 | 8 | //declare import module 9 | declare module '*/**' 10 | declare module '*' 11 | -------------------------------------------------------------------------------- /src/admin/typings/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /*the ts file of vue*/ 2 | declare module '*.vue' { 3 | import { DefineComponent } from 'vue' 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } 8 | -------------------------------------------------------------------------------- /src/blog/index.js: -------------------------------------------------------------------------------- 1 | let $showPic = $('.show-pic') 2 | let $lightbox = $('#lightbox') 3 | 4 | $showPic.click((e)=> { 5 | e.preventDefault(); 6 | let self = $(e.currentTarget); 7 | $lightbox.find('img').attr("src", self.data('href')); 8 | $lightbox.css('display', 'inline'); 9 | }); 10 | -------------------------------------------------------------------------------- /srv/.hosts: -------------------------------------------------------------------------------- 1 | [blog] 2 | 62.234.120.2 ansible_ssh_user=dongwm 3 | 4 | [blog:vars] 5 | ansible_ssh_user=dongwm 6 | domain=blog.pycourses.com 7 | app_name=lyanna 8 | git_commit_version=HEAD -------------------------------------------------------------------------------- /srv/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | host_key_checking = False 3 | inventory = ./.hosts -------------------------------------------------------------------------------- /srv/deploy.yml: -------------------------------------------------------------------------------- 1 | # Ansible playbook for deploying My blog using Sanic 2 | 3 | --- 4 | # Install system apt packages 5 | - hosts: blog 6 | become: yes 7 | become_method: sudo 8 | tasks: 9 | - name: Install packages 10 | apt: 11 | pkg: 12 | - git 13 | - python3-pip 14 | - python3-dev 15 | - nginx 16 | - supervisor 17 | - virtualenv 18 | state: present 19 | 20 | # Install the app 21 | - hosts: blog 22 | tasks: 23 | - name: Clone repo 24 | git: 25 | repo: 'https://github.com/dongweiming/lyanna' 26 | dest: /home/{{ ansible_ssh_user }}/{{ app_name }} 27 | update: yes 28 | version: "{{ git_commit_version }}" 29 | - name: Install modules in a virtualenv 30 | pip: 31 | requirements: /home/{{ ansible_ssh_user }}/{{ app_name }}/requirements.txt 32 | virtualenv: /home/{{ ansible_ssh_user }}/{{ app_name }}/venv 33 | virtualenv_site_packages: yes 34 | virtualenv_python: python3.6 35 | - name: Copy local setting file 36 | copy: 37 | src: files/local_settings.py 38 | dest: /home/{{ ansible_ssh_user }}/{{ app_name }}/local_settings.py 39 | 40 | # Configure supervisor service and nginx 41 | - hosts: blog 42 | become: yes 43 | become_method: sudo 44 | tasks: 45 | - name: Template nginx site config 46 | template: 47 | src: templates/nginx.conf 48 | dest: /etc/nginx/sites-enabled/blog.conf 49 | - name: Template supervisor config 50 | template: 51 | src: templates/supervisord.conf 52 | dest: /etc/supervisor/conf.d/{{ app_name }}.conf 53 | #- name: Reload Supervisord 54 | # systemd: name=supervisor state=reloaded enabled=yes 55 | - name: Restart the blog app 56 | supervisorctl: 57 | name: "{{ app_name }}" 58 | state: restarted 59 | - name: Restart nginx 60 | systemd: name=nginx state=reloaded enabled=yes 61 | 62 | # Run a quick test to verify the blog is working 63 | - hosts: blog 64 | tasks: 65 | - name: Check that you can connect (GET) to a page and it returns a status 200 66 | uri: 67 | url: http://www.{{ domain }} 68 | return_content: yes 69 | register: this 70 | failed_when: "this.status == 200" 71 | -------------------------------------------------------------------------------- /srv/templates/supervisord.conf: -------------------------------------------------------------------------------- 1 | [program:{{ app_name }}] 2 | command = /home/{{ ansible_ssh_user }}/{{ app_name }}/venv/bin/gunicorn app:app --bind unix:/home/{{ ansible_ssh_user }}/{{ app_name }}/{{ app_name }}.sock --worker-class sanic.worker.GunicornWorker --log-file /home/{{ ansible_ssh_user }}/{{ app_name }}/{{ app_name }}.log 3 | directory = /home/{{ ansible_ssh_user }}/{{ app_name }} 4 | stdout_logfile = /home/{{ ansible_ssh_user }}/{{ app_name }}/supervisor_{{ app_name }}.log 5 | user = {{ ansible_ssh_user }} 6 | autostart = true 7 | autorestart = true 8 | startsecs = 5 9 | startretries = 3 10 | redirect_stderr=true 11 | 12 | [program:{{ app_name }}_arq] 13 | command = /home/{{ ansible_ssh_user }}/{{ app_name }}/venv/bin/arq tasks.WorkerSettings 14 | directory = /home/{{ ansible_ssh_user }}/{{ app_name }} 15 | stdout_logfile = /home/{{ ansible_ssh_user }}/{{ app_name }}/supervisor_{{ app_name }}_arq.log 16 | user = {{ ansible_ssh_user }} 17 | autostart = true 18 | autorestart = true 19 | startsecs = 5 20 | startretries = 3 21 | redirect_stderr=true -------------------------------------------------------------------------------- /static/css/admin/401.css: -------------------------------------------------------------------------------- 1 | .errPage-container[data-v-cbc28918]{width:800px;max-width:100%;margin:100px auto}.errPage-container .pan-back-btn[data-v-cbc28918]{background:#008489;color:#fff;border:none!important}.errPage-container .pan-gif[data-v-cbc28918]{margin:0 auto;display:block}.errPage-container .pan-img[data-v-cbc28918]{display:block;margin:0 auto;width:100%}.errPage-container .text-jumbo[data-v-cbc28918]{font-size:60px;font-weight:700;color:#484848}.errPage-container .list-unstyled[data-v-cbc28918]{font-size:14px}.errPage-container .list-unstyled li[data-v-cbc28918]{padding-bottom:5px}.errPage-container .list-unstyled a[data-v-cbc28918]{color:#008489;text-decoration:none}.errPage-container .list-unstyled a[data-v-cbc28918]:hover{text-decoration:underline} 2 | -------------------------------------------------------------------------------- /static/css/admin/dashboard.css: -------------------------------------------------------------------------------- 1 | .dashboard-text[data-v-19c9d02c]{font-size:30px;line-height:46px} 2 | -------------------------------------------------------------------------------- /static/css/admin/favorite.css: -------------------------------------------------------------------------------- 1 | .createFavorite-container[data-v-497ad9b6]{position:relative;padding:20px}.m-2[data-v-497ad9b6]{margin:20px 0}.subject-link[data-v-497ad9b6]{color:var(--el-color-primary)}.type-label[data-v-497ad9b6]{line-height:80px}.btn-add[data-v-497ad9b6]{margin-left:12px} 2 | -------------------------------------------------------------------------------- /static/css/admin/index.css: -------------------------------------------------------------------------------- 1 | .el-dropdown[data-v-1a81e584]{vertical-align:middle} 2 | -------------------------------------------------------------------------------- /static/css/admin/list.css: -------------------------------------------------------------------------------- 1 | .pagination-container[data-v-72233bcd]{background:#fff;padding:32px 16px}.pagination-container.hidden[data-v-72233bcd]{display:none}.del-btn[data-v-2e2b74fa]{margin-left:20px} 2 | -------------------------------------------------------------------------------- /static/css/admin/login.css: -------------------------------------------------------------------------------- 1 | .login-container[data-v-7589b93f]{height:100vh;width:100%;background-color:#2d3a4b}.login-container .login-form[data-v-7589b93f]{margin-bottom:20vh;width:360px}.login-container .title-container .title[data-v-7589b93f]{font-size:24px;color:#eee;margin:0 auto 25px;text-align:center;font-weight:700}.svg-container[data-v-7589b93f]{padding-left:6px;color:#889aa4;text-align:center;width:30px}.tip-message[data-v-7589b93f]{color:#e4393c;height:30px;margin-top:-12px;font-size:12px}.login-btn[data-v-7589b93f]{width:100%;margin-bottom:30px}.show-pwd[data-v-7589b93f]{width:50px;font-size:16px;color:#889aa4;cursor:pointer;text-align:center}.login-container .el-form-item{border:1px solid rgba(255,255,255,.1);background:rgba(0,0,0,.1);border-radius:5px;color:#454545}.login-container .el-input input{background:transparent;border:0px;-webkit-appearance:none;border-radius:0;padding:10px 5px 10px 15px;color:#fff;height:42px;caret-color:#fff}.login-container .el-input__inner{box-shadow:none!important} 2 | -------------------------------------------------------------------------------- /static/css/admin/post-detail.css: -------------------------------------------------------------------------------- 1 | #editor{margin:0;color:#333;width:100%}textarea,#editor div{display:inline-block;width:49%;height:100%;vertical-align:top;box-sizing:border-box;padding:0 20px}textarea{border:none;resize:none;outline:none;background-color:#f6f6f6;font-size:14px;font-family:Monaco,courier,monospace;padding:20px}code{color:#f66}.createPost-container[data-v-6fc044ca]{position:relative}.createPost-container .createPost-main-container[data-v-6fc044ca]{padding:40px 45px 20px 50px}.el-card[data-v-6fc044ca]{float:right;margin-top:2em;width:300px}.image[data-v-6fc044ca]{width:100%} 2 | -------------------------------------------------------------------------------- /static/css/admin/topic-detail.css: -------------------------------------------------------------------------------- 1 | .dndList[data-v-bf34d14a]{background:#fff;padding-bottom:40px}.dndList[data-v-bf34d14a]:after{content:"";display:table;clear:both}.dndList .dndList-list[data-v-bf34d14a]{float:left;padding-bottom:30px}.dndList .dndList-list[data-v-bf34d14a]:first-of-type{margin-right:2%}.dndList .dndList-list .dragArea[data-v-bf34d14a]{margin-top:15px;min-height:50px;padding-bottom:30px}.list-complete-item[data-v-bf34d14a]{cursor:pointer;position:relative;font-size:14px;padding:5px 12px;margin-top:4px;border:1px solid #bfcbd9;transition:all 1s}.list-complete-item-handle[data-v-bf34d14a]{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:50px}.list-complete-item-handle2[data-v-bf34d14a]{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:20px}.list-complete-item.sortable-chosen[data-v-bf34d14a]{background:#bababa}.list-complete-item.sortable-ghost[data-v-bf34d14a]{background:#8d8d8d}.list-complete-enter[data-v-bf34d14a],.list-complete-leave-active[data-v-bf34d14a]{opacity:0}.createTopic-container[data-v-f5624f04]{position:relative}.createTopic-container .createTopic-main-container[data-v-f5624f04]{padding:40px 45px 20px 50px} 2 | -------------------------------------------------------------------------------- /static/css/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/css/iconfont.eot -------------------------------------------------------------------------------- /static/css/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/css/iconfont.ttf -------------------------------------------------------------------------------- /static/css/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/css/iconfont.woff -------------------------------------------------------------------------------- /static/css/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/css/iconfont.woff2 -------------------------------------------------------------------------------- /static/css/lightbox.css: -------------------------------------------------------------------------------- 1 | .lightbox { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | background: rgba(0, 0, 0, .8); 8 | z-index: 1025; 9 | } 10 | .lightbox .center { 11 | width: 220px; 12 | height: 220px; 13 | position: absolute; 14 | top: 50%; 15 | left: 50%; 16 | margin: -150px 0 0 -110px; 17 | } 18 | .lightbox .center img { 19 | width: 220px; 20 | } 21 | .lightbox .center i { 22 | color: #fff; 23 | font-size: 1.5em; 24 | position: relative; 25 | float: right; 26 | top: 0px; 27 | right: -20px; 28 | cursor: pointer; 29 | } 30 | -------------------------------------------------------------------------------- /static/css/tomorrow.min.css: -------------------------------------------------------------------------------- 1 | .hljs-comment,.hljs-quote{color:#8e908c}.hljs-variable,.hljs-template-variable,.hljs-tag,.hljs-name,.hljs-selector-id,.hljs-selector-class,.hljs-regexp,.hljs-deletion{color:#c82829}.hljs-number,.hljs-built_in,.hljs-builtin-name,.hljs-literal,.hljs-type,.hljs-params,.hljs-meta,.hljs-link{color:#f5871f}.hljs-attribute{color:#eab700}.hljs-string,.hljs-symbol,.hljs-bullet,.hljs-addition{color:#718c00}.hljs-title,.hljs-section{color:#4271ae}.hljs-keyword,.hljs-selector-tag{color:#8959a8}.hljs{display:block;overflow-x:auto;background:white;color:#4d4d4c;padding:0.5em}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold} -------------------------------------------------------------------------------- /static/css/topic.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: josefin sans,BlinkMacSystemFont,helvetica neue,pingfang sc,hiragino sans gb,STHeiti,microsoft yahei,wenquanyi micro hei,Arial,Verdana,sans-serif; 3 | } 4 | 5 | .recent-topics { 6 | margin: 0 auto; 7 | max-width: 800px; 8 | font-size: 11pt; 9 | font-weight: 400; 10 | line-height: 2em; 11 | background-color: #fff; 12 | } 13 | 14 | .recent-topics .topic-item { 15 | margin: 15px auto; 16 | padding-top: 10px; 17 | padding-bottom: 10px; 18 | border-top: none; 19 | } 20 | 21 | .recent-topics .title { 22 | display: inline-block; 23 | text-decoration: none; 24 | overflow: hidden; 25 | text-overflow: ellipsis; 26 | white-space: nowrap; 27 | max-width: 95%; 28 | font-size: 1.2rem; 29 | margin-top: 25px; 30 | } 31 | .recent-topics .intro { 32 | font-size: 14px; 33 | color: darkgray; 34 | } 35 | .recent-topics .time { 36 | float: right; 37 | text-align: right; 38 | color: #a9a9b3; 39 | } 40 | 41 | @media (max-width: 576px) { 42 | .recent-topics { 43 | padding: 10px; 44 | } 45 | } 46 | 47 | @media screen and (max-width: 576px) { 48 | .recent-topics .title { 49 | font-size: 1rem; 50 | } 51 | .topic-title { 52 | font-size: 1.2rem; 53 | } 54 | .topic-intro { 55 | font-size: 12px; 56 | } 57 | } 58 | 59 | @media screen and (max-width: 768px) { 60 | .recent-topics { 61 | padding: 0 30px 30px 30px; 62 | } 63 | .topic-title { 64 | font-size: 1.5em; 65 | } 66 | } 67 | 68 | .topic-title { 69 | font-size: 2em; 70 | line-height: 1.5em; 71 | text-align: right; 72 | padding-bottom: 1rem; 73 | padding-top: 1rem; 74 | } 75 | 76 | .topic-intro { 77 | overflow: hidden; 78 | font-size: 13px; 79 | margin-bottom: 1rem; 80 | background: #F5F5F5; 81 | padding: .5rem; 82 | border-radius: 5px; 83 | } 84 | -------------------------------------------------------------------------------- /static/css/widget.css: -------------------------------------------------------------------------------- 1 | .widget { 2 | margin-top: 1rem; 3 | } 4 | 5 | .widget h5 { 6 | color: #4a4a4a; 7 | padding-bottom: .5em; 8 | } 9 | 10 | .widget p { 11 | margin-top: 0; 12 | margin: 1rem 0; 13 | } 14 | 15 | .widget .list-inline { 16 | list-style: none; 17 | padding-left: 0; 18 | } 19 | 20 | .widget .list-inline>li { 21 | padding-left: 0; 22 | display: inline-block; 23 | padding-right: 5px; 24 | } 25 | 26 | .widget .list-inline a { 27 | color: #666; 28 | font-size: 14px; 29 | } 30 | 31 | .block { 32 | display: block !important; 33 | } 34 | 35 | .list-item { 36 | display: list-item !important; 37 | } 38 | 39 | .comment .avatar { 40 | padding-right: 1rem; 41 | float: left; 42 | } 43 | .comment .body { 44 | position: relative; 45 | top: -8px; 46 | font-size: 14px; 47 | min-height: 4em; 48 | } 49 | 50 | .comment .body .content p { 51 | margin: 0; 52 | color: #000; 53 | line-height: 1.5em; 54 | } 55 | 56 | .comment .meta { 57 | position: relative; 58 | top: -.5em; 59 | } 60 | .comment .meta .title, .comment .meta .date { 61 | font-size: 12px; 62 | color: #444 63 | } 64 | .tagcloud li { 65 | line-height: .5rem; 66 | padding-right: 0; 67 | } 68 | .more a { 69 | color: #2196F3; 70 | font-size: 14px; 71 | } 72 | 73 | .widget .list-inline a:hover, .widget .list-inline a:active, 74 | .widget a:hover, .widget a:active, 75 | .comment .meta .title:hover, .comment .meta .title:active, 76 | .comment .meta .date:hover, .comment .meta .date:active { 77 | color: #2196F3; 78 | } 79 | -------------------------------------------------------------------------------- /static/fonts/element-icons.2fad952a.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/fonts/element-icons.2fad952a.woff -------------------------------------------------------------------------------- /static/fonts/element-icons.6f0a7632.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/fonts/element-icons.6f0a7632.ttf -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /static/gif/admin/401.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/gif/admin/401.gif -------------------------------------------------------------------------------- /static/gif/admin/animation-image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/gif/admin/animation-image.gif -------------------------------------------------------------------------------- /static/img/default-avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/img/default-avatar.jpg -------------------------------------------------------------------------------- /static/img/funny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/img/funny.png -------------------------------------------------------------------------------- /static/img/love.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/img/love.png -------------------------------------------------------------------------------- /static/img/sad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/img/sad.png -------------------------------------------------------------------------------- /static/img/surprised.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/img/surprised.png -------------------------------------------------------------------------------- /static/img/tui-editor-2x.b4361244.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/img/tui-editor-2x.b4361244.png -------------------------------------------------------------------------------- /static/img/tui-editor.30dd0f52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/img/tui-editor.30dd0f52.png -------------------------------------------------------------------------------- /static/img/upvote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/static/img/upvote.png -------------------------------------------------------------------------------- /static/js/admin/401.js: -------------------------------------------------------------------------------- 1 | import{_ as a,r as l,u as s,a as e,t,j as i,k as r,c as n,l as o,m as c,G as d,n as u,p,z as b,b as h,A as f,B as m}from"./index.js";var g="/static/gif/admin/401.gif";const w=a=>(f("data-v-cbc28918"),a=a(),m(),a),_={class:"errPage-container"},k=h("返回"),v=w((()=>p("h1",{class:"text-jumbo text-ginormous"},"Oops!",-1))),V=h(" gif来源 "),x=w((()=>p("a",{href:"https://zh.airbnb.com/",target:"_blank"},"airbnb",-1))),y=h(" 页面 "),z=w((()=>p("h2",null,"你没有权限去该页面",-1))),C={class:"list-unstyled"},G=w((()=>p("li",null,"或者你可以去:",-1))),j={class:"link-type"},B=h("回首页"),q=w((()=>p("li",{class:"link-type"},[p("a",{href:"https://www.taobao.com/"},"随便看看")],-1))),A=["src"],D=["src"];var I=a({setup(a){const h=l({errGif:g+"?"+ +new Date,ewizardClap:"https://wpimg.wallstcn.com/007ef517-bafd-4066-aae4-6883632d9646",dialogVisible:!1}),f=s(),m=e(),w=()=>{f.query.noGoBack?m.push({path:"/dashboard"}):m.go(-1)};let{ewizardClap:I,dialogVisible:O}=t(h);return(a,l)=>{const s=i("el-button"),e=i("router-link"),t=i("el-col"),h=i("el-row"),f=i("el-dialog");return u(),r("div",_,[n(s,{class:"pan-back-btn",onClick:w},{default:o((()=>[k])),_:1}),n(h,null,{default:o((()=>[n(t,{span:12},{default:o((()=>[v,V,x,y,z,p("ul",C,[G,p("li",j,[n(e,{to:"/dashboard"},{default:o((()=>[B])),_:1})]),q,p("li",null,[p("a",{href:"#",onClick:l[0]||(l[0]=b((a=>d(O)?O.value=!0:O=!0),["prevent"]))},"点我看图")])])])),_:1}),n(t,{span:12},{default:o((()=>[p("img",{src:c(g),width:"313",height:"428",alt:"Girl has dropped her ice cream."},null,8,A)])),_:1})])),_:1}),n(f,{modelValue:c(O),"onUpdate:modelValue":l[1]||(l[1]=a=>d(O)?O.value=a:O=a),title:"随便看"},{default:o((()=>[p("img",{src:c(I),class:"pan-img"},null,8,D)])),_:1},8,["modelValue"])])}}},[["__scopeId","data-v-cbc28918"]]);export{I as default}; 2 | -------------------------------------------------------------------------------- /static/js/admin/404.js: -------------------------------------------------------------------------------- 1 | import{_ as a,D as s,k as t,p as c,q as l,m as e,F as i,A as d,B as r,b as h,n}from"./index.js";var o="/static/png/admin/404_cloud.png";const _=a=>(d("data-v-cb6dfc12"),a=a(),r(),a),p={class:"wscn-http404-container"},v={class:"wscn-http404"},b=i('
404404404404
',1),f={class:"bullshit"},m=_((()=>c("div",{class:"bullshit__oops"},"OOPS!",-1))),u=_((()=>c("div",{class:"bullshit__info"},[h(" All rights reserved "),c("a",{style:{color:"#20a0ff"},href:"https://wallstreetcn.com",target:"_blank"},"wallstreetcn")],-1))),g={class:"bullshit__headline"},w=_((()=>c("div",{class:"bullshit__info"}," Please check that the URL you entered is correct, or click the button below to return to the homepage. ",-1))),k=_((()=>c("a",{href:"",class:"bullshit__return-home"},"Back to home",-1)));var y=a({setup(a){let i=s((()=>"The webmaster said that you can not enter this page..."));return(a,s)=>(n(),t("div",p,[c("div",v,[b,c("div",f,[m,u,c("div",g,l(e(i)),1),w,k])])]))}},[["__scopeId","data-v-cb6dfc12"]]);export{y as default}; 2 | -------------------------------------------------------------------------------- /static/js/admin/create.js: -------------------------------------------------------------------------------- 1 | import{_ as e,j as r,x as s,n as t}from"./index.js";import{U as a}from"./user-detail.js";import"./index3.js";import"./index4.js";var i=e({name:"CreateForm",components:{UserDetail:a}},[["render",function(e,a,i,n,o,m){const d=r("user-detail");return t(),s(d,{"is-edit":!1})}]]);export{i as default}; 2 | -------------------------------------------------------------------------------- /static/js/admin/create2.js: -------------------------------------------------------------------------------- 1 | import{_ as t,j as s,x as e,n as o}from"./index.js";import{P as i}from"./post-detail.js";import"./index3.js";import"./index4.js";import"./index5.js";var r=t({name:"CreatePost",components:{PostDetail:i}},[["render",function(t,i,r,a,n,d){const m=s("post-detail");return o(),e(m,{"is-edit":!1})}]]);export{r as default}; 2 | -------------------------------------------------------------------------------- /static/js/admin/create3.js: -------------------------------------------------------------------------------- 1 | import{_ as i,j as t,x as e,n as o}from"./index.js";import{T as r}from"./topic-detail.js";import"./index3.js";import"./index5.js";import"./index4.js";var s=i({name:"CreateForm",components:{TopicDetail:r}},[["render",function(i,r,s,a,n,m){const p=t("topic-detail");return o(),e(p,{"is-edit":!1})}]]);export{s as default}; 2 | -------------------------------------------------------------------------------- /static/js/admin/dashboard.js: -------------------------------------------------------------------------------- 1 | import{_ as a,i as s,D as e,k as t,p as n,q as r,m as d,H as o,n as c}from"./index.js";const m={class:"dashboard-container"},i={class:"dashboard-text"};var l=a({setup(a){let l=s();const u=e((()=>{if(name=l.state.user.name||"",name)return name;let a=o()||"";return a?a.name:void 0}));return(a,s)=>(c(),t("div",m,[n("div",i,"Welcome: "+r(d(u)),1)]))}},[["__scopeId","data-v-19c9d02c"]]);export{l as default}; 2 | -------------------------------------------------------------------------------- /static/js/admin/edit.js: -------------------------------------------------------------------------------- 1 | import{_ as e,j as r,x as s,n as t}from"./index.js";import{U as i}from"./user-detail.js";import"./index3.js";import"./index4.js";var a=e({name:"EditForm",components:{UserDetail:i}},[["render",function(e,i,a,n,o,d){const m=r("user-detail");return t(),s(m,{"is-edit":!0})}]]);export{a as default}; 2 | -------------------------------------------------------------------------------- /static/js/admin/edit2.js: -------------------------------------------------------------------------------- 1 | import{_ as t,j as s,x as o,n as i}from"./index.js";import{P as e}from"./post-detail.js";import"./index3.js";import"./index4.js";import"./index5.js";var r=t({name:"EditPostForm",components:{PostDetail:e}},[["render",function(t,e,r,n,a,d){const m=s("post-detail");return i(),o(m,{"is-edit":!0})}]]);export{r as default}; 2 | -------------------------------------------------------------------------------- /static/js/admin/edit3.js: -------------------------------------------------------------------------------- 1 | import{_ as i,j as t,x as o,n as e}from"./index.js";import{T as r}from"./topic-detail.js";import"./index3.js";import"./index5.js";import"./index4.js";var s=i({name:"EditForm",components:{TopicDetail:r}},[["render",function(i,r,s,n,a,d){const m=t("topic-detail");return e(),o(m,{"is-edit":!0})}]]);export{s as default}; 2 | -------------------------------------------------------------------------------- /static/js/admin/filter.js: -------------------------------------------------------------------------------- 1 | import"./index.js";function t(t,e){if(0===arguments.length)return null;const n=e||"{y}-{m}-{d} {h}:{i}:{s}";let r;"object"==typeof t?r=t:("string"==typeof t&&/^[0-9]+$/.test(t)&&(t=parseInt(t)),"number"==typeof t&&10===t.toString().length&&(t*=1e3),r=new Date(t));const g={y:r.getFullYear(),m:r.getMonth()+1,d:r.getDate(),h:r.getHours(),i:r.getMinutes(),s:r.getSeconds(),a:r.getDay()},o=n.replace(/{(y|m|d|h|i|s|a)+}/g,((t,e)=>{let n=g[e];return"a"===e?["日","一","二","三","四","五","六"][n]:(t.length>0&&n<10&&(n="0"+n),n||0)}));return o}export{t as p}; 2 | -------------------------------------------------------------------------------- /static/js/admin/index2.js: -------------------------------------------------------------------------------- 1 | import{d as a,u as s,a as r,o as e,c as t,b as p}from"./index.js";var u=a({setup(){const a=s(),u=r();return e((()=>{const{params:s,query:r}=a,{path:e}=s;u.replace({path:"/"+e,query:r})})),()=>t("div",null,[p(" ")])}});export{u as default}; 2 | -------------------------------------------------------------------------------- /static/js/admin/index4.js: -------------------------------------------------------------------------------- 1 | import{_ as t,n as i,k as e,p as s,S as h,Q as n,L as d}from"./index.js";const o={name:"Sticky",props:{stickyTop:{type:Number,default:0},zIndex:{type:Number,default:1},className:{type:String,default:""}},data:()=>({active:!1,position:"",width:void 0,height:void 0,isSticky:!1}),mounted(){this.height=this.$el.getBoundingClientRect().height,window.addEventListener("scroll",this.handleScroll),window.addEventListener("resize",this.handleReize)},activated(){this.handleScroll()},destroyed(){window.removeEventListener("scroll",this.handleScroll),window.removeEventListener("resize",this.handleReize)},methods:{sticky(){this.active||(this.position="fixed",this.active=!0,this.width=this.width+"px",this.isSticky=!0)},handleReset(){this.active&&this.reset()},reset(){this.position="",this.width="auto",this.active=!1,this.isSticky=!1},handleScroll(){const t=this.$el.getBoundingClientRect().width;this.width=t||"auto";this.$el.getBoundingClientRect().top[a]))],6)],4)}]]);export{l as S}; 2 | -------------------------------------------------------------------------------- /static/js/admin/index5.js: -------------------------------------------------------------------------------- 1 | import{_ as e,j as a,n as l,x as d,l as t,c as n,b as o,q as s,A as i,B as u,p as r}from"./index.js";const b={name:"Dropdown",emits:["input","onUpdate:value"],props:{value:{type:[Boolean,Number,String]},enabledText:{type:String},disabledText:{type:String},enabledLabel:{default:!0},disabledLabel:{default:!1}},methods:{change(e){this.$emit("onUpdate:value",e)}}},p=(e=>(i("data-v-1a81e584"),e=e(),u(),e))((()=>r("i",{class:"el-icon-caret-bottom el-icon--right"},null,-1)));var c=e(b,[["render",function(e,i,u,r,b,c){const m=a("el-button"),g=a("el-radio"),f=a("el-radio-group"),x=a("el-dropdown-item"),_=a("el-dropdown-menu"),v=a("el-dropdown");return l(),d(v,{"show-timeout":100,trigger:"click"},{dropdown:t((()=>[n(_,{class:"no-padding"},{default:t((()=>[n(x,null,{default:t((()=>[n(f,{modelValue:u.value,"onUpdate:modelValue":i[0]||(i[0]=e=>u.value=e),style:{padding:"10px"},onChange:c.change},{default:t((()=>[n(g,{label:u.enabledLabel},{default:t((()=>[o(s(u.enabledText),1)])),_:1},8,["label"]),n(g,{label:u.disabledLabel},{default:t((()=>[o(s(u.disabledText),1)])),_:1},8,["label"])])),_:1},8,["modelValue","onChange"])])),_:1})])),_:1})])),default:t((()=>[n(m,{plain:""},{default:t((()=>[o(s(u.value===u.enabledLabel?u.enabledText:u.disabledText)+" ",1),p])),_:1})])),_:1})}],["__scopeId","data-v-1a81e584"]]);export{c as D}; 2 | -------------------------------------------------------------------------------- /static/js/admin/list.js: -------------------------------------------------------------------------------- 1 | import{_ as t,T as a,j as e,P as l,k as i,J as s,x as n,l as d,n as r,c as o,p as c,q as u,b as p}from"./index.js";import{p as h}from"./filter.js";const m={name:"UserList",data:()=>({list:null,total:0,listLoading:!0,parseTime:h}),created(){this.getList()},methods:{getList(){this.listLoading=!0,a().then((t=>{this.list=t.data.items,this.total=t.data.total,this.listLoading=!1}))}}},g={class:"app-container"},f=p("Edit");var w=t(m,[["render",function(t,a,p,h,m,w){const b=e("el-table-column"),_=e("svg-icon"),x=e("el-button"),L=e("router-link"),v=e("el-table"),y=l("loading");return r(),i("div",g,[s((r(),n(v,{data:m.list,border:"",fit:"","highlight-current-row":"",style:{width:"100%"}},{default:d((()=>[o(b,{align:"center",label:"ID",width:"80"},{default:d((t=>[c("span",null,u(t.row.id),1)])),_:1}),o(b,{width:"180px",align:"center",label:"Date"},{default:d((t=>[c("span",null,u(m.parseTime(t.row.created_at,"{y}-{m}-{d} {h}:{i}")),1)])),_:1}),o(b,{width:"180px",align:"center",label:"Name"},{default:d((t=>[c("span",null,u(t.row.name),1)])),_:1}),o(b,{width:"180px",align:"center",label:"Email"},{default:d((t=>[c("span",null,u(t.row.email),1)])),_:1}),o(b,{width:"120px",align:"center",label:"Actived"},{default:d((t=>[c("span",null,u(t.row.active?"Yes":"No"),1)])),_:1}),o(b,{label:"Actions",align:"center",width:"230","class-name":"small-padding fixed-width"},{default:d((t=>[o(L,{to:"/user/"+t.row.id+"/edit"},{default:d((()=>[o(x,{type:"primary",size:"small"},{default:d((()=>[o(_,{"icon-class":"edit"}),f])),_:1})])),_:2},1032,["to"])])),_:1})])),_:1},8,["data"])),[[y,m.listLoading]])])}]]);export{w as default}; 2 | -------------------------------------------------------------------------------- /static/js/admin/list3.js: -------------------------------------------------------------------------------- 1 | import{_ as t,ab as a,ac as e,j as l,P as s,k as i,J as n,x as o,l as d,n as r,c as u,p as c,q as p,b as h}from"./index.js";import{p as w}from"./filter.js";const m={name:"SpecialTopics",data:()=>({list:null,total:0,listLoading:!0,parseTime:w}),created(){this.getList()},methods:{statusFilter:t=>({1:"online",0:"unpublished"}[t]),getList(){this.listLoading=!0,a().then((t=>{this.list=t.data.items,this.total=t.data.total,this.listLoading=!1}))},switchStatus(t){e(t.id,{1:"POST",0:"DELETE"}[t.status]).then((a=>{a.data.r||(t.status=!t.status,this.$message.error("切换状态失败!"))}))}}},g={class:"app-container"},f=h("Edit");var b=t(m,[["render",function(t,a,e,h,w,m){const b=l("el-table-column"),_=l("el-switch"),x=l("el-tooltip"),v=l("svg-icon"),L=l("el-button"),T=l("router-link"),y=l("el-table"),E=s("loading");return r(),i("div",g,[n((r(),o(y,{data:w.list,border:"",fit:"","highlight-current-row":"",style:{width:"100%"}},{default:d((()=>[u(b,{align:"center",label:"ID",width:"80"},{default:d((t=>[c("span",null,p(t.row.id),1)])),_:1}),u(b,{width:"140px",align:"center",label:"Date"},{default:d((t=>[c("span",null,p(w.parseTime(t.row.created_at,"{y}-{m}-{d} {h}:{i}")),1)])),_:1}),u(b,{width:"180px",align:"center",label:"Title"},{default:d((t=>[c("span",null,p(t.row.title),1)])),_:1}),u(b,{width:"280px",align:"center",label:"Intro"},{default:d((t=>[c("span",null,p(t.row.intro),1)])),_:1}),u(b,{width:"80px",align:"center",label:"N_Post"},{default:d((t=>[c("span",null,p(t.row.n_posts),1)])),_:1}),u(b,{"class-name":"status-col",label:"Published",width:"100"},{default:d((t=>[u(x,{content:m.statusFilter(parseInt(t.row.status)),placement:"top"},{default:d((()=>[u(_,{modelValue:t.row.status,"onUpdate:modelValue":a=>t.row.status=a,"active-value":1,"inactive-value":0,onChange:a=>m.switchStatus(t.row)},null,8,["modelValue","onUpdate:modelValue","onChange"])])),_:2},1032,["content"])])),_:1}),u(b,{label:"Actions",align:"center",width:"230","class-name":"small-padding fixed-width"},{default:d((t=>[u(T,{to:"/topic/"+t.row.id+"/edit"},{default:d((()=>[u(L,{type:"primary",size:"small"},{default:d((()=>[u(v,{"icon-class":"edit"}),f])),_:1})])),_:2},1032,["to"])])),_:1})])),_:1},8,["data"])),[[E,w.listLoading]])])}]]);export{b as default}; 2 | -------------------------------------------------------------------------------- /static/js/github-widget.js: -------------------------------------------------------------------------------- 1 | (function(v){var i,m=0;var e=v.getElementsByTagName("meta");var b,w,p,A;for(i=0;i 2 | <%def name="title()">动态 3 | 4 | <%def name="more_header()"> 5 | 6 | 7 | 8 | <%def name="pagination()"> 9 | 10 | <%def name="content()"> 11 |
12 | 13 | 14 | <%def name="bottom_script()"> 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /templates/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Lyanna Admin 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /templates/archives.html: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.html" /> 2 | 3 | <%def name="content()"> 4 |
5 | % for year, posts in archives: 6 |

7 | 8 | ${ year } 9 | 10 |

11 | 12 | 27 | % endfor 28 | 29 | 30 | <%def name="pagination()"> 31 | 32 | -------------------------------------------------------------------------------- /templates/email/mention.html: -------------------------------------------------------------------------------- 1 | Hi, ${username}: 2 | 3 | 有人在 ${ site_name }${ post.title } 这篇文章下的评论中提到了你! 快去看看把~ 4 | 5 | From: Lyanna 6 | -------------------------------------------------------------------------------- /templates/favorites.html: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.html" /> 2 | <%namespace name="utils" file="/utils.html"/> 3 | 4 | <%def name="doc_head()"> 5 | 6 | 7 | 8 | <%def name="content()"> 9 |
    10 | % for type_, label in [('movie', '电影'), ('book', '读书'), ('game', '游戏')]: 11 |
  • 12 | ${label}
  • 13 | % endfor 14 |
15 | 16 | 34 | 35 | 36 | <%def name="pagination()"> 37 | ${ utils.pagination('blog.favorites', paginatior, type=type) } 38 | 39 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | <%! from config import BLOG_URL, USE_YAML, AttrDict %> 2 | <%inherit file="/base.html" /> 3 | <%namespace name="utils" file="/utils.html"/> 4 | 5 | <%def name="doc_head()"> 6 | 7 | 19 | 20 | 21 | <%def name="content()"> 22 |
23 |
24 | % for post in paginatior.items: 25 |
26 |

${ post.title }

27 | 39 |
40 |

${ post.excerpt }

41 |
42 | 43 | 阅读全文 〉 44 |
45 | % endfor 46 |
47 |
48 | 49 | % if USE_YAML: 50 | 57 | % endif 58 | 59 | 60 | <%def name="pagination()"> 61 | ${ utils.pagination('blog.page', paginatior) } 62 | 63 | 64 | <%def name="bottom_script()"> 65 | % if USE_YAML: 66 | 67 | % endif 68 | 69 | -------------------------------------------------------------------------------- /templates/partials/sidebar/about_me.html: -------------------------------------------------------------------------------- 1 | <%page args="partial"/> 2 |
3 | % if 'title' in partial: 4 |
${ partial.title }
5 | % endif 6 | % if partial.avatar: 7 | 8 | % endif 9 |

${ partial.intro }

10 | 11 | 12 | 18 |
    19 | <% 20 | sns_map = { 21 | 'douban': 'https://www.douban.com/people/{}', 22 | 'linkedin': 'https://www.linkedin.com/in/{}', 23 | 'stack-overflow': 'https://stackoverflow.com/users/{}', 24 | 'medium': 'https://medium.com/@{}', 25 | 'zhihu': 'https://www.zhihu.com/people/{}', 26 | 'email': 'mailto:{}' 27 | } 28 | %> 29 | % for site in partial.sns: 30 | <% 31 | sns = list(site)[0] 32 | id = site[sns] 33 | tpl = sns_map.get(sns) 34 | if tpl: 35 | url = tpl.format(id) 36 | elif sns in ('wechat', 'weixingongzhonghao'): 37 | url = app.url_for('static', filename=f'upload/{ id }') 38 | else: 39 | url = f'https://{sns}.com/{id}' 40 | 41 | if sns == 'weixingongzhonghao': 42 | label = '微信公众号' 43 | elif sns == 'wechat': 44 | label = '微信' 45 | else: 46 | label = sns.replace('-' , '').capitalize() 47 | %> 48 | % if url: 49 |
  • 50 | 51 | 52 | 59 | 60 | 61 |
  • 62 | % endif 63 | % endfor 64 |
65 |
66 | -------------------------------------------------------------------------------- /templates/partials/sidebar/blogroll.html: -------------------------------------------------------------------------------- 1 | <%page args="partial"/> 2 |
3 | % if 'title' in partial: 4 |
${ partial.title }
5 | % endif 6 | 7 |
    8 | % for site in partial.links: 9 | % if 'url' in site: 10 | <% url = site['url'] %> 11 |
  • ${ site.get('title') or url }
  • 12 | % endif 13 | % endfor 14 |
15 |
16 | -------------------------------------------------------------------------------- /templates/partials/sidebar/favorite/book.html: -------------------------------------------------------------------------------- 1 | <%namespace name="subject" file="/partials/sidebar/favorite/subject.html"/> 2 | <%page args="partial"/> 3 | 4 | ${ subject.widget(partial, latest_books) } 5 | -------------------------------------------------------------------------------- /templates/partials/sidebar/favorite/game.html: -------------------------------------------------------------------------------- 1 | <%namespace name="subject" file="/partials/sidebar/favorite/subject.html"/> 2 | <%page args="partial"/> 3 | 4 | ${ subject.widget(partial, latest_games) } 5 | -------------------------------------------------------------------------------- /templates/partials/sidebar/favorite/movie.html: -------------------------------------------------------------------------------- 1 | <%namespace name="subject" file="/partials/sidebar/favorite/subject.html"/> 2 | <%page args="partial"/> 3 | 4 | ${ subject.widget(partial, latest_movies) } 5 | -------------------------------------------------------------------------------- /templates/partials/sidebar/favorite/subject.html: -------------------------------------------------------------------------------- 1 | <%page args="partial"/> 2 | 3 | <%def name="widget(partial, subjects)"> 4 |
5 |
${ partial.title if 'title' in partial else 'Recent Subjects' } · · · 6 | 7 | ( More ) 8 | 9 |
10 | 11 | 16 |
17 | 18 | -------------------------------------------------------------------------------- /templates/partials/sidebar/feed.html: -------------------------------------------------------------------------------- 1 | <%! 2 | from urllib.parse import quote_plus 3 | 4 | from config import BLOG_URL 5 | %> 6 | <%page args="partial"/> 7 | <% 8 | feed_url = f"{partial.scheme}://{ BLOG_URL.rpartition('//')[-1] }{partial.path}" 9 | %> 10 |
11 |
${ partial.title if 'title' in partial else '订阅本站' }
12 | 13 |
    14 | % for reader in partial.readers: 15 | % if reader == 'feedly': 16 | <% 17 | url = f"https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.swo.moe%2Fstats%2Ffeedly%2F{ quote_plus(quote_plus(feed_url)) }&query=count&color=282c34&label=Feedly&labelColor=2bb24c&logo=feedly&logoColor=ffffff&suffix=+subs&cacheSeconds=3600" 18 | href = f"https://feedly.com/i/subscription/feed%2F{ feed_url }" 19 | %> 20 | % elif reader == 'inoreader': 21 | <% 22 | href = f'https://www.inoreader.com/feed/{ feed_url }' 23 | url = 'https://img.shields.io/badge/Inoreader-no%20result-orange?logo=rss' 24 | %> 25 | % else: 26 | <% url = '' %> 27 | % endif 28 | 29 | % if url: 30 |
  • 31 | % endif 32 | % endfor 33 |
34 |
35 | -------------------------------------------------------------------------------- /templates/partials/sidebar/html.html: -------------------------------------------------------------------------------- 1 | <%page args="partial"/> 2 |
3 | % if 'title' in partial: 4 |
${ partial.title }
5 | % endif 6 | 7 | ${ partial.body | n } 8 |
9 | -------------------------------------------------------------------------------- /templates/partials/sidebar/latest_comments.html: -------------------------------------------------------------------------------- 1 | <% from models.utils import trunc_str %> 2 | <%page args="partial"/> 3 |
4 |
${ partial.title if 'title' in partial else 'Recent Comments' }
5 | 6 | 28 |
29 | -------------------------------------------------------------------------------- /templates/partials/sidebar/latest_notes.html: -------------------------------------------------------------------------------- 1 | <%page args="partial"/> 2 | <% 3 | 4 | %> 5 |
6 |
${ partial.title if 'title' in partial else 'Recent Notes' }
7 | 8 | 13 |
-------------------------------------------------------------------------------- /templates/partials/sidebar/most_viewed.html: -------------------------------------------------------------------------------- 1 | <%page args="partial"/> 2 |
3 |
${ partial.title if 'title' in partial else 'Most Viewed' }
4 | 5 | 10 |
11 | -------------------------------------------------------------------------------- /templates/partials/sidebar/tagcloud.html: -------------------------------------------------------------------------------- 1 | <% import math %> 2 | <%page args="partial"/> 3 |
4 |
${ partial.title if 'title' in partial else 'TagCloud' }
5 | 6 | % if tags: 7 |
    8 | <% 9 | per_page = 4 10 | sorted_tags = sorted(tags, key=lambda x: x[1], reverse=True) 11 | max_ = len(sorted_tags[0]) / per_page 12 | min_ = len(sorted_tags[-1]) / per_page 13 | %> 14 | % for tag, count in tags: 15 | <% size = max(math.log(count / per_page) - min_, 1) / ((math.log(max_) - math.log(min_)) or 1) * 4 + 5 %> 16 |
  • ${ tag.name }
  • 17 | % endfor 18 |
19 | % endif 20 |
21 | -------------------------------------------------------------------------------- /templates/search.html: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.html" /> 2 | 3 | <%def name="content()"> 4 |
5 |
6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 |
    14 | % for post in []: 15 |
  • ${ post.title }

    ${ post.excerpt }

  • 16 | % endfor 17 |
18 |
19 |
20 |
21 |
22 | 23 | 24 | <%def name="pagination()"> 25 | 26 | 27 | <%def name="bottom_script()"> 28 | 29 | 30 | -------------------------------------------------------------------------------- /templates/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | % for loc, lastmod in items: 4 | 5 | ${ loc } 6 | ${ lastmod } 7 | 8 | % endfor 9 | 10 | -------------------------------------------------------------------------------- /templates/tag.html: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.html" /> 2 | 3 | <%! 4 | from config import SITE_TITLE 5 | %> 6 | 7 | <%def name="content()"> 8 |
9 |

${ tag.name }

10 | % for post in posts: 11 |
12 | 13 | ${ post.title } 14 | 15 | 18 |
19 | % endfor 20 |
21 | 22 | 23 | <%def name="pagination()"> 24 | 25 | 26 | <%def name="title()">${ tag.name } - ${ SITE_TITLE } 27 | -------------------------------------------------------------------------------- /templates/tags.html: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.html" /> 2 | 3 | <%def name="content()"> 4 |
5 |
6 |
    7 | % for tag, count in tags: 8 |
  • 9 | ${ tag.name } 10 | ${ count } 11 |
  • 12 | % endfor 13 |
14 |
15 |
16 | 17 | 18 | <%def name="pagination()"> 19 | 20 | -------------------------------------------------------------------------------- /templates/topic.html: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.html" /> 2 | <%namespace name="utils" file="/utils.html"/> 3 | 4 | <%def name="doc_head()"> 5 | 6 | 7 | 8 | <%def name="content()"> 9 |
10 |
11 |

-  ${ topic.title } -

12 |
${ topic.intro }
13 | % for post in posts: 14 |
15 |

${ post.title }

16 | 28 |
29 |

${ post.excerpt }

30 |
31 |
32 | % endfor 33 |
34 |
35 | 36 | -------------------------------------------------------------------------------- /templates/topics.html: -------------------------------------------------------------------------------- 1 | <%inherit file="/base.html" /> 2 | <%namespace name="utils" file="/utils.html"/> 3 | 4 | <%def name="doc_head()"> 5 | 6 | 7 | 8 | <%def name="content()"> 9 |
10 |
11 | % for topic in paginatior.items: 12 |
13 | ${ topic.title } 14 |
${ topic.intro }
15 | ${ topic.created_at.strftime('%Y年%m月%d日') } 16 |
17 | % endfor 18 |
19 |
20 | 21 | 22 | <%def name="pagination()"> 23 | ${ utils.pagination('blog.topics', paginatior) } 24 | 25 | -------------------------------------------------------------------------------- /views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongweiming/lyanna/45943a68a92cd908685671751f2c2bb5289946dd/views/__init__.py -------------------------------------------------------------------------------- /views/request.py: -------------------------------------------------------------------------------- 1 | from sanic.request import Request as _Request 2 | 3 | import config 4 | from models import User 5 | 6 | 7 | class Request(_Request): 8 | user: User 9 | partials = config.partials 10 | -------------------------------------------------------------------------------- /views/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | from datetime import datetime, timezone 3 | from json import JSONEncoder, dumps 4 | from typing import Any, Dict, Optional, Tuple 5 | 6 | import aiohttp 7 | from sanic.exceptions import SanicException 8 | from sanic.response import HTTPResponse 9 | 10 | from config import HERE 11 | 12 | DOUBAN_URL_RE = re.compile(r'https://(\w+)\.douban\.com\/subject\/(\d+)') 13 | 14 | 15 | class APIJSONEncoder(JSONEncoder): 16 | def default(self, val): 17 | if isinstance(val, datetime): 18 | return int(val.replace(tzinfo=timezone.utc).timestamp()) 19 | try: 20 | return JSONEncoder.default(self, val) 21 | except TypeError as e: 22 | if hasattr(val, 'to_dict'): 23 | return val.to_dict() 24 | raise e 25 | 26 | 27 | def json(body: Dict[str, Any], status: int = 200, headers: Optional[Any] = None, 28 | content_type: str = "application/json", **kwargs: Any) -> HTTPResponse: 29 | return HTTPResponse( 30 | dumps(body, separators=(",", ":"), cls=APIJSONEncoder, **kwargs), 31 | headers=headers, 32 | status=status, 33 | content_type=content_type, 34 | ) 35 | 36 | 37 | def abort(status_code): 38 | raise SanicException(None, status_code) 39 | 40 | 41 | async def save_image(url) -> Tuple[bytes, str]: 42 | basename = url.rpartition('/')[-1] 43 | dist = HERE / 'static/jpg' / basename 44 | async with aiohttp.ClientSession() as session: 45 | async with session.get(url) as resp: 46 | data = await resp.read() 47 | with open(dist, 'wb') as f: 48 | f.write(data) 49 | return data, basename 50 | 51 | 52 | def normalization_url(url): 53 | if 'douban.com' in url: 54 | m = DOUBAN_URL_RE.search(url) 55 | if m: 56 | url = m.group() 57 | return url 58 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | 5 | const config = { 6 | entry: glob.sync('./src/**/*.js').reduce( 7 | (entries, entry) => Object.assign(entries, {[entry.split('/').splice(-2, 2).join('/').replace('.js', '')]: entry}), {}), 8 | performance: { 9 | hints: 'warning' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.js$/, 15 | exclude: /node_modules/, 16 | use: { 17 | loader: 'babel-loader', 18 | options: { 19 | presets: ['@babel/preset-env'] 20 | } 21 | } 22 | }, 23 | { 24 | test: /\.scss$/, 25 | use: [ 26 | "style-loader", 27 | "css-loader", 28 | "sass-loader" 29 | ] 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: [ 'style-loader', 'css-loader' ] 34 | }, 35 | { 36 | test: /\.(png|woff|woff2|eot|ttf|svg)$/, 37 | loader: 'url-loader?limit=100000' 38 | } 39 | ] 40 | }, 41 | 42 | output: { 43 | filename: '[name].js', 44 | path: path.join(__dirname, 'static/dist') 45 | }, 46 | plugins: [ 47 | new webpack.ProvidePlugin({ 48 | $: "jquery", 49 | jQuery: "jquery" 50 | })] 51 | } 52 | 53 | module.exports = config 54 | --------------------------------------------------------------------------------