赞
踩
需要设计一个电子拍卖商城,能够允许用户可以上传商品,对商品进行出价,以及进行评论,并将商品添加到watchlist。
最近在学习CS50的web课程,CS50系列课程的最大亮点是一个又一个难度高的project,这个项目我断断续续的花了3,4周的时间完成,克服了很多困难,也学到了很多的知识,我将完整的代码在文末。CS50作业地址在 这里 ,最终商城界面如下:
这次项目的任务是创建一个名为"Commerce"的网站。虽然官方提供了代码的模板,但项目本身涵盖了广泛的内容,考验着参与者的综合素质。为了实现整体页面的美观,需要对CSS有深入的了解和掌握。如果你想更进一步地掌握样式的高级和简便控制,就需要了解一些常见的CSS框架,比如Bootstrap等。此外,为了实现出色的用户与前端交互体验,需要使用JavaScript来操作页面上的元素。JavaScript也有许多方便的工具,比如jQuery等,这些工具在处理大型项目时能够提供更便捷的操作。
这些技术都是构建更好网页的辅助工具。然而,在这个项目中,最核心的目标还是掌握Django框架。在CS50课程中,只会论述Django框架的基础知识。但在这个项目中,我们需要制作更复杂的管理应用程序,例如处理复杂的图片上传请求、JavaScript与Django后端的协作,以及使用Django管理界面等等。这些都需要查阅资料,掌握更多技能,来完成应用的目标。
下面我会开始按照项目的完成顺序来介绍项目以及讲解部分代码
1.设计电子拍卖商城模型,商城拥有商品,用户,竞价,评论等多个要素,而且要素之间会互相关联。商品需要关联用户为创造者,竞价有出价者(用户),以及价格两个要素,评论与竞价相同。设计Django模型,也需要数据库的基础以及运用,需要了解建表,数据的增删改查,这些知识。
from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): pass class AuctionListing(models.Model): title = models.CharField(max_length=100) description = models.TextField() starting_bid = models.DecimalField(max_digits=10, decimal_places=2) current_bid = models.DecimalField(max_digits=10, decimal_places=2, default=0) image_url = models.ImageField(upload_to='gimages/') category = models.CharField(max_length=50, choices=[ ('Fashion', 'Fashion'), ('Toys', 'Toys'), ('Electronics', 'Electronics'), ('Home', 'Home'), # 添加更多类别 ]) creator = models.ForeignKey(User, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) is_active = models.BooleanField(default=True) def __str__(self): return self.title # 投标模型 class Bid(models.Model): listing = models.ForeignKey(AuctionListing, on_delete=models.CASCADE, related_name='bids') bidder = models.ForeignKey(User, on_delete=models.CASCADE) amount = models.DecimalField(max_digits=10, decimal_places=2) timestamp = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Bid on {self.listing.title} by {self.bidder.username}" # 评论模型 class Comment(models.Model): listing = models.ForeignKey(AuctionListing, on_delete=models.CASCADE, related_name='comments') commenter = models.ForeignKey(User, on_delete=models.CASCADE) text = models.TextField() timestamp = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Comment on {self.listing.title} by {self.commenter.username}"
最核心的AuctionListing
模型储存着各个商品,这个模型有着title,description
等域,以及category
域,这个域有着Choice这个参数,规定了category的选项,在前端的表单中,可将category渲染成可选参数的表单.creator
这个域关联着创建商品的用户,在程序中,creator可以简单的表示成request.user
,Create_at
为自动生成的时间域。
值得注意的是creator
中有着on_delete
选项,这个选项规定了在外键所指的User对象被删除时,如何处理对应的AuctionListing元素。
关于域的设计可以参考Django的models模块,Model field reference | Django documentation | Django,同时这里的image_url域会在后文提到。
Bid
竞价模型,以及Comment
评论模型见代码部分。
设计完数据库模型后,我们需要首先创造出一些简单的网页,完成用于上传商品,展示商品等功能 。在Django的应用中,这意味着**views**模块(处理后端视图),**urls**模块(处理访问的url),以及储存模版的**template**文件夹(HTML文件)。
2.模型的图片上传问题 商品界面需要展示图片,我们遇到了图片上传的问题,在这里我参考了很多的博客,来解决媒体文件以及图片上传的问题,我在setting.py 总加入了这几行设置参数,
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
以及在数据库模型中使用ImageField来对应图片文件。
image_url = models.ImageField(upload_to='gimages/')
使用ImageField上传文件后,在需要用到图片的地方使用 Field_Name.url
来进行展示图片 , 注意这里的Field_Name是所设置域的名字,url属性返回图片的地址,完成图片的上传,设置后,商品的展示问题也被解决 。
<div class="listing-item">
{% if listing.image_url %}
<img class="l-img" src="{{ listing.image_url.url }}" alt="{{ listing.title }}">
{% endif %}
在上文,我们讨论了如何解决单个商品的上传问题,但接下来,我们需要整合商品索引页和商品详情页。这些HTML文件是通过Django模板一个个生成的,因此我们需要学习如何处理文件之间的配合(layout文件),以及文件内部如何与Django程序进行有效的通信,包括变量传递、for循环、if语句和Django URL的运用。
1. 文件的整合与布局
首先,让我们谈一下文件的整合与布局。在Django项目中,通常会有多个HTML文件,包括基础布局文件layout.html
、索引页index.html
和详情页detail.html
。这些文件之间需要进行协调,引用,以确保整个网站的外观和行为是一致的。
基础布局文件:我们可以为网站提供一个通用的基础HTML文件,其中包含网站的通用结构,例如头部、导航栏和底部。这个基础布局文件通常命名为layout.html
。
索引页和详情页:对于索引页和详情页,你可以创建单独的模板文件,如index.html
和detail.html
。这些文件可以继承基础布局文件,通过{% extends ‘layout.html’ %}语句指定。
块区域:在基础布局文件中,你可以使用块区域(block)来标识可以被子模板覆盖的内容。在子模板中,使用{% block block_name %}…{% endblock %}来定义和覆盖这些块区域。
2. 变量传递
Django模板允许你将变量从视图传递到HTML文件中,以动态生成内容。在视图函数中,你可以使用render
函数将变量传递给模板。例如:
from django.shortcuts import render
def index(request):
products = Product.objects.all()
return render(request, 'index.html', {'products': products})
在index.html
模板中,你可以通过双括号语法({{ variable_name }})来显示传递的变量,如:
{% for product in products %}
<p>{{ product.name }}</p>
{% endfor %}
3. for循环和if语句
在Django模板中,你可以使用{% for %}循环和{% if %}条件语句来处理数据和控制显示。例如,你可以使用for循环遍历商品列表,并使用if语句根据条件显示特定的内容。
{% for product in products %}
<p>{{ product.name }}</p>
{% if product.price > 50 %}
<p>Expensive</p>
{% else %}
<p>Affordable</p>
{% endif %}
{% endfor %}
通过良好的文件布局、变量传递、for循环、if语句,我们可以更加便利的构建一个网站。构建出令人满意的Web应用程序。更多的Django template信息请参阅 Django官方文档。
这部分相应的部分代码如下,注意这里展示的是完整的代码,因此有一些函数目前文章中还没有提到相关的知识。
views.py from django.contrib.auth import authenticate, login, logout from django.db import IntegrityError from django.http import HttpResponse, HttpResponseRedirect,JsonResponse from django.shortcuts import render,redirect,get_object_or_404 from django.urls import reverse from .models import User,AuctionListing,Bid,Comment,WatchList from .form import AuctionListingForm,BidForm,CommentForm,WatchListForm from django.conf import settings import os import pdb IMAGE_ROOT = settings.MEDIA_ROOT def create_listing(request): """处理表单 以及上传的文件""" if request.method == 'POST': if request.user.is_authenticated: # pdb.set_trace() form = AuctionListingForm(request.POST,request.FILES) # new_listing.image_url = request.FILES if form.is_valid(): # 保存商品到数据库 new_listing = form.save(commit=False) new_listing.creator = request.user # 关联创建者 new_listing.save() return redirect('listing_detail',new_listing.pk) # 重定向到新商品的详情页 else: return redirect("index") else: form = AuctionListingForm() return render(request, 'auctions/create_listing.html', {'form': form}) # 使用login require 装饰,需要进行登录 def watch_list(request): """add table to the watch list """ if request.method == 'POST': if 'name' in request.POST: # == 'btn-delete-item' # pdb.set_trace() # delete btn 传来的数据 删除这个数据 auction_id = request.POST['auction_id'] item = WatchList.objects.filter(liker=request.user,auction=auction_id) item.delete() data = request.POST.dict() data['status'] = 'success' return JsonResponse(data) elif 'auction_id' in request.POST: # 获取到user-id , auction - id 储存watch form # pdb.set_trace() watch_info = request.POST watch_form = WatchList() watch_form.liker = request.user watch_form.auction = AuctionListing.objects.get(pk=watch_info['auction_id']) watch_form.save() return redirect("listing_detail",watch_info['auction_id']) # 得到auction id auction_ids = WatchList.objects.filter(liker=request.user).values_list('auction',flat=True) # 得到 acution list 使用数据库的In方法 来筛选所有id在列表中的商品 auction_list = AuctionListing.objects.filter(id__in=auction_ids) # [get_object_or_404(AuctionListing,id=wid) for wid in watch_id] context = {'auction_list':auction_list} return render(request,'auctions/watch_list.html',context) def index(request): """重名函数 也可能会导致原本的变量名字被占用""" # 查询数据库获取所有活动商品列表 active_listings = AuctionListing.objects.filter(is_active=True) # pdb.set_trace() return render(request, 'auctions/index.html', {'active_listings': active_listings }) def listing_detail(request, listing_id): """为每一个单独的商品简历 使用最高的进行排序 使用urls来传递图片的真实路径""" listing = get_object_or_404(AuctionListing, id=listing_id) # 完整的comments bids = Bid.objects.filter(listing=listing).order_by('-amount') comments = Comment.objects.filter(listing=listing).order_by('-timestamp') bid_form = BidForm() comment_form = CommentForm() if request.method == 'POST': if 'bid_button' in request.POST: # 处理投标表单 bid_form = BidForm(request.POST) new_bid = bid_form.save(commit=False) new_bid.bidder = request.user new_bid.listing = listing new_bid.save() # 获取最高的投标金额, 并且保存最高投标金额 highest_bid = Bid.objects.filter(listing=listing).order_by('-amount').first() if highest_bid: listing.current_bid = highest_bid.amount listing.save() elif 'comment_button' in request.POST: # 处理评论表单 comment_form = CommentForm(request.POST) if comment_form.is_valid(): new_comment = comment_form.save(commit=False) new_comment.commenter = request.user new_comment.listing = listing new_comment.save() return redirect('listing_detail', listing_id=listing_id) context = { 'listing': listing, 'bids': bids, 'bid_form': bid_form, 'comment_form': comment_form, 'comments': comments, # 'image_url':os.path.join(IMAGE_ROOT,listing.image_url), } return render(request, 'auctions/listing_detail.html', context) def login_view(request): if request.method == "POST": # Attempt to sign user in username = request.POST["username"] password = request.POST["password"] user = authenticate(request, username=username, password=password) # Check if authentication successful if user is not None: login(request, user) return HttpResponseRedirect(reverse("index")) else: return render(request, "auctions/login.html", { "message": "Invalid username and/or password." }) else: return render(request, "auctions/login.html") def logout_view(request): logout(request) return HttpResponseRedirect(reverse("index")) def register(request): if request.method == "POST": username = request.POST["username"] email = request.POST["email"] # Ensure password matches confirmation password = request.POST["password"] confirmation = request.POST["confirmation"] if password != confirmation: return render(request, "auctions/register.html", { "message": "Passwords must match." }) # Attempt to create new user try: user = User.objects.create_user(username, email, password) user.save() except IntegrityError: return render(request, "auctions/register.html", { "message": "Username already taken." }) login(request, user) return HttpResponseRedirect(reverse("index")) else: return render(request, "auctions/register.html") url.py from django.urls import path from . import views urlpatterns = [ path("", views.index, name="index"), path("login", views.login_view, name="login"), path("logout", views.logout_view, name="logout"), path("register", views.register, name="register"), path('create_listing',views.create_listing,name="create_listing"), path('watch_list',views.watch_list,name='watch_list'), # 使用id的方式 使用id来证明 这样你的函数在接收到id的时候就会使用这个url path('<int:listing_id>/',views.listing_detail,name='listing_detail'), ]
到目前为止,我们已经完成了一个线上拍卖商城的初步雏形,该网站已经包括了上传商品和展示商品等功能。接下来,我们需要添加一些高级功能和进一步优化商城的界面。这些功能包括:
在商品详情页中添加评论以及竞价的功能
实现一个watch - listing ,类似于听歌软件中,我喜欢的歌。同理,在本项目中可以添加喜欢的商品到一个列表中去。
使用css样式以及javascript 来美化只有HTML的界面。
1.商品的评论与竞价:
我们可以在商品的详情页中,添加评论表单以及竞价表单,来实现用户与商品之间的交互。
class BidForm(forms.ModelForm):
class Meta:
model = Bid
fields = ['amount']
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ['text']
bid,以及comment的表单,两者的模型可以见上文模型代码。
def listing_detail(request, listing_id): """为每一个单独的商品简历 使用最高的进行排序 使用urls来传递图片的真实路径""" listing = get_object_or_404(AuctionListing, id=listing_id) # 完整的comments bids = Bid.objects.filter(listing=listing).order_by('-amount') comments = Comment.objects.filter(listing=listing).order_by('-timestamp') bid_form = BidForm() comment_form = CommentForm() if request.method == 'POST': if 'bid_button' in request.POST: # 处理投标表单 bid_form = BidForm(request.POST) new_bid = bid_form.save(commit=False) new_bid.bidder = request.user new_bid.listing = listing new_bid.save() # 获取最高的投标金额, 并且保存最高投标金额 highest_bid = Bid.objects.filter(listing=listing).order_by('-amount').first() if highest_bid: listing.current_bid = highest_bid.amount listing.save() elif 'comment_button' in request.POST: # 处理评论表单 comment_form = CommentForm(request.POST) if comment_form.is_valid(): new_comment = comment_form.save(commit=False) new_comment.commenter = request.user new_comment.listing = listing new_comment.save() return redirect('listing_detail', listing_id=listing_id) context = { 'listing': listing, 'bids': bids, 'bid_form': bid_form, 'comment_form': comment_form, 'comments': comments, # 'image_url':os.path.join(IMAGE_ROOT,listing.image_url), } return render(request, 'auctions/listing_detail.html', context)
投标部分
投标表单创建与提交:在商品详情页面,用户可以使用投标表单 BidForm()
进行投标。当用户填写表单并提交时,我们检查HTTP POST请求中的 “bid_button” 字段,以确定用户正在提交投标表单。
数据验证与保存:我们验证用户提交的投标表单数据,确保数据合法。有效数据将创建一个新的投标记录 new_bid
,其中包括投标者(bidder
)、商品(listing
),然后将其保存到数据库。
获取最高投标金额:为了维护商品的当前最高投标金额 current_bid
,我们检索所有投标记录,按照投标金额降序排序,获取最高投标。如果有最高投标,将其金额保存到商品的 current_bid
字段,并保存商品记录。更新完成后,我们刷新整个页面,来
评论部分
评论表单创建:商品详情页面允许用户提交评论,因此我们创建了一个评论表单 CommentForm()
。
处理评论表单的提交:当用户填写评论表单并提交时,我们检查HTTP POST请求中的 “comment_button” 字段,以确定用户正在提交评论表单。
数据验证与保存:我们验证用户提交的评论表单数据,确保数据合法。有效数据将创建一个新的评论记录 new_comment
,其中包括评论者(commenter
)、商品(listing
),然后将其保存到数据库。储存完成后,我们重定向用户回到当前商品的详情页面,以便他们可以查看新的评论记录。
<div class="listing-container"> <div class="listing-item" style="max-width: max-content;"> {% if listing.image_url %} <img class="l-img" src="{{ listing.image_url.url }}" alt="{{ listing.title }}"> {% endif %} <div class="bid-comment-form"> <p class="para-title">Place a Bid:</p> <p>Current bid: ${{ listing.current_bid }}</p> <form method="post" class="custom-form"> {% csrf_token %} {{ bid_form.amount.label_tag }} {{ bid_form.amount }} <button type="submit" name="bid_button">Submit Bid</button> </form> <p class="para-title">Add a Comment:</p> <div> {{ comment_form.text.label_tag }} </div> <form method="post" class="custom-form"> {% csrf_token %} {{ comment_form.text }} <br> <button type="submit" name="comment_button" style="padding: 3px 10px;">Submit Comment</button> </form> </div> </div> </div>
最终界面如下
2.添加watchlist 的功能:
**1)首先设计watchlist 模型类**,建立商品与用户之间的映射关系
class WatchList(models.Model):
liker = models.ForeignKey(User,on_delete=models.CASCADE)
auction = models.ForeignKey(AuctionListing,on_delete=models.CASCADE)
timestamp = models.DateTimeField(auto_now_add=True)
**2)再通过前端页面**用户按下Add to WatchList 按钮,将商品加入watch list
<form action="{% url 'watch_list' %}" style="text-align: center" method="post">
{% csrf_token %}
<input type="hidden" name="auction_id" value="{{listing.id}}">
<button type="submit" class="add-watch-list">Add to watchlist</button>
</form>
**3)最后通过watchlist 的 user键**筛选出登录用户所有在Watchlist中的商品,并使用展示index的方式来展示watchlist 的商品。
auction_ids = WatchList.objects.filter(liker=request.user).values_list('auction',flat=True)
# 得到 acution list 使用数据库的In方法 来筛选所有id在列表中的商品
auction_list = AuctionListing.objects.filter(id__in=auction_ids) # [get_object_or_404(AuctionListing,id=wid) for wid in watch_id]
context = {'auction_list':auction_list}
return render(request,'auctions/watch_list.html',context)
4)在页面中使用Ajax技术管理商品 在watchlist
页面中,我们额外使用了JavaScript中的AJAX技术,以实现不必刷新整个页面即可删除商品的功能。由于篇幅限制,本博客未涉及详细介绍AJAX、网页调试和CSS的方法,但这些内容将在其他博客中进行解释。
代码如下:
$(document).ready(function() { {# write javascript to send messiage #} $('.btn-delete-item').click(function() { {#获取到循环id#} var loop_id = $(this).data('looper'); var div_id = 'div-'+loop_id ; var auction_id = $(this).siblings('input[name=listing-id]').val() $.ajax({ type: 'POST', url: '/watch_list', data: { 'name':'btn-delete-item', 'auction_id': auction_id, // 发送商品ID 'div_id':div_id, 'csrfmiddlewaretoken': $('input[name=csrfmiddlewaretoken]').val(), // 包括CSRF令牌 }, success: function(data) { // data.remove() 删除对应的元素 alert('删除成功!') console.log(data) $('#'+data['div_id']).remove() }, error: function() { alert('删除失败!!') } }); }); });
最终watch list如下
3.css样式美化商城界面 为了提高网站的外观和用户体验,我们使用了CSS样式来美化整个项目页面。没有CSS修饰的页面可能会显得奇怪和简陋。因此,我花了一些时间来学习CSS,并最终完成了整个项目的美化。我们可以使用CSS来规范元素的各个属性,包括背景颜色、居中、字体颜色、大小、字体类型以及弹性布局等。更多有关这些内容的详细方法将在下一篇博客中进行讲解。
虽然在文字中项目的制作很简单,流畅,但是实际开始会有一个一个的bug出现,一开始我的进度举步维艰,中途也有各种的事情阻挠,导致项目的进度拖延了很多,但好在我一步一步的坚持了下来,一开始其实也就是想着把这个项目直接简易的做完,快点上传项目,写出博客,进行下一个项目。但是做的途中,发现了很多可以优化,美观的点,都进行了尝试,虽然花了很久时间,但是获得的收获也是不可比拟的。现在我最大的感受也就是,花时间做十个一般的作品不如花好时间做一个优质的作品。只有沉下心来去做才能得到最大的收获以及满足
至此整个项目已经完成,项目的地址如下 地址,请期待其他CS50作业的作品。如果喜欢的话请点个赞哦❤️❤️。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。