331 lines
8.5 KiB
HTML
331 lines
8.5 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>App Selector</title>
|
|
<style>
|
|
body {
|
|
font-family: "Segoe UI", sans-serif;
|
|
padding: 20px;
|
|
background-color: #f7f9fc;
|
|
color: #333;
|
|
}
|
|
|
|
h3 {
|
|
margin-bottom: 15px;
|
|
font-size: 20px;
|
|
}
|
|
|
|
input[type="text"] {
|
|
padding: 8px 12px;
|
|
margin-bottom: 12px;
|
|
width: 320px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
transition: border-color 0.2s ease;
|
|
}
|
|
|
|
input[type="text"]:focus {
|
|
outline: none;
|
|
border-color: #007bff;
|
|
box-shadow: 0 0 4px #007bff33;
|
|
}
|
|
|
|
ul {
|
|
list-style: none;
|
|
padding-left: 0;
|
|
max-height: 70vh;
|
|
overflow-y: auto;
|
|
border: 1px solid #ddd;
|
|
border-radius: 6px;
|
|
background-color: #fff;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
|
padding: 10px 15px;
|
|
}
|
|
|
|
.category-item {
|
|
margin: 6px 0;
|
|
font-size: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.category-item input[type="checkbox"] {
|
|
margin-right: 8px;
|
|
transform: scale(1.1);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.children {
|
|
margin-left: 20px;
|
|
border-left: 1px dashed #ccc;
|
|
padding-left: 10px;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.hidden {
|
|
display: none;
|
|
}
|
|
|
|
button {
|
|
padding: 8px 14px;
|
|
border: none;
|
|
background-color: #007bff;
|
|
color: white;
|
|
font-size: 14px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s ease;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
button:hover {
|
|
background-color: #0056b3;
|
|
}
|
|
|
|
.quick-filter {
|
|
margin: 5px 8px 15px 0;
|
|
padding: 6px 12px;
|
|
background-color: #f0f0f0;
|
|
border: 1px solid #ccc;
|
|
border-radius: 20px;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
color: black;
|
|
}
|
|
|
|
.quick-filter:hover {
|
|
background-color: #e0e0e0;
|
|
border-color: #aaa;
|
|
color: green;
|
|
}
|
|
|
|
/* Scrollbar đẹp */
|
|
ul::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
ul::-webkit-scrollbar-track {
|
|
background: #f1f1f1;
|
|
}
|
|
|
|
ul::-webkit-scrollbar-thumb {
|
|
background-color: #ccc;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
ul::-webkit-scrollbar-thumb:hover {
|
|
background: #bbb;
|
|
}
|
|
</style>
|
|
|
|
</head>
|
|
<body>
|
|
<h3>Search Categories</h3>
|
|
<div>
|
|
<button class="quick-filter" data-keyword="Facebook">Facebook</button>
|
|
<button class="quick-filter" data-keyword="Youtube">Youtube</button>
|
|
<button class="quick-filter" data-keyword="Proxy">Proxy</button>
|
|
<button class="quick-filter" data-keyword="Gaming">Gaming</button>
|
|
</div>
|
|
|
|
<input type="text" id="searchInput" placeholder="Search..." />
|
|
<label><input type="checkbox" id="selectAll"/> Block All</label>
|
|
<button id="applyBtn">Apply</button>
|
|
<ul id="categoryList"></ul>
|
|
|
|
|
|
<script>
|
|
const selectedAppIds = new Set();
|
|
const SERVER_DOMAIN = "http://localhost:3000"
|
|
let allData = [];
|
|
let treeData = [];
|
|
let initAppIds = [];
|
|
async function fetchData() {
|
|
const res = await fetch(SERVER_DOMAIN+'/api/getListApp');
|
|
const list = await res.json();
|
|
initAppIds = list.init
|
|
allData = list.data;
|
|
|
|
// Move init apps to top
|
|
allData.sort((a, b) => {
|
|
const aIsInit = list.init?.includes(a.id);
|
|
const bIsInit = list.init?.includes(b.id);
|
|
return aIsInit === bIsInit ? 0 : aIsInit ? -1 : 1;
|
|
});
|
|
console.log("allData", allData)
|
|
// Tạo map để truy cập theo ID
|
|
const idMap = new Map();
|
|
allData.forEach(item => idMap.set(item.id, { ...item, children: [] }));
|
|
|
|
// Gắn children object thay vì chỉ id
|
|
idMap.forEach(item => {
|
|
if (Array.isArray(item.children) && item.children.length > 0) {
|
|
item.children = item.children.map(id => idMap.get(id)).filter(Boolean);
|
|
}
|
|
});
|
|
|
|
// Xác định các node gốc (không nằm trong children của bất kỳ ai)
|
|
const childIds = new Set();
|
|
idMap.forEach(item => {
|
|
item.children.forEach(child => childIds.add(child.id));
|
|
});
|
|
|
|
treeData = Array.from(idMap.values()).filter(item => !childIds.has(item.id));
|
|
render();
|
|
}
|
|
|
|
function buildTree(data, parentId) {
|
|
return data
|
|
.filter(item => item.parent_id === parentId)
|
|
.map(item => ({
|
|
...item,
|
|
children: buildTree(data, item.id),
|
|
}));
|
|
}
|
|
|
|
function render() {
|
|
const container = document.getElementById('categoryList');
|
|
container.innerHTML = '';
|
|
renderTree(treeData, container, initAppIds); // ⬅ truyền thêm init vào
|
|
}
|
|
|
|
function renderTree(tree, container, initIds = []) {
|
|
tree.forEach(item => {
|
|
const li = document.createElement("li");
|
|
li.classList.add("category-item");
|
|
|
|
const checkbox = document.createElement("input");
|
|
checkbox.type = "checkbox";
|
|
checkbox.dataset.id = item.id;
|
|
|
|
// ✅ Nếu ID nằm trong init => mặc định được chọn
|
|
if (initIds.includes(item.id)) {
|
|
checkbox.checked = true;
|
|
selectedAppIds.add(item.id); // lưu vào set để submit
|
|
} else {
|
|
checkbox.checked = selectedAppIds.has(item.id);
|
|
}
|
|
|
|
checkbox.addEventListener("change", (e) => {
|
|
const id = parseInt(e.target.dataset.id);
|
|
if (e.target.checked) {
|
|
selectedAppIds.add(id);
|
|
} else {
|
|
selectedAppIds.delete(id);
|
|
}
|
|
});
|
|
|
|
const label = document.createElement("label");
|
|
label.appendChild(checkbox);
|
|
label.appendChild(document.createTextNode(" " + item.name));
|
|
li.appendChild(label);
|
|
|
|
if (item.children.length > 0) {
|
|
const ul = document.createElement("ul");
|
|
ul.classList.add("children");
|
|
renderTree(item.children, ul, initIds); // ⬅ truyền tiếp danh sách init
|
|
li.appendChild(ul);
|
|
}
|
|
|
|
container.appendChild(li);
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll(".quick-filter").forEach(btn => {
|
|
btn.addEventListener("click", () => {
|
|
render()
|
|
const keyword = btn.dataset.keyword;
|
|
|
|
document.getElementById("searchInput").value = keyword;
|
|
|
|
if (keyword === "Proxy" || keyword ==="Gaming") {
|
|
// Tìm node có name = "Proxy"
|
|
const proxyNode = allData.find(item => item.name === keyword);
|
|
|
|
if (proxyNode && Array.isArray(proxyNode.children)) {
|
|
// ⚠️ KHÔNG thay đổi treeData gốc
|
|
|
|
// Tạo tạm cây con để render
|
|
const idMap = new Map();
|
|
allData.forEach(item => idMap.set(item.id, { ...item, children: [] }));
|
|
|
|
// Gắn lại children object
|
|
idMap.forEach(item => {
|
|
if (Array.isArray(item.children) && item.children.length > 0) {
|
|
item.children = item.children.map(id => idMap.get(id)).filter(Boolean);
|
|
}
|
|
});
|
|
|
|
// Render tạm UI chỉ phần children của Proxy
|
|
const proxyChildren = proxyNode.children.map(id => idMap.get(id)).filter(Boolean);
|
|
const container = document.getElementById('categoryList');
|
|
container.innerHTML = '';
|
|
renderTree(proxyChildren, container, initAppIds); // render tạm
|
|
}
|
|
} else {
|
|
// Filter từ khóa bình thường
|
|
const searchKeyword = keyword.toLowerCase();
|
|
const items = categoryList.querySelectorAll(":scope > li");
|
|
items.forEach(item => filterTree(item, searchKeyword));
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
function filterTree(element, query) {
|
|
const text = element.textContent.toLowerCase();
|
|
const match = text.includes(query);
|
|
|
|
let childMatch = false;
|
|
const children = element.querySelectorAll(":scope > ul > li");
|
|
|
|
children.forEach(child => {
|
|
if (filterTree(child, query)) childMatch = true;
|
|
});
|
|
|
|
const visible = match || childMatch;
|
|
element.style.display = visible ? "" : "none";
|
|
return visible;
|
|
}
|
|
|
|
document.getElementById("searchInput").addEventListener("input", e => {
|
|
const query = e.target.value.toLowerCase();
|
|
const items = categoryList.querySelectorAll(":scope > li");
|
|
items.forEach(item => filterTree(item, query));
|
|
});
|
|
|
|
document.getElementById("selectAll").addEventListener("change", e => {
|
|
const checked = e.target.checked;
|
|
const visibleCheckboxes = categoryList.querySelectorAll("input[type='checkbox']");
|
|
visibleCheckboxes.forEach(cb => {
|
|
if (cb.closest("li").offsetParent !== null) {
|
|
cb.checked = checked;
|
|
const id = parseInt(cb.dataset.id);
|
|
if (checked) selectedAppIds.add(id);
|
|
else selectedAppIds.delete(id);
|
|
}
|
|
});
|
|
});
|
|
|
|
document.getElementById("applyBtn").addEventListener("click", async () => {
|
|
const body = JSON.stringify({ app_ids: Array.from(selectedAppIds) });
|
|
const res = await fetch(SERVER_DOMAIN+"/update", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body
|
|
});
|
|
const json = await res.json();
|
|
alert("Submitted:\n" + JSON.stringify(json, null, 2));
|
|
});
|
|
|
|
// Initial fetch
|
|
fetchData();
|
|
</script>
|
|
</body>
|
|
</html>
|