devicetypeAPIGET1
This commit is contained in:
@@ -1,13 +1,45 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NDMS - Quản lý thiết bị mạng</title>
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
<body class="bg-gray-100">
|
||||
|
||||
<!-- THANH MENU -->
|
||||
<nav class="bg-white shadow-md px-6 py-4">
|
||||
<div class="flex gap-3 flex-wrap">
|
||||
<button data-page="dashboard"
|
||||
class="menu-btn bg-indigo-600 text-white px-5 py-2 rounded-lg font-medium">
|
||||
🏠 Trang chủ
|
||||
</button>
|
||||
<button data-page="device-type"
|
||||
class="menu-btn bg-gray-200 text-gray-700 px-5 py-2 rounded-lg font-medium">
|
||||
📦 Loại thiết bị
|
||||
</button>
|
||||
<button data-page="device"
|
||||
class="menu-btn bg-gray-200 text-gray-700 px-5 py-2 rounded-lg font-medium">
|
||||
🖥️ Thiết bị
|
||||
</button>
|
||||
<button data-page="alert"
|
||||
class="menu-btn bg-gray-200 text-gray-700 px-5 py-2 rounded-lg font-medium">
|
||||
🔔 Cảnh báo
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- NỘI DUNG CHÍNH -->
|
||||
<div id="app" class="p-6">
|
||||
<div class="text-center text-gray-500">Đang tải...</div>
|
||||
</div>
|
||||
|
||||
<!-- QUAN TRỌNG: Đường dẫn tới main.js phải đúng -->
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
979
frontend/package-lock.json
generated
979
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,8 @@
|
||||
"axios": "^1.16.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-router-dom": "^7.14.2"
|
||||
"react-router-dom": "^7.14.2",
|
||||
"tailwindcss": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
@@ -25,6 +26,7 @@
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.5.0",
|
||||
"vite": "^8.0.9"
|
||||
"vite": "^8.0.12",
|
||||
"vite-plugin-html": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
114
frontend/src/features/dashboard/dashboard.js
Normal file
114
frontend/src/features/dashboard/dashboard.js
Normal file
@@ -0,0 +1,114 @@
|
||||
// ============================================
|
||||
// FILE NÀY LÀ TRANG CHỦ (DASHBOARD)
|
||||
// Nhiệm vụ: Tạo ra giao diện và dữ liệu cho trang chủ
|
||||
// ============================================
|
||||
|
||||
// Hàm này được gọi từ main.js
|
||||
export async function showDashboard(container) {
|
||||
// 1. TẠO GIAO DIỆN HTML
|
||||
container.innerHTML = `
|
||||
<div class="space-y-6">
|
||||
<!-- Tiêu đề chào mừng -->
|
||||
<div class="bg-gradient-to-r from-indigo-600 to-purple-600 rounded-2xl p-6 text-white">
|
||||
<h1 class="text-2xl font-bold mb-2">Chào mừng đến với NDMS</h1>
|
||||
<p>Hệ thống quản lý thiết bị mạng tập trung</p>
|
||||
</div>
|
||||
|
||||
<!-- 4 Ô THỐNG KÊ -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<!-- Ô 1: Tổng thiết bị -->
|
||||
<div class="bg-white rounded-xl p-5 shadow-md">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">Tổng thiết bị</p>
|
||||
<p class="text-3xl font-bold text-indigo-600" id="stat-total">0</p>
|
||||
</div>
|
||||
<i class="fas fa-server text-3xl text-indigo-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ô 2: Đang hoạt động -->
|
||||
<div class="bg-white rounded-xl p-5 shadow-md">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">Đang hoạt động</p>
|
||||
<p class="text-3xl font-bold text-green-600" id="stat-up">0</p>
|
||||
</div>
|
||||
<i class="fas fa-check-circle text-3xl text-green-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ô 3: Đang tắt -->
|
||||
<div class="bg-white rounded-xl p-5 shadow-md">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">Đang tắt</p>
|
||||
<p class="text-3xl font-bold text-red-600" id="stat-down">0</p>
|
||||
</div>
|
||||
<i class="fas fa-power-off text-3xl text-red-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ô 4: Cảnh báo -->
|
||||
<div class="bg-white rounded-xl p-5 shadow-md">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">Cảnh báo</p>
|
||||
<p class="text-3xl font-bold text-yellow-600" id="stat-alert">0</p>
|
||||
</div>
|
||||
<i class="fas fa-bell text-3xl text-yellow-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DANH SÁCH HOẠT ĐỘNG GẦN ĐÂY -->
|
||||
<div class="bg-white rounded-xl shadow-md p-5">
|
||||
<h2 class="font-bold text-lg mb-4">
|
||||
<i class="fas fa-clock text-gray-500 mr-2"></i>Hoạt động gần đây
|
||||
</h2>
|
||||
<div id="activity-list" class="space-y-2">
|
||||
<!-- Dữ liệu sẽ được chèn vào đây -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 2. ĐỔ DỮ LIỆU VÀO CÁC Ô THỐNG KÊ
|
||||
// (Đây là dữ liệu mẫu - sau này sẽ gọi API từ backend)
|
||||
document.getElementById('stat-total').innerText = '24';
|
||||
document.getElementById('stat-up').innerText = '18';
|
||||
document.getElementById('stat-down').innerText = '6';
|
||||
document.getElementById('stat-alert').innerText = '3';
|
||||
|
||||
// 3. ĐỔ DỮ LIỆU VÀO DANH SÁCH HOẠT ĐỘNG
|
||||
const activities = [
|
||||
{ time: '10:30', action: 'Switch Tầng 1', status: 'down', message: 'Mất kết nối' },
|
||||
{ time: '09:15', action: 'Router Chính', status: 'up', message: 'Đã kết nối lại' },
|
||||
{ time: '08:00', action: 'Server DB', status: 'warning', message: 'CPU quá tải' },
|
||||
{ time: 'Hôm qua', action: 'Cập nhật hệ thống', status: 'info', message: 'Đã cập nhật cấu hình' },
|
||||
];
|
||||
|
||||
// Tạo HTML cho từng hoạt động
|
||||
let activityHtml = '';
|
||||
for (let act of activities) {
|
||||
let iconClass = '';
|
||||
if (act.status === 'down') iconClass = 'text-red-500 fa-exclamation-circle';
|
||||
else if (act.status === 'up') iconClass = 'text-green-500 fa-check-circle';
|
||||
else if (act.status === 'warning') iconClass = 'text-yellow-500 fa-exclamation-triangle';
|
||||
else iconClass = 'text-blue-500 fa-info-circle';
|
||||
|
||||
activityHtml += `
|
||||
<div class="flex items-center gap-3 p-3 border-b hover:bg-gray-50">
|
||||
<i class="fas ${iconClass} w-5"></i>
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">${act.action}</span>
|
||||
<span class="text-xs text-gray-400">${act.time}</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500">${act.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
document.getElementById('activity-list').innerHTML = activityHtml;
|
||||
}
|
||||
52
frontend/src/features/device-type/device-type-api.js
Normal file
52
frontend/src/features/device-type/device-type-api.js
Normal file
@@ -0,0 +1,52 @@
|
||||
// ============================================
|
||||
// API: KẾT NỐI VỚI BACK-END
|
||||
// ============================================
|
||||
|
||||
// Địa chỉ Back-end (chạy cổng 5000)
|
||||
const API_BASE_URL = 'http://localhost:5000/api';
|
||||
|
||||
// Hàm gọi API chung
|
||||
async function callAPI(endpoint, method = 'GET', body = null) {
|
||||
const options = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
};
|
||||
|
||||
if (body && (method === 'POST' || method === 'PUT')) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, options);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || 'Có lỗi xảy ra');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Các hàm API cho Device Type
|
||||
export const deviceTypeAPI = {
|
||||
// Lấy danh sách tất cả loại thiết bị
|
||||
getAll: () => callAPI('/device-types'),
|
||||
|
||||
// Lấy chi tiết 1 loại thiết bị
|
||||
getById: (id) => callAPI(`/device-types/${id}`),
|
||||
|
||||
// Thêm mới loại thiết bị
|
||||
create: (data) => callAPI('/device-types', 'POST', data),
|
||||
|
||||
// Cập nhật loại thiết bị
|
||||
update: (id, data) => callAPI(`/device-types/${id}`, 'PUT', data),
|
||||
|
||||
// Xóa loại thiết bị
|
||||
delete: (id) => callAPI(`/device-types/${id}`, 'DELETE'),
|
||||
};
|
||||
474
frontend/src/features/device-type/device-type.js
Normal file
474
frontend/src/features/device-type/device-type.js
Normal file
@@ -0,0 +1,474 @@
|
||||
// // ============================================
|
||||
// // MODULE: QUẢN LÝ LOẠI THIẾT BỊ (DEVICE TYPE)
|
||||
// // ============================================
|
||||
// // Nhiệm vụ: Thêm, sửa, xóa, hiển thị danh sách loại thiết bị
|
||||
// // ============================================
|
||||
|
||||
// // DỮ LIỆU MẪU (lưu trong bộ nhớ tạm)
|
||||
// let deviceTypes = [
|
||||
// { id: '1', name: 'Switch', description: 'Thiết bị chuyển mạch mạng', color: '#10B981', isActive: true },
|
||||
// { id: '2', name: 'Router', description: 'Bộ định tuyến mạng', color: '#3B82F6', isActive: true },
|
||||
// { id: '3', name: 'WorkStation', description: 'Máy trạm làm việc', color: '#8B5CF6', isActive: true },
|
||||
// { id: '4', name: 'Server', description: 'Máy chủ', color: '#EF4444', isActive: true },
|
||||
// ];
|
||||
|
||||
// // HÀM HIỂN THỊ TRANG DEVICE TYPE
|
||||
// export async function showDeviceTypePage(container) {
|
||||
// // Tạo giao diện HTML
|
||||
// container.innerHTML = `
|
||||
// <div class="space-y-6">
|
||||
// <!-- Tiêu đề và nút thêm -->
|
||||
// <div class="flex justify-between items-center">
|
||||
// <div>
|
||||
// <h1 class="text-2xl font-bold text-gray-800">
|
||||
// <i class="fas fa-tags text-indigo-500 mr-2"></i>Quản lý loại thiết bị
|
||||
// </h1>
|
||||
// <p class="text-gray-500 text-sm mt-1">Danh mục các loại thiết bị mạng trong hệ thống</p>
|
||||
// </div>
|
||||
// <button id="addDeviceTypeBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg shadow transition flex items-center gap-2">
|
||||
// <i class="fas fa-plus"></i>
|
||||
// <span>Thêm loại thiết bị</span>
|
||||
// </button>
|
||||
// </div>
|
||||
|
||||
// <!-- 2 ô thống kê nhanh -->
|
||||
// <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
// <div class="bg-blue-50 rounded-xl p-4 border border-blue-100">
|
||||
// <div class="flex justify-between items-center">
|
||||
// <div>
|
||||
// <p class="text-blue-600 text-sm font-medium">Tổng số loại</p>
|
||||
// <p class="text-2xl font-bold text-blue-700" id="totalCount">0</p>
|
||||
// </div>
|
||||
// <i class="fas fa-layer-group text-3xl text-blue-400"></i>
|
||||
// </div>
|
||||
// </div>
|
||||
// <div class="bg-green-50 rounded-xl p-4 border border-green-100">
|
||||
// <div class="flex justify-between items-center">
|
||||
// <div>
|
||||
// <p class="text-green-600 text-sm font-medium">Đang hoạt động</p>
|
||||
// <p class="text-2xl font-bold text-green-700" id="activeCount">0</p>
|
||||
// </div>
|
||||
// <i class="fas fa-check-circle text-3xl text-green-400"></i>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// <!-- Ô tìm kiếm -->
|
||||
// <div class="relative">
|
||||
// <i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
|
||||
// <input type="text" id="searchInput" placeholder="Tìm kiếm theo tên hoặc mô tả..." class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
||||
// </div>
|
||||
|
||||
// <!-- Danh sách loại thiết bị -->
|
||||
// <div id="deviceTypeList" class="space-y-3">
|
||||
// <!-- Nội dung sẽ được render bằng JavaScript -->
|
||||
// <div class="text-center py-8 text-gray-400">Đang tải...</div>
|
||||
// </div>
|
||||
// </div>
|
||||
// `;
|
||||
|
||||
// // HIỂN THỊ DANH SÁCH
|
||||
// renderList();
|
||||
|
||||
// // CẬP NHẬT THỐNG KÊ
|
||||
// updateStats();
|
||||
|
||||
// // GẮN SỰ KIỆN CHO NÚT THÊM
|
||||
// document.getElementById('addDeviceTypeBtn').onclick = showAddForm;
|
||||
|
||||
// // GẮN SỰ KIỆN CHO Ô TÌM KIẾM
|
||||
// document.getElementById('searchInput').oninput = function(e) {
|
||||
// const keyword = e.target.value.toLowerCase();
|
||||
// const filtered = deviceTypes.filter(item =>
|
||||
// item.name.toLowerCase().includes(keyword) ||
|
||||
// (item.description && item.description.toLowerCase().includes(keyword))
|
||||
// );
|
||||
// renderListWithData(filtered);
|
||||
// };
|
||||
// }
|
||||
|
||||
// // HÀM HIỂN THỊ DANH SÁCH (lấy từ mảng deviceTypes)
|
||||
// function renderList() {
|
||||
// renderListWithData(deviceTypes);
|
||||
// }
|
||||
|
||||
// // HÀM HIỂN THỊ DANH SÁCH VỚI DỮ LIỆU TÙY CHỈNH
|
||||
// function renderListWithData(data) {
|
||||
// const container = document.getElementById('deviceTypeList');
|
||||
|
||||
// if (!container) return;
|
||||
|
||||
// if (data.length === 0) {
|
||||
// container.innerHTML = `
|
||||
// <div class="text-center py-12 bg-white rounded-xl border">
|
||||
// <i class="fas fa-inbox text-gray-300 text-5xl mb-3"></i>
|
||||
// <p class="text-gray-500">Chưa có loại thiết bị nào</p>
|
||||
// <button onclick="document.getElementById('addDeviceTypeBtn').click()" class="mt-3 text-indigo-600 hover:text-indigo-700 text-sm">
|
||||
// <i class="fas fa-plus mr-1"></i>Thêm loại đầu tiên
|
||||
// </button>
|
||||
// </div>
|
||||
// `;
|
||||
// return;
|
||||
// }
|
||||
|
||||
// let html = '';
|
||||
// for (let item of data) {
|
||||
// html += `
|
||||
// <div class="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-md transition-shadow">
|
||||
// <div class="flex items-start justify-between">
|
||||
// <div class="flex items-center gap-4">
|
||||
// <!-- Icon màu sắc -->
|
||||
// <div class="w-12 h-12 rounded-xl flex items-center justify-center shadow-sm" style="background-color: ${item.color}20">
|
||||
// <i class="fas fa-network-wired text-xl" style="color: ${item.color}"></i>
|
||||
// </div>
|
||||
// <!-- Thông tin -->
|
||||
// <div>
|
||||
// <div class="flex items-center gap-2">
|
||||
// <h3 class="font-semibold text-gray-800 text-lg">${escapeHtml(item.name)}</h3>
|
||||
// <span class="text-xs px-2 py-0.5 rounded-full ${item.isActive ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'}">
|
||||
// ${item.isActive ? '● Hoạt động' : '○ Dừng'}
|
||||
// </span>
|
||||
// </div>
|
||||
// <p class="text-sm text-gray-500 mt-0.5">${escapeHtml(item.description || 'Chưa có mô tả')}</p>
|
||||
// <div class="flex items-center gap-3 mt-2">
|
||||
// <span class="text-xs text-gray-400">ID: ${item.id}</span>
|
||||
// <span class="text-xs text-gray-400">Mã màu: ${item.color}</span>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// <!-- Nút hành động -->
|
||||
// <div class="flex gap-1">
|
||||
// <button onclick="window.editDeviceType('${item.id}')" class="p-2 text-indigo-600 hover:bg-indigo-50 rounded-lg transition" title="Sửa">
|
||||
// <i class="fas fa-edit"></i>
|
||||
// </button>
|
||||
// <button onclick="window.deleteDeviceType('${item.id}')" class="p-2 text-red-600 hover:bg-red-50 rounded-lg transition" title="Xóa">
|
||||
// <i class="fas fa-trash"></i>
|
||||
// </button>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// `;
|
||||
// }
|
||||
// container.innerHTML = html;
|
||||
// }
|
||||
|
||||
// // CẬP NHẬT SỐ LIỆU THỐNG KÊ
|
||||
// function updateStats() {
|
||||
// const totalEl = document.getElementById('totalCount');
|
||||
// const activeEl = document.getElementById('activeCount');
|
||||
|
||||
// if (totalEl) totalEl.innerText = deviceTypes.length;
|
||||
// if (activeEl) activeEl.innerText = deviceTypes.filter(item => item.isActive).length;
|
||||
// }
|
||||
|
||||
// // HIỂN THỊ FORM THÊM MỚI
|
||||
// function showAddForm() {
|
||||
// // Tạo modal đơn giản bằng prompt (dễ hiểu cho người mới)
|
||||
// const name = prompt('📝 Nhập tên loại thiết bị:', '');
|
||||
// if (!name) return;
|
||||
|
||||
// const description = prompt('📄 Nhập mô tả (không bắt buộc):', '');
|
||||
// const color = prompt('🎨 Nhập mã màu (ví dụ: #3B82F6, #10B981, #EF4444):', '#3B82F6');
|
||||
|
||||
// // Tạo mới
|
||||
// const newType = {
|
||||
// id: Date.now().toString(),
|
||||
// name: name,
|
||||
// description: description || '',
|
||||
// color: color || '#3B82F6',
|
||||
// isActive: true
|
||||
// };
|
||||
|
||||
// deviceTypes.push(newType);
|
||||
|
||||
// // Cập nhật lại giao diện
|
||||
// renderList();
|
||||
// updateStats();
|
||||
|
||||
// alert(`✅ Đã thêm loại thiết bị "${name}" thành công!`);
|
||||
// }
|
||||
|
||||
// // HIỂN THỊ FORM SỬA
|
||||
// window.editDeviceType = function(id) {
|
||||
// const item = deviceTypes.find(t => t.id === id);
|
||||
// if (!item) return;
|
||||
|
||||
// const newName = prompt('✏️ Sửa tên loại thiết bị:', item.name);
|
||||
// if (!newName) return;
|
||||
|
||||
// const newDesc = prompt('📄 Sửa mô tả:', item.description || '');
|
||||
// const newColor = prompt('🎨 Sửa mã màu:', item.color);
|
||||
// const isActive = confirm(`✅ Loại thiết bị này có đang hoạt động không?\nOK = Có, Cancel = Không`);
|
||||
|
||||
// // Cập nhật
|
||||
// item.name = newName;
|
||||
// item.description = newDesc || '';
|
||||
// item.color = newColor || '#3B82F6';
|
||||
// item.isActive = isActive;
|
||||
|
||||
// // Cập nhật lại giao diện
|
||||
// renderList();
|
||||
// updateStats();
|
||||
|
||||
// alert(`✅ Đã cập nhật "${newName}" thành công!`);
|
||||
// };
|
||||
|
||||
// // XÓA LOẠI THIẾT BỊ
|
||||
// window.deleteDeviceType = function(id) {
|
||||
// const item = deviceTypes.find(t => t.id === id);
|
||||
// if (!item) return;
|
||||
|
||||
// if (confirm(`⚠️ Bạn có chắc muốn xóa "${item.name}"?\nHành động này không thể hoàn tác!`)) {
|
||||
// deviceTypes = deviceTypes.filter(t => t.id !== id);
|
||||
|
||||
// // Cập nhật lại giao diện
|
||||
// renderList();
|
||||
// updateStats();
|
||||
|
||||
// alert(`🗑️ Đã xóa "${item.name}" thành công!`);
|
||||
// }
|
||||
// };
|
||||
|
||||
// // Hàm bảo vệ chống mã độc XSS
|
||||
// function escapeHtml(str) {
|
||||
// if (!str) return '';
|
||||
// return str
|
||||
// .replace(/&/g, '&')
|
||||
// .replace(/</g, '<')
|
||||
// .replace(/>/g, '>')
|
||||
// .replace(/"/g, '"')
|
||||
// .replace(/'/g, ''');
|
||||
// }
|
||||
|
||||
// // Export để dùng ở nơi khác (nếu cần)
|
||||
// export { deviceTypes, renderList, updateStats };
|
||||
|
||||
|
||||
// ============================================
|
||||
// UI: GIAO DIỆN QUẢN LÝ LOẠI THIẾT BỊ
|
||||
// ============================================
|
||||
|
||||
import { deviceTypeAPI } from './device-type-api.js';
|
||||
|
||||
let deviceTypes = [];
|
||||
|
||||
// HÀM HIỂN THỊ TRANG DEVICE TYPE
|
||||
export async function showDeviceTypePage(container) {
|
||||
// Tạo giao diện HTML
|
||||
container.innerHTML = `
|
||||
<div class="space-y-6">
|
||||
<!-- Tiêu đề và nút thêm -->
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-800">
|
||||
<i class="fas fa-tags text-indigo-500 mr-2"></i>Quản lý loại thiết bị
|
||||
</h1>
|
||||
<p class="text-gray-500 text-sm mt-1">Danh mục các loại thiết bị mạng</p>
|
||||
</div>
|
||||
<button id="addDeviceTypeBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg shadow transition flex items-center gap-2">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span>Thêm loại thiết bị</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 2 ô thống kê -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="bg-blue-50 rounded-xl p-4">
|
||||
<p class="text-blue-600 text-sm">Tổng số loại</p>
|
||||
<p class="text-2xl font-bold text-blue-700" id="totalCount">0</p>
|
||||
</div>
|
||||
<div class="bg-green-50 rounded-xl p-4">
|
||||
<p class="text-green-600 text-sm">Đang hoạt động</p>
|
||||
<p class="text-2xl font-bold text-green-700" id="activeCount">0</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ô tìm kiếm -->
|
||||
<div class="relative">
|
||||
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
|
||||
<input type="text" id="searchInput" placeholder="Tìm kiếm..." class="w-full pl-10 pr-4 py-2 border rounded-lg">
|
||||
</div>
|
||||
|
||||
<!-- Danh sách -->
|
||||
<div id="deviceTypeList" class="space-y-3">
|
||||
<div class="text-center py-8 text-gray-400">Đang tải dữ liệu...</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Load dữ liệu từ Back-end
|
||||
await loadDataFromAPI();
|
||||
|
||||
// Gắn sự kiện
|
||||
document.getElementById('addDeviceTypeBtn').onclick = showAddForm;
|
||||
document.getElementById('searchInput').oninput = handleSearch;
|
||||
}
|
||||
|
||||
// LOAD DỮ LIỆU TỪ BACK-END
|
||||
async function loadDataFromAPI() {
|
||||
try {
|
||||
// Gọi API lấy danh sách
|
||||
const result = await deviceTypeAPI.getAll();
|
||||
deviceTypes = Array.isArray(result) ? result : result.data || [];
|
||||
|
||||
// Hiển thị lên giao diện
|
||||
renderList(deviceTypes);
|
||||
updateStats();
|
||||
|
||||
console.log('Đã tải', deviceTypes.length, 'loại thiết bị');
|
||||
} catch (error) {
|
||||
console.error('Lỗi tải dữ liệu:', error);
|
||||
document.getElementById('deviceTypeList').innerHTML = `
|
||||
<div class="text-center py-8 text-red-500">
|
||||
<i class="fas fa-exclamation-triangle text-3xl mb-2"></i>
|
||||
<p>Không thể kết nối đến Back-end!</p>
|
||||
<p class="text-sm mt-2">Vui lòng đảm bảo Back-end đang chạy tại http://localhost:5000</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// HIỂN THỊ DANH SÁCH
|
||||
function renderList(data) {
|
||||
const container = document.getElementById('deviceTypeList');
|
||||
|
||||
if (!container) return;
|
||||
|
||||
if (data.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-12 bg-white rounded-xl border">
|
||||
<i class="fas fa-inbox text-gray-300 text-5xl mb-3"></i>
|
||||
<p class="text-gray-500">Chưa có loại thiết bị nào</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (let item of data) {
|
||||
html += `
|
||||
<div class="bg-white border rounded-xl p-4 hover:shadow-md">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-xl flex items-center justify-center" style="background-color: ${item.color}20">
|
||||
<i class="fas fa-network-wired text-xl" style="color: ${item.color}"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800">${escapeHtml(item.name)}</h3>
|
||||
<p class="text-sm text-gray-500">${escapeHtml(item.description || 'Chưa có mô tả')}</p>
|
||||
<span class="text-xs px-2 py-0.5 rounded-full ${item.isActive ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'}">
|
||||
${item.isActive ? '● Hoạt động' : '○ Dừng'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<button onclick="window.editDeviceType('${item.id}')" class="p-2 text-indigo-600 hover:bg-indigo-50 rounded-lg">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button onclick="window.deleteDeviceType('${item.id}')" class="p-2 text-red-600 hover:bg-red-50 rounded-lg">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// CẬP NHẬT THỐNG KÊ
|
||||
function updateStats() {
|
||||
const totalEl = document.getElementById('totalCount');
|
||||
const activeEl = document.getElementById('activeCount');
|
||||
|
||||
if (totalEl) totalEl.innerText = deviceTypes.length;
|
||||
if (activeEl) activeEl.innerText = deviceTypes.filter(item => item.isActive).length;
|
||||
}
|
||||
|
||||
// TÌM KIẾM
|
||||
function handleSearch(e) {
|
||||
const keyword = e.target.value.toLowerCase();
|
||||
const filtered = deviceTypes.filter(item =>
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
(item.description && item.description.toLowerCase().includes(keyword))
|
||||
);
|
||||
renderList(filtered);
|
||||
}
|
||||
|
||||
// THÊM MỚI (GỌI API)
|
||||
async function showAddForm() {
|
||||
const name = prompt('📝 Nhập tên loại thiết bị:');
|
||||
if (!name) return;
|
||||
|
||||
const description = prompt('📄 Nhập mô tả:');
|
||||
const color = prompt('🎨 Nhập mã màu (vd: #3B82F6):', '#3B82F6');
|
||||
|
||||
try {
|
||||
const newType = await deviceTypeAPI.create({
|
||||
name: name,
|
||||
description: description || '',
|
||||
color: color || '#3B82F6',
|
||||
isActive: true
|
||||
});
|
||||
|
||||
// Tải lại dữ liệu
|
||||
await loadDataFromAPI();
|
||||
alert(`✅ Đã thêm "${name}" thành công!`);
|
||||
} catch (error) {
|
||||
alert('❌ Lỗi: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// SỬA (GỌI API)
|
||||
window.editDeviceType = async function(id) {
|
||||
const item = deviceTypes.find(t => t.id === id);
|
||||
if (!item) return;
|
||||
|
||||
const newName = prompt('✏️ Sửa tên:', item.name);
|
||||
if (!newName) return;
|
||||
|
||||
const newDesc = prompt('📄 Sửa mô tả:', item.description || '');
|
||||
const newColor = prompt('🎨 Sửa màu:', item.color);
|
||||
const isActive = confirm('Có đang hoạt động không?');
|
||||
|
||||
try {
|
||||
await deviceTypeAPI.update(id, {
|
||||
...item,
|
||||
name: newName,
|
||||
description: newDesc || '',
|
||||
color: newColor || '#3B82F6',
|
||||
isActive: isActive
|
||||
});
|
||||
|
||||
await loadDataFromAPI();
|
||||
alert('✅ Cập nhật thành công!');
|
||||
} catch (error) {
|
||||
alert('❌ Lỗi: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// XÓA (GỌI API)
|
||||
window.deleteDeviceType = async function(id) {
|
||||
const item = deviceTypes.find(t => t.id === id);
|
||||
if (!item) return;
|
||||
|
||||
if (confirm(`⚠️ Xóa "${item.name}"?`)) {
|
||||
try {
|
||||
await deviceTypeAPI.delete(id);
|
||||
await loadDataFromAPI();
|
||||
alert('🗑️ Xóa thành công!');
|
||||
} catch (error) {
|
||||
alert('❌ Lỗi: ' + error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/[&<>]/g, function(m) {
|
||||
if (m === '&') return '&';
|
||||
if (m === '<') return '<';
|
||||
if (m === '>') return '>';
|
||||
return m;
|
||||
});
|
||||
}
|
||||
57
frontend/src/main.js
Normal file
57
frontend/src/main.js
Normal file
@@ -0,0 +1,57 @@
|
||||
// ============================================
|
||||
// FILE NÀY CÓ NHIỆM VỤ: ĐIỀU KHIỂN CHUYỂN TRANG
|
||||
// ============================================
|
||||
|
||||
// Import các module (tính năng) của ứng dụng
|
||||
import { showDashboard } from './features/dashboard/dashboard.js';
|
||||
import { showDeviceTypePage } from './features/device-type/device-type.js';
|
||||
|
||||
// Hàm chuyển đổi giữa các trang
|
||||
async function switchToPage(pageName) {
|
||||
// Lấy thẻ div chứa nội dung
|
||||
const appDiv = document.getElementById('app');
|
||||
|
||||
// Debug: In ra console để kiểm tra xem hàm có được gọi đúng không
|
||||
if (!appDiv) {
|
||||
console.error('Không tìm thấy element #app');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Chuyển đến trang:', pageName); // Debug
|
||||
|
||||
|
||||
// Kiểm tra xem người dùng bấm vào trang nào
|
||||
if (pageName === 'dashboard') {
|
||||
// Gọi hàm showDashboard từ file dashboard.js
|
||||
await showDashboard(appDiv);
|
||||
}
|
||||
else if (pageName === 'device-type') {
|
||||
await showDeviceTypePage(appDiv);
|
||||
}
|
||||
else if (pageName === 'device') {
|
||||
appDiv.innerHTML = '<div class="bg-white p-6 rounded-lg shadow">🖥️ Trang Thiết bị - Đang phát triển</div>';
|
||||
}
|
||||
else if (pageName === 'alert') {
|
||||
appDiv.innerHTML = '<div class="bg-white p-6 rounded-lg shadow">🔔 Trang Cảnh báo - Đang phát triển</div>';
|
||||
}
|
||||
|
||||
// Đổi màu nút menu đang được chọn
|
||||
document.querySelectorAll('.menu-btn').forEach(btn => {
|
||||
if (btn.dataset.page === pageName) {
|
||||
btn.className = 'menu-btn bg-indigo-600 text-white px-5 py-2 rounded-lg font-medium';
|
||||
} else {
|
||||
btn.className = 'menu-btn bg-gray-200 text-gray-700 px-5 py-2 rounded-lg font-medium';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// BẮT SỰ KIỆN KHI BẤM VÀO CÁC NÚT MENU
|
||||
document.querySelectorAll('.menu-btn').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const pageName = button.dataset.page; // lấy tên trang từ thuộc tính data-page
|
||||
switchToPage(pageName);
|
||||
});
|
||||
});
|
||||
|
||||
// ⭐ QUAN TRỌNG: Khi load trang, mặc định hiển thị DASHBOARD (trang chủ)
|
||||
switchToPage('dashboard');
|
||||
@@ -1,7 +1,22 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from 'vite';
|
||||
import { createHtmlPlugin } from 'vite-plugin-html';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
plugins: [
|
||||
createHtmlPlugin({
|
||||
minify: true,
|
||||
entry: '/src/main.js',
|
||||
template: 'index.html',
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
port: 5173,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5000', // Backend Flask của bạn
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user