mirror of
https://github.com/ngthanhvu/kr-phim.git
synced 2026-08-10 17:07:46 +00:00
feat: refactor vvv
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/vue'
|
||||||
|
import { Autoplay, EffectFade } from 'swiper/modules'
|
||||||
|
import 'swiper/css'
|
||||||
|
import 'swiper/css/effect-fade'
|
||||||
|
|
||||||
|
interface Movie {
|
||||||
|
source?: string
|
||||||
|
slug?: string
|
||||||
|
thumb?: string
|
||||||
|
poster?: string
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
slides: Movie[]
|
||||||
|
modelValue?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [index: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const swiperRef = ref<any>(null)
|
||||||
|
|
||||||
|
function onSlideChange(swiper: any) {
|
||||||
|
emit('update:modelValue', swiper.realIndex ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goTo(index: number) {
|
||||||
|
if (!swiperRef.value) return
|
||||||
|
const swiper = swiperRef.value.swiper ?? swiperRef.value
|
||||||
|
if (typeof swiper.slideToLoop === 'function') {
|
||||||
|
swiper.slideToLoop(index)
|
||||||
|
} else if (typeof swiper.slideTo === 'function') {
|
||||||
|
swiper.slideTo(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ goTo })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Swiper ref="swiperRef" :modules="[Autoplay, EffectFade]" effect="fade" :fade-effect="{ crossFade: true }"
|
||||||
|
:slides-per-view="1" :loop="props.slides.length > 1"
|
||||||
|
:autoplay="{ delay: 6500, disableOnInteraction: false, pauseOnMouseEnter: true }" :speed="800"
|
||||||
|
class="absolute inset-0 h-full w-full" @slide-change="onSlideChange">
|
||||||
|
<SwiperSlide v-for="(slide, index) in props.slides" :key="`${slide.source}-${slide.slug}-${index}`"
|
||||||
|
class="h-full w-full">
|
||||||
|
<img :src="slide.thumb || slide.poster" :alt="slide.name"
|
||||||
|
class="h-full w-full object-cover object-top lg:object-[72%_center]">
|
||||||
|
</SwiperSlide>
|
||||||
|
</Swiper>
|
||||||
|
</template>
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
faCirclePlay,
|
faCirclePlay,
|
||||||
faCircleQuestion,
|
faCircleQuestion,
|
||||||
faClock,
|
faClock,
|
||||||
|
faClockRotateLeft,
|
||||||
faClosedCaptioning,
|
faClosedCaptioning,
|
||||||
faComment,
|
faComment,
|
||||||
faCommentDots,
|
faCommentDots,
|
||||||
@@ -114,6 +115,7 @@ library.add(
|
|||||||
faCirclePlay,
|
faCirclePlay,
|
||||||
faCircleQuestion,
|
faCircleQuestion,
|
||||||
faClock,
|
faClock,
|
||||||
|
faClockRotateLeft,
|
||||||
faClosedCaptioning,
|
faClosedCaptioning,
|
||||||
faComment,
|
faComment,
|
||||||
faCommentDots,
|
faCommentDots,
|
||||||
|
|||||||
+22
-27
@@ -37,8 +37,8 @@ const {
|
|||||||
clearWatchHistory: removeWatchHistory,
|
clearWatchHistory: removeWatchHistory,
|
||||||
} = useWatchHistory()
|
} = useWatchHistory()
|
||||||
|
|
||||||
let heroTimer: ReturnType<typeof setInterval> | undefined
|
|
||||||
const heroDetailCache = new Map<string, any>()
|
const heroDetailCache = new Map<string, any>()
|
||||||
|
const heroSliderRef = ref<{ goTo: (index: number) => void } | null>(null)
|
||||||
|
|
||||||
const heroDescription = computed(() => {
|
const heroDescription = computed(() => {
|
||||||
const content = String(heroDetail.value?.content || '').trim()
|
const content = String(heroDetail.value?.content || '').trim()
|
||||||
@@ -164,18 +164,8 @@ function getEpisodeDisplay(movie: any) {
|
|||||||
return ep
|
return ep
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectHero(index: number) {
|
function goToSlide(index: number) {
|
||||||
heroIndex.value = index
|
heroSliderRef.value?.goTo(index)
|
||||||
}
|
|
||||||
|
|
||||||
function nextHero() {
|
|
||||||
if (!heroSlides.value.length) return
|
|
||||||
heroIndex.value = (heroIndex.value + 1) % heroSlides.value.length
|
|
||||||
}
|
|
||||||
|
|
||||||
function previousHero() {
|
|
||||||
if (!heroSlides.value.length) return
|
|
||||||
heroIndex.value = (heroIndex.value - 1 + heroSlides.value.length) % heroSlides.value.length
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function watchHistoryLink(item: WatchHistoryItem) {
|
function watchHistoryLink(item: WatchHistoryItem) {
|
||||||
@@ -267,13 +257,11 @@ watch(hero, async (currentHero) => {
|
|||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
heroTimer = setInterval(nextHero, 6500)
|
|
||||||
loadWatchHistory()
|
loadWatchHistory()
|
||||||
window.addEventListener('storage', loadWatchHistory)
|
window.addEventListener('storage', loadWatchHistory)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (heroTimer) clearInterval(heroTimer)
|
|
||||||
if (import.meta.client) window.removeEventListener('storage', loadWatchHistory)
|
if (import.meta.client) window.removeEventListener('storage', loadWatchHistory)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -300,12 +288,16 @@ useHead({
|
|||||||
<main class="min-h-screen overflow-hidden bg-black">
|
<main class="min-h-screen overflow-hidden bg-black">
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
|
|
||||||
<section v-if="hero" class="relative w-full h-screen overflow-hidden">
|
<div v-if="hero" class="relative bg-black mt-16 md:mt-0">
|
||||||
<TransitionGroup name="hero-fade">
|
<section class="relative w-full aspect-video md:h-screen overflow-hidden">
|
||||||
<img v-for="(slide, index) in heroSlides" v-show="index === heroIndex" :key="`${slide.source}-${slide.slug}`"
|
<ClientOnly>
|
||||||
:src="slide.thumb || slide.poster" :alt="slide.name"
|
<HeroSlider
|
||||||
class="absolute inset-0 h-full w-full object-cover object-top lg:object-[72%_center]">
|
v-if="heroSlides.length"
|
||||||
</TransitionGroup>
|
ref="heroSliderRef"
|
||||||
|
:slides="heroSlides"
|
||||||
|
@update:modelValue="heroIndex = $event"
|
||||||
|
/>
|
||||||
|
</ClientOnly>
|
||||||
|
|
||||||
<!-- Dot pattern overlay (desktop) -->
|
<!-- Dot pattern overlay (desktop) -->
|
||||||
<div class="absolute inset-0 pointer-events-none opacity-30 hidden md:block"
|
<div class="absolute inset-0 pointer-events-none opacity-30 hidden md:block"
|
||||||
@@ -313,7 +305,7 @@ useHead({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile gradient overlay -->
|
<!-- Mobile gradient overlay -->
|
||||||
<div class="absolute inset-0 pointer-events-none md:hidden" style="background:linear-gradient(to top, #0f111a 5%, rgba(15,17,21,0.6) 30%, transparent 60%),
|
<div class="absolute inset-0 pointer-events-none md:hidden" style="background:linear-gradient(to top, black 0%, rgba(15,17,21,0.95) 45%, rgba(15,17,21,0.7) 65%, transparent 85%),
|
||||||
radial-gradient(ellipse at center, transparent 60%, rgba(15,17,26,0.7) 100%)">
|
radial-gradient(ellipse at center, transparent 60%, rgba(15,17,26,0.7) 100%)">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -410,20 +402,23 @@ useHead({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Mobile fade gradient between hero and thumbnails -->
|
||||||
<!-- Thumbnail navigation -->
|
<!-- Thumbnail navigation -->
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-4 md:bottom-8 left-1/2 -translate-x-1/2 md:left-auto md:right-8 md:translate-x-0 z-30 flex gap-2 overflow-x-auto max-w-[calc(100%-2rem)] md:max-w-md lg:max-w-lg pb-2 md:pb-0 snap-x px-2 pointer-events-auto scroll-smooth [&::-webkit-scrollbar]:hidden"
|
class="relative md:absolute md:bottom-8 md:left-auto md:right-8 md:translate-x-0 z-50 flex gap-2 overflow-x-auto max-w-full md:max-w-md lg:max-w-lg py-3 md:py-0 pb-2 md:pb-0 snap-x px-4 md:px-2 pointer-events-auto scroll-smooth [&::-webkit-scrollbar]:hidden bg-black md:bg-transparent"
|
||||||
style="scrollbar-width:none;-ms-overflow-style:none">
|
style="scrollbar-width:none;-ms-overflow-style:none">
|
||||||
<button v-for="(slide, index) in heroSlides" :key="`${slide.source}-${slide.slug}-thumb`" type="button"
|
<button v-for="(slide, index) in heroSlides" :key="`${slide.source}-${slide.slug}-thumb`" type="button"
|
||||||
class="relative shrink-0 w-[14vw] sm:w-[10vw] md:w-[7vw] lg:w-[6vw] xl:w-[5vw] max-w-10 md:max-w-18.75 lg:max-w-21.25 aspect-square md:aspect-video transition-all duration-300 rounded-full md:rounded-lg overflow-hidden snap-center transform-gpu border-2"
|
class="relative shrink-0 w-[14vw] sm:w-[10vw] md:w-[7vw] lg:w-[6vw] xl:w-[5vw] max-w-10 md:max-w-18.75 lg:max-w-21.25 aspect-square md:aspect-video transition-all duration-300 rounded-full md:rounded-lg overflow-hidden snap-center transform-gpu border-2 cursor-pointer"
|
||||||
:class="index === heroIndex ? 'border-white/90 opacity-100 scale-100 shadow-md' : 'border-transparent opacity-80 scale-95 hover:scale-100 hover:opacity-100'"
|
:class="index === heroIndex ? 'border-white/90 opacity-100 scale-100 shadow-md' : 'border-transparent opacity-80 scale-95 hover:scale-100 hover:opacity-100'"
|
||||||
style="-webkit-mask-image:-webkit-radial-gradient(white, black)" :aria-label="`Chuyển đến phim ${index + 1}`"
|
style="-webkit-mask-image:-webkit-radial-gradient(white, black)" :aria-label="`Chuyển đến phim ${index + 1}`"
|
||||||
@click="selectHero(index)">
|
@click="goToSlide(index)">
|
||||||
<img :src="slide.thumb || slide.poster" :alt="slide.name"
|
<img :src="slide.thumb || slide.poster" :alt="slide.name"
|
||||||
class="absolute inset-0 h-full w-full object-cover rounded-full md:rounded-lg">
|
class="absolute inset-0 h-full w-full object-cover rounded-full md:rounded-lg pointer-events-none">
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<section v-else-if="pending"
|
<section v-else-if="pending"
|
||||||
class="mx-auto flex min-h-screen max-w-390 items-center px-4 pt-36 sm:px-6 lg:px-8 lg:pt-24 xl:px-10">
|
class="mx-auto flex min-h-screen max-w-390 items-center px-4 pt-36 sm:px-6 lg:px-8 lg:pt-24 xl:px-10">
|
||||||
|
|||||||
Generated
+20
@@ -20,6 +20,7 @@
|
|||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"mysql2": "^3.23.1",
|
"mysql2": "^3.23.1",
|
||||||
"nuxt": "^4.4.5",
|
"nuxt": "^4.4.5",
|
||||||
|
"swiper": "^14.0.6",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
"vue": "^3.5.34",
|
"vue": "^3.5.34",
|
||||||
"vue-router": "^5.0.6"
|
"vue-router": "^5.0.6"
|
||||||
@@ -10422,6 +10423,25 @@
|
|||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/swiper": {
|
||||||
|
"version": "14.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/swiper/-/swiper-14.0.6.tgz",
|
||||||
|
"integrity": "sha512-ArptjmcZMBvpLCbvzSKuqvQpjjITCbZdymiGOrbZQffQ3ZSjgpvMByXX0WZBUt+FV4PPIumdYfZ9ZdBbFsdH6g==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "custom",
|
||||||
|
"url": "https://sponsors.nolimits4web.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/nolimits4web"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tagged-tag": {
|
"node_modules/tagged-tag": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"mysql2": "^3.23.1",
|
"mysql2": "^3.23.1",
|
||||||
"nuxt": "^4.4.5",
|
"nuxt": "^4.4.5",
|
||||||
|
"swiper": "^14.0.6",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
"vue": "^3.5.34",
|
"vue": "^3.5.34",
|
||||||
"vue-router": "^5.0.6"
|
"vue-router": "^5.0.6"
|
||||||
|
|||||||
Reference in New Issue
Block a user