mirror of
https://github.com/ngthanhvu/kr-phim.git
synced 2026-08-10 17:47:47 +00:00
feat(admin): smart dropdown positioning, filter & pagination fixes
- Add getDropdownPosition() to handle table dropdown placement dynamically - Remove overflow-hidden on admin-card so dropdowns aren't clipped - Fix totalPages calculation using total/limit instead of missing API field - Add role/status filters + full pagination to member page - Create .agents/admin-ui-pitfalls.md for future reference
This commit is contained in:
@@ -123,6 +123,17 @@ async function handleDeleteAll() {
|
||||
const movies = computed(() => data.value?.items || [])
|
||||
const totalPages = computed(() => data.value?.totalPages || 1)
|
||||
|
||||
// Helper để dropdown luôn hiển thị bên trong vùng nhìn thấy
|
||||
function getDropdownPosition(index: number) {
|
||||
const total = movies.value?.length || 0
|
||||
// Chỉ mở lên trên khi còn ít nhất 2 hàng phía dưới
|
||||
// Còn lại mở xuống dưới (mặc định)
|
||||
if (total <= 2 || index < total - 2) {
|
||||
return 'top-full mt-1'
|
||||
}
|
||||
return 'bottom-full mb-1'
|
||||
}
|
||||
|
||||
const visiblePages = computed(() => {
|
||||
const total = totalPages.value
|
||||
const current = currentPage.value
|
||||
@@ -233,7 +244,7 @@ const syncSourceOptions = [
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card overflow-hidden">
|
||||
<div class="admin-card overflow-visible!">
|
||||
<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]
|
||||
@@ -390,8 +401,8 @@ const syncSourceOptions = [
|
||||
@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">
|
||||
<td class="px-4 py-3.5 text-center relative">
|
||||
<div class="inline-flex">
|
||||
<button type="button" class="grid size-9 place-items-center rounded-lg
|
||||
text-zinc-500 transition
|
||||
hover:bg-slate-100 hover:text-zinc-700" title="Thao tác"
|
||||
@@ -399,9 +410,10 @@ const syncSourceOptions = [
|
||||
<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-slate-200
|
||||
bg-white py-1 shadow-xl">
|
||||
<div v-if="menuOpen === movie.id" :class="[
|
||||
'absolute right-0 min-w-40 rounded-lg border border-slate-200 bg-white py-1 shadow-xl z-50',
|
||||
getDropdownPosition(movies.indexOf(movie))
|
||||
]">
|
||||
<NuxtLink :to="`/admin/phim/${movie.id}`" class="flex w-full items-center gap-2 px-3 py-2
|
||||
text-sm text-slate-700 transition
|
||||
hover:bg-slate-100 hover:text-slate-900" @click="menuOpen = null">
|
||||
|
||||
@@ -66,7 +66,23 @@ async function deleteMember(member: any) {
|
||||
}
|
||||
|
||||
const members = computed(() => data.value?.items || [])
|
||||
const totalPages = computed(() => data.value?.totalPages || 1)
|
||||
const totalPages = computed(() => {
|
||||
const limit = 20
|
||||
return Math.ceil((data.value?.total || 1) / limit)
|
||||
})
|
||||
|
||||
// 3-dot menu state
|
||||
const menuOpen = ref<number | null>(null)
|
||||
|
||||
// Helper để dropdown luôn hiển thị bên trong vùng nhìn thấy
|
||||
function getDropdownPosition(index: number) {
|
||||
const total = members.value?.length || 0
|
||||
// Chỉ mở lên trên khi còn ít nhất 2 hàng phía dưới
|
||||
if (total <= 2 || index < total - 2) {
|
||||
return 'top-full mt-1'
|
||||
}
|
||||
return 'bottom-full mb-1'
|
||||
}
|
||||
|
||||
const visiblePages = computed(() => {
|
||||
const total = totalPages.value
|
||||
@@ -108,7 +124,7 @@ const visiblePages = computed(() => {
|
||||
<p class="admin-section-subtitle mt-1">Quản lý tài khoản thành viên CineK ({{ data?.total || 0 }} thành viên)</p>
|
||||
</div>
|
||||
|
||||
<div class="admin-card overflow-hidden">
|
||||
<div class="admin-card overflow-visible!">
|
||||
<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] xl:items-center">
|
||||
<div class="relative min-w-0 w-full">
|
||||
@@ -149,17 +165,9 @@ const visiblePages = computed(() => {
|
||||
<tr v-for="member in members" :key="member.id" class="transition hover:bg-white/2">
|
||||
<td class="px-4 py-3.5 text-center text-sm font-semibold text-slate-500">{{ members.indexOf(member) + 1 }}
|
||||
</td>
|
||||
<td class="min-w-0 px-4 py-3.5 text-center">
|
||||
<div class="flex items-center gap-3 justify-center">
|
||||
<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 shrink-0">
|
||||
{{ (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-xs text-slate-400">{{ member.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<td class="px-4 py-3.5 text-center">
|
||||
<p class="truncate text-sm font-bold text-white">{{ member.name || 'Chưa có tên' }}</p>
|
||||
<p class="truncate text-xs text-slate-400">{{ member.email }}</p>
|
||||
</td>
|
||||
<td class="px-4 py-3.5 text-center">
|
||||
<select :value="member.role"
|
||||
@@ -178,13 +186,31 @@ const visiblePages = computed(() => {
|
||||
{{ new Date(member.createdAt).toLocaleDateString('vi-VN') }}
|
||||
</td>
|
||||
<td class="px-4 py-3.5 text-center">
|
||||
<button type="button"
|
||||
class="grid size-8 place-items-center rounded-lg text-red-400 transition hover:bg-red-500/10"
|
||||
:disabled="currentUser?.id === member.id"
|
||||
:title="currentUser?.id === member.id ? 'Không thể xoá chính mình' : 'Xoá thành viên'"
|
||||
@click="deleteMember(member)">
|
||||
<AppIcon name="trash" class="size-4" />
|
||||
</button>
|
||||
<div class="relative inline-flex">
|
||||
<button type="button"
|
||||
class="grid size-9 place-items-center rounded-lg text-zinc-500 transition
|
||||
hover:bg-slate-100 hover:text-zinc-700" title="Thao tác"
|
||||
@click="menuOpen = menuOpen === member.id ? null : member.id">
|
||||
<AppIcon name="ellipsis-vertical" class="size-5 stroke-[2.5]" />
|
||||
</button>
|
||||
<Transition name="dropdown-fade">
|
||||
<div v-if="menuOpen === member.id" :class="[
|
||||
'absolute right-0 min-w-40 rounded-lg border border-slate-200 bg-white py-1 shadow-xl z-50',
|
||||
getDropdownPosition(members.indexOf(member))
|
||||
]">
|
||||
<button type="button"
|
||||
v-if="currentUser?.id !== member.id"
|
||||
class="flex w-full items-center gap-2 px-3 py-2 text-sm text-red-500 transition hover:bg-red-50"
|
||||
@click="menuOpen = null; deleteMember(member)">
|
||||
<AppIcon name="trash" class="size-4" />
|
||||
Xoá thành viên
|
||||
</button>
|
||||
<div v-else class="px-3 py-2 text-xs text-slate-400">
|
||||
Không thể xoá chính mình
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!members.length">
|
||||
|
||||
Reference in New Issue
Block a user