歡迎查看本系列的其他文章:
Django實現迷你淘寶(五) — 基於jQuery和AJax的購物車設計與實現
文章目錄
本文代碼
1. 購物車設計
先來看看最終效果
1.1 修改模型
分析一下購物車對應的數據庫模型,首先必要的信息就是商品信息(如:名字,價格)和對應的數量;其次,一個購物車可能會有多個商品。這裏簡單起見,我只新建了一個Order類,代表了一個商品和其對應的數量。修改taobao/models.py
添加以下內容:
class Order(models.Model):
# user info
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="orders")
item = models.ForeignKey(Item, on_delete=models.SET_NULL, null=True)
item_cnt = models.IntegerField(default=1)
# return the total price for current order
def total(self):
return self.item_cnt * self.item.price
def __str__(self):
return "<" + str(self.item_id) + ', ' + str(self.item_cnt) + ">"
有了該模型,用戶每次添加到購物車我們就只需要創建一個新的Order
,或者修改已有Order
的數量。
修改taobao/views.py
裏面的item_detail
函數(關鍵部分都已經添加了註釋)
def item_detail(request, item_id):
item = Item.objects.get(pk=item_id)
context = {}
if request.method == "POST":
if not request.user.is_authenticated:
return redirect(reverse("login"))
cnt = int(request.POST["count"])
if request.POST["action"] == "buy":
# do nothing for now
print("buy")
else:
try:
# try to get an existing order
exist_order = Order.objects.get(owner=request.user, item=item, package__isnull=True)
exist_order.item_cnt += cnt
exist_order.save()
except Order.DoesNotExist:
# create a new order
order = Order(owner=request.user, item=item, item_cnt=cnt)
order.save()
return render(request, "taobao/success.html", context)
else:
context["item"] = item
return render(request, "taobao/item_detail.html", context)
1.2 HTML模板文件
這裏就不詳細敘述HTML的具體代碼了,有興趣的同學可以照着最終效果嘗試實現一下。
{% extends "users/base.html" %}
{% block content %}
<h1>購物車</h1>
<!-- order list -->
{% if orders %}
<!-- NOTE: nested form is not allowed!!! -->
<form id="form_orders" name="form_orders" action="{% url 'shop_cart' %}" method="post">
{% csrf_token %}
<div class="table-responsive mt-4 table-hover">
<table id="order_table" class="table">
<thead>
<tr class="row text-center">
<th class="col-1">
<input class="form-check-input" type="checkbox" id="check_all">
<label class="form-check-label" for="check_all">#</label>
</th>
<!-- thumbnail + description -->
<th class="col-3 text-left">
<a href="#" style="color: #080000;">商品
<i class="fa fa-sort"></i>
</a>
</th>
<th class="col-2">
<a href="#" style="color: #080000;">數量
<i class="fa fa-sort"></i>
</a>
</th>
<th class="col-2">
<a href="#" style="color: #080000;">價格
<i class="fa fa-sort"></i>
</a>
</th>
<th class="col-2">
<a href="#" style="color: #080000;">總計
<i class="fa fa-sort"></i>
</a>
</th>
<!-- delete button -->
<th class="col-2"></th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr class="row text-center border-bottom">
<td class="col-1 align-self-center border-top-0">
<input class="form-check-input" type="checkbox" value="{{ order.id }}"
name="checked_orders" id="checkbox{{ order.id }}">
<label class="form-check-label"
for="checkbox{{ order.id }}"> {{ forloop.counter }}</label>
</td>
<td class="col-3 text-left border-top-0">
<img class="img-thumbnail" style="width: 50px; height: 50px" src="{{ order.item.img }}">
{{ order.item.description }}
</td>
<td class="col-2 align-self-center border-top-0">
<button type="button" class="btn" onclick="change_cnt({{ order.id }}, false)">
<i class="fa fa-minus-square-o"></i>
</button>
<span id="cnt{{ order.id }}">{{ order.item_cnt }}</span>
<button type="button" class="btn" onclick="change_cnt({{ order.id }}, true)">
<i class="fa fa-plus-square-o"></i>
</button>
</td>
<td class="col-2 align-self-center border-top-0">
$ {{ order.item.price|floatformat:2 }}</td>
<td class="col-2 align-self-center border-top-0">
$
<span id="total_order{{ order.id }}">{{ order.total|floatformat:2 }}</span>
</td>
<td class="col-2 align-self-center border-top-0">
<button onclick="delete_order({{ order.id }})" class="btn btn-outline-secondary">
刪除
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- total price -->
<div class="row">
<div class="col-5 offset-7 text-right mt-4 mb-4">
<b>總計(所有選中商品): </b>
<span style="color: red">$</span>
<span id="total_cart" style="color: red">{{ total|floatformat:2 }}</span>
</div>
</div>
<div class="text-right">
<button onclick="check_out()" class="btn btn-primary"><i class="fa fa-dollar"></i> 結算</button>
</div>
</form>
{% else %}
<h4 class="m-4">這裏空空如也,
<a href="{% url 'home' %}">去買點什麼吧。</a>
</h4>
{% endif %}
{% endblock content %}
<!-- javascript -->
{% block script %}
<script type="text/javascript">
function delete_order(order_id) {
add_operation_type(document.form_orders, "delete")
const id_field = document.createElement("input")
id_field.type = "hidden"
id_field.name = "order_id"
id_field.value = order_id
document.form_orders.appendChild(id_field)
document.form_orders.submit();
}
function check_out() {
add_operation_type(document.form_orders, "checkout")
document.form_orders.submit()
}
function add_operation_type(element, type) {
const operation_field = document.createElement("input");
operation_field.type = "hidden"
operation_field.name = "operation"
operation_field.value = type
element.appendChild(operation_field)
}
function change_cnt(id, isAdd) {
}
// calculate the total price
function cal_total() {
}
</script>
{% endblock script %}
這裏有很多script函數是空的,別急,後面會一一實現。
1.3 視圖函數
對應的視圖函數主要處理幾種情況:刪除商品,結算,計算總價(所有選中的商品)。
@login_required
def shop_cart(request):
orders = Order.objects.filter(owner=request.user)
if request.method == 'POST':
operation = request.POST["operation"]
# user delete some order
if operation == "delete":
oid = request.POST["order_id"]
orders.get(pk=oid).delete()
elif operation == "checkout":
# get all checked orders
checked_orders = request.POST.getlist("checked_orders")
print(checked_orders)
# will only create a new package when at least one order is chosen
if len(checked_orders) > 0:
return render(request, "taobao/success.html")
# api for calculating the total price
elif operation == "cal_total" and request.is_ajax():
checked_orders = request.POST.getlist("checked_orders")
total = 0.0
for o in checked_orders:
total += orders.get(pk=o).total()
return JsonResponse({"total_cart": ("%.2f" % total)})
total = 0
for o in orders:
total += o.total()
context = {"orders": orders, "total": total}
return render(request, "taobao/shopping_cart.html", context)
注:別忘了去urls.py
裏面“註冊”一下該函數
介紹完基本的購物車樣式,下面就來一一實現具體的功能吧~
2. 刪除商品
刪除功能比較好實現,我們只需要在用戶點擊刪除按鈕時,向表單裏面插入一些隱藏的輸入信息,一併提交,然後代碼裏獲取相關數據即可。
- 首先通過
onclick="delete_order({{ order.id }})"
,使得用戶每次點擊刪除按鈕時,都會調用該javaScript函數並傳入待刪除的訂單id - 然後在
delete_order
函數裏面,首先插入一個operation說明是想“刪除”,然後再插入一個訂單id的信息,最後一併提交整個表單 - 視圖函數裏面,檢測到operation是delete的話,就獲取對應的訂單id,然後刪除即可
3. 動態修改商品數量
細心的小夥伴可能已經發現,我們數量左右的加減按鈕是用<button>
實現的,所以一個很直接的想法就是,使用和刪除商品類似的方案,調用一個JavaScript函數,然後向表單裏面插入一些信息,再提交。原理上來說這個方案是完全可行的,但是面臨着一個問題,就是“頁面刷新”。注意,至今爲止,我們的視圖函數都是直接返回一個新的html(整個頁面),所以前端會刷新整個頁面,而這個在某些情況下十分的用戶不友好,試想你滾動到了頁面底端,修改了一個商品數量,結果整個頁面刷新並回到頂端,是不是很“詭異”。
仔細分析需求我們會發現,我們需要刷新的內容只有,該商品總價,該商品數量和購物車總價三個變量。所以,這裏我們使用AJax來實現頁面的局部刷新。
3.1 修改數量API
首先,我們需要有一個API,能夠修改某一個商品的數量(加一或減一)。
在views.py
裏面添加一個新的函數change_cnt()
@login_required
def change_cnt(request):
if request.is_ajax() and request.method == "POST":
order_id = request.POST["order_id"]
operation = request.POST["operation"]
total_cart = float(request.POST["total_cart"])
order = Order.objects.get(pk=order_id)
# lower and upper limit --- 1 ~ 99
if operation == "add" and order.item_cnt < 99:
order.item_cnt += 1
order.save()
total_cart += order.item.price
elif operation == "minus" and order.item_cnt > 1:
order.item_cnt -= 1
order.save()
total_cart -= order.item.price
data = {
# latest count
"cnt": order.item_cnt,
# total price for the order
"total_order": ("%.2f" % order.total()),
# total price for all
"total_cart": ("%.2f" % total_cart)
}
return JsonResponse(data)
return JsonResponse({})
整體思路也很簡單:
- 判斷請求是否爲
POST
並且是AJax
- 獲取當前數據:訂單ID,具體操作(加or減),當前購物車總價
- 根據訂單ID獲得具體的對象,並執行對應操作
- 返回最新數據:最新訂單數量,該商品總價,購物車總價
這裏比較有趣的一個點是,我們的返回值不再是render
或者redirect
,而是一個JsonResponse
,只包含了我們感興趣的數據,而不是整個html頁面,所以前端可以從這裏面獲得感興趣的內容並且更新對應元素。
注:別忘了去urls.py
裏面“註冊”一下該函數
3.2 引入jQuery
爲了使用AJax和jQuery我們首先需要下載最新的jQuery並引入項目。
- 下載最新的jQuery,地址
- 放到
taobao/static/js
目錄下面 - 添加
<script src="{% static "js/jquery-3.5.0.min.js" %}"></script>
到base.html
裏面(放到最下面,緊跟着bootstrap的三個script)
3.3 AJax異步請求
有了API之後,我們就可以在前端調用這個API了。需要注意的是,我們最好使用異步的方式,因爲我們不知道調用這個API需要花費多少時間(網絡狀態,服務器荷載等),但是我們不希望前端頁面“卡住”,所以我們需要使用異步操作。
添加一下內容到JavaScript的change_cnt
函數裏面
function change_cnt(id, isAdd) {
let opera;
if (isAdd) {
opera = "add";
} else {
opera = "minus";
}
const total = $("#total_cart").text();
// use ajax to communicate with backend, change the count of specific order
const config = {
"url": "{% url "change_cnt" %}",
"async": true,
"type": "post",
"dataType": "json",
"data": {
"order_id": id,
"operation": opera,
"total_cart": total,
"csrfmiddlewaretoken": "{{ csrf_token }}"
},
"success": function (result) {
$("#total_order" + id).text(result["total_order"]);
$("#cnt" + id).text(result["cnt"]);
// only update the total price if this order is checked
if ($("#checkbox" + id).is(":checked")) {
$("#total_cart").text(result["total_cart"]);
}
},
"error": function (xhr, status, error) { }
};
$.ajax(config);
}
這裏我們使用了AJax來進行異步請求,並使用了一些jQuery的語法來獲取某些元素的當前值。
- 利用
$("#total_cart").text();
來獲取當前的購物車總金額 - 注:jQuery裏面使用
$("#<id>");
來通過id定位一個元素(別忘了那個**#**)
const config = {
"url": "{% url "change_cnt" %}",
"async": true,
"type": "post",
"dataType": "json",
"data": {
"order_id": id,
"operation": opera,
"total_cart": total,
"csrfmiddlewaretoken": "{{ csrf_token }}"
},
"success": function (result) {
$("#total_order" + id).text(result["total_order"]);
$("#cnt" + id).text(result["cnt"]);
// only update the total price if this order is checked
if ($("#checkbox" + id).is(":checked")) {
$("#total_cart").text(result["total_cart"]);
}
},
"error": function (xhr, status, error) { }
};
- 這裏整個
config
變量使用鍵值 對的形式來配置AJax的內容url
設置該請求的目標地址async
設置該請求爲異步請求(與之對應的爲同步/阻塞方式)type
設置該請求使用POST方式(與之對應的爲GET方式)dataType
設置我們期待的返回值爲JSON格式data
包含了所有的數據success
成功的回調函數,假如請求返回成功,會調用該函數(在這個函數裏面更新UI)- 這裏的
result
是一個字典,我們可以通過result["xxx"]
獲得裏面的值 - 同時我們可以通過jQuery獲得任意一個元素,並通過
.text()
修改元素的值
- 這裏的
error
請求失敗的回調函數
- 利用
$.ajax(config);
來提交AJax請求
現在,保存並運行程序,去享受你的成果吧!
4. 動態選擇商品
我們不可能每次結算都“清空”購物車(雖然這是夢想,但是現實很骨感),所以我們需要有“選中”功能(包括全選,反選等),用戶可以選中這次想購買的物品,並結算,其他商品不受影響。
仔細分析一下需求,我們至少需要做到:
- 全選(全不選)整個購物車
- 選擇任意商品
- 購物車總金額根據選擇的商品動態變化
- 全選框會根據當前狀態動態調整(如,你手動勾選了所有商品,那麼此時全選框也應該被自動勾選)
- 用戶每次刷新頁面默認全選購物車(也可以默認全不選)
下面我們來一一實現這些功能,注意UI部分(checkbox)我們已經在前面的章節實現了,所以這裏我們主要看背後的邏輯函數。
4.0 計算總價函數
首先我們先補全之前的cal_total()
函數,用於計算並更新當前購物車所有選中商品的總價。
function cal_total() {
add_operation_type(document.form_orders, "cal_total")
const config = {
"url": "{% url "shop_cart" %}",
"async": true,
"type": "post",
"dataType": "json",
"data": $("#form_orders").serialize(),
"success": function (result) {
$("#total_cart").text(result["total_cart"]);
},
"error": function (xhr, status, error) { }
};
$.ajax(config);
}
這個函數很簡單,和修改商品數量基本一樣,發送整個表單到後臺,後臺計算總額之後,返回結果並局部刷新總金額。
比較有趣的一點就是,這裏我們使用$("#form_orders").serialize()
來把整個表單序列化並傳回後臺,這樣後臺就可以和處理普通表單提交一樣的方式處理。
這個函數就實現了功能3(動態更新總價),我們只需要在每個可能影響總價的操作最後調用一下這個函數即可。
4.1 全選(全不選)購物車
這個功能實現起來也比較簡單,基本思路就是監聽全選框,一旦被勾選(或者取消),就自動勾選(取消)所有的checkbox。具體實現代碼如下:
$("#check_all").on("click", function () {
if (this.checked) {
// use "prop" to set the value of all checkboxes
$(":checkbox").prop("checked", true);
} else {
$(":checkbox").prop("checked", false);
}
// refresh total price
cal_total();
});
這裏我們用$("#check_all")
來獲得全選框這個元素,然後通過.on()
函數來監聽click
事件。修改完所有的checkbox狀態之後,我們再重新計算總價。
4.2 選擇任意商品
這裏由於我們不僅要實現可選擇任意商品(很容易實現,checkbox本身就支持任意選擇),我們更需要的是選中(取消)任意一個checkbox之後,檢測所有checkbox的狀態,並據此更新全選框的狀態。
const checkboxes = $("input[name='checked_orders']");
checkboxes.on("click", function () {
if (this.checked) {
let i = 0;
for (i = 0; i < checkboxes.length; i++) {
if (!checkboxes[i].checked) {
break;
}
}
if (i === checkboxes.length) {
$("#check_all").prop("checked", true);
}
} else {
$("#check_all").prop("checked", false);
}
// refresh total price
cal_total();
});
這裏我們首先通過const c heckboxes = $("input[name='checked_orders']");
獲得所有的名字爲checked_orders的checkbox,並對他們進行監聽,一旦監聽到任何一個checkbox被選中,則遍歷所有的checkbox,假如都被選中了,就把全選框選中,否則保持現狀;假如任何一個checkbox被取消選中,則取消全選框的選中狀態。同樣的,函數最後調用cal_total();
來更新購物車總價。
4.3 默認全選(全不選)
這裏基本的實現也很簡單,我們只需要等待整個頁面加載完成後(確保所有元素都已經渲染完成),把所有checkbox的狀態都改爲選中(或未選中)。
那麼關鍵問題就在於,如何知道頁面什麼時候加載完成,別擔心,jQuery早就幫我們弄好了。
$(function () {
// do something
})
這個函數會在頁面加載完成後被自動調用,所以我們只需要在這裏面執行我們的操作即可。添加以下兩行:
$(function () {
$(":checkbox").prop("checked", true);
cal_total();
})
5. 表格排序
有一個也比較常見的功能就是表格排序,用戶可以通過點擊表頭的任意列,從而根據該列的值進行排序。既然這個功能如此常見,沒錯,jQuery已經有了對應的插件了。下面來一步步看看如何使用
- 下載插件tablesorter
- js文件放到
taobao/static/js
路徑下面(與jQuery文件在一起) - 購物車的HTML文件裏面添加一下代碼(script裏面)
{% load static %}
<script src="{% static "js/jquery.tablesorter.js" %}"></script>
- (可選):在頁面加載完成的回調函數裏面配置tablesorter,如:有時候我們只需要某幾列可以排序,其他的不可以
$(function () {
$(":checkbox").prop("checked", true);
cal_total();
$("#order_table").tablesorter({
headers: {
0: {sorter: false}
}
});
})
這裏我們使得第一列(編號)無法排序,其他默認都可以用於排序。
至此,一個基本的購物車就已經完成了,所有的基本功能都已經實現,謝謝每一個耐心看到最後的讀者。
本文代碼