mirror of
https://github.com/ngthanhvu/kr-phim.git
synced 2026-08-10 17:47:47 +00:00
feat: cập nhật admin layout
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -12,5 +12,5 @@ const icon = computed(() => getIcon(props.name))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FontAwesomeIcon :icon="icon" :class="props.class" />
|
||||
<FontAwesomeIcon :icon="icon" :class="props.class" :style="{ verticalAlign: 'middle' }" />
|
||||
</template>
|
||||
|
||||
+149
-27
@@ -33,18 +33,16 @@ const displayInitial = computed(() => displayName.value.charAt(0).toUpperCase())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-[#0A0B0E]">
|
||||
<div class="admin-light min-h-screen bg-[#f8fafc]">
|
||||
<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="sidebarOpen ? 'translate-x-0' : '-translate-x-full'">
|
||||
<div class="flex h-16 items-center justify-between border-b border-white/[0.06] px-5">
|
||||
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">
|
||||
<div class="flex h-16 items-center justify-between border-white/20 px-5">
|
||||
<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-4 text-slate-950" />
|
||||
</div>
|
||||
<span class="text-lg font-black text-white">CineK Admin</span>
|
||||
<AppIcon name="film" class="size-5 text-white" />
|
||||
<span class="text-lg font-black text-white tracking-tight">Admin Management</span>
|
||||
</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">
|
||||
<AppIcon name="x" class="size-5" />
|
||||
</button>
|
||||
@@ -52,24 +50,22 @@ const displayInitial = computed(() => displayName.value.charAt(0).toUpperCase())
|
||||
|
||||
<nav class="flex flex-col gap-1 p-3">
|
||||
<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="isActive(item.to)
|
||||
? 'bg-yellow-400/10 text-yellow-400'
|
||||
: '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" />
|
||||
class="group relative flex items-center gap-3 rounded-xl px-4 py-3 text-sm font-semibold transition" :class="isActive(item.to)
|
||||
? 'bg-white/15 text-white font-semibold'
|
||||
: 'text-blue-100 hover:bg-white/10 hover:text-white'">
|
||||
<AppIcon :name="item.icon" class="size-5 transition group-hover:scale-110" />
|
||||
{{ item.label }}
|
||||
</NuxtLink>
|
||||
</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="/"
|
||||
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" />
|
||||
Về trang chủ
|
||||
</NuxtLink>
|
||||
<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">
|
||||
<AppIcon name="log-out" class="size-5" />
|
||||
Đăng xuất
|
||||
@@ -79,24 +75,26 @@ const displayInitial = computed(() => displayName.value.charAt(0).toUpperCase())
|
||||
|
||||
<div class="lg:pl-64">
|
||||
<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">
|
||||
<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"
|
||||
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-slate-100 hover:text-slate-600 lg:hidden"
|
||||
@click="sidebarOpen = true">
|
||||
<AppIcon name="menu" class="size-5" />
|
||||
</button>
|
||||
|
||||
<div class="ml-auto flex items-center gap-4">
|
||||
<div class="hidden items-center gap-2 text-sm text-slate-400 md:flex">
|
||||
<span class="size-2 rounded-full bg-emerald-500 shadow-[0_0_8px] shadow-emerald-500/50" />
|
||||
<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_rgba(16,185,129,0.5)]" />
|
||||
Hệ thống hoạt động
|
||||
</div>
|
||||
<div class="flex items-center gap-3 rounded-2xl border border-white/[0.06] bg-[#131418] 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="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-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 }}
|
||||
</div>
|
||||
<div class="hidden sm:block">
|
||||
<p class="text-sm font-bold text-white">{{ displayName }}</p>
|
||||
<p class="text-xs capitalize text-slate-400">{{ user?.role || 'admin' }}</p>
|
||||
<p class="text-sm font-bold text-slate-900">{{ displayName }}</p>
|
||||
<p class="text-xs capitalize text-slate-500">{{ user?.role || 'admin' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,12 +106,136 @@ const displayInitial = computed(() => displayName.value.charAt(0).toUpperCase())
|
||||
</div>
|
||||
|
||||
<Transition name="sidebar-fade">
|
||||
<div v-if="sidebarOpen" class="fixed inset-0 z-30 bg-black/70 backdrop-blur-sm lg:hidden"
|
||||
@click="closeSidebar" />
|
||||
<div v-if="sidebarOpen" class="fixed inset-0 z-30 bg-black/40 backdrop-blur-sm lg:hidden" @click="closeSidebar" />
|
||||
</Transition>
|
||||
</div>
|
||||
</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>
|
||||
.sidebar-fade-enter-active,
|
||||
.sidebar-fade-leave-active {
|
||||
|
||||
@@ -87,6 +87,7 @@ import {
|
||||
faCommentSlash,
|
||||
faRotate,
|
||||
faPenToSquare,
|
||||
faEllipsisVertical,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
@@ -180,6 +181,7 @@ library.add(
|
||||
faCommentSlash,
|
||||
faRotate,
|
||||
faPenToSquare,
|
||||
faEllipsisVertical,
|
||||
)
|
||||
|
||||
export type IconName =
|
||||
@@ -204,6 +206,7 @@ export type IconName =
|
||||
| 'corner-down-left'
|
||||
| 'crown'
|
||||
| 'edit'
|
||||
| 'ellipsis-vertical'
|
||||
| 'eye'
|
||||
| 'eye-off'
|
||||
| 'fast-forward'
|
||||
@@ -287,6 +290,7 @@ const iconMap: Record<IconName, [string, string]> = {
|
||||
'corner-down-left': ['fas', 'arrow-turn-down'],
|
||||
'crown': ['fas', 'crown'],
|
||||
'edit': ['fas', 'pen-to-square'],
|
||||
'ellipsis-vertical': ['fas', 'ellipsis-vertical'],
|
||||
'eye': ['fas', 'eye'],
|
||||
'eye-off': ['fas', 'eye-slash'],
|
||||
'fast-forward': ['fas', 'forward-fast'],
|
||||
|
||||
+12
-12
@@ -24,44 +24,44 @@ const allowRegistration = ref(true)
|
||||
<div class="space-y-6 lg:col-span-2">
|
||||
<div class="admin-card p-5">
|
||||
<div class="mb-5 flex items-center gap-3">
|
||||
<div class="grid size-11 place-items-center rounded-xl bg-yellow-400/10">
|
||||
<AppIcon name="settings" class="size-5 text-yellow-400" />
|
||||
<div class="grid size-11 place-items-center rounded-xl bg-sky-50">
|
||||
<AppIcon name="settings" class="size-5 text-sky-600" />
|
||||
</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 cơ bản về website</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<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">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-white">Mô tả website</label>
|
||||
<label class="mb-2 block text-sm font-semibold text-slate-900">Mô tả website</label>
|
||||
<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 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">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<AdminToggle v-model="maintenanceMode" />
|
||||
</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>
|
||||
<p class="text-sm font-bold text-white">Cho phép đăng ký</p>
|
||||
<p class="text-sm font-bold text-slate-900">Cho phép đăng ký</p>
|
||||
<p class="mt-1 text-xs text-slate-400">Cho phép người dùng mới đăng ký tài khoản</p>
|
||||
</div>
|
||||
<AdminToggle v-model="allowRegistration" />
|
||||
@@ -79,7 +79,7 @@ const allowRegistration = ref(true)
|
||||
|
||||
<div class="space-y-6">
|
||||
<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">
|
||||
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>
|
||||
|
||||
+42
-42
@@ -30,7 +30,7 @@ const sourceSegments = computed(() => {
|
||||
const kkphim = stats.value?.kkphim || 0
|
||||
if (!total) 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: 'kkphim', label: 'KKPhim', value: kkphim, pct: (kkphim / total) * 100, color: '#60a5fa' },
|
||||
].filter(s => s.value > 0)
|
||||
@@ -58,14 +58,14 @@ function barWidth(views: number) {
|
||||
<!-- 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">
|
||||
<!-- 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="flex items-start justify-between">
|
||||
<div>
|
||||
<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>
|
||||
</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" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,19 +73,19 @@ function barWidth(views: number) {
|
||||
<div class="mt-8">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<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 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"
|
||||
:style="{ width: seg.pct + '%', backgroundColor: seg.color }"
|
||||
:title="`${seg.label}: ${seg.value} (${Math.round(seg.pct)}%)`" />
|
||||
</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 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="text-xs text-zinc-400">{{ seg.label }}</span>
|
||||
<span class="text-xs font-semibold text-zinc-200">{{ seg.value.toLocaleString() }}</span>
|
||||
<span class="text-xs text-slate-400">{{ seg.label }}</span>
|
||||
<span class="text-xs font-semibold text-slate-700">{{ seg.value.toLocaleString() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,8 +102,8 @@ function barWidth(views: number) {
|
||||
<div>
|
||||
<p class="admin-label">Chưa hiển thị</p>
|
||||
<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="text-xs font-medium text-zinc-500">{{ percentOf(stats?.inactive || 0, stats?.total || 0)
|
||||
<span class="admin-num text-xl text-slate-600">{{ stats?.inactive?.toLocaleString() || 0 }}</span>
|
||||
<span class="text-xs font-medium text-slate-500">{{ percentOf(stats?.inactive || 0, stats?.total || 0)
|
||||
}}%</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -119,7 +119,7 @@ function barWidth(views: number) {
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<!-- KPI: Phim bộ -->
|
||||
<div class="admin-card p-5">
|
||||
@@ -130,7 +130,7 @@ function barWidth(views: number) {
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<!-- KPI: Phim lẻ -->
|
||||
<div class="admin-card p-5">
|
||||
@@ -141,18 +141,18 @@ function barWidth(views: number) {
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<!-- KPI: Tổng lượt xem -->
|
||||
<div class="admin-card p-5">
|
||||
<div class="flex items-start justify-between">
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<!-- Bento: Charts row -->
|
||||
@@ -161,13 +161,13 @@ function barWidth(views: number) {
|
||||
<div class="admin-card p-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<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 class="mt-4 flex items-center justify-center">
|
||||
<div class="relative">
|
||||
<svg viewBox="0 0 100 100" class="size-40">
|
||||
<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 v-if="stats?.nguonc">
|
||||
<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)"
|
||||
fill="#60a5fa" />
|
||||
</template>
|
||||
<circle cx="50" cy="50" r="22" fill="#131418" />
|
||||
<circle cx="50" cy="50" r="22" fill="#ffffff" />
|
||||
</svg>
|
||||
<div class="absolute inset-0 grid place-items-center">
|
||||
<div class="text-center">
|
||||
<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 class="mt-4 space-y-1.5">
|
||||
<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: 'kkphim', label: 'KKPhim', color: 'bg-blue-400' },
|
||||
]" :key="s.key" class="flex items-center justify-between text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<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>
|
||||
<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>
|
||||
@@ -206,7 +206,7 @@ function barWidth(views: number) {
|
||||
<div class="admin-card p-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<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 class="mt-4 flex items-center justify-center">
|
||||
<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)"
|
||||
fill="#a855f7" />
|
||||
</template>
|
||||
<circle cx="50" cy="50" r="22" fill="#131418" />
|
||||
<circle cx="50" cy="50" r="22" fill="#ffffff" />
|
||||
</svg>
|
||||
<div class="absolute inset-0 grid place-items-center">
|
||||
<div class="text-center">
|
||||
<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>
|
||||
@@ -232,16 +232,16 @@ function barWidth(views: number) {
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<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>
|
||||
<span class="font-semibold text-zinc-200">{{ stats?.series || 0 }}</span>
|
||||
<span class="font-semibold text-slate-700">{{ stats?.series || 0 }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<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>
|
||||
<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>
|
||||
@@ -249,28 +249,28 @@ function barWidth(views: number) {
|
||||
<div class="admin-card p-5 sm:col-span-2 lg:col-span-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<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 class="mt-4 space-y-3.5">
|
||||
<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="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"
|
||||
: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 }}
|
||||
</span>
|
||||
<span class="truncate text-sm text-zinc-100">{{ movie.name }}</span>
|
||||
<span class="truncate text-sm text-slate-800">{{ movie.name }}</span>
|
||||
</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>
|
||||
</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"
|
||||
:class="i === 0 ? 'bg-yellow-400' : 'bg-zinc-500'"
|
||||
:class="i === 0 ? 'bg-sky-500' : 'bg-zinc-500'"
|
||||
:style="{ width: barWidth(movie.views || 0) + '%' }" />
|
||||
</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>
|
||||
@@ -278,27 +278,27 @@ function barWidth(views: number) {
|
||||
<div class="admin-card p-6">
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<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 class="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<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 }} · {{
|
||||
percentOf(stats?.active || 0, stats?.total || 0) }}%</span>
|
||||
</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"
|
||||
:style="{ width: percentOf(stats?.active || 0, stats?.total || 0) + '%' }" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between text-sm">
|
||||
<span class="text-zinc-400">Chưa hiển thị</span>
|
||||
<span class="font-semibold text-zinc-400 tabular-nums">{{ stats?.inactive?.toLocaleString() || 0 }} · {{
|
||||
<span class="text-slate-500">Chưa hiển thị</span>
|
||||
<span class="font-semibold text-slate-400 tabular-nums">{{ stats?.inactive?.toLocaleString() || 0 }} · {{
|
||||
percentOf(stats?.inactive || 0, stats?.total || 0) }}%</span>
|
||||
</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"
|
||||
:style="{ width: percentOf(stats?.inactive || 0, stats?.total || 0) + '%' }" />
|
||||
</div>
|
||||
|
||||
@@ -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%)]" />
|
||||
|
||||
<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="mb-8 text-center">
|
||||
<div class="mb-4 inline-flex items-center gap-3">
|
||||
<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>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
|
||||
@@ -277,13 +277,13 @@ function useApiActors() {
|
||||
<template>
|
||||
<div class="space-y-5">
|
||||
<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" />
|
||||
Quay lại danh sách phim
|
||||
</NuxtLink>
|
||||
|
||||
<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 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">
|
||||
<img v-if="customPoster || movie.poster" :src="customPoster || movie.poster || undefined" :alt="movie.name"
|
||||
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" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -307,11 +307,11 @@ function useApiActors() {
|
||||
</p>
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<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' }}
|
||||
</span>
|
||||
<span v-if="movie.quality" class="admin-badge bg-white/6 text-zinc-400">{{ movie.quality }}</span>
|
||||
<span class="admin-badge bg-white/6 text-zinc-400">{{ movie.episodeTotal || '—' }} tập</span>
|
||||
<span v-if="movie.quality" class="admin-badge bg-slate-200/50 text-slate-400">{{ movie.quality }}</span>
|
||||
<span class="admin-badge bg-slate-200/50 text-slate-400">{{ movie.episodeTotal || '—' }} tập</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -335,15 +335,15 @@ function useApiActors() {
|
||||
<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="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'
|
||||
: 'text-zinc-400 hover:bg-white/3 hover:text-white'" @click="activeSection = s.key">
|
||||
? '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-slate-500 hover:bg-slate-100 hover:text-white'" @click="activeSection = s.key">
|
||||
<AppIcon :name="s.icon" class="size-4" />
|
||||
<span>{{ s.label }}</span>
|
||||
<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 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>
|
||||
</button>
|
||||
</nav>
|
||||
@@ -356,41 +356,41 @@ function useApiActors() {
|
||||
<AppIcon name="info" class="size-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-sm font-bold text-white">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>
|
||||
<h2 class="text-sm font-bold text-slate-900">Thông tin chi tiết</h2>
|
||||
<p class="text-xs text-slate-500">Dữ liệu hiện tại của phim</p>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="rounded-xl border border-white/6 bg-white/2 p-4">
|
||||
<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 class="rounded-xl border border-white/6 bg-white/2 p-4">
|
||||
<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 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>
|
||||
<dd class="text-zinc-200">{{ movie.year || '—' }}</dd>
|
||||
<dd class="text-slate-700">{{ movie.year || '—' }}</dd>
|
||||
</div>
|
||||
<div class="rounded-xl border border-white/6 bg-white/2 p-4">
|
||||
<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 class="rounded-xl border border-white/6 bg-white/2 p-4">
|
||||
<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 class="rounded-xl border border-white/6 bg-white/2 p-4">
|
||||
<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>
|
||||
</div>
|
||||
</dl>
|
||||
<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>
|
||||
<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>
|
||||
@@ -403,20 +403,20 @@ function useApiActors() {
|
||||
<AppIcon name="type" class="size-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-sm font-bold text-white">Mô tả phim</h2>
|
||||
<p class="text-xs text-zinc-500">{{ customContent ? 'Nội dung tuỳ chỉnh' : 'Nội dung từ API' }}
|
||||
<h2 class="text-sm font-bold text-slate-900">Mô tả phim</h2>
|
||||
<p class="text-xs text-slate-500">{{ customContent ? 'Nội dung tuỳ chỉnh' : 'Nội dung từ API' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<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 mô tả API</button>
|
||||
<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>
|
||||
</div>
|
||||
<textarea v-model="customContent" :rows="contentRows" :placeholder="apiContent || 'Nhập mô tả phim...'"
|
||||
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>
|
||||
|
||||
<!-- Section: Diễn viên -->
|
||||
@@ -427,23 +427,23 @@ function useApiActors() {
|
||||
<AppIcon name="users" class="size-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-sm font-bold text-white">Diễn viên</h2>
|
||||
<p class="text-xs text-zinc-500">{{ customActors.length }} diễn viên</p>
|
||||
<h2 class="text-sm font-bold text-slate-900">Diễn viên</h2>
|
||||
<p class="text-xs text-slate-500">{{ customActors.length }} diễn viên</p>
|
||||
</div>
|
||||
</div>
|
||||
<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"
|
||||
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
|
||||
</button></div>
|
||||
</div>
|
||||
<div v-if="customActors.length" class="space-y-2">
|
||||
<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">
|
||||
<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-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>
|
||||
<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
|
||||
@@ -456,7 +456,7 @@ function useApiActors() {
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
có diễn viên.</div>
|
||||
</div>
|
||||
|
||||
@@ -483,9 +483,9 @@ function useApiActors() {
|
||||
<div class="space-y-3 2xl:col-span-7">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="text-sm font-bold text-white">Server & tập phim</h3>
|
||||
<p class="text-xs text-zinc-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"
|
||||
<h3 class="text-sm font-bold text-slate-900">Server & tập phim</h3>
|
||||
<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-slate-200/50 text-slate-800 hover:bg-white/10"
|
||||
@click="addServer">
|
||||
<AppIcon name="plus" class="size-3" />Thêm server
|
||||
</button>
|
||||
@@ -494,10 +494,10 @@ function useApiActors() {
|
||||
class="rounded-xl border border-white/6 bg-white/2">
|
||||
<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)">
|
||||
<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="truncate text-sm font-bold text-white">{{ server.name || `Server ${serverIndex + 1}`
|
||||
}}</span><span class="rounded-full bg-white/8 px-2 py-0.5 text-[10px] text-zinc-300">{{
|
||||
class="truncate text-sm font-bold text-slate-900">{{ server.name || `Server ${serverIndex + 1}`
|
||||
}}</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>
|
||||
</button><button type="button"
|
||||
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"
|
||||
class="admin-input-sm w-24"><select class="admin-input-sm min-w-0 flex-1"
|
||||
@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"
|
||||
class="bg-[#131418]">{{ item.label }}</option>
|
||||
class="bg-white">{{ item.label }}</option>
|
||||
</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}`)">
|
||||
<AppIcon name="pencil" class="size-3" />
|
||||
</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 m3u8" class="admin-input-sm"></div>
|
||||
</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)">
|
||||
<AppIcon name="plus" class="size-3" />Thêm tập
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<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 có server.</div>
|
||||
</div>
|
||||
<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="mb-3">
|
||||
<h3 class="text-sm font-bold text-white">Nguồn API</h3>
|
||||
<p class="text-xs text-zinc-500">Thêm nhanh tập vào server</p>
|
||||
<h3 class="text-sm font-bold text-slate-900">Nguồn API</h3>
|
||||
<p class="text-xs text-slate-500">Thêm nhanh tập vào server</p>
|
||||
</div>
|
||||
<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>
|
||||
</select>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(source, sourceIndex) in sourceData?.sources || []" :key="source.source"
|
||||
class="rounded-lg border border-white/6"><button type="button"
|
||||
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>
|
||||
<AppIcon name="chevron-down" class="size-4 text-zinc-400" />
|
||||
<AppIcon name="chevron-down" class="size-4 text-slate-400" />
|
||||
</button>
|
||||
<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 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>
|
||||
<div class="flex flex-wrap gap-2"><button v-for="(ep, epIndex) in server.episodes"
|
||||
: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
|
||||
+ 1}` }}</button></div>
|
||||
</div>
|
||||
@@ -583,21 +583,21 @@ function useApiActors() {
|
||||
<div class="admin-card overflow-hidden">
|
||||
<div class="flex items-center justify-between border-b border-white/6 px-4 py-3">
|
||||
<div>
|
||||
<h3 class="text-sm font-bold text-white">Đăng phim</h3>
|
||||
<p class="text-xs text-zinc-500">Lưu và cập nhật nội dung</p>
|
||||
<h3 class="text-sm font-bold text-slate-900">Đăng phim</h3>
|
||||
<p class="text-xs text-slate-500">Lưu và cập nhật nội dung</p>
|
||||
</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>
|
||||
</div>
|
||||
<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>
|
||||
<div class="flex justify-between gap-3"><span class="text-zinc-500">Năm</span><span
|
||||
class="text-zinc-200">{{ movie.year || '—' }}</span></div>
|
||||
<div class="flex justify-between gap-3"><span class="text-zinc-500">Tổng tập</span><span
|
||||
class="text-zinc-200">{{ movie.episodeTotal || '—' }}</span></div>
|
||||
<div class="flex justify-between gap-3"><span class="text-zinc-500">Đồng bộ</span><span
|
||||
class="truncate text-right text-zinc-200">{{ new Date(movie.syncedAt).toLocaleString('vi-VN')
|
||||
<div class="flex justify-between gap-3"><span class="text-slate-9000">Năm</span><span
|
||||
class="text-slate-700">{{ movie.year || '—' }}</span></div>
|
||||
<div class="flex justify-between gap-3"><span class="text-slate-9000">Tổng tập</span><span
|
||||
class="text-slate-700">{{ movie.episodeTotal || '—' }}</span></div>
|
||||
<div class="flex justify-between gap-3"><span class="text-slate-9000">Đồng bộ</span><span
|
||||
class="truncate text-right text-slate-700">{{ new Date(movie.syncedAt).toLocaleString('vi-VN')
|
||||
}}</span></div>
|
||||
</div>
|
||||
<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="border-b border-white/6 px-4 py-3">
|
||||
<h3 class="text-sm font-bold text-white">Ảnh đại diện ngang</h3>
|
||||
<p class="text-xs text-zinc-500">Poster 16:9</p>
|
||||
<h3 class="text-sm font-bold text-slate-900">Ảnh đại diện ngang</h3>
|
||||
<p class="text-xs text-slate-500">Poster 16:9</p>
|
||||
</div>
|
||||
<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"
|
||||
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" />
|
||||
</div>
|
||||
</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="border-b border-white/6 px-4 py-3">
|
||||
<h3 class="text-sm font-bold text-white">Ảnh đại diện dọc</h3>
|
||||
<p class="text-xs text-zinc-500">Thumbnail 2:3</p>
|
||||
<h3 class="text-sm font-bold text-slate-900">Ảnh đại diện dọc</h3>
|
||||
<p class="text-xs text-slate-500">Thumbnail 2:3</p>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<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"
|
||||
: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" />
|
||||
</div>
|
||||
</div><input v-model="customThumb" type="url" placeholder="URL ảnh dọc..."
|
||||
|
||||
+153
-73
@@ -22,6 +22,10 @@ const deleteConfirmOpen = ref(false)
|
||||
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)
|
||||
|
||||
// 3-dot menu state
|
||||
const menuOpen = ref<number | null>(null)
|
||||
const selectedMovie = ref<any>(null)
|
||||
|
||||
const { bust } = useCacheBust()
|
||||
|
||||
let searchTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
@@ -198,8 +202,8 @@ function formatRelativeTime(dateValue?: string | number | Date | null) {
|
||||
return 'Vừa xong'
|
||||
}
|
||||
|
||||
function sortIcon(column: string): string {
|
||||
if (sortBy.value !== column) return 'chevron-down'
|
||||
function sortIcon(column: string): import('~/lib/icons').IconName {
|
||||
if (sortBy.value !== column) return 'chevron-down' as const
|
||||
return sortOrder.value === 'asc' ? 'chevron-up' : 'chevron-down'
|
||||
}
|
||||
|
||||
@@ -218,7 +222,7 @@ const syncSourceOptions = [
|
||||
<p class="admin-section-subtitle mt-1">Quản lý phim hiển thị trên CineK ({{ data?.total || 0 }} phim)</p>
|
||||
</div>
|
||||
<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" />
|
||||
Xoá tất cả
|
||||
</button>
|
||||
@@ -230,23 +234,32 @@ const syncSourceOptions = [
|
||||
</div>
|
||||
|
||||
<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="relative flex-1">
|
||||
<AppIcon name="search" class="absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-slate-400" />
|
||||
<input v-model="searchInput" type="search" placeholder="Tìm kiếm phim..." class="admin-input pl-10">
|
||||
<div class="grid grid-cols-1 gap-3 border-b border-white/6 p-4
|
||||
md:grid-cols-2
|
||||
xl:grid-cols-[minmax(280px,1fr)_180px_160px_150px]
|
||||
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>
|
||||
<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="active" class="bg-[#131418]">Đang hiển thị</option>
|
||||
<option value="inactive" class="bg-[#131418]">Chưa hiển thị</option>
|
||||
</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="ophim" class="bg-[#131418]">OPhim</option>
|
||||
<option value="nguonc" class="bg-[#131418]">NguonC</option>
|
||||
<option value="kkphim" class="bg-[#131418]">KKPhim</option>
|
||||
</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="series" class="bg-[#131418]">Phim bộ</option>
|
||||
<option value="single" class="bg-[#131418]">Phim lẻ</option>
|
||||
@@ -254,124 +267,190 @@ const syncSourceOptions = [
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto admin-scrollbar">
|
||||
<table class="w-full">
|
||||
<table class="w-full" style="border-collapse: collapse; border-spacing: 0;">
|
||||
<thead>
|
||||
<tr class="border-b border-white/[0.06] text-left text-xs font-bold uppercase tracking-wide text-slate-400">
|
||||
<th class="w-14 px-5 py-3">STT</th>
|
||||
<th class="cursor-pointer select-none px-5 py-3 transition hover:text-white" @click="toggleSort('name')">
|
||||
<div class="flex items-center gap-1">
|
||||
<tr class="text-xs font-bold uppercase tracking-wide text-slate-400">
|
||||
<th style="border-bottom: 3px solid #eeee;" class="px-4 py-3 text-center">STT</th>
|
||||
<th style="border-bottom: 3px solid #eeee;"
|
||||
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
|
||||
<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>
|
||||
</th>
|
||||
<th class="px-5 py-3">Nguồn</th>
|
||||
<th class="px-5 py-3">Tập</th>
|
||||
<th class="cursor-pointer select-none px-5 py-3 transition hover:text-white" @click="toggleSort('views')">
|
||||
<div class="flex items-center gap-1">
|
||||
<th style="border-bottom: 3px solid #eeee;" class="px-4 py-3 text-center">Nguồn</th>
|
||||
<th style="border-bottom: 3px solid #eeee;" class="px-4 py-3 text-center">Tập</th>
|
||||
<th style="border-bottom: 3px solid #eeee;"
|
||||
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
|
||||
<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>
|
||||
</th>
|
||||
<th class="cursor-pointer select-none px-5 py-3 transition hover:text-white" @click="toggleSort('apiUpdatedAt')">
|
||||
<div class="flex items-center gap-1">
|
||||
<th style="border-bottom: 3px solid #eeee;"
|
||||
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
|
||||
<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>
|
||||
</th>
|
||||
<th class="cursor-pointer select-none px-5 py-3 transition hover:text-white" @click="toggleSort('active')">
|
||||
<div class="flex items-center gap-1">
|
||||
<th style="border-bottom: 3px solid #eeee;"
|
||||
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
|
||||
<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>
|
||||
</th>
|
||||
<th class="px-5 py-3">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">Tuỳ chỉnh</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>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-white/[0.04]">
|
||||
<tr v-for="movie in movies" :key="movie.id" class="transition hover:bg-white/[0.02]">
|
||||
<td class="px-5 py-3.5 text-sm font-semibold text-slate-500">
|
||||
<tbody class="divide-y divide-[#eeee]">
|
||||
<tr v-for="movie in movies" :key="movie.id" class="transition hover:bg-white/2">
|
||||
<td class="px-4 py-3.5 text-center text-sm font-semibold text-slate-500">
|
||||
{{ (currentPage - 1) * 20 + movies.indexOf(movie) + 1 }}
|
||||
</td>
|
||||
<td class="px-5 py-3.5">
|
||||
<td class="px-4 py-3.5">
|
||||
<div class="min-w-0">
|
||||
<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>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-3.5">
|
||||
<span class="admin-badge bg-blue-400/10 text-blue-400">{{ (movie.sources || []).length }} nguồn</span>
|
||||
<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>
|
||||
</td>
|
||||
<td class="px-5 py-3.5 text-sm">
|
||||
<span class="font-bold text-yellow-300"
|
||||
:title="(movie.sources || []).map((s: any) => `${s.source.toUpperCase()}: ${s.episode || s.episodeTotal || '?'}`).join('\n')">
|
||||
<td class="px-4 py-3.5 text-center text-sm">
|
||||
<span class="font-bold text-yellow-500" :title="(movie.sources || []).map(
|
||||
(s: any) =>
|
||||
`${s.source.toUpperCase()}: ${s.episode || s.episodeTotal || '?'}`
|
||||
).join('\n')">
|
||||
{{ formatEpisode(movie) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-5 py-3.5 text-sm text-slate-300">{{ (movie.views || 0).toLocaleString() }}</td>
|
||||
<td class="px-5 py-3.5 text-sm text-slate-300">
|
||||
<td class="px-4 py-3.5 text-center text-sm text-slate-500">
|
||||
{{ (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)">
|
||||
{{ formatRelativeTime(movie.apiUpdatedAt || movie.syncedAt) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-5 py-3.5">
|
||||
<span class="admin-badge" :class="movie.active ? 'bg-emerald-400/10 text-emerald-400' : 'bg-slate-400/10 text-slate-400'">
|
||||
<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'
|
||||
">
|
||||
{{ movie.active ? 'Hiển thị' : 'Ẩn' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-5 py-3.5">
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<span v-if="movie.customContent" class="admin-badge bg-blue-400/10 text-blue-400">Mô tả</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>
|
||||
<td class="px-4 py-3.5">
|
||||
<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">
|
||||
Mô tả
|
||||
</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">
|
||||
{{ 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 v-if="!movie.customContent && !movie.customPoster && !movie.customThumb && !movie.customServers?.length" class="text-xs text-slate-500">Mặc định</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-3.5">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<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>
|
||||
<td class="px-4 py-3.5">
|
||||
<div class="flex items-center justify-center">
|
||||
<AdminToggle :model-value="movie.active"
|
||||
@update:model-value="(val: boolean) => { movie.active = val; toggleActive(movie) }" />
|
||||
</div>
|
||||
</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 v-if="!movies.length">
|
||||
<td colspan="9" 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.' }}
|
||||
<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.'
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
<div class="flex items-center gap-2">
|
||||
<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--">
|
||||
<AppIcon name="chevron-left" class="size-4" />
|
||||
</button>
|
||||
<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>
|
||||
<button v-else type="button"
|
||||
class="grid size-9 place-items-center rounded-lg border text-sm font-semibold transition"
|
||||
:class="page === currentPage
|
||||
class="grid size-9 place-items-center rounded-lg border text-sm font-semibold transition" :class="page === currentPage
|
||||
? 'border-yellow-400 bg-yellow-400 text-slate-950'
|
||||
: 'border-white/[0.08] text-slate-400 hover:bg-white/[0.05] hover:text-white'"
|
||||
@click="currentPage = page">
|
||||
: 'border-white/8 text-slate-400 hover:bg-white/5 hover:text-white'" @click="currentPage = page">
|
||||
{{ page }}
|
||||
</button>
|
||||
</template>
|
||||
<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++">
|
||||
<AppIcon name="chevron-right" class="size-4" />
|
||||
</button>
|
||||
@@ -383,9 +462,9 @@ const syncSourceOptions = [
|
||||
<Transition name="modal-fade">
|
||||
<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="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"
|
||||
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">
|
||||
<AppIcon name="x" class="size-5" />
|
||||
</button>
|
||||
@@ -397,7 +476,7 @@ const syncSourceOptions = [
|
||||
|
||||
<div class="space-y-3">
|
||||
<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>
|
||||
<p class="text-sm font-bold text-white">{{ source.label }}</p>
|
||||
<p class="text-xs text-slate-400">{{ source.domain }}</p>
|
||||
@@ -406,7 +485,7 @@ const syncSourceOptions = [
|
||||
</label>
|
||||
</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>
|
||||
<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>
|
||||
@@ -439,18 +518,19 @@ const syncSourceOptions = [
|
||||
<Transition name="modal-fade">
|
||||
<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="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">
|
||||
<AppIcon name="trash" class="size-6 text-red-400" />
|
||||
</div>
|
||||
<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">
|
||||
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>
|
||||
<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 bg-red-500 text-white hover:bg-red-400 disabled:opacity-50"
|
||||
<button type="button" class="admin-btn bg-red-500 text-white hover:bg-red-400 disabled:opacity-50"
|
||||
:disabled="deleting" @click="handleDeleteAll">
|
||||
<AppIcon name="trash" class="size-4" />
|
||||
{{ deleting ? 'Đang xoá...' : 'Xoá tất cả' }}
|
||||
|
||||
@@ -65,10 +65,12 @@ const members = computed(() => data.value?.items || [])
|
||||
|
||||
<div class="admin-card overflow-hidden">
|
||||
<div class="flex items-center gap-3 border-b border-white/6 p-4">
|
||||
<div class="relative flex-1">
|
||||
<AppIcon name="search" class="absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-slate-400" />
|
||||
<input v-model="searchInput" type="search" placeholder="Tìm kiếm thành viên..."
|
||||
class="admin-input pl-10">
|
||||
<div class="relative flex-1 min-w-0">
|
||||
<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 thành viên..."
|
||||
class="admin-input h-10 w-full pl-10! pr-3">
|
||||
</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">
|
||||
<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() }}
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-3.5">
|
||||
<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)">
|
||||
<option value="user" class="bg-[#131418]">User</option>
|
||||
<option value="moderator" class="bg-[#131418]">Moderator</option>
|
||||
<option value="admin" class="bg-[#131418]">Admin</option>
|
||||
<option value="user" class="bg-white">User</option>
|
||||
<option value="moderator" class="bg-white">Moderator</option>
|
||||
<option value="admin" class="bg-white">Admin</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-5 py-3.5">
|
||||
|
||||
Reference in New Issue
Block a user