提交 c841cc13 编写于 作者: ld_4215105's avatar ld_4215105

add

上级 703f1344
# 注意:uwsgi.ini最好放在项目的根目录下,方便管理,配置。
[uwsgi]
# 指定项目的绝对路径
chdir = /root/project/
# django项目的wsgi文件的位置(写相对于chdir的相对路径)
module = project.wsgi
# 写入虚拟环境解释器的绝对路径
# home = /home/echo/Desktop/djangoApp/webapp/venv
master = true
# 指定uwsgi启动的进程个数
processes = 1
#uwsgi启动一个socket连接,当你使用nginx+uwsgi的时候,应该使用socket参数
socket = 172.30.0.9:8000
# 这个参数是uwsgi启动一个http连接,当你不用nginx只用uwsgi的时候,使用这个参数
# http = 0.0.0.0:8000
vacuum = true
# 后台运行uwsgi(使用supervisor进程管理工具的时候,不要使用这个参数,不过我一般都不会用)
# daemonize=yes
from django.contrib import admin
# Register your models here.
from .models import User, Article, Comment, Label
class UserAdmin(admin.ModelAdmin):
"""
重写管理界面展示内容,系统默认展示用户模型的__str__()方法返回的数据
"""
list_display = ["username", "email", "password", "createTime", "lastActive", "aboutMe", "isDeleted"]
search_fields = ["username", "email"]
actions_on_top = False
actions_on_bottom = True
class ArticleAdmin(admin.ModelAdmin):
"""
重写管理界面展示内容,系统默认展示用户模型的__str__()方法返回的数据
"""
list_display = ["title", "label", "author", "createTime", "modifyTime", "isDeleted", "isPrimary"]
search_fields = ["title", "author"]
actions_on_top = False
actions_on_bottom = True
class CommentAdmin(admin.ModelAdmin):
"""
重写管理界面展示内容,系统默认展示用户模型的__str__()方法返回的数据
"""
list_display = ["commentator", "article", "createTime", "isDeleted", "isPrimary"]
search_fields = ["title", "author"]
actions_on_top = False
actions_on_bottom = True
# 注册用户数据库模型
admin.site.register(User, UserAdmin)
admin.site.register(Article, ArticleAdmin)
admin.site.register(Comment, CommentAdmin)
admin.site.register(Label)
from django.apps import AppConfig
class AppConfig(AppConfig):
name = 'app'
# Generated by Django 3.0.8 on 2020-07-12 00:43
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(max_length=20, unique=True, verbose_name='用户名')),
('email', models.EmailField(max_length=254, unique=True, verbose_name='邮箱')),
('password', models.CharField(max_length=20, verbose_name='用户密码')),
('aboutMe', models.CharField(default=None, max_length=1024, verbose_name='个人简介')),
('createTime', models.DateTimeField(auto_now_add=True, verbose_name='注册时间')),
('lastActive', models.DateTimeField(auto_now=True, verbose_name='最后活跃时间')),
('isDeleted', models.BooleanField(default=False, verbose_name='删除')),
],
),
migrations.CreateModel(
name='Article',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=256, verbose_name='标题')),
('label', models.IntegerField(default=0, verbose_name='标签')),
('content', models.TextField(default=None, max_length=65536, verbose_name='内容')),
('createTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('modifyTime', models.DateTimeField(auto_now=True, verbose_name='最近修改时间')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.User', verbose_name='作者')),
],
),
]
# Generated by Django 3.0.8 on 2020-07-12 00:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='article',
name='isDeleted',
field=models.BooleanField(default=False, verbose_name='删除'),
),
]
# Generated by Django 3.0.8 on 2020-07-12 13:23
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('app', '0002_article_isdeleted'),
]
operations = [
migrations.AddField(
model_name='article',
name='isPrimary',
field=models.BooleanField(default=False, verbose_name='仅个人可见'),
),
migrations.CreateModel(
name='Comment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('createTime', models.DateTimeField(auto_now_add=True, verbose_name='评论时间')),
('isDeleted', models.BooleanField(default=False, verbose_name='删除')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.Article', verbose_name='文章')),
('commentator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.User', verbose_name='评论者')),
],
),
]
# Generated by Django 3.0.8 on 2020-07-12 13:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app', '0003_auto_20200712_1323'),
]
operations = [
migrations.AddField(
model_name='comment',
name='isPrimary',
field=models.BooleanField(default=False, verbose_name='是否可见'),
),
]
# Generated by Django 3.0.8 on 2020-07-12 13:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app', '0004_comment_isprimary'),
]
operations = [
migrations.AddField(
model_name='comment',
name='content',
field=models.TextField(default=None, max_length=65536, verbose_name='评论内容'),
),
]
# Generated by Django 3.0.8 on 2020-07-12 15:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app', '0005_comment_content'),
]
operations = [
migrations.AlterField(
model_name='user',
name='password',
field=models.CharField(max_length=1024, verbose_name='用户密码'),
),
]
# Generated by Django 3.0.8 on 2020-08-08 10:13
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('app', '0006_auto_20200712_1532'),
]
operations = [
migrations.CreateModel(
name='Token',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('token', models.TextField(max_length=64, verbose_name='token')),
('createTime', models.DateTimeField(auto_now_add=True, verbose_name='授权时间')),
('expireTime', models.DateTimeField(verbose_name='过期时间')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.User', verbose_name='授权用户')),
],
),
]
# Generated by Django 3.0.8 on 2020-08-08 22:54
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('app', '0007_token'),
]
operations = [
migrations.RemoveField(
model_name='token',
name='expireTime',
),
]
# Generated by Django 3.0.8 on 2020-08-09 09:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app', '0008_remove_token_expiretime'),
]
operations = [
migrations.AlterField(
model_name='user',
name='aboutMe',
field=models.CharField(default='最美的我~', max_length=1024, verbose_name='个人简介'),
),
]
# Generated by Django 3.0.8 on 2020-08-29 11:11
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('app', '0009_auto_20200809_0942'),
]
operations = [
migrations.CreateModel(
name='Label',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.TextField(max_length=64, verbose_name='标签')),
('createTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
],
),
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(max_length=254, unique=True, validators=[django.core.validators.EmailValidator()], verbose_name='邮箱'),
),
migrations.AlterField(
model_name='article',
name='label',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.Label', verbose_name='分类'),
),
]
from django.db import models
from datetime import datetime
import re
import logging
import traceback
from django.contrib.auth.hashers import make_password, check_password
from django.core.validators import validate_email
RE_USERNAME = re.compile(r'^[a-zA-Z][a-zA-Z0-9_]{4,15}$')
RE_PASSWORD = re.compile(r'^(?=.*\d)(?=.*[a-zA-Z]).{8,16}$')
# Create your models here.
class User(models.Model):
"""
这是用户数据库表对应的模型类
Django支持ORM(对象关系映射)
通俗来说就是无需使用sql语句操作数据库,仅仅需要使用models.Model父类提供的方法就可以实现你使用sql能办到的事情
"""
username = models.CharField(verbose_name="用户名", unique=True, max_length=20)
email = models.EmailField(verbose_name="邮箱", unique=True, validators=[validate_email, ])
password = models.CharField("用户密码", max_length=1024)
aboutMe = models.CharField(verbose_name="个人简介", max_length=1024, default="最美的我~")
# auto_now_add 代表实例化对象时默认添加的时间
createTime = models.DateTimeField(verbose_name="注册时间", auto_now_add=True)
# auto_now 代表修改对象属性时默认保存当前时间
lastActive = models.DateTimeField(verbose_name="最后活跃时间", auto_now=True)
isDeleted = models.BooleanField(verbose_name="删除", default=False)
def __str__(self):
"""
打印实例化出来的对象时默认展示用户名
:return:
"""
return self.username
@classmethod
def make_password(cls, password):
"""
使用Django的默认生成密码密文
:param password:
:return:
"""
return make_password(password)
@classmethod
def check_password(cls, password):
"""
使用Django的默认密码校验
:param password:
:return:
"""
return check_password(password, cls.password)
def save(self, *args, **kwargs):
"""
重写保存方法,加入写入前数据验证
:param args:
:param kwargs:
:return:
"""
self.valid()
super().save(*args, **kwargs)
def valid(self):
"""
验证数据是否合法
:return:
"""
try:
for func in (self.username_valid,
self.password_valid,
self.email_valid):
func()
except Exception as err:
logging.error(str(err))
logging.error(traceback.format_exc())
raise ValueError("数据验证失败!")
def username_valid(self):
"""
用户名验证
:return:
"""
if not re.match(RE_USERNAME, self.username):
raise ValueError("用户名不合法!")
def password_valid(self):
"""
密码验证
:return:
"""
if not re.match(RE_PASSWORD, self.password):
raise ValueError("密码不合法!")
def email_valid(self):
"""
邮箱验证
:return:
"""
validate_email(self.email)
class Label(models.Model):
"""
标签
"""
label = models.TextField(verbose_name="标签", name="label", max_length=64)
createTime = models.DateTimeField(verbose_name="创建时间", auto_now_add=True)
def __str__(self):
"""
标签
:return:
"""
return self.label
class Article(models.Model):
"""
文章数据库表模型
"""
title = models.CharField(verbose_name="标题", max_length=256)
content = models.TextField(verbose_name="内容", max_length=65536, default=None)
createTime = models.DateTimeField(verbose_name="创建时间", auto_now_add=True)
modifyTime = models.DateTimeField(verbose_name="最近修改时间", auto_now=True)
# 外联标签表
label = models.ForeignKey(Label, verbose_name="分类", on_delete=models.CASCADE)
# 外键关联用户表
author = models.ForeignKey(User, verbose_name="作者", on_delete=models.CASCADE)
isDeleted = models.BooleanField(verbose_name="删除", default=False)
isPrimary = models.BooleanField(verbose_name="仅个人可见", default=False)
def __str__(self):
"""
打印实例化出来的对象时默认展示文章标题
:return:
"""
return self.title
class Comment(models.Model):
"""
文章评论数据库表模型
"""
commentator = models.ForeignKey(User, verbose_name="评论者", on_delete=models.CASCADE)
article = models.ForeignKey(Article, verbose_name="文章", on_delete=models.CASCADE)
content = models.TextField(verbose_name="评论内容", max_length=65536, default=None)
createTime = models.DateTimeField(verbose_name="评论时间", auto_now_add=True)
isDeleted = models.BooleanField(verbose_name="删除", default=False)
isPrimary = models.BooleanField(verbose_name="是否可见", default=False)
class Token(models.Model):
"""
用户免登陆验证
"""
token = models.TextField(verbose_name="token", name="token", max_length=64)
createTime = models.DateTimeField(verbose_name="授权时间", auto_now_add=True)
user = models.ForeignKey(User, verbose_name="授权用户", on_delete=models.CASCADE)
def __str__(self):
"""
打印实例化出来的对象时默认展示文章标题
:return:
"""
return self.token
from rest_framework import serializers
from . import models
class ArticleListSerializer(serializers.ModelSerializer):
class Meta:
fields = ('id', 'title', 'label', 'content',
'createTime', 'author', 'isDeleted')
model = models.Article
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
fields = ('id', 'title', 'label', 'content', 'isDeleted',
'createTime', 'modifyTime', 'author', 'isPrimary')
model = models.Article
class UserSerializer(serializers.ModelSerializer):
class Meta:
fields = ('id', 'username', 'aboutMe', 'lastActive',
'createTime')
model = models.User
class LabelSerializer(serializers.ModelSerializer):
class Meta:
fields = ('id', 'label')
model = models.Label
class TokenSerializer(serializers.ModelSerializer):
class Meta:
fields = ('token', )
model = models.Token
from django.test import TestCase
# Create your tests here.
from django.urls import path
from django.conf.urls import url, include
from . import views
from rest_framework.routers import DefaultRouter
app_name = "app"
article_list = views.ArticleViewSet.as_view(actions={'get': 'list', 'post': 'create'})
article_detail = views.ArticleViewSet.as_view(actions={
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
})
# urlpatterns = [
# path('', article_list, name='article-list'),
# path('<int:pk>/', article_detail, name='article-detail'),
# path('register', views.register, name='register'),
# ]
router = DefaultRouter()
# router.register('article', views.ArticleViewSet)
router.register('tags', views.LabelViewSet)
urlpatterns = [
url(r'^article/$', article_list),
url(r'^article/(?P<pk>\d+)/$', article_detail),
url(r'^', include(router.urls)),
url('image\/(?P<image>\S+)?\/?$', views.ImageAPI.as_view()),
path("public_key", views.KeyAPI.as_view()),
path("user", views.UserAPI.as_view())
]
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDQVro3P6NTxld9zm+PUpth3wxqEVxIdnyF4J6oVIgOEsDpjAdT
pUcCa3k8O2h+G0cP0i5vB/uCqio4KamdM5TcrpItlpawZrzsnk39xCS1HMeEWlWs
aY0Sojq/YKitAsVtiENvlAcXHzed6Yp/4vPICRRv5fc0oumvYLHhY2fd1wIDAQAB
AoGAKci7IEmLIEovUfQNO9l9gGuTudIxh/b7kn4yU+BgSTJJro1/Cq+jRkD317Sy
iFhAMFQfK/WV4+btAMaaVgT6XctVykOlI6odNTr4HWJNUbjkKV6pxBIz5viDdex+
PocW8Lp9pRCIbMhbar2dHWjiYi8tK9R4QHpq4HkDFN2ZAkECQQDVHXhcAoMHx7sv
FEjE1X+vkNvITMoCHreqlXW0F2pz/n+KPxrbrnR/rpsspccV6NtxvLr4n52Qg3Ls
e7pS7pGzAkEA+kM0z7P596MXuNIVtbaXAAe3AYkQmmHeE1U7B1MwEWW0x/tNbrIf
ubUzfLOVAMbGOx037voBmH0VlLUGfH5JTQJBAII+2ZE6A8Scf7y9I+AQL1IoMPpQ
W+FBzrQVVfMzoF61ulLOUGbUTuS93J5DztGIF52CX66VNmHcxD7dOzl2DlsCQEdM
REx0Ot/JMCXwfEzjswtIJ2Qhl4BUJ3+chY+lrIkYT7O1ra6+wnkW3Y9GXkn1V15a
Cahkw3WdFTvjgvCOzfUCQAYWJNltM9tPn0wkUbtHXF1vjSomJaly0h5GdqDzleK6
ViDqw8FkGe0tiskxaXElwA21g/Apq/Z7XTQF3oQK6qg=
-----END RSA PRIVATE KEY-----
\ No newline at end of file
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQVro3P6NTxld9zm+PUpth3wxq
EVxIdnyF4J6oVIgOEsDpjAdTpUcCa3k8O2h+G0cP0i5vB/uCqio4KamdM5TcrpIt
lpawZrzsnk39xCS1HMeEWlWsaY0Sojq/YKitAsVtiENvlAcXHzed6Yp/4vPICRRv
5fc0oumvYLHhY2fd1wIDAQAB
-----END PUBLIC KEY-----
\ No newline at end of file
"""
加密用公钥
揭秘用私钥
"""
import os
import base64
import logging
import traceback
from Crypto import Random
from Crypto.Cipher import PKCS1_v1_5
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
PRIVATE_KEY = os.path.join(BASE_DIR, "private_key.pem")
PUBLIC_KEY = os.path.join(BASE_DIR, "public_key.pem")
def generator_key():
"""
生成密钥对
:return:
"""
# random_generator = Random.new().read
# key = RSA.RSA.generate(1024, random_generator)
key = RSA.generate(1024)
file_out = open(PRIVATE_KEY, "wb")
private_key = key.export_key()
file_out.write(private_key)
public_key = key.publickey().export_key()
file_out = open(PUBLIC_KEY, "wb")
file_out.write(public_key)
def get_key(key, public=True, need_bytes=False):
"""
获取公钥
:param need_bytes:
:param key:
:param public:
:return:
"""
try:
if not (os.path.exists(PUBLIC_KEY) and
os.path.exists(PRIVATE_KEY)):
logging.debug("youyouyou")
generator_key()
if not os.path.exists(key):
return open(
PUBLIC_KEY if public else PRIVATE_KEY,
"rb" if need_bytes else "r").read()
else:
return open(key, "rb" if need_bytes else "r").read()
except Exception as err:
logging.error(str(err))
logging.error(traceback.format_exc())
return None
def encrypt_data(data, public_key):
"""
用公钥进行加密
:param data:
:param public_key:
:return:
"""
if isinstance(data, str):
data = bytes(data, encoding="utf8")
elif not isinstance(data, bytes):
return None
key = get_key(public_key, need_bytes=True)
if not key:
return None
try:
key = RSA.import_key(key)
cipher = PKCS1_v1_5.new(key)
return cipher.encrypt(data)
except Exception as err:
logging.error(str(err))
logging.error(traceback.format_exc())
return None
def decrypt_data(data, private_key):
"""
用私钥进行解密
:param data:
:param private_key:
:return:
"""
if isinstance(data, str):
data = base64.b64decode(bytes(data, encoding="utf8"))
elif not isinstance(data, bytes):
return None
key = get_key(private_key, need_bytes=True)
if not key:
return None
try:
key = RSA.import_key(key)
cipher = PKCS1_v1_5.new(key)
length = len(data)
default_length = 128
if length < 128:
return cipher.decrypt(data, sentinel='error').decode("utf8")
else:
offset = 0
res = []
while length - offset > 0:
if length - offset > default_length:
res.append(cipher.decrypt(data[offset: offset + default_length], sentinel='error'))
else:
res.append(cipher.decrypt(data[offset:], sentinel='error'))
offset += default_length
return b''.join(res).decode("utf8")
except Exception as err:
logging.error(str(err))
logging.error(traceback.format_exc())
return None
#! /usr/bin/python3
# coding:utf8
"""
此处应该定义如下API
/app/user 负责用户的注册和登录
post请求:
负载中 action 决定注册还是登录
/app/article[/article_id:int] get不带id负责返回不带内容的文章基础信息列表
get带id返回带详细内容的一篇文章
post不带id则代表保存文章
post带id则代表
delete 代表删除文章
/app/public_key 负责获取公钥加密密码
/app/tag /article_id:int get不带id负责返回不带内容的标签基础信息列表
get带id返回带详细内容的一个标签
post不带id则代表保存标签
post带id则代表更新次标签
delete 代表删除标签
"""
import os
from django.shortcuts import render
import json
import traceback
import logging
import copy
import time
# Create your views here.
from django.http import HttpResponse, FileResponse
from rest_framework import generics, mixins, viewsets
from rest_framework.views import APIView
from rest_framework.response import Response
from django.core.paginator import Paginator
from . import models
from . import serializers
from .utils.rsa_crypt import get_key, PUBLIC_KEY, decrypt_data, PRIVATE_KEY
import hashlib
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
def register(request):
return HttpResponse("<center><p>欢迎欢迎,您现在处于注册用户界面。</p></center>")
def json_dumps(data):
try:
return json.dumps(data, indent=4, ensure_ascii=False)
except Exception as err:
logging.error(data)
logging.error(str(err))
logging.error(traceback.format_exc())
return '{"message": "错误的Json格式,请系统管理员检查错误!"}'
# class ArticleList(generics.ListCreateAPIView):
# queryset = models.Article.objects.all().order_by('id')
# serializer_class = serializers.ArticleSerializer
# class ArticleList(mixins.ListModelMixin,
# mixins.CreateModelMixin,
# generics.GenericAPIView):
# queryset = models.Article.objects.all()
# serializer_class = serializers.ArticleSerializer
#
# def get(self, request, *args, **kwargs):
# return self.list(request, *args, **kwargs)
#
# def post(self, request, *args, **kwargs):
# return self.create(request, *args, **kwargs)
# class ArticleDetail(generics.RetrieveUpdateDestroyAPIView):
# queryset = models.Article.objects.all().order_by("id")
# serializer_class = serializers.ArticleSerializer
# class ArticleDetail(mixins.RetrieveModelMixin,
# mixins.UpdateModelMixin,
# mixins.DestroyModelMixin,
# generics.GenericAPIView):
# queryset = models.Article.objects.all()
# serializer_class = serializers.ArticleSerializer
#
# def get(self, request, *args, **kwargs):
# return self.retrieve(request, *args, **kwargs)
#
# def put(self, request, *args, **kwargs):
# return self.update(request, *args, **kwargs)
#
# def delete(self, request, *args, **kwargs):
# return self.destroy(request, *args, **kwargs)
class ArticleViewSet(viewsets.ModelViewSet):
queryset = models.Article.objects.all().order_by("-createTime")
serializer_class = serializers.ArticleSerializer
def list(self, request, *args, **kwargs):
try:
label_id = request.query_params.get('value', 0)
if label_id:
label_id = int(label_id[0])
else:
label_id = 0
except Exception as err:
label_id = 0
if not label_id:
queryset = models.Article.objects.all().order_by("-createTime")
else:
queryset = models.Article.objects.all().filter(label=label_id).order_by("-createTime")
page = self.paginate_queryset(queryset)
if page is not None:
serializer = serializers.ArticleListSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = serializers.ArticleListSerializer(queryset, many=True)
return Response(serializer.data)
class LabelViewSet(viewsets.ModelViewSet):
queryset = models.Label.objects.all().order_by("id")
serializer_class = serializers.LabelSerializer
def list(self, request, *args, **kwargs):
queryset = self.queryset
serializer = serializers.LabelSerializer(queryset, many=True)
return Response(serializer.data)
class KeyAPI(APIView):
"""
获取公钥
"""
return_data = {
"message": None,
"status": 0,
"data": None
}
def get(self, request):
"""
直接返回公钥
:param request:
:return:
"""
key = get_key(PUBLIC_KEY)
if not key:
self.return_data["message"] = "公钥获取失败!"
self.return_data["status"] = 1
else:
self.return_data["data"] = key
return HttpResponse(json_dumps(self.return_data))
def make_token(user):
"""
制作用户的token
:param user:
:return:
"""
now = str(time.time())
verify = hashlib.sha1(bytes(now+user.username,
encoding="utf-8")).hexdigest()[:16]
token = models.Token()
token.token = verify
token.user = user
token.save()
return verify
class UserAPI(APIView):
"""
管理用户登陆注册的API
"""
return_template = {
"message": None,
"status": 0,
"data": None
}
def post(self, request):
"""
注册或者登陆
:param request:
:return:
"""
self.return_data = copy.deepcopy(self.return_template)
action = request.data.get("action")
logging.debug(request.data)
if action == "register":
return self.register(request)
elif action == "login":
return self.login(request)
elif action == "token":
return self.verify_token(request)
else:
self.return_data["message"] = "错误请求!"
self.return_data["status"] = 1
return HttpResponse(json_dumps(self.return_data))
def register(self, request):
"""
用户注册
:param request:
:return:
"""
data = copy.deepcopy(request.data)
keys = ("username", "password", "email")
verify = dict()
for key in keys:
verify[key] = data.get(key)
if not all(verify):
self.return_data["message"] = "参数错误!"
self.return_data["status"] = 1
return HttpResponse(json_dumps(self.return_data))
verify["password"] = decrypt_data(verify["password"], PRIVATE_KEY)
data["repeat_password"] = decrypt_data(data["repeat_password"], PRIVATE_KEY)
if verify["password"] != data["repeat_password"]:
self.return_data["message"] = "两次密码不一致!"
self.return_data["status"] = 1
return HttpResponse(json_dumps(self.return_data))
user = models.User()
user.username = verify["username"]
user.password = verify["password"]
user.email = verify["email"]
try:
user.save()
self.return_data["message"] = "注册成功!"
except Exception as err:
logging.error(str(err))
logging.error(traceback.format_exc())
self.return_data["message"] = "验证失败!"
self.return_data["status"] = 1
return HttpResponse(json_dumps(self.return_data))
def login(self, request):
"""
用户注册
:param request:
:return:
"""
data = copy.deepcopy(request.data)
data["password"] = decrypt_data(data["password"], PRIVATE_KEY)
data["username"] = data.get("username")
if data["password"] and data["username"]:
try:
user = models.User.objects.filter(
username=data["username"],
password=data["password"]).first()
if user:
if data.get("rememberMe") is True:
self.return_data["message"] = "登陆成功!"
self.return_data["token"] = make_token(user)
self.return_data["user"] = serializers.UserSerializer(user).data
else:
self.return_data["message"] = "登陆成功!"
self.return_data["token"] = make_token(user)
self.return_data["user"] = serializers.UserSerializer(user).data
return HttpResponse(json_dumps(self.return_data))
except Exception as err:
logging.error(str(err))
logging.error(traceback.format_exc())
self.return_data["status"] = 1
self.return_data["message"] = "用户名或密码错误!"
return HttpResponse(json_dumps(self.return_data))
def verify_token(self, request):
"""
直接验证token是否存在或者存在但是过期
:param request:
:return:
"""
verify = request.data.get("token")
if not verify:
self.return_data["message"] = "登录信息不存在或者已经过期!"
self.return_data["status"] = 1
return HttpResponse(json_dumps(self.return_data))
token = models.Token.objects.filter(token=verify).first()
if ((not token) or
(time.time() - token.createTime.timestamp() > 7*24*60*60)):
self.return_data["message"] = "登录信息不存在或者已经过期!"
self.return_data["status"] = 1
return HttpResponse(json_dumps(self.return_data))
user = token.user
self.return_data["user"] = serializers.UserSerializer(user).data
self.return_data["token"] = token.token
now = time.time()
return HttpResponse(json_dumps(self.return_data))
class ImageAPI(APIView):
"""
获取公钥
"""
return_data = {
"message": None,
"status": 0,
"data": None
}
def get(self, request, image=None):
"""
直接返回图片
:param request:
:return:
"""
data = copy.deepcopy(self.return_data)
file_path = os.path.join(BASE_DIR, "images", image or '')
if not os.path.isfile(file_path):
data["status"] = 1
data["message"] = "图片已经不存在了!"
return HttpResponse(json_dumps(data))
else:
res = FileResponse(open(file_path, "rb"))
res["Content-Type"] = "image/png"
res["Content-Disposition"] = "inline"
return res
def post(self, request):
"""
制作图片外链
"""
data = copy.deepcopy(self.return_data)
image = request.FILES.get("file")
if not image:
data["status"] = 1
data["message"] = "图片上传失败!"
return HttpResponse(json_dumps(data))
filename = str(time.time())
file_path = os.path.join(BASE_DIR, "images", filename)
with open(file_path, "wb") as fd:
fd.write(image.read())
data["filename"] = filename
return HttpResponse(json_dumps(data))
文件已添加
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
"""
ASGI config for project project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
application = get_asgi_application()
"""
Django settings for project project.
Generated by 'django-admin startproject' using Django 3.0.8.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.0/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'zmxxnyu2$=yt$+cl^w486=_cu%i-=ils=1xu(ja$k0_cd73$&7'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ["94.191.4.111", "note02.xyz", "192.168.1.3"]
# Application definition
INSTALLED_APPS = [
'corsheaders',
'rest_framework',
'app.apps.AppConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'project.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'project.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = '/static/'
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}
# 跨域增加忽略
CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_ALLOW_ALL = True
CORS_ORIGIN_WHITELIST = ()
CORS_ALLOW_METHODS = (
'DELETE',
'GET',
'OPTIONS',
'PATCH',
'POST',
'PUT',
'VIEW',
)
CORS_ALLOW_HEADERS = (
'XMLHttpRequest',
'X_FILENAME',
'accept-encoding',
'authorization',
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with'
)
"""project URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('app/', include('app.urls')),
path('admin/', admin.site.urls),
]
"""
WSGI config for project project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
application = get_wsgi_application()
asgiref==3.2.10
Django==3.0.8
django-cors-headers==3.4.0
djangorestframework==3.11.0
pycryptodome==3.9.8
pytz==2020.1
sqlparse==0.3.1
##
# You should look at the following URL's in order to grasp a solid understanding
# of Nginx configuration files in order to fully unleash the power of Nginx.
# https://www.nginx.com/resources/wiki/start/
# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/
# https://wiki.debian.org/Nginx/DirectoryStructure
#
# In most cases, administrators will remove this file from sites-enabled/ and
# leave it as reference inside of sites-available where it will continue to be
# updated by the nginx packaging team.
#
# This file will automatically load configuration files provided by other
# applications, such as Drupal or Wordpress. These applications will be made
# available underneath a path with that package name, such as /drupal8.
#
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
##
# Default server configuration
#
# server {
#
# listen 80;
# listen [::]:80 default_server;
# SSL configuration
#
# listen 443 ssl default_server;
# listen [::]:443 ssl default_server;
#
# Note: You should disable gzip for SSL traffic.
# See: https://bugs.debian.org/773332
#
# Read up on ssl_ciphers to ensure a secure configuration.
# See: https://bugs.debian.org/765782
#
# Self signed certs generated by the ssl-cert package
# Don't use them in a production server!
#
# include snippets/snakeoil.conf;
# root /root/webapp;
# Add index.php to the list if you are using PHP
# index index.html index.htm index.nginx-debian.html;
# server_name note02.xyz;
#
# rewrite ^(.*)$ https://$server_name$1 permanent;
#
# location / {
# include /etc/nginx/uwsgi_params;
# uwsgi_pass 127.0.0.1:5000;
# }
# location / {
# # First attempt to serve request as file, then
# # as directory, then fall back to displaying a 404.
# try_files $uri $uri/ =404;
# }
# pass PHP scripts to FastCGI server
#
#location ~ \.php$ {
# include snippets/fastcgi-php.conf;
#
# # With php-fpm (or other unix sockets):
# fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
# # With php-cgi (or other tcp sockets):
# fastcgi_pass 127.0.0.1:9000;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
# }
# server {
#
# listen 443;
# server_name note02.xyz;
#
# ssl on;
#
# ssl_certificate /root/webapp/CRT/Nginx/1_note02.xyz_bundle.crt;
# ssl_certificate_key /root/webapp/CRT/Nginx/2_note02.xyz.key;
# ssl_session_timeout 5m;
# ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA;
# ssl_session_cache shared:SSL:50m;
# ssl_prefer_server_ciphers on;
#
#
# charset utf-8;
#
# root /root/webapp;
#
# client_max_body_size 75M;
#
# location / {
# include /etc/nginx/uwsgi_params;
# uwsgi_pass 127.0.0.1:5000;
# }
# }
# Virtual Host configuration for example.com
#
# You can move that to a different file under sites-available/ and symlink that
# to sites-enabled/ to enable it.
#
#server {
# listen 80;
# listen [::]:80;
#
# server_name example.com;
#
# root /var/www/example.com;
# index index.html;
#
# location / {
# try_files $uri $uri/ =404;
# }
#}
server {
include /etc/nginx/mime.types;
default_type application/octet-stream;
listen 443 ssl;
server_name note02.xyz;
charset utf-8;
ssl_certificate /root/webapp/CRT/Nginx/1_note02.xyz_bundle.crt;
ssl_certificate_key /root/webapp/CRT/Nginx/2_note02.xyz.key;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA;
ssl_session_cache shared:SSL:50m;
ssl_prefer_server_ciphers on;
location / {
root /opt/VueBlog/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
location /static {
alias /opt/VueBlog/dist/static;
}
location /app {
client_max_body_size 50m;
include /etc/nginx/uwsgi_params;
uwsgi_pass 172.30.0.9:8000;
}
}
server {
listen 80;
server_name note02.xyz;
rewrite ^(.*)$ https://$server_name$1 permanent;
}
require('./check-versions')()
process.env.NODE_ENV = 'production'
var ora = require('ora')
var rm = require('rimraf')
var path = require('path')
var chalk = require('chalk')
var webpack = require('webpack')
var config = require('../config')
var webpackConfig = require('./webpack.prod.conf')
var spinner = ora('building for production...')
spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, function (err, stats) {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n')
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})
var chalk = require('chalk')
var semver = require('semver')
var packageConfig = require('../package.json')
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
var versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
},
{
name: 'npm',
currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm
}
]
module.exports = function () {
var warnings = []
for (var i = 0; i < versionRequirements.length; i++) {
var mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
)
}
}
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log()
for (var i = 0; i < warnings.length; i++) {
var warning = warnings[i]
console.log(' ' + warning)
}
console.log()
process.exit(1)
}
}
/* eslint-disable */
require('eventsource-polyfill')
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
hotClient.subscribe(function (event) {
if (event.action === 'reload') {
window.location.reload()
}
})
require('./check-versions')()
var config = require('../config')
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}
var opn = require('opn')
var path = require('path')
var express = require('express')
var webpack = require('webpack')
var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = require('./webpack.dev.conf')
// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port
// automatically open browser, if not set will be false
var autoOpenBrowser = !!config.dev.autoOpenBrowser
// Define HTTP proxies to your custom API backend
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable
var app = express()
var compiler = webpack(webpackConfig)
var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
quiet: true
})
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
log: () => {}
})
// force page reload when html-webpack-plugin template changes
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleware.publish({ action: 'reload' })
cb()
})
})
// proxy api requests
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(options.filter || context, options))
})
// handle fallback for HTML5 history API
app.use(require('connect-history-api-fallback')())
// serve webpack bundle output
app.use(devMiddleware)
// enable hot-reload and state-preserving
// compilation error display
app.use(hotMiddleware)
// serve pure static assets
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))
var uri = 'http://localhost:' + port
devMiddleware.waitUntilValid(function () {
console.log('> Listening at ' + uri + '\n')
})
module.exports = app.listen(port, function (err) {
if (err) {
console.log(err)
return
}
// when env is testing, don't need open it
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
opn(uri)
}
})
var path = require('path')
var config = require('../config')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
exports.assetsPath = function (_path) {
var assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
var cssLoader = {
loader: 'css-loader',
options: {
minimize: process.env.NODE_ENV === 'production',
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
var loaders = [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// http://vuejs.github.io/vue-loader/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
var output = []
var loaders = exports.cssLoaders(options)
for (var extension in loaders) {
var loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
var utils = require('./utils')
var config = require('../config')
var isProduction = process.env.NODE_ENV === 'production'
module.exports = {
loaders: utils.cssLoaders({
sourceMap: isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap,
extract: isProduction
})
}
var path = require('path')
var utils = require('./utils')
var config = require('../config')
var vueLoaderConfig = require('./vue-loader.conf')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
module: {
rules: [
{
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: "pre",
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter')
}
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
query: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
query: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
}
}
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
// add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})
module.exports = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
},
// cheap-module-eval-source-map is faster for development
devtool: '#cheap-module-eval-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': config.dev.env
}),
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
new FriendlyErrorsPlugin()
]
})
var path = require('path')
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var CopyWebpackPlugin = require('copy-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
var env = config.build.env
var webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: false
})
},
devtool: config.build.productionSourceMap ? '#source-map' : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
sourceMap: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css')
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin(),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
if (config.build.productionGzip) {
var CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
if (config.build.bundleAnalyzerReport) {
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig
var merge = require('webpack-merge')
var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})
// see http://vuejs-templates.github.io/webpack for documentation.
var path = require('path')
module.exports = {
build: {
env: require('./prod.env'),
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
productionSourceMap: true,
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
},
dev: {
env: require('./dev.env'),
port: 8082,
autoOpenBrowser: true,
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {
'/api': {
target: 'http://localhost:3003',
changeOrigin: true,
pathRewrite: {
'^/api': '/api'
}
}
},
// CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README
// (https://github.com/webpack/css-loader#sourcemaps)
// In our experience, they generally work as expected,
// just be aware of this issue when enabling this option.
cssSourceMap: false
},
jwt: {
cert: '123'
}
}
module.exports = {
NODE_ENV: '"production"'
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>vue-blog</title>
<link rel='shortcut icon' type='image/x-icon' href='static/favicon.ico'/>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
{
"name": "vueblog",
"version": "1.0.0",
"description": "A Vue.js project",
"author": "FatDong1 <xuhaodong66@foxmail.com>",
"private": true,
"scripts": {
"start": "cross-env NODE_ENV=development supervisor -i src server/app.js",
"dev": "cross-env NODE_ENV=development node build/dev-server.js",
"build": "cross-env NODE_ENV=production node build/build.js",
"lint": "eslint --ext .js,.vue src"
},
"dependencies": {
"body-parser": "^1.17.1",
"csprng": "^0.1.2",
"express": "^4.15.2",
"highlight.js": "^9.10.0",
"jsencrypt": "^3.0.0-rc.1",
"jsonwebtoken": "^7.3.0",
"marked": "^0.3.6",
"mavon-editor": "^2.9.0",
"mongoose": "^4.9.1",
"nodemailer": "^4.0.0",
"sha1": "^1.1.1",
"smooth-scroll": "github:cferdinandi/smooth-scroll",
"vue": "^2.2.2",
"vue-router": "^2.2.0"
},
"devDependencies": {
"autoprefixer": "^6.7.2",
"babel-core": "^6.22.1",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.10",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-env": "^1.2.1",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"chalk": "^1.1.3",
"connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^4.0.1",
"cross-env": "^3.2.4",
"css-loader": "^0.26.1",
"eslint": "^3.14.1",
"eslint-config-standard": "^6.2.1",
"eslint-friendly-formatter": "^2.0.7",
"eslint-loader": "^1.6.1",
"eslint-plugin-html": "^2.0.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^2.0.1",
"eventsource-polyfill": "^0.9.6",
"extract-text-webpack-plugin": "^2.0.0",
"file-loader": "^0.10.0",
"friendly-errors-webpack-plugin": "^1.1.3",
"function-bind": "^1.1.0",
"html-webpack-plugin": "^2.28.0",
"http-proxy-middleware": "^0.17.3",
"node-sass": "^4.5.0",
"opn": "^4.0.2",
"optimize-css-assets-webpack-plugin": "^1.3.0",
"ora": "^1.1.0",
"rimraf": "^2.6.0",
"sass-loader": "^6.0.3",
"semver": "^5.3.0",
"supervisor": "^0.12.0",
"url-loader": "^0.5.7",
"vue-loader": "^11.1.4",
"vue-resource": "^1.2.1",
"vue-style-loader": "^2.0.0",
"vue-template-compiler": "^2.2.4",
"vuex": "^2.2.1",
"webpack": "^2.2.1",
"webpack-bundle-analyzer": "^2.2.1",
"webpack-dev-middleware": "^1.10.0",
"webpack-hot-middleware": "^2.16.1",
"webpack-merge": "^2.6.1"
},
"engines": {
"node": ">= 4.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
<template>
<div id="app">
<fire-canvas class="fire"></fire-canvas>
<router-view></router-view>
<transition enter-active-class="animated fadeIn" leave-active-class="animated fadeOut">
<dialog-box v-if="dialog.show"></dialog-box>
</transition>
</div>
</template>
<script>
import FireCanvas from './components/share/FireCanvas'
import spinner from './components/share/spinner'
import DialogBox from './components/share/DialogBox'
import {mapState} from 'vuex'
export default {
mounted () {
document.addEventListener('visibilitychange', this.changeTitle, false)
},
components: {
FireCanvas,
spinner,
DialogBox
},
computed: {
...mapState(['isLoading', 'dialog'])
},
methods: {
changeTitle () { // 切换标签页后,改变title
if (document.hidden) {
document.title = '去吧,皮卡丘!'
} else {
document.title = '欢迎回来'
}
}
}
}
</script>
<style>
#app {
margin: 0;
padding: 0;
overflow: hidden;
min-height: 100%;
width: 100%;
}
@media screen and (max-width: 440px) {
.fire {
display: none;
}
#app {
background: #000;
}
}
</style>
此差异已折叠。
* {
margin: 0;
padding: 0;
}
html {
height: 100%;
font-size: 16px;
}
body {
height: 100%;
font-family: "Microsoft YaHei", "微软雅黑", STXihei, "华文细黑", serif;
}
a {
text-decoration: none;
color: #55ffff;
}
button {
height: 2.5rem;
border: none;
outline: none;
color: #00193a;
background: rgb(129, 216, 208);
margin-top: 1.875rem;
padding-left: 0.4375rem;
cursor: pointer;
border-radius: 0.25rem;
transition: 0.5s;
font-size: 0.875rem;
span {
display: inline-block;
height: 2.5rem;
line-height: 2.5rem;
position: relative;
transition: 0.5s;
}
&:hover span {
padding-right: 0.625rem;
transition: 0.5s;
}
span:after {
content: '\00bb';
opacity: 0;
transition: 0.5s;
font-size: 1.25rem;
position: relative;
right: 0;
}
&:hover span:after{
opacity: 1;
right: -0.5rem;
}
}
.hashTitle {
color: white;
}
h2, h3, h4, h5, h6 {
padding-top: 0.8rem;
color: #F0F0F0;
}
@media screen and (max-width: 400px) {
html {font-size: 18px;}
}
@media screen and (max-width: 360px) {
html {font-size: 17px;}
}
@media screen and (max-width: 320px) {
html {font-size: 16px;}
}
.lang-js {
padding: 0;
}
.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: darkturquoise;
}
.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: greenyellow;
}
.hljs-title,
.hljs-section {
color: #55ffff;
}
.hljs-keyword,
.hljs-selector-tag {
color: rgb(207, 32, 255);
}
.hljs {
display: block;
overflow-x: auto;
background: white;
color: #4d4d4c;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
\ No newline at end of file
@font-face {font-family: "iconfont";
src: url('//at.alicdn.com/t/font_hvlvsgwsxwm6lxr.eot?t=1492504246115'); /* IE9*/
src: url('//at.alicdn.com/t/font_hvlvsgwsxwm6lxr.eot?t=1492504246115#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('//at.alicdn.com/t/font_hvlvsgwsxwm6lxr.woff?t=1492504246115') format('woff'), /* chrome, firefox */
url('//at.alicdn.com/t/font_hvlvsgwsxwm6lxr.ttf?t=1492504246115') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
url('//at.alicdn.com/t/font_hvlvsgwsxwm6lxr.svg?t=1492504246115#iconfont') format('svg'); /* iOS 4.1- */
}
.iconfont {
font-family:"iconfont" !important;
font-size:16px;
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-zhanghu:before { content: "\e600"; }
.icon-fanhui:before { content: "\e607"; }
.icon-shanchu:before { content: "\e636"; }
.icon-zhuye:before { content: "\e610"; }
.icon-label:before { content: "\e61b"; }
.icon-iconsf:before { content: "\e602"; }
.icon-like:before { content: "\e609"; }
.icon-zengjia:before { content: "\e6a7"; }
.icon-shanchu1:before { content: "\e620"; }
.icon-github:before { content: "\e69f"; }
.icon-shijian:before { content: "\e626"; }
.icon-icon13:before { content: "\e627"; }
.icon-draft:before { content: "\e679"; }
.icon-biji-copy:before { content: "\e601"; }
.icon-mulu:before { content: "\e638"; }
.icon-cion15:before { content: "\e60f"; }
.icon-huojian:before { content: "\e617"; }
.icon-icon69:before { content: "\e786"; }
.icon-yuechi:before { content: "\e676"; }
.icon-search:before { content: "\e603"; }
.icon-left:before { content: "\e604"; }
.icon-right:before { content: "\e605"; }
.icon-huifu:before { content: "\e61c"; }
.icon-out:before { content: "\e611"; }
@import './common.scss';
@import './marked.scss';
@import '../../assets/css/animate.min.css';
@import 'highlight';
@import '../../assets/css/icon.scss';
@import './normalize.css'
blockquote {
margin-top: 1.25rem;
border-left: 0.1875rem solid rgb(129, 216, 208);
padding: 0 0.625rem;
background: rgba(204, 204, 204, 0.5);
}
ul, ol{
margin-top: 0;
margin-bottom: -2rem;
padding-left: 1.25rem;
line-height: 1.125rem;
}
img:not(.bgImage) {
border: 1px solid #ccc;
}
code {
color: rgb(147, 240, 216);
background: rgb(71, 73,73);
font-family: Consolas;
padding: 0 0.3125rem;
border-radius: 0.3125rem;
overflow-x: auto;
}
hr {
margin: 0.625rem 0 0;
}
pre {
padding: 0.625rem 0.625rem;
background: rgb(71, 73,73);
border-radius: 0.3125rem;
overflow-x: auto;
}
h1, h2, h3, h4, h5, h6 {
margin-bottom: 0.625rem;
}
\ No newline at end of file
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in
* IE on Windows Phone and in iOS.
*/
html {
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers (opinionated).
*/
body {
margin: 0;
}
/**
* Add the correct display in IE 9-.
*/
article,
aside,
footer,
header,
nav,
section {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* Add the correct display in IE 9-.
* 1. Add the correct display in IE.
*/
figcaption,
figure,
main { /* 1 */
display: block;
}
/**
* Add the correct margin in IE 8.
*/
figure {
margin: 1em 40px;
}
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
/**
* 1. Remove the bottom border in Chrome 57- and Firefox 39-.
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
*/
b,
strong {
font-weight: inherit;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-size: 1em; /* 2 */
}
/**
* Add the correct font style in Android 4.3-.
*/
dfn {
font-style: italic;
}
/**
* Add the correct background and color in IE 9-.
*/
mark {
background-color: #ff0;
color: #000;
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
audio,
video {
display: inline-block;
}
/**
* Add the correct display in iOS 4-7.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Remove the border on images inside links in IE 10-.
*/
img {
border-style: none;
}
/**
* Hide the overflow in IE.
*/
svg:not(:root) {
overflow: hidden;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers (opinionated).
* 2. Remove the margin in Firefox and Safari.
*/
input,
optgroup,
select,
textarea {
font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
*/
button,
html [type="button"], /* 1 */
[type="reset"],
[type="submit"] {
-webkit-appearance: button; /* 2 */
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* 1. Add the correct display in IE 9-.
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Remove the default vertical scrollbar in IE.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in IE 9-.
* 1. Add the correct display in Edge, IE, and Firefox.
*/
details, /* 1 */
menu {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Scripting
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
canvas {
display: inline-block;
}
/**
* Add the correct display in IE.
*/
template {
display: none;
}
/* Hidden
========================================================================== */
/**
* Add the correct display in IE 10-.
*/
[hidden] {
display: none;
}
\ No newline at end of file
/*! highlight.js v9.10.0 | BSD3 License | git.io/hljslicense */
!function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/[&<>]/gm,function(e){return j[e]})}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return w(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||w(i))return i}function o(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset<r[0].offset?e:r:"start"===r[0].event?e:r:e.length?e:r}function o(e){function r(e){return" "+e.nodeName+'="'+n(e.value)+'"'}l+="<"+t(e)+E.map.call(e.attributes,r).join("")+">"}function u(e){l+="</"+t(e)+">"}function c(e){("start"===e.event?o:u)(e.node)}for(var s=0,l="",f=[];e.length||r.length;){var g=i();if(l+=n(a.substring(s,g[0].offset)),s=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===s);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return l+n(a.substr(s))}function s(e){return e.v&&!e.cached_variants&&(e.cached_variants=e.v.map(function(n){return o(e,{v:null},n)})),e.cached_variants||e.eW&&[o(e)]||[e]}function l(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},u=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");o[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?u("keyword",a.k):x(a.k).forEach(function(e){u(e,a.k[e])}),a.k=o}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]),a.c=Array.prototype.concat.apply([],a.c.map(function(e){return s("self"===e?a:e)})),a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var c=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=c.length?t(c.join("|"),!0):{exec:function(){return null}}}}r(e)}function f(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function s(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function p(e,n,t,r){var a=r?"":I.classPrefix,i='<span class="'+a,o=t?"":C;return i+=e+'">',i+n+o}function h(){var e,t,r,a;if(!E.k)return n(k);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(k);r;)a+=n(k.substring(t,r.index)),e=s(E,r),e?(B+=e[1],a+=p(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(k);return a+n(k.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!L[E.sL])return n(k);var t=e?f(E.sL,k,!0,x[E.sL]):g(k,E.sL.length?E.sL:void 0);return E.r>0&&(B+=t.r),e&&(x[E.sL]=t.top),p(t.language,t.value,!1,!0)}function b(){y+=null!=E.sL?d():h(),k=""}function v(e){y+=e.cN?p(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(k+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?k+=n:(t.eB&&(k+=n),b(),t.rB||t.eB||(k=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?k+=n:(a.rE||a.eE||(k+=n),b(),a.eE&&(k=n));do E.cN&&(y+=C),E.skip||(B+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"<unnamed>")+'"');return k+=n,n.length||1}var N=w(e);if(!N)throw new Error('Unknown language: "'+e+'"');l(N);var R,E=i||N,x={},y="";for(R=E;R!==N;R=R.parent)R.cN&&(y=p(R.cN,"",!0)+y);var k="",B=0;try{for(var M,j,O=0;;){if(E.t.lastIndex=O,M=E.t.exec(t),!M)break;j=m(t.substring(O,M.index),M[0]),O=M.index+j}for(m(t.substr(O)),R=E;R.parent;R=R.parent)R.cN&&(y+=C);return{r:B,value:y,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function g(e,t){t=t||I.languages||x(L);var r={r:0,value:n(e)},a=r;return t.filter(w).forEach(function(n){var t=f(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function p(e){return I.tabReplace||I.useBR?e.replace(M,function(e,n){return I.useBR&&"\n"===e?"<br>":I.tabReplace?n.replace(/\t/g,I.tabReplace):""}):e}function h(e,n,t){var r=n?y[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function d(e){var n,t,r,o,s,l=i(e);a(l)||(I.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(/<br[ \/]*>/g,"\n")):n=e,s=n.textContent,r=l?f(l,s,!0):g(s),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),s)),r.value=p(r.value),e.innerHTML=r.value,e.className=h(e.className,l,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function b(e){I=o(I,e)}function v(){if(!v.called){v.called=!0;var e=document.querySelectorAll("pre code");E.forEach.call(e,d)}}function m(){addEventListener("DOMContentLoaded",v,!1),addEventListener("load",v,!1)}function N(n,t){var r=L[n]=t(e);r.aliases&&r.aliases.forEach(function(e){y[e]=n})}function R(){return x(L)}function w(e){return e=(e||"").toLowerCase(),L[e]||L[y[e]]}var E=[],x=Object.keys,L={},y={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="</span>",I={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},j={"&":"&amp;","<":"&lt;",">":"&gt;"};return e.highlight=f,e.highlightAuto=g,e.fixMarkup=p,e.highlightBlock=d,e.configure=b,e.initHighlighting=v,e.initHighlightingOnLoad=m,e.registerLanguage=N,e.listLanguages=R,e.getLanguage=w,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("xml",function(s){var e="[A-Za-z0-9\\._:-]+",t={eW:!0,i:/</,r:0,c:[{cN:"attr",b:e,r:0},{b:/=\s*/,r:0,c:[{cN:"string",endsParent:!0,v:[{b:/"/,e:/"/},{b:/'/,e:/'/},{b:/[^\s"'=<>`]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist"],cI:!0,c:[{cN:"meta",b:"<!DOCTYPE",e:">",r:10,c:[{b:"\\[",e:"\\]"}]},s.C("<!--","-->",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{b:/<\?(php)?/,e:/\?>/,sL:"php",c:[{b:"/\\*",e:"\\*/",skip:!0}]},{cN:"tag",b:"<style(?=\\s|>|$)",e:">",k:{name:"style"},c:[t],starts:{e:"</style>",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"<script(?=\\s|>|$)",e:">",k:{name:"script"},c:[t],starts:{e:"</script>",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"meta",v:[{b:/<\?xml/,e:/\?>/,r:10},{b:/<\?\w+/,e:/\?>/}]},{cN:"tag",b:"</?",e:"/?>",c:[{cN:"name",b:/[^\/><\s]+/,r:0},t]}]}});hljs.registerLanguage("markdown",function(e){return{aliases:["md","mkdown","mkd"],c:[{cN:"section",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"quote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"^```w*s*$",e:"^```s*$"},{b:"`.+?`"},{b:"^( {4}| )",e:"$",r:0}]},{b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"string",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"symbol",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:/^\[[^\n]+\]:/,rB:!0,c:[{cN:"symbol",b:/\[/,e:/\]/,eB:!0,eE:!0},{cN:"link",b:/:\s*/,e:/$/,eB:!0}]}]}});hljs.registerLanguage("coffeescript",function(e){var c={keyword:"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super yield import export from as default await then unless until loop of by when and or is isnt not",literal:"true false null undefined yes no on off",built_in:"npm require console print module global window document"},n="[A-Za-z$_][0-9A-Za-z$_]*",r={cN:"subst",b:/#\{/,e:/}/,k:c},i=[e.BNM,e.inherit(e.CNM,{starts:{e:"(\\s*/)?",r:0}}),{cN:"string",v:[{b:/'''/,e:/'''/,c:[e.BE]},{b:/'/,e:/'/,c:[e.BE]},{b:/"""/,e:/"""/,c:[e.BE,r]},{b:/"/,e:/"/,c:[e.BE,r]}]},{cN:"regexp",v:[{b:"///",e:"///",c:[r,e.HCM]},{b:"//[gim]*",r:0},{b:/\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/}]},{b:"@"+n},{sL:"javascript",eB:!0,eE:!0,v:[{b:"```",e:"```"},{b:"`",e:"`"}]}];r.c=i;var s=e.inherit(e.TM,{b:n}),t="(\\(.*\\))?\\s*\\B[-=]>",o={cN:"params",b:"\\([^\\(]",rB:!0,c:[{b:/\(/,e:/\)/,k:c,c:["self"].concat(i)}]};return{aliases:["coffee","cson","iced"],k:c,i:/\/\*/,c:i.concat([e.C("###","###"),e.HCM,{cN:"function",b:"^\\s*"+n+"\\s*=\\s*"+t,e:"[-=]>",rB:!0,c:[s,o]},{b:/[:\(,=]\s*/,r:0,c:[{cN:"function",b:t,e:"[-=]>",rB:!0,c:[o]}]},{cN:"class",bK:"class",e:"$",i:/[:="\[\]]/,c:[{bK:"extends",eW:!0,i:/[:="\[\]]/,c:[s]},s]},{b:n+":",e:":",rB:!0,rE:!0,r:0}])}});hljs.registerLanguage("javascript",function(e){var r="[A-Za-z$_][0-9A-Za-z$_]*",t={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},a={cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},n={cN:"subst",b:"\\$\\{",e:"\\}",k:t,c:[]},c={cN:"string",b:"`",e:"`",c:[e.BE,n]};n.c=[e.ASM,e.QSM,c,a,e.RM];var s=n.c.concat([e.CBCM,e.CLCM]);return{aliases:["js","jsx"],k:t,c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,c,e.CLCM,e.CBCM,a,{b:/[{,]\s*/,r:0,c:[{b:r+"\\s*:",rB:!0,r:0,c:[{cN:"attr",b:r,r:0}]}]},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+r+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:r},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:t,c:s}]}]},{b:/</,e:/(\/\w+|\w+\/)>/,sL:"xml",c:[{b:/<\w+\s*\/>/,skip:!0},{b:/<\w+/,e:/(\/\w+|\w+\/)>/,skip:!0,c:[{b:/<\w+\s*\/>/,skip:!0},"self"]}]}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:r}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:s}],i:/\[|%/},{b:/\$[(.]/},e.METHOD_GUARD,{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#(?!!)/}});hljs.registerLanguage("css",function(e){var c="[a-zA-Z-][a-zA-Z0-9_-]*",t={b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\w-]+\(/,rB:!0,c:[{cN:"built_in",b:/[\w-]+/},{b:/\(/,e:/\)/,c:[e.ASM,e.QSM]}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",i:/:/,c:[{cN:"keyword",b:/\w+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:"selector-tag",b:c,r:0},{b:"{",e:"}",i:/\S/,c:[e.CBCM,t]}]}});hljs.registerLanguage("json",function(e){var i={literal:"true false null"},n=[e.QSM,e.CNM],r={e:",",eW:!0,eE:!0,c:n,k:i},t={b:"{",e:"}",c:[{cN:"attr",b:/"/,e:/"/,c:[e.BE],i:"\\n"},e.inherit(r,{b:/:/})],i:"\\S"},c={b:"\\[",e:"\\]",c:[e.inherit(r)],i:"\\S"};return n.splice(n.length,0,t,c),{c:n,k:i,i:"\\S"}});hljs.registerLanguage("python",function(e){var r={keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},b={cN:"meta",b:/^(>>>|\.\.\.) /},c={cN:"subst",b:/\{/,e:/\}/,k:r,i:/#/},a={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[b],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[b],r:10},{b:/(fr|rf|f)'''/,e:/'''/,c:[b,c]},{b:/(fr|rf|f)"""/,e:/"""/,c:[b,c]},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},{b:/(fr|rf|f)'/,e:/'/,c:[c]},{b:/(fr|rf|f)"/,e:/"/,c:[c]},e.ASM,e.QSM]},s={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},i={cN:"params",b:/\(/,e:/\)/,c:["self",b,s,a]};return c.c=[a,s,b],{aliases:["py","gyp"],k:r,i:/(<\/|->|\?)|=>/,c:[b,s,a,e.HCM,{v:[{cN:"function",bK:"def"},{cN:"class",bK:"class"}],e:/:/,i:/[${=;\n,]/,c:[e.UTM,i,{b:/->/,eW:!0,k:"None"}]},{cN:"meta",b:/^[\t ]*@/,e:/$/},{b:/\b(print|exec)\(/}]}});
\ No newline at end of file
<template>
<div class="account">
<p class="icon"><i class="iconfont icon-icon69"></i></p>
<div>
<input type="text" placeholder="请输入新的用户名" v-model="name" />
<i class="iconfont icon-zhanghu"></i>
</div>
<div>
<input type="password" placeholder="请输入你的密码" v-model="password"/>
<i class="iconfont icon-yuechi"></i>
</div>
<div>
<input type="password" placeholder="请再次输入你的密码" v-model="repassword"/>
<i class="iconfont icon-yuechi"></i>
</div>
<transition name="fade" enter-active-class="animated zoomInLeft"><p v-if="show">{{msg}}</p></transition>
<button @click="reset"><span>确认修改</span></button>
</div>
</template>
<script>
import {_debounce} from '../../lib/utils.js'
import {mapActions, mapState} from 'vuex'
export default {
data () {
return {
msg: 'haha',
name: '',
password: '',
repassword: '',
show: false
}
},
computed: {
...mapState(['user'])
},
methods: {
...mapActions(['resetUser']),
checkName () {
if (this.name.length > 5) {
this.msg = '请输入合适长度的用户名'
}
},
reset () {
if (this.repassword === this.password) {
this.resetUser({id: this.user.id, name: this.name, password: this.password})
}
}
},
watch: {
name: _debounce(function () {
if (this.name.length > 5) {
this.msg = '请输入合适长度的用户名'
this.show = true
} else {
this.msg = ''
this.show = false
}
}, 500),
password: _debounce(function () {
if (this.password.length < 6) {
this.msg = '请输入长度大于6位的密码'
this.show = true
} else {
this.msg = ''
this.show = false
}
}, 500),
repassword: _debounce(function () {
if (this.repassword !== this.password) {
this.msg = '请输入相同的密码'
this.show = true
} else {
this.msg = ''
this.show = false
}
}, 500)
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.account {
position: relative;
margin: 5rem auto 2rem;
height: 25rem;
p.icon {
width: calc(100% - 6.25rem);
text-align: center;
margin: 0 auto 6.25rem;
.icon-icon69 {
font-size: 3.75rem;
color: rgb(129, 216, 208);
}
}
div {
width: 18.75rem;
margin: 0 auto;
position: relative;
i {
color: rgb(129, 216, 208);
font-size: 1.875rem;
display: block;
position: absolute;
top: 0;
left: 1.25rem;
transition: 0.5s;
}
}
input {
width: 12.5rem;
height: 1.875rem;
display: block;
margin-top: 2.5rem;
margin-bottom: 1.25rem;
margin-left: 4.375rem;
outline: none;
border: none;
border-bottom: 0.1875rem solid rgb(129, 216, 208);
background: transparent;
color: #fff;
font-size: 1rem;
padding-left: 0.625rem;
&:focus + i {
color: darkturquoise;
}
}
button {
width: 12.5rem;
position: absolute;
bottom: 0;
left: 50%;
margin-left: -5rem;
background: rgb(129, 216, 208);
}
}
p{
text-align: center;
height: 1rem;
color: rgb(129, 216, 208);
}
.fade-leave-active {
transition: opacity .5s
}
.fade-enter, .fade-leave-active {
opacity: 0
}
</style>
<template>
<div class="wrapper">
<div class="statusLine">
<p class="left">
<router-link :to="{name: 'home'}" class="iconfont icon-zhuye" tag="i"></router-link>
<span>{{time}}好,{{name}}</span>
</p>
<p class="right" @click="imageUpload">
<i class="iconfont icon-out"></i>
<span>图片外链</span>
</p>
<p class="right" @click="logout">
<i class="iconfont icon-out"></i>
<span>登出</span>
</p>
</div>
<nav>
<ul>
<router-link :to="{name: 'posts'}" tag="li"><i class="iconfont icon-biji-copy"></i>文章</router-link>
<router-link :to="{name: 'search'}" tag="li"><i class="iconfont icon-search"></i>搜索</router-link>
<router-link :to="{name: 'drafts'}" tag="li"><i class="iconfont icon-draft"></i>草稿</router-link>
<router-link :to="{name: 'account'}" tag="li"><i class="iconfont icon-zhanghu"></i>账户</router-link>
</ul>
</nav>
<div id="imageLink" v-show="show">
<img-inputer v-model="file"
auto-upload="true"
theme="light"
size="large"
max-size=51200
action="/app/image/"
:headers="headers"
:extra-data="args"
:on-success="showImageUrl"/>
<p><span>{{ imageUrl }}</span></p>
<button @click="show = false">关闭</button>
</div>
<transition mode="out-in" enter-active-class="animated fadeIn" leave-active-class="animated fadeOut">
<router-view class="content"></router-view>
</transition>
</div>
</template>
<script>
import {mapMutations, mapState} from 'vuex'
export default {
data () {
return {
file: {},
show: false,
headers: {
'X-CSRFToken': ''
},
args: {
token: this.$store.state.token
},
imageUrl: '',
baseUrl: location.protocol + '//' + location.host + '/app/image/'
}
},
computed: {
...mapState(['user']),
time () {
const hours = new Date().getHours()
if (hours > 5 && hours < 12) {
return '早上'
} else if (hours > 12 && hours < 19) {
return '下午'
} else if (hours === 12) {
return '中午'
} else {
return '晚上'
}
},
name () {
return localStorage.userName
}
},
methods: {
...mapMutations(['unset_user', 'getCookie']),
imageUpload () {
this.show = !this.show
console.log(this.show)
this.getCookie()
this.headers['X-CSRFToken'] = this.$store.state.csrftoken
},
logout () {
this.unset_user()
this.$router.go({name: 'login'})
},
showImageUrl: function (res, file) {
console.log(res)
if (res.status === 0) {
this.imageUrl = this.baseUrl + res.filename
}
}
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
@import '../../assets/css/icon.scss';
.wrapper {
.statusLine {
background: transparent;
width: 100%;
height: 3.125rem;
line-height: 3.125rem;
color: #fff;
font-size: 1.125rem;
display: flex;
justify-content: space-between;
p.left {
margin-left: 1.25rem;
i.icon-zhuye {
font-size: 1.875rem;
color: rgb(129, 216, 208);
cursor: pointer;
&:hover {
color: darkturquoise;
}
}
}
p.right {
cursor: pointer;
margin-right: 1.25rem;
color: rgb(129, 216, 208);
i.icon-out {
font-size: 1.25rem;
}
&:hover {
color: rgb(129, 216, 208);
}
}
}
nav {
float: left;
display: inline-block;
position: relative;
color: #fff;
margin-top: 13.75rem;
width: 6.25rem;
ul {
padding-left: 0;
list-style: none;
li {
width: 6.25rem;
height: 2.5rem;
line-height: 2.5rem;
text-align: center;
cursor: pointer;
border-left: 0.1875rem solid transparent;
transition: 1s;
&:hover {
transition: 1s;
padding-left: 1.25rem;
color: darkturquoise;
}
i {
font-size: 1.125rem;
margin-right: 0.625rem;
}
}
li.router-link-active {
border-left: 0.1875rem solid darkturquoise;
background: rgba(204, 204, 204, 0.5);
}
}
}
#imageLink {
position: fixed;
margin: 0;
padding: 0;
top: 50%;
left: 50%;
z-index: 10;
-webkit-transform: translate(-50%, -50%);
-moz-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
-o-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
p {
background: rgb(129, 216, 208);
text-align: center;
}
button {
display: block;
margin: 0.5rem auto;
padding: 0.125rem;
text-align: center;
}
}
.content {
margin-left: 6.25rem;
}
}
@media screen and (max-width: 440px) {
.content {
margin-left: 0 !important;
}
nav {
float: none !important;
margin-top: 1rem !important;
width: 100% !important;
ul {
display: flex;
justify-content: space-between;
flex-wrap: nowrap;
width: 100% !important;
li:hover {
padding-left: 0 !important;
}
}
}
}
</style>
<template>
<table>
<thead>
<tr>
<th>标题</th>
<th>标签</th>
<th>日期</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="article in articles">
<router-link :to="{name: 'editor', query: {aid: article.aid}}" tag="td" class="title">{{article.title}}</router-link>
<td>{{article.tags | toTag}}</td>
<td>{{article.date | toDate}}</td>
<td>
<router-link :to="{name: 'editor', query: {aid: article.aid}}" class="iconfont icon-biji-copy" tag="i"></router-link>
<i class="iconfont icon-shanchu" @click="deleteConfirm(article.aid)"></i>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td @click="getPrePage">上一页</td>
<td colspan="2">{{page}}</td>
<td @click="getNextPage">下一页</td>
</tr>
</tfoot>
</table>
</template>
<script>
import {mapState, mapActions, mapMutations} from 'vuex'
export default {
data () {
return {
page: 1
}
},
computed: {
...mapState(['articles', 'dialog', 'nextPage', 'previousPage'])
},
methods: {
...mapActions(['delArticle']),
...mapMutations(['set_dialog']),
getNextPage () {
if (!this.nextPage) {
alert('已经到最后一页咯')
} else {
this.page++
this.$emit('addPage') // 传递给父组件
}
},
getPrePage () {
if (!(this.page - 1)) {
alert('已经到第一页咯')
} else {
this.page--
this.$emit('dropPage') // 传递给父组件
}
},
deleteConfirm (aid) {
this.set_dialog({
info: '确认删除(⊙o⊙)?',
hasTwoBtn: true,
show: true
})
new Promise((resolve, reject) => {
this.dialog.resolveFn = resolve
this.dialog.rejectFn = reject
}).then(() => {
this.delArticle({aid: aid, page: this.page, route: this.$route})
}).catch((err) => {
console.log(err)
})
}
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
table {
border-left: 0.1875rem solid rgb(129, 216, 208);
border-right: 0.1875rem solid rgb(129, 216, 208);
margin: 0 auto;
text-align: center;
max-height: 25rem;
min-width: 70%;
th, td {
width: 25%;
}
thead, tfoot {
color: darkturquoise;
}
tbody {
color: #ffffff;
}
tr {
height: 2.5rem;
line-height: 1.875rem;
}
i {
font-size: 1.25rem;
margin-right: 0.625rem;
color: rgb(129, 216, 208);
cursor: pointer;
&:hover {
color: #ffc520;
}
}
tfoot tr td:nth-child(1), tfoot tr td:nth-child(3) {
cursor: pointer;
&:hover {
color: #ffc520;
}
}
}
.title {
cursor: pointer;
&:hover {
color: #ffc520;
}
}
</style>
<template>
<div class="tag_div" v-if="show">
<select name="label" id="select-label" v-model="label">
<option class="newInput"
v-for="tag in selectItems"
v-bind:value="tag.id"
:key="tag.id"
@click="seleteLabel">{{ tag.value }}}</option>
</select>
<!-- <input-->
<!-- type="text"-->
<!-- class="newInput"-->
<!-- placeholder="标签"-->
<!-- v-model="tagContent"-->
<!-- onfocus="this.placeholder=''"-->
<!-- @blur="isRepeated"-->
<!-- @keydown.enter="addTag"-->
<!-- />-->
<!-- <i class="iconfont icon-shanchu1" @click="delTag"></i>-->
<!-- <i class="iconfont icon-zengjia" @click="addTag" v-if="index === tags.length - 1"></i>-->
</div>
</template>
<script>
export default {
name: 'Label',
data () {
return {
show: true,
label: 0
}
},
props: {
tags: Array
},
methods: {
selectLabel (e) {
console.log(e.target)
}
},
computed: {
selectItems: {
get () {
return this.tags.slice(1)
}
}
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.tag_div {
position: relative;
display: inline-block;
&:hover i.icon-shanchu1{
opacity: 1;
transition: 1s;
}
.newInput {
border: none;
border-bottom: 0.125rem solid rgb(129, 216, 208);
outline: none;
background: transparent;
color: #ffffff;
margin-bottom: 0.625rem;
margin-right: 0.3125rem;
text-align: center;
width: 6.25rem;
height: 1.875rem;
font-size: 1rem;
}
i.icon-shanchu1 {
position: absolute;
right: 0;
top: -0.3125rem;
font-size: 1rem;
color: #ffc520;
cursor: pointer;
opacity: 0;
transition: 1s;
&:hover {
color: darkturquoise;
font-weight: bolder;
}
}
}
.icon-zengjia {
position: absolute;
right: -1rem;
top: 0.4rem;
font-size: 1rem;
color: rgb(129, 216, 208);
cursor: pointer;
&:hover {
color: darkturquoise;
font-weight: bolder;
}
}
</style>
<template>
<div class="tag_div" v-if="show">
<input
type="text"
class="newInput"
placeholder="标签"
v-model="tagContent"
onfocus="this.placeholder=''"
@blur="isRepeated"
@keydown.enter="addTag"
/>
<i class="iconfont icon-shanchu1" @click="delTag"></i>
<i class="iconfont icon-zengjia" @click="addTag" v-if="index === tags.length - 1"></i>
</div>
</template>
<script>
import {mapMutations} from 'vuex'
export default {
data () {
return {
show: true
}
},
props: {
tags: Array,
index: Number
},
methods: {
...mapMutations(['set_dialog']),
delTag () {
this.tags.splice(this.index, 1) // 通过操作数组来删除标签
console.log(this.tags)
},
addTag () {
this.tags.push('') // 通过操作数组来增加空标签
setTimeout(() => {
document.getElementsByClassName('newInput')[this.index + 1].focus() // 新生成的空标签获得焦点
}, 0)
},
isRepeated () {
let currentIndex = this.index
let currentValue = this.tags[currentIndex]
while (currentIndex) {
if (currentValue.toLowerCase() === this.tags[currentIndex - 1].toLowerCase()) { // 标签去重
this.set_dialog({
info: '傻了吧,标签不能重复',
hasTwoBtn: false,
show: true
})
this.tags.splice(this.index, 1, '')
break
} else {
currentIndex--
}
}
}
},
computed: {
tagContent: {
get () {
return this.tags[this.index] // 获取标签数组元素值
},
set (value) {
this.tags[this.index] = value.trim() // 改变标签数组
}
}
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.tag_div {
position: relative;
display: inline-block;
&:hover i.icon-shanchu1{
opacity: 1;
transition: 1s;
}
.newInput {
border: none;
border-bottom: 0.125rem solid rgb(129, 216, 208);
outline: none;
background: transparent;
color: #ffffff;
margin-bottom: 0.625rem;
margin-right: 0.3125rem;
text-align: center;
width: 6.25rem;
height: 1.875rem;
font-size: 1rem;
}
i.icon-shanchu1 {
position: absolute;
right: 0;
top: -0.3125rem;
font-size: 1rem;
color: #ffc520;
cursor: pointer;
opacity: 0;
transition: 1s;
&:hover {
color: darkturquoise;
font-weight: bolder;
}
}
}
.icon-zengjia {
position: absolute;
right: -1rem;
top: 0.4rem;
font-size: 1rem;
color: rgb(129, 216, 208);
cursor: pointer;
&:hover {
color: darkturquoise;
font-weight: bolder;
}
}
</style>
<template>
<div>
<img-inputer v-model="file" theme="dark" auto-upload size="large"/>
<p><span></span></p>
</div>
</template>
<script>
export default {
name: 'file-upload',
data () {
return {
}
}
}
</script>
<style lang="less" rel="stylesheet/less" scoped>
</style>
<template>
<div class="container">
<p>所有草稿</p>
<article-content v-on:addPage="nextPage" v-on:dropPage="prePage"></article-content>
<router-link
:to="{name: 'editor'}"
class="addPost" tag="button"
><span>添加草稿</span></router-link>
</div>
</template>
<script>
import {mapActions, mapState} from 'vuex'
import ArticleContent from './component/ArticleContent'
export default {
created () {
this.getAllDrafts({page: this.page, limit: 8})
},
data () {
return {
page: 1
}
},
methods: {
...mapActions(['getAllDrafts']),
nextPage () {
this.page++
this.getAllDrafts({page: this.page, limit: 8})
},
prePage () {
if (!(this.page - 1)) {
alert('已经到第一页咯')
} else {
this.page--
this.getAllDrafts({page: this.page, limit: 8})
}
}
},
computed: {
...mapState(['articles'])
},
components: {
ArticleContent
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.container {
padding-top: 0.625rem;
color: #ffffff;
position: relative;
P {
border-bottom: 0.1875rem double rgb(129, 216, 208);
width: 12.5rem;
font-size: 1.875rem;
margin:0 auto 2.5rem;
padding-bottom: 0.625rem;
text-align: center;
}
.addPost {
position: fixed;
bottom: 1.25rem;
right: 1.25rem;
}
}
@media screen and (max-width: 440px) {
.container {
padding-top: 2rem !important;
margin-bottom: 3rem;
.addPost {
position: absolute;
bottom: -3rem;
}
}
}
</style>
<template>
<div class="wrapper">
<div class="title">
<input type="text" placeholder="文章标题" onfocus="this.placeholder=''" onblur="this.placeholder='文章标题'" v-model="title"/>
</div>
<div id="tags">
<ul>
<li class="newInput" v-for="item in items" :value=item.id :key="item.id" @click="selectThis">
{{ item.label }}
</li>
</ul>
</div>
<div class="info">
<div class="right">
<p @click="isMarked = true" :class="{active: isMarked}">原文</p>
<p @click="isMarked = false" :class="{active: !isMarked}">预览</p>
</div>
<textarea
id="editor" spellcheck="false"
v-if="isMarked" v-model="mdContent"
@keydown.once.ctrl.13="saveDraft($route.query.aid)"
v-focus
></textarea>
<div class="preview animated fadeIn" v-if="!isMarked" v-html="mdHtml" tabIndex="1" v-focus></div>
</div>
<button class="publish" @click="saveArticle($route.query.aid)"><span>发布文章</span></button>
<button class="draft" @click="saveDraft($route.query.aid)"><span>存为草稿</span></button>
</div>
</template>
<script>
import marked from 'marked'
import hljs from 'highlight.js'
import { mapActions, mapMutations, mapState } from 'vuex'
import TagInput from './component/TagInput'
marked.setOptions({
highlight: function (code) {
return hljs.highlightAuto(code).value
},
sanitize: true
})
const renderer = new marked.Renderer()
renderer.heading = function (text, level) {
return '<a href="#' + text + '" class="hashTitle" data-scroll><h' + level +
' id="' + text + '">' + text + '</h' + level + '></a>'
}
export default {
data () {
return {
isMarked: true,
firstUpdate: true,
isChange: false,
mdHtml: '',
selected: null,
items: []
}
},
directives: {
focus: {
inserted: (el) => {
el.focus()
}
}
},
created () {
const aid = this.$route.query.aid
this.items = this.tags
this.isSaving_toggle(false)
if (aid) {
return this.getArticle(aid)
}
this.set_article({
content: '',
title: '',
tags: 0
})
},
mounted () {
document.addEventListener('keydown', (e) => {
if (e.keyCode === 40 && e.ctrlKey) { // ctrl + ↓ 切换
this.isMarked = !this.isMarked
}
})
},
updated () {
// 因为切换预览模式,也会触发数据更新,所以不用beforeUpdate, 而用watch监听数据变化
if (this.firstUpdate) {
this.isChange = false
}
this.firstUpdate = false
},
computed: {
...mapState(['article', 'isSaving', 'dialog', 'tags']),
mdContent: {
get () {
this.mdHtml = marked(this.article.content || '', { renderer: renderer })
return this.article.content
},
set (value) { this.update_post_content(value) }
},
title: {
get () { return this.article.title || '' },
set (value) { this.update_post_title(value) }
},
tag () {
let t = this.$store.state.article.tags || null
if (t) {
this.markLabel(t)
}
return t
}
},
methods: {
...mapMutations(['set_article', 'update_post_content', 'update_post_title', 'update_post_tags', 'isSaving_toggle', 'set_dialog']),
...mapActions(['saveArticle', 'getArticle', 'saveDraft']),
selectThis: function (e) {
let t = parseInt(e.target.getAttribute('value'))
this.$store.state.article.tags = t
this.markLabel(this.tag)
},
markLabel: function (labelId) {
let all = document.getElementsByClassName('newInput')
for (let c of all) {
if (parseInt(c.getAttribute('value')) === labelId) {
c.style.background = 'rgb(129, 216, 208)'
c.style.color = '#000'
} else {
c.style.background = 'transparent'
c.style.color = '#ffffff'
}
}
}
},
components: {
TagInput
},
watch: {
title () {
this.isChange = true
},
mdContent () {
this.isChange = true
setTimeout(() => { // 按下tab键后重新获得焦点
document.getElementById('editor').focus()
}, 0)
},
tag () {
this.markLabel(this.tag)
}
},
beforeRouteLeave (to, from, next) {
if (this.isChange && !this.isSaving) {
this.set_dialog({
info: '还没保存,确认离开(⊙o⊙)?',
hasTwoBtn: true,
show: true
})
new Promise((resolve, reject) => {
this.dialog.resolveFn = resolve
this.dialog.rejectFn = reject
}).then(
() => { next() },
() => { next(false) }
).catch((err) => {
console.log(err)
})
} else {
next()
}
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.wrapper {
position: relative;
padding: 0 3.125rem 0;
input {
border: none;
border-bottom: 0.125rem solid rgb(129, 216, 208);
outline: none;
background: transparent;
color: #ffffff;
margin-bottom: 0.625rem;
text-align: center;
}
.title input {
width: 100%;
height: 3.125rem;
font-size: 1.875rem;
}
#tags {
ul {
margin: 0;
padding: 0;
height: 2.5rem;
list-style-type: none;
li {
width: 6.25rem;
height: 1.875rem;
font-size: 1rem;
line-height: 2rem;
border: none;
border-bottom: 0.125rem solid rgb(129, 216, 208);
outline: none;
background: transparent;
color: #ffffff;
margin-bottom: 0.3125rem;
margin-right: 0.3125rem;
text-align: center;
float: left;
}
}
}
.info {
border: 0.125rem solid rgb(129, 216, 208);
position: relative;
height: 29.375rem;
.right {
color: #ffffff;
position: absolute;
right: -0.125rem;
top: -2rem;
border-top: 0.125rem solid rgb(129, 216, 208);
border-right: 0.125rem solid rgb(129, 216, 208);
border-left: 0.125rem solid rgb(129, 216, 208);
p {
float: left;
width: 5rem;
height: 1.875rem;
line-height: 1.875rem;
text-align: center;
cursor: pointer;
}
p:nth-child(1) {
border-right: 0.125rem solid rgb(129, 216, 208);
}
}
#editor {
width: 100%;
height: 29.375rem;
border: none;
background: transparent;
resize: none;
outline: none;
font-size: 1rem;
overflow-y: auto;
white-space: pre-wrap;
font-family: Georgia, "Times New Roman", "Microsoft YaHei", "微软雅黑", STXihei, "华文细黑", serif;
color: #E5E9F2;
}
.preview {
font-family: Georgia, "Times New Roman", "Microsoft YaHei", "微软雅黑", STXihei, "华文细黑", serif;
display: block;
height: 26.875rem;
color: #ffffff;
font-size: 1rem;
overflow-y: auto;
padding: 1.25rem 1.25rem;
white-space: pre-wrap;
word-wrap: break-word;
outline: none;
border: none;
}
}
}
.publish {
width: 6.25rem;
position: fixed;
left: 1rem;
bottom: 32.5rem;
background: rgb(129, 216, 208);
color: #000;
}
.draft {
width: 6.25rem;
position: fixed;
left: 1rem;
bottom: 2.5rem;
background: rgb(129, 216, 208);
color: #000;
}
.active {
background: rgb(129, 216, 208);
color: #111111;
}
@media screen and (max-width: 440px) {
.wrapper {
padding-left: 1rem !important;
padding-right: 1rem !important;
margin-bottom: 5rem;
}
.publish {
position: absolute !important;
bottom: -3rem !important;
left: 1rem !important;
}
.draft {
position: absolute !important;
left: calc(100% - 7.25rem) !important;
bottom: -3rem !important;
}
#tags {
width: 13.7rem !important;
}
.icon-zengjia {
margin-left: -1rem !important;
}
}
</style>
<template>
<div class="wrapper">
<div class="login">
<i class="iconfont icon-icon69"></i>
<div>
<input type="text" placeholder="请输入你的账号" v-model="username"/>
<i class="iconfont icon-zhanghu"></i>
</div>
<div>
<input type="password" placeholder="请输入你的密码" v-model="password" @keydown.enter="confirm(username, password)"/>
<i class="iconfont icon-yuechi"></i>
</div>
<p>{{info}}</p>
<button @click="confirm(username, password)"><span>登录</span></button>
</div>
</div>
</template>
<script>
import {mapActions, mapMutations} from 'vuex'
export default {
data () {
return {
username: '',
password: '',
info: ''
}
},
mounted () {
this.loginBefore().then((res) => {
if (res.status) {
this.info = res.message
} else {
this.info = '正在登录中...'
this.set_user(res)
this.info = '登录成功...'
this.$router.push({name: 'posts'})
}
}).catch((err) => {
console.log(err)
this.info = '登录信息过期, 请重新登录'
})
},
methods: {
...mapActions(['login', 'loginBefore']),
...mapMutations(['set_user']),
confirm (username, password) {
this.login({username: username, password: password}).then((res) => {
this.info = '正在登录中...'
this.set_user(res)
this.info = '登录成功...'
this.$router.push({name: 'posts'})
}).catch((err) => {
console.log(err)
this.info = '登录失败, 请重新登录'
})
}
},
watch: {
name () {
this.info = ''
},
password () {
this.info = ''
}
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.wrapper {
.login {
width: 16.125rem;
height: 20rem;
margin: 7rem auto 0;
text-align: center;
position: relative;
.icon-icon69 {
font-size: 3.75rem;
color: darkturquoise;
}
div {
width: 100%;
margin: 0 auto;
position: relative;
i {
color: darkturquoise;
font-size: 1.875rem;
display: block;
position: absolute;
top: 0;
left: 0;
transition: 0.5s;
}
}
input {
width: 12.5rem;
height: 1.875rem;
display: block;
margin-top: 2.5rem;
margin-bottom: 1.25rem;
margin-left: 3rem;
outline: none;
border: none;
border-bottom: 0.1875rem solid darkturquoise;
background: transparent;
color: #fff;
font-size: 1rem;
padding-left: 0.625rem;
&:focus + i {
color: #ffc520;
}
}
button {
width: 12.5rem;
padding-left: 0;
margin-top: 1.25rem;
position: relative;
left: 1.25rem;
text-align: center;
background: darkturquoise;
}
}
}
p {
color: #ffffff;
width: 100%;
height: 1.25rem;
}
@media screen and (max-width: 440px) {
.login {
margin-top: 2rem !important;
}
}
</style>
<template>
<div class="wrapper">
<p>所有文章</p>
<article-content v-on:addPage="nextPage" v-on:dropPage="prePage"></article-content>
<router-link
:to="{name: 'editor'}"
class="addPost" tag="button"
><span>添加文章</span></router-link>
</div>
</template>
<script>
import {mapActions, mapState} from 'vuex'
import ArticleContent from './component/ArticleContent'
export default {
created () {
this.getAllArticles({page: this.page, value: 0})
this.getAllTags()
},
data () {
return {
page: 1
}
},
methods: {
...mapActions(['getAllArticles', 'getAllTags']),
nextPage () {
this.page++
this.getAllArticles({page: this.page, value: 0})
},
prePage () {
this.page--
this.getAllArticles({page: this.page, value: 0})
}
},
computed: {
...mapState(['articles'])
},
components: {
ArticleContent
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.wrapper {
position: relative;
padding-top: 0.625rem;
/*height: 35.625rem;*/
color: #ffffff;
p {
border-bottom: 0.1875rem double rgb(129, 216, 208);
width: 12.5rem;
font-size: 1.875rem;
margin:0 auto 2.5rem;
padding-bottom: 0.625rem;
text-align: center;
}
.addPost {
position: fixed;
bottom: 1.25rem;
right: 1.25rem;
}
}
@media screen and (max-width: 440px) {
.wrapper {
padding-top: 2rem !important;
margin-bottom: 4rem;
.addPost {
position: absolute !important;
bottom: -3rem !important;
}
}
}
</style>
<template>
<div class="container">
<div class="search">
<input
type="text" v-model="text"
:placeholder="tip" onfocus="this.placeholder=''"
@keydown.enter="searchArticles({key: picked, value: text, page: page})"
/>
<i class="iconfont icon-search" @click="searchArticles({key: picked, value: text, page: page})"></i>
</div>
<div class="searchString">
搜索匹配:
<label for="title"><input type="radio" value="title" id="title" v-model="picked"/>标题</label>
<label for="tags"><input type="radio" value="tags" id="tags" v-model="picked"/>标签</label>
<label for="date"><input type="radio" value="date" id="date" v-model="picked"/>日期</label>
</div>
<p>搜索结果</p>
<article-content v-on:addPage="nextPage" v-on:dropPage="prePage"></article-content>
</div>
</template>
<script>
import ArticleContent from './component/ArticleContent'
import {mapActions, mapMutations} from 'vuex'
export default {
data () {
return {
picked: 'title',
text: '',
page: 1
}
},
created () {
this.set_all_articles({})
},
computed: {
tip () {
if (this.picked === 'title') return '请输入标题的部分内容'
if (this.picked === 'tags') return '请输入完整的标签,多个标签空格隔开'
if (this.picked === 'date') return '检索格式: 2017-04-01'
}
},
methods: {
...mapActions(['searchArticles']),
...mapMutations(['set_all_articles']),
nextPage () {
this.page++
this.searchArticles({key: this.picked, value: this.text, page: this.page})
},
prePage () {
if (!(this.page - 1)) {
alert('已经到第一页咯')
} else {
this.page--
this.searchArticles({key: this.picked, value: this.text, page: this.page})
}
}
},
components: {
ArticleContent
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.container {
text-align: center;
padding-bottom: 2rem;
.search {
margin-right: -2.8125rem;
input {
width: 28.125rem;
margin: 1.25rem auto 0.625rem;
height: 2.8125rem;
font-size: 1.25rem;
border: 0.125rem solid rgb(129, 216, 208);
border-radius: 1.875rem;
outline: none;
text-align: center;
color: #ffffff;
background: transparent;
}
i {
position: relative;
left: -3.4375rem;
top: 0.5625rem;
color: rgb(169, 169, 169);
font-size: 2.5rem;
cursor: pointer;
&:hover {
color: rgb(129, 216, 208);
}
}
}
.searchString {
color: #fff;
font-size: 1.25rem;
margin-bottom: 4.375rem;
input {
outline: none;
cursor: pointer;
margin-right: 0.625rem;
}
label {
margin-right: 1.25rem;
cursor: pointer;
}
}
p {
border-bottom: 0.1875rem double rgb(129, 216, 208);
width: 12.5rem;
font-size: 1.875rem;
margin:0 auto 2.5rem;
padding-bottom: 0.625rem;
color: #ffffff;
}
}
@media screen and (max-width: 440px) {
.search {
input {
width: 80% !important;
}
}
}
</style>
<template>
<div id="contact">
<a href="#about_me" class="title animated bounceIn">
<p class="headline" id="about_me">About me</p>
</a>
<div class="info animated tada">
<img src="../../../static/me.jpg" alt="me"/>
<div class="wraper">
<p class="left">邮箱:</p><p class="right">xuhaodong66@gmail.com</p>
<p class="left">QQ:</p><p class="right">3552116732</p>
<p class="left">学校:</p><p class="right">广州大学大三在校生</p>
<p class="left">在线简历:</p><a class="right" href="https://job.xuhaodong.cn">job.xuhaodong.cn</a>
</div>
</div>
</div>
</template>
<script>
import {mapMutations} from 'vuex'
export default {
created () {
this.set_headline({
content: '关于我',
animation: 'animated bounce'
})
},
methods: mapMutations(['set_headline'])
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
#contact {
min-height: 30rem;
padding: 0 1rem 5rem;
background: rgba(55, 57, 65, 0.2);
.title {
p.headline {
width: 13rem;
}
}
.info {
position: relative;
width: 55%;
margin: 6.875rem auto 0;
padding-bottom: 2rem;
border: 0.1875rem solid rgb(129, 216, 208);
transition: all 2s;
&:hover {
background: rgb(0, 194, 169);
transition: all 2s;
}
img {
width: 9.375rem;
height: 9.375rem;
border: 0.1875rem solid #cccccc;
border-radius: 50%;
position: absolute;
left: 50%;
margin-left: -4.6875rem;
margin-top: -4.6875rem;
}
div.wraper {
margin-top: 8.125rem;
padding-left: 2rem;
p {
display: inline-block;
font-size: 1.25rem;
color: #fff;
margin-bottom: 1.5625rem;
}
.left {
width: 30%;
}
.right {
width: 50%;
}
}
}
}
p.headline {
padding-top: 4.375rem;
margin: 0 auto 1.25rem;
text-align: center;
color: #fff;
font-size: 2rem;
padding-bottom: 1.25rem;
border-bottom: 0.3125rem double rgb(129, 216, 208);
}
@media screen and (max-width: 440px) {
/*.title, .info {*/
/*display: block !important;*/
/*}*/
.info {
width: 100% !important;
}
.wraper {
padding-left: 1rem !important;
p {
display: block !important;
}
p.left {
color: rgb(129, 216, 208) !important;
}
p.right {
width: 100% !important;
}
}
}
</style>
<template>
<div class="articleContent">
<div id="articles">
<div class="tags animated fadeIn">
<div class="tagFlex">
<button
v-for="(tag, index) in allTags"
v-bind:class="{activeBtn: selectIndex === index}"
v-on:click="switchTag({value: tag.id, page: 1}, index, tag)"
>
<span>{{tag.tags}}</span>
</button>
</div>
</div>
<div v-for="(article, index) in reducedArticles" id="article" class="animated fadeIn" :key="article.aid">
<h2>{{article.title}}</h2>
<time><i class="iconfont icon-shijian"></i>{{article.date | toDate}}</time>
<span class="articleTag"><i class="iconfont icon-label"></i>{{article.tags | toTag}}</span>
<span class="commentNumber"><i class="iconfont icon-huifu"></i>{{article.comment_n}}</span>
<p>{{article.content}}</p>
<router-link :to="{name: 'article', params: {id: article.aid, index: index, page: page}, hash: '#article'}" tag="button" exact>
<span>Continue reading</span>
</router-link>
</div>
<p v-if="!loadMore" v-show="!noMore" class="noMore animated fadeIn">下拉加载更多</p>
<p v-if="noMore" class="noMore animated fadeIn">已经到底了,别扯了</p>
</div>
<spinner v-show="loadMore" class="spinner"></spinner>
</div>
</template>
<script>
import {mapMutations, mapActions, mapGetters, mapState} from 'vuex'
import spinner from '../share/spinner'
export default {
data () {
return {
selectIndex: 0,
page: 1,
timeout: true
}
},
created () {
this.set_headline({
content: '文章见解',
animation: 'animated flipInY'
})
this.getAllArticles({page: 1})
this.getAllTags()
},
computed: {
...mapGetters(['reducedArticles', 'allTags']),
...mapState(['curTag', 'loadMore', 'moreArticle', 'isLoading', 'noMore'])
},
mounted () {
window.addEventListener('scroll', this.handleScroll)
},
beforeRouteLeave (to, from, next) {
window.removeEventListener('scroll', this.handleScroll)
next()
},
methods: {
...mapMutations(['set_headline', 'set_curtag', 'moreArticle_toggle']),
...mapActions(['getAllArticles', 'getAllTags', 'searchArticles']),
switchTag (payload, index, tag) {
this.page = 1
this.getAllArticles(payload)
this.selectIndex = index
this.set_curtag(tag)
},
handleScroll () {
if (!this.timeout) {
return null
}
this.timeout = false
if (!this.moreArticle) {
this.page = 1
}
if (!this.isLoading && this.moreArticle && this.$route.name === 'articles') {
let ScrollTop = document.documentElement.scrollTop || document.body.scrollTop
let ClientHeight = document.documentElement.clientHeight
let ScrollHeight = document.documentElement.scrollHeight
if (ScrollHeight - ScrollTop - ClientHeight < 60) {
this.getAllArticles({value: this.curTag.id, add: true, page: ++this.page})
}
// let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
// let clientHeight = document.documentElement.clientHeight || document.body.clientHeight
// let scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight
// if (scrollHeight > clientHeight && scrollTop + clientHeight === scrollHeight) {
// this.getAllArticles({value: this.curTag.id, add: true, page: ++this.page})
// }
}
setTimeout(() => {
this.timeout = true
}, 500)
}
},
components: {
spinner
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.articleContent {
#articles {
padding: 1.875rem 12.5rem 0;
.tags {
.tagFlex {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
.activeBtn {
background: #ffc520;
color: #ffffff;
transition: 1s;
}
button {
transition: 1s;
padding-left: 1rem;
padding-right: 0.2rem;
text-align: center;
background: rgb(129, 216, 208);
color: #00193a;
margin: 0 1.25rem 1.25rem 0;
}
}
}
div#article {
color: #fff;
width: 100%;
border-bottom: 0.125rem solid rgb(129, 216, 208);
h2 {
color: rgb(129, 216, 208);
margin-top: 1.875rem;
margin-bottom: 1.25rem;
}
time {
margin-top: 0.625rem;
margin-right: 0.625rem;
}
p {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
white-space: pre-wrap;
word-wrap: break-word;
margin-top: 1.875rem;
margin-bottom: 1.875rem;
height: 9rem;
}
button {
width: 8.75rem;
height: 2.5rem;
line-height: 2.5rem;
margin-bottom: 1.25rem;
border-radius: 0.25rem;
margin-left: calc(100% - 8.75rem);
}
.articleTag {
margin-bottom: 1.875rem;
margin-right: 0.625rem;
}
.commentNumber {
color: #ffffff;
i {
font-size: 1.125rem;
margin-right: 0.3125rem;
}
}
i.icon-label, i.icon-shijian {
color: #ffffff;
font-size: 1.25rem;
margin-right: 0.3125rem;
}
}
p.noMore {
width: 100%;
height: 1.5rem;
line-height: 1.5rem;
color: #ffffff;
margin-top: 1.875rem;
margin-bottom: 1.875rem;
text-align: center;
}
}
}
@media screen and (max-width: 440px) {
#articles {
padding-left: 1rem !important;
padding-right: 1rem !important;
}
}
</style>
<template>
<div class="container">
<section class="newBlog">
<a href="#lastPost" class="title animated bounceIn">
<p class="headline" id="lastPost">最近更新</p>
</a>
<div class="posts animated fadeIn">
<div class="flex">
<div v-for="(article, index) in reducedArticles" class="oneArticle">
<div class="option">
<time>{{article.date | toDate}}</time>
<span class="commentNumber"><i class="iconfont icon-huifu"></i>{{article.comment_n}}</span>
</div>
<router-link :to="{name: 'article', params: {id: article.aid, index: index, page: 1}, hash: '#article'}" tag="p" exact class="title_1">{{article.title}}</router-link>
<p class="content">{{article.content}}</p>
<router-link :to="{name: 'article', params: {id: article.aid, index: index, page: 1}, hash: '#article'}" tag="button" exact><span>Read More</span></router-link>
</div>
</div>
</div>
</section>
<!-- <section class="contact">-->
<!-- <a href="#contactMe" class="title animated bounceIn">-->
<!-- <p class="headline" id="contactMe">Contact me</p>-->
<!-- </a>-->
<!-- <div class="email animated fadeIn">-->
<!-- <input type="text" placeholder=" 邮件主题" v-model="subject"/>-->
<!-- <input type="text" placeholder=" 邮箱" v-model="address"/>-->
<!-- <textarea placeholder=" 来唠唠嗑呗" spellcheck="false" v-model="content"></textarea>-->
<!-- <button class="sendEmail" @click="send" :disabled="sendFlag">-->
<!-- <span>{{sendFlag ? '发送中...' : '确认'}}</span>-->
<!-- </button>-->
<!-- </div>-->
<!-- </section>-->
</div>
</template>
<script>
import {mapMutations, mapActions, mapGetters} from 'vuex'
export default {
data () {
return {
subject: '',
address: '',
content: '',
sendFlag: false
}
},
created () {
this.set_headline({
content: 'Welcome to my blog',
animation: 'animated bounceIn'
})
this.getAllArticles({page: 1, limit: 3})
},
computed: {
...mapGetters(['reducedArticles'])
},
methods: {
...mapMutations(['set_headline', 'set_dialog']),
...mapActions(['getAllArticles', 'sendMail']),
send () {
const re = /^[\w_-]+@[\w_-]+\.[\w\\.]+$/
if (!this.subject || !this.content) {
this.set_dialog({
info: '还有选项没填(⊙o⊙)?',
hasTwoBtn: false,
show: true
})
return
} else if (!re.test(this.address)) {
this.set_dialog({
info: '请正确填写邮箱地址',
hasTwoBtn: false,
show: true
})
return
}
this.sendFlag = true
this.sendMail({
subject: this.subject,
address: this.address,
content: this.content
}).then(() => {
this.subject = ''
this.content = ''
this.address = ''
this.sendFlag = false
}).catch(() => {
this.sendFlag = false
this.set_dialog({
info: 'sorry, 邮件发送失败,请重新发送',
hasTwoBtn: false,
show: true
})
})
}
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.container {
background: rgba(55, 57, 65, 0.2);
.newBlog {
min-height: 43.75rem;
.title {
margin-bottom: 4.75rem;
p {
padding-top: 2.8125rem;
width: 13rem;
}
}
.posts {
margin-top:1rem;
.flex {
color: #fff;
display: flex;
flex-wrap: wrap;
justify-content: center;
padding-left: 1rem;
padding-right: 1rem;
div.oneArticle {
flex-shrink: 1;
width: 15rem;
border: 0.1875rem solid rgb(129, 216, 208);
padding: 0 2rem 1.25rem;
margin: 0 2rem 4rem 2rem;
.option {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
padding-top: 1rem;
time {
flex-shrink: 1;
width: 16rem;
padding-bottom: 1rem;
display: inline-block;
font-size: 1.25rem;
}
}
p.content {
height: 9.125rem;
text-overflow:ellipsis;
overflow: hidden;
}
p:nth-child(2) {
font-size: 1.5rem;
font-weight: bold;
padding-top: 1.25rem;
border-top: 0.125rem dashed rgb(129, 216, 208);
&:hover {
color: rgb(0, 194, 169);
cursor: pointer;
}
}
p:nth-child(3) {
margin-top: 1.875rem;
}
}
}
}
}
.contact {
background: rgba(55, 57, 65, 0.2);
padding: 0 1rem;
padding-bottom: 2rem;
min-height: 30rem;
.title {
color: white;
margin-bottom: 3.75rem;
p {
padding-top: 2.8125rem;
width: 13rem;
}
}
.email {
margin: 3.125rem auto 0;
width: 40%;
input {
color: #ffffff;
font-size: 1.125rem;
border: 0.125rem solid rgb(129, 216, 208);
width: 70%;
height: 1.5625rem;
margin-bottom: 1.25rem;
display: block;
background: transparent;
}
textarea {
color: #ffffff;
font-size: 1.125rem;
border: 0.125rem solid rgb(129, 216, 208);
width: 100%;
height: 15rem;
resize: none;
background: transparent;
padding-top: 0.5rem;
font-family: Georgia, "Microsoft YaHei", "微软雅黑", STXihei, "华文细黑", serif;
}
.sendEmail {
width: 6.25rem;
margin-top: 0.625rem;
margin-left: calc(100% - 6.25rem);
}
}
}
}
p.headline {
padding-top: 4.375rem;
margin: 0 auto 2.25rem;
text-align: center;
color: #fff;
font-size: 2rem;
padding-bottom: 1.25rem;
border-bottom: 0.3125rem double rgb(129, 216, 208);
}
.commentNumber {
color: #ffffff;
font-size: 1.25rem;
i {
font-size: 1.25rem;
margin-right: 0.3125rem;
}
}
@media screen and (max-width: 440px) {
.oneArticle {
flex-grow: 1;
margin-left: 0 !important;
margin-right: 0 !important;
}
.email {
width: 100% !important;
}
}
</style>
<template>
<div id="search">
<div v-for="(article, index) in reducedArticles" id="article">
<h2>{{article.title}}</h2>
<time><i class="iconfont icon-shijian"></i>{{article.date | toDate}}</time>
<span class="articleTag"><i class="iconfont icon-label"></i>{{article.tags | toTag}}</span>
<span class="commentNumber"><i class="iconfont icon-huifu"></i>{{article.comment_n}}</span>
<p>{{article.content}}</p>
<router-link :to="{name: 'article', params: {id: article.aid, index: index, page: page}, hash: '#article'}" tag="button" exact>
<span>Continue reading</span>
</router-link>
</div>
<spinner v-show="loadMore" class="loading"></spinner>
<p v-if="!loadMore" v-show="!noMore" class="noMore animated fadeIn">下拉加载更多</p>
<p v-if="noMore" class="noMore animated fadeIn">没啦没啦,别扯了</p>
</div>
</template>
<script>
import {mapState, mapActions, mapMutations, mapGetters} from 'vuex'
import spinner from '../share/spinner'
export default {
data () {
return {
page: 1
}
},
created () {
this.searchArticles({key: 'title', value: this.$route.params.text})
this.set_headline({
content: '搜索结果',
animation: 'animated rotateIn'
})
},
beforeRouteUpdate (to, from, next) {
if (to.params.text) {
this.searchArticles({key: 'title', value: to.params.text})
}
next()
},
mounted () {
window.addEventListener('scroll', this.handleScroll)
},
beforeRouteLeave (to, from, next) {
window.removeEventListener('scroll', this.handleScroll)
next()
},
computed: {
...mapState(['loadMore', 'moreArticle', 'isLoading', 'noMore']),
...mapGetters(['reducedArticles'])
},
methods: {
...mapActions(['searchArticles']),
...mapMutations(['set_headline']),
handleScroll () {
if (!this.isLoading && this.$route.name === 'SearchResult') {
const body = document.body
const totalHeight = body.scrollHeight
const scrollTop = body.scrollTop
const clientHeight = window.innerHeight
if (totalHeight - scrollTop - clientHeight === 0 && this.moreArticle) {
this.searchArticles({key: 'title', value: this.$route.params.text, add: true, page: ++this.page})
}
if (!this.moreArticle) {
this.page = 1
}
}
}
},
components: {
spinner
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
#search {
padding: 0 12.5rem 0;
div#article {
color: #fff;
width: 100%;
border-bottom: 0.125rem solid rgb(129, 216, 208);
h2 {
margin-top: 1.875rem;
margin-bottom: 1.25rem;
}
time {
margin-top: 0.625rem;
}
p {
white-space: pre-wrap;
word-wrap: break-word;
margin-top: 1.875rem;
margin-bottom: 1.875rem;
}
button {
width: 8.75rem;
height: 2.5rem;
line-height: 2.5rem;
background: rgb(129, 216, 208);
margin-bottom: 1.25rem;
border-radius: 0.25rem;
margin-left: calc(100% - 8.75rem);
}
.articleTag {
margin-left: 1.25rem;
margin-bottom: 1.875rem;
}
.commentNumber {
margin-left: 0.625rem;
color: #ffffff;
i {
font-size: 1.125rem;
margin-right: 0.3125rem;
}
}
i.icon-label, i.icon-shijian {
color: #ffffff;
font-size: 1.25rem;
margin-right: 0.3125rem;
}
}
p.noMore {
width: 100%;
color: #ffffff;
margin-top: 1.875rem;
margin-bottom: 1.875rem;
text-align: center;
}
}
@media screen and (max-width: 440px) {
#search {
padding-left: 1rem !important;
padding-right: 1rem !important;
}
}
</style>
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
// 让一个高频触发的函数在一定时间内只触发一次
export function _debounce (func, wait) {
let _timestamp, _timer
return function () {
let now = Date.now()
if (_timestamp && ((now - _timestamp) < wait)) {
clearTimeout(_timer)
}
_timestamp = now
_timer = setTimeout(func.bind(this, ...arguments), wait)
}
}
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册