2026年3月26日 星期四

[水井USR] 在Pythonanaywhere上用Django網站結合MindAR實現擴增實境

 



資料來源:https://hiukim.github.io/mind-ar-js-doc/face-tracking-examples/tryon
成果網站:https://virtualtryon.pythonanywhere.com/

1. virtualtryon/virtualtryon/settings.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
"""
Django settings for virtualtryon project.

Generated by 'django-admin startproject' using Django 5.1.3.

For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-%f3v+f#_-n3rgr=3d&g($*hufyzkxi+_uqs=1+=+1gj*60qiqr"

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['virtualtryon.pythonanywhere.com']


# Application definition

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "tryon",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = "virtualtryon.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / 'templates'],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

WSGI_APPLICATION = "virtualtryon.wsgi.application"


# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}


# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
    },
]


# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/

LANGUAGE_CODE = "en-us"

TIME_ZONE = "UTC"

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/

STATIC_URL = "static/"

# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

# default static files settings for PythonAnywhere.
# see https://help.pythonanywhere.com/pages/DjangoStaticFiles for more info
MEDIA_ROOT = '/home/virtualtryon/virtualtryon/media'
MEDIA_URL = '/media/'
STATIC_ROOT = '/home/virtualtryon/virtualtryon/static'
STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']

2.virtualtryon/virtualtryon/urls.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
"""
URL configuration for virtualtryon project.

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/5.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""

from django.contrib import admin
from django.urls import path
from tryon import views

urlpatterns = [
    path("admin/", admin.site.urls),
    path('', views.index, name='index'),
]

3.virtualtryon/tryon/views.py

1
2
3
4
from django.shortcuts import render

def index(request):
    return render(request, 'index.html')

4.virtualtryon/templates/index.html

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
{% load static %}

<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>虛擬試戴 - MindAR Face Tracking</title>
    
    <script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/mind-ar@1.2.5/dist/mindar-face-aframe.prod.js"></script>

    <style>
        body { margin: 0; overflow: hidden; background: #000; }
        .example-container {
            position: absolute;
            width: 100%;
            height: 100%;
        }
        .options-panel {
            position: fixed;
            left: 10px;
            top: 10px;
            z-index: 100;
            background: rgba(0,0,0,0.5);
            padding: 10px;
            border-radius: 8px;
            display: flex;
            gap: 8px;
            flex-wrap: wrap;
        }
        .options-panel img {
            width: 60px;
            height: 60px;
            object-fit: cover;
            border: 3px solid transparent;
            cursor: pointer;
            border-radius: 6px;
        }
        .options-panel img.selected {
            border-color: #00ff00;
        }
    </style>
</head>
<body>
    <div class="example-container">
        <!-- 選項面板 -->
        <div class="options-panel">
            <img id="hat1"    src="https://cdn.jsdelivr.net/gh/hiukim/mind-ar-js@1.2.5/examples/face-tracking/assets/hat/thumbnail.png" alt="Hat 1">
            <img id="hat2"    src="https://cdn.jsdelivr.net/gh/hiukim/mind-ar-js@1.2.5/examples/face-tracking/assets/hat2/thumbnail.png" alt="Hat 2">
            <img id="glasses1" src="https://cdn.jsdelivr.net/gh/hiukim/mind-ar-js@1.2.5/examples/face-tracking/assets/glasses/thumbnail.png" alt="Glasses 1">
            <img id="glasses2" src="https://cdn.jsdelivr.net/gh/hiukim/mind-ar-js@1.2.5/examples/face-tracking/assets/glasses2/thumbnail.png" alt="Glasses 2">
            <img id="earring"  src="https://cdn.jsdelivr.net/gh/hiukim/mind-ar-js@1.2.5/examples/face-tracking/assets/earring/thumbnail.png" alt="Earring">
        </div>

        <!-- A-Frame Scene -->
        <a-scene mindar-face embedded 
                 color-space="sRGB" 
                 renderer="colorManagement: true, physicallyCorrectLights" 
                 vr-mode-ui="enabled: false" 
                 device-orientation-permission-ui="enabled: false">

            <a-assets>
                <a-asset-item id="headModel"    src="https://cdn.jsdelivr.net/gh/hiukim/mind-ar-js@1.2.5/examples/face-tracking/assets/sparkar/headOccluder.glb"></a-asset-item>
                <a-asset-item id="glassesModel" src="https://cdn.jsdelivr.net/gh/hiukim/mind-ar-js@1.2.5/examples/face-tracking/assets/glasses/scene.gltf"></a-asset-item>
                <a-asset-item id="glassesModel2" src="https://cdn.jsdelivr.net/gh/hiukim/mind-ar-js@1.2.5/examples/face-tracking/assets/glasses2/scene.gltf"></a-asset-item>
                <a-asset-item id="hatModel"     src="https://cdn.jsdelivr.net/gh/hiukim/mind-ar-js@1.2.5/examples/face-tracking/assets/hat/scene.gltf"></a-asset-item>
                <a-asset-item id="hatModel2"    src="https://cdn.jsdelivr.net/gh/hiukim/mind-ar-js@1.2.5/examples/face-tracking/assets/hat2/scene.gltf"></a-asset-item>
                <a-asset-item id="earringModel" src="https://cdn.jsdelivr.net/gh/hiukim/mind-ar-js@1.2.5/examples/face-tracking/assets/earring/scene.gltf"></a-asset-item>
            </a-assets>

            <a-camera active="false" position="0 0 0"></a-camera>

            <!-- Head Occluder -->
            <a-entity mindar-face-target="anchorIndex: 168">
                <a-gltf-model mindar-face-occluder position="0 -0.3 0.15" rotation="0 0 0" scale="0.065 0.065 0.065" src="#headModel"></a-gltf-model>
            </a-entity>

            <!-- 配件 -->
            <a-entity mindar-face-target="anchorIndex: 10">
                <a-gltf-model class="hat1-entity"   visible="false" rotation="0 0 0" position="0 1.0 -0.5" scale="0.35 0.35 0.35" src="#hatModel"></a-gltf-model>
            </a-entity>
            <a-entity mindar-face-target="anchorIndex: 10">
                <a-gltf-model class="hat2-entity"   visible="false" rotation="0 0 0" position="0 -0.2 -0.5" scale="0.008 0.008 0.008" src="#hatModel2"></a-gltf-model>
            </a-entity>
            <a-entity mindar-face-target="anchorIndex: 168">
                <a-gltf-model class="glasses1-entity" visible="false" rotation="0 0 0" position="0 0 0" scale="0.01 0.01 0.01" src="#glassesModel"></a-gltf-model>
            </a-entity>
            <a-entity mindar-face-target="anchorIndex: 168">
                <a-gltf-model class="glasses2-entity" visible="false" rotation="0 -90 0" position="0 -0.3 0" scale="0.6 0.6 0.6" src="#glassesModel2"></a-gltf-model>
            </a-entity>
            <a-entity mindar-face-target="anchorIndex: 127">
                <a-gltf-model class="earring-entity" visible="false" rotation="-0.1 0 0" position="0 -0.3 -0.3" scale="0.05 0.05 0.05" src="#earringModel"></a-gltf-model>
            </a-entity>
            <a-entity mindar-face-target="anchorIndex: 356">
                <a-gltf-model class="earring-entity" visible="false" rotation="0.1 0 0" position="0 -0.3 -0.3" scale="0.05 0.05 0.05" src="#earringModel"></a-gltf-model>
            </a-entity>
        </a-scene>
    </div>

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            const list = ["glasses1", "glasses2", "hat1", "hat2", "earring"];
            const visibles = [true, false, false, true, true];

            const setVisible = (button, entities, visible) => {
                if (visible) {
                    button.classList.add("selected");
                } else {
                    button.classList.remove("selected");
                }
                entities.forEach((entity) => {
                    entity.setAttribute("visible", visible);
                });
            };

            list.forEach((item, index) => {
                const button = document.querySelector("#" + item);
                const entities = document.querySelectorAll("." + item + "-entity");
                
                if (button && entities.length > 0) {
                    setVisible(button, entities, visibles[index]);
                    
                    button.addEventListener('click', () => {
                        visibles[index] = !visibles[index];
                        setVisible(button, entities, visibles[index]);
                    });
                }
            });
        });
    </script>
</body>
</html>

[水井USR] 在Pythonanaywhere上用Django網站顯示3D模型

 


原始網站:https://aframe.io/examples/showcase/modelviewer/
成果網址:https://armodelviewer.pythonanywhere.com/

1. 先用Pythonanywhere建立一個Django的專案-名稱:ARmodelViewer

2. 利用console建立app



3.ARmodelViewer/ARmodelViewer/settings.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
"""
Django settings for ARmodelViewer project.

Generated by 'django-admin startproject' using Django 5.1.3.

For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-*bq*cvi-kt)h@l4yi2mto%h($^_n5rl^6q4udiyu==j###sgaz"

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['ARmodelViewer.pythonanywhere.com']


# Application definition

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "myapp",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = "ARmodelViewer.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / 'templates'],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

WSGI_APPLICATION = "ARmodelViewer.wsgi.application"


# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}


# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
    },
]


# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/

LANGUAGE_CODE = "en-us"

TIME_ZONE = "UTC"

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/

STATIC_URL = "static/"

# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

# default static files settings for PythonAnywhere.
# see https://help.pythonanywhere.com/pages/DjangoStaticFiles for more info
MEDIA_ROOT = '/home/ARmodelViewer/ARmodelViewer/media'
MEDIA_URL = '/media/'
STATIC_ROOT = '/home/ARmodelViewer/ARmodelViewer/static'
STATIC_URL = '/static/'
STATICFILES_DIRS = [
    BASE_DIR / "static",
]

4. ARmodelViewer/ARmodelViewer/urls.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
"""
URL configuration for ARmodelViewer project.

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/5.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""

from django.contrib import admin
from django.urls import path
from myapp import views

urlpatterns = [
    path("admin/", admin.site.urls),
    path('', views.model_viewer, name='ar_model_viewer'),
    path('ar-model-viewer/', views.model_viewer, name='ar_model_viewer'),
]

6. ARmodelViewer/myapp/views.py
1
2
3
4
from django.shortcuts import render

def model_viewer(request):
    return render(request, 'ar_model_viewer.html')

7.ARmodelViewer/templates/ar_model_viewer.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{% load static %}

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Model Viewer AR - VR</title>
  <meta name="description" content="Model Viewer (VR / AR) • A-Frame">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

  <!-- A-Frame 主程式庫使用官方最新穩定版 CDN -->
  <script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>

  <!-- 自訂的 A-Frame components請放到您的 static 資料夾 -->
  <script src="{% static 'js/animation-mixer.js' %}"></script>
  <script src="{% static 'js/ar-shadows.js' %}"></script>
  <script src="{% static 'js/model-viewer.js' %}"></script>
  <script src="{% static 'js/background-gradient.js' %}"></script>
</head>
<body>
  <a-scene
    renderer="colorManagement: true;"
    info-message="htmlSrc: #messageText"
    model-viewer="gltfModel: #triceratops; title: Triceratops"
    xr-mode-ui="XRMode: xr">

    <a-assets timeout="10000">
      <!--
        Model source: https://sketchfab.com/3d-models/triceratops-d16aabe33dc24f8ab37e3df50c068265
        Model author: https://sketchfab.com/VapTor
        Model license: Sketchfab Standard
      -->
      <a-asset-item id="triceratops"
        src="https://cdn.aframe.io/examples/ar/models/triceratops/scene.gltf"
        response-type="arraybuffer"></a-asset-item>

      <img id="shadow" src="{% static 'images/shadow.png' %}">

    </a-assets>

  </a-scene>
</body>
</html>

8.從A-Frame網站上下載js檔案,先按View Source,從Github上下載。


9.把js上傳到網站
ARmodelViewer/static/js


10.重新整理(Reload),開啟網站,就可以看到模型如文章剛開始的第一張圖。


2026年3月23日 星期一

[水井USR] 使用Session來記錄旅人到水井村觀光

 



1.樣版程式:suijing_village/templates/index.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
  <meta charset="UTF-8">
  <title>水井村導覽</title>
  <style>
    body { font-family: system-ui; max-width: 700px; margin: 40px auto; padding: 0 20px; }
    h1 { color: #1e5955; text-align: center; }
    .card { background: #f8f9fa; padding: 20px; border-radius: 12px; margin: 20px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
    .btn { padding: 10px 20px; background: #2c7a77; color: white; border: none; border-radius: 8px; cursor: pointer; }
    .btn:hover { background: #1e5955; }
    .taste-btn { margin: 8px; padding: 10px 18px; background: #e0f2f1; border: 1px solid #b2dfdb; border-radius: 999px; cursor: pointer; }
    .taste-btn.active { background: #80cbc4; }
  </style>
</head>
<body>

  <h1>水井村導覽</h1>

  <div class="card">
    <h3>歡迎!{% if is_first_visit %}初次造訪的旅人{% else %}第 <strong>{{ visit_count }}</strong> 次回訪{% endif %}</h3>
    <p>現在時間:{{ now }}</p>
  </div>

  <div class="card">
    <h3>目前導覽進度</h3>
    <p>正在參觀: <strong>{{ current_place }}</strong></p>
    <p>進度:{{ current_step }} / {{ total_steps }}</p>

    {% if current_step < total_steps %}
      <a href="{% url 'village:next_step' %}"><button class="btn">前往下一站 →</button></a>
    {% else %}
      <p style="color:#2c7a77;">恭喜!你已完成水井村全路線!</p>
    {% endif %}
  </div>

  <div class="card">
    <h3>你最喜歡的水井產品</h3>
    <p>目前選擇: <strong>{{ favorite_taste }}</strong></p>

    <form method="post" action="{% url 'village:choose_taste' %}">
      {% csrf_token %}
      <button type="submit" name="taste" value="蕃茄" class="taste-btn {% if favorite_taste == '蕃茄' %}active{% endif %}">蕃茄</button>
      <button type="submit" name="taste" value="工作蝦" class="taste-btn {% if favorite_taste == '工作蝦' %}active{% endif %}">工作蝦</button>
      <button type="submit" name="taste" value="文蛤" class="taste-btn {% if favorite_taste == '文蛤' %}active{% endif %}">文蛤</button>
      <button type="submit" name="taste" value="其他" class="taste-btn {% if favorite_taste == '其他' %}active{% endif %}">其他</button>
    </form>
  </div>

  <div style="text-align:center; margin-top:40px;">
    <a href="{% url 'village:reset' %}"><button class="btn" style="background:#c62828;">重置我的水井村之旅</button></a>
  </div>

</body>
</html>

2.視域程式:suijing_village/village/views.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# village/views.py
from django.shortcuts import render, redirect
from django.http import HttpResponse
from datetime import datetime

def home(request):
    # 確保 session 已初始化(Django 會自動處理,但有時需要強制觸發)
    if not request.session.session_key:
        request.session.create()   # 手動建立 session key(進階用法)

    # === 來訪次數 ===
    visit_count = request.session.get('suijing_visit_count', 0) + 1
    request.session['suijing_visit_count'] = visit_count

    # === 使用者目前路線進度(多步驟導覽) ===
    current_route_step = request.session.get('current_route_step', 1)
    route_steps = [
        "起點:水井奉天宮",
        "步驟2:水井姻緣花",
        "步驟3:水井柴井車",
        "終點:風雲客棧"
    ]

    # === 使用者偏好:喜歡的產品 ===
    favorite_taste = request.session.get('favorite_well_taste', '尚未選擇')

    context = {
        'visit_count': visit_count,
        'is_first_visit': visit_count == 1,
        'current_step': current_route_step,
        'current_place': route_steps[current_route_step - 1] if current_route_step <= len(route_steps) else "已完成全部路線",
        'total_steps': len(route_steps),
        'favorite_taste': favorite_taste,
        'now': datetime.now().strftime("%Y-%m-%d %H:%M"),
    }

    return render(request, 'index.html', context)


def next_step(request):
    """下一站導覽"""
    current = request.session.get('current_route_step', 1)
    request.session['current_route_step'] = current + 1
    # 可選:設定更短的 session 過期時間(例如導覽 2 小時內有效)
    # request.session.set_expiry(60 * 60 * 2)
    return redirect('village:home')


def choose_taste(request):
    """選擇喜歡的井水口味"""
    if request.method == 'POST':
        taste = request.POST.get('taste')
        if taste in ['蕃茄', '工作蝦', '文蛤', '其他']:
            request.session['favorite_well_taste'] = taste
            # 進階:可以把偏好設成較長存活時間
            request.session.set_expiry(60 * 60 * 24 * 30)  # 30 天
    return redirect('village:home')


def reset_journey(request):
    """重置我的水井村之旅"""
    # 方法一:刪除特定 key
    # request.session.pop('suijing_visit_count', None)
    # request.session.pop('current_route_step', None)
    # request.session.pop('favorite_well_taste', None)

    # 方法二:全部清空(最乾脆)
    request.session.flush()          # 清空所有 session 資料,但保留 session key
    # request.session.clear()        # 另一種寫法(Django 4.0+ 推薦)

    return redirect('village:home')


def debug_session(request):
    """開發時用的除錯頁面"""
    return HttpResponse(
        f"<pre>{dict(request.session)}</pre>"
        "<br><a href='/'>回首頁</a>"
    )

3.app路由器:suijing_village/village/urls.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# village/urls.py

from django.urls import path
from . import views

app_name = 'village'

urlpatterns = [
    path('', views.home, name='home'),
    path('next/', views.next_step, name='next_step'),
    path('choose-taste/', views.choose_taste, name='choose_taste'),
    path('reset/', views.reset_journey, name='reset'),
    path('debug/', views.debug_session, name='debug_session'),  # 開發用
]


3.主路由器:suijing_village/suijing_village/urls.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
"""suijing_village URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/4.0/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('village.urls')),
]

4.設定檔案:suijing_village/suijing_village/settings.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
"""
Django settings for suijing_village project.

Generated by 'django-admin startproject' using Django 4.0.6.

For more information on this file, see
https://docs.djangoproject.com/en/4.0/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.0/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-5d5y6c=nx8d1x+tn!r2!tl7fnlj)6_+l2_o90czc#d907!q6df'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['cmlin.pythonanywhere.com']


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'village',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'suijing_village.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'suijing_village.wsgi.application'


# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/

STATIC_URL = 'static/'

# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

# default static files settings for PythonAnywhere.
# see https://help.pythonanywhere.com/pages/DjangoStaticFiles for more info
MEDIA_ROOT = '/home/cmlin/suijing_village/media'
MEDIA_URL = '/media/'
STATIC_ROOT = '/home/cmlin/suijing_village/static'
STATIC_URL = '/static/'