案例:留言板
-
在「實戰:留言板」章節已經大致說明了留言板的程式架構,接著來進行版面美化的工作。在這個案例中,同樣直接透過 CDN 來引用 Bootstrap 框架,同時為了讓頁面看起來更活潑,也會引用 Font Awesome 在頁面加上一些圖示。
Font Awesome 是什麼
Font Awesome 也是一個 CSS 框架,比較特別的是它提供了一個風格統一的圖示庫,讓網頁設計師可以方便地在頁面上擺放圖示。
官網:https://fontawesome.com/
引用 Bootstrap 與 FontAwesome 框架
如同前一個案例,先修改 `web/templates/base.html`:
<!DOCTYPE html> <html lang="zh-hant"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- 引用 Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> <!-- FontAwesome --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer" /> <title>留言板</title> </head> <body> <div class="container"> <div>{% block content %}{% endblock %}</div> </div> <!-- Bootstrap library --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script> </body> </html>
- ++第 7 行++,透過 CDN 引用 Bootstrap CSS 框架
- ++第 9 行++,透過 CDN 引用 FontAwesome CSS 框架,以便之後在頁面上加上圖示
- ++第 13 與 15 行++,在 block content 外加一個容器,用來框住頁面主要內容並在周圍留白
- ++第 17 行++,透過 CDN 載入 Bootstrap Javascript Library【注意】
原本的「首頁」、「新增留言」、「登入」/「登出」等連結暫且移除,稍候製作導覽列時再將其移入。
加入頁面導覽列
新增 `web/templates/navbar.html` 檔案,並鍵入以下內容:
<nav class="navbar navbar-expand-sm bg-dark mb-3" data-bs-theme="dark"> <div class="container-fluid"> <!-- 網站標誌 --> <div class="navbar-brand">留言板</div> <!-- 展開/收合切換鈕(畫面小時才會顯示) --> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <!-- 導覽列選單內容(可收合) --> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav ms-auto"> <li class="nav-item"><a href="/" class="nav-link">首頁</a></li> <li class="nav-item"><a href="{% url 'msg_create' %}" class="nav-link">新增留言</a></li> {% if user.is_authenticated %} <li class="nav-item"> <form action="{% url 'logout' %}" method="post"> {% csrf_token %} <input type="submit" value="{{ user.username }} 登出" class="btn btn-sm btn-primary" /> </form> </li> {% else %} <li class="nav-item"><a href="{% url 'login' %}" class="btn btn-sm btn-primary">登入</a></li> {% endif %} </ul> </div> </div> </nav>
Bootstrap 的導覽列的架構大致如下:
- 整個導覽列的內容被包覆在同時套用 `.navbar` 及 `.navbar-expand{-sm|-md|-lg|-xl}` 類別的標籤內
- 選用 `<nav></nav>` 的原因是 HTML5 的標準中,此標籤原本的語意就是導覽列
- 一定要套用 `navbar-expand{-sm|-md|-lg|-xl}` 其中一個類別,否則導覽選單會被收合起來,頁面上就看不到了
- 如果套用的是 `navbar-expand-sm` 、 `navbar-expand-md` 、 `navbar-expand-lg` 或 `navbar-expand-xl` 等類別,導覽選單在大螢幕上會展開,在小螢幕上會自動收合起來
- 若套用 `navbar-expand` 類別,則導覽選單會永遠展開,在小畫面上也不會收合
- ++第 1 行++套用 `bg-dark` 類別是為了設定導覽列的顏色
- ++第 4 行++套用 `navbar-brand` 類別的 `<div></div>` 是用來放置網站標誌用的,通常會在這邊網站名稱或商標
- ++第 6 - 8 行++是在畫面上放置一個切換導覽列選單展開/收合狀態的按鈕,它必須套用 `navbar-toggler` 這個類別
- 在大螢幕上這個按鈕不會顯現
- 留意一下它的 `data-bs-target` 屬性,此屬性用來指定欲切換狀態的導覽選單的 id
- ++第 10 - 25 行++定義了可展開/收合導覽列選單的內容
- ++第 10 行++的 `id` 屬性要跟++第 6 行++的 `data-bs-target` 屬性對應,否則無法切換導覽選單的狀態
- ++第 11 - 24 行++為導覽選單的實際定義
- ++第 11 行++額外套用 `ms-auto` 類別是為了讓導覽選單項目靠右,若套用 `mx-auto` 則會讓導覽選單項目置中
- 套用 `nav-item` 的標籤代表一個選單項目,其下的 `<a></a>` 請套用 `nav-link` 類別然後記得把這個範本加入 `guestbook/templates/base.html` ,這樣才會在每個頁面顯示:
<div class="container"> {% include "navbar.html" %} {% block content %}{% endblock %} </div>
增加++第 14 行++的 `{% include "navbar.html" %}` 讓 `base.html` 引用 `navbar.html`。
加入 Font Awesome 圖示
先到 Font Awesome 的圖示展示頁面(https://fontawesome.com/icons)搜尋需要的圖示,可以在頁面上的搜尋框輸入關鍵字來篩選圖示。列表中以沒有黃色「Pro」字樣呈現的圖示是可以免費使用的,有標黃色Pro的圖示則是需要另外付費購買。
假設現在要在「留言板」前面加上一個代表「訊息」的圖示,可以在搜尋框輸入「message」來篩選圖示。
點選想使用的圖亦,可查看細節。
重點在頁面左下方,有列出使用這個圖示的 HTML 碼 `<i class="fa-regular fa-message"></i>` ,把這段 HTML 碼放在導覽列的「留言板」之前,修改 `web/templates/navbar.html` 的第 3 行:
<div class="navbar-brand"><i class="fa-regular fa-message"></i> 留言板</div>
修改後的導覽列會變成這樣:
持續修改 `web/templates/navbar.html` ,為「首頁」、「新增留言」、「登入」、「登出」等加上合適的圖示:<nav class="navbar navbar-expand-sm bg-dark mb-3" data-bs-theme="dark"> <div class="container-fluid"> <!-- 網站標誌 --> <div class="navbar-brand"><i class="fa-regular fa-message"></i> 留言板</div> <!-- 展開/收合切換鈕(畫面小時才會顯示) --> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <!-- 導覽列選單內容(可收合) --> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav ms-auto"> <li class="nav-item"><a href="/" class="nav-link"><i class="fa-solid fa-house-chimney"></i>首頁</a></li> <li class="nav-item"><a href="{% url 'msg_create' %}" class="nav-link"><i class="fa-solid fa-pen-to-square"></i>新增留言</a></li> {% if user.is_authenticated %} <li class="nav-item"> <form action="{% url 'logout' %}" method="post" class="nav-link"> {% csrf_token %} <i class="fa-solid fa-right-from-bracket"></i><input type="submit" value="{{ user.username }} 登出" class="btn btn-sm btn-primary" /> </form> </li> {% else %} <li class="nav-item"><a href="{% url 'login' %}" class="btn btn-sm btn-primary"><i class="fa-solid fa-right-to-bracket"></i>登入</a></li> {% endif %} </ul> </div> </div> </nav>
整個修改完的導覽列外觀如下,與原本純文字的導覽列相比,看起來稍微活潑了些:
美化其他頁面範本
留言列表
修改 `web/templates/web/message_list.html` ,使用列表、清單(List Group)來顯示留言列表:
{% extends "base.html" %} {% block content %} <ul class="list-group"> {% for msg in message_list %} <li class="list-group-item list-group-item-action"> <small class="text-muted"><i class="fas fa-clock"></i>{{ msg.created|date:"Y-m-d H:i" }}</small> {% if user.is_authenticated %} <a href="{% url 'msg_delete' msg.id %}" class="text-danger" title="刪除"><i class="fas fa-minus-circle"></i></a> {% endif %} <a href="{% url 'msg_view' msg.id %}">{{ msg.subject }}</a> </li> {% endfor %} </ul> {% endblock %}
- ++第 4 行++套用 `list-group` 類別
- ++第 6 行++額外套用 `list-group-item-action` 類別的用意是當滑鼠游標移到某筆留言時,該留言的底色會改變
- ++第 7 行++的 `<small></small>` 可以讓顯示的文字變小,套用 `text-muted` 的作用可以讓文字的顏色變淺一點
- ++第 7 行++ `{{message.publication_date|date:"Y/m/d H:i"}}` 透過 django 內建的 `date` filter 來變更日期與時間輸出的格式留言檢視
修改 `web/templates/web/message_detail.html` ,同樣以資訊卡片(Card)來顯示留言的詳細內容:
{% extends "base.html" %} {% block content %} <div class="card"> <div class="card-header"> <div class="font-weight-bold"><i class="fas fa-file-alt"></i> {{ message.subject }}</div> </div> <div class="card-body"> <div class="card-text">{{ message.content|linebreaks }}</div> </div> <div class="card-footer d-flex justify-content-between"> <small class="text-muted"><i class="fas fa-user"></i> {{ message.user }}</small> <small class="text-muted"><i class="fas fa-clock"></i> {{ message.created|date:"Y-m-d H:i"}}</small> </div> </div> {% endblock %}
- ++第 9 行++,`{{ message.content|linebreaks }}` 透過 django 內建的 `linebreaks` filter 將留言內容的換行符號替換為 `<br>` 或是 `<p></p>`,讓留言在顯示的時候得以分行呈現。
原先沒有透過 `linebreaks` 處理時,多行的留言會擠在同一行呈現。
- ++第 11 行++額外套用了 `d-flex` 與 `justify-content-between` 的用意是為了讓其內的 2 個元素,可以分別往頭尾對齊。新增留言
修改 `web/templates/message_form.html` ,讓頁面上的表格也套用 Bootstrap 框架的表格外觀:
{% extends 'base.html' %} {% block content %} <form action="" method="post"> {% csrf_token %} <table class="table table-sm"> {{ form.as_table }} </table> <input type="submit" value="送出" class="btn btn-sm btn-primary" /> </form> {% endblock %}
修改的地方只有++第 6 行++,為 `<table>` 標籤加上 `class="table table-sm"` 屬性,額外套用 `table-sm` 的原因是想讓表格的列可以排列得稍微緊緻一些。
接下來再來修正一下頁面上輸入元件的外觀。Bootstrap 框架也有輸入元件的相關定義,最基本的方式就是為這些輸入元件套用 `form-control` 類別,例:
<form> <!-- ...略... --> <input type="text" class="form-control"> <!-- ...略... --> <textarea class="form-control"></textarea> <!-- ...略... --> </form>
新增留言這個頁面範本產生輸入元件的方式是透過第 8 行的 `{{ form.as_table }}` 將接收到的 `form` 變數的內容轉成相對應的 HTML 碼,沒有辦法直接以新增屬性的方式來套用 CSS 類別。
在不安裝額外的 python 套件的前提下,有 2 種方式來解決這個問題:
1. 在視圖(View)產生 form 的時候,就指定每個輸入元件要套用的 CSS 類別
2. 不使用 `{{ form.as_table }}` 來自動產生 HTML 碼,而是自行撰寫這些輸入元件的 HTML 碼方案1:預先指定要套用的 CSS 類別
看一下 `web/views.py` ,新增留言是由 `MessageCreate` 這個 class 負責的,它繼承了 django 內建的 `CreateView` 類別,由我們指定的 `Message` 這個 model 自動生成 form 的定義。
class MessageCreate(CreateView): model = Message fields = ['user', 'subject', 'content'] # 僅顯示 user, subject, content 這 3 個欄位 success_url = reverse_lazy('msg_list') # 新增成功後,導向留言列表
在這邊無法調整 form 的內容,所幸 CreateView 除了指定 `model` 與 `fields` 的方式自動生成 form 之外,也接受使用者以指定 `form_class` 的方式,轉而參考該 class 來產生 form。
新增 `web/forms.py` 自行定義 form class:
from django import forms from .models import Message class MessageForm(forms.ModelForm): class Meta: model = Message fields = ['user', 'subject', 'content'] widgets = { 'user': forms.TextInput(attrs={'class': 'form-control'}), 'subject': forms.TextInput(attrs={'class': 'form-control'}), 'content': forms.Textarea(attrs={'class': 'form-control'}), }
在這邊透過繼承 `forms.ModelForm` 的方式來定義 `MessageForm` :
- ++第 6 行++,指定參考 `Message` 這個 model 來產生表單
- ++第 7 行++,指定只需要產生 `user` 、 `subject` 、 `content` 這 3 個欄位的表單輸入元件
- ++第 8 - 12 行++,分別指定這 3 個欄位呈現時要使用的小工具(widget)
- `forms.TextInput` 是單行輸入元件
- `forms.Textarea` 是多行輸入元件
- 用 `attrs={}` 來指定額外的 HTML 屬性,在這個例子中是指定 `class` 屬性的值要被設為 `form-control`定義好表單之後,再修正 `web/views.py`,修改 `MessageCreate` 的內容:
from django.shortcuts import render from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView from django.urls import reverse_lazy from .models import * from django.contrib.auth.mixins import LoginRequiredMixin from .forms import MessageForm
- 新增++第 6 行++,引用剛才定義的 `MessageForm`
# 新增留言 class MessageCreate(CreateView): form_class = MessageForm success_url = reverse_lazy('msg_list') # 新增成功後,導向留言列表 template_name = 'web/message_form.html'
- 刪除原本 `MessageCreate` 裡的 `model` 以及 `fields`,改以++第 19 行++的 `form_class` 來指定使用 `MessageForm`
- 指定 `form_class` 時,`CreateView` 就不需要自行由 `model` 指定的資料模型取出 `fields` 所列出來的欄位來產生表單元件,因此這兩個屬性就不需要寫
- 新增++第 21 行++,以 `template_name` 屬性指定要使用的頁面範本。原本有指定 `model` 屬性時,`CreateView` 可以自動推導使用的頁面範本,方案2:自行撰寫 HTML 碼
採用這個方案的話,表示要完全忽略傳遞給頁面範本的 `form` 變數的內容,在範本中就不以 `{{ form.as_table }}` 來產生相關輸入元件的 HTML 碼。不需要新增 `forms.py`,不需要修改 `views.py` ,僅修改頁面範本即可。
修改 `web/templates/web/message_form.html`:
{% extends 'base.html' %} {% block content %} <form action="" method="post"> {% csrf_token %} <table class="table table-sm"> <tr> <th><label for="id_user">姓名</label></th> <td><input type="text" id="id_user" name="user" class="form-control"></td> </tr> <tr> <th><label for="id_subject">主旨</label></th> <td><input type="text" id="id_subject" name="subject" class="form-control"></td> </tr> <tr> <th><label for="id_content">內容</label></th> <td><textarea id="id_content" name="content" class="form-control" cols="30" rows="10"></textarea></td> </tr> </table> <input type="submit" value="送出" class="btn btn-sm btn-primary" /> </form> {% endblock %}
- 將原本的 `{{ form.as_table }}` 刪除,改以++第 7 - 18 行++的 HTML 碼取代。
【注意】
採用這個方案要特別注意自行撰寫的 HTML 碼,每個輸入欄位的 `name` 屬性與視圖裡所參照的 model 是否有對應到,如果無法完全對應的話,可能有些欄位的值就無法被寫入資料庫。建議盡量採用方案1,比較不會發生不慎打錯欄位名稱的狀況。
刪除留言
修改 `web/templates/web/message_confirm_delete.html` :
{% extends "base.html" %} {% block content %} <h2>刪除記錄</h2> <p>您確定要刪除「{{ object }}」這筆記錄嗎?</p> <form action="" method="POST"> {% csrf_token %} <input type="submit" action="" value="是的,我要刪除" class="btn btn-sm btn-danger" /> </form> {% endblock %}
主要是第 8 行,為按鈕套用 `btn btn-sm btn-danger` 等類別。
使用者登入
修改 `web/templates/registration/login.html` :
{% extends "base.html" %} {% block content %} {% if form.errors %} <p class="alert alert-danger">帳號或密碼不符合,請再試一次。</p> {% endif %} <form method="post" action="{% url 'login' %}"> {% csrf_token %} <div class="h3 mb5">請輸入您的帳號密碼</div> <div class="row row-cols-lg-auto align-items-center"> <div class="col-12"> <input name="username" autofocus="" required="" id="id_username" maxlength="254" type="text" class="form-control mr-2" placeholder="帳號"> </div> <div class="col-12"> <input name="password" required="" id="id_password" type="password" class="form-control mr-2" placeholder="密碼"> </div> <div class="col-12"> <input type="submit" value="login" class="btn btn-primary"/> </div> </div> </form> {% endblock %}
主要修改項目如下:
- ++第 4 - 6 行++,為錯誤訊息套用相關 CSS 類別,以突顯操作錯誤
- ++第 10, 20 行++,將原本的表單輸入元件外加一層 `<div>`,並為其套用 `row`、`row-cols-lg-auto` 類別,讓其下的輸入元件可以接在同一行
- ++第 11 - 19 行++,分別為 `<input>` 元件套用 `form-control` 類別,移除原本外加的「帳號」、「密碼」等欄位標籤文字,改以 `placeholder` 屬性提示每個 `<input>` 元件預期該輸入什麼內容使用者登出
修改 `web/templates/registration/logged_out.html` :
{% extends "base.html" %} {% block content %} <p class="alert alert-info">您已登出!!</p> <a href="{% url 'login'%}" class="btn btn-primary">請按此處重新<i class="fas fa-sign-in-alt"></i> 登入</a> {% endblock %}