2025年7月25日 星期五

使用ASP.NETCore MVC設計電子商務網站 - 2


這一篇文章中,將延續《使用ASP.NETCore MVC設計電子商務網站 - 1》一文的專案情境,說明如何使用ASP.NET Core MVC這個框架,來設計一個簡易的電子商務購物網站。前一期的文章中我們建立了網站所需的「CartItem」「Product」模型 ,同時設計好控制器的程式碼,現在我們來看看檢視(View)的設計。


.NET Magazine國際中文電子雜誌
作者:許薰尹 | 審稿:張智凱 | 文章編號:N250727501 | 出刊日期:2025/7/9


設計「Cart」檢視

「Cart」檢視用於顯示電子商務應用程式中的購物車內容。我們要以表格形式顯示購物車中的商品列表,包括商品資訊、價格、數量和每項商品的小計。此外也要提供調整數量和移除商品的選項。頁面還得計算並顯示購物車中所有商品的總計。

建立「Cart」檢視。將游標停留在「HomeController」控制器程式設計畫面「Cart」方法之中,按滑鼠右鍵,從快捷選單選取「Add View」,請參考下圖所示。



圖 1:加入檢視


在「Add New Scaffolded Item」視窗選取「Razor View」,請參考下圖所示:

圖 2:「Add New Scaffolded Item」視窗


在「Add Razor View」對話盒中,設定:

  • View name:「Cart」
  • Template:「Empty (without model)」
  • 勾選「Use a layout page」核取方塊


然後按下「Add」按鈕。Visual Studio 2022便會在「Views\Home」資料夾下,新增一個「Cart.cshtml」檔案,請參考下圖所示:

圖 3:「Add Razor View」對話盒


在「Cart 」檢視加入以下程式碼:

Cart.cshtml

@model IEnumerable<MVCShopping.Models.CartItem>
@{
  ViewData[ "Title" ] = "購物車";
}
<div class = "container mt-4">
  <!-- 購物車圖示和標題文字 -->
  <div class = "row mb-4">
    <div class = "col">
      <h2 class = "text-center">
        <i class = "bi bi-cart3 text-primary me-2"> </i> 購物車內容
      </h2>
    </div>
  </div>
  <!-- 如果購物車是空的 -->
  @if ( !Model.Any( ) ) {
    <div class = "text-center py-5">
      <!-- 顯示空購物車圖示 -->
      <i class = "bi bi-cart-x text-muted" style = "font-size: 4rem;"></i>
      <!-- 顯示購物車是空的訊息 -->
      <h3 class = "mt-3 text-muted"> 購物車是空的 </h3>
      <a href = "@Url.Action("Index")" class = "btn btn-primary mt-3">
        <!-- 繼續購物按鈕 -->
        <i class = "bi bi-arrow-left me-2"> </i> 繼續購物
      </a>
    </div>
  }
  else {
    <!-- 如果購物車有商品 , 使用卡片顯示商品-->
    <div class = "card shadow-sm border-0">
      <div class = "card-body">
        <!-- 卡片內容 -->
        <div class = "table-responsive">
          <!-- 建立一個表格 -->
          <table class = "table align-middle">
            <!-- 表格標題 -->
            <thead class = "table-light">
              <tr>
                <th>商品資訊</th>
                <th class = "text-center"> 單價 </th>
                <th class = "text-center"> 數量 </th>
                <th class = "text-center"> 動作 </th>
                <th class = "text-end"> 小計 </th>
              </tr>
            </thead>
            <tbody>
              <!-- 表格內容 -->
              @foreach ( var item in Model ) {
                <!-- 每個商品的<tr>,包含商品ID -->
                <tr data-product-id = "@item.Product.Id">
                  <!-- 商品資訊欄 -->
                  <td>
                    <div class = "d-flex align-items-center">
                      <!-- 商品圖片 -->
                      <img src = "@item.Product.ImageUrl" alt = "@item.Product.Name"
                           class = "rounded me-3" style = "width: 80px; height: 60px; object-fit: cover;">
                      <div>
                        <!-- 商品名稱 -->
                        <h6 class = "mb-1"> @item.Product.Name </h6>
                        <!-- 商品描述 -->
                        <small class = "text-muted"> @item.Product.Description </small>
                      </div>
                    </div>
                  </td>
                  <!-- 單價欄 -->
                  <td class = "text-center"> NT$ @item.Product.Price.ToString( "N0" ) </td>
                  <!-- 數量欄-->
                  <td class = "text-center" style = "width: 200px;">
                    <!-- 數量輸入方塊 -->
                    <div class = "input-group" style = "width: 150px; margin: 0 auto;">
                      <!-- 減少數量按鈕 -->
                      <button class = "btn btn-outline-secondary quantity-minus" type = "button">
                        <!-- 減少數量圖示 -->
                        <i class = "bi bi-dash"> </i>
                      </button>
                      <!-- 數量輸入方塊 -->
                      <input type = "number" class = "form-control text-center quantity-input"
                             value = "@item.Quantity" min = "1" max = "99"
                             data-product-id = "@item.Product.Id">
                      <!-- 增加數量按鈕 -->
                      <button class = "btn btn-outline-secondary quantity-plus" type = "button">
                        <!-- 增加數量圖示 -->
                        <i class = "bi bi-plus"> </i>
                      </button>
                    </div>
                  </td>
                  <!-- 動作欄 -->
                  <td class="text-center" style = "width: 200px;">
                    <!-- 移除商品按鈕 -->
                    <button class = "btn btn-danger btn-sm remove-item" data-product-id = "@item.Product.Id" type = "button">
                      <!-- 移除圖示和文字 -->
                      <i class = "bi bi-trash"></i> 移除
                    </button>
                  </td>
                  <!-- 小計欄 -->
                  <td class = "text-end"> NT$ @(( item.Product.Price * item.Quantity ).ToString( "N0" )) </td>
                </tr>
              }
            </tbody>
            <!-- 表格底部 -->
            <tfoot class = "table-light">
              <tr>
                <!-- 總計欄 -->
                <td colspan = "3" class = "text-end"> <strong> 總計: </strong> </td>
                <td class = "text-end">
                  <!-- 計算總計 -->
                  <strong class = "text-primary fs-4">
                    NT$ @Model.Sum( i => i.Product.Price * i.Quantity ).ToString( "N0" )
                  </strong>
                </td>
              </tr>
            </tfoot>
          </table>
        </div>
      </div>
    </div>
    <div class = "d-flex justify-content-between mt-4">
      <!-- 繼續購物按鈕 -->
      <a href = "@Url.Action("Index")" class = "btn btn-outline-primary">
        <i class = "bi bi-arrow-left me-2"> </i> 繼續購物
      </a>
      <!-- 結帳按鈕 -->
      <button class = "btn btn-primary">
        <i class = "bi bi-credit-card me-2"> </i >結帳
      </button>
    </div>
  }
</div>

@section Scripts {
  <script>
    document.addEventListener('DOMContentLoaded', function () {
        document.querySelectorAll('.quantity-minus, .quantity-plus').forEach(button => {
            button.addEventListener('click', function() {
                const input = this.parentElement.querySelector('.quantity-input');
                let value = parseInt(input.value);
                if ( this.classList.contains('quantity-minus') ) {
                    value = Math.max( 1, value - 1 );
                } else {
                    value = Math.min( 99, value + 1 );
                }
                input.value = value;
                updateQuantity(input);
            });
        });
        document.querySelectorAll('.quantity-input').forEach(input => {
            input.addEventListener('change', function() {
                updateQuantity( this );
            });
        });
        document.querySelectorAll('.remove-item').forEach(button => {
            button.addEventListener('click', function() {
                const productId = this.dataset.productId;
                removeItem( productId );
            });
        });
        function updateQuantity(input) {
            const productId = input.dataset.productId;
            let quantity = parseInt( input.value );
            if (isNaN(quantity) || quantity < 1) quantity = 1;
            if (quantity > 99) quantity = 99;
            input.value = quantity;
            fetch('/Home/UpdateCartQuantity', {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: `id=${productId}&quantity=${quantity}`
            })
            .then( response => response.json())
            .then( data => {
                if ( data.success ) {
                    location.reload();
                } else {
                    alert( data.message );
                }
            })
            .catch( error => {
                alert( '操作失敗,請稍後再試' );
            });
        }
        function removeItem(productId) {
           fetch('/Home/RemoveFromCart', {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: `id=${productId}`
            })
            .then( response => response.json() )
            .then( data => {
                if ( data.success ) {
                    location.reload();
                } else {
                    alert( data.message );
                }
            })
            .catch( error => {
                alert( '操作失敗,請稍後再試' );
            });
        }
    });
  </script>
}

設計「Index」檢視

「Index」檢視是網站的首頁,用於顯示電子商務應用程式中的商品清單。頁面需要展示每個商品的圖片、名稱、描述和價格,並提供一個按鈕讓使用者將商品加入購物車。當使用者點擊加入購物車按鈕時,會發送 POST 請求到伺服器以更新購物車,並顯示一個對話框通知使用者操作結果。這個頁面旨在提供一個直觀的介面,讓使用者可以輕鬆瀏覽和購買商品。


修改「Index」檢視加入以下程式碼:

Index

@model IEnumerable<MVCShopping.Models.Product>
@{
  ViewData[ "Title" ] = "電子產品清單";
}
<div class = "container mt-4">
  <div class = "row mb-4">
    <div class = "col">
      <h2 class = "text-center mb-4">
        <i class = "bi bi-laptop text-primary"> </i> 精選商品
      </h2>
    </div>
  </div>
  <div class = "row row-cols-1 row-cols-md-3 g-4">
    @foreach ( var product in Model ) {
      <div class = "col">
        <div class = "card h-100 border-0 rounded-3">
          <div class = "position-relative">
            <!-- 商品圖片 -->
            <img src = "@product.ImageUrl" class = "card-img-top rounded-top product-image" alt = "@product.Name" style = "width: 400px; height: 300px;">
            <div class = "position-absolute top-0 end-0 m-2">
              <!-- 熱銷標籤 -->
              <span class = "badge bg-primary rounded-pill">
                <i class = "bi bi-tag-fill"> </i>  熱銷
              </span>
            </div>
          </div>
          <div class = "card-body">
            <!-- 商品名稱 -->
            <h5 class = "card-title">
              <i class = "bi bi-phone me-2 text-primary"> </i> @product.Name
            </h5>
            <!-- 商品描述 -->
            <p class = "card-text text-muted">
              <i class = "bi bi-info-circle me-2"> </i> @product.Description
            </p>
            <div class = "d-flex justify-content-between align-items-center">
              <!-- 商品價格 -->
              <h4 class = "text-primary mb-0">
                <i class = "bi bi-currency-dollar"> </i> @product.Price.ToString( "N0" )
              </h4>
              <!-- 加入購物車按鈕 -->
              <form class = "add-to-cart-form" data-product-id = "@product.Id">
                <button type = "submit" class = "btn btn-primary">
                  <i class = "bi bi-cart-plus me-2"> </i> 加入購物車
                </button>
              </form>
            </div>
          </div>
        </div>
      </div>
    }
  </div>
</div>
<!-- Bootstrap 對話盒 -->
<div class = "modal fade" id = "cartModal" tabindex = "-1" aria-labelledby = "cartModalLabel" aria-hidden = "true">
  <div class = "modal-dialog">
    <div class = "modal-content">
      <div class = "modal-header bg-primary text-white">
        <h5 class = "modal-title" id = "cartModalLabel">
          <i class = "bi bi-cart-check me-2"> </i> 購物車訊息
        </h5>
        <button type = "button" class = "btn-close btn-close-white" data-bs-dismiss = "modal" aria-label = "Close"> </button>
      </div>
      <div class = "modal-body">
        <p id = "modalMessage" class = "mb-0"> </p>
      </div>
      <div class = "modal-footer">
        <button type = "button" class = "btn btn-primary" data-bs-dismiss = "modal">
          <i class = "bi bi-check2 me-2"> </i> 確定
        </button>
      </div>
    </div>
  </div>
</div>

@section Scripts {
  <script>
    document.addEventListener('DOMContentLoaded', function () {
      const cartModal  =  new bootstrap.Modal(document.getElementById('cartModal'));
      document.querySelectorAll('.add-to-cart-form').forEach( form => {
        form.addEventListener('submit', function (e) {
          e.preventDefault();
          const productId = this.getAttribute('data-product-id');
          fetch('/Home/AddToCart', {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: `id=${productId}`
          })
          .then( response => response.json() )
          .then( data => {
            document.getElementById('modalMessage').innerHTML = `
              <i class="bi bi-check-circle-fill text-success me-2"></i>${data.message}
            `;
            cartModal.show();
            updateCartBadge(data.cartQuantity);
          })
          .catch( error => {
            document.getElementById('modalMessage').innerHTML = `
              <i class="bi bi-exclamation-circle-fill text-danger me-2"></i>操作失敗,請稍後再試
            `;
            cartModal.show();
          });
        });
      });
    });
    function updateCartBadge( quantity ) {
      const cartBadge = document.querySelector('.navbar .badge');
      if ( cartBadge ) {
        cartBadge.textContent = quantity;
      }
    }
  </script>
}

設計版面配置

「_Layout」檢視定義電子商務應用程式的整體頁面結構和樣式,如導覽列。這個檔案引用了「Bootstrap」、「bootstrap-icons」的 CSS 和 JavaScript 檔案,以確保頁面具有一致的外觀和互動性。

_Layout.cshtml

<!DOCTYPE html>
<html lang = "en">
<head>
  <meta charset = "utf-8" />
  <meta name = "viewport" content = "width = device-width, initial-scale = 1.0" />
  <title> @ViewData[ "Title" ] – MVCShopping </title>
  <!--引用Bootstrap CSS檔案-->
  <link rel = "stylesheet" href = "~/lib/bootstrap/dist/css/bootstrap.min.css" />
  <!--引用Bootstrap icons 檔案-->
  <link href = "~/lib/bootstrap-icons/font/bootstrap-icons.min.css" rel = "stylesheet" />
  <link rel = "stylesheet"  href  = "~/css/site.css"  asp-append-version  = "true" />
  <link rel = "stylesheet"  href  = "~/MVCShopping.styles.css"  asp-append-version  = "true" />
  <style>
    .navbar-custom {
      background: linear-gradient(135deg, #13547a 0%, #80d0c7 100%);
    }
    .footer {
      background: linear-gradient(135deg, #13547a 0%, #80d0c7 100%);
      color: white !important;
    }
    .card {
      transition: transform 0.2s;
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    }
      .card:hover {
        transform: translateY(-5px);
      }
  </style>
</head>
<body class = "bg-light">
  <header>
    <!-- 導覽列 -->
    <nav class = "navbar navbar-expand-sm navbar-dark navbar-custom">
      <div class = "container">
        <a class = "navbar-brand" asp-area = "" asp-controller = "Home" asp-action = "Index">
          <i class = "bi bi-shop me-2"> </i> EShop
        </a>
        <button class = "navbar-toggler" type = "button" data-bs-toggle = "collapse" data-bs-target = ".navbar-collapse">
          <span class = "navbar-toggler-icon"></span>
        </button>
        <div class = "navbar-collapse collapse d-sm-inline-flex justify-content-between">
          <ul class = "navbar-nav flex-grow-1">
            <li class = "nav-item">
              <!--首頁連結-->
              <a class = "nav-link text-white" asp-controller = "Home" asp-action = "Index">
                <i class = "bi bi-house-door me-1"> </i> 首頁
              </a>
            </li>
          </ul>
          <ul class = "navbar-nav">
            <li class = "nav-item">
              <!--購物車連結-->
              <a class = "nav-link text-white position-relative" asp-controller = "Home" asp-action = "Cart">
                <i class = "bi bi-cart3 fs-5"></i>
                <!--顯示購物車數量-->
                <span class = "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" id = "cart-badge">
                  @(ViewBag.CartQuantity ?? 0)
                </span>
              </a>
            </li>
          </ul>
        </div>
      </div>
    </nav>
  </header>
 <!--View的內容-->
  <div class = "container mt-4">
    <main role = "main" class = "pb-3">
      @RenderBody( )
    </main>
  </div>
 <!--頁尾-->
  <footer class = "footer border-top text-muted py-3 mt-4">
    <div class = "container text-center text-white">
      <i class = "bi bi-heart-fill text-danger"> </i> @DateTime.Now.Year - EShop - 您的線上購物首選
    </div>
  </footer>
  <script src = "~/lib/jquery/dist/jquery.min.js"> </script>
  <!--引用Bootstrap JavaScript檔案-->
  <script src = "~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"> </script>
  <script src  = "~/js/site.js" asp-append-version = "true"> </script>
  @await RenderSectionAsync( "Scripts" , required: false )
</body>
</html>

網站測試

在Visual Studio 開發工具執行這個專案,首頁可以看到商品清單,點選其中一個商品的「加入購物車」按鈕,網頁上方就會跳出確認對話盒,請參考下圖所示:

圖 4:「加入購物車」

您可以點選畫面右上方購物車圖示,便可看到目前購物車的內容,請參考下圖所示:

圖 5:購物車的內容

0 意見:

張貼留言