顯示具有 Bootstrap 標籤的文章。 顯示所有文章
顯示具有 Bootstrap 標籤的文章。 顯示所有文章

2023年12月11日 星期一

2023年11月18日 星期六

Python 學習筆記 : Django 4 用法整理 (八) 表單模型化 (1)

在前兩篇筆記中分別使用一般 HTML 與 Bootstrap 網頁框架作為前端, 測試了網頁表單與後端資料表的 CRUD 操作, 但都必須於模板網頁中自行手刻 HTML 表單, 在開發與維護上要麻煩.  Django 提供了能自動生成表單的表單模型化做法, 可解決表單生成問題. 

本系列之前的文章參考 :


所謂表單模型化是指以類似資料表模型化方式使用 Python 物件來建立網頁表單, 透過繼承 Django 內建的 django.forms.Form 類別可定義一個與資料表模型類別相對應的表單類別, 實體化後的表單物件即可傳入模板網頁中利用該物件的 as_p(), as_ul(), 或 as_table() 等方法自動生成網頁表單.


一. 建立表單模型 :    

本篇接續前一篇測試 "Python 學習筆記 : Django 4 用法整理 (六) CRUD 操作 (2)" 中所使用的 myapp1 應用程式, 將這個以 Bootstrap 框架為基礎的網站用表單模型來改寫基本的資料表 CRUD 操作, 不同之處是使用表單模型的網站具有欄位驗證功能, 這是前一篇測試沒有的 (須自行手工添加). 前一次測試的專案下載網址 :

在改寫前先在上層專案目錄下用 tree /f 指令檢視前一篇結束時的網站結構 :

D:.
│  manage.py
│  db.sqlite3
│
├─mysite
│  │  asgi.py
│  │  settings.py
│  │  urls.py
│  │  wsgi.py
│  │  __init__.py
│  │
│  └─__pycache__
│          __init__.cpython-310.pyc
│          settings.cpython-310.pyc
│          wsgi.cpython-310.pyc
│          __init__.cpython-311.pyc
│          settings.cpython-311.pyc
│          urls.cpython-311.pyc
│          wsgi.cpython-311.pyc
│          urls.cpython-310.pyc
│
└─myapp1
    │  admin.py
    │  apps.py
    │  models.py   
    │  tests.py
    │  views.py
    │  __init__.py
    │  urls.py
    │
    ├─migrations
    │  │  __init__.py
    │  │  0001_initial.py
    │  │  0002_alter_members_email.py
    │  │
    │  └─__pycache__
    │          __init__.cpython-310.pyc
    │          0001_initial.cpython-310.pyc
    │          0002_alter_members_email.cpython-310.pyc
    │          __init__.cpython-311.pyc
    │          0002_alter_members_email.cpython-311.pyc
    │          0001_initial.cpython-311.pyc
    │
    ├─__pycache__
    │      __init__.cpython-310.pyc
    │      apps.cpython-310.pyc
    │      models.cpython-310.pyc
    │      __init__.cpython-311.pyc
    │      apps.cpython-311.pyc
    │      models.cpython-311.pyc
    │      admin.cpython-311.pyc
    │      admin.cpython-310.pyc
    │      urls.cpython-310.pyc
    │      views.cpython-310.pyc
    │
    └─templates
            get_record.htm
            list_all_records.htm
            add_record.htm
            edit_record.htm
            base.htm
            bootstrap.htm

其中的模型層 models.py 裡面定義了一個包含六個欄位的資料表模型類別 Members :

from django.db import models

class Members(models.Model):
    name=models.CharField(max_length=20, null=False)
    gender=models.CharField(max_length=2, default='男', null=False)
    birthday=models.DateField(null=False)
    email=models.CharField(max_length=120, blank=True, default='')
    phone=models.CharField(max_length=50, blank=True, default='')
    address=models.CharField(max_length=255, blank=True, default='')
    
    def __str__(self):
        return self.name

在前一篇測試中, 我們是在模板網頁 add_record.htm 與 edit_record.htm 裡依據 Members 模型中的欄位手刻 HTML 網頁表單來新增或編輯紀錄; 其實 Django 提供了更省力的表單模型化作法, 它模仿 ORM 機制以資料模型物件來映射實體資料表做法, 內建了 Form 與 ModelForm 表單類別, 利用表單物件來映射實體網頁表單, 本篇先測試 Form 類別用法. 

首先在應用程式目錄 myapp1 底下建立一個 forms.py 模組 (這名稱是固定的), 從 django 匯入 forms 模組, 然後定義一個繼承 forms.Form 類別的子類別, 於其中呼叫 forms.xxxField() 欄位定義函式來建立表單欄位變數, 格式與 models.py 類似, 語法如下 : 

# forms.py 
from django import forms

class table_nameForm(forms.Form):
    field_name_1=forms.xxxField(**kwargs)
    field_name_2=forms.xxxField(**kwargs)
    ......

其中類別名稱可自訂, 但為了與 models.py 中的資料表相對應, 通常會用資料模型類別名稱後面串上 'Form' 為表單類別名稱. 常用欄位定義函式如下表 : 


 常用的表單欄位定義函式 說明
 CharField(**kwargs) 單行文字欄位或多行文字區域欄位, 常用參數 :
 label (標籤), max_length (長度), nitial (初始值),
 widget (控件), 設為 forms.Textarea 時為多行文字區域 (Textarea)
 ChoiceField(**kwargs) 下拉式選單, 常用參數 label, choices 選項 (二維串列 [[值, 顯示]])
 BooleanField(**kwargs) 核取方塊 (checkbox), 常用參數 label (標籤)
 EmailField(**kwargs) Email 欄位, 常用參數 label (標籤), required (必填)
 URLField(**kwargs) URL 欄位, 常用參數 label (標籤), 
 IntegerField(**kwargs) 整數欄位, 常用參數 label (標籤)
 DateField(**kwargs) 日期欄位, 常用參數 label (標籤)
 DateTimeField(**kwargs) 日期時間欄位, 常用參數 label (標籤)


欄位定義函式可傳入關鍵字參數, 常用參數如下表 :


 常用的欄位參數 說明
 label 欄位標籤 (字串), 用來標示這是甚麼欄位
 requred 必填欄位 (布林), 欄位驗證用
 initial 欄位初始值 (任何值), 即元素的 value 屬性值
 widget 指定欄位的輸入控件, 例如 Textarea, RadioSelect, CheckBoxSelect
 max_length 最大字元長度 (整數), 用於 CharField 欄位驗證
 min_length 最小字元長度 (整數), 用於 CharField 欄位驗證
 label_suffix 添加在欄位標籤後面的尾綴 (字串),  也可放在 Form 類別參數 (全部欄位加尾綴)
 help_text 顯示在欄位下方的說明 (字串), 會放在一個 span 元素中. 
 error_messages 驗證失敗時用來覆蓋預設錯誤訊息 (字典), 例如 {'required' : '必填欄位'})
 help_text 顯示在欄位下方的說明 (字串), 會放在一個 span 元素中. 
 disabled 是否將此欄位禁能 (布林), disabled=True 會在該元素中添加 disabled 屬性


其中 label 參數幾乎是每個欄位的必要參數, 用來顯示欄位標籤, 事實上就是渲染成 HTML 的 label 標籤. 更多表單欄位定義函式參考 :


依據上面的語法仿照 models.py 於 myapp1 底下建立 forms.py 群組 :

# forms.py of App=myapp1
from django import forms

class MembersForm(forms.Form):
    name=forms.CharField(label='姓名', max_length=20, required=True)
    gender=forms.CharField(label='性別', max_length=2, initial='男')
    birthday=forms.DateField(label='生日')
    email=forms.CharField(label='信箱', required=True)
    phone=forms.CharField(label='電話', required=True)
    address=forms.CharField(label='地址', max_length=255)

存檔後開啟命令提示字元視窗 (如果已開啟要關掉重開才會抓到 forms.py 模組, 這很重要, 否則會出現模組不存在錯誤), 切換到上層專案目錄 mysite, 用 python manage.py shell 指令進入 Python 環境, 匯入上面在 forms.py 中定義的表單類別 MembersForm 後建立表單實例 : 

python manage.py shell 

E:\django\test4\mysite>python manage.py shell      
Python 3.10.11 (tags/v3.10.11:7d4cc5a, Apr  5 2023, 00:38:17) [MSC v.1929 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.15.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from myapp1.forms import MembersForm      # 匯入表單類別  

In [2]: f=MembersForm()      # 建立表單物件

In [3]: f    
Out[3]: <MembersForm bound=False, valid=Unknown, fields=(name;gender;birthday;email;phone;address)>

此處表單物件屬性 bound=False 表示此表單尚未綁定 (即尚未填入資料), fields 欄位則列舉此表單之欄位名稱. 建立表單物件後即可呼叫下列 4 個方法來生成表單 : 


 表單物件的方法 說明
 as_p() 生成以 <P> 標籤裝載之表單欄位 HTML 碼
 as_div() 生成以 <DIV> 標籤裝載之表單欄位 HTML 碼
 as_ul() 生成以 <UL> 標籤裝載之表單欄位 HTML 碼 (不含 <UL>, 須自行添加)
 as_table() 生成以 <TABLE> 標籤裝載之表單欄位 HTML 碼 (不含 <TABLE>, 須自行添加)


注意, as_ul() 與 as_table() 均不含外圍的 <UL> 與 <TABLE> 標籤, 其傳回值從視圖模組傳遞至模板網頁時必須自行添加 <UL> 與 <TABLE> 標籤, 否則 HTML 碼將不完整. 這種設計有個好處, 我們可在 <UL> 與 <TABLE> 套上 id 與樣式類別以控制表格的外觀或互動特性. 

In [4]: f.as_p()   
Out[4]: '<p>\n    <label for="id_name">姓名:</label>\n    <input type="text" name="name" maxlength="20" required id="id_name">\n    \n    \n  </p>\n\n  \n  <p>\n    <label for="id_gender">性別:</label>\n    <input type="text" name="gender" value="男" maxlength="2" required id="id_gender">\n    \n    \n  </p>\n\n  \n  <p>\n    <label for="id_birthday">生日:</label>\n    <input type="text" name="birthday" required id="id_birthday">\n    \n    \n  </p>\n\n  \n  <p>\n    <label for="id_email">信箱:</label>\n    <input type="text" name="email" required id="id_email">\n    \n    \n  </p>\n\n  \n  <p>\n    <label for="id_phone">電話:</label>\n    <input type="text" name="phone" required id="id_phone">\n    \n    \n  </p>\n\n  \n  <p>\n    <label for="id_address">地址:</label>\n    <input type="text" name="address" maxlength="255" required id="id_address">\n    \n    \n      \n    \n  </p>'

可見每個欄位都被包在各自的 <P> 元素中. 

In [5]: f.as_ul()
Out[5]: '<li>\n    \n    <label for="id_name">姓名:</label>\n    <input type="text" name="name" maxlength="20" required id="id_name">\n    \n    \n  </li>\n\n  <li>\n    \n    <label for="id_gender">性別:</label>\n    <input type="text" name="gender" value="男" maxlength="2" required id="id_gender">\n    \n    \n  </li>\n\n  <li>\n    \n    <label for="id_birthday">生日:</label>\n    <input type="text" name="birthday" required id="id_birthday">\n    \n    \n  </li>\n\n  <li>\n    \n    <label for="id_email">信箱:</label>\n    <input type="text" name="email" required id="id_email">\n    \n    \n  </li>\n\n  <li>\n    \n    <label for="id_phone">電 話:</label>\n    <input type="text" name="phone" required id="id_phone">\n    \n    \n  </li>\n\n  <li>\n    \n    <label for="id_address">地址:</label>\n    <input type="text" name="address" maxlength="255" required id="id_address">\n    \n    \n      \n    \n  </li>'

可見每個欄位都被包在各自的 <LI> 元素中. 

In [6]: f.as_div()   
Out[6]: '<div>\n    \n      <label for="id_name">姓名:</label>\n    \n    \n    \n    <input type="text" name="name" maxlength="20" required id="id_name">\n    \n    \n</div>\n\n  <div>\n    \n      <label for="id_gender">性別:</label>\n    \n    \n    \n    <input type="text" name="gender" value="男" maxlength="2" required id="id_gender">\n    \n    \n</div>\n\n  <div>\n    \n      <label for="id_birthday">生日:</label>\n    \n    \n    \n    <input type="text" name="birthday" required id="id_birthday">\n    \n    \n</div>\n\n  <div>\n    \n      <label for="id_email">信箱:</label>\n    \n    \n    \n    <input type="text" name="email" required id="id_email">\n    \n    \n</div>\n\n  <div>\n    \n      <label for="id_phone">電話:</label>\n    \n    \n    \n    <input type="text" name="phone" required id="id_phone">\n    \n    \n</div>\n\n  <div>\n    \n      <label for="id_address">地址:</label>\n    \n    \n    \n    <input type="text" name="address" maxlength="255" required id="id_address">\n    \n    \n      \n    \n</div>'

可見每個欄位都被包在各自的 <DIV> 元素中.

In [7]: f.as_table()   
Out[7]: '<tr>\n    <th><label for="id_name">姓名:</label></th>\n    <td>\n      \n      <input type="text" name="name" maxlength="20" required id="id_name">\n      \n      \n    </td>\n  </tr>\n\n  <tr>\n    <th><label for="id_gender">性別:</label></th>\n    <td>\n      \n      <input type="text" name="gender" value="男" maxlength="2" required id="id_gender">\n      \n      \n    </td>\n  </tr>\n\n  <tr>\n    <th><label for="id_birthday">生日:</label></th>\n    <td>\n      \n      <input type="text" name="birthday" required id="id_birthday">\n      \n      \n    </td>\n  </tr>\n\n  <tr>\n    <th><label for="id_email">信箱:</label></th>\n    <td>\n      \n      <input type="text" name="email" required id="id_email">\n      \n      \n    </td>\n  </tr>\n\n  <tr>\n    <th><label for="id_phone">電話:</label></th>\n    <td>\n      \n      <input type="text" name="phone" required id="id_phone">\n      \n      \n    </td>\n  </tr>\n\n  <tr>\n    <th><label for="id_address">地址:</label></th>\n    <td>\n      \n      <input type="text" name="address" maxlength="255" required id="id_address">\n      \n      \n        \n      \n    </td>\n  </tr>'

可見每個欄位都被包在各自的 <TD> 元素中, 而欄位標籤 (label) 則是被包在 <TH> 元素中. 注意, 以上 as_p(), as_div(), as_ul(), 與 as_table() 四個方法均只生成表單欄位, 並不含 SUBMIT 與 RESET 按鈕, 必須自行添加. 


二. 修改視圖模組 views.py 與模板網頁 :    

接下來要修改視圖模組 views.py, 於其中建立表單物件, 然後將其傳遞給模板網頁輸出, 修改的部分僅在於 add_record() 與 edit_record() 這兩個函式 (藍色字體部分) :

# views.py of App=myapp1
from django.shortcuts import render, redirect
from myapp1.models import Members
from myapp1.forms import MembersForm

def get_record_by_name(request, name):
    try:
        record=Members.objects.get(name=name)
    except:
        message='讀取失敗'
    return render(request, 'get_record.htm', locals())

def get_record_by_id(request, id):
    try:
        record=Members.objects.get(id=id)
    except:
        message='讀取失敗'
    return render(request, 'get_record.htm', locals())

def list_all_records(request):
    try: 
        records=Members.objects.all().order_by('-id')
    except:
        pass
    return render(request, 'list_all_records.htm', locals())

def add_record(request):
    f=MembersForm()   # 建立空表單物件 (未綁定)  
    if request.method=='POST':   # 來自表單提交
        name=request.POST['name']
        gender=request.POST['gender']
        birthday=request.POST['birthday']
        email=request.POST['email']
        phone=request.POST['phone']
        address=request.POST['address']
        record=Members.objects.create(name=name,
                                     gender=gender,
                                     birthday=birthday,
                                     email=email,
                                     phone=phone,
                                     address=address)
        record.save()
        return redirect('/myapp1/list_all_records/')
    else:   # 來自 list_all_records.htm 的新增紀錄超連結
        message='請輸入資料'
        return render(request, 'add_record.htm', locals())
        
def edit_record(request, id=None, mode=None):
    if mode=='update':  # 來自在 edit_record.htm 按送出
        record=Members.objects.get(id=id)  # id 一定有不須捕捉例外
        record.name=request.POST['name']
        record.gender=request.POST['gender']
        record.birthday=request.POST['birthday']
        record.email=request.POST['email']
        record.phone=request.POST['phone']
        record.address=request.POST['address']
        record.save()
        return redirect('/myapp1/list_all_records/')
    else: # 來自按 list_all_records.htm 中的編輯超連結:顯示編輯頁面        
        try: # 防止可能來自網址列的 id 不存在
            record=Members.objects.get(id=id)
            # 轉換 birthday 的格式 (因資料表欄位為 date 型態)
            birthday=str(record.birthday)
            birthday=birthday.replace('年', '-')
            birthday=birthday.replace('月', '-')
            birthday=birthday.replace('日', '-')
            # 建立表單物件 (已綁定)
            f=MembersForm({'name': record.name,
                           'gender': record.gender,
                           'birthday': birthday,
                           'email': record.email,
                           'phone': record.phone,
                           'address': record.address})  
        except:
            message='id 不存在'
        return render(request, 'edit_record.htm', locals())
   
def delete_record(request, id=None):
    if id:  # 有傳入 id 才刪除
        try: # 防止可能來自網址列的 id 不存在
            record=Members.objects.get(id=id)  # 取得紀錄物件
            record.delete()  # 刪除紀錄
        except: # 不處理
            pass
    return redirect('/myapp1/list_all_records/')   
  
在 add_record() 中我們只是在開頭添加 f=MembersForm() 建立一個空的表單物件 f  而已, 它會被 locals() 打包到字典中傳遞給模板網頁 add_record.htm, 即可用 f.as_table/f.as_p/f.as_ul/f_as_div 等 自動生成表單欄位. 

在 edit_record() 中也是用 f=MembersForm() 建立一個表單物件, 但傳入從資料表查詢到的紀錄欄位組成的字典作為參數, 這個填入動作稱為綁定 (bound), 用 python manage.py shell 指令進入 Python 命令列查詢 f.is_bound 屬性會傳回 True; 反之, 空的表單物件會傳回 False. 

接下來修改此模板網頁 add_record.htm, 刪除原先手刻放在 table 元素中的表單欄位, 改為模板指令 {{ f.as_table }}, 如上所述, as_table 方法只輸出 TABLE 的 TH, TD 等欄位部分, 不包含 TABLE 本身與 SUBMIT, RESET 按鈕 :

<!-- add_record.htm -->
{% extends "bootstrap.htm" %}
{% block title %}新增紀錄{% endblock %}
{% block body %}
<h4 class="m-4">新增會員資料</h4>
<div class="container ml-3">
  <form method="post" action="." name="add_record">
    {% csrf_token %}
    <table class="table-bordered">
      {{ f.as_table() }}  
      <tr>
        <td colspan="2" class="p-2">
          <input type="submit" value="送出">
          <input type="reset" value="重設">
        </td>
      </tr>
    </table>
    <span style='color:red;'>{{ message }}</span>
  </form>
</div>
{% endblock %}

注意, 在模板中呼叫物件的方法不可以加括號 (只要用 f.as_table 即可), 否則會出現如下錯誤 : 

Could not parse the remainder: '()' from 'f.as_table()'

這時用 python manage.py runerver 指令開啟開發伺服器, 瀏覽 http://127.0.0.1:8000/myapp1/add_record/ 網頁就可以看到 f.as_table 所生成的表單欄位 :




由於是自動生成, 無法在 th 與 td 元素上添加 Bootstrap 樣式類別, 渲染效果與前一篇測試之結果有些差異, 不過 padding/magin 可以利用 style 區塊另行設定. 輸入欄位資料後送出即能存入資料表, 功能與前一篇相同. 

最後修改 edit_record.htm 模板, 與 add_record.htm 類似, 也是用模板指令 {{ f.as_table }} 取代手刻欄位, 由於表單物件 f 已在 views.py 的 edit_record() 函式中綁定 (填入)一筆紀錄, 因此用 f.as_table 生成欄位時會在欄位中自動填入記錄中相對應的欄位值 :

<!-- edit_record.htm -->
{% extends "bootstrap.htm" %}
{% block title %}編輯紀錄{% endblock %}
{% block body %}
<h4 class="m-4">編輯會員資料</h4>
<div class="container ml-3">
  <form method="post" action="/myapp1/edit_record/{{ record.id }}/update/" name="add_record">
    {% csrf_token %}
    <table class="table-bordered">
      {{ f.as_table }}
      <tr>
        <td colspan="2" class="p-2">
          <input type="submit" value="送出">
          <input type="reset" value="重設">
        </td>
      </tr>
    </table>
    <span style='color:red;'>{{ message }}</span>
  </form>
</div>
{% endblock %}

瀏覽 http://127.0.0.1:8000/myapp1/list_all_rcords/ 網頁, 並點擊 "編輯" 超連結就會前往 /myapp1/edit_rcord/id 紀錄編輯網頁, 看到 f.as_table 所生成的表單欄位 :





修改後按送出更新資料表, 然後重導向至顯示全部記錄. 從以上測試可知, 用表單物件取代手刻可以更方便簡潔地生成表單欄位. 以上測試之專案 zip 檔存放於 GitHub :


三. 欄位驗證功能 : 

上面的測試中在定義表單欄位時有傳入 required 這個參數, 若設為 True 表示此欄位必須輸入才能提交; 另外 min_length 與 max_length 參數則是用來限制輸入欄位的字元長度, 這些參數都是用來做欄位驗證用的, required 會在元素中添加 required 屬性; min_length 與 max_length 則會分別添加 minlength 與 maxlength 屬性 . 

但是, 在上面範例的 MembersForm 表單模型中雖然只在 name, email, form 三個欄位的定義中有指定 required, 但實際測試發現, 其實最後生成的表單欄位都有 required 屬性, 例如在新增紀錄時若所有欄位都填了, 只有生日未填就按送出, 則該欄位下方會出現 "請填寫這個欄位" 提示, 表單並未被提交 :



這是因為 Django 表單物件在生成欄位時預設會替每個欄位添加 required 屬性之故, 檢視新增記錄的網頁原始碼, 可見 {{ f.as_table }} 生成的每個欄位都有 required 屬性 : 

  <form method="post" action="." name="add_record">
    <input type="hidden" name="csrfmiddlewaretoken" value="5ZJ7iENDjzjxWkxkLNXzIYATzAN8ecv1Qs2oIKpKVqMnsl5GHJEI1jeEWNC2PI6P">
    <table class="table-bordered">
      <tr>
    <th><label for="id_name">姓名:</label></th>
    <td>      
      <input type="text" name="name" maxlength="20" required id="id_name"> 
    </td>
  </tr>
  <tr>
    <th><label for="id_gender">性別:</label></th>
    <td>      
      <input type="text" name="gender" value="男" maxlength="2" required id="id_gender">     
     </td>
  </tr>
  <tr>
    <th><label for="id_birthday">生日:</label></th>
    <td>      
      <input type="text" name="birthday" required id="id_birthday">      
    </td>
  </tr>
  <tr>
    <th><label for="id_email">信箱:</label></th>
    <td>      
      <input type="text" name="email" required id="id_email">      
    </td>
  </tr>
  <tr>
    <th><label for="id_phone">電話:</label></th>
    <td>      
      <input type="text" name="phone" required id="id_phone">     
    </td>
  </tr>
  <tr>
    <th><label for="id_address">地址:</label></th>
    <td>      
      <input type="text" name="address" maxlength="255" required id="id_address"> 
    </td>
  </tr>  
      <tr>
        <td colspan="2" class="p-2">
          <input type="submit" value="送出">
          <input type="reset" value="重設">
        </td>
      </tr>
    </table>
    <span style='color:red;'>請輸入資料</span>
  </form>

可見不論在表單模型定義中有沒有傳入 required 參數, 每一個欄位都有 required 屬性. 但 minlength 與 maxlength 則必須在表單模型定義中有傳入 min_length 與 max_length 參數才會有. 

如果要做進一步的欄位驗證, 例如 email 欄位格式是否正確 (上面的 email 欄沒有 @ 亂填也是可以順利提交的), 必須使用字串處理或正規表示法, 字串處理只能簡單判別 email 欄位的輸入值是否含有 @; 正規表示法則可以做 email 格式驗證, 例如 :

reg = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'

修改視圖模組 views.py 中的 add_record() 與 edit_record() 函式如下 :

# views.py of App=myapp1
from django.shortcuts import render, redirect
from myapp1.models import Members
from myapp1.forms import MembersForm
import re

def get_record_by_name(request, name):
    try:
        record=Members.objects.get(name=name)
    except:
        message='讀取失敗'
    return render(request, 'get_record.htm', locals())

def get_record_by_id(request, id):
    try:
        record=Members.objects.get(id=id)
    except:
        message='讀取失敗'
    return render(request, 'get_record.htm', locals())

def list_all_records(request):
    try: 
        records=Members.objects.all().order_by('-id')
    except:
        pass
    return render(request, 'list_all_records.htm', locals())

def add_record(request):
    f=MembersForm()   # 建立空表單物件 (未綁定)
    if request.method=='POST':   # 來自表單提交
        name=request.POST['name']
        gender=request.POST['gender']
        birthday=request.POST['birthday']
        email=request.POST['email']
        phone=request.POST['phone']
        address=request.POST['address']
        reg=r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        if re.match(reg, email): # email 格式正確           
            record=Members.objects.create(name=name,
                                         gender=gender,
                                         birthday=birthday,
                                         email=email,
                                         phone=phone,
                                         address=address)
            record.save()
            return redirect('/myapp1/list_all_records/')
        else:  # email 格式不正確, 綁定原輸入值傳回去
            f=MembersForm({'name': name,
                           'gender': gender,
                           'birthday': birthday,
                           'email': email,
                           'phone': phone,
                           'address': address})             
            message='E-mail 格式不正確'
            return render(request, 'add_record.htm', locals())
    else:   # 來自 list_all_records.htm 的新增紀錄超連結
        message='請輸入資料'
        return render(request, 'add_record.htm', locals())
        
def edit_record(request, id=None, mode=None):
    if mode=='update':  # 來自在 edit_record.htm 按送出
        record=Members.objects.get(id=id)  # id 一定有不須捕捉例外
        record.name=request.POST['name']
        record.gender=request.POST['gender']
        record.birthday=request.POST['birthday']
        record.email=request.POST['email']
        record.phone=request.POST['phone']
        record.address=request.POST['address']
        reg=r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        if re.match(reg, record.email):        
            record.save()
            return redirect('/myapp1/list_all_records/')
        else: # email 格式不正確, 綁定原輸入值傳回去
            f=MembersForm({'name': record.name,
                           'gender': record.gender,
                           'birthday': record.birthday,
                           'email': record.email,
                           'phone': record.phone,
                           'address': record.address})             
            message='E-mail 格式不正確'
            return render(request, 'edit_record.htm', locals())       
    else: # 來自按 list_all_records.htm 中的編輯超連結:顯示編輯頁面        
        try: # 防止可能來自網址列的 id 不存在
            record=Members.objects.get(id=id)
            # 轉換 birthday 的格式 (因資料表欄位為 date 型態)
            birthday=str(record.birthday)
            birthday=birthday.replace('年', '-')
            birthday=birthday.replace('月', '-')
            birthday=birthday.replace('日', '-')
            # 建立表單物件 (已綁定)
            f=MembersForm({'name': record.name,
                           'gender': record.gender,
                           'birthday': birthday,
                           'email': record.email,
                           'phone': record.phone,
                           'address': record.address}) 
        except:
            message='id 不存在'
        return render(request, 'edit_record.htm', locals())
   
def delete_record(request, id=None):
    if id:  # 有傳入 id 才刪除
        try: # 防止可能來自網址列的 id 不存在
            record=Members.objects.get(id=id)  # 取得紀錄物件
            record.delete()  # 刪除紀錄
        except: # 不處理
            pass
    return redirect('/myapp1/list_all_records/')   
  
因為要用到正規式, 所以此處的 views.py 需匯入 re 模組, 在 add_record() 中, 如果 request.method 是 'POST' 表示係請求來自於新增表單的提交, 這時需用正規式驗證傳來的 email 欄位格式是否符合要求, 若符合就新增紀錄至資料表, 否則就把接收到的所有參數打包成字典, 傳給 MembersForm() 建立表單物件 (綁定), 傳回給模板網頁 add_record.htm, 這樣使用者才不會因為 email 欄不合格而必須重新輸入所有欄位資料. edit_record() 的改法也是類似. 

新增紀錄時若 email 格式不符, 按提交後會在表單底下顯示 "E-mail 格式不正確" :





注意, 此例修改了 forms.py 裡面表單模型定義中的 birthday 欄位, 添加了 help_text 參數, 這樣會在該欄位底下多出一個 span 元素, 說明生日的格式是 'YYMM-DD'.

# forms.py of App=myapp1
from django import forms

class MembersForm(forms.Form):
    name=forms.CharField(label='姓名', max_length=20, required=True)
    gender=forms.CharField(label='性別', max_length=2, initial='男')
    birthday=forms.DateField(label='生日', help_text='格式: YY-MM-DD')
    email=forms.CharField(label='信箱', required=True)
    phone=forms.CharField(label='電話', required=True)
    address=forms.CharField(label='地址', max_length=255)

同樣地, 編輯紀錄時也是如此 : 





如果 email 格式正確, 就能順利新增或更新了 :





以上測試的欄位驗證都屬於後後端驗證, 當驗證失敗時都會重新載入頁面, 雖然驗證也可以在前端以 Javascript 來做, 但若用戶端關掉 Javascript 則驗證就無法執行, 所以還是用後端驗證較保險. 

此測試之專案壓縮檔存在 GitHub :


2023年11月10日 星期五

好站 : 使用 Bootstrap 快速建立響應式網站

最近在 Hahow 企業版上六角學院的響應式網站設計課程時, 找到該學院架設的 Bootstrp 教學網站, 內容豐富鉅細靡遺, 超棒的 :


去年為了 GitHub 架站教學的需要花了一個月複習 Bootstrap, 以為已練成 Bootstrap 大法, 如今看來還只是幼稚園階段啊! 加油!


2023年11月6日 星期一

Python 學習筆記 : Django 4 用法整理 (六) CRUD 操作 (2)

在前一篇的資料表 CRUD 測試中為了聚焦只使用單純的 HTML 模板, 本篇則是要用 Bootstrap 框架來美化版面, 本系列之前的文章參考 :


由於只是改變前端模板網頁, 因此後端的 views.py 與 urls.py 等控制器都不用改. 首先參考 "Python 學習筆記 : Django 4 用法整理 (二) 網頁模板" 這篇建立一個基礎模板 : 

<!--base.htm-->
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{% block title %}{% endblock %}</title>
  {% block link %}{% endblock %}
  {% block script %}{% endblock %}
  <style>
  {% block style %}{% endblock %}
  </style>
 <body>
 {% block body %}{% endblock %}
</body>
</html>

然後繼承 base.htm 建立 Bootstrap 框架的模板網頁 bootstrap.htm :

<!--bootstrap.htm-->
{% extends "base.htm" %}
{% block link %}
  <link rel="stylesheet" href="https://unpkg.com/bootstrap@4.0.0/dist/css/bootstrap.min.css">
{% endblock %}
{% block script %}
  <script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
  <script src="https://unpkg.com/bootstrap@4.0.0/dist/js/bootstrap.min.js"></script>
{% endblock %}

這樣就可以修改 templates 下前一個 CRUD 測試中的模板, 讓他們都繼承 bootstrap.htm.

首先修改 list_all_records.htm 為繼承 bootstrap.htm, 並於 table, th, td, h3 等標籤上添加 margin (m 開頭) 與 padding (p 開頭) 的樣式類別以調整內外距離 : 

<!-- list_all_records.htm -->
{% extends "bootstrap.htm" %}
{% block title %}讀取資料表{% endblock %}
{% block body %}
<h4 class="m-4">顯示 Members 資料表的全部紀錄
  <a href="/myapp1/add_record/">+ 新增紀錄</a>
</h4>
<div class="container ml-3">
  <table class="table-bordered">
    <thead>
      <tr>
        <th class="p-2">編號</th>
        <th class="p-2">姓名</th>
        <th class="p-2">性別</th>
        <th class="p-2">生日</th>
        <th class="p-2">信箱</th>
        <th class="p-2">電話</th>
        <th class="p-2">地址</th>
        <th class="p-2">動作</th>
      </tr>
    </thead>
    <tbody>
    {% for record in records %}
      <tr>
        <td class="p-2">
          <a href="/myapp1/get_record_by_id/{{ record.id }}/">
          {{ record.id }}
          </a>
        </td>
        <td class="p-2">
          <a href="/myapp1/get_record_by_name/{{ record.name }}/">
          {{ record.name }}
          </a>
        </td>
        <td class="p-2">{{ record.gender }}</td>
        <td class="p-2">{{ record.birthday }}</td>
        <td class="p-2">{{ record.email }}</td>
        <td class="p-2">{{ record.phone }}</td>
        <td class="p-2">{{ record.address }}</td>
        <td class="p-2">
          <a href="../edit_record/{{ record.id }}">編輯</a>
          <a href="/myapp1/delete_record/{{ record.id }}">刪除</a>
        </td>
      </tr>
    {% empty %}
      <tr>
        <td colspan="8">資料表內無紀錄</td>
      </tr>
    {% endfor %}
    </tbody>
  </table>
</div>
{% endblock %}

與前次測試不同之處是這裡把 id 與 name 欄位都加上超連結分別瀏覽 /myapp1/list_record_by_id/id 與 /myapp1/list_record_by_name/name, 節果如下 : 




接著修改其他模板網頁, 均繼承 bootstrap.htm : 

<!-- get_record.htm -->
{% extends "bootstrap.htm" %}
{% block title %}讀取資料表{% endblock %}
{% block body %}
<div class="container ml-3">
  <h2>顯示 Members 資料表的一筆紀錄</h2>
  <h3>{{type_of_record}}</h3>
  編號 : {{record.id}}<br>
  姓名 : {{record.name}}<br>
  性別 : {{record.gender}}<br>
  生日 : {{record.birthday}}<br>
  信箱 : {{record.email}}<br>
  電話 : {{record.phone}}<br>
  地址 : {{record.address}}<br>
  <h3 style='color:red;'>{{message}}</h3>
</div>
{% endblock %}

點擊 list_all_records.htm 中的 id 或 name 欄位的超連結就會顯示那筆紀錄 :





下面是新增紀錄網頁 : 

<!-- add_record.htm -->
{% extends "bootstrap.htm" %}
{% block title %}新增紀錄{% endblock %}
{% block body %}
<h4 class="m-4">新增會員資料</h4>
<div class="container ml-3">
  <form method="post" action="." name="add_record">
    {% csrf_token %}
    <table class="table-bordered">
      <tr>
        <th class="p-2">姓名</td>
        <td class="p-2"><input type='text' name='name' id='name'></td>
      </tr>
      <tr>
        <th class="p-2">性別</td>
        <td class="p-2">
          <input type='radio' name='gender' id='male' value='男' checked="checked">男
          <input type='radio' name='gender' id='female' value='女'>女
        </td>
      </tr>
      <tr>
        <th class="p-2">生日</td>
        <td class="p-2"><input type='text' name='birthday' id='birthday' value='2023-01-01'></td>
      </tr>
      <tr>
        <th class="p-2">信箱</td>
        <td class="p-2"><input type='text' name='email' id='email'></td>
      </tr>
      <tr>
        <th class="p-2">電話</td>
        <td class="p-2"><input type='text' name='phone' id='phone'></td>
      </tr>
      <tr>
        <th class="p-2">地址</td>
        <td class="p-2"><input type='text' name='address' id='address'></td>
      </tr>
      <tr>
        <td colspan="2" class="p-2">
          <input type="submit" value="送出">
          <input type="reset" value="重設">
        </td>
      </tr>
    </table>
    <span style='color:red;'>{{ message }}</span>
  </form>
</div>
{% endblock %}

點擊 list_all_records.htm 中的 + 新增紀錄顯示如下頁面 : 



下面是編輯紀錄網頁 edit_record.htm :

<!-- edit_record.htm -->
{% extends "bootstrap.htm" %}
{% block title %}編輯紀錄{% endblock %}
{% block body %}
<h4 class="m-4">編輯會員資料</h4>
<div class="container ml-3">
  <form method="post" action="/myapp1/edit_record/{{ record.id }}/update/" name="add_record">
    {% csrf_token %}
    <table class="table-bordered">
      <tr>
        <th class="p-2">姓名</td>
        <td class="p-2"><input type='text' name='name' id='name' value={{ record.name }}></td>
      </tr>
      <tr>
        <th class="p-2">性別</td>
        <td class="p-2">
        {% if record.gender == '男' %}
          <input type='radio' name='gender' id='male' value='男' checked="checked">男
          <input type='radio' name='gender' id='female' value='女'>女
        {% else %}
          <input type='radio' name='gender' id='male' value='男'>男
          <input type='radio' name='gender' id='female' value='女'checked="checked">女
        {% endif %}
        </td>
      </tr>
      <tr>
        <th class="p-2">生日</td>
        <td class="p-2"><input type='text' name='birthday' id='birthday' value={{ record.birthday }}></td>
      </tr>
      <tr>
        <th class="p-2">信箱</td>
        <td class="p-2"><input type='text' name='email' id='email' value={{ record.email }}></td>
      </tr>
      <tr>
        <th class="p-2">電話</td>
        <td class="p-2"><input type='text' name='phone' id='phone' value={{ record.phone }}></td>
      </tr>
      <tr>
        <th class="p-2">地址</td>
        <td class="p-2"><input type='text' name='address' id='address' value={{ record.address }}></td>
      </tr>
      <tr>
        <td colspan="2" class="p-2">
          <input type="submit" value="送出">
          <input type="reset" value="重設">
        </td>
      </tr>
    </table>
    <span style='color:red;'>{{ message }}</span>
  </form>
</div>
{% endblock %}

點擊 list_all_records.htm 中的編輯超連結顯示如下頁面 : 




用 Bootstrap 簡簡單單就讓醜醜的原生網頁改頭換面, 有空要好好地來把 Bootstrap 再複習一遍. 以上測試範例壓縮檔放在 GitHub :


2023年10月6日 星期五

安裝 Viusal Studio Code

最近網友詢問我關於用 VS code 做 ESP32 開發問題, 我是建議若寫 MicroPython 可用 Thonny, 我平常都用這個小巧的編輯器寫 Python/MicroPython. 若在 ESP32 要寫 C 程式那當然用 Arcuino IDE. 

今天上 Hahow 六角學院的 Bootstrap 課程時老師使用 Visual Studio Code 寫網頁, 我發現 VS code 還蠻好用的 (例如按快捷鍵將凌亂的網頁整理好), 所以就下載安裝來玩看看, 下載網址如下 :


安裝檔大小約 92 MB, 對大檔案有恐懼症的我還可接受, 安裝過程只有一個地方可能需要改預設設定 (主要是取消將 VSC 設為所支援檔案類型之預設編輯器, 因我不喜歡它一桶江湖) : 







為了避免 VS Code 變成很多檔案的預設編輯器, 將預設勾選改成如下 :







從課程中發現 VS Code 有許多編輯上的便利, 例如將 HTML 碼弄整齊, 同時編輯好幾列程式碼等, 確實是個好工具. 

學習 Bootsrap 可安裝下面兩個 VScode 擴充套件 :
  • Preview on Web Server : 
    可直接在 VS code 直接瀏覽網頁, 不須另外開啟瀏覽器, 只要存檔立即更新顯示網頁, 且可與 HTML 編碼左右對照.
  • Sublime Text Keymap : 
    鍵盤映射文件 (還不清楚實際用途). 
按左邊上面第五個按鈕 (Extensions), 分別輸入上面兩個擴充套件名稱, 按 Install 安裝 :





其中 Preview on Web Server 提供兩個常用快捷鍵, 可以用來瀏覽網頁 : 
  • Ctrl+Shift+ P : 在 VS code 右邊開啟邊框來顯示網頁
  • Ctrl+Shift+ L : 開啟預設瀏覽器來顯示網頁
這樣就可以開始用 VS code 來編寫網頁了. 按右上角第一個按鈕 (Explorer), 再按 Open a folder 按鈕開啟一個工作目錄 (在 VS code 一個目錄就是一個專案) :




這時會彈出一個視窗詢問是否信任此 folder 下檔案之作者, 按 Yes :




這樣便建立了一個在 demo 資料夾下的專案, 按右上方圈起來的按鈕新增網頁 index.htm : 




然後在編輯區輸入驚嘆號 ! 再按 Enter 就會自動載入預設網頁模板 :





模板內容如下 : 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
   
</body>
</html>

2022年8月28日 星期日

好站 : 好用工具箱

最近在測試 Python 的 random 模組中的排列函式時, 偶然找到這個好站 :


裡面分門別類提供了時間, 數學, 文字, 財務等線上工具, 例如股票賣多少不吃虧, 我以前也想過這問題, 打算用 Javascript 寫個程式來算, 但有現成的就不用啦 !




觀察原始碼, 原來這網站是用 Bootstrap 設計的, 有空可以回頭觀摩他的做法.

2022年8月14日 星期日

完成教學網站 "用 GitHub 輕鬆架站"

因下周學會上課需要, 這兩周努力將之前部落格文章關於 GitHub 架站的濃縮整理在新申請的 GitHub 帳號上, 今天終於算是完工了. 雖然有將 CSS, Javascriipt, 與 jQuery 列入教學文件項目, 但這三個部分屬於進階要寫程式的領域, 對於在 GitHub 上架站來說, 只要對 HTML 與 Bootstrap 有概念, 會將模板改成自己的網頁就夠了, 所以以後有空再補充吧. 


本機專案資料夾 : D:\test\html\github_page 

雖然兩周前交出簡報檔就算交差了, 但花兩周時間手刻一個網站卻是十幾年前才有的傻勁, 趁這機會又把 HTML 與 Bootstrap 複習一遍也還不錯. 


2022-10-27 補充 : 

這兩天新增 Jumbotron 大螢幕廣告範例, 但 Carousel 廣告輪播測試 NG, 可能是版本關係. 

2023-01-15 補充 : 

今天發現一個不錯的 GitPage 教學網站 :


2022年8月3日 星期三

Bootstrap 4 學習筆記 (十) : 警示, 面板與手風琴

這幾天因為要準備客家語文學會的 GitHub 架站簡報資料, 把 Bootstrap 筆記複習了一遍, 發現還缺了 alert (警示), panel (面板), 以及 accordion (手風琴) 這幾個內容呈現容器元件還沒測試, 所以就趁這機會做個完結唄. 本系列之前的測試文章參考 :

# Bootstrap 4 學習筆記 (一) : 環境配置
# Bootstrap 4 學習筆記 (二) : 網格與容器
# Bootstrap 4 學習筆記 (三) : 表格
# Bootstrap 4 學習筆記 (四) : 圖示與按鈕
# Bootstrap 4 學習筆記 (五) : 文字與圖片效果
# Bootstrap 4 學習筆記 (六) : 表單
# Bootstrap 4 學習筆記 (七) : 按鈕選單
# Bootstrap 4 學習筆記 (八) : 導覽頁籤
# Bootstrap 4 學習筆記 (九) : 導覽列

Bootstrap 教學文件參考 :

# 第 10 堂課 - 初探 bootstrap 網頁製作
# Bootstrap 初學介紹 #靜態篇


一. Alert (警示) : 

此元件使用具有醒目背景色的 div 區塊來顯示重要訊息, 主要套用 alert 與下列五個樣式類別, 並指定 role="alert" 屬性 :


 樣式類別 說明
 alert 必要的警示區塊樣式
 alert-success 背景為咖啡色之區塊
 alert-info 背景為淡藍色之區塊 
 alert-warning 背景為黃色之區塊 
 alert-danger 背景為黃色之區塊
 alert-dismissible 添加可關閉按鈕 (x)


這些 div 外面再用 class="container" 的 div 區塊包起來, 基本結構如下 :

    <div class="container">
      <div class="alert alert-success" role="alert">
        這是 alert-success 訊息!
      </div>
    </div>

例如 : 



<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap-theme.min.css">
    <script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
    <script src="https://unpkg.com/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
  </head>
  <body>
    <div class="container">
      <div class="alert alert-success" role="alert">
        這是 alert-success 訊息!
      </div>
      <div class="alert alert-info" role="alert">
        這是 alert-info 訊息!
      </div>
      <div class="alert alert-warning" role="alert">
        這是 alert-warning 訊息!
      </div>
      <div class="alert alert-danger" role="alert">
        這是 alert-danger 訊息!
      </div> 
    </div>
  </body>
</html>

結果如下 : 




如果要建立可以關閉的警示, 需要添加 alert-dismissible 樣式類別, 並搭配一個有 close 樣式的 button 物件, 例如 :

      <div class="alert alert-danger alert-dismissible" role="alert">
        <button class="close" data-dismiss="alert">
          <span>&times;</span>
        </button>
        這是 alert-danger 訊息!
      </div> 

完整範例如下 : 



<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap-theme.min.css">
    <script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
    <script src="https://unpkg.com/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
  </head>
  <body>
    <div class="container">
      <div class="alert alert-success" role="alert">
        這是 alert-success 訊息!
      </div>
      <div class="alert alert-info" role="alert">
        這是 alert-info 訊息!
      </div>
      <div class="alert alert-warning" role="alert">
        這是 alert-warning 訊息!
      </div>
      <div class="alert alert-danger" role="alert">
        這是 alert-danger 訊息!
      </div> 
      <div class="alert alert-danger alert-dismissible" role="alert">
        <button class="close" data-dismiss="alert">
          <span>&times;</span>
        </button>
        這是 alert-danger 訊息!
      </div> 
    </div>
  </body>
</html>

結果如下 :




按後面那個 x 按鈕就可以關閉該條警示訊息了. 


二. Panel (面板) : 

面板也是像上面 alert 一樣的區塊, 但它除了主體 (body) 外還多出板首 (heading) 板尾 (footer) 兩部分, 不過首尾都是可有可無的. 基本結構由兩層 div 構成, 外層 div 需套用 panel 與下列四種背景色樣式類別 : 


 面板背景色樣式類別 說明
 panel-default 背景為灰色之預設面板
 panel-primary 背景為藍色之面板
 panel-success 背景為咖啡色之區塊 
 panel-info 背景為淡藍色之區塊
 panel-warning 背景為黃色之區塊
 panel-danger 背景為紅色之區塊


主體要套用 panel-body 樣式類別, 而板尾則套用 panel-footer 樣式類別, 基本結構如下 : 

     <div class="panel panel-default">
        <div class="panel-heading">面板板首</div>
        <div class="panel-body">面板主體</div>
        <div class="panel-body">面板板尾</div>
     </div>

例如 : 


範例 2-1 : 各種背景色之面板區塊測試 [看原始碼] 

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap-theme.min.css">
    <script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
    <script src="https://unpkg.com/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
  </head>
  <body>
    <div class="container">
      <div class="panel panel-default">
        <div class="panel-heading">面板板首 (default)</div>
        <div class="panel-body">面板主體</div>
        <div class="panel-body">面板板尾</div>
      </div>
      <div class="panel panel-primary">
        <div class="panel-heading">面板板首 (primary)</div>
        <div class="panel-body">面板主體</div>
        <div class="panel-body">面板板尾</div>
      </div>
      <div class="panel panel-success">
        <div class="panel-heading">面板板首 (success)</div>
        <div class="panel-body">面板主體</div>
        <div class="panel-body">面板板尾</div>
      </div>
      <div class="panel panel-info">
        <div class="panel-heading">面板板首 (info)</div>
        <div class="panel-body">面板主體</div>
        <div class="panel-body">面板板尾</div>
      </div>
      <div class="panel panel-warning">
        <div class="panel-heading">面板板首 (warning)</div>
        <div class="panel-body">面板主體</div>
        <div class="panel-body">面板板尾</div>
      </div>
      <div class="panel panel-danger">
        <div class="panel-heading">面板板首 (danger)</div>
        <div class="panel-body">面板主體</div>
        <div class="panel-body">面板板尾</div>
      </div>
    </div>
  </body>
</html>

結果如下 : 









板尾部分若不需要可以去除. 


三. Accordion (手風琴) : 

所謂手風琴效果 (accordion) 就是將網頁內容上下堆疊, 一次只展開一部份, 其餘部分會自動縮合起來的呈現方式, 這種可縮合 (collapse) 結構是在上面介紹的面板 (panel) 上發展出來的, 主要是添加了 a 標籤的頁內錨點 (anchor) 與 panel-collapse, collapse 等樣式類別來控制面板主體區塊的展開與縮合, 基本結構如下 : 

      <div class="panel panel-default">
        <div class="panel-heading">
          <h4 class="panel-title">
            <a data-toggle="collapse" href="#collapse1">標題</a>
          </h4>
        </div>
        <div id="collapse1" class="panel-collapse collapse">
          <div class="panel-body">內容</div>
        </div>
      </div>

參考 :


可見此組件基本上就是一個加工後的面板, 主要是板首標題改用 a 標籤來控制本體 div 區塊中的錨點 (id='collapse1') 以使其展開或縮合, 並為 a 標籤添加 data-toggle="collapse" 屬性, 另外, 本體 div 區塊也要加上 panel-collapse 與 collapse 兩個樣式類別, 完整網頁碼如下例 : 


 
<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap-theme.min.css">
    <script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
    <script src="https://unpkg.com/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
  </head>
  <body>
    <div class="container">
      <div class="panel panel-default">
        <div class="panel-heading">
          <h4 class="panel-title">
            <a data-toggle="collapse" href="#collapse1">標題</a>
          </h4>
        </div>
        <div id="collapse1" class="panel-collapse collapse">
          <div class="panel-body">內容</div>
        </div>
      </div>
    </div>
  </body>
</html>

結果如下 : 




預設內容是收合起來的, 點標題才會展開. 

如果預設要展開, 可在本體的 div 區塊添加 in 樣式類別 : 

      <div class="panel panel-default">
        <div class="panel-heading">
          <h4 class="panel-title">
            <a data-toggle="collapse" href="#collapse1">標題</a>
          </h4>
        </div>
        <div id="collapse1" class="panel-collapse collapse in">
          <div class="panel-body">內容</div>
        </div>
      </div>

例如 :



結果如下 : 




手風琴 (Accordion) 就是以上面的可收合面板為基礎建構的, 將多個可縮合面板再用一個樣式類別為 panel-group 的 div 區塊包起來並指定一個 id, 每一個可縮合面板添加一個 data-parent 屬性指向此 id 即可, 基本結構 (兩個可收合面板) 如下 : 

      <div class="panel-group" id="accordion">
        <div class="panel panel-default">
          <div class="panel-heading">
            <h4 class="panel-title">
              <a data-toggle="collapse" data-parent="#accordion" href="#collapse1">標題 1</a>
            </h4>
          </div>
          <div id="collapse1" class="panel-collapse collapse in">
            <div class="panel-body">內容 1</div>
          </div>
        </div>
        <div class="panel panel-default">
          <div class="panel-heading">
            <h4 class="panel-title">
              <a data-toggle="collapse" data-parent="#accordion" href="#collapse2">標題 2</a>
            </h4>
          </div>
          <div id="collapse2" class="panel-collapse collapse">
            <div class="panel-body">內容 2</div>
          </div>
        </div>
      </div>

手風琴群組裡面的每個面板都可以各自指定板首的背景色樣式類別, 例如 panel-primary, panel-info 等, 如下列範例所示  : 



<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap-theme.min.css">
    <script src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
    <script src="https://unpkg.com/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
  </head>
  <body>
    <div class="container">
      <div class="panel-group" id="accordion">
        <div class="panel panel-default">
          <div class="panel-heading">
            <h4 class="panel-title">
              <a data-toggle="collapse" data-parent="#accordion" href="#collapse1">標題 1</a>
            </h4>
          </div>
          <div id="collapse1" class="panel-collapse collapse in">
            <div class="panel-body">內容 1 (default)</div>
          </div>
        </div>
        <div class="panel panel-primary">
          <div class="panel-heading">
            <h4 class="panel-title">
              <a data-toggle="collapse" data-parent="#accordion" href="#collapse2">標題 2</a>
            </h4>
          </div>
          <div id="collapse2" class="panel-collapse collapse">
            <div class="panel-body">內容 2 (primary)</div>
          </div>
        </div>
        <div class="panel panel-success">
          <div class="panel-heading">
            <h4 class="panel-title">
              <a data-toggle="collapse" data-parent="#accordion" href="#collapse3">標題 3</a>
            </h4>
          </div>
          <div id="collapse3" class="panel-collapse collapse">
            <div class="panel-body">內容 3 (success)</div>
          </div>
        </div>
        <div class="panel panel-info">
          <div class="panel-heading">
            <h4 class="panel-title">
              <a data-toggle="collapse" data-parent="#accordion" href="#collapse4">標題 4</a>
            </h4>
          </div>
          <div id="collapse4" class="panel-collapse collapse">
            <div class="panel-body">內容 4 (info)</div>
          </div>
        </div>
        <div class="panel panel-warning">
          <div class="panel-heading">
            <h4 class="panel-title">
              <a data-toggle="collapse" data-parent="#accordion" href="#collapse5">標題 5</a>
            </h4>
          </div>
          <div id="collapse5" class="panel-collapse collapse">
            <div class="panel-body">內容 5 (warning)</div>
          </div>
        </div>
        <div class="panel panel-danger">
          <div class="panel-heading">
            <h4 class="panel-title">
              <a data-toggle="collapse" data-parent="#accordion" href="#collapse6">標題 6</a>
            </h4>
          </div>
          <div id="collapse6" class="panel-collapse collapse">
            <div class="panel-body">內容 6 (danger)</div>
          </div>
        </div>
      </div>
    </div>
  </body>
</html>

注意, 其中面板 1 的主體區塊添加了 in 樣式類別, 所以面板 1 預設是展開的, 結果如下 : 




可見每開啟一個面板, 其餘面板就會自動收合. 不過因板首的標題是 a 超連結, 所以會出現底線, 若不想出現底線可於每個板首 a 標籤添加 style="text-decoration:none;" 的樣式屬性, 或者於 head 標籤內的 style 標籤中, 統一規範所有超連結都步出現底線 :

a {text-decoration:none;}