Initial commit

This commit is contained in:
2025-09-08 17:06:58 +08:00
commit b135a7aa20
11 changed files with 1531 additions and 0 deletions

65
static/add_user.html Normal file
View 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
View 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
View 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
View 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
View 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);
}
}