Django實現迷你淘寶(五) --- 基於jQuery和AJax的購物車設計與實現

歡迎查看本系列的其他文章:

  1. postgres安裝與入門
  2. django安裝與入門
  3. 基於django的用戶驗證系統實現
  4. 基於Bootstrap的商品頁面設計與美化
  5. 基於jQuery和AJax的購物車設計與實現

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">
                                &dollar; {{ order.item.price|floatformat:2 }}</td>
                            <td class="col-2 align-self-center border-top-0">
                                &dollar;
                                <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">&dollar;</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({})

整體思路也很簡單:

  1. 判斷請求是否爲POST並且是AJax
  2. 獲取當前數據:訂單ID,具體操作(加or減),當前購物車總價
  3. 根據訂單ID獲得具體的對象,並執行對應操作
  4. 返回最新數據:最新訂單數量,該商品總價,購物車總價

這裏比較有趣的一個點是,我們的返回值不再是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. 動態選擇商品

我們不可能每次結算都“清空”購物車(雖然這是夢想,但是現實很骨感),所以我們需要有“選中”功能(包括全選,反選等),用戶可以選中這次想購買的物品,並結算,其他商品不受影響。
仔細分析一下需求,我們至少需要做到:

  1. 全選(全不選)整個購物車
  2. 選擇任意商品
  3. 購物車總金額根據選擇的商品動態變化
  4. 全選框會根據當前狀態動態調整(如,你手動勾選了所有商品,那麼此時全選框也應該被自動勾選)
  5. 用戶每次刷新頁面默認全選購物車(也可以默認全不選)

下面我們來一一實現這些功能,注意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已經有了對應的插件了。下面來一步步看看如何使用

  1. 下載插件tablesorter
  2. js文件放到taobao/static/js路徑下面(與jQuery文件在一起)
  3. 購物車的HTML文件裏面添加一下代碼(script裏面)
{% load static %}
<script src="{% static "js/jquery.tablesorter.js" %}"></script>
  1. (可選):在頁面加載完成的回調函數裏面配置tablesorter,如:有時候我們只需要某幾列可以排序,其他的不可以
$(function () {
    $(":checkbox").prop("checked", true);
    cal_total();
    $("#order_table").tablesorter({
        headers: {
            0: {sorter: false}
        }
    });
})

這裏我們使得第一列(編號)無法排序,其他默認都可以用於排序。

至此,一個基本的購物車就已經完成了,所有的基本功能都已經實現,謝謝每一個耐心看到最後的讀者。
本文代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章