dptxa-proxy / src /pages /admin /dien-vien.astro
TXAVLOG
Deploy DPTXA to Hugging Face Spaces
4bea261
Raw
History Blame Contribute Delete
14.2 kB
---
import AdminLayout from '../../layouts/AdminLayout.astro';
import { getActors } from '../../lib/localDb.js';
const actors = await getActors();
---
<AdminLayout title="Quản lý Diễn viên">
<div class="mb-12 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 class="text-4xl font-black tracking-tighter text-white">Quản lý Diễn viên</h1>
<p class="mt-2 text-gray-500">Danh sách diễn viên tham gia diễn xuất trong các bộ phim của hệ thống.</p>
</div>
<div class="flex flex-wrap gap-3">
<button id="sync-actors-btn" class="flex items-center gap-2 rounded-2xl bg-emerald-500/10 hover:bg-emerald-500 text-emerald-400 hover:text-white border border-emerald-500/20 px-6 py-3.5 text-xs font-black uppercase tracking-widest transition-all active:scale-95 cursor-pointer">
<i class="fas fa-sync-alt"></i>
Đồng bộ từ Phim
</button>
<button id="create-actor-btn" class="flex items-center gap-2 rounded-2xl bg-primary px-6 py-3.5 text-xs font-black uppercase tracking-widest text-dark hover:scale-105 transition-all shadow-[0_0_20px_var(--primary)] cursor-pointer">
<i class="fas fa-plus"></i>
Thêm Diễn viên mới
</button>
</div>
</div>
<!-- Bulk Action Bar -->
<div id="bulk-bar" class="hidden glass rounded-3xl p-6 border border-primary/20 mb-8 items-center justify-between">
<div class="flex items-center gap-3">
<span class="text-xs font-black uppercase tracking-widest text-primary"><i class="fas fa-check-square"></i> Đã chọn <span id="selected-count">0</span> diễn viên</span>
</div>
<div class="flex gap-2">
<button id="bulk-delete-btn" class="flex items-center gap-2 rounded-xl bg-red-500/10 hover:bg-red-500 text-red-400 hover:text-white border border-red-500/20 px-4 py-2.5 text-xs font-black uppercase tracking-widest transition-all active:scale-95 cursor-pointer">
<i class="fas fa-trash"></i> Xóa hàng loạt
</button>
</div>
</div>
<div class="glass rounded-[40px] p-8 md:p-10 border border-white/5 space-y-6">
<!-- Filter section -->
<div class="flex items-center justify-between border-b border-white/5 pb-6">
<div class="relative w-full max-w-md">
<i class="fas fa-search absolute left-5 top-1/2 -translate-y-1/2 text-gray-500 text-sm"></i>
<input type="text" id="search-input" placeholder="Tìm kiếm tên diễn viên..." class="w-full bg-[#12141d] border border-white/5 rounded-2xl pl-12 pr-5 py-3.5 text-sm text-white focus:outline-none focus:border-primary/50 transition-all" />
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left">
<thead>
<tr class="border-b border-white/5">
<th class="pb-6 w-12">
<label class="relative flex items-center justify-center cursor-pointer">
<input type="checkbox" id="chk-all" class="peer sr-only" />
<div class="h-5 w-5 rounded-lg border border-white/10 bg-[#12141d] flex items-center justify-center transition-all peer-checked:bg-primary peer-checked:border-primary">
<i class="fas fa-check text-[10px] text-dark opacity-0 peer-checked:opacity-100 transition-opacity"></i>
</div>
</label>
</th>
<th class="pb-6 text-[10px] font-black uppercase tracking-widest text-gray-500">Tên Diễn viên</th>
<th class="pb-6 text-right text-[10px] font-black uppercase tracking-widest text-gray-500">Thao tác</th>
</tr>
</thead>
<tbody id="table-body" class="divide-y divide-white/5">
<!-- Rendered via Javascript -->
</tbody>
</table>
</div>
</div>
</AdminLayout>
<script is:inline define:vars={{ initialActors: actors }}>
let actors = initialActors || [];
let searchQuery = '';
function renderTable() {
const tbody = document.getElementById('table-body');
if (!tbody) return;
const filtered = actors.filter(a => a.name.toLowerCase().includes(searchQuery.toLowerCase()));
if (filtered.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="3" class="py-12 text-center text-gray-500 text-sm">
<i class="fas fa-user-friends text-3xl mb-3 block opacity-20"></i>
Không tìm thấy diễn viên nào
</td>
</tr>
`;
return;
}
tbody.innerHTML = filtered.map(a => `
<tr class="hover:bg-white/[0.01] transition-colors group">
<td class="py-5">
<label class="relative flex items-center justify-center cursor-pointer">
<input type="checkbox" class="chk-item peer sr-only" data-name="${a.name}" />
<div class="h-5 w-5 rounded-lg border border-white/10 bg-[#12141d] flex items-center justify-center transition-all peer-checked:bg-primary peer-checked:border-primary">
<i class="fas fa-check text-[10px] text-dark opacity-0 peer-checked:opacity-100 transition-opacity"></i>
</div>
</label>
</td>
<td class="py-5 text-sm font-bold text-white">${a.name}</td>
<td class="py-5 text-right">
<div class="flex justify-end gap-2">
<button class="edit-btn h-9 w-9 rounded-xl bg-white/5 hover:bg-primary/20 text-gray-400 hover:text-primary transition-all border border-white/5 flex items-center justify-center" data-name="${a.name}">
<i class="fas fa-edit text-xs"></i>
</button>
<button class="delete-btn h-9 w-9 rounded-xl bg-white/5 hover:bg-red-500/20 text-gray-400 hover:text-red-500 transition-all border border-white/5 flex items-center justify-center" data-name="${a.name}">
<i class="fas fa-trash text-xs"></i>
</button>
</div>
</td>
</tr>
`).join('');
bindEvents();
updateBulkBar();
}
function bindEvents() {
// Edit Actor
document.querySelectorAll('.edit-btn').forEach(btn => {
btn.onclick = () => {
const currentName = btn.getAttribute('data-name');
window.txamodal.show({
title: 'Sửa tên Diễn viên',
message: `Thay đổi tên diễn viên:
<input type="text" id="edit-act-input" value="${currentName}" class="w-full bg-[#12141d] border border-white/5 rounded-xl px-4 py-2.5 mt-2 text-sm text-white focus:outline-none focus:border-primary/50" />`,
type: 'warning',
confirmText: 'Lưu thay đổi',
onConfirm: async () => {
const newName = (document.getElementById('edit-act-input'))?.value?.trim() || '';
if (!newName) {
window.txatoast.error('Tên không được để trống!');
return false;
}
try {
const res = await fetch('/api/admin/actors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName, originalName: currentName })
});
const data = await res.json();
if (res.ok) {
actors = data.list;
renderTable();
window.txatoast.success(data.message);
} else {
window.txatoast.error(data.error);
}
} catch(e) {
window.txatoast.error('Lỗi kết nối máy chủ!');
}
return true;
}
});
};
});
// Delete Actor
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.onclick = () => {
const name = btn.getAttribute('data-name');
window.txamodal.show({
title: 'Xác nhận xóa Diễn viên',
message: `Bạn có chắc chắn muốn xóa Diễn viên <b>${name}</b> khỏi hệ thống?`,
type: 'error',
confirmText: 'Xóa ngay',
onConfirm: async () => {
try {
const res = await fetch('/api/admin/actors', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
const data = await res.json();
if (res.ok) {
actors = data.list;
renderTable();
window.txatoast.success(data.message);
} else {
window.txatoast.error(data.error);
}
} catch(e) {
window.txatoast.error('Lỗi kết nối máy chủ!');
}
return true;
}
});
};
});
document.querySelectorAll('.chk-item').forEach(chk => {
chk.onchange = () => {
updateBulkBar();
};
});
}
function updateBulkBar() {
const chkAll = document.getElementById('chk-all');
const checked = document.querySelectorAll('.chk-item:checked');
const totalCount = document.querySelectorAll('.chk-item').length;
const bulkBar = document.getElementById('bulk-bar');
const selectedCount = document.getElementById('selected-count');
if (chkAll) {
chkAll.checked = totalCount > 0 && checked.length === totalCount;
}
if (checked.length > 0) {
if (bulkBar) {
bulkBar.style.display = 'flex';
}
if (selectedCount) selectedCount.textContent = checked.length;
} else {
if (bulkBar) {
bulkBar.style.display = 'none';
}
}
}
function init() {
const searchInput = document.getElementById('search-input');
if (searchInput) {
searchInput.oninput = (e) => {
searchQuery = e.target.value.trim();
renderTable();
};
}
const chkAll = document.getElementById('chk-all');
if (chkAll) {
chkAll.onchange = () => {
document.querySelectorAll('.chk-item').forEach(chk => {
chk.checked = chkAll.checked;
});
updateBulkBar();
};
}
// Create Actor
const createBtn = document.getElementById('create-actor-btn');
if (createBtn) {
createBtn.onclick = () => {
window.txamodal.show({
title: 'Thêm Diễn viên mới',
message: `Nhập tên diễn viên mới:
<input type="text" id="new-act-input" placeholder="Ví dụ: Châu Tinh Trì" class="w-full bg-[#12141d] border border-white/5 rounded-xl px-4 py-2.5 mt-2 text-sm text-white focus:outline-none focus:border-primary/50" />`,
type: 'success',
confirmText: 'Tạo diễn viên',
onConfirm: async () => {
const name = (document.getElementById('new-act-input'))?.value?.trim() || '';
if (!name) {
window.txatoast.error('Tên không được để trống!');
return false;
}
try {
const res = await fetch('/api/admin/actors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
const data = await res.json();
if (res.ok) {
actors = data.list;
renderTable();
window.txatoast.success(data.message);
} else {
window.txatoast.error(data.error);
}
} catch(e) {
window.txatoast.error('Lỗi kết nối máy chủ!');
}
return true;
}
});
};
}
// Sync Actors
const syncBtn = document.getElementById('sync-actors-btn');
if (syncBtn) {
syncBtn.onclick = async () => {
window.txatoast.info('Đang đồng bộ diễn viên từ kho phim...');
try {
const res = await fetch('/api/admin/actors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sync: true })
});
const data = await res.json();
if (res.ok) {
actors = data.list;
renderTable();
window.txamodal.show({
title: 'Đồng bộ diễn viên thành công',
message: data.message,
type: 'success'
});
} else {
window.txatoast.error(data.error);
}
} catch(e) {
window.txatoast.error('Lỗi đồng bộ diễn viên!');
}
};
}
// Bulk Delete
const bulkDeleteBtn = document.getElementById('bulk-delete-btn');
if (bulkDeleteBtn) {
bulkDeleteBtn.onclick = () => {
const checked = document.querySelectorAll('.chk-item:checked');
const names = Array.from(checked).map(c => c.getAttribute('data-name'));
window.txamodal.show({
title: 'Xác nhận xóa hàng loạt',
message: `Bạn có chắc chắn muốn xóa <b>${names.length}</b> diễn viên đã chọn?`,
type: 'error',
confirmText: 'Xóa tất cả',
onConfirm: async () => {
try {
const res = await fetch('/api/admin/actors', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ names })
});
const data = await res.json();
if (res.ok) {
actors = data.list;
renderTable();
window.txatoast.success(data.message);
} else {
window.txatoast.error(data.error);
}
} catch(e) {
window.txatoast.error('Lỗi kết nối máy chủ!');
}
return true;
}
});
};
}
renderTable();
}
document.addEventListener('astro:page-load', init);
</script>
<style>
.glass {
background: rgba(18, 20, 29, 0.4);
backdrop-filter: blur(40px) saturate(200%);
}
</style>