Feb 1997

Overtime and overdue


  • Home

  • Tags

  • Categories

  • Archives

  • Search

官方文档:Django shortcut functions

Posted on 2019-08-16 Edited on 2019-08-29

django.shortcuts包整合了“跨越”MVC中很多层的方法和类。

render()

1
render(request,template_name,context=None,content_type=None,status=None,using=None)

将一个已有的模板和一个已有的字典结合起来并且返回一个带有被渲染的文本的HttpResponse对象。
Django不提供返回TemplateResponse的快捷方式因为render()和TemplateResponse的构造器一样方便。

必须参数

  1. request
      用来生成这个response的request对象。
  2. template_name
      要使用的模板或模板序列的全名。如果给了一个序列,那么将会使用第一个已存在的模板。

可选参数

  1. context
      一个字典,里面的值被加入模板文本中。默认情况下这是一个空字典。如果字典中的值是可调用的,视图会在渲染模板之前调用它。
  2. context_type
      The MIME type to use for the resulting document. Defaults to the value of the DEFAULT_CONTENT_TYPE setting.
  3. status
      响应状态码,默认为200。
  4. using
      用来加载模板的模板引擎的名字。

Django踩坑:数据迁移遇到的问题

Posted on 2019-08-14 Edited on 2019-08-29 In Python

问题描述

  在学习过程中我在同一个APP(名为’tst’)下反复修改模型(注释旧的模型,创建新的模型),在创建新模型后尝试进行数据迁移,出现如下错误信息:

1
No migrations to apply

  在尝试从数据库中删除所有与tst相关的字段后再次尝试迁移:

1
1050, "Table 'django_content_type' already exists"

原因分析

  造成多次应用migrations失败的原因是,当前model是修改过的,原来的migrations已经被我删除,但是,重新生成的migrations使用递增整数记名,所以,在django_migrations表中0001,0002等前面几个数字的文件都已被记录,在Django看来,被记录了就相当于已应用,所以,会出现刚开始的No migrations to apply.

解决方案

  python manage.py migrate —fake-initial
  python manage.py makemigrations
  python manage.py migrate

关于—fake-initial和—fake

—fake-initial

Allows Django to skip an app’s initial migration if all database tables with the names of all models created by all CreateModel operations in that migration already exist. This option is intended for use when first running migrations against a database that preexisted the use of migrations. This option does not, however, check for matching database schema beyond matching table names and so is only safe to use if you are confident that your existing schema matches what is recorded in your initial migration.

—fake

Marks the migrations up to the target one (following the rules above) as applied, but without actually running the SQL to change your database schema.

This is intended for advanced users to manipulate the current migration state directly if they’re manually applying changes; be warned that using —fake runs the risk of putting the migration state table into a state where manual recovery will be needed to make migrations run correctly.

引用参考

cdsn
Django2.0官方文档

(译)"related_name" in models.py

Posted on 2019-08-13 Edited on 2019-08-29 In Python

  假设你有一个叫Book的模型和一个叫Category的模型。每本书只属于一个分类,用一个外键表示。因此你的模型设计如下:

1
2
3
4
5
6
class Category(models.Model):
name = models.CharFeild(max_length=128)

class Book(modesl.Model):
name = models.CharField(max_length=128)
category = models.ForeignKey(Category, on_delete=models.CASCADE)

  如果你有一个Book实例,你可以通过对应的field访问它的分类。并且,如果你有一个分类的实例,默认情况下Django会添加一个叫做”book_set”的属性,这个属性返回该分类下所有书的集合,因此可以进行如下操作:

1
2
3
4
5
from tst.models import Category, Book
category = Category.objects.get(pk=1)
print("Books in "+category.name)
for book in category.book_set.all():
print(book.name)

  book_set是一个django默认给我们构造的属性,通过外键的related_name属性可以给这个属性换个名字,比如说如果用category = models.ForeighKey(Category, related_name='book_collection')定义一个分类,我可以用category.book_collection.all()而不是category.book_set.all()。
  大多数情况下都不需要修改related_name,而且django默认的x_set很好记。然而有一种情况下必须要用related_name:当你有多个从一个模型到另一个模型的外键时。这种情况下可能会产生冲突(因为django会尝试给同一个模型创建两个x_set属性)。
  例如,如果我的Book模型如下(有一个分类和一个子分类):

1
2
3
4
class Book(models.Model):
name = models.CharField(max_length=128)
category = models.ForeignKey(Category)
sub_category = models.ForeignKey(Category)

于是模型不会生效除非你给一个(或两个)外键related_name属性,因此冲突就解决了。例如:

1
2
3
4
class Books(models.Model):
name = models.CharField(max_length=128)
category = models.ForeignKey(Category, related_name='book_category_set')
sub_category = models.ForeignKey(Category, related_name='book_sub_category_set')

原文连接

Reddit

Django学习笔记:模型搭建

Posted on 2019-08-12 Edited on 2019-08-20 In Python

模型简介

  模型是一个用于表示数据的Python类,包含基本的数据字段和行为,在Django中,通常一个模型就代表一个数据库表。模型继承自django.db.models.Model, 模型的每一个属性代表一个数据表的列。
  例子:

1
2
3
4
5
from django.db import models

class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)

  需要注意的是django根据模型所属应用程序生成数据库表明,命名规则:应用程序名_模型名,Django会自动添加id字段作为数据库表的主键。

个人理解

  个人对模型部分的理解是对数据库的操作,models.py文件中的所有操作都可以找到与之对应的数据库操作,只不过有特有的字段名称。而在编辑好模型后进行的数据迁移操作实际上就是将models.py里面的翻译成脚本,然后通过执行脚本文件对数据库进行操作从而生成当前APP所需要用到的数据库。

字段

  字段是一系列数据表列的定义。字段名字中不能出现连续的两个下划线,因为连续的两个下划线是Django数据库API的特殊语法。

常用字段

  1. AutoField
    根据已有的ID自动增长,常用作主键,一般情况下会自动创建。
  2. BooleanField
    字段值只包含True和False,默认情况下对应HTML的复选框:
  3. CharField
    保存不太长的字符串(超长字段建议使用TextField)。必须给出CharField.max_length属性值,默认情况下对应HTML的文本框:。
  4. DateField
    日期类型,对应Python中的datetime.date类型。参数有:
    auto_now:每当保存数据(Save())时都会更新时间为当前时间且不能被重写。
    auto_now_add: 只有在数据第一次创建时才会保存当前时间且不能被重写。
  5. DateTimeField
    日期时间类型,对应Python中的datetime.datetime类型,参数与DateField一样。默认情况下对应HTML的复选框:。
  6. EmailField
    可以验证有效邮件地址的CharField。
  7. FileField
    文件上传控件,可选参数:
    upload_to:文件上传后保存位置(在settings.py中设置MEDIA_ROOT,upload_to所指定的路径会拼接在MEDIA_ROOT之后)
    storage:负责文件存储的Python类,用于存储和提取文件。
  8. ImageField
    包含FileField的全部属性与方法,但是仅允许上传图片类型的文件,额外两个可选属性:
    height_field: 高度
    width_field: 宽度
  9. TextField
    超长文本类型
  10. URLField
    只接受URL字符串的CharField类型。

通用属性

  1. null
      默认为True,保存模型后数据库的对应字段中保存空。
      文本型字段尽可能不用null属性,因为当时用默认值null时,数据库中就可能出现两种空数据:NULL和空字符串,而Django默认使用空字符串。
  2. blank
    默认值为False,当设置为True时字段值允许为空
  3. choices
    属性值为一个可迭代对象,如列表或元组,迭代对象的每个成员包括两个字符串,第一个值作为字段值保存到数据库中,第二个值用于提高字段的可读性。例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    YEAR_IN_SCHOOL_CHOIECES = (
    ('FR', 'Freshman'),
    ('SO', 'Sophomore'),
    ('JR', 'Junior'),
    ('SR', 'Senior'),
    ) # 定义一个元组

    year_in_school = models.CharField(
    max_length = 2,
    choices = YEAR_IN_SCHOOL_CHOICES, # 直接引用元组
    default = 'Freshman',
    )
  4. default
    设置字段默认值。属性值可以是字符串也可以是方法。默认值不可以是可变对象,如列表。

  5. primary_key
    将字段设置为数据表主键。如果模型中任何字段都不包含primary_key=True属性,会自动添加一个字段作为主键。
  6. verbose_name
    类似于字段的说明。
    除了ForeignKey、ManyToManyField、OneToOneField三种字段类型外,其他字段类型都包含一个默认的verbose_name属性,可以直接在字段属性列表的第一位输入文本作为verbose_name属性值,如未给出则会将字段名作为verbose_name(字段名如果包含下划线会换成空格)。

    个人理解

    模型对应数据库的表,那么字段就对应表的列。

元属性

通过在模型中添加一个叫做Meta的子类定义。

abstract

abstract = True,当前模型将成为一个抽象类。

ordering

该属性是一个元组、列表或者查询表达式。定义了显示的顺序。

总结

  模型的构建对应数据库的构建,这次只记录了目前为止用过的一些知识,还有很多字段、属性、元属性尚未涉及,以后碰到了再继续补充。

Django踩坑:python manage.py dbshell报错

Posted on 2019-08-11 In Python

问题描述

  之前为了避免麻烦,Django的数据库配置用的都是自带的sqlite,今天尝试用Pycharm进行数据库的可视化,但是sqlite好像用不了,而且网上的相关资料比较少,于是还是改用MySQL。
  省略掉settings.py中对数据库的配置过程,最后在我新建模型并完成迁移操作后,我在pycharm的终端使用命令行

manage.py dbshell```,出现错误信息:
1
2
```
CommandError: You appear not to have the ‘mysql’ program installed or on your path.

大意是指路径中找不到mysql。

问题解决

  找到Mysql所在的文件夹的bin目录,添加至系统环境变量的Path即可,例如:D:\mysql-5.7.27-winx64\bin
  测试是否生效的方法就是打开CMD,输入mysql -hroot -ppwd即可直接进入数据库,其中root是用户名,pwd是密码。
  最后一部是重启pycharm,我在没重启的之前还是会报错,应该是重启之后配置才生效的。

Djangos学习笔记:上传文件/图片

Posted on 2019-08-09 Edited on 2019-08-29 In Python

  首先在根目录下的settings中添加配置:

1
2
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

这样上传的文件就会在media文件夹中

  Models.py中添加模型并完成迁移,例如:

1
2
3
4
5
6
class ImageStore(models.Model):
name = models.CharField(max_length=150, null=True)
img = models.ImageField(upload_to='img') # 会上传至/media/img文件夹中,如不存在则会自动生成一个

def __str__(self):
return self.name

  上传图片功能需要安装Pillow:

1
pip install pillow

  然后在admin.py中注册模型:

1
admin.site.register(ImageStore)

即可在管理员界面对图片进行上传,并且上传后图片会出现在文件夹中。

  目前还存在的问题是无法在管理员界面显示图片,如果直接点链接会跳转至一个新的无法到达的地址,原因是因为相应的路由地址我还没写。

Django踩坑:PK

Posted on 2019-08-08 Edited on 2019-08-29 In Python

问题描述

  在《第一个Django应用》中,我根据教程总结了我对于Django应用开发过程的初步理解以及对应用开发步骤的梳理,在我尝试仅根据博文内容对应用进行复现的过程中出现了如下错误:

  这个错误出现在我进入polls主页后选择其中一项标题进行投票时,投票结束后跳转到了http://127.0.0.1:8000/polls/1/vote/,本来按道理应该是http://127.0.0.1:8000/polls/1/results/,于是我将检查目标定在了/polls/views.py文件的vote()方法里,因为我在这个方法里设置了页面的跳转,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
def vote(request, question_id):
question = get_object_or_404(Question, pk=question_id)
try:
selected_choice = question.choice_set.get(request.POST['choice'])
except (KeyError, Choice.DoesNotExist):
return render(request, 'polls/detail.html', {
'question': question,
'error_message': "You didn't select a choice.",
})
else:
selected_choice.votes += 1
selected_choice.save()
return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

分析解决

  结合报错信息’not enough values to unpack(excepted 2, got 1)’,意思是在解包过程中少了一个数值,通过检察源码找到get()方法的信息:Perform the query and return a single object matching the given keyword arguments.通过给定的关键词语句执行查询并返回匹配的对象。request.POST是一个类似字典的对象,允许你通过键名访问提交的数据。本例中,request.POST[’choice’]返回被选择选项的ID,而ID是主键,因此改为pk=request.POST['choice']即可。

总结

  在创建一个新的models实例时,如果没有设置主键,那么Django会自动创建一个id字段作为该模型的主键,有时候用id和pk都能达到预期的效果,但是pk更加独立于真正的主键,也就是说不用在意主键是叫id或者是object_id。并且使用pk可以提高一致性,即便模型中有不同的主键。

参考资料

stackoverflow
李健.《Django 2.0入门与实践》.清华大学出版社

Django学习笔记:第一个Django应用

Posted on 2019-08-04 Edited on 2019-08-15 In Python

基本流程

创建工程

1
django-admin startproject mysite  # mysite为工程名

此时的文件目录结构:

1
2
3
4
5
6
7
mysite/
manage.py
mysite/
__init__.py
settings.py
urls.py
wsgi.py

  • mysite/: 整个项目的容器,没有太大意义,名字可以随意更改
  • manage.py: 命令行工具,用来管理Django项目
  • mysite/: 当前Django工程所使用的Python包
  • __init__.py: 表明当前文件夹是一个Python包
  • settings.py: 配置文件
  • urls.py: 路由配置文件
  • wsgi.py: 兼容WSGI的Web服务入口

运行工程

1
python manage.py runserver

创建应用程序

工程(Project)和程序(App): 应用程序是真正工作的组件,例如一个博客系统或者投票系统。工程师包含网站配置信息和应用程序等的集合,一个工程可以包含多个应用程序,而一个应用程序可以属于多个工程。

1
python manage.py startapp polls  # 创建一个名为polls的投票程序

polls的目录结构:

1
2
3
4
5
6
7
8
9
polls\
__init__.py
admin.py
apps.py
migrations/
__init__.py
models.py
tests.py
views.py

配置数据库以及创建模型

  使用Django自带的数据库SQLite。

  现在,我们来定义模型model,模型本质上就是数据库表的布局,再附加一些元数据。
  Django通过自定义Python类的形式来定义具体的模型,每个模型的物理存在方式就是一个Python的类Class,每一个类都是django.db.models.Model的子类。每一个字段都是Field类的一个实例,例如用于保存字符数据的CharField和用于保存时间类型的DateTimeField,它们告诉Django每一个字段保存的数据类型。每个模型代表数据库中的一张表,每个类的实例代表数据表中的一行数据,类中的每个变量代表数据表中的一列字段。
  Django通过模型,将Python代码和数据库操作结合起来,实现对SQL查询语言的封装。也就是说,你可以不会管理数据库,可以不会SQL语言,你同样能通过Python的代码进行数据库的操作。Django通过ORM对数据库进行操作,奉行代码优先的理念,将Python程序员和数据库管理员进行分工解耦。
  示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# polls/models.py

from django.db import models


class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')


class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)

  可以在每个Field中使用一个可选的第一位置参数用于提供一个人类可读的字段名,让你的模型更友好,更易读,并且将被作为文档的一部分来增强代码的可读性,如例子中的pub_date。
  使用ForeignKey定义了一个外键关系。它告诉Django,每一个Choice关联到一个对应的Question(注意要将外键写在‘多’的一方)。Django支持通用的数据关系:一对一,多对一和多对多,如例子中的question。

启用模型

  把polls加入mysite/settings.py中的INSTALLED_APPS让项目知道该应用的存在。
  运行命令$ python manage.py makemigrations polls将数据模型的改动保存为一个’migration’
  运行命令$ python manage.py migrate对数据库执行真正的迁移动作。在这一步中会自动生成表,表明为“应用名_模型名(小写)”的组合,自动添加主键id并且按照惯例在外键字段名上附加“_id”。

开发视图

  Django的视图是负责页面展示的重要模块,用于处理网站业务逻辑。
  打开polls/view.py文件,每个视图至少做两件事之一:返回一个包含请求页面的HttpResponse对象或者弹出一个类似Http404的异常。其它的则自己定义。
  render()方法: 从Django.shortcuts导入render,将加载模板、传递参数、返回HttpResponse对象整合一起。第一个位置参数是请求对象(就是view函数的第一个参数),第二个参数是模板,第三个参数是字典,包含需要传递给模板的数据。最后render函数返回一个经过字典数据渲染过的模板封装而成的HttpResponse对象。例如:

1
2
3
4
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
context = {'latest_question_list': latest_question_list}
return render(request, 'polls/index.html', context)

  创建视图后,为了能够访问它,需要在URL中添加路由映射。在polls中创建文件urls.py:

1
2
3
4
5
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
]

  接下来需要在根目录的urls.py中引用polls/urls.py,修改mysite/urls.py:

1
2
3
4
5
6
7
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
path('polls/', include('polls.urls')),
path('admin/', admin.site.urls),
]

  上面代码中include()方法可以用来引用其他URLconfs(urls.py)。出了admin.site.urls之外,在任何时候都应该使用include()方法引用其他路由模块。

模板系统

  Django提供的模板系统可以解耦视图和模板之间的硬连接。
  首先,在polls目录下创建一个新的templates目录,Django会在它里面查找模板文件。
  项目的 TEMPLATES配置项描述了 Django 如何载入和渲染模板。默认的设置文件设置了 DjangoTemplates 后端,并将 APP_DIRS设置成了 True。这一选项将会让 DjangoTemplates 在每个 INSTALLED_APPS 文件夹中寻找 “templates” 子目录。这就是为什么尽管我们没有像在第二部分中那样修改 DIRS 设置,Django 也能正确找到 polls 的模板位置的原因。
  在templates目录中,再创建一个新的子目录名叫polls,进入该子目录,创建一个新的html文件index.html。换句话说,你的模板文件应该是polls/templates/polls/index.html。因为 Django 会寻找到对应的app_directories ,所以你只需要使用polls/index.html就可以引用到这一模板了。
  在视图view.py中通过render()向模板中的页面传递的上下文变量可以直接使用,例如在detail()视图里通过return render(request, 'polls/detail.html', {'question': question})传递的question在模板polls/detail.html中通过question.question_text使用

后台管理

  运行命令$ python manage.py createsuperuser创建一个可以登录admin站点的用户,填入所需信息后通过http://127.0.0.1:8000/admin/即可访问。
随后为了让应用的信息可以在后台显示,打开polls/admin.py注册投票应用:

1
2
3
4
from django.contrib import admin
from .models import Question

admin.site.register(Question)

参考资料

李健.《Django 2.0入门与实践》.清华大学出版社
刘江的博客教程


更新 2019-08-15

使用MySQL的配置方案

./settings.py:

1
2
3
4
5
6
7
8
9
10
    DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'NewTest',
'HOST': '127.0.0.1',
'USER': 'root',
'PASSWORD': 'pwd',
'PORT': '3306',
}
}

记载数据库驱动

./init.py:

1
2
import pymysql
pymysql.install_as_MySQLdb() # 完成数据库的驱动加载

解决mysqlclient版本问题

base.py 中搜索:

1
version = Database.version_info

行内添加一个pass

1
raise ImproperlyConfigured('mysqlclient 1.3.13 or newer is required; you have %s.' % Database.version)

如下:

1
2
3
4
5
6
7
8
if version < (1, 3, 13):
pass
'''
raise ImproperlyConfigured(
'mysqlclient 1.3.13 or newer is required; you have %s.'
% Database.__version__
)
'''

保存关闭并打开 operations.py搜索

1
query = query.decode(errors='replace')

decode换为encode:

1
query = query.encode(errors='replace')

其他

使用MySQL要注意自己创建数据库

1
CREATE DATABASE <NAME>;

Django踩坑:python manage.py migrate报错

Posted on 2019-08-01 Edited on 2019-08-11 In Python

  在配置Django开发环境时,为了用到Django自带的数据模型,使用python manage.py migrate对数据进行迁移,所谓迁移,就是根据模型自动生成关系数据库中的二维表。

报错分析

在实际操作过程中我先后遇到如下两种报错信息:

  1. 意思是说我的mysqlclient版本过低,但是我在退出虚拟环境后通过pip list检查出版本是最新的,进入虚拟环境后就显示不出来了,pip也不能用,尝试了网上各种方法无果,猜想可能是我虚拟环境的配置出了问题,我觉得如果不在虚拟环境内进行数据迁移的话可能不会有问题,但是毕竟虚拟环境是Python开发神器,还是很想把虚拟环境配置好。
    Stackoverflow上有一个人针对这个错误提供了一个方案,大致的思路是对虚拟环境下的后台mysql文件进行修改,在版本检测这一环节对检测到低于1.3.13版本的mysqlclient采取直接pass的措施,不捕捉异常,经测试有效。具体操作过程见参考引用中的链接。
    最后通过virtualenvwrapper创建虚拟环境解决了问题,将所有的虚拟环境目录全都集中起来,比如放到 ~/Envs/,并对不同的虚拟环境使用不同的目录来管理。并且,它还省去了每次开启虚拟环境时候的 source 操作,使得虚拟环境更加好用。

    1
    mysqlclient 1.3.13 or newer is required; you have 0.9.3
  2. 在第一个问题解决后,我在新的虚拟环境重新配置,同样进行到python manage.py migrate步骤时出现了如下问题,意思是说我的SQL语法有问题,但是因为当时我只进行了一步CREATE database的操作,并且操作成功,所以排除我SQL语句错误的可能性。后来了解到Django 2.0已经停止了对MySQL 5.5的支持,而我系统安装的MySQL正好是5.5版本的,于是尝试用MySQL 5.7替换,最后问题得到解决。

    1
    django.db.utils.ProgrammingError: (1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(6) NOT NULL)' at line 1")

参考引用

Stackoverflow

virtualenvwrapper的安装及使用

Windows10系统下,彻底删除卸载MySQL


更新(2019-08-04)

  之前的学习都是跟着Github上的Python-100-Days操作的,全程用命令行Vim操作,用了一段时间觉得操作不是很方便,今天重新用Pycharm操作Django项目,另外为了避免之前的问题用的是Django自带的Sqlite数据库而不是Mysql,一路下来很顺畅。另外个人觉得Python-100-Days的教程太过笼统,更适合作为一个学习路线和查漏补缺。

参考引用

刘江的博客教程

Hexo配置优化

Posted on 2019-07-31 In Hexo

Hexo的配置文件中有很多可以自定义修改的值,以及Github上有很多Hexo的主题可供下载,本文记录一下当前博客的修改过程,因为崇尚简洁而且不懂前端,所以没有太多花哨的优化。

下载主题

该博客使用的是NexT主题,在GitBash中进入博客文件夹后使用命令

1
$ git clone https://github.com/theme-next/hexo-theme-next themes/next

即可将主题下载到themes文件夹中,在这里也能找到主题所对应的配置文件。

修改Hexo配置文件

Vim中打开Hexo的配置文件(_config.yml),对以下项目进行了修改:

1
2
3
4
5
title: Feb 1997  
subtitle: Overtime and overdue
author: Feb 1997
language: zh-CN # 将主题的语言设定为中文
theme: next # 将主题设置为next

修改主题配置文件

Vim中打开NexT的配置文件(_config.yml),对一下项目进行了修改:

1
2
3
4
5
6
tags: /tags/ || tags  # 菜单显示标签
categories: /categories/ || th # 菜单显示分类
scheme: Pisces # 选择主题的风格
avatar:
url: /images/avatar.jpg # 头像图片在 /source/images 里面
rounded: true # 把头像设置成圆形展示

总结

  原模板已经很简洁了,因此在原主题的基础上没有太多的变动。更多油画不仅局限于配置文件的改动,很多前端的代码也可以改动以达到各种效果,网上已经有了很多分享,这里就不一一尝试了。

1…101112
Feb 1997

Feb 1997

112 posts
4 categories
24 tags
© 2020 Feb 1997