feat: cập nhật admin layout

This commit is contained in:
ngthanhvu
2026-08-07 12:41:20 -04:00
parent 8c25f121f2
commit 9c8a77f134
10 changed files with 608 additions and 233 deletions
+166
View File
@@ -0,0 +1,166 @@
/* Admin Light Mode Overrides — only applies inside .admin-light */
.admin-light .admin-page {
background-color: #f8fafc !important;
color: #334155 !important;
}
.admin-light .admin-card {
border-color: #e2e8f0 !important;
background-color: #ffffff !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}
.admin-light .admin-card:hover {
border-color: #cbd5e1 !important;
}
.admin-light .admin-card-gradient {
border-color: #e2e8f0 !important;
background-color: #ffffff !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}
.admin-light .admin-input {
border-color: #e2e8f0 !important;
background-color: #f8fafc !important;
color: #334155 !important;
}
.admin-light .admin-input::placeholder {
color: #94a3b8 !important;
}
.admin-light .admin-input:focus {
border-color: #095DF1 !important;
background-color: #fff !important;
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.12) !important;
}
.admin-light .admin-btn-primary {
background-color: #095DF1 !important;
color: #fff !important;
box-shadow: 0 4px 6px rgba(14, 165, 233, 0.2) !important;
}
.admin-light .admin-btn-primary:hover {
background-color: #095DF1 !important;
}
.admin-light .admin-btn-secondary {
border-color: #e2e8f0 !important;
background-color: #fff !important;
color: #475569 !important;
}
.admin-light .admin-btn-secondary:hover {
background-color: #f1f5f9 !important;
}
.admin-light .admin-btn-danger {
background-color: #fef2f2 !important;
color: #dc2626 !important;
}
.admin-light .admin-btn-danger:hover {
background-color: #fee2e2 !important;
}
.admin-light .admin-label {
color: #64748b !important;
}
.admin-light .admin-section-title {
color: #0f172a !important;
}
.admin-light .admin-section-subtitle {
color: #64748b !important;
}
.admin-light .admin-num {
color: #0f172a !important;
}
.admin-light .admin-icon-square {
border-color: #e2e8f0 !important;
background-color: #f8fafc !important;
color: #94a3b8 !important;
}
.admin-light .admin-input-sm {
border-color: #e2e8f0 !important;
background-color: #f8fafc !important;
color: #334155 !important;
}
.admin-light .admin-input-sm::placeholder {
color: #94a3b8 !important;
}
.admin-light .admin-input-sm:focus {
border-color: #095DF1 !important;
background-color: #fff !important;
}
/* Override badge colors for light mode */
.admin-light .admin-badge {
background-color: rgba(241, 245, 249, 0.8) !important;
color: #475569 !important;
}
.admin-light .admin-badge.bg-yellow-400,
.admin-light [class*="bg-yellow-400"] {
background-color: rgba(14, 165, 233, 0.1) !important;
color: #0284c7 !important;
}
.admin-light .admin-badge.bg-emerald-400 {
background-color: rgba(16, 185, 129, 0.1) !important;
color: #059669 !important;
}
.admin-light .admin-badge.bg-slate-400 {
background-color: rgba(148, 163, 184, 0.1) !important;
color: #64748b !important;
}
.admin-light .admin-badge.bg-blue-400 {
background-color: rgba(59, 130, 246, 0.1) !important;
color: #2563eb !important;
}
.admin-light .admin-badge.bg-purple-400 {
background-color: rgba(168, 85, 247, 0.1) !important;
color: #9333ea !important;
}
.admin-light .admin-badge.bg-pink-400 {
background-color: rgba(236, 72, 153, 0.1) !important;
color: #db2777 !important;
}
/* Override dark scrollbar */
.admin-light .admin-scrollbar::-webkit-scrollbar-thumb {
background: rgba(14, 165, 233, 0.45) !important;
}
.admin-light .admin-scrollbar::-webkit-scrollbar-track {
background: rgba(241, 245, 249, 0.5) !important;
}
/* Fix Font Awesome SVG icons drooping below text baseline */
.admin-light .fa-solid,
.admin-light .fa-regular,
.admin-light .fa-light,
.admin-light .fa-thin,
.admin-light .fa-duotone,
.admin-light svg[class*="fa"] {
vertical-align: middle !important;
}
/* Modal dark overlay fix inside admin */
.admin-light .modal-fade-enter-from,
.admin-light .modal-fade-leave-to {
opacity: 0;
transform: scale(0.98);
}
+1 -1
View File
@@ -12,5 +12,5 @@ const icon = computed(() => getIcon(props.name))
</script> </script>
<template> <template>
<FontAwesomeIcon :icon="icon" :class="props.class" /> <FontAwesomeIcon :icon="icon" :class="props.class" :style="{ verticalAlign: 'middle' }" />
</template> </template>
+149 -27
View File
@@ -33,18 +33,16 @@ const displayInitial = computed(() => displayName.value.charAt(0).toUpperCase())
</script> </script>
<template> <template>
<div class="min-h-screen bg-[#0A0B0E]"> <div class="admin-light min-h-screen bg-[#f8fafc]">
<aside <aside
class="fixed inset-y-0 left-0 z-40 w-64 transform border-r border-white/[0.06] bg-[#0E0F13] transition-transform lg:translate-x-0" class="fixed inset-y-0 left-0 z-40 w-64 transform border-r border-slate-200 bg-[#095DF2] transition-transform lg:translate-x-0 border-l-4 border-l-blue-700">
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'"> <div class="flex h-16 items-center justify-between border-white/20 px-5">
<div class="flex h-16 items-center justify-between border-b border-white/[0.06] px-5">
<NuxtLink to="/admin" class="flex items-center gap-3"> <NuxtLink to="/admin" class="flex items-center gap-3">
<div class="grid size-9 place-items-center rounded-xl bg-gradient-to-br from-yellow-400 to-amber-500 shadow-lg shadow-yellow-400/20"> <AppIcon name="film" class="size-5 text-white" />
<AppIcon name="film" class="size-4 text-slate-950" /> <span class="text-lg font-black text-white tracking-tight">Admin Management</span>
</div>
<span class="text-lg font-black text-white">CineK Admin</span>
</NuxtLink> </NuxtLink>
<button type="button" class="grid size-8 place-items-center rounded-lg text-slate-400 transition hover:bg-white/[0.05] hover:text-white lg:hidden" <button type="button"
class="grid size-8 place-items-center rounded-lg text-slate-400 transition hover:bg-slate-100 hover:text-slate-600 lg:hidden"
@click="closeSidebar"> @click="closeSidebar">
<AppIcon name="x" class="size-5" /> <AppIcon name="x" class="size-5" />
</button> </button>
@@ -52,24 +50,22 @@ const displayInitial = computed(() => displayName.value.charAt(0).toUpperCase())
<nav class="flex flex-col gap-1 p-3"> <nav class="flex flex-col gap-1 p-3">
<NuxtLink v-for="item in navItems" :key="item.to" :to="item.to" <NuxtLink v-for="item in navItems" :key="item.to" :to="item.to"
class="group relative flex items-center gap-3 rounded-xl px-4 py-3 text-sm font-semibold transition" class="group relative flex items-center gap-3 rounded-xl px-4 py-3 text-sm font-semibold transition" :class="isActive(item.to)
:class="isActive(item.to) ? 'bg-white/15 text-white font-semibold'
? 'bg-yellow-400/10 text-yellow-400' : 'text-blue-100 hover:bg-white/10 hover:text-white'">
: 'text-slate-400 hover:bg-white/[0.03] hover:text-white'">
<span v-if="isActive(item.to)" class="absolute left-0 top-1/2 h-6 w-1 -translate-y-1/2 rounded-r-full bg-yellow-400" />
<AppIcon :name="item.icon" class="size-5 transition group-hover:scale-110" /> <AppIcon :name="item.icon" class="size-5 transition group-hover:scale-110" />
{{ item.label }} {{ item.label }}
</NuxtLink> </NuxtLink>
</nav> </nav>
<div class="absolute inset-x-0 bottom-0 space-y-1 border-t border-white/[0.06] p-3"> <div class="absolute inset-x-0 bottom-0 space-y-1 border-t border-white/20 p-3">
<NuxtLink to="/" <NuxtLink to="/"
class="flex items-center gap-3 rounded-xl px-4 py-3 text-sm font-semibold text-slate-400 transition hover:bg-white/[0.03] hover:text-white"> class="flex items-center gap-3 rounded-xl px-4 py-3 text-sm font-semibold text-blue-100 transition hover:bg-white/10 hover:text-white">
<AppIcon name="home" class="size-5" /> <AppIcon name="home" class="size-5" />
Về trang chủ Về trang chủ
</NuxtLink> </NuxtLink>
<button type="button" <button type="button"
class="flex w-full items-center gap-3 rounded-xl px-4 py-3 text-sm font-semibold text-red-400 transition hover:bg-red-500/10" class="flex w-full items-center gap-3 rounded-xl px-4 py-3 text-sm font-semibold text-red-500 transition hover:bg-white/10 hover:text-red-600"
@click="handleLogout"> @click="handleLogout">
<AppIcon name="log-out" class="size-5" /> <AppIcon name="log-out" class="size-5" />
Đăng xuất Đăng xuất
@@ -79,24 +75,26 @@ const displayInitial = computed(() => displayName.value.charAt(0).toUpperCase())
<div class="lg:pl-64"> <div class="lg:pl-64">
<header <header
class="sticky top-0 z-30 flex h-16 items-center gap-4 border-b border-white/[0.06] bg-[#0A0B0E]/80 px-4 backdrop-blur-xl lg:px-6"> class="sticky top-0 z-30 flex h-16 items-center gap-4 border-b border-slate-200 bg-white/80 px-4 backdrop-blur-xl lg:px-6 shadow-sm">
<button type="button" class="grid size-10 place-items-center rounded-xl text-slate-400 transition hover:bg-white/[0.05] hover:text-white lg:hidden" <button type="button"
class="grid size-10 place-items-center rounded-xl text-slate-400 transition hover:bg-slate-100 hover:text-slate-600 lg:hidden"
@click="sidebarOpen = true"> @click="sidebarOpen = true">
<AppIcon name="menu" class="size-5" /> <AppIcon name="menu" class="size-5" />
</button> </button>
<div class="ml-auto flex items-center gap-4"> <div class="ml-auto flex items-center gap-4">
<div class="hidden items-center gap-2 text-sm text-slate-400 md:flex"> <div class="hidden items-center gap-2 text-sm text-slate-500 md:flex">
<span class="size-2 rounded-full bg-emerald-500 shadow-[0_0_8px] shadow-emerald-500/50" /> <span class="size-2 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" />
Hệ thống hoạt động Hệ thống hoạt động
</div> </div>
<div class="flex items-center gap-3 rounded-2xl border border-white/[0.06] bg-[#131418] px-3 py-1.5"> <div class="flex items-center gap-3 rounded-2xl border border-slate-200 bg-slate-50 px-3 py-1.5">
<div class="grid size-8 place-items-center rounded-full bg-gradient-to-br from-yellow-400/20 to-yellow-400/5 text-sm font-black text-yellow-400 ring-1 ring-yellow-400/20"> <div
class="grid size-8 place-items-center rounded-full bg-linear-to-br from-sky-400/20 to-cyan-600/10 text-sm font-black text-sky-700 ring-1 ring-[rgba(14,165,233,0.2)]">
{{ displayInitial }} {{ displayInitial }}
</div> </div>
<div class="hidden sm:block"> <div class="hidden sm:block">
<p class="text-sm font-bold text-white">{{ displayName }}</p> <p class="text-sm font-bold text-slate-900">{{ displayName }}</p>
<p class="text-xs capitalize text-slate-400">{{ user?.role || 'admin' }}</p> <p class="text-xs capitalize text-slate-500">{{ user?.role || 'admin' }}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -108,12 +106,136 @@ const displayInitial = computed(() => displayName.value.charAt(0).toUpperCase())
</div> </div>
<Transition name="sidebar-fade"> <Transition name="sidebar-fade">
<div v-if="sidebarOpen" class="fixed inset-0 z-30 bg-black/70 backdrop-blur-sm lg:hidden" <div v-if="sidebarOpen" class="fixed inset-0 z-30 bg-black/40 backdrop-blur-sm lg:hidden" @click="closeSidebar" />
@click="closeSidebar" />
</Transition> </Transition>
</div> </div>
</template> </template>
<style>
/* Non-scoped: MUST override Tailwind global styles from main.css */
.admin-light .admin-page {
background-color: #f8fafc !important;
color: #334155 !important;
}
.admin-light .admin-card,
.admin-light .admin-card-gradient {
background-color: #ffffff !important;
border-color: #e2e8f0 !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06) !important;
color: #1e293b !important;
}
.admin-light .admin-input {
background-color: #f8fafc !important;
border-color: #e2e8f0 !important;
color: #334155 !important;
}
.admin-light .admin-input::placeholder {
color: #94a3b8 !important;
}
.admin-light .admin-section-title {
color: #0f172a !important;
}
.admin-light .admin-num {
color: #0f172a !important;
}
.admin-light .admin-label {
color: #64748b !important;
}
.admin-light .admin-btn-primary {
background-color: #095DF1 !important;
color: #fff !important;
}
.admin-light .admin-btn-secondary {
background-color: #fff !important;
border-color: #e2e8f0 !important;
color: #475569 !important;
}
.admin-light .admin-btn-danger {
background-color: #fef2f2 !important;
color: #dc2626 !important;
}
.admin-light .admin-badge[class*="bg-yellow-400"] {
background-color: rgba(14, 165, 233, 0.1) !important;
color: #0284c7 !important;
}
.admin-light .admin-badge[class*="bg-emerald-400"] {
background-color: rgba(16, 185, 129, 0.1) !important;
color: #059669 !important;
}
.admin-light .admin-badge[class*="bg-slate-400"] {
background-color: rgba(148, 163, 184, 0.1) !important;
color: #64748b !important;
}
.admin-light .admin-badge[class*="bg-blue-400"] {
background-color: rgba(59, 130, 246, 0.1) !important;
color: #2563eb !important;
}
.admin-light .admin-badge[class*="bg-purple-400"] {
background-color: rgba(168, 85, 247, 0.1) !important;
color: #9333ea !important;
}
/* --- Contrast Fixes: Darken Faint Gray Text --- */
.admin-light h1,
.admin-light h2,
.admin-light h3,
.admin-light .admin-section-title,
.admin-light .admin-num,
.admin-light strong {
color: #0f172a !important;
/* Slate 900 - Darkest */
}
.admin-light .admin-label,
.admin-light .admin-section-subtitle,
.admin-light p {
color: #334155 !important;
/* Slate 700 - Medium Dark */
}
/* Override ALL gray text classes to make them darker */
.admin-light [class*="text-slate-400"],
.admin-light [class*="text-zinc-400"],
.admin-light [class*="text-slate-500"],
.admin-light [class*="text-zinc-500"] {
color: #475569 !important;
/* Force to Slate 600 */
}
.admin-light .text-red-400,
.admin-light .text-red-300 {
color: #dc2626 !important;
/* Red-600 */
}
.admin-light .text-emerald-400 {
color: #059669 !important;
/* Emerald-600 */
}
/* Input placeholder */
.admin-light input::placeholder,
.admin-light textarea::placeholder {
color: #94a3b8 !important;
/* Slate 400 - Visible but muted */
}
</style>
<style scoped> <style scoped>
.sidebar-fade-enter-active, .sidebar-fade-enter-active,
.sidebar-fade-leave-active { .sidebar-fade-leave-active {
+4
View File
@@ -87,6 +87,7 @@ import {
faCommentSlash, faCommentSlash,
faRotate, faRotate,
faPenToSquare, faPenToSquare,
faEllipsisVertical,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@@ -180,6 +181,7 @@ library.add(
faCommentSlash, faCommentSlash,
faRotate, faRotate,
faPenToSquare, faPenToSquare,
faEllipsisVertical,
) )
export type IconName = export type IconName =
@@ -204,6 +206,7 @@ export type IconName =
| 'corner-down-left' | 'corner-down-left'
| 'crown' | 'crown'
| 'edit' | 'edit'
| 'ellipsis-vertical'
| 'eye' | 'eye'
| 'eye-off' | 'eye-off'
| 'fast-forward' | 'fast-forward'
@@ -287,6 +290,7 @@ const iconMap: Record<IconName, [string, string]> = {
'corner-down-left': ['fas', 'arrow-turn-down'], 'corner-down-left': ['fas', 'arrow-turn-down'],
'crown': ['fas', 'crown'], 'crown': ['fas', 'crown'],
'edit': ['fas', 'pen-to-square'], 'edit': ['fas', 'pen-to-square'],
'ellipsis-vertical': ['fas', 'ellipsis-vertical'],
'eye': ['fas', 'eye'], 'eye': ['fas', 'eye'],
'eye-off': ['fas', 'eye-slash'], 'eye-off': ['fas', 'eye-slash'],
'fast-forward': ['fas', 'forward-fast'], 'fast-forward': ['fas', 'forward-fast'],
+12 -12
View File
@@ -24,44 +24,44 @@ const allowRegistration = ref(true)
<div class="space-y-6 lg:col-span-2"> <div class="space-y-6 lg:col-span-2">
<div class="admin-card p-5"> <div class="admin-card p-5">
<div class="mb-5 flex items-center gap-3"> <div class="mb-5 flex items-center gap-3">
<div class="grid size-11 place-items-center rounded-xl bg-yellow-400/10"> <div class="grid size-11 place-items-center rounded-xl bg-sky-50">
<AppIcon name="settings" class="size-5 text-yellow-400" /> <AppIcon name="settings" class="size-5 text-sky-600" />
</div> </div>
<div> <div>
<h2 class="text-lg font-black text-white">Cài đặt chung</h2> <h2 class="text-lg font-black text-slate-900">Cài đặt chung</h2>
<p class="text-sm text-slate-400">Thông tin bản về website</p> <p class="text-sm text-slate-400">Thông tin bản về website</p>
</div> </div>
</div> </div>
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<label class="mb-2 block text-sm font-semibold text-white">Tên website</label> <label class="mb-2 block text-sm font-semibold text-slate-900">Tên website</label>
<input v-model="siteName" type="text" class="admin-input"> <input v-model="siteName" type="text" class="admin-input">
</div> </div>
<div> <div>
<label class="mb-2 block text-sm font-semibold text-white"> tả website</label> <label class="mb-2 block text-sm font-semibold text-slate-900"> tả website</label>
<textarea v-model="siteDescription" rows="3" <textarea v-model="siteDescription" rows="3"
class="w-full rounded-xl border border-white/8 bg-white/3 px-4 py-2.5 text-sm text-white outline-none transition placeholder:text-slate-500 focus:border-yellow-400/50 focus:bg-white/5 focus:ring-2 focus:ring-yellow-400/20"></textarea> class="w-full rounded-xl border border-slate-200 bg-slate-100 px-4 py-2.5 text-sm text-slate-900 outline-none transition placeholder:text-slate-500 focus:border-sky-500 focus:bg-slate-100/60 focus:ring-2 focus:ring-sky-500/20"></textarea>
</div> </div>
</div> </div>
</div> </div>
<div class="admin-card p-5"> <div class="admin-card p-5">
<h2 class="mb-5 text-lg font-black text-white">Tùy chọn hệ thống</h2> <h2 class="mb-5 text-lg font-black text-slate-900">Tùy chọn hệ thống</h2>
<div class="space-y-3"> <div class="space-y-3">
<label class="flex cursor-pointer items-center justify-between rounded-xl border border-white/6 bg-white/3 p-4 transition hover:border-white/10 hover:bg-white/5"> <label class="flex cursor-pointer items-center justify-between rounded-xl border border-white/6 bg-slate-100 p-4 transition hover:border-white/10 hover:bg-slate-100/60">
<div> <div>
<p class="text-sm font-bold text-white">Chế độ bảo trì</p> <p class="text-sm font-bold text-slate-900">Chế độ bảo trì</p>
<p class="mt-1 text-xs text-slate-400">Tạm thời tắt website để bảo trì</p> <p class="mt-1 text-xs text-slate-400">Tạm thời tắt website để bảo trì</p>
</div> </div>
<AdminToggle v-model="maintenanceMode" /> <AdminToggle v-model="maintenanceMode" />
</label> </label>
<label class="flex cursor-pointer items-center justify-between rounded-xl border border-white/6 bg-white/3 p-4 transition hover:border-white/10 hover:bg-white/5"> <label class="flex cursor-pointer items-center justify-between rounded-xl border border-white/6 bg-slate-100 p-4 transition hover:border-white/10 hover:bg-slate-100/60">
<div> <div>
<p class="text-sm font-bold text-white">Cho phép đăng </p> <p class="text-sm font-bold text-slate-900">Cho phép đăng </p>
<p class="mt-1 text-xs text-slate-400">Cho phép người dùng mới đăng tài khoản</p> <p class="mt-1 text-xs text-slate-400">Cho phép người dùng mới đăng tài khoản</p>
</div> </div>
<AdminToggle v-model="allowRegistration" /> <AdminToggle v-model="allowRegistration" />
@@ -79,7 +79,7 @@ const allowRegistration = ref(true)
<div class="space-y-6"> <div class="space-y-6">
<div class="admin-card-gradient p-5"> <div class="admin-card-gradient p-5">
<h3 class="mb-2 text-lg font-black text-white">Cần giúp đỡ?</h3> <h3 class="mb-2 text-lg font-black text-slate-900">Cần giúp đỡ?</h3>
<p class="text-sm leading-relaxed text-slate-400"> <p class="text-sm leading-relaxed text-slate-400">
Các cài đặt này ảnh hưởng đến toàn bộ hệ thống. Hãy cẩn thận khi thay đổi chế độ bảo trì. Các cài đặt này ảnh hưởng đến toàn bộ hệ thống. Hãy cẩn thận khi thay đổi chế độ bảo trì.
</p> </p>
+42 -42
View File
@@ -30,7 +30,7 @@ const sourceSegments = computed(() => {
const kkphim = stats.value?.kkphim || 0 const kkphim = stats.value?.kkphim || 0
if (!total) return [] if (!total) return []
return [ return [
{ key: 'ophim', label: 'OPhim', value: ophim, pct: (ophim / total) * 100, color: '#facc15' }, { key: 'ophim', label: 'OPhim', value: ophim, pct: (ophim / total) * 100, color: '#fbbf24' },
{ key: 'nguonc', label: 'NguonC', value: nguonc, pct: (nguonc / total) * 100, color: '#34d399' }, { key: 'nguonc', label: 'NguonC', value: nguonc, pct: (nguonc / total) * 100, color: '#34d399' },
{ key: 'kkphim', label: 'KKPhim', value: kkphim, pct: (kkphim / total) * 100, color: '#60a5fa' }, { key: 'kkphim', label: 'KKPhim', value: kkphim, pct: (kkphim / total) * 100, color: '#60a5fa' },
].filter(s => s.value > 0) ].filter(s => s.value > 0)
@@ -58,14 +58,14 @@ function barWidth(views: number) {
<!-- Hero: Total movies (col-span-2, row-span-2) --> <!-- Hero: Total movies (col-span-2, row-span-2) -->
<div class="admin-card-gradient relative overflow-hidden p-6 sm:col-span-2 lg:row-span-2"> <div class="admin-card-gradient relative overflow-hidden p-6 sm:col-span-2 lg:row-span-2">
<!-- subtle accent glow --> <!-- subtle accent glow -->
<div class="pointer-events-none absolute -right-16 -top-16 size-48 rounded-full bg-yellow-400/10 blur-3xl" /> <div class="pointer-events-none absolute -right-16 -top-16 size-48 rounded-full bg-sky-50 blur-3xl" />
<div class="relative flex h-full flex-col"> <div class="relative flex h-full flex-col">
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<div> <div>
<p class="admin-label">Tổng số phim</p> <p class="admin-label">Tổng số phim</p>
<p class="admin-num mt-3 text-5xl leading-none sm:text-6xl">{{ stats?.total?.toLocaleString() || 0 }}</p> <p class="admin-num mt-3 text-5xl leading-none sm:text-6xl">{{ stats?.total?.toLocaleString() || 0 }}</p>
</div> </div>
<div class="grid size-10 place-items-center rounded-xl border border-white/8 bg-white/3 text-yellow-400"> <div class="grid size-10 place-items-center rounded-xl border border-white/8 bg-slate-100 text-sky-600">
<AppIcon name="film" class="size-5" /> <AppIcon name="film" class="size-5" />
</div> </div>
</div> </div>
@@ -73,19 +73,19 @@ function barWidth(views: number) {
<div class="mt-8"> <div class="mt-8">
<div class="mb-2 flex items-center justify-between"> <div class="mb-2 flex items-center justify-between">
<p class="admin-label">Phân bổ theo nguồn</p> <p class="admin-label">Phân bổ theo nguồn</p>
<span class="text-xs text-zinc-500">{{ sourceSegments.length }} nguồn</span> <span class="text-xs text-slate-500">{{ sourceSegments.length }} nguồn</span>
</div> </div>
<div v-if="sourceSegments.length" class="flex h-3 w-full overflow-hidden rounded-full bg-white/4"> <div v-if="sourceSegments.length" class="flex h-3 w-full overflow-hidden rounded-full bg-slate-100">
<div v-for="seg in sourceSegments" :key="seg.key" class="h-full transition-all duration-700" <div v-for="seg in sourceSegments" :key="seg.key" class="h-full transition-all duration-700"
:style="{ width: seg.pct + '%', backgroundColor: seg.color }" :style="{ width: seg.pct + '%', backgroundColor: seg.color }"
:title="`${seg.label}: ${seg.value} (${Math.round(seg.pct)}%)`" /> :title="`${seg.label}: ${seg.value} (${Math.round(seg.pct)}%)`" />
</div> </div>
<div v-else class="h-3 w-full rounded-full bg-white/4" /> <div v-else class="h-3 w-full rounded-full bg-slate-100" />
<div class="mt-3 flex flex-wrap gap-x-5 gap-y-1.5"> <div class="mt-3 flex flex-wrap gap-x-5 gap-y-1.5">
<div v-for="seg in sourceSegments" :key="seg.key" class="flex items-center gap-2"> <div v-for="seg in sourceSegments" :key="seg.key" class="flex items-center gap-2">
<span class="size-2 rounded-full" :style="{ backgroundColor: seg.color }" /> <span class="size-2 rounded-full" :style="{ backgroundColor: seg.color }" />
<span class="text-xs text-zinc-400">{{ seg.label }}</span> <span class="text-xs text-slate-400">{{ seg.label }}</span>
<span class="text-xs font-semibold text-zinc-200">{{ seg.value.toLocaleString() }}</span> <span class="text-xs font-semibold text-slate-700">{{ seg.value.toLocaleString() }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -102,8 +102,8 @@ function barWidth(views: number) {
<div> <div>
<p class="admin-label">Chưa hiển thị</p> <p class="admin-label">Chưa hiển thị</p>
<p class="mt-1.5 flex items-baseline gap-2"> <p class="mt-1.5 flex items-baseline gap-2">
<span class="admin-num text-xl text-zinc-300">{{ stats?.inactive?.toLocaleString() || 0 }}</span> <span class="admin-num text-xl text-slate-600">{{ stats?.inactive?.toLocaleString() || 0 }}</span>
<span class="text-xs font-medium text-zinc-500">{{ percentOf(stats?.inactive || 0, stats?.total || 0) <span class="text-xs font-medium text-slate-500">{{ percentOf(stats?.inactive || 0, stats?.total || 0)
}}%</span> }}%</span>
</p> </p>
</div> </div>
@@ -119,7 +119,7 @@ function barWidth(views: number) {
</div> </div>
</div> </div>
<p class="admin-num mt-4 text-3xl">{{ stats?.active?.toLocaleString() || 0 }}</p> <p class="admin-num mt-4 text-3xl">{{ stats?.active?.toLocaleString() || 0 }}</p>
<p class="mt-1 text-xs text-zinc-500">{{ percentOf(stats?.active || 0, stats?.total || 0) }}% tổng phim</p> <p class="mt-1 text-xs text-slate-500">{{ percentOf(stats?.active || 0, stats?.total || 0) }}% tổng phim</p>
</div> </div>
<!-- KPI: Phim bộ --> <!-- KPI: Phim bộ -->
<div class="admin-card p-5"> <div class="admin-card p-5">
@@ -130,7 +130,7 @@ function barWidth(views: number) {
</div> </div>
</div> </div>
<p class="admin-num mt-4 text-3xl">{{ stats?.series?.toLocaleString() || 0 }}</p> <p class="admin-num mt-4 text-3xl">{{ stats?.series?.toLocaleString() || 0 }}</p>
<p class="mt-1 text-xs text-zinc-500">{{ percentOf(stats?.series || 0, stats?.total || 0) }}% tổng phim</p> <p class="mt-1 text-xs text-slate-500">{{ percentOf(stats?.series || 0, stats?.total || 0) }}% tổng phim</p>
</div> </div>
<!-- KPI: Phim lẻ --> <!-- KPI: Phim lẻ -->
<div class="admin-card p-5"> <div class="admin-card p-5">
@@ -141,18 +141,18 @@ function barWidth(views: number) {
</div> </div>
</div> </div>
<p class="admin-num mt-4 text-3xl">{{ stats?.single?.toLocaleString() || 0 }}</p> <p class="admin-num mt-4 text-3xl">{{ stats?.single?.toLocaleString() || 0 }}</p>
<p class="mt-1 text-xs text-zinc-500">{{ percentOf(stats?.single || 0, stats?.total || 0) }}% tổng phim</p> <p class="mt-1 text-xs text-slate-500">{{ percentOf(stats?.single || 0, stats?.total || 0) }}% tổng phim</p>
</div> </div>
<!-- KPI: Tổng lượt xem --> <!-- KPI: Tổng lượt xem -->
<div class="admin-card p-5"> <div class="admin-card p-5">
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<p class="admin-label">Tổng lượt xem</p> <p class="admin-label">Tổng lượt xem</p>
<div class="grid size-8 place-items-center rounded-lg bg-yellow-400/10 text-yellow-400"> <div class="grid size-8 place-items-center rounded-lg bg-sky-50 text-sky-600">
<AppIcon name="bar-chart" class="size-4" /> <AppIcon name="bar-chart" class="size-4" />
</div> </div>
</div> </div>
<p class="admin-num mt-4 text-3xl">{{ stats?.totalViews?.toLocaleString() || 0 }}</p> <p class="admin-num mt-4 text-3xl">{{ stats?.totalViews?.toLocaleString() || 0 }}</p>
<p class="mt-1 text-xs text-zinc-500">trên {{ stats?.total?.toLocaleString() || 0 }} phim</p> <p class="mt-1 text-xs text-slate-500">trên {{ stats?.total?.toLocaleString() || 0 }} phim</p>
</div> </div>
</div> </div>
<!-- Bento: Charts row --> <!-- Bento: Charts row -->
@@ -161,13 +161,13 @@ function barWidth(views: number) {
<div class="admin-card p-5"> <div class="admin-card p-5">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<p class="admin-label">Phân bổ nguồn</p> <p class="admin-label">Phân bổ nguồn</p>
<span class="admin-badge bg-white/5 text-zinc-400">Nguồn</span> <span class="admin-badge bg-slate-200/50 text-slate-400">Nguồn</span>
</div> </div>
<div class="mt-4 flex items-center justify-center"> <div class="mt-4 flex items-center justify-center">
<div class="relative"> <div class="relative">
<svg viewBox="0 0 100 100" class="size-40"> <svg viewBox="0 0 100 100" class="size-40">
<template v-if="stats?.ophim"> <template v-if="stats?.ophim">
<path :d="donutSegment(stats.ophim, stats.total, 0)" fill="#facc15" /> <path :d="donutSegment(stats.ophim, stats.total, 0)" fill="#f59e0b" />
</template> </template>
<template v-if="stats?.nguonc"> <template v-if="stats?.nguonc">
<path :d="donutSegment(stats.nguonc, stats.total, (stats.ophim || 0) / (stats.total || 1) * 360)" <path :d="donutSegment(stats.nguonc, stats.total, (stats.ophim || 0) / (stats.total || 1) * 360)"
@@ -178,27 +178,27 @@ function barWidth(views: number) {
:d="donutSegment(stats.kkphim, stats.total, ((stats.ophim || 0) + (stats.nguonc || 0)) / (stats.total || 1) * 360)" :d="donutSegment(stats.kkphim, stats.total, ((stats.ophim || 0) + (stats.nguonc || 0)) / (stats.total || 1) * 360)"
fill="#60a5fa" /> fill="#60a5fa" />
</template> </template>
<circle cx="50" cy="50" r="22" fill="#131418" /> <circle cx="50" cy="50" r="22" fill="#ffffff" />
</svg> </svg>
<div class="absolute inset-0 grid place-items-center"> <div class="absolute inset-0 grid place-items-center">
<div class="text-center"> <div class="text-center">
<p class="admin-num text-xl">{{ stats?.total?.toLocaleString() || 0 }}</p> <p class="admin-num text-xl">{{ stats?.total?.toLocaleString() || 0 }}</p>
<p class="text-[10px] uppercase tracking-widest text-zinc-500">phim</p> <p class="text-[10px] uppercase tracking-widest text-slate-500">phim</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="mt-4 space-y-1.5"> <div class="mt-4 space-y-1.5">
<div v-for="s in [ <div v-for="s in [
{ key: 'ophim', label: 'OPhim', color: 'bg-yellow-400' }, { key: 'ophim', label: 'OPhim', color: 'bg-sky-500' },
{ key: 'nguonc', label: 'NguonC', color: 'bg-emerald-400' }, { key: 'nguonc', label: 'NguonC', color: 'bg-emerald-400' },
{ key: 'kkphim', label: 'KKPhim', color: 'bg-blue-400' }, { key: 'kkphim', label: 'KKPhim', color: 'bg-blue-400' },
]" :key="s.key" class="flex items-center justify-between text-sm"> ]" :key="s.key" class="flex items-center justify-between text-sm">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="size-2 rounded-full" :class="s.color" /> <span class="size-2 rounded-full" :class="s.color" />
<span class="text-zinc-400">{{ s.label }}</span> <span class="text-slate-500">{{ s.label }}</span>
</div> </div>
<span class="font-semibold text-zinc-200">{{ stats?.[s.key] || 0 }}</span> <span class="font-semibold text-slate-700">{{ stats?.[s.key] || 0 }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -206,7 +206,7 @@ function barWidth(views: number) {
<div class="admin-card p-5"> <div class="admin-card p-5">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<p class="admin-label">Bộ vs Lẻ</p> <p class="admin-label">Bộ vs Lẻ</p>
<span class="admin-badge bg-white/5 text-zinc-400">Loại</span> <span class="admin-badge bg-slate-200/50 text-slate-400">Loại</span>
</div> </div>
<div class="mt-4 flex items-center justify-center"> <div class="mt-4 flex items-center justify-center">
<div class="relative"> <div class="relative">
@@ -218,12 +218,12 @@ function barWidth(views: number) {
<path :d="donutSegment(stats.single, stats.total, (stats.series || 0) / (stats.total || 1) * 360)" <path :d="donutSegment(stats.single, stats.total, (stats.series || 0) / (stats.total || 1) * 360)"
fill="#a855f7" /> fill="#a855f7" />
</template> </template>
<circle cx="50" cy="50" r="22" fill="#131418" /> <circle cx="50" cy="50" r="22" fill="#ffffff" />
</svg> </svg>
<div class="absolute inset-0 grid place-items-center"> <div class="absolute inset-0 grid place-items-center">
<div class="text-center"> <div class="text-center">
<p class="admin-num text-xl">{{ stats?.total?.toLocaleString() || 0 }}</p> <p class="admin-num text-xl">{{ stats?.total?.toLocaleString() || 0 }}</p>
<p class="text-[10px] uppercase tracking-widest text-zinc-500">phim</p> <p class="text-[10px] uppercase tracking-widest text-slate-500">phim</p>
</div> </div>
</div> </div>
</div> </div>
@@ -232,16 +232,16 @@ function barWidth(views: number) {
<div class="flex items-center justify-between text-sm"> <div class="flex items-center justify-between text-sm">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="size-2 rounded-full bg-blue-500" /> <span class="size-2 rounded-full bg-blue-500" />
<span class="text-zinc-400">Phim bộ</span> <span class="text-slate-500">Phim bộ</span>
</div> </div>
<span class="font-semibold text-zinc-200">{{ stats?.series || 0 }}</span> <span class="font-semibold text-slate-700">{{ stats?.series || 0 }}</span>
</div> </div>
<div class="flex items-center justify-between text-sm"> <div class="flex items-center justify-between text-sm">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="size-2 rounded-full bg-purple-500" /> <span class="size-2 rounded-full bg-purple-500" />
<span class="text-zinc-400">Phim lẻ</span> <span class="text-slate-500">Phim lẻ</span>
</div> </div>
<span class="font-semibold text-zinc-200">{{ stats?.single || 0 }}</span> <span class="font-semibold text-slate-700">{{ stats?.single || 0 }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -249,28 +249,28 @@ function barWidth(views: number) {
<div class="admin-card p-5 sm:col-span-2 lg:col-span-2"> <div class="admin-card p-5 sm:col-span-2 lg:col-span-2">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<p class="admin-label">Top phim xem nhiều</p> <p class="admin-label">Top phim xem nhiều</p>
<span class="admin-badge bg-yellow-400/10 text-yellow-400">Top 5</span> <span class="admin-badge bg-sky-50 text-sky-600">Top 5</span>
</div> </div>
<div class="mt-4 space-y-3.5"> <div class="mt-4 space-y-3.5">
<div v-for="(movie, i) in stats?.topMovies || []" :key="movie.slug"> <div v-for="(movie, i) in stats?.topMovies || []" :key="movie.slug">
<div class="mb-1.5 flex items-center justify-between gap-3"> <div class="mb-1.5 flex items-center justify-between gap-3">
<div class="flex min-w-0 items-center gap-2.5"> <div class="flex min-w-0 items-center gap-2.5">
<span class="flex size-5 shrink-0 items-center justify-center rounded-md text-[10px] font-bold" <span class="flex size-5 shrink-0 items-center justify-center rounded-md text-[10px] font-bold"
:class="i === 0 ? 'bg-yellow-400 text-zinc-950' : 'bg-white/6 text-zinc-400'"> :class="i === 0 ? 'bg-sky-500 text-slate-900' : 'bg-slate-100/60 text-slate-400'">
{{ i + 1 }} {{ i + 1 }}
</span> </span>
<span class="truncate text-sm text-zinc-100">{{ movie.name }}</span> <span class="truncate text-sm text-slate-800">{{ movie.name }}</span>
</div> </div>
<span class="shrink-0 text-xs font-semibold text-zinc-400 tabular-nums">{{ movie.views?.toLocaleString() <span class="shrink-0 text-xs font-semibold text-slate-400 tabular-nums">{{ movie.views?.toLocaleString()
}}</span> }}</span>
</div> </div>
<div class="h-1.5 overflow-hidden rounded-full bg-white/4"> <div class="h-1.5 overflow-hidden rounded-full bg-slate-100">
<div class="h-full rounded-full transition-all duration-700" <div class="h-full rounded-full transition-all duration-700"
:class="i === 0 ? 'bg-yellow-400' : 'bg-zinc-500'" :class="i === 0 ? 'bg-sky-500' : 'bg-zinc-500'"
:style="{ width: barWidth(movie.views || 0) + '%' }" /> :style="{ width: barWidth(movie.views || 0) + '%' }" />
</div> </div>
</div> </div>
<p v-if="!stats?.topMovies?.length" class="py-6 text-center text-sm text-zinc-500">Chưa có dữ liệu</p> <p v-if="!stats?.topMovies?.length" class="py-6 text-center text-sm text-slate-500">Chưa có dữ liệu</p>
</div> </div>
</div> </div>
</div> </div>
@@ -278,27 +278,27 @@ function barWidth(views: number) {
<div class="admin-card p-6"> <div class="admin-card p-6">
<div class="mb-5 flex items-center justify-between"> <div class="mb-5 flex items-center justify-between">
<p class="admin-label">Trạng thái hiển thị</p> <p class="admin-label">Trạng thái hiển thị</p>
<span class="admin-badge bg-white/5 text-zinc-400">{{ stats?.total?.toLocaleString() || 0 }} phim</span> <span class="admin-badge bg-slate-200/50 text-slate-400">{{ stats?.total?.toLocaleString() || 0 }} phim</span>
</div> </div>
<div class="grid gap-6 md:grid-cols-2"> <div class="grid gap-6 md:grid-cols-2">
<div> <div>
<div class="mb-2 flex items-center justify-between text-sm"> <div class="mb-2 flex items-center justify-between text-sm">
<span class="text-zinc-400">Đang hiển thị</span> <span class="text-slate-500">Đang hiển thị</span>
<span class="font-semibold text-emerald-400 tabular-nums">{{ stats?.active?.toLocaleString() || 0 }} · {{ <span class="font-semibold text-emerald-400 tabular-nums">{{ stats?.active?.toLocaleString() || 0 }} · {{
percentOf(stats?.active || 0, stats?.total || 0) }}%</span> percentOf(stats?.active || 0, stats?.total || 0) }}%</span>
</div> </div>
<div class="h-2.5 overflow-hidden rounded-full bg-white/4"> <div class="h-2.5 overflow-hidden rounded-full bg-slate-100">
<div class="h-full rounded-full bg-linear-to-r from-emerald-500 to-emerald-400 transition-all duration-700" <div class="h-full rounded-full bg-linear-to-r from-emerald-500 to-emerald-400 transition-all duration-700"
:style="{ width: percentOf(stats?.active || 0, stats?.total || 0) + '%' }" /> :style="{ width: percentOf(stats?.active || 0, stats?.total || 0) + '%' }" />
</div> </div>
</div> </div>
<div> <div>
<div class="mb-2 flex items-center justify-between text-sm"> <div class="mb-2 flex items-center justify-between text-sm">
<span class="text-zinc-400">Chưa hiển thị</span> <span class="text-slate-500">Chưa hiển thị</span>
<span class="font-semibold text-zinc-400 tabular-nums">{{ stats?.inactive?.toLocaleString() || 0 }} · {{ <span class="font-semibold text-slate-400 tabular-nums">{{ stats?.inactive?.toLocaleString() || 0 }} · {{
percentOf(stats?.inactive || 0, stats?.total || 0) }}%</span> percentOf(stats?.inactive || 0, stats?.total || 0) }}%</span>
</div> </div>
<div class="h-2.5 overflow-hidden rounded-full bg-white/4"> <div class="h-2.5 overflow-hidden rounded-full bg-slate-100">
<div class="h-full rounded-full bg-zinc-600 transition-all duration-700" <div class="h-full rounded-full bg-zinc-600 transition-all duration-700"
:style="{ width: percentOf(stats?.inactive || 0, stats?.total || 0) + '%' }" /> :style="{ width: percentOf(stats?.inactive || 0, stats?.total || 0) + '%' }" />
</div> </div>
+2 -2
View File
@@ -50,12 +50,12 @@ async function handleLogin() {
class="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(250,204,21,0.08),transparent_40%),radial-gradient(circle_at_bottom_left,rgba(59,130,246,0.05),transparent_40%)]" /> class="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(250,204,21,0.08),transparent_40%),radial-gradient(circle_at_bottom_left,rgba(59,130,246,0.05),transparent_40%)]" />
<div class="relative w-full max-w-md"> <div class="relative w-full max-w-md">
<div class="rounded-3xl border border-white/6 bg-[#131418]/80 p-1 shadow-2xl backdrop-blur-sm"> <div class="rounded-3xl border border-slate-200 bg-white/90 p-1 shadow-2xl backdrop-blur-sm">
<div class="rounded-[22px] bg-linear-to-b from-white/8 to-transparent p-6"> <div class="rounded-[22px] bg-linear-to-b from-white/8 to-transparent p-6">
<div class="mb-8 text-center"> <div class="mb-8 text-center">
<div class="mb-4 inline-flex items-center gap-3"> <div class="mb-4 inline-flex items-center gap-3">
<div <div
class="grid size-12 place-items-center rounded-2xl bg-linear-to-br from-yellow-400 to-amber-500 shadow-lg shadow-yellow-400/20"> class="grid size-12 place-items-center rounded-2xl bg-linear-to-br from-sky-400 to-cyan-600 shadow-lg shadow-sky-500/20">
<span class="text-xl font-black text-slate-950">C</span> <span class="text-xl font-black text-slate-950">C</span>
</div> </div>
<div class="text-left"> <div class="text-left">
+66 -66
View File
@@ -277,13 +277,13 @@ function useApiActors() {
<template> <template>
<div class="space-y-5"> <div class="space-y-5">
<NuxtLink to="/admin/phim" <NuxtLink to="/admin/phim"
class="inline-flex items-center gap-2 text-sm font-medium text-zinc-400 transition hover:text-white"> class="inline-flex items-center gap-2 text-sm font-medium text-slate-400 transition hover:text-white">
<AppIcon name="arrow-left" class="size-4" /> <AppIcon name="arrow-left" class="size-4" />
Quay lại danh sách phim Quay lại danh sách phim
</NuxtLink> </NuxtLink>
<div v-if="!movie" class="admin-card p-12 text-center"> <div v-if="!movie" class="admin-card p-12 text-center">
<p class="text-sm text-zinc-500">Đang tải...</p> <p class="text-sm text-slate-500">Đang tải...</p>
</div> </div>
<div v-else class="space-y-5"> <div v-else class="space-y-5">
@@ -295,7 +295,7 @@ function useApiActors() {
class="aspect-video w-full overflow-hidden rounded-xl border border-white/6 bg-white/2 sm:w-44"> class="aspect-video w-full overflow-hidden rounded-xl border border-white/6 bg-white/2 sm:w-44">
<img v-if="customPoster || movie.poster" :src="customPoster || movie.poster || undefined" :alt="movie.name" <img v-if="customPoster || movie.poster" :src="customPoster || movie.poster || undefined" :alt="movie.name"
class="h-full w-full object-cover"> class="h-full w-full object-cover">
<div v-else class="grid h-full place-items-center text-zinc-600"> <div v-else class="grid h-full place-items-center text-slate-600">
<AppIcon name="image" class="size-6" /> <AppIcon name="image" class="size-6" />
</div> </div>
</div> </div>
@@ -307,11 +307,11 @@ function useApiActors() {
</p> </p>
<div class="mt-3 flex flex-wrap items-center gap-2"> <div class="mt-3 flex flex-wrap items-center gap-2">
<span class="admin-badge" <span class="admin-badge"
:class="movie.active ? 'bg-emerald-400/10 text-emerald-400' : 'bg-zinc-400/10 text-zinc-400'"> :class="movie.active ? 'bg-emerald-400/10 text-emerald-400' : 'bg-zinc-400/10 text-slate-400'">
{{ movie.active ? 'Đang hiển thị' : 'Ẩn' }} {{ movie.active ? 'Đang hiển thị' : 'Ẩn' }}
</span> </span>
<span v-if="movie.quality" class="admin-badge bg-white/6 text-zinc-400">{{ movie.quality }}</span> <span v-if="movie.quality" class="admin-badge bg-slate-200/50 text-slate-400">{{ movie.quality }}</span>
<span class="admin-badge bg-white/6 text-zinc-400">{{ movie.episodeTotal || '—' }} tập</span> <span class="admin-badge bg-slate-200/50 text-slate-400">{{ movie.episodeTotal || '—' }} tập</span>
</div> </div>
</div> </div>
</div> </div>
@@ -335,15 +335,15 @@ function useApiActors() {
<button v-for="s in sections" :key="s.key" type="button" <button v-for="s in sections" :key="s.key" type="button"
class="relative flex shrink-0 items-center gap-2 rounded-t-lg px-4 py-3 text-sm font-semibold transition" class="relative flex shrink-0 items-center gap-2 rounded-t-lg px-4 py-3 text-sm font-semibold transition"
:class="activeSection === s.key :class="activeSection === s.key
? 'bg-white/6 text-yellow-400 after:absolute after:inset-x-3 after:bottom-0 after:h-0.5 after:rounded-full after:bg-yellow-400' ? 'bg-slate-100/60 text-sky-600 after:absolute after:inset-x-3 after:bottom-0 after:h-0.5 after:rounded-full after:bg-sky-500'
: 'text-zinc-400 hover:bg-white/3 hover:text-white'" @click="activeSection = s.key"> : 'text-slate-500 hover:bg-slate-100 hover:text-white'" @click="activeSection = s.key">
<AppIcon :name="s.icon" class="size-4" /> <AppIcon :name="s.icon" class="size-4" />
<span>{{ s.label }}</span> <span>{{ s.label }}</span>
<span v-if="s.key === 'actors' && customActors.length" <span v-if="s.key === 'actors' && customActors.length"
class="rounded-full bg-white/8 px-2 py-0.5 text-[10px] text-zinc-300">{{ customActors.length class="rounded-full bg-slate-200/50 px-2 py-0.5 text-[10px] text-slate-600">{{ customActors.length
}}</span> }}</span>
<span v-else-if="s.key === 'episodes' && customServers.length" <span v-else-if="s.key === 'episodes' && customServers.length"
class="rounded-full bg-white/8 px-2 py-0.5 text-[10px] text-zinc-300">{{customServers.reduce((a, class="rounded-full bg-slate-200/50 px-2 py-0.5 text-[10px] text-slate-600">{{customServers.reduce((a,
b) => a + b.episodes.length, 0)}}</span> b) => a + b.episodes.length, 0)}}</span>
</button> </button>
</nav> </nav>
@@ -356,41 +356,41 @@ function useApiActors() {
<AppIcon name="info" class="size-4" /> <AppIcon name="info" class="size-4" />
</div> </div>
<div> <div>
<h2 class="text-sm font-bold text-white">Thông tin chi tiết</h2> <h2 class="text-sm font-bold text-slate-900">Thông tin chi tiết</h2>
<p class="text-xs text-zinc-500">Dữ liệu hiện tại của phim</p> <p class="text-xs text-slate-500">Dữ liệu hiện tại của phim</p>
</div> </div>
</div> </div>
<dl class="grid gap-4 sm:grid-cols-2"> <dl class="grid gap-4 sm:grid-cols-2">
<div class="rounded-xl border border-white/6 bg-white/2 p-4"> <div class="rounded-xl border border-white/6 bg-white/2 p-4">
<dt class="admin-label mb-1">Nguồn</dt> <dt class="admin-label mb-1">Nguồn</dt>
<dd class="font-semibold text-white">{{ movie.source?.toUpperCase() }}</dd> <dd class="font-semibold text-slate-900">{{ movie.source?.toUpperCase() }}</dd>
</div> </div>
<div class="rounded-xl border border-white/6 bg-white/2 p-4"> <div class="rounded-xl border border-white/6 bg-white/2 p-4">
<dt class="admin-label mb-1">Slug</dt> <dt class="admin-label mb-1">Slug</dt>
<dd class="truncate text-zinc-200" :title="movie.slug">{{ movie.slug }}</dd> <dd class="truncate text-slate-700" :title="movie.slug">{{ movie.slug }}</dd>
</div> </div>
<div class="rounded-xl border border-white/6 bg-white/2 p-4"> <div class="rounded-xl border border-white/6 bg-white/2 p-4">
<dt class="admin-label mb-1">Năm phát hành</dt> <dt class="admin-label mb-1">Năm phát hành</dt>
<dd class="text-zinc-200">{{ movie.year || '—' }}</dd> <dd class="text-slate-700">{{ movie.year || '—' }}</dd>
</div> </div>
<div class="rounded-xl border border-white/6 bg-white/2 p-4"> <div class="rounded-xl border border-white/6 bg-white/2 p-4">
<dt class="admin-label mb-1">Số tập</dt> <dt class="admin-label mb-1">Số tập</dt>
<dd class="text-zinc-200">{{ movie.episodeTotal || '—' }}</dd> <dd class="text-slate-700">{{ movie.episodeTotal || '—' }}</dd>
</div> </div>
<div class="rounded-xl border border-white/6 bg-white/2 p-4"> <div class="rounded-xl border border-white/6 bg-white/2 p-4">
<dt class="admin-label mb-1">Chất lượng</dt> <dt class="admin-label mb-1">Chất lượng</dt>
<dd class="text-zinc-200">{{ movie.quality || '—' }}</dd> <dd class="text-slate-700">{{ movie.quality || '—' }}</dd>
</div> </div>
<div class="rounded-xl border border-white/6 bg-white/2 p-4"> <div class="rounded-xl border border-white/6 bg-white/2 p-4">
<dt class="admin-label mb-1">Trạng thái</dt> <dt class="admin-label mb-1">Trạng thái</dt>
<dd class="font-semibold" :class="movie.active ? 'text-emerald-400' : 'text-zinc-400'">{{ <dd class="font-semibold" :class="movie.active ? 'text-emerald-400' : 'text-slate-500'">{{
movie.active ? 'Đang hiển thị' : 'Ẩn' }}</dd> movie.active ? 'Đang hiển thị' : 'Ẩn' }}</dd>
</div> </div>
</dl> </dl>
<div v-if="movie.categories?.length" class="mt-5 border-t border-white/6 pt-4"> <div v-if="movie.categories?.length" class="mt-5 border-t border-white/6 pt-4">
<h4 class="admin-label mb-2">Thể loại</h4> <h4 class="admin-label mb-2">Thể loại</h4>
<div class="flex flex-wrap gap-2"><span v-for="cat in movie.categories" :key="cat" <div class="flex flex-wrap gap-2"><span v-for="cat in movie.categories" :key="cat"
class="rounded-full bg-white/6 px-3 py-1 text-xs font-medium text-zinc-300">{{ cat }}</span> class="rounded-full bg-slate-200/50 px-3 py-1 text-xs font-medium text-slate-600">{{ cat }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -403,20 +403,20 @@ function useApiActors() {
<AppIcon name="type" class="size-4" /> <AppIcon name="type" class="size-4" />
</div> </div>
<div> <div>
<h2 class="text-sm font-bold text-white"> tả phim</h2> <h2 class="text-sm font-bold text-slate-900"> tả phim</h2>
<p class="text-xs text-zinc-500">{{ customContent ? 'Nội dung tuỳ chỉnh' : 'Nội dung từ API' }} <p class="text-xs text-slate-500">{{ customContent ? 'Nội dung tuỳ chỉnh' : 'Nội dung từ API' }}
</p> </p>
</div> </div>
</div> </div>
<button v-if="apiContent && !customContent" type="button" <button v-if="apiContent && !customContent" type="button"
class="admin-btn-sm bg-yellow-400/10 text-yellow-400 hover:bg-yellow-400/20" class="admin-btn-sm bg-sky-50 text-sky-600 hover:bg-sky-500/20"
@click="useApiContent">Dùng tả API</button> @click="useApiContent">Dùng tả API</button>
<button v-else-if="customContent" type="button" class="admin-btn-sm text-red-400 hover:bg-red-400/10" <button v-else-if="customContent" type="button" class="admin-btn-sm text-red-400 hover:bg-red-400/10"
@click="clearField('customContent')">Về mặc định</button> @click="clearField('customContent')">Về mặc định</button>
</div> </div>
<textarea v-model="customContent" :rows="contentRows" :placeholder="apiContent || 'Nhập mô tả phim...'" <textarea v-model="customContent" :rows="contentRows" :placeholder="apiContent || 'Nhập mô tả phim...'"
style="field-sizing: content; min-height: 320px;" style="field-sizing: content; min-height: 320px;"
class="w-full rounded-xl border border-white/8 bg-white/3 p-4 text-sm leading-7 text-white outline-none transition placeholder:text-zinc-500 focus:border-yellow-400/40 focus:bg-white/5" /> class="w-full rounded-xl border border-slate-200 bg-slate-100 p-4 text-sm leading-7 text-slate-900 outline-none transition placeholder:text-slate-400 focus:border-sky-500 focus:bg-slate-100/60" />
</div> </div>
<!-- Section: Diễn viên --> <!-- Section: Diễn viên -->
@@ -427,23 +427,23 @@ function useApiActors() {
<AppIcon name="users" class="size-4" /> <AppIcon name="users" class="size-4" />
</div> </div>
<div> <div>
<h2 class="text-sm font-bold text-white">Diễn viên</h2> <h2 class="text-sm font-bold text-slate-900">Diễn viên</h2>
<p class="text-xs text-zinc-500">{{ customActors.length }} diễn viên</p> <p class="text-xs text-slate-500">{{ customActors.length }} diễn viên</p>
</div> </div>
</div> </div>
<div class="flex gap-2"><button v-if="apiActors.length" type="button" <div class="flex gap-2"><button v-if="apiActors.length" type="button"
class="admin-btn-sm bg-yellow-400/10 text-yellow-400 hover:bg-yellow-400/20" class="admin-btn-sm bg-sky-50 text-sky-600 hover:bg-sky-500/20"
@click="useApiActors">Import ({{ apiActors.length }})</button><button type="button" @click="useApiActors">Import ({{ apiActors.length }})</button><button type="button"
class="admin-btn-sm bg-white/6 text-zinc-100 hover:bg-white/10" @click="addActor"> class="admin-btn-sm bg-slate-200/50 text-slate-800 hover:bg-white/10" @click="addActor">
<AppIcon name="plus" class="size-3" />Thêm <AppIcon name="plus" class="size-3" />Thêm
</button></div> </button></div>
</div> </div>
<div v-if="customActors.length" class="space-y-2"> <div v-if="customActors.length" class="space-y-2">
<div v-for="(actor, index) in customActors" :key="index" <div v-for="(actor, index) in customActors" :key="index"
class="grid gap-2 rounded-xl border border-white/6 bg-white/2 p-3 md:grid-cols-[44px_1fr_1fr_140px_1fr_36px] md:items-center"> class="grid gap-2 rounded-xl border border-white/6 bg-white/2 p-3 md:grid-cols-[44px_1fr_1fr_140px_1fr_36px] md:items-center">
<div class="grid size-10 place-items-center overflow-hidden rounded-full bg-white/6"><img <div class="grid size-10 place-items-center overflow-hidden rounded-full bg-slate-200/50"><img
v-if="actor.avatar" :src="actor.avatar" :alt="actor.name" class="size-full object-cover"><span v-if="actor.avatar" :src="actor.avatar" :alt="actor.name" class="size-full object-cover"><span
v-else class="text-sm font-bold text-zinc-400">{{ (actor.name?.[0] || '?').toUpperCase() }}</span> v-else class="text-sm font-bold text-slate-400">{{ (actor.name?.[0] || '?').toUpperCase() }}</span>
</div> </div>
<input v-model="actor.name" type="text" placeholder="Tên" class="admin-input-sm"><input <input v-model="actor.name" type="text" placeholder="Tên" class="admin-input-sm"><input
v-model="actor.originalName" type="text" placeholder="Tên gốc" class="admin-input-sm"><input v-model="actor.originalName" type="text" placeholder="Tên gốc" class="admin-input-sm"><input
@@ -456,7 +456,7 @@ function useApiActors() {
</div> </div>
</div> </div>
<div v-else <div v-else
class="rounded-xl border border-dashed border-white/10 p-10 text-center text-sm text-zinc-500">Chưa class="rounded-xl border border-dashed border-white/10 p-10 text-center text-sm text-slate-500">Chưa
diễn viên.</div> diễn viên.</div>
</div> </div>
@@ -483,9 +483,9 @@ function useApiActors() {
<div class="space-y-3 2xl:col-span-7"> <div class="space-y-3 2xl:col-span-7">
<div class="flex items-center justify-between gap-3"> <div class="flex items-center justify-between gap-3">
<div> <div>
<h3 class="text-sm font-bold text-white">Server & tập phim</h3> <h3 class="text-sm font-bold text-slate-900">Server & tập phim</h3>
<p class="text-xs text-zinc-500">Danh sách hiển thị trên website</p> <p class="text-xs text-slate-500">Danh sách hiển thị trên website</p>
</div><button type="button" class="admin-btn-sm bg-white/6 text-zinc-100 hover:bg-white/10" </div><button type="button" class="admin-btn-sm bg-slate-200/50 text-slate-800 hover:bg-white/10"
@click="addServer"> @click="addServer">
<AppIcon name="plus" class="size-3" />Thêm server <AppIcon name="plus" class="size-3" />Thêm server
</button> </button>
@@ -494,10 +494,10 @@ function useApiActors() {
class="rounded-xl border border-white/6 bg-white/2"> class="rounded-xl border border-white/6 bg-white/2">
<div class="flex items-center gap-2 p-3"><button type="button" <div class="flex items-center gap-2 p-3"><button type="button"
class="flex min-w-0 flex-1 items-center gap-2 text-left" @click="toggleServer(serverIndex)"> class="flex min-w-0 flex-1 items-center gap-2 text-left" @click="toggleServer(serverIndex)">
<AppIcon name="chevron-down" class="size-4 text-zinc-400 transition" <AppIcon name="chevron-down" class="size-4 text-slate-400 transition"
:class="collapsedServers[serverIndex] ? '-rotate-90' : ''" /><span :class="collapsedServers[serverIndex] ? '-rotate-90' : ''" /><span
class="truncate text-sm font-bold text-white">{{ server.name || `Server ${serverIndex + 1}` class="truncate text-sm font-bold text-slate-900">{{ server.name || `Server ${serverIndex + 1}`
}}</span><span class="rounded-full bg-white/8 px-2 py-0.5 text-[10px] text-zinc-300">{{ }}</span><span class="rounded-full bg-slate-200/50 px-2 py-0.5 text-[10px] text-slate-600">{{
server.episodes.length }} tập</span> server.episodes.length }} tập</span>
</button><button type="button" </button><button type="button"
class="grid size-8 place-items-center rounded-lg text-red-400 hover:bg-red-400/10" class="grid size-8 place-items-center rounded-lg text-red-400 hover:bg-red-400/10"
@@ -512,11 +512,11 @@ function useApiActors() {
<div class="flex gap-2"><input v-model="ep.name" type="text" placeholder="Tên tập" <div class="flex gap-2"><input v-model="ep.name" type="text" placeholder="Tên tập"
class="admin-input-sm w-24"><select class="admin-input-sm min-w-0 flex-1" class="admin-input-sm w-24"><select class="admin-input-sm min-w-0 flex-1"
@change="handleSourceLinkChange(serverIndex, episodeIndex, $event)"> @change="handleSourceLinkChange(serverIndex, episodeIndex, $event)">
<option value="" class="bg-[#131418]">Chọn link từ API</option> <option value="" class="bg-white">Chọn link từ API</option>
<option v-for="item in availableEpisodes" :key="item.key" :value="item.key" <option v-for="item in availableEpisodes" :key="item.key" :value="item.key"
class="bg-[#131418]">{{ item.label }}</option> class="bg-white">{{ item.label }}</option>
</select><button type="button" </select><button type="button"
class="grid size-8 place-items-center rounded-lg text-zinc-400 hover:bg-white/6" class="grid size-8 place-items-center rounded-lg text-slate-400 hover:bg-slate-100/60"
@click="toggleLinks(`${serverIndex}-${episodeIndex}`)"> @click="toggleLinks(`${serverIndex}-${episodeIndex}`)">
<AppIcon name="pencil" class="size-3" /> <AppIcon name="pencil" class="size-3" />
</button><button type="button" </button><button type="button"
@@ -529,42 +529,42 @@ function useApiActors() {
placeholder="Link embed" class="admin-input-sm"><input v-model="ep.linkM3u8" type="url" placeholder="Link embed" class="admin-input-sm"><input v-model="ep.linkM3u8" type="url"
placeholder="Link m3u8" class="admin-input-sm"></div> placeholder="Link m3u8" class="admin-input-sm"></div>
</div><button type="button" </div><button type="button"
class="admin-btn-sm border border-dashed border-white/15 text-zinc-300" class="admin-btn-sm border border-dashed border-white/15 text-slate-600"
@click="addServerEpisode(serverIndex)"> @click="addServerEpisode(serverIndex)">
<AppIcon name="plus" class="size-3" />Thêm tập <AppIcon name="plus" class="size-3" />Thêm tập
</button> </button>
</div> </div>
</div> </div>
<div v-if="!customServers.length" <div v-if="!customServers.length"
class="rounded-xl border border-dashed border-white/10 p-8 text-center text-sm text-zinc-500"> class="rounded-xl border border-dashed border-white/10 p-8 text-center text-sm text-slate-500">
Chưa server.</div> Chưa server.</div>
</div> </div>
<div class="2xl:col-span-5"> <div class="2xl:col-span-5">
<div class="rounded-xl border border-white/6 bg-white/2 p-4 2xl:sticky 2xl:top-5"> <div class="rounded-xl border border-white/6 bg-white/2 p-4 2xl:sticky 2xl:top-5">
<div class="mb-3"> <div class="mb-3">
<h3 class="text-sm font-bold text-white">Nguồn API</h3> <h3 class="text-sm font-bold text-slate-900">Nguồn API</h3>
<p class="text-xs text-zinc-500">Thêm nhanh tập vào server</p> <p class="text-xs text-slate-500">Thêm nhanh tập vào server</p>
</div> </div>
<select v-model="selectedTargetServer" class="admin-input-sm mb-3 w-full"> <select v-model="selectedTargetServer" class="admin-input-sm mb-3 w-full">
<option v-for="(server, index) in customServers" :key="index" :value="index" class="bg-[#131418]"> <option v-for="(server, index) in customServers" :key="index" :value="index" class="bg-white">
{{ server.name || `Server ${index + 1}` }}</option> {{ server.name || `Server ${index + 1}` }}</option>
</select> </select>
<div class="space-y-2"> <div class="space-y-2">
<div v-for="(source, sourceIndex) in sourceData?.sources || []" :key="source.source" <div v-for="(source, sourceIndex) in sourceData?.sources || []" :key="source.source"
class="rounded-lg border border-white/6"><button type="button" class="rounded-lg border border-white/6"><button type="button"
class="flex w-full items-center justify-between p-3 text-left" class="flex w-full items-center justify-between p-3 text-left"
@click="toggleSource(source.source)"><span class="text-xs font-bold uppercase text-white">{{ @click="toggleSource(source.source)"><span class="text-xs font-bold uppercase text-slate-900">{{
source.source }} · {{ source.name }}</span> source.source }} · {{ source.name }}</span>
<AppIcon name="chevron-down" class="size-4 text-zinc-400" /> <AppIcon name="chevron-down" class="size-4 text-slate-400" />
</button> </button>
<div v-if="expandedSources[source.source]" class="space-y-3 border-t border-white/6 p-3"> <div v-if="expandedSources[source.source]" class="space-y-3 border-t border-white/6 p-3">
<div v-for="(server, serverIndex) in source.servers" :key="serverIndex"> <div v-for="(server, serverIndex) in source.servers" :key="serverIndex">
<div class="mb-2 flex items-center justify-between"><span class="admin-label">{{ server.name <div class="mb-2 flex items-center justify-between"><span class="admin-label">{{ server.name
}}</span><button type="button" class="text-xs font-semibold text-yellow-400" }}</span><button type="button" class="text-xs font-semibold text-sky-600"
@click="quickAddAllEpisodes(sourceIndex, serverIndex)">Thêm tất cả</button></div> @click="quickAddAllEpisodes(sourceIndex, serverIndex)">Thêm tất cả</button></div>
<div class="flex flex-wrap gap-2"><button v-for="(ep, epIndex) in server.episodes" <div class="flex flex-wrap gap-2"><button v-for="(ep, epIndex) in server.episodes"
:key="epIndex" type="button" :key="epIndex" type="button"
class="rounded-lg bg-white/5 px-2.5 py-1.5 text-xs text-zinc-200 hover:bg-yellow-400/10 hover:text-yellow-400" class="rounded-lg bg-slate-200/50 px-2.5 py-1.5 text-xs text-slate-700 hover:bg-sky-500/10 hover:text-sky-600"
@click="quickAddEpisode(sourceIndex, serverIndex, epIndex)">{{ ep.name || `Tập ${epIndex @click="quickAddEpisode(sourceIndex, serverIndex, epIndex)">{{ ep.name || `Tập ${epIndex
+ 1}` }}</button></div> + 1}` }}</button></div>
</div> </div>
@@ -583,21 +583,21 @@ function useApiActors() {
<div class="admin-card overflow-hidden"> <div class="admin-card overflow-hidden">
<div class="flex items-center justify-between border-b border-white/6 px-4 py-3"> <div class="flex items-center justify-between border-b border-white/6 px-4 py-3">
<div> <div>
<h3 class="text-sm font-bold text-white">Đăng phim</h3> <h3 class="text-sm font-bold text-slate-900">Đăng phim</h3>
<p class="text-xs text-zinc-500">Lưu cập nhật nội dung</p> <p class="text-xs text-slate-500">Lưu cập nhật nội dung</p>
</div><span class="admin-badge" </div><span class="admin-badge"
:class="movie.active ? 'bg-emerald-400/10 text-emerald-400' : 'bg-zinc-400/10 text-zinc-400'">{{ :class="movie.active ? 'bg-emerald-400/10 text-emerald-400' : 'bg-zinc-400/10 text-slate-400'">{{
movie.active ? 'Hiển thị' : 'Đang ẩn' }}</span> movie.active ? 'Hiển thị' : 'Đang ẩn' }}</span>
</div> </div>
<div class="space-y-3 p-4 text-sm"> <div class="space-y-3 p-4 text-sm">
<div class="flex justify-between gap-3"><span class="text-zinc-500">Nguồn</span><strong <div class="flex justify-between gap-3"><span class="text-slate-9000">Nguồn</span><strong
class="text-white">{{ movie.source?.toUpperCase() }}</strong></div> class="text-white">{{ movie.source?.toUpperCase() }}</strong></div>
<div class="flex justify-between gap-3"><span class="text-zinc-500">Năm</span><span <div class="flex justify-between gap-3"><span class="text-slate-9000">Năm</span><span
class="text-zinc-200">{{ movie.year || '—' }}</span></div> class="text-slate-700">{{ movie.year || '—' }}</span></div>
<div class="flex justify-between gap-3"><span class="text-zinc-500">Tổng tập</span><span <div class="flex justify-between gap-3"><span class="text-slate-9000">Tổng tập</span><span
class="text-zinc-200">{{ movie.episodeTotal || '—' }}</span></div> class="text-slate-700">{{ movie.episodeTotal || '—' }}</span></div>
<div class="flex justify-between gap-3"><span class="text-zinc-500">Đồng bộ</span><span <div class="flex justify-between gap-3"><span class="text-slate-9000">Đồng bộ</span><span
class="truncate text-right text-zinc-200">{{ new Date(movie.syncedAt).toLocaleString('vi-VN') class="truncate text-right text-slate-700">{{ new Date(movie.syncedAt).toLocaleString('vi-VN')
}}</span></div> }}</span></div>
</div> </div>
<div class="border-t border-white/6 p-4"><button type="button" <div class="border-t border-white/6 p-4"><button type="button"
@@ -612,14 +612,14 @@ function useApiActors() {
<div class="admin-card overflow-hidden"> <div class="admin-card overflow-hidden">
<div class="border-b border-white/6 px-4 py-3"> <div class="border-b border-white/6 px-4 py-3">
<h3 class="text-sm font-bold text-white">Ảnh đại diện ngang</h3> <h3 class="text-sm font-bold text-slate-900">Ảnh đại diện ngang</h3>
<p class="text-xs text-zinc-500">Poster 16:9</p> <p class="text-xs text-slate-500">Poster 16:9</p>
</div> </div>
<div class="p-4"> <div class="p-4">
<div class="aspect-video overflow-hidden rounded-xl border border-white/8 bg-white/2"><img <div class="aspect-video overflow-hidden rounded-xl border border-slate-200 bg-white/2"><img
v-if="customPoster || movie.poster" :src="customPoster || movie.poster || undefined" :alt="movie.name" v-if="customPoster || movie.poster" :src="customPoster || movie.poster || undefined" :alt="movie.name"
class="size-full object-cover"> class="size-full object-cover">
<div v-else class="grid size-full place-items-center text-zinc-600"> <div v-else class="grid size-full place-items-center text-slate-600">
<AppIcon name="image" class="size-7" /> <AppIcon name="image" class="size-7" />
</div> </div>
</div><input v-model="customPoster" type="url" placeholder="URL ảnh ngang..." </div><input v-model="customPoster" type="url" placeholder="URL ảnh ngang..."
@@ -629,15 +629,15 @@ function useApiActors() {
<div class="admin-card overflow-hidden"> <div class="admin-card overflow-hidden">
<div class="border-b border-white/6 px-4 py-3"> <div class="border-b border-white/6 px-4 py-3">
<h3 class="text-sm font-bold text-white">Ảnh đại diện dọc</h3> <h3 class="text-sm font-bold text-slate-900">Ảnh đại diện dọc</h3>
<p class="text-xs text-zinc-500">Thumbnail 2:3</p> <p class="text-xs text-slate-500">Thumbnail 2:3</p>
</div> </div>
<div class="p-4"> <div class="p-4">
<div <div
class="mx-auto aspect-2/3 w-full max-w-55 overflow-hidden rounded-xl border border-white/8 bg-white/2"> class="mx-auto aspect-2/3 w-full max-w-55 overflow-hidden rounded-xl border border-slate-200 bg-white/2">
<img v-if="customThumb || movie.thumb || movie.poster" :src="customThumb || movie.thumb || movie.poster || undefined" <img v-if="customThumb || movie.thumb || movie.poster" :src="customThumb || movie.thumb || movie.poster || undefined"
:alt="movie.name" class="size-full object-cover"> :alt="movie.name" class="size-full object-cover">
<div v-else class="grid size-full place-items-center text-zinc-600"> <div v-else class="grid size-full place-items-center text-slate-600">
<AppIcon name="image" class="size-7" /> <AppIcon name="image" class="size-7" />
</div> </div>
</div><input v-model="customThumb" type="url" placeholder="URL ảnh dọc..." </div><input v-model="customThumb" type="url" placeholder="URL ảnh dọc..."
+153 -73
View File
@@ -22,6 +22,10 @@ const deleteConfirmOpen = ref(false)
const syncSources = ref({ ophim: true, nguonc: true, kkphim: true }) const syncSources = ref({ ophim: true, nguonc: true, kkphim: true })
const syncResult = ref<{ total: number, created: number, updated: number, sourceStats: Record<string, { fetched: number, error?: string }> } | null>(null) const syncResult = ref<{ total: number, created: number, updated: number, sourceStats: Record<string, { fetched: number, error?: string }> } | null>(null)
// 3-dot menu state
const menuOpen = ref<number | null>(null)
const selectedMovie = ref<any>(null)
const { bust } = useCacheBust() const { bust } = useCacheBust()
let searchTimeout: ReturnType<typeof setTimeout> | undefined let searchTimeout: ReturnType<typeof setTimeout> | undefined
@@ -198,8 +202,8 @@ function formatRelativeTime(dateValue?: string | number | Date | null) {
return 'Vừa xong' return 'Vừa xong'
} }
function sortIcon(column: string): string { function sortIcon(column: string): import('~/lib/icons').IconName {
if (sortBy.value !== column) return 'chevron-down' if (sortBy.value !== column) return 'chevron-down' as const
return sortOrder.value === 'asc' ? 'chevron-up' : 'chevron-down' return sortOrder.value === 'asc' ? 'chevron-up' : 'chevron-down'
} }
@@ -218,7 +222,7 @@ const syncSourceOptions = [
<p class="admin-section-subtitle mt-1">Quản phim hiển thị trên CineK ({{ data?.total || 0 }} phim)</p> <p class="admin-section-subtitle mt-1">Quản phim hiển thị trên CineK ({{ data?.total || 0 }} phim)</p>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button type="button" class="admin-btn-danger" @click="deleteConfirmOpen = true"> <button type="button" class="admin-btn-danger border" @click="deleteConfirmOpen = true">
<AppIcon name="trash" class="size-4" /> <AppIcon name="trash" class="size-4" />
Xoá tất cả Xoá tất cả
</button> </button>
@@ -230,23 +234,32 @@ const syncSourceOptions = [
</div> </div>
<div class="admin-card overflow-hidden"> <div class="admin-card overflow-hidden">
<div class="flex flex-col gap-3 border-b border-white/[0.06] p-4 sm:flex-row sm:items-center"> <div class="grid grid-cols-1 gap-3 border-b border-white/6 p-4
<div class="relative flex-1"> md:grid-cols-2
<AppIcon name="search" class="absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-slate-400" /> xl:grid-cols-[minmax(280px,1fr)_180px_160px_150px]
<input v-model="searchInput" type="search" placeholder="Tìm kiếm phim..." class="admin-input pl-10"> xl:items-center">
<div class="relative min-w-0 w-full">
<AppIcon name="search"
class="pointer-events-none absolute left-3.5 top-1/2 z-10 size-4 -translate-y-1/2 text-slate-400" />
<input v-model="searchInput" type="text" placeholder="Tìm kiếm phim..."
class="admin-input h-10 w-full pl-10! pr-3">
</div> </div>
<select v-model="statusFilter" class="admin-input w-full sm:w-auto">
<select v-model="statusFilter" class="admin-input h-10 w-full min-w-0 px-3">
<option value="" class="bg-[#131418]">Tất cả trạng thái</option> <option value="" class="bg-[#131418]">Tất cả trạng thái</option>
<option value="active" class="bg-[#131418]">Đang hiển thị</option> <option value="active" class="bg-[#131418]">Đang hiển thị</option>
<option value="inactive" class="bg-[#131418]">Chưa hiển thị</option> <option value="inactive" class="bg-[#131418]">Chưa hiển thị</option>
</select> </select>
<select v-model="sourceFilter" class="admin-input w-full sm:w-auto">
<select v-model="sourceFilter" class="admin-input h-10 w-full min-w-0 px-3">
<option value="" class="bg-[#131418]">Tất cả nguồn</option> <option value="" class="bg-[#131418]">Tất cả nguồn</option>
<option value="ophim" class="bg-[#131418]">OPhim</option> <option value="ophim" class="bg-[#131418]">OPhim</option>
<option value="nguonc" class="bg-[#131418]">NguonC</option> <option value="nguonc" class="bg-[#131418]">NguonC</option>
<option value="kkphim" class="bg-[#131418]">KKPhim</option> <option value="kkphim" class="bg-[#131418]">KKPhim</option>
</select> </select>
<select v-model="typeFilter" class="admin-input w-full sm:w-auto">
<select v-model="typeFilter" class="admin-input h-10 w-full min-w-0 px-3">
<option value="" class="bg-[#131418]">Tất cả loại</option> <option value="" class="bg-[#131418]">Tất cả loại</option>
<option value="series" class="bg-[#131418]">Phim bộ</option> <option value="series" class="bg-[#131418]">Phim bộ</option>
<option value="single" class="bg-[#131418]">Phim lẻ</option> <option value="single" class="bg-[#131418]">Phim lẻ</option>
@@ -254,124 +267,190 @@ const syncSourceOptions = [
</div> </div>
<div class="overflow-x-auto admin-scrollbar"> <div class="overflow-x-auto admin-scrollbar">
<table class="w-full"> <table class="w-full" style="border-collapse: collapse; border-spacing: 0;">
<thead> <thead>
<tr class="border-b border-white/[0.06] text-left text-xs font-bold uppercase tracking-wide text-slate-400"> <tr class="text-xs font-bold uppercase tracking-wide text-slate-400">
<th class="w-14 px-5 py-3">STT</th> <th style="border-bottom: 3px solid #eeee;" class="px-4 py-3 text-center">STT</th>
<th class="cursor-pointer select-none px-5 py-3 transition hover:text-white" @click="toggleSort('name')"> <th style="border-bottom: 3px solid #eeee;"
<div class="flex items-center gap-1"> class="cursor-pointer select-none px-4 py-3 text-center transition hover:text-[#333]"
@click="toggleSort('name')">
<div class="flex items-center justify-center gap-1">
Phim Phim
<AppIcon :name="sortIcon('name')" class="size-3" :class="sortBy === 'name' ? 'text-yellow-400' : 'opacity-50'" /> <AppIcon :name="sortIcon('name')" class="size-3"
:class="sortBy === 'name' ? 'text-yellow-400' : 'opacity-50'" />
</div> </div>
</th> </th>
<th class="px-5 py-3">Nguồn</th> <th style="border-bottom: 3px solid #eeee;" class="px-4 py-3 text-center">Nguồn</th>
<th class="px-5 py-3">Tập</th> <th style="border-bottom: 3px solid #eeee;" class="px-4 py-3 text-center">Tập</th>
<th class="cursor-pointer select-none px-5 py-3 transition hover:text-white" @click="toggleSort('views')"> <th style="border-bottom: 3px solid #eeee;"
<div class="flex items-center gap-1"> class="cursor-pointer select-none px-4 py-3 text-center transition hover:text-white"
@click="toggleSort('views')">
<div class="flex items-center justify-center gap-1">
Lượt xem Lượt xem
<AppIcon :name="sortIcon('views')" class="size-3" :class="sortBy === 'views' ? 'text-yellow-400' : 'opacity-50'" /> <AppIcon :name="sortIcon('views')" class="size-3"
:class="sortBy === 'views' ? 'text-yellow-400' : 'opacity-50'" />
</div> </div>
</th> </th>
<th class="cursor-pointer select-none px-5 py-3 transition hover:text-white" @click="toggleSort('apiUpdatedAt')"> <th style="border-bottom: 3px solid #eeee;"
<div class="flex items-center gap-1"> class="cursor-pointer select-none px-4 py-3 text-center transition hover:text-white"
@click="toggleSort('apiUpdatedAt')">
<div class="flex items-center justify-center gap-1">
Cập nhật API Cập nhật API
<AppIcon :name="sortIcon('apiUpdatedAt')" class="size-3" :class="sortBy === 'apiUpdatedAt' ? 'text-yellow-400' : 'opacity-50'" /> <AppIcon :name="sortIcon('apiUpdatedAt')" class="size-3"
:class="sortBy === 'apiUpdatedAt' ? 'text-yellow-400' : 'opacity-50'" />
</div> </div>
</th> </th>
<th class="cursor-pointer select-none px-5 py-3 transition hover:text-white" @click="toggleSort('active')"> <th style="border-bottom: 3px solid #eeee;"
<div class="flex items-center gap-1"> class="cursor-pointer select-none px-4 py-3 text-center transition hover:text-white"
@click="toggleSort('active')">
<div class="flex items-center justify-center gap-1">
Trạng thái Trạng thái
<AppIcon :name="sortIcon('active')" class="size-3" :class="sortBy === 'active' ? 'text-yellow-400' : 'opacity-50'" /> <AppIcon :name="sortIcon('active')" class="size-3"
:class="sortBy === 'active' ? 'text-yellow-400' : 'opacity-50'" />
</div> </div>
</th> </th>
<th class="px-5 py-3">Tuỳ chỉnh</th> <th style="border-bottom: 3px solid #eeee;" class="px-4 py-3 text-center">Tuỳ chỉnh</th>
<th class="px-5 py-3 text-right">Thao tác</th> <th style="border-bottom: 3px solid #eeee;" class="px-4 py-3 text-center">Hiển thị</th>
<th style="border-bottom: 3px solid #eeee;" class="px-4 py-3 text-center">Hành động</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-white/[0.04]"> <tbody class="divide-y divide-[#eeee]">
<tr v-for="movie in movies" :key="movie.id" class="transition hover:bg-white/[0.02]"> <tr v-for="movie in movies" :key="movie.id" class="transition hover:bg-white/2">
<td class="px-5 py-3.5 text-sm font-semibold text-slate-500"> <td class="px-4 py-3.5 text-center text-sm font-semibold text-slate-500">
{{ (currentPage - 1) * 20 + movies.indexOf(movie) + 1 }} {{ (currentPage - 1) * 20 + movies.indexOf(movie) + 1 }}
</td> </td>
<td class="px-5 py-3.5"> <td class="px-4 py-3.5">
<div class="min-w-0"> <div class="min-w-0">
<p class="truncate text-sm font-bold text-white">{{ movie.name }}</p> <p class="truncate text-sm font-bold text-white">{{ movie.name }}</p>
<p class="truncate text-xs text-slate-400">{{ movie.originName || movie.slug }}</p> <p class="truncate text-xs text-slate-400">{{ movie.originName || movie.slug }}</p>
</div> </div>
</td> </td>
<td class="px-5 py-3.5"> <td class="px-4 py-3.5 text-center">
<span class="admin-badge bg-blue-400/10 text-blue-400">{{ (movie.sources || []).length }} nguồn</span> <span class="admin-badge bg-blue-400/10 text-blue-400">
{{ (movie.sources || []).length }} nguồn
</span>
</td> </td>
<td class="px-5 py-3.5 text-sm"> <td class="px-4 py-3.5 text-center text-sm">
<span class="font-bold text-yellow-300" <span class="font-bold text-yellow-500" :title="(movie.sources || []).map(
:title="(movie.sources || []).map((s: any) => `${s.source.toUpperCase()}: ${s.episode || s.episodeTotal || '?'}`).join('\n')"> (s: any) =>
`${s.source.toUpperCase()}: ${s.episode || s.episodeTotal || '?'}`
).join('\n')">
{{ formatEpisode(movie) }} {{ formatEpisode(movie) }}
</span> </span>
</td> </td>
<td class="px-5 py-3.5 text-sm text-slate-300">{{ (movie.views || 0).toLocaleString() }}</td> <td class="px-4 py-3.5 text-center text-sm text-slate-500">
<td class="px-5 py-3.5 text-sm text-slate-300"> {{ (movie.views || 0).toLocaleString() }}
</td>
<td class="px-4 py-3.5 text-center text-sm text-slate-500">
<span :title="formatDate(movie.apiUpdatedAt || movie.syncedAt)"> <span :title="formatDate(movie.apiUpdatedAt || movie.syncedAt)">
{{ formatRelativeTime(movie.apiUpdatedAt || movie.syncedAt) }} {{ formatRelativeTime(movie.apiUpdatedAt || movie.syncedAt) }}
</span> </span>
</td> </td>
<td class="px-5 py-3.5"> <td class="px-4 py-3.5 text-center">
<span class="admin-badge" :class="movie.active ? 'bg-emerald-400/10 text-emerald-400' : 'bg-slate-400/10 text-slate-400'"> <span class="admin-badge" :class="movie.active
? 'bg-emerald-400/10 text-emerald-400'
: 'bg-slate-400/10 text-slate-400'
">
{{ movie.active ? 'Hiển thị' : 'Ẩn' }} {{ movie.active ? 'Hiển thị' : 'Ẩn' }}
</span> </span>
</td> </td>
<td class="px-5 py-3.5"> <td class="px-4 py-3.5">
<div class="flex flex-wrap items-center gap-1"> <div class="flex flex-wrap items-center justify-center gap-1">
<span v-if="movie.customContent" class="admin-badge bg-blue-400/10 text-blue-400"> tả</span> <span v-if="movie.customContent" class="admin-badge bg-blue-400/10 text-blue-400">
<span v-if="movie.customPoster" class="admin-badge bg-purple-400/10 text-purple-400">Poster</span> tả
<span v-if="movie.customThumb" class="admin-badge bg-pink-400/10 text-pink-400">Thumb</span> </span>
<span v-if="movie.customPoster" class="admin-badge bg-purple-400/10 text-purple-400">
Poster
</span>
<span v-if="movie.customThumb" class="admin-badge bg-pink-400/10 text-pink-400">
Thumb
</span>
<span v-if="movie.customServers?.length" class="admin-badge bg-emerald-400/10 text-emerald-400"> <span v-if="movie.customServers?.length" class="admin-badge bg-emerald-400/10 text-emerald-400">
{{ movie.customServers.length }} server · {{ movie.customServers.reduce((t: number, s: any) => t + (s.episodes?.length || 0), 0) }} tập {{ movie.customServers.length }} server ·
{{
movie.customServers.reduce(
(t: number, s: any) => t + (s.episodes?.length || 0),
0
)
}} tập
</span>
<span v-if="
!movie.customContent &&
!movie.customPoster &&
!movie.customThumb &&
!movie.customServers?.length
" class="text-xs text-slate-500">
Mặc định
</span> </span>
<span v-if="!movie.customContent && !movie.customPoster && !movie.customThumb && !movie.customServers?.length" class="text-xs text-slate-500">Mặc định</span>
</div> </div>
</td> </td>
<td class="px-5 py-3.5"> <td class="px-4 py-3.5">
<div class="flex items-center justify-end gap-2"> <div class="flex items-center justify-center">
<NuxtLink :to="`/admin/phim/${movie.id}`"
class="grid size-8 place-items-center rounded-lg text-slate-400 transition hover:bg-white/[0.05] hover:text-white"
title="Chỉnh sửa">
<AppIcon name="pencil" class="size-4" />
</NuxtLink>
<AdminToggle :model-value="movie.active" <AdminToggle :model-value="movie.active"
@update:model-value="(val: boolean) => { movie.active = val; toggleActive(movie) }" /> @update:model-value="(val: boolean) => { movie.active = val; toggleActive(movie) }" />
</div> </div>
</td> </td>
<td class="px-4 py-3.5 text-center">
<div class="relative inline-flex">
<button type="button" class="grid size-9 place-items-center rounded-lg
text-zinc-500 transition
hover:bg-white/5 hover:text-zinc-300" title="Thao tác"
@click="menuOpen = menuOpen === movie.id ? null : movie.id">
<AppIcon name="ellipsis-vertical" class="size-5 stroke-[2.5]" />
</button>
<Transition name="dropdown-fade">
<div v-if="menuOpen === movie.id" class="absolute right-0 top-full z-50 mt-1 min-w-40
rounded-lg border border-white/8
bg-[#131418] py-1 shadow-xl">
<NuxtLink :to="`/admin/phim/${movie.id}`" class="flex w-full items-center gap-2 px-3 py-2
text-sm text-slate-300 transition
hover:bg-white/5 hover:text-white" @click="menuOpen = null">
<AppIcon name="pencil" class="size-4" />
Chỉnh sửa
</NuxtLink>
<button type="button" class="flex w-full items-center gap-2 px-3 py-2
text-sm text-red-400 transition hover:bg-white/5" @click="
menuOpen = null;
selectedMovie = movie;
deleteConfirmOpen = true
">
<AppIcon name="trash" class="size-4" />
Xoá phim
</button>
</div>
</Transition>
</div>
</td>
</tr> </tr>
<tr v-if="!movies.length"> <tr v-if="!movies.length">
<td colspan="9" class="px-5 py-12 text-center text-sm text-slate-400"> <td colspan="10" class="px-5 py-12 text-center text-sm text-slate-400">
{{ debouncedKeyword ? 'Không tìm thấy phim nào.' : 'Chưa có phim nào. Bấm "Đồng bộ từ API" để lấy phim.' }} {{ debouncedKeyword ? 'Không tìm thấy phim nào.' : 'Chưa có phim nào. Bấm "Đồng bộ từ API" để lấy phim.'
}}
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div v-if="totalPages > 1" class="flex items-center justify-between border-t border-white/[0.06] p-4"> <div v-if="totalPages > 1" class="flex items-center justify-between border-t border-white/6 p-4">
<p class="text-sm text-slate-400">Trang {{ currentPage }} / {{ totalPages }}</p> <p class="text-sm text-slate-400">Trang {{ currentPage }} / {{ totalPages }}</p>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button type="button" <button type="button"
class="grid size-9 place-items-center rounded-lg border border-white/[0.08] text-slate-400 transition hover:bg-white/[0.05] hover:text-white disabled:opacity-30" class="grid size-9 place-items-center rounded-lg border border-white/8 text-slate-400 transition hover:bg-white/5 hover:text-white disabled:opacity-30"
:disabled="currentPage <= 1" @click="currentPage--"> :disabled="currentPage <= 1" @click="currentPage--">
<AppIcon name="chevron-left" class="size-4" /> <AppIcon name="chevron-left" class="size-4" />
</button> </button>
<template v-for="(page, index) in visiblePages" :key="index"> <template v-for="(page, index) in visiblePages" :key="index">
<span v-if="page === 'ellipsis'" class="grid size-9 place-items-center text-sm text-slate-500">...</span> <span v-if="page === 'ellipsis'" class="grid size-9 place-items-center text-sm text-slate-500">...</span>
<button v-else type="button" <button v-else type="button"
class="grid size-9 place-items-center rounded-lg border text-sm font-semibold transition" class="grid size-9 place-items-center rounded-lg border text-sm font-semibold transition" :class="page === currentPage
:class="page === currentPage
? 'border-yellow-400 bg-yellow-400 text-slate-950' ? 'border-yellow-400 bg-yellow-400 text-slate-950'
: 'border-white/[0.08] text-slate-400 hover:bg-white/[0.05] hover:text-white'" : 'border-white/8 text-slate-400 hover:bg-white/5 hover:text-white'" @click="currentPage = page">
@click="currentPage = page">
{{ page }} {{ page }}
</button> </button>
</template> </template>
<button type="button" <button type="button"
class="grid size-9 place-items-center rounded-lg border border-white/[0.08] text-slate-400 transition hover:bg-white/[0.05] hover:text-white disabled:opacity-30" class="grid size-9 place-items-center rounded-lg border border-white/8 text-slate-400 transition hover:bg-white/5 hover:text-white disabled:opacity-30"
:disabled="currentPage >= totalPages" @click="currentPage++"> :disabled="currentPage >= totalPages" @click="currentPage++">
<AppIcon name="chevron-right" class="size-4" /> <AppIcon name="chevron-right" class="size-4" />
</button> </button>
@@ -383,9 +462,9 @@ const syncSourceOptions = [
<Transition name="modal-fade"> <Transition name="modal-fade">
<div v-if="syncOpen" class="fixed inset-0 z-70 grid place-items-center px-3"> <div v-if="syncOpen" class="fixed inset-0 z-70 grid place-items-center px-3">
<div class="absolute inset-0 bg-black/70 backdrop-blur-sm" @click="syncOpen = false" /> <div class="absolute inset-0 bg-black/70 backdrop-blur-sm" @click="syncOpen = false" />
<div class="relative w-full max-w-md rounded-2xl border border-white/[0.08] bg-[#131418] p-6 shadow-2xl"> <div class="relative w-full max-w-md rounded-2xl border border-white/8 bg-[#131418] p-6 shadow-2xl">
<button type="button" <button type="button"
class="absolute right-3 top-3 grid size-8 place-items-center rounded-full text-slate-400 transition hover:bg-white/[0.05] hover:text-white" class="absolute right-3 top-3 grid size-8 place-items-center rounded-full text-slate-400 transition hover:bg-white/5 hover:text-white"
@click="syncOpen = false"> @click="syncOpen = false">
<AppIcon name="x" class="size-5" /> <AppIcon name="x" class="size-5" />
</button> </button>
@@ -397,7 +476,7 @@ const syncSourceOptions = [
<div class="space-y-3"> <div class="space-y-3">
<label v-for="source in syncSourceOptions" :key="source.key" <label v-for="source in syncSourceOptions" :key="source.key"
class="flex cursor-pointer items-center justify-between rounded-xl border border-white/[0.06] bg-white/[0.03] p-4 transition hover:border-white/[0.1] hover:bg-white/[0.05]"> class="flex cursor-pointer items-center justify-between rounded-xl border border-white/6 bg-white/3 p-4 transition hover:border-white/10 hover:bg-white/5">
<div> <div>
<p class="text-sm font-bold text-white">{{ source.label }}</p> <p class="text-sm font-bold text-white">{{ source.label }}</p>
<p class="text-xs text-slate-400">{{ source.domain }}</p> <p class="text-xs text-slate-400">{{ source.domain }}</p>
@@ -406,7 +485,7 @@ const syncSourceOptions = [
</label> </label>
</div> </div>
<div v-if="syncResult" class="mt-4 rounded-xl border border-white/[0.06] bg-white/[0.03] p-4"> <div v-if="syncResult" class="mt-4 rounded-xl border border-white/6 bg-white/3 p-4">
<p class="text-sm font-bold text-white">Kết quả đồng bộ</p> <p class="text-sm font-bold text-white">Kết quả đồng bộ</p>
<div class="mt-2 space-y-1 text-xs text-slate-400"> <div class="mt-2 space-y-1 text-xs text-slate-400">
<p>Tổng phim lấy được: <span class="font-bold text-white">{{ syncResult.total }}</span></p> <p>Tổng phim lấy được: <span class="font-bold text-white">{{ syncResult.total }}</span></p>
@@ -439,18 +518,19 @@ const syncSourceOptions = [
<Transition name="modal-fade"> <Transition name="modal-fade">
<div v-if="deleteConfirmOpen" class="fixed inset-0 z-70 grid place-items-center px-3"> <div v-if="deleteConfirmOpen" class="fixed inset-0 z-70 grid place-items-center px-3">
<div class="absolute inset-0 bg-black/70 backdrop-blur-sm" @click="deleteConfirmOpen = false" /> <div class="absolute inset-0 bg-black/70 backdrop-blur-sm" @click="deleteConfirmOpen = false" />
<div class="relative w-full max-w-sm rounded-2xl border border-white/[0.08] bg-[#131418] p-6 shadow-2xl"> <div class="relative w-full max-w-sm rounded-2xl border border-white/8 bg-[#131418] p-6 shadow-2xl">
<div class="mb-4 grid size-12 place-items-center rounded-full bg-red-500/10"> <div class="mb-4 grid size-12 place-items-center rounded-full bg-red-500/10">
<AppIcon name="trash" class="size-6 text-red-400" /> <AppIcon name="trash" class="size-6 text-red-400" />
</div> </div>
<h2 class="text-xl font-black text-white">Xoá tất cả phim?</h2> <h2 class="text-xl font-black text-white">Xoá tất cả phim?</h2>
<p class="mt-2 text-sm leading-6 text-slate-400"> <p class="mt-2 text-sm leading-6 text-slate-400">
Hành động này sẽ xoá toàn bộ {{ data?.total || 0 }} phim trong hệ thống, bao gồm cả các tuỳ chỉnh. Không thể hoàn tác. Hành động này sẽ xoá toàn bộ {{ data?.total || 0 }} phim trong hệ thống, bao gồm cả các tuỳ chỉnh. Không
thể
hoàn tác.
</p> </p>
<div class="mt-6 flex justify-end gap-3"> <div class="mt-6 flex justify-end gap-3">
<button type="button" class="admin-btn-secondary" @click="deleteConfirmOpen = false">Huỷ</button> <button type="button" class="admin-btn-secondary" @click="deleteConfirmOpen = false">Huỷ</button>
<button type="button" <button type="button" class="admin-btn bg-red-500 text-white hover:bg-red-400 disabled:opacity-50"
class="admin-btn bg-red-500 text-white hover:bg-red-400 disabled:opacity-50"
:disabled="deleting" @click="handleDeleteAll"> :disabled="deleting" @click="handleDeleteAll">
<AppIcon name="trash" class="size-4" /> <AppIcon name="trash" class="size-4" />
{{ deleting ? 'Đang xoá...' : 'Xoá tất cả' }} {{ deleting ? 'Đang xoá...' : 'Xoá tất cả' }}
+13 -10
View File
@@ -65,10 +65,12 @@ const members = computed(() => data.value?.items || [])
<div class="admin-card overflow-hidden"> <div class="admin-card overflow-hidden">
<div class="flex items-center gap-3 border-b border-white/6 p-4"> <div class="flex items-center gap-3 border-b border-white/6 p-4">
<div class="relative flex-1"> <div class="relative flex-1 min-w-0">
<AppIcon name="search" class="absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-slate-400" /> <AppIcon name="search"
<input v-model="searchInput" type="search" placeholder="Tìm kiếm thành viên..." class="pointer-events-none absolute left-3.5 top-1/2 z-10 size-4 -translate-y-1/2 text-slate-400" />
class="admin-input pl-10">
<input v-model="searchInput" type="text" placeholder="Tìm kiếm thành viên..."
class="admin-input h-10 w-full pl-10! pr-3">
</div> </div>
</div> </div>
@@ -89,22 +91,23 @@ const members = computed(() => data.value?.items || [])
<td class="px-5 py-3.5 text-sm font-semibold text-slate-500">{{ members.indexOf(member) + 1 }}</td> <td class="px-5 py-3.5 text-sm font-semibold text-slate-500">{{ members.indexOf(member) + 1 }}</td>
<td class="px-5 py-3.5"> <td class="px-5 py-3.5">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="grid size-9 place-items-center rounded-full bg-linear-to-br from-slate-700 to-slate-800 text-sm font-bold text-slate-300"> <div
class="grid size-9 place-items-center rounded-full bg-linear-to-br from-slate-700 to-slate-800 text-sm font-bold text-slate-300">
{{ (member.name || member.email || '?').charAt(0).toUpperCase() }} {{ (member.name || member.email || '?').charAt(0).toUpperCase() }}
</div> </div>
<div class="min-w-0"> <div class="min-w-0">
<p class="truncate text-sm font-bold text-white">{{ member.name || 'Chưa có tên' }}</p> <p class="truncate text-sm font-bold text-slate-900">{{ member.name || 'Chưa có tên' }}</p>
<p class="truncate text-xs text-slate-400">{{ member.email }}</p> <p class="truncate text-xs text-slate-400">{{ member.email }}</p>
</div> </div>
</div> </div>
</td> </td>
<td class="px-5 py-3.5"> <td class="px-5 py-3.5">
<select :value="member.role" <select :value="member.role"
class="h-8 rounded-full border border-white/8 bg-white/3 px-3 text-xs font-bold text-white outline-none transition hover:bg-white/5 focus:border-yellow-400/50" class="h-8 rounded-full border border-slate-200 bg-slate-100 px-3 text-xs font-bold text-slate-900 outline-none transition hover:bg-slate-100/60 focus:border-sky-500"
@change="changeRole(member, ($event.target as HTMLSelectElement).value)"> @change="changeRole(member, ($event.target as HTMLSelectElement).value)">
<option value="user" class="bg-[#131418]">User</option> <option value="user" class="bg-white">User</option>
<option value="moderator" class="bg-[#131418]">Moderator</option> <option value="moderator" class="bg-white">Moderator</option>
<option value="admin" class="bg-[#131418]">Admin</option> <option value="admin" class="bg-white">Admin</option>
</select> </select>
</td> </td>
<td class="px-5 py-3.5"> <td class="px-5 py-3.5">