From b135a7aa206910e0ad6ac86759a2c564f4b3d3e1 Mon Sep 17 00:00:00 2001 From: wcchiu Date: Mon, 8 Sep 2025 17:06:58 +0800 Subject: [PATCH] Initial commit --- README.md | 75 +++++++ app.py | 464 +++++++++++++++++++++++++++++++++++++++++ create_users_table.sql | 17 ++ package.json | 12 ++ requirements.txt | 3 + sample_users.csv | 6 + static/add_user.html | 65 ++++++ static/css/style.css | 298 ++++++++++++++++++++++++++ static/index.html | 113 ++++++++++ static/js/add_user.js | 141 +++++++++++++ static/js/script.js | 337 ++++++++++++++++++++++++++++++ 11 files changed, 1531 insertions(+) create mode 100644 README.md create mode 100644 app.py create mode 100644 create_users_table.sql create mode 100644 package.json create mode 100644 requirements.txt create mode 100644 sample_users.csv create mode 100644 static/add_user.html create mode 100644 static/css/style.css create mode 100644 static/index.html create mode 100644 static/js/add_user.js create mode 100644 static/js/script.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..643c392 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# Flask MySQL API + +這是一個使用 Flask 和 MySQL 建立的 RESTful API 服務,提供使用者資料的 CRUD 操作。 + +## 功能特點 + +- 完整的 RESTful API 設計 +- MySQL 資料庫連接 +- 參數化查詢防止 SQL 注入 +- 支援分頁和過濾 +- 統一的回應格式 +- 錯誤處理 +- CORS 支援 + +## API 端點 + +- `GET /v1/users`:取得所有使用者,支援 `min_age`、`max_age`、`page`、`limit` 參數 +- `GET /v1/users/`:取得特定使用者 +- `POST /v1/users`:建立新使用者 +- `PATCH /v1/users/`:更新使用者資料 +- `DELETE /v1/users/`:刪除使用者 + +## 安裝與執行 + +1. 安裝必要套件: + +```bash +pip install Flask mysql-connector-python flask-cors +``` + +2. 執行應用程式: + +```bash +python app.py +``` + +伺服器將在 http://localhost:5000 啟動。 + +## 使用範例 + +### 取得所有使用者(含分頁和過濾) + +``` +GET /v1/users?min_age=18&max_age=30&page=1&limit=10 +``` + +### 建立新使用者 + +``` +POST /v1/users +Content-Type: application/json + +{ + "name": "王小明", + "email": "wang@example.com", + "age": 25 +} +``` + +### 更新使用者 + +``` +PATCH /v1/users/1 +Content-Type: application/json + +{ + "age": 26 +} +``` + +### 刪除使用者 + +``` +DELETE /v1/users/1 +``` \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..9eb2e8e --- /dev/null +++ b/app.py @@ -0,0 +1,464 @@ +from flask import Flask, request, jsonify, send_from_directory, render_template +from flask_cors import CORS +import mysql.connector +from mysql.connector import Error +import os +import pandas as pd +import io +import csv +from werkzeug.utils import secure_filename + +app = Flask(__name__, static_folder='static') +CORS(app) + +# 檔案上傳設定 +UPLOAD_FOLDER = 'uploads' +ALLOWED_EXTENSIONS = {'csv', 'xlsx', 'xls'} + +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER +app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 限制上傳檔案大小為 16MB + +# 確保上傳資料夾存在 +os.makedirs(UPLOAD_FOLDER, exist_ok=True) + +# 資料庫連接設定 +db_config = { + 'host': 'mysql.theaken.com', + 'port': 33306, + 'user': 'A008', + 'password': '74knygxwzEms', + 'database': 'db_A008' +} + +# 建立資料庫連接 +def get_db_connection(): + try: + conn = mysql.connector.connect(**db_config) + return conn + except Error as e: + print(f"資料庫連接錯誤: {e}") + return None + +# 統一回應格式 +def make_response(status, code, message, data=None): + response = { + "status": status, + "code": code, + "message": message + } + if data is not None: + response["data"] = data + return jsonify(response) + +# 錯誤處理 +@app.errorhandler(404) +def not_found(error): + return make_response("error", 404, "Not Found"), 404 + +@app.errorhandler(400) +def bad_request(error): + return make_response("error", 400, "Bad Request"), 400 + +@app.errorhandler(500) +def server_error(error): + return make_response("error", 500, "Internal Server Error"), 500 + +# 路由: GET /v1/users +@app.route('/v1/users', methods=['GET']) +def get_users(): + try: + # 取得查詢參數 + min_age = request.args.get('min_age', type=int) + max_age = request.args.get('max_age', type=int) + page = request.args.get('page', 1, type=int) + limit = request.args.get('limit', 10, type=int) + + # 計算偏移量 + offset = (page - 1) * limit + + conn = get_db_connection() + if not conn: + return make_response("error", 500, "Database connection error"), 500 + + cursor = conn.cursor(dictionary=True) + + # 建立基本查詢 + query = "SELECT * FROM users" + count_query = "SELECT COUNT(*) as total FROM users" + params = [] + where_clauses = [] + + # 加入年齡過濾條件 + if min_age is not None: + where_clauses.append("age >= %s") + params.append(min_age) + + if max_age is not None: + where_clauses.append("age <= %s") + params.append(max_age) + + # 組合 WHERE 子句 + if where_clauses: + query += " WHERE " + " AND ".join(where_clauses) + count_query += " WHERE " + " AND ".join(where_clauses) + + # 加入分頁 + query += " LIMIT %s OFFSET %s" + params.extend([limit, offset]) + + # 執行查詢 + cursor.execute(query, params) + users = cursor.fetchall() + + # 取得總筆數 + cursor.execute(count_query, params[:-2] if params else []) + total = cursor.fetchone()['total'] + + # 計算總頁數 + total_pages = (total + limit - 1) // limit + + # 建立 meta 資訊 + meta = { + "total": total, + "page": page, + "limit": limit, + "total_pages": total_pages + } + + cursor.close() + conn.close() + + return make_response("success", 200, "Users retrieved successfully", {"users": users, "meta": meta}), 200 + + except Exception as e: + print(f"Error: {e}") + return make_response("error", 500, str(e)), 500 + +# 路由: GET /v1/users/ +@app.route('/v1/users/', methods=['GET']) +def get_user(user_id): + try: + conn = get_db_connection() + if not conn: + return make_response("error", 500, "Database connection error"), 500 + + cursor = conn.cursor(dictionary=True) + query = "SELECT * FROM users WHERE id = %s" + cursor.execute(query, (user_id,)) + user = cursor.fetchone() + + cursor.close() + conn.close() + + if not user: + return make_response("error", 404, f"User with id {user_id} not found"), 404 + + return make_response("success", 200, "User retrieved successfully", {"user": user}), 200 + + except Exception as e: + print(f"Error: {e}") + return make_response("error", 500, str(e)), 500 + +# 路由: POST /v1/users +@app.route('/v1/users', methods=['POST']) +def create_user(): + try: + data = request.get_json() + + # 驗證必要欄位 + if not all(key in data for key in ['name', 'email', 'age']): + return make_response("error", 400, "Missing required fields: name, email, age"), 400 + + # 驗證資料類型 + if not isinstance(data['name'], str) or not data['name'].strip(): + return make_response("error", 400, "Name must be a non-empty string"), 400 + + if not isinstance(data['email'], str) or '@' not in data['email']: + return make_response("error", 400, "Invalid email format"), 400 + + if not isinstance(data['age'], int) or data['age'] < 0: + return make_response("error", 400, "Age must be a positive integer"), 400 + + conn = get_db_connection() + if not conn: + return make_response("error", 500, "Database connection error"), 500 + + cursor = conn.cursor(dictionary=True) + + # 使用參數化查詢避免 SQL 注入 + query = "INSERT INTO users (name, email, age) VALUES (%s, %s, %s)" + cursor.execute(query, (data['name'], data['email'], data['age'])) + + # 取得新增的使用者 ID + user_id = cursor.lastrowid + conn.commit() + + # 取得新增的使用者資料 + cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) + new_user = cursor.fetchone() + + cursor.close() + conn.close() + + return make_response("success", 201, "User created successfully", {"user": new_user}), 201 + + except Exception as e: + print(f"Error: {e}") + return make_response("error", 500, str(e)), 500 + +# 路由: PATCH /v1/users/ +@app.route('/v1/users/', methods=['PATCH']) +def update_user(user_id): + try: + data = request.get_json() + + # 檢查是否有要更新的欄位 + if not any(key in data for key in ['name', 'email', 'age']): + return make_response("error", 400, "No fields to update"), 400 + + # 驗證資料類型 + if 'name' in data and (not isinstance(data['name'], str) or not data['name'].strip()): + return make_response("error", 400, "Name must be a non-empty string"), 400 + + if 'email' in data and (not isinstance(data['email'], str) or '@' not in data['email']): + return make_response("error", 400, "Invalid email format"), 400 + + if 'age' in data and (not isinstance(data['age'], int) or data['age'] < 0): + return make_response("error", 400, "Age must be a positive integer"), 400 + + conn = get_db_connection() + if not conn: + return make_response("error", 500, "Database connection error"), 500 + + cursor = conn.cursor(dictionary=True) + + # 檢查使用者是否存在 + cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) + user = cursor.fetchone() + + if not user: + cursor.close() + conn.close() + return make_response("error", 404, f"User with id {user_id} not found"), 404 + + # 建立更新查詢 + update_fields = [] + params = [] + + if 'name' in data: + update_fields.append("name = %s") + params.append(data['name']) + + if 'email' in data: + update_fields.append("email = %s") + params.append(data['email']) + + if 'age' in data: + update_fields.append("age = %s") + params.append(data['age']) + + # 加入使用者 ID 到參數列表 + params.append(user_id) + + # 執行更新查詢 + query = f"UPDATE users SET {', '.join(update_fields)} WHERE id = %s" + cursor.execute(query, params) + conn.commit() + + # 取得更新後的使用者資料 + cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) + updated_user = cursor.fetchone() + + cursor.close() + conn.close() + + return make_response("success", 200, "User updated successfully", {"user": updated_user}), 200 + + except Exception as e: + print(f"Error: {e}") + return make_response("error", 500, str(e)), 500 + +# 路由: DELETE /v1/users/ +@app.route('/v1/users/', methods=['DELETE']) +def delete_user(user_id): + try: + conn = get_db_connection() + if not conn: + return make_response("error", 500, "Database connection error"), 500 + + cursor = conn.cursor() + + # 檢查使用者是否存在 + cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) + user = cursor.fetchone() + + if not user: + cursor.close() + conn.close() + return make_response("error", 404, f"User with id {user_id} not found"), 404 + + # 刪除使用者 + cursor.execute("DELETE FROM users WHERE id = %s", (user_id,)) + conn.commit() + + cursor.close() + conn.close() + + return make_response("success", 204, "User deleted successfully"), 204 + + except Exception as e: + print(f"Error: {e}") + return make_response("error", 500, str(e)), 500 + +# 檢查檔案副檔名是否允許 +def allowed_file(filename): + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + +# 解析 Excel 檔案 +def parse_excel(file_path): + try: + df = pd.read_excel(file_path) + return df.to_dict('records') + except Exception as e: + print(f"Excel 解析錯誤: {e}") + return None + +# 解析 CSV 檔案 +def parse_csv(file_path): + try: + df = pd.read_csv(file_path) + return df.to_dict('records') + except Exception as e: + print(f"CSV 解析錯誤: {e}") + return None + +# 將使用者資料匯入資料庫 +def import_users_to_db(users_data): + try: + conn = get_db_connection() + if not conn: + return False, "資料庫連接錯誤" + + cursor = conn.cursor() + success_count = 0 + error_count = 0 + errors = [] + + for user in users_data: + try: + # 確保必要欄位存在 + if 'name' not in user or 'email' not in user or 'age' not in user: + error_count += 1 + errors.append(f"缺少必要欄位: {user}") + continue + + # 驗證資料類型 + if not isinstance(user['name'], str) or not user['name'].strip(): + error_count += 1 + errors.append(f"名稱必須是非空字串: {user}") + continue + + if not isinstance(user['email'], str) or '@' not in user['email']: + error_count += 1 + errors.append(f"無效的電子郵件格式: {user}") + continue + + try: + age = int(user['age']) + if age < 0: + error_count += 1 + errors.append(f"年齡必須是正整數: {user}") + continue + except (ValueError, TypeError): + error_count += 1 + errors.append(f"年齡必須是整數: {user}") + continue + + # 插入資料 + query = "INSERT INTO users (name, email, age) VALUES (%s, %s, %s)" + cursor.execute(query, (user['name'], user['email'], age)) + success_count += 1 + except Exception as e: + error_count += 1 + errors.append(f"插入錯誤: {str(e)}, 資料: {user}") + + conn.commit() + cursor.close() + conn.close() + + return True, { + "success_count": success_count, + "error_count": error_count, + "errors": errors[:10] # 只返回前 10 個錯誤 + } + except Exception as e: + print(f"匯入錯誤: {e}") + return False, str(e) + +# 主頁路由 +@app.route('/') +def index(): + return send_from_directory('static', 'index.html') + +# 靜態文件路由 +@app.route('/') +def static_files(path): + return send_from_directory('static', path) + +# 檔案上傳和匯入路由 +@app.route('/v1/users/import', methods=['POST']) +def import_users(): + try: + # 檢查是否有檔案 + if 'file' not in request.files: + return make_response("error", 400, "未找到檔案"), 400 + + file = request.files['file'] + + # 檢查檔案名稱 + if file.filename == '': + return make_response("error", 400, "未選擇檔案"), 400 + + # 檢查檔案類型 + if not allowed_file(file.filename): + return make_response("error", 400, f"不支援的檔案類型,僅支援 {', '.join(ALLOWED_EXTENSIONS)}"), 400 + + # 安全地保存檔案 + filename = secure_filename(file.filename) + file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + file.save(file_path) + + # 根據檔案類型解析 + file_ext = filename.rsplit('.', 1)[1].lower() + if file_ext in ['xlsx', 'xls']: + users_data = parse_excel(file_path) + elif file_ext == 'csv': + users_data = parse_csv(file_path) + else: + # 刪除檔案 + os.remove(file_path) + return make_response("error", 400, "不支援的檔案類型"), 400 + + # 檢查解析結果 + if not users_data or len(users_data) == 0: + # 刪除檔案 + os.remove(file_path) + return make_response("error", 400, "檔案解析失敗或沒有資料"), 400 + + # 匯入資料庫 + success, result = import_users_to_db(users_data) + + # 刪除檔案 + os.remove(file_path) + + if not success: + return make_response("error", 500, f"匯入失敗: {result}"), 500 + + return make_response("success", 200, "使用者資料匯入成功", result), 200 + + except Exception as e: + print(f"Error: {e}") + return make_response("error", 500, str(e)), 500 + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/create_users_table.sql b/create_users_table.sql new file mode 100644 index 0000000..a321db0 --- /dev/null +++ b/create_users_table.sql @@ -0,0 +1,17 @@ +-- 建立 users 資料表 +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(100) NOT NULL, + age INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- 插入一些測試資料 +INSERT INTO users (name, email, age) VALUES +('張三', 'zhang@example.com', 25), +('李四', 'li@example.com', 30), +('王五', 'wang@example.com', 22), +('趙六', 'zhao@example.com', 35), +('孫七', 'sun@example.com', 28); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..75f80f6 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "ai_20250908", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "" +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d923f13 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask==3.1.2 +mysql-connector-python==9.4.0 +flask-cors==6.0.1 \ No newline at end of file diff --git a/sample_users.csv b/sample_users.csv new file mode 100644 index 0000000..2df0505 --- /dev/null +++ b/sample_users.csv @@ -0,0 +1,6 @@ +name,email,age +張三,zhangsan@example.com,25 +李四,lisi@example.com,30 +王五,wangwu@example.com,35 +趙六,zhaoliu@example.com,40 +錢七,qianqi@example.com,45 \ No newline at end of file diff --git a/static/add_user.html b/static/add_user.html new file mode 100644 index 0000000..c967881 --- /dev/null +++ b/static/add_user.html @@ -0,0 +1,65 @@ + + + + + + 新增用戶 - 用戶管理系統 + + + +
+

用戶管理系統 - 新增用戶

+ + + + +
+

新增用戶資料

+
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+
+ + +
+

批量匯入用戶

+
+
+ + +
+
+ +
+
+
+

支援的檔案格式: CSV, Excel (.xlsx, .xls)

+

檔案必須包含以下欄位: name, email, age

+
+
+ + +
+
+ + + + \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..b1707c6 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,298 @@ +/* 全局樣式 */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Microsoft JhengHei', Arial, sans-serif; + line-height: 1.6; + color: #333; + background-color: #f5f5f5; + padding: 20px; +} + +.container { + max-width: 1200px; + margin: 0 auto; + background-color: #fff; + padding: 20px; + border-radius: 8px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +h1 { + text-align: center; + margin-bottom: 20px; + color: #2c3e50; +} + +h2 { + color: #2c3e50; + margin-bottom: 15px; +} + +/* 過濾區域 */ +.filter-container { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 20px; + padding: 15px; + background-color: #f8f9fa; + border-radius: 5px; +} + +.filter-group { + display: flex; + align-items: center; + gap: 5px; +} + +.filter-group label { + font-weight: bold; +} + +.filter-group input { + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + width: 80px; +} + +button { + padding: 8px 15px; + background-color: #3498db; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s; +} + +button:hover { + background-color: #2980b9; +} + +button:disabled { + background-color: #95a5a6; + cursor: not-allowed; +} + +#reset-filter-btn { + background-color: #95a5a6; +} + +#reset-filter-btn:hover { + background-color: #7f8c8d; +} + +/* 批量匯入樣式 */ +.import-container { + margin-bottom: 30px; + padding: 20px; + background-color: #f8f9fa; + border-radius: 5px; + border: 1px solid #e9ecef; +} + +.import-info { + margin-top: 15px; + font-size: 0.9em; + color: #6c757d; +} + +.import-info p { + margin-bottom: 5px; +} + +#import-file { + padding: 10px; + border: 1px dashed #ced4da; + border-radius: 4px; + width: 100%; + background-color: #fff; +} + +/* 表格樣式 */ +.table-container { + overflow-x: auto; + margin-bottom: 20px; +} + +table { + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; +} + +table th, table td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid #ddd; +} + +table th { + background-color: #3498db; + color: white; +} + +table tr:nth-child(even) { + background-color: #f2f2f2; +} + +table tr:hover { + background-color: #e9f7fe; +} + +.action-btn { + margin-right: 5px; + padding: 5px 10px; + font-size: 0.9em; +} + +.edit-btn { + background-color: #f39c12; +} + +.edit-btn:hover { + background-color: #d35400; +} + +.delete-btn { + background-color: #e74c3c; +} + +.delete-btn:hover { + background-color: #c0392b; +} + +/* 分頁控制 */ +.pagination-container { + display: flex; + justify-content: center; + align-items: center; + gap: 15px; + margin-bottom: 20px; +} + +#page-info { + font-size: 0.9em; +} + +.page-size { + margin-left: 20px; + display: flex; + align-items: center; + gap: 5px; +} + +.page-size select { + padding: 5px; + border: 1px solid #ddd; + border-radius: 4px; +} + +/* 表單樣式 */ +.form-container { + background-color: #f8f9fa; + padding: 20px; + border-radius: 5px; + margin-bottom: 20px; +} + +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} + +.form-group input { + width: 100%; + padding: 10px; + border: 1px solid #ddd; + border-radius: 4px; +} + +.form-actions { + display: flex; + gap: 10px; + margin-top: 20px; +} + +#cancel-btn { + background-color: #95a5a6; +} + +#cancel-btn:hover { + background-color: #7f8c8d; +} + +/* 訊息提示 */ +.message { + padding: 10px; + margin-bottom: 20px; + border-radius: 4px; + text-align: center; + display: none; +} + +.success { + background-color: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + display: block; +} + +.error { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + display: block; +} + +/* 導航鏈接 */ +.nav-links { + margin-bottom: 20px; + padding: 10px 0; + border-bottom: 1px solid #eee; +} + +.nav-link { + display: inline-block; + padding: 8px 15px; + background-color: #3498db; + color: white; + text-decoration: none; + border-radius: 4px; + transition: background-color 0.3s; +} + +.nav-link:hover { + background-color: #2980b9; +} + +/* 響應式設計 */ +@media (max-width: 768px) { + .filter-container { + flex-direction: column; + align-items: flex-start; + } + + .pagination-container { + flex-wrap: wrap; + } + + .form-actions { + flex-direction: column; + } + + button { + width: 100%; + } +} \ No newline at end of file diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..dfdd83c --- /dev/null +++ b/static/index.html @@ -0,0 +1,113 @@ + + + + + + 用戶管理系統 + + + +
+

用戶管理系統

+ + + + +
+
+ + +
+
+ + +
+ + +
+ + +
+ + + + + + + + + + + + + +
ID姓名電子郵件年齡操作
+
+ + +
+ + 1 頁,共 1 + +
+ + +
+
+ + +
+

批量匯入用戶

+
+
+ + +
+
+ +
+
+
+

支援的檔案格式: CSV, Excel (.xlsx, .xls)

+

檔案必須包含以下欄位: name, email, age

+
+
+ + + + + +
+
+ + + + \ No newline at end of file diff --git a/static/js/add_user.js b/static/js/add_user.js new file mode 100644 index 0000000..0137f80 --- /dev/null +++ b/static/js/add_user.js @@ -0,0 +1,141 @@ +// API 基礎 URL +const API_BASE_URL = '/v1/users'; + +// DOM 元素 +const addUserForm = document.getElementById('add-user-form'); +const nameInput = document.getElementById('name'); +const emailInput = document.getElementById('email'); +const ageInput = document.getElementById('age'); +const saveBtn = document.getElementById('save-btn'); +const resetBtn = document.getElementById('reset-btn'); +const cancelBtn = document.getElementById('cancel-btn'); +const messageDiv = document.getElementById('message'); +const importForm = document.getElementById('import-form'); +const importFileInput = document.getElementById('import-file'); + +// 頁面載入時設置事件監聽器 +document.addEventListener('DOMContentLoaded', () => { + setupEventListeners(); +}); + +// 設置事件監聽器 +function setupEventListeners() { + // 表單提交事件 + addUserForm.addEventListener('submit', handleFormSubmit); + + // 重置按鈕事件 + resetBtn.addEventListener('click', resetForm); + + // 取消按鈕事件 + cancelBtn.addEventListener('click', () => { + window.location.href = 'index.html'; + }); + + // 檔案匯入表單提交事件 + importForm.addEventListener('submit', handleImportSubmit); +} + +// 處理表單提交 +async function handleFormSubmit(event) { + event.preventDefault(); + + const userData = { + name: nameInput.value.trim(), + email: emailInput.value.trim(), + age: parseInt(ageInput.value) + }; + + try { + const response = await fetch(API_BASE_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(userData) + }); + + const result = await response.json(); + + if (result.status === 'success') { + showMessage('用戶創建成功', 'success'); + resetForm(); + + // 延遲後跳轉回用戶列表頁面 + setTimeout(() => { + window.location.href = 'index.html'; + }, 2000); + } else { + showMessage(result.message, 'error'); + } + } catch (error) { + console.error('保存用戶失敗:', error); + showMessage('保存用戶失敗', 'error'); + } +} + +// 重置表單 +function resetForm() { + addUserForm.reset(); +} + +// 顯示訊息 +function showMessage(message, type) { + messageDiv.textContent = message; + messageDiv.className = `message ${type}`; + + // 5 秒後自動隱藏訊息 + setTimeout(() => { + messageDiv.className = 'message'; + }, 5000); +} + +// 處理檔案匯入 +async function handleImportSubmit(event) { + event.preventDefault(); + + if (!importFileInput.files || importFileInput.files.length === 0) { + showMessage('請選擇檔案', 'error'); + return; + } + + const file = importFileInput.files[0]; + const formData = new FormData(); + formData.append('file', file); + + try { + showMessage('正在匯入資料,請稍候...'); + + const response = await fetch(`${API_BASE_URL}/import`, { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + if (result.status === 'success') { + // 顯示匯入結果 + const successCount = result.data.success_count; + const errorCount = result.data.error_count; + let message = `匯入成功: ${successCount} 筆資料`; + + if (errorCount > 0) { + message += `, 失敗: ${errorCount} 筆資料`; + } + + showMessage(message, 'success'); + + // 重置表單 + importForm.reset(); + + // 延遲後跳轉回用戶列表頁面 + setTimeout(() => { + window.location.href = 'index.html'; + }, 3000); + } else { + showMessage(`匯入失敗: ${result.message}`, 'error'); + } + } catch (error) { + console.error('匯入錯誤:', error); + showMessage('匯入過程中發生錯誤,請稍後再試', 'error'); + } +} \ No newline at end of file diff --git a/static/js/script.js b/static/js/script.js new file mode 100644 index 0000000..af948bf --- /dev/null +++ b/static/js/script.js @@ -0,0 +1,337 @@ +// 全局變數 +let currentPage = 1; +let totalPages = 1; +let limit = 10; +let minAge = null; +let maxAge = null; +let editMode = false; +let users = []; + +// API 基礎 URL +const API_BASE_URL = '/v1/users'; + +// DOM 元素 +const usersList = document.getElementById('users-list'); +const userForm = document.getElementById('user-form'); +const formTitle = document.getElementById('form-title'); +const userIdInput = document.getElementById('user-id'); +const nameInput = document.getElementById('name'); +const emailInput = document.getElementById('email'); +const ageInput = document.getElementById('age'); +const saveBtn = document.getElementById('save-btn'); +const cancelBtn = document.getElementById('cancel-btn'); +const minAgeInput = document.getElementById('min-age'); +const maxAgeInput = document.getElementById('max-age'); +const filterBtn = document.getElementById('filter-btn'); +const resetFilterBtn = document.getElementById('reset-filter-btn'); +const prevPageBtn = document.getElementById('prev-page'); +const nextPageBtn = document.getElementById('next-page'); +const currentPageSpan = document.getElementById('current-page'); +const totalPagesSpan = document.getElementById('total-pages'); +const pageLimitSelect = document.getElementById('page-limit'); +const messageDiv = document.getElementById('message'); +const importForm = document.getElementById('import-form'); +const importFileInput = document.getElementById('import-file'); + +// 頁面載入時獲取用戶列表 +document.addEventListener('DOMContentLoaded', () => { + fetchUsers(); + setupEventListeners(); +}); + +// 設置事件監聽器 +function setupEventListeners() { + // 表單提交事件 + userForm.addEventListener('submit', handleFormSubmit); + + // 取消按鈕事件 + cancelBtn.addEventListener('click', resetForm); + + // 過濾按鈕事件 + filterBtn.addEventListener('click', () => { + minAge = minAgeInput.value ? parseInt(minAgeInput.value) : null; + maxAge = maxAgeInput.value ? parseInt(maxAgeInput.value) : null; + currentPage = 1; + fetchUsers(); + }); + + // 重置過濾按鈕事件 + resetFilterBtn.addEventListener('click', () => { + minAgeInput.value = ''; + maxAgeInput.value = ''; + minAge = null; + maxAge = null; + currentPage = 1; + fetchUsers(); + }); + + // 分頁按鈕事件 + prevPageBtn.addEventListener('click', () => { + if (currentPage > 1) { + currentPage--; + fetchUsers(); + } + }); + + nextPageBtn.addEventListener('click', () => { + if (currentPage < totalPages) { + currentPage++; + fetchUsers(); + } + }); + + // 每頁顯示數量變更事件 + pageLimitSelect.addEventListener('change', () => { + limit = parseInt(pageLimitSelect.value); + currentPage = 1; + fetchUsers(); + }); + + // 檔案匯入表單提交事件 + importForm.addEventListener('submit', handleImportSubmit); +} + +// 獲取用戶列表 +async function fetchUsers() { + try { + // 構建 URL 查詢參數 + const params = new URLSearchParams(); + params.append('page', currentPage); + params.append('limit', limit); + + if (minAge !== null) params.append('min_age', minAge); + if (maxAge !== null) params.append('max_age', maxAge); + + const response = await fetch(`${API_BASE_URL}?${params.toString()}`); + const result = await response.json(); + + if (result.status === 'success') { + users = result.data.users; + renderUsers(users); + + // 更新分頁信息 + const meta = result.data.meta; + totalPages = meta.total_pages; + currentPageSpan.textContent = meta.page; + totalPagesSpan.textContent = totalPages; + + // 更新分頁按鈕狀態 + prevPageBtn.disabled = meta.page <= 1; + nextPageBtn.disabled = meta.page >= totalPages; + } else { + showMessage(result.message, 'error'); + } + } catch (error) { + console.error('獲取用戶列表失敗:', error); + showMessage('獲取用戶列表失敗', 'error'); + } +} + +// 渲染用戶列表 +function renderUsers(users) { + usersList.innerHTML = ''; + + if (users.length === 0) { + const emptyRow = document.createElement('tr'); + emptyRow.innerHTML = `沒有找到用戶`; + usersList.appendChild(emptyRow); + return; + } + + users.forEach(user => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${user.id} + ${user.name} + ${user.email} + ${user.age} + + + + + `; + usersList.appendChild(row); + }); + + // 添加編輯和刪除按鈕的事件監聽器 + document.querySelectorAll('.edit-btn').forEach(btn => { + btn.addEventListener('click', () => editUser(parseInt(btn.dataset.id))); + }); + + document.querySelectorAll('.delete-btn').forEach(btn => { + btn.addEventListener('click', () => deleteUser(parseInt(btn.dataset.id))); + }); +} + +// 處理表單提交 +async function handleFormSubmit(event) { + event.preventDefault(); + + // 如果不是編輯模式,重定向到新增用戶頁面 + if (!editMode) { + window.location.href = 'add_user.html'; + return; + } + + const userData = { + name: nameInput.value.trim(), + email: emailInput.value.trim(), + age: parseInt(ageInput.value) + }; + + try { + // 更新用戶 + const userId = parseInt(userIdInput.value); + const url = `${API_BASE_URL}/${userId}`; + const method = 'PATCH'; + + const response = await fetch(url, { + method: method, + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(userData) + }); + + const result = await response.json(); + + if (result.status === 'success') { + showMessage(editMode ? '用戶更新成功' : '用戶創建成功', 'success'); + resetForm(); + fetchUsers(); + } else { + showMessage(result.message, 'error'); + } + } catch (error) { + console.error('保存用戶失敗:', error); + showMessage('保存用戶失敗', 'error'); + } +} + +// 編輯用戶 +async function editUser(userId) { + try { + const response = await fetch(`${API_BASE_URL}/${userId}`); + const result = await response.json(); + + if (result.status === 'success') { + const user = result.data.user; + + // 填充表單 + userIdInput.value = user.id; + nameInput.value = user.name; + emailInput.value = user.email; + ageInput.value = user.age; + + // 切換到編輯模式 + editMode = true; + + // 顯示編輯表單 + const editFormContainer = document.getElementById('edit-form-container'); + editFormContainer.style.display = 'block'; + + // 滾動到表單 + editFormContainer.scrollIntoView({ behavior: 'smooth' }); + } else { + showMessage(result.message, 'error'); + } + } catch (error) { + console.error('獲取用戶詳情失敗:', error); + showMessage('獲取用戶詳情失敗', 'error'); + } +} + +// 刪除用戶 +async function deleteUser(userId) { + if (!confirm(`確定要刪除 ID 為 ${userId} 的用戶嗎?`)) { + return; + } + + try { + const response = await fetch(`${API_BASE_URL}/${userId}`, { + method: 'DELETE' + }); + + if (response.status === 204) { + showMessage('用戶刪除成功', 'success'); + fetchUsers(); + } else { + const result = await response.json(); + showMessage(result.message, 'error'); + } + } catch (error) { + console.error('刪除用戶失敗:', error); + showMessage('刪除用戶失敗', 'error'); + } +} + +// 重置表單 +function resetForm() { + userForm.reset(); + userIdInput.value = ''; + editMode = false; + + // 隱藏編輯表單 + const editFormContainer = document.getElementById('edit-form-container'); + editFormContainer.style.display = 'none'; +} + +// 顯示訊息 +function showMessage(message, type) { + messageDiv.textContent = message; + messageDiv.className = `message ${type}`; + + // 5 秒後自動隱藏訊息 + setTimeout(() => { + messageDiv.className = 'message'; + }, 5000); +} + +// 處理檔案匯入 +async function handleImportSubmit(event) { + event.preventDefault(); + + if (!importFileInput.files || importFileInput.files.length === 0) { + showMessage('請選擇檔案', true); + return; + } + + const file = importFileInput.files[0]; + const formData = new FormData(); + formData.append('file', file); + + try { + showMessage('正在匯入資料,請稍候...'); + + const response = await fetch(`${API_BASE_URL}/import`, { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + if (result.status === 'success') { + // 顯示匯入結果 + const successCount = result.data.success_count; + const errorCount = result.data.error_count; + let message = `匯入成功: ${successCount} 筆資料`; + + if (errorCount > 0) { + message += `, 失敗: ${errorCount} 筆資料`; + } + + showMessage(message); + + // 重新載入用戶列表 + fetchUsers(); + + // 重置表單 + importForm.reset(); + } else { + showMessage(`匯入失敗: ${result.message}`, true); + } + } catch (error) { + console.error('匯入錯誤:', error); + showMessage('匯入過程中發生錯誤,請稍後再試', true); + } +} \ No newline at end of file