Initial commit
This commit is contained in:
75
README.md
Normal file
75
README.md
Normal file
@@ -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/<id>`:取得特定使用者
|
||||
- `POST /v1/users`:建立新使用者
|
||||
- `PATCH /v1/users/<id>`:更新使用者資料
|
||||
- `DELETE /v1/users/<id>`:刪除使用者
|
||||
|
||||
## 安裝與執行
|
||||
|
||||
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
|
||||
```
|
464
app.py
Normal file
464
app.py
Normal file
@@ -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/<id>
|
||||
@app.route('/v1/users/<int:user_id>', 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/<id>
|
||||
@app.route('/v1/users/<int:user_id>', 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/<id>
|
||||
@app.route('/v1/users/<int:user_id>', 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('/<path:path>')
|
||||
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)
|
17
create_users_table.sql
Normal file
17
create_users_table.sql
Normal file
@@ -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);
|
12
package.json
Normal file
12
package.json
Normal file
@@ -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": ""
|
||||
}
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Flask==3.1.2
|
||||
mysql-connector-python==9.4.0
|
||||
flask-cors==6.0.1
|
6
sample_users.csv
Normal file
6
sample_users.csv
Normal file
@@ -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
|
|
65
static/add_user.html
Normal file
65
static/add_user.html
Normal file
@@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-TW">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>新增用戶 - 用戶管理系統</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>用戶管理系統 - 新增用戶</h1>
|
||||
|
||||
<div class="nav-links">
|
||||
<a href="index.html" class="nav-link">返回用戶列表</a>
|
||||
</div>
|
||||
|
||||
<!-- 用戶表單 -->
|
||||
<div class="form-container">
|
||||
<h2>新增用戶資料</h2>
|
||||
<form id="add-user-form">
|
||||
<div class="form-group">
|
||||
<label for="name">姓名:</label>
|
||||
<input type="text" id="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">電子郵件:</label>
|
||||
<input type="email" id="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="age">年齡:</label>
|
||||
<input type="number" id="age" min="0" required>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" id="save-btn">儲存</button>
|
||||
<button type="button" id="reset-btn">重置</button>
|
||||
<button type="button" id="cancel-btn">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 批量匯入 -->
|
||||
<div class="import-container">
|
||||
<h2>批量匯入用戶</h2>
|
||||
<form id="import-form" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="import-file">選擇檔案 (Excel 或 CSV):</label>
|
||||
<input type="file" id="import-file" name="file" accept=".csv, .xlsx, .xls" required>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" id="import-btn">匯入</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="import-info">
|
||||
<p>支援的檔案格式: CSV, Excel (.xlsx, .xls)</p>
|
||||
<p>檔案必須包含以下欄位: name, email, age</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 訊息提示 -->
|
||||
<div id="message" class="message"></div>
|
||||
</div>
|
||||
|
||||
<script src="js/add_user.js"></script>
|
||||
</body>
|
||||
</html>
|
298
static/css/style.css
Normal file
298
static/css/style.css
Normal file
@@ -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%;
|
||||
}
|
||||
}
|
113
static/index.html
Normal file
113
static/index.html
Normal file
@@ -0,0 +1,113 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-TW">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用戶管理系統</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>用戶管理系統</h1>
|
||||
|
||||
<div class="nav-links">
|
||||
<a href="add_user.html" class="nav-link">新增用戶</a>
|
||||
</div>
|
||||
|
||||
<!-- 過濾和分頁控制 -->
|
||||
<div class="filter-container">
|
||||
<div class="filter-group">
|
||||
<label for="min-age">最小年齡:</label>
|
||||
<input type="number" id="min-age" min="0" max="150">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="max-age">最大年齡:</label>
|
||||
<input type="number" id="max-age" min="0" max="150">
|
||||
</div>
|
||||
<button id="filter-btn">過濾</button>
|
||||
<button id="reset-filter-btn">重置</button>
|
||||
</div>
|
||||
|
||||
<!-- 用戶列表 -->
|
||||
<div class="table-container">
|
||||
<table id="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>姓名</th>
|
||||
<th>電子郵件</th>
|
||||
<th>年齡</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="users-list">
|
||||
<!-- 用戶資料將由 JavaScript 動態填充 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分頁控制 -->
|
||||
<div class="pagination-container">
|
||||
<button id="prev-page" disabled>上一頁</button>
|
||||
<span id="page-info">第 <span id="current-page">1</span> 頁,共 <span id="total-pages">1</span> 頁</span>
|
||||
<button id="next-page" disabled>下一頁</button>
|
||||
<div class="page-size">
|
||||
<label for="page-limit">每頁顯示:</label>
|
||||
<select id="page-limit">
|
||||
<option value="5">5</option>
|
||||
<option value="10" selected>10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量匯入 -->
|
||||
<div class="import-container">
|
||||
<h2>批量匯入用戶</h2>
|
||||
<form id="import-form" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="import-file">選擇檔案 (Excel 或 CSV):</label>
|
||||
<input type="file" id="import-file" name="file" accept=".csv, .xlsx, .xls" required>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" id="import-btn">匯入</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="import-info">
|
||||
<p>支援的檔案格式: CSV, Excel (.xlsx, .xls)</p>
|
||||
<p>檔案必須包含以下欄位: name, email, age</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 編輯用戶表單 (僅在編輯模式顯示) -->
|
||||
<div class="form-container" id="edit-form-container" style="display: none;">
|
||||
<h2 id="form-title">編輯用戶</h2>
|
||||
<form id="user-form">
|
||||
<input type="hidden" id="user-id">
|
||||
<div class="form-group">
|
||||
<label for="name">姓名:</label>
|
||||
<input type="text" id="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">電子郵件:</label>
|
||||
<input type="email" id="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="age">年齡:</label>
|
||||
<input type="number" id="age" min="0" required>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" id="save-btn">更新</button>
|
||||
<button type="button" id="cancel-btn">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 訊息提示 -->
|
||||
<div id="message" class="message"></div>
|
||||
</div>
|
||||
|
||||
<script src="js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
141
static/js/add_user.js
Normal file
141
static/js/add_user.js
Normal file
@@ -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');
|
||||
}
|
||||
}
|
337
static/js/script.js
Normal file
337
static/js/script.js
Normal file
@@ -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 = `<td colspan="5" style="text-align: center;">沒有找到用戶</td>`;
|
||||
usersList.appendChild(emptyRow);
|
||||
return;
|
||||
}
|
||||
|
||||
users.forEach(user => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${user.id}</td>
|
||||
<td>${user.name}</td>
|
||||
<td>${user.email}</td>
|
||||
<td>${user.age}</td>
|
||||
<td>
|
||||
<button class="action-btn edit-btn" data-id="${user.id}">編輯</button>
|
||||
<button class="action-btn delete-btn" data-id="${user.id}">刪除</button>
|
||||
</td>
|
||||
`;
|
||||
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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user