From 00036a7cc2bf5ce6639282b0ef2bfa83e2eff25f Mon Sep 17 00:00:00 2001 From: ngthanhvu Date: Wed, 29 Jul 2026 23:10:40 -0400 Subject: [PATCH] feat: implement sorting functionality for movie management and add sections API for recent comments and trending movies --- app/pages/admin/phim/index.vue | 96 ++++++++++- app/pages/index.vue | 271 +++++++++++++++++++++++++++++++- server/api/admin/movies.get.ts | 21 ++- server/api/home/sections.get.ts | 149 ++++++++++++++++++ 4 files changed, 527 insertions(+), 10 deletions(-) create mode 100644 server/api/home/sections.get.ts diff --git a/app/pages/admin/phim/index.vue b/app/pages/admin/phim/index.vue index ab8062b..33d9dba 100644 --- a/app/pages/admin/phim/index.vue +++ b/app/pages/admin/phim/index.vue @@ -13,6 +13,8 @@ const statusFilter = ref('') const sourceFilter = ref('') const typeFilter = ref('') const currentPage = ref(1) +const sortBy = ref('') +const sortOrder = ref<'asc' | 'desc'>('desc') const syncing = ref(false) const syncOpen = ref(false) const deleting = ref(false) @@ -37,6 +39,8 @@ const { data, refresh } = await useFetch('/api/admin/movies', { status: statusFilter.value, source: sourceFilter.value, type: typeFilter.value, + sortBy: sortBy.value, + sortOrder: sortOrder.value, limit: 20, })), }) @@ -45,6 +49,24 @@ watch([statusFilter, sourceFilter, typeFilter], () => { currentPage.value = 1 }) +watch([sortBy, sortOrder], () => { + currentPage.value = 1 +}) + +function getDefaultSortOrder(column: string): 'asc' | 'desc' { + if (column === 'name') return 'asc' + return 'desc' +} + +function toggleSort(column: string) { + if (sortBy.value === column) { + sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc' + } else { + sortBy.value = column + sortOrder.value = getDefaultSortOrder(column) + } +} + async function toggleActive(movie: any) { await $fetch(`/api/admin/movies/${movie.id}`, { method: 'PATCH', @@ -92,6 +114,38 @@ async function handleDeleteAll() { const movies = computed(() => data.value?.items || []) const totalPages = computed(() => data.value?.totalPages || 1) +const visiblePages = computed(() => { + const total = totalPages.value + const current = currentPage.value + const pages: (number | 'ellipsis')[] = [] + + if (total <= 7) { + for (let i = 1; i <= total; i++) pages.push(i) + return pages + } + + pages.push(1) + + if (current > 4) { + pages.push('ellipsis') + } + + const start = Math.max(2, Math.min(current - 1, total - 4)) + const end = Math.min(total - 1, Math.max(current + 1, 5)) + + for (let i = start; i <= end; i++) { + pages.push(i) + } + + if (end < total - 1) { + pages.push('ellipsis') + } + + pages.push(total) + + return pages +}) + function extractEpisodeNumber(value?: string): number { if (!value) return 0 const match = value.match(/(\d+)(?:\/\d+)?\s*$/) @@ -196,12 +250,32 @@ function formatRelativeTime(dateValue?: string | number | Date | null) { STT - Phim + +
+ Phim + +
+ Nguồn trùng Tập - Lượt xem - Cập nhật API - Trạng thái + +
+ Lượt xem + +
+ + +
+ Cập nhật API + +
+ + +
+ Trạng thái + +
+ Chỉnh sửa Thao tác @@ -288,6 +362,20 @@ function formatRelativeTime(dateValue?: string | number | Date | null) { :disabled="currentPage <= 1" @click="currentPage--"> ‹ + + + + + + +
+ +
+
+ +

Sôi nổi nhất

+
+
+ + {{ index + 1 }}. + + +
+

{{ movie.name }}

+

{{ movie.originName || movie.source }}

+
+
+
+ + Xem thêm + +
+ + +
+
+ +

Yêu thích nhất

+
+
+ + {{ index + 1 }}. + + +
+

{{ movie.name }}

+

{{ movie.originName || movie.source }}

+
+
+
+ + Xem thêm + +
+ + +
+
+ +

Thể loại hot

+
+
+ + {{ index + 1 }}. + + + {{ genre.name }} + + +
+ + Xem thêm + +
+
+ diff --git a/server/api/admin/movies.get.ts b/server/api/admin/movies.get.ts index bf3ee1c..6bb809e 100644 --- a/server/api/admin/movies.get.ts +++ b/server/api/admin/movies.get.ts @@ -1,5 +1,5 @@ import { movies } from '../../database/schema' -import { desc, eq, like, and, sql } from 'drizzle-orm' +import { desc, eq, like, and, sql, asc } from 'drizzle-orm' export default defineEventHandler(async (event) => { const db = useDb() @@ -11,6 +11,8 @@ export default defineEventHandler(async (event) => { const status = typeof query.status === 'string' ? query.status : '' const source = typeof query.source === 'string' ? query.source : '' const type = typeof query.type === 'string' ? query.type : '' + const sortBy = typeof query.sortBy === 'string' ? query.sortBy : '' + const sortOrder = typeof query.sortOrder === 'string' ? query.sortOrder : 'desc' const conditions: any[] = [] @@ -22,12 +24,27 @@ export default defineEventHandler(async (event) => { const whereClause = conditions.length > 0 ? and(...conditions) : undefined + const allowedSortColumns: Record = { + name: movies.name, + views: movies.views, + apiUpdatedAt: movies.apiUpdatedAt, + active: movies.active, + } + + const orderBy: any[] = [] + if (sortBy && allowedSortColumns[sortBy]) { + const column = allowedSortColumns[sortBy] + orderBy.push(sortOrder === 'asc' ? asc(column) : desc(column)) + } else { + orderBy.push(desc(movies.apiUpdatedAt), desc(movies.year), desc(movies.syncedAt)) + } + const [items, countResult] = await Promise.all([ db .select() .from(movies) .where(whereClause) - .orderBy(desc(movies.apiUpdatedAt), desc(movies.year), desc(movies.syncedAt)) + .orderBy(...orderBy) .limit(limit) .offset(offset), db diff --git a/server/api/home/sections.get.ts b/server/api/home/sections.get.ts new file mode 100644 index 0000000..61f439a --- /dev/null +++ b/server/api/home/sections.get.ts @@ -0,0 +1,149 @@ +import { eq, and, isNull, desc, inArray, count } from 'drizzle-orm' +import { comments, users, movies } from '../../database/schema' + +export default defineEventHandler(async () => { + const db = useDb() + + const [recentComments, trending, mostRated, allCategories] = await Promise.all([ + // Recent comments (top-level only) with user and movie info + db + .select({ + id: comments.id, + content: comments.content, + likeCount: comments.likeCount, + dislikeCount: comments.dislikeCount, + createdAt: comments.createdAt, + userId: comments.userId, + userName: users.name, + userAvatar: users.avatar, + userRole: users.role, + userGender: users.gender, + source: comments.source, + slug: comments.slug, + movieName: comments.movieName, + movieThumb: movies.thumb, + moviePoster: movies.poster, + }) + .from(comments) + .leftJoin(users, eq(comments.userId, users.id)) + .leftJoin(movies, and(eq(comments.slug, movies.slug), eq(comments.source, movies.source))) + .where(isNull(comments.parentId)) + .orderBy(desc(comments.createdAt)) + .limit(10), + + // Trending by views + db + .select({ + source: movies.source, + slug: movies.slug, + name: movies.name, + originName: movies.originName, + thumb: movies.thumb, + poster: movies.poster, + views: movies.views, + rating: movies.rating, + }) + .from(movies) + .where(eq(movies.active, true)) + .orderBy(desc(movies.views), desc(movies.rating)) + .limit(5), + + // Most rated / favorite + db + .select({ + source: movies.source, + slug: movies.slug, + name: movies.name, + originName: movies.originName, + thumb: movies.thumb, + poster: movies.poster, + views: movies.views, + rating: movies.rating, + }) + .from(movies) + .where(eq(movies.active, true)) + .orderBy(desc(movies.rating), desc(movies.views)) + .limit(5), + + // All categories for hot genres calculation + db + .select({ categories: movies.categories }) + .from(movies) + .where(eq(movies.active, true)), + ]) + + // Count replies for each recent comment + const commentIds = recentComments.map(c => c.id) + let replyCounts: Record = {} + if (commentIds.length > 0) { + const replyRows = await db + .select({ + parentId: comments.parentId, + count: count(), + }) + .from(comments) + .where(inArray(comments.parentId, commentIds)) + .groupBy(comments.parentId) + replyCounts = Object.fromEntries(replyRows.map(r => [r.parentId!, Number(r.count)])) + } + + // Aggregate hot genres + const genreMap = new Map() + for (const row of allCategories) { + for (const category of (row.categories || [])) { + genreMap.set(category, (genreMap.get(category) || 0) + 1) + } + } + const hotGenres = [...genreMap.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, 10) + .map(([name, count]) => ({ name, count })) + + return { + recentComments: recentComments.map(c => ({ + id: c.id, + content: c.content, + likeCount: c.likeCount, + dislikeCount: c.dislikeCount, + replyCount: replyCounts[c.id] || 0, + createdAt: c.createdAt, + user: { + id: c.userId, + name: c.userName || 'Ẩn danh', + avatar: c.userAvatar, + gender: c.userGender, + role: c.userRole, + }, + movie: c.slug && c.source + ? { + source: c.source, + slug: c.slug, + name: c.movieName || '', + thumb: c.movieThumb, + poster: c.moviePoster, + } + : null, + })), + trending: trending.map(m => ({ + source: m.source, + slug: m.slug, + name: m.name, + originName: m.originName, + thumb: m.thumb, + poster: m.poster, + views: m.views, + rating: m.rating, + })), + mostRated: mostRated.map(m => ({ + source: m.source, + slug: m.slug, + name: m.name, + originName: m.originName, + thumb: m.thumb, + poster: m.poster, + views: m.views, + rating: m.rating, + })), + hotGenres, + } +})