mirror of
https://github.com/ngthanhvu/kr-phim.git
synced 2026-08-10 14:27:47 +00:00
feat: Update nhiều thứ
This commit is contained in:
@@ -115,7 +115,7 @@ const apiActors = computed(() => {
|
||||
})
|
||||
|
||||
function addServer() {
|
||||
customServers.value.push({ name: '', episodes: [] })
|
||||
customServers.value.push({ name: `Server ${customServers.value.length + 1}`, episodes: [] })
|
||||
}
|
||||
|
||||
function removeServer(serverIndex: number) {
|
||||
|
||||
@@ -91,6 +91,53 @@ async function handleDeleteAll() {
|
||||
|
||||
const movies = computed(() => data.value?.items || [])
|
||||
const totalPages = computed(() => data.value?.totalPages || 1)
|
||||
|
||||
function extractEpisodeNumber(value?: string): number {
|
||||
if (!value) return 0
|
||||
const match = value.match(/(\d+)(?:\/\d+)?\s*$/)
|
||||
if (match) return Number(match[1]) || 0
|
||||
const anyNumber = value.match(/\d+/)
|
||||
return anyNumber ? Number(anyNumber[0]) : 0
|
||||
}
|
||||
|
||||
function formatEpisode(movie: any) {
|
||||
const current = extractEpisodeNumber(movie.episode)
|
||||
const total = extractEpisodeNumber(movie.episodeTotal)
|
||||
if (current > 0 && total > 0) {
|
||||
return `${current}/${total}`
|
||||
}
|
||||
if (current > 0) return String(current)
|
||||
if (movie.episode) return movie.episode
|
||||
return '-'
|
||||
}
|
||||
|
||||
function formatDate(value?: string | number | Date | null) {
|
||||
if (!value) return ''
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return ''
|
||||
return date.toLocaleString('vi-VN')
|
||||
}
|
||||
|
||||
function formatRelativeTime(dateValue?: string | number | Date | null) {
|
||||
if (!dateValue) return 'Không rõ'
|
||||
const date = new Date(dateValue)
|
||||
if (Number.isNaN(date.getTime())) return 'Không rõ'
|
||||
const now = new Date()
|
||||
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000)
|
||||
const units = [
|
||||
{ label: 'năm', seconds: 365 * 24 * 60 * 60 },
|
||||
{ label: 'tháng', seconds: 30 * 24 * 60 * 60 },
|
||||
{ label: 'tuần', seconds: 7 * 24 * 60 * 60 },
|
||||
{ label: 'ngày', seconds: 24 * 60 * 60 },
|
||||
{ label: 'giờ', seconds: 60 * 60 },
|
||||
{ label: 'phút', seconds: 60 },
|
||||
]
|
||||
for (const unit of units) {
|
||||
const value = Math.floor(seconds / unit.seconds)
|
||||
if (value > 0) return `${value} ${unit.label} trước`
|
||||
}
|
||||
return 'Vừa xong'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -153,6 +200,7 @@ const totalPages = computed(() => data.value?.totalPages || 1)
|
||||
<th class="px-4 py-3 text-center">Nguồn trùng</th>
|
||||
<th class="px-4 py-3 text-center">Tập</th>
|
||||
<th class="px-4 py-3 text-center">Lượt xem</th>
|
||||
<th class="px-4 py-3 text-center">Cập nhật API</th>
|
||||
<th class="px-4 py-3 text-center">Trạng thái</th>
|
||||
<th class="px-4 py-3 text-center">Chỉnh sửa</th>
|
||||
<th class="px-4 py-3 text-center">Thao tác</th>
|
||||
@@ -175,14 +223,20 @@ const totalPages = computed(() => data.value?.totalPages || 1)
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center text-sm text-slate-300">
|
||||
<template v-if="movie.episode && movie.episodeTotal && !movie.episode.includes('/')">
|
||||
<span class="font-semibold text-yellow-300">Tập {{ movie.episode.replace(/[^0-9]/g, '') }}/{{ movie.episodeTotal.replace(/[^0-9]/g, '') }}</span>
|
||||
</template>
|
||||
<span v-else>{{ movie.episode || '-' }}</span>
|
||||
<span
|
||||
class="font-semibold text-yellow-300"
|
||||
:title="(movie.sources || []).map((s: any) => `${s.source.toUpperCase()}: ${s.episode || s.episodeTotal || '?'}`).join('\n')">
|
||||
{{ formatEpisode(movie) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center text-sm text-slate-300">
|
||||
{{ (movie.views || 0).toLocaleString() }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center text-sm text-slate-300">
|
||||
<span :title="formatDate(movie.apiUpdatedAt || movie.syncedAt)">
|
||||
{{ formatRelativeTime(movie.apiUpdatedAt || movie.syncedAt) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<span class="rounded-full px-2.5 py-1 text-xs font-semibold"
|
||||
:class="movie.active ? 'bg-green-400/10 text-green-400' : 'bg-slate-400/10 text-slate-400'">
|
||||
@@ -218,7 +272,7 @@ const totalPages = computed(() => data.value?.totalPages || 1)
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!movies.length">
|
||||
<td colspan="8" class="px-4 py-12 text-center text-sm text-slate-400">
|
||||
<td colspan="9" class="px-4 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>
|
||||
|
||||
+2
-16
@@ -149,20 +149,6 @@ function movieLink(movie: any) {
|
||||
}
|
||||
}
|
||||
|
||||
function getEpisodeDisplay(movie: any) {
|
||||
const ep = movie?.episode
|
||||
const total = movie?.episodeTotal
|
||||
if (!ep) return undefined
|
||||
|
||||
const totalNum = total ? total.replace(/[^0-9]/g, '') : ''
|
||||
const epNum = ep.replace(/[^0-9]/g, '')
|
||||
|
||||
if (epNum && totalNum && epNum !== totalNum) {
|
||||
return `Tập ${epNum}/${totalNum}`
|
||||
}
|
||||
|
||||
return ep
|
||||
}
|
||||
|
||||
function goToSlide(index: number) {
|
||||
heroSliderRef.value?.goTo(index)
|
||||
@@ -335,8 +321,8 @@ useHead({
|
||||
style="background-color:#ffd875;background-image:linear-gradient(220deg, #ffd875 0%, #ffe7a8 45%, #ffffff 100%)">
|
||||
{{ hero.quality }}
|
||||
</span>
|
||||
<span v-if="getEpisodeDisplay(hero)" class="px-2 py-0.75 rounded border border-white bg-black/40">
|
||||
{{ getEpisodeDisplay(hero) }}
|
||||
<span v-if="getEpisodeDisplay(hero.episode, hero.episodeTotal)" class="px-2 py-0.75 rounded border border-white bg-black/40">
|
||||
{{ getEpisodeDisplay(hero.episode, hero.episodeTotal) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -60,19 +60,8 @@ function formatWeekday(dateKey: string) {
|
||||
return weekdays[date.getDay()] || ''
|
||||
}
|
||||
|
||||
function getEpisodeDisplay(movie: any) {
|
||||
const ep = movie?.episode
|
||||
const total = movie?.episodeTotal
|
||||
if (!ep) return 'Mới cập nhật'
|
||||
|
||||
const totalNum = total ? total.replace(/[^0-9]/g, '') : ''
|
||||
const epNum = ep.replace(/[^0-9]/g, '')
|
||||
|
||||
if (epNum && totalNum && epNum !== totalNum) {
|
||||
return `Tập ${epNum}/${totalNum}`
|
||||
}
|
||||
|
||||
return ep
|
||||
function movieEpisodeDisplay(movie: any) {
|
||||
return getEpisodeDisplay(movie?.episode, movie?.episodeTotal) ?? 'Mới cập nhật'
|
||||
}
|
||||
|
||||
// Build map ngày -> phim
|
||||
@@ -210,7 +199,7 @@ useHead({
|
||||
{{ movie.name }}
|
||||
</h2>
|
||||
<p class="schedule-card-episode">
|
||||
{{ getEpisodeDisplay(movie) }}
|
||||
{{ movieEpisodeDisplay(movie) }}
|
||||
</p>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
|
||||
+10
-18
@@ -68,17 +68,9 @@ function episodeLink(index: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function getEpisodeDisplay() {
|
||||
const ep = movie.value?.episode
|
||||
const total = movie.value?.episodeTotal
|
||||
if (!ep) return undefined
|
||||
const totalNum = total ? total.replace(/[^0-9]/g, '') : ''
|
||||
const epNum = ep.replace(/[^0-9]/g, '')
|
||||
if (epNum && totalNum && epNum !== totalNum) {
|
||||
return `Tập ${epNum}/${totalNum}`
|
||||
}
|
||||
return ep
|
||||
}
|
||||
const episodeDisplay = computed(() =>
|
||||
getEpisodeDisplay(movie.value?.episode, movie.value?.episodeTotal),
|
||||
)
|
||||
|
||||
function serverLabel(server: any, index: number) {
|
||||
const label = String(server?.name || '').replace(/^(ophim|nguonc|kkphim)\s*-\s*/i, '').trim()
|
||||
@@ -254,9 +246,9 @@ useHead(() => ({
|
||||
class="px-2 py-0.75 rounded border border-white bg-black/20 backdrop-blur-sm font-medium">
|
||||
{{ movie.lang }}
|
||||
</span>
|
||||
<span v-if="getEpisodeDisplay()"
|
||||
<span v-if="episodeDisplay"
|
||||
class="px-2 py-0.75 rounded border border-white bg-black/20 backdrop-blur-sm font-medium">
|
||||
{{ getEpisodeDisplay() }}
|
||||
{{ episodeDisplay }}
|
||||
</span>
|
||||
<span v-if="movie.rating"
|
||||
class="inline-flex items-center rounded overflow-hidden border border-solid border-[rgba(1,180,228,0.5)]">
|
||||
@@ -331,7 +323,7 @@ useHead(() => ({
|
||||
<div class="hidden md:grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm mb-5">
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="text-slate-500 shrink-0 w-20">Trạng thái:</span>
|
||||
<span class="text-slate-200">{{ getEpisodeDisplay() || movie.episode || 'Đang cập nhật' }}</span>
|
||||
<span class="text-slate-200">{{ episodeDisplay || movie.episode || 'Đang cập nhật' }}</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="text-slate-500 shrink-0 w-20">Loại:</span>
|
||||
@@ -375,9 +367,9 @@ useHead(() => ({
|
||||
class="px-2 py-0.75 rounded border border-white bg-black/20 backdrop-blur-sm font-medium">
|
||||
{{ movie.lang }}
|
||||
</span>
|
||||
<span v-if="getEpisodeDisplay()"
|
||||
<span v-if="episodeDisplay"
|
||||
class="px-2 py-0.75 rounded border border-white bg-black/20 backdrop-blur-sm font-medium">
|
||||
{{ getEpisodeDisplay() }}
|
||||
{{ episodeDisplay }}
|
||||
</span>
|
||||
<span v-if="movie.rating"
|
||||
class="inline-flex items-center rounded overflow-hidden border border-solid border-[rgba(1,180,228,0.5)]">
|
||||
@@ -387,7 +379,7 @@ useHead(() => ({
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2"><span class="text-slate-500 shrink-0 w-20">Trạng thái:</span><span>{{
|
||||
getEpisodeDisplay() || movie.episode || 'Đang cập nhật' }}</span></div>
|
||||
episodeDisplay || movie.episode || 'Đang cập nhật' }}</span></div>
|
||||
<div class="flex gap-2"><span class="text-slate-500 shrink-0 w-20">Loại:</span><span>{{ movie.type ===
|
||||
'single' ? 'Phim lẻ' : movie.type === 'series' ? 'Phim bộ' : movie.type || 'Đang cập nhật' }}</span>
|
||||
</div>
|
||||
@@ -440,7 +432,7 @@ useHead(() => ({
|
||||
<AppIcon name="server" class="size-4" />
|
||||
Máy chủ:
|
||||
</div>
|
||||
<button v-for="(server, index) in servers" :key="server.name" type="button"
|
||||
<button v-for="(server, index) in servers" :key="index" type="button"
|
||||
class="group flex items-center gap-1.5 text-[13px] transition-colors rounded-md px-3.5 py-2 border font-medium"
|
||||
:class="selectedServer === index
|
||||
? 'border-white/30 text-white bg-white/5'
|
||||
|
||||
@@ -70,20 +70,6 @@ function movieLink(movie: any) {
|
||||
}
|
||||
}
|
||||
|
||||
function getEpisodeDisplay(movie: any) {
|
||||
const ep = movie.episode
|
||||
const total = movie.episodeTotal
|
||||
if (!ep) return undefined
|
||||
|
||||
const totalNum = total ? total.replace(/[^0-9]/g, '') : ''
|
||||
const epNum = ep.replace(/[^0-9]/g, '')
|
||||
|
||||
if (epNum && totalNum && epNum !== totalNum) {
|
||||
return `Tập ${epNum}/${totalNum}`
|
||||
}
|
||||
|
||||
return ep
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
page.value += 1
|
||||
@@ -135,7 +121,7 @@ useHead({
|
||||
<div
|
||||
class="flex min-w-0 flex-1 items-center rounded-full border border-white/10 bg-white/8 px-4 py-3 shadow-2xl shadow-yellow-950/20">
|
||||
<AppIcon name="search" class="mr-3 size-4 shrink-0 text-yellow-200" />
|
||||
<input v-model="keyword" type="search" placeholder="Tìm phim Hàn Quốc..."
|
||||
<input v-model="keyword" type="search" placeholder="Tìm phim..."
|
||||
class="w-full bg-transparent text-sm text-white outline-none placeholder:text-slate-400">
|
||||
</div>
|
||||
|
||||
@@ -181,15 +167,15 @@ useHead({
|
||||
class="h-full w-full object-cover transition duration-500 group-hover:scale-105">
|
||||
<div class="absolute inset-x-0 bottom-0 bg-linear-to-t from-slate-950 via-slate-950/70 to-transparent p-3">
|
||||
<p class="line-clamp-2 text-sm font-bold leading-snug text-white">{{ movie.name }}</p>
|
||||
<p class="mt-1 truncate text-xs text-yellow-100">{{ getEpisodeDisplay(movie) || movie.year || movie.quality }}</p>
|
||||
<p class="mt-1 truncate text-xs text-yellow-100">{{ getEpisodeDisplay(movie.episode, movie.episodeTotal) || movie.year || movie.quality }}</p>
|
||||
</div>
|
||||
<span v-if="movie.sources?.length > 1"
|
||||
class="absolute left-2 top-2 rounded bg-yellow-400 px-2 py-1 text-xs font-black text-slate-950">
|
||||
{{ movie.sources.length }} server
|
||||
</span>
|
||||
<span v-if="getEpisodeDisplay(movie)"
|
||||
<span v-if="getEpisodeDisplay(movie.episode, movie.episodeTotal)"
|
||||
class="absolute right-2 top-2 rounded bg-red-600/90 px-2 py-1 text-xs font-black text-white backdrop-blur-sm">
|
||||
{{ getEpisodeDisplay(movie) }}
|
||||
{{ getEpisodeDisplay(movie.episode, movie.episodeTotal) }}
|
||||
</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
|
||||
@@ -374,13 +374,9 @@ async function shareMovie() {
|
||||
catch { flashActionMessage('Chưa chia sẻ được.') }
|
||||
}
|
||||
|
||||
function getEpisodeDisplay() {
|
||||
const ep = movie.value?.episode, total = movie.value?.episodeTotal
|
||||
if (!ep) return undefined
|
||||
const totalNum = total ? total.replace(/[^0-9]/g, '') : '', epNum = ep.replace(/[^0-9]/g, '')
|
||||
if (epNum && totalNum && epNum !== totalNum) return `Tập ${epNum}/${totalNum}`
|
||||
return ep
|
||||
}
|
||||
const episodeDisplay = computed(() =>
|
||||
getEpisodeDisplay(movie.value?.episode, movie.value?.episodeTotal),
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await refreshFavoriteState()
|
||||
@@ -635,9 +631,9 @@ useHead(() => ({
|
||||
<span v-if="movie.year"
|
||||
class="text-[11px] font-medium px-2 py-0.5 border border-white/20 rounded text-white/90">{{
|
||||
movie.year }}</span>
|
||||
<span v-if="getEpisodeDisplay()"
|
||||
<span v-if="episodeDisplay"
|
||||
class="text-[11px] font-medium px-2 py-0.5 border border-white/20 rounded text-white/90">{{
|
||||
getEpisodeDisplay() }}</span>
|
||||
episodeDisplay }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Genres -->
|
||||
|
||||
Reference in New Issue
Block a user