sheng #1
@ -1,9 +1,48 @@
|
|||||||
from flask import Flask
|
from flask import Flask, render_template
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_login import LoginManager
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
def create_app():
|
db = SQLAlchemy()
|
||||||
|
login_manager = LoginManager()
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(id):
|
||||||
|
from app.models.user import User
|
||||||
|
return User.query.get(int(id))
|
||||||
|
|
||||||
|
def create_app(config_class=Config):
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
app.config.from_object(config_class)
|
||||||
|
|
||||||
|
# 初始化擴展
|
||||||
|
db.init_app(app)
|
||||||
|
login_manager.init_app(app)
|
||||||
|
login_manager.login_view = 'auth.login' # 設置登入頁面的端點
|
||||||
|
login_manager.login_message = '請先登入後再訪問此頁面' # 自定義提示訊息
|
||||||
|
login_manager.login_message_category = 'warning' # 設置提示訊息的類別
|
||||||
|
|
||||||
|
# 註冊藍圖
|
||||||
from app.routes.main import main_bp
|
from app.routes.main import main_bp
|
||||||
|
from app.routes.settings import settings_bp
|
||||||
|
from app.routes.auth import auth_bp
|
||||||
|
|
||||||
app.register_blueprint(main_bp)
|
app.register_blueprint(main_bp)
|
||||||
|
app.register_blueprint(settings_bp)
|
||||||
|
app.register_blueprint(auth_bp)
|
||||||
|
|
||||||
|
# 確保錯誤處理模板存在
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def not_found_error(error):
|
||||||
|
return render_template('errors/404.html'), 404
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def internal_error(error):
|
||||||
|
db.session.rollback()
|
||||||
|
return render_template('errors/500.html'), 500
|
||||||
|
|
||||||
|
# 創建所有數據表
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
19
app/config.py
Normal file
19
app/config.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import os
|
||||||
|
try:
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
SECRET_KEY = os.environ.get('SECRET_KEY') or 'default-secret-key'
|
||||||
|
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
|
||||||
|
'sqlite:///app.db'
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
|
||||||
|
# 其他配置項
|
||||||
|
MAIL_SERVER = os.environ.get('MAIL_SERVER')
|
||||||
|
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25)
|
||||||
|
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
|
||||||
|
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
||||||
|
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
27
app/models/user.py
Normal file
27
app/models/user.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from app import db, login_manager
|
||||||
|
from flask_login import UserMixin
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(id):
|
||||||
|
return User.query.get(int(id))
|
||||||
|
|
||||||
|
class User(UserMixin, db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
username = db.Column(db.String(64), unique=True, index=True, nullable=False)
|
||||||
|
email = db.Column(db.String(120), unique=True, index=True, nullable=False)
|
||||||
|
password_hash = db.Column(db.String(128))
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
last_login = db.Column(db.DateTime, default=datetime.utcnow) # 新增此欄位
|
||||||
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
|
is_admin = db.Column(db.Boolean, default=False)
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
self.password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<User {self.username}>'
|
@ -1,5 +0,0 @@
|
|||||||
from flask import Blueprint
|
|
||||||
|
|
||||||
main_bp = Blueprint('main', __name__)
|
|
||||||
|
|
||||||
from app.routes import main
|
|
0
app/routes/api.py
Normal file
0
app/routes/api.py
Normal file
127
app/routes/auth.py
Normal file
127
app/routes/auth.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||||
|
from flask_login import login_user, logout_user, login_required, current_user
|
||||||
|
from app.models.user import User
|
||||||
|
from app import db
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
auth_bp = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for('main.index'))
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
email = request.form.get('email')
|
||||||
|
password = request.form.get('password')
|
||||||
|
remember = request.form.get('remember', False)
|
||||||
|
|
||||||
|
user = User.query.filter_by(email=email).first()
|
||||||
|
if user is None or not user.check_password(password):
|
||||||
|
flash('電子郵件或密碼錯誤', 'danger')
|
||||||
|
return redirect(url_for('auth.login'))
|
||||||
|
|
||||||
|
# 更新最後登入時間
|
||||||
|
user.last_login = datetime.utcnow()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
login_user(user, remember=remember)
|
||||||
|
next_page = request.args.get('next')
|
||||||
|
if not next_page or urlparse(next_page).netloc != '':
|
||||||
|
next_page = url_for('main.index')
|
||||||
|
flash('登入成功!', 'success')
|
||||||
|
return redirect(next_page)
|
||||||
|
|
||||||
|
return render_template('auth/login.html', title='登入')
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/register', methods=['GET', 'POST'])
|
||||||
|
def register():
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for('main.index'))
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
email = request.form.get('email')
|
||||||
|
username = request.form.get('username')
|
||||||
|
password = request.form.get('password')
|
||||||
|
confirm_password = request.form.get('confirm_password')
|
||||||
|
|
||||||
|
if password != confirm_password:
|
||||||
|
flash('密碼不一致', 'danger')
|
||||||
|
return redirect(url_for('auth.register'))
|
||||||
|
|
||||||
|
if User.query.filter_by(email=email).first():
|
||||||
|
flash('此電子郵件已被註冊', 'danger')
|
||||||
|
return redirect(url_for('auth.register'))
|
||||||
|
|
||||||
|
if User.query.filter_by(username=username).first():
|
||||||
|
flash('此用戶名已被使用', 'danger')
|
||||||
|
return redirect(url_for('auth.register'))
|
||||||
|
|
||||||
|
user = User(email=email, username=username)
|
||||||
|
user.set_password(password)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash('註冊成功!請登入。', 'success')
|
||||||
|
return redirect(url_for('auth.login'))
|
||||||
|
|
||||||
|
return render_template('auth/register.html', title='註冊')
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/logout')
|
||||||
|
@login_required
|
||||||
|
def logout():
|
||||||
|
logout_user()
|
||||||
|
flash('已登出', 'info')
|
||||||
|
return redirect(url_for('main.index'))
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/profile', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def profile():
|
||||||
|
if request.method == 'POST':
|
||||||
|
action = request.form.get('action')
|
||||||
|
|
||||||
|
if action == 'update_profile':
|
||||||
|
# 檢查用戶名是否已存在
|
||||||
|
if (request.form.get('username') != current_user.username and
|
||||||
|
User.query.filter_by(username=request.form.get('username')).first()):
|
||||||
|
flash('此用戶名已被使用', 'danger')
|
||||||
|
return redirect(url_for('auth.profile'))
|
||||||
|
|
||||||
|
# 檢查郵箱是否已存在
|
||||||
|
if (request.form.get('email') != current_user.email and
|
||||||
|
User.query.filter_by(email=request.form.get('email')).first()):
|
||||||
|
flash('此電子郵件已被註冊', 'danger')
|
||||||
|
return redirect(url_for('auth.profile'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_user.username = request.form.get('username')
|
||||||
|
current_user.email = request.form.get('email')
|
||||||
|
db.session.commit()
|
||||||
|
flash('個人資料已更新', 'success')
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash('更新失敗', 'danger')
|
||||||
|
|
||||||
|
return redirect(url_for('auth.profile'))
|
||||||
|
|
||||||
|
elif action == 'update_password':
|
||||||
|
if not current_user.check_password(request.form.get('current_password')):
|
||||||
|
flash('目前密碼不正確', 'danger')
|
||||||
|
elif request.form.get('new_password') != request.form.get('confirm_password'):
|
||||||
|
flash('新密碼與確認密碼不符', 'danger')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
current_user.set_password(request.form.get('new_password'))
|
||||||
|
db.session.commit()
|
||||||
|
flash('密碼已更新', 'success')
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash('更新失敗', 'danger')
|
||||||
|
|
||||||
|
return redirect(url_for('auth.profile'))
|
||||||
|
|
||||||
|
return render_template('auth/profile.html', title='個人資料')
|
@ -1,7 +1,11 @@
|
|||||||
from flask import render_template, Blueprint
|
from flask import render_template, Blueprint
|
||||||
|
|
||||||
main_bp = Blueprint('main', __name__)
|
main_bp = Blueprint('main', __name__, url_prefix='/')
|
||||||
|
|
||||||
@main_bp.route('/')
|
@main_bp.route('/')
|
||||||
def index():
|
def index():
|
||||||
return render_template('main/index.html', title='首頁')
|
return render_template('main/index.html', title='首頁')
|
||||||
|
|
||||||
|
@main_bp.route('/about')
|
||||||
|
def about():
|
||||||
|
return render_template('main/about.html', title='關於我們')
|
||||||
|
76
app/routes/settings.py
Normal file
76
app/routes/settings.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
from flask import render_template, Blueprint, flash, redirect, url_for, request
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from app import db
|
||||||
|
from app.models.user import User
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
settings_bp = Blueprint('settings', __name__, url_prefix='/settings')
|
||||||
|
|
||||||
|
def get_system_stats():
|
||||||
|
"""獲取系統統計數據"""
|
||||||
|
try:
|
||||||
|
# 獲取總用戶數
|
||||||
|
total_users = User.query.count()
|
||||||
|
|
||||||
|
# 獲取活躍用戶數(最近30天內有登入的用戶)
|
||||||
|
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
|
||||||
|
active_users = User.query.filter(User.last_login >= thirty_days_ago).count()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total_users': total_users,
|
||||||
|
'active_users': active_users,
|
||||||
|
'system_version': '1.0.0',
|
||||||
|
'last_update': '2024-10-27'
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
# 如果發生錯誤,返回預設值
|
||||||
|
return {
|
||||||
|
'total_users': 0,
|
||||||
|
'active_users': 0,
|
||||||
|
'system_version': '1.0.0',
|
||||||
|
'last_update': '2024-10-27'
|
||||||
|
}
|
||||||
|
|
||||||
|
@settings_bp.route('/')
|
||||||
|
@login_required
|
||||||
|
def index():
|
||||||
|
if request.method == 'POST':
|
||||||
|
action = request.form.get('action')
|
||||||
|
|
||||||
|
if action == 'update_profile':
|
||||||
|
try:
|
||||||
|
current_user.username = request.form.get('username', current_user.username)
|
||||||
|
current_user.email = request.form.get('email', current_user.email)
|
||||||
|
db.session.commit()
|
||||||
|
flash('個人資料已更新', 'success')
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash(f'更新失敗: {str(e)}', 'danger')
|
||||||
|
|
||||||
|
elif action == 'update_password':
|
||||||
|
current_password = request.form.get('current_password')
|
||||||
|
new_password = request.form.get('new_password')
|
||||||
|
confirm_password = request.form.get('confirm_password')
|
||||||
|
|
||||||
|
if not current_user.check_password(current_password):
|
||||||
|
flash('目前密碼不正確', 'danger')
|
||||||
|
elif new_password != confirm_password:
|
||||||
|
flash('新密碼與確認密碼不符', 'danger')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
current_user.set_password(new_password)
|
||||||
|
db.session.commit()
|
||||||
|
flash('密碼已更新', 'success')
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash(f'更新失敗: {str(e)}', 'danger')
|
||||||
|
|
||||||
|
return redirect(url_for('settings.index'))
|
||||||
|
|
||||||
|
# 獲取系統統計數據
|
||||||
|
stats = get_system_stats()
|
||||||
|
|
||||||
|
return render_template('pages/settings.html',
|
||||||
|
title='設定',
|
||||||
|
stats=stats,
|
||||||
|
)
|
32
app/templates/auth/login.html
Normal file
32
app/templates/auth/login.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">登入</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ url_for('auth.login') }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">電子郵件</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">密碼</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="remember" name="remember">
|
||||||
|
<label class="form-check-label" for="remember">記住我</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">登入</button>
|
||||||
|
</form>
|
||||||
|
<hr>
|
||||||
|
<p>還沒有帳號? <a href="{{ url_for('auth.register') }}">立即註冊</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
102
app/templates/auth/profile.html
Normal file
102
app/templates/auth/profile.html
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<!-- 個人資料卡片 -->
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="card-title mb-0">個人資料</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ url_for('auth.profile') }}">
|
||||||
|
<input type="hidden" name="action" value="update_profile">
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="avatar-circle bg-primary text-white d-flex align-items-center justify-content-center"
|
||||||
|
style="width: 100px; height: 100px; border-radius: 50%; font-size: 2.5rem;">
|
||||||
|
{{ current_user.username[0].upper() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<h5 class="mb-1">{{ current_user.username }}</h5>
|
||||||
|
<p class="text-muted mb-0">{{ current_user.email }}</p>
|
||||||
|
<small class="text-muted">加入時間:{{ current_user.created_at.strftime('%Y-%m-%d') }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">用戶名</label>
|
||||||
|
<input type="text" class="form-control" name="username"
|
||||||
|
value="{{ current_user.username }}" required>
|
||||||
|
<div class="form-text">用戶名將顯示在您的個人檔案中</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">電子郵件</label>
|
||||||
|
<input type="email" class="form-control" name="email"
|
||||||
|
value="{{ current_user.email }}" required>
|
||||||
|
<div class="form-text">用於接收通知和重要更新</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-save"></i> 保存變更
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 密碼修改卡片 -->
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="card-title mb-0">修改密碼</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ url_for('auth.profile') }}">
|
||||||
|
<input type="hidden" name="action" value="update_password">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">目前密碼</label>
|
||||||
|
<input type="password" class="form-control" name="current_password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">新密碼</label>
|
||||||
|
<input type="password" class="form-control" name="new_password" required>
|
||||||
|
<div class="form-text">密碼必須包含至少8個字元</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">確認新密碼</label>
|
||||||
|
<input type="password" class="form-control" name="confirm_password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-key"></i> 更新密碼
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// 如果有 Flash 消息,自動隱藏
|
||||||
|
var alerts = document.querySelectorAll('.alert');
|
||||||
|
alerts.forEach(function(alert) {
|
||||||
|
setTimeout(function() {
|
||||||
|
alert.classList.add('fade');
|
||||||
|
setTimeout(function() {
|
||||||
|
alert.remove();
|
||||||
|
}, 150);
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
36
app/templates/auth/register.html
Normal file
36
app/templates/auth/register.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">註冊</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ url_for('auth.register') }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">電子郵件</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">用戶名</label>
|
||||||
|
<input type="text" class="form-control" id="username" name="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">密碼</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="confirm_password" class="form-label">確認密碼</label>
|
||||||
|
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">註冊</button>
|
||||||
|
</form>
|
||||||
|
<hr>
|
||||||
|
<p>已有帳號? <a href="{{ url_for('auth.login') }}">登入</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -16,9 +16,19 @@
|
|||||||
<body>
|
<body>
|
||||||
{% include 'components/navbar.html' %}
|
{% include 'components/navbar.html' %}
|
||||||
|
|
||||||
<main class="container py-4">
|
<div class="container py-4" role="main">
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</div>
|
||||||
|
|
||||||
{% include 'components/footer.html' %}
|
{% include 'components/footer.html' %}
|
||||||
|
|
||||||
|
@ -18,6 +18,58 @@
|
|||||||
首頁
|
首頁
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link px-3 {% if request.endpoint == 'main.about' %}fw-medium text-primary{% endif %}"
|
||||||
|
href="{{ url_for('main.about') }}">
|
||||||
|
關於我們
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link px-3 dropdown-toggle" href="#"
|
||||||
|
id="navbarDropdown" role="button"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="bi bi-person-circle"></i>
|
||||||
|
{{ current_user.username }}
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if request.endpoint == 'auth.profile' %}fw-medium text-primary{% endif %}"
|
||||||
|
href="{{ url_for('auth.profile') }}">
|
||||||
|
<i class="bi bi-person me-2"></i>個人資料
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if request.endpoint == 'settings.index' %}fw-medium text-primary{% endif %}"
|
||||||
|
href="{{ url_for('settings.index') }}">
|
||||||
|
<i class="bi bi-gear me-2"></i>設定
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item text-danger" href="{{ url_for('auth.logout') }}">
|
||||||
|
<i class="bi bi-box-arrow-right me-2"></i>登出
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link px-3 {% if request.endpoint == 'auth.login' %}fw-medium text-primary{% endif %}"
|
||||||
|
href="{{ url_for('auth.login') }}">
|
||||||
|
<i class="bi bi-box-arrow-in-right"></i> 登入
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link px-3 {% if request.endpoint == 'auth.register' %}fw-medium text-primary{% endif %}"
|
||||||
|
href="{{ url_for('auth.register') }}">
|
||||||
|
<i class="bi bi-person-plus"></i> 註冊
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
7
app/templates/errors/404.html
Normal file
7
app/templates/errors/404.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>找不到頁面</h1>
|
||||||
|
<p>請檢查網址是否正確</p>
|
||||||
|
<p><a href="{{ url_for('main.index') }}">返回首頁</a></p>
|
||||||
|
{% endblock %}
|
7
app/templates/errors/500.html
Normal file
7
app/templates/errors/500.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>發生錯誤</h1>
|
||||||
|
<p>抱歉,發生了一些問題。請稍後再試。</p>
|
||||||
|
<p><a href="{{ url_for('main.index') }}">返回首頁</a></p>
|
||||||
|
{% endblock %}
|
29
app/templates/main/about.html
Normal file
29
app/templates/main/about.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h1 class="card-title mb-4">關於我們</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h5 class="mb-3">我們的故事</h5>
|
||||||
|
<p>在這裡寫入關於網站或團隊的介紹...</p>
|
||||||
|
|
||||||
|
<h5 class="mb-3 mt-4">我們的使命</h5>
|
||||||
|
<p>在這裡寫入網站的目標和使命...</p>
|
||||||
|
|
||||||
|
<h5 class="mb-3 mt-4">聯絡我們</h5>
|
||||||
|
<p>如果您有任何問題,歡迎聯繫我們:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Email: example@example.com</li>
|
||||||
|
<li>電話: (02) 1234-5678</li>
|
||||||
|
<li>地址: 台灣台北市...</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -1,5 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<h1>你好! {{ current_user.username }}</h1>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
73
app/templates/pages/settings.html
Normal file
73
app/templates/pages/settings.html
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<!-- 左側導航 -->
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<button class="list-group-item list-group-item-action active"
|
||||||
|
data-bs-toggle="list"
|
||||||
|
href="#overview">
|
||||||
|
<i class="bi bi-grid-1x2"></i> 總覽
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右側內容 -->
|
||||||
|
<div class="col-md-7">
|
||||||
|
<div class="tab-content">
|
||||||
|
<!-- 總覽面板 -->
|
||||||
|
<div class="tab-pane fade show active" id="overview">
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="card-title mb-0">系統總覽</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="border rounded p-3 text-center">
|
||||||
|
<h3 class="mb-0">{{ stats.total_users }}</h3>
|
||||||
|
<small>總用戶數</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="border rounded p-3 text-center">
|
||||||
|
<h3 class="mb-0">{{ stats.active_users }}</h3>
|
||||||
|
<small>活躍用戶</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<h6 class="fw-bold">系統資訊</h6>
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<tr>
|
||||||
|
<td class="w-25">版本</td>
|
||||||
|
<td>{{ stats.system_version }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>最後更新</td>
|
||||||
|
<td>{{ stats.last_update }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// 初始化 Bootstrap 的 tab 功能
|
||||||
|
var triggerTabList = [].slice.call(document.querySelectorAll('[data-bs-toggle="list"]'));
|
||||||
|
triggerTabList.forEach(function(triggerEl) {
|
||||||
|
new bootstrap.Tab(triggerEl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@ -1 +1,4 @@
|
|||||||
|
Flask-Login~=0.6.3
|
||||||
|
Werkzeug~=3.0.6
|
||||||
Flask~=3.0.3
|
Flask~=3.0.3
|
||||||
|
python-dotenv~=1.0.1
|
||||||
|
Loading…
Reference in New Issue
Block a user