Merge branch 'feature/202501107-all' into feature/20251001-gil
This commit is contained in:
265
layers/templates/FxVideo01/index.vue
Normal file
265
layers/templates/FxVideo01/index.vue
Normal file
@@ -0,0 +1,265 @@
|
||||
<script setup lang="ts">
|
||||
import { SplideSlide } from '@splidejs/vue-splide'
|
||||
import {
|
||||
getComponentGroup,
|
||||
getComponentContainer,
|
||||
} from '#layers/utils/dataUtil'
|
||||
import { getYouTubeThumbnail } from '#layers/utils/youtubeUtil'
|
||||
import { formatTimestamp } from '#layers/utils/formatUtil'
|
||||
import type { Splide as SplideType } from '@splidejs/splide'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
import type { OperateGroupItem } from '#layers/types/api/resourcesData'
|
||||
|
||||
interface Props {
|
||||
components: PageDataTemplateComponents
|
||||
pageVerTmplSeq: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const pageDataStore = usePageDataStore()
|
||||
const breakpoints = useResponsiveBreakpoints()
|
||||
const { getOperateResourcesData } = useResourcesData()
|
||||
|
||||
const { pageData } = storeToRefs(pageDataStore)
|
||||
|
||||
// Configuration
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl
|
||||
const multilingualFileName = 'test_homepage_brand_video.json'
|
||||
|
||||
// Multilingual
|
||||
const resultGetMultilingual = await useGetMultilingual({
|
||||
baseApiUrl: dataResourcesUrl,
|
||||
fileName: multilingualFileName,
|
||||
})
|
||||
|
||||
const { tm, locale } = useI18n({
|
||||
useScope: 'local',
|
||||
messages: Object(resultGetMultilingual?.value?.multilingual),
|
||||
})
|
||||
|
||||
const backgroundData = computed(() =>
|
||||
getComponentGroup(props.components, 'background')
|
||||
)
|
||||
const officialUrlData = computed(
|
||||
() => getComponentGroup(props.components, 'officialUrl')?.display?.text ?? ''
|
||||
)
|
||||
|
||||
const { data: slideData } = await useAsyncData(
|
||||
`fx-video-01-resources-${pageData.value?.page_seq}-${pageData.value?.page_ver}-${props.pageVerTmplSeq}`,
|
||||
async () => {
|
||||
if (!pageData.value?.page_seq || !pageData.value?.page_ver) {
|
||||
return []
|
||||
}
|
||||
|
||||
const operateGroupList = await getOperateResourcesData({
|
||||
pageSeq: pageData.value.page_seq,
|
||||
pageVer: pageData.value.page_ver,
|
||||
pageVerTmplSeq: props.pageVerTmplSeq,
|
||||
langCode: locale.value,
|
||||
})
|
||||
|
||||
const mediaList = getComponentContainer(operateGroupList, 'mediaList', {
|
||||
hasGroup: true,
|
||||
}) as OperateGroupItem[]
|
||||
|
||||
return mediaList ?? []
|
||||
},
|
||||
{
|
||||
default: () => [],
|
||||
server: false,
|
||||
}
|
||||
)
|
||||
|
||||
const VIDEO_TYPE = {
|
||||
RECOMMENDED: 1,
|
||||
RECENT: 0,
|
||||
} as const
|
||||
const ITEMS_PER_PAGE = {
|
||||
MOBILE: 8,
|
||||
DESKTOP: 12,
|
||||
} as const
|
||||
const currentRecommendedIndex = ref(1)
|
||||
const currentRecentPage = ref(1)
|
||||
|
||||
// 추천 영상 (option01 === 1)
|
||||
const recommendedVideos = computed(
|
||||
() =>
|
||||
slideData.value?.filter(item => item.option01 === VIDEO_TYPE.RECOMMENDED) ||
|
||||
[]
|
||||
)
|
||||
// 최근 영상 (option01 === 0)
|
||||
const recentVideos = computed(
|
||||
() =>
|
||||
slideData.value?.filter(item => item.option01 === VIDEO_TYPE.RECENT) || []
|
||||
)
|
||||
// 페이지당 보여줄 개수 (PC: 12개, Mobile: 8개)
|
||||
const itemsPerPage = computed(() =>
|
||||
breakpoints.value.isMobile ? ITEMS_PER_PAGE.MOBILE : ITEMS_PER_PAGE.DESKTOP
|
||||
)
|
||||
const visibleVideos = computed(() => {
|
||||
const endIndex = currentRecentPage.value * itemsPerPage.value
|
||||
return recentVideos.value.slice(0, endIndex)
|
||||
})
|
||||
const hasMore = computed(
|
||||
() => visibleVideos.value.length < recentVideos.value.length
|
||||
)
|
||||
|
||||
const handleSplideMove = (_splide: SplideType, newIndex: number) => {
|
||||
currentRecommendedIndex.value = newIndex + 1
|
||||
}
|
||||
|
||||
const handleLoadMoreRecent = () => {
|
||||
if (hasMore.value) {
|
||||
currentRecentPage.value++
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WidgetsFixMainTitle
|
||||
:title="tm('Video_Page_Title')"
|
||||
:resources-data="backgroundData"
|
||||
/>
|
||||
|
||||
<div class="section-container static">
|
||||
<section class="section-static">
|
||||
<WidgetsFixSubTitle
|
||||
:title="tm('Video_Section_Recommendation_Title')"
|
||||
:description="tm('Video_Page_Link')"
|
||||
:link="officialUrlData"
|
||||
/>
|
||||
<div
|
||||
class="relative content-static bg-[#fff] rounded-[12px] md:rounded-[16px]"
|
||||
>
|
||||
<BlocksSlideFade
|
||||
:pagination="false"
|
||||
:drag="false"
|
||||
@move="handleSplideMove"
|
||||
>
|
||||
<template
|
||||
v-for="(item, index) in recommendedVideos"
|
||||
:key="`recommended-${item.url}-${index}`"
|
||||
>
|
||||
<SplideSlide>
|
||||
<div
|
||||
class="overflow-hidden aspect-[16/9] flex-shrink-0 w-full rounded-[4px] sm:w-[60.3%] sm:rounded-[8px] md:w-[56%] lg:w-[710px] lg:rounded-[12px]"
|
||||
>
|
||||
<img
|
||||
:src="getYouTubeThumbnail(item.url)"
|
||||
:alt="item.title || 'Video thumbnail'"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="w-full mx-[8px] pb-[20px] border-b border-[rgba(0,0,0,0.08)] sm:mx-[0] sm:pt-[12px] sm:pr-[16px] sm:pb-[0] sm:border-none md:pt-[20px] md:pr-[28px] lg:pt-[40px] lg:pr-[48px]"
|
||||
>
|
||||
<h4
|
||||
class="h-[52px] text-[18px] font-[700] leading-[26px] tracking-[-0.54px] text-[#1F1F1F] line-clamp-2 sm:h-[auto] sm:text-[20px] sm:leading-[30px] sm:tracking-[-0.6px] sm:line-clamp-3 md:text-[32px] md:leading-[44px] md:tracking-[-0.96px]"
|
||||
>
|
||||
{{ item.title || '' }}
|
||||
</h4>
|
||||
<p
|
||||
class="mt-2 text-[14px] leading-[24px] tracking-[-0.42px] text-[#999] sm:mt-3.5 md:mt-4 md:text-[18px] md:font-[500] md:leading-[26px] md:tracking-[-0.54px] lg:mt-5 lg:text-[20px] lg:leading-[30px] lg:tracking-[-0.6px]"
|
||||
>
|
||||
{{ formatTimestamp(item.reg_dt, 'YYYY.MM.DD') }}
|
||||
</p>
|
||||
</div>
|
||||
</SplideSlide>
|
||||
</template>
|
||||
</BlocksSlideFade>
|
||||
<div class="splide-pagination">
|
||||
<span class="font-[700] text-[#1F1F1F]">
|
||||
{{ currentRecommendedIndex }}
|
||||
</span>
|
||||
/
|
||||
<span>{{ recommendedVideos.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section-static">
|
||||
<WidgetsFixSubTitle :title="tm('Video_Section_Recent_Title')" />
|
||||
<div class="content-static">
|
||||
<ul class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
<li
|
||||
v-for="(item, index) in visibleVideos"
|
||||
:key="`recent-${item.url}-${index}`"
|
||||
class="p-3 rounded-[12px] bg-white md:p-4 md:rounded-[16px] lg:p-5"
|
||||
>
|
||||
<div
|
||||
class="overflow-hidden aspect-[16/9] w-full rounded-[4px] sm:rounded-[8px] lg:rounded-[12px]"
|
||||
>
|
||||
<img
|
||||
:src="getYouTubeThumbnail(item.url)"
|
||||
:alt="item.title || 'Video thumbnail'"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2 pt-5 md:p-3 md:pt-5">
|
||||
<h4
|
||||
class="h-[48px] text-[16px] font-[700] leading-[24px] tracking-[-0.48px] text-[#1F1F1F] line-clamp-2 md:h-[52px] md:text-[18px] md:leading-[26px] md:tracking-[-0.54px]"
|
||||
>
|
||||
{{ item.title || '' }}
|
||||
</h4>
|
||||
<p
|
||||
class="mt-2 text-[14px] leading-[24px] tracking-[-0.42px] text-[#999] md:text-[16px] md:leading-[26px] md:tracking-[-0.48px]"
|
||||
>
|
||||
{{ formatTimestamp(item.reg_dt, 'YYYY.MM.DD') }}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="hasMore" class="mt-[40px] flex justify-center md:mt-[80px]">
|
||||
<AtomsButton
|
||||
type="action"
|
||||
button-size="size-small md:size-medium"
|
||||
background-color="#383838"
|
||||
text-color="#FFFFFF"
|
||||
@click="handleLoadMoreRecent"
|
||||
>
|
||||
{{ tm('Text_More') }}
|
||||
</AtomsButton>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.splide {
|
||||
@apply pb-[68px] sm:pb-[0];
|
||||
}
|
||||
.splide:deep(.splide-arrows) {
|
||||
@apply block;
|
||||
}
|
||||
.splide:deep(.splide-arrows) .splide-arrow {
|
||||
@apply block top-[unset] bottom-[20px] translate-y-0 w-[40px] h-[40px] bg-cover bg-center bg-no-repeat
|
||||
sm:bottom-[24px] md:bottom-[36px] md:w-[48px] md:h-[48px] lg:bottom-[60px];
|
||||
}
|
||||
.splide:deep(.splide-arrows) .arrow-prev {
|
||||
@apply left-[20px] bg-[image:url('/images/common/btn_system_arrow_rev.png')]
|
||||
sm:left-[calc(60.3%+21px)]
|
||||
md:left-[calc(56%+39px)]
|
||||
lg:left-[790px];
|
||||
}
|
||||
.splide:deep(.splide-arrows) .arrow-next {
|
||||
@apply right-[20px] bg-[image:url('/images/common/btn_system_arrow_next.png')]
|
||||
sm:right-[28px]
|
||||
md:right-[unset] md:left-[calc(56%+99px)]
|
||||
lg:left-[850px];
|
||||
}
|
||||
.splide__slide {
|
||||
@apply flex flex-col p-3 gap-4 sm:flex-row sm:gap-6 md:gap-10 md:p-4 lg:gap-[60px] lg:p-5;
|
||||
}
|
||||
.splide-pagination {
|
||||
@apply absolute bottom-[28px] left-[80px] right-[80px] text-center font-[500] text-[16px] leading-[24px] tracking-[-0.48px] text-[#999]
|
||||
sm:bottom-[32px] sm:left-[calc(60.3%+73px)] sm:right-[80px]
|
||||
md:bottom-[47px] md:left-[calc(56%+167px)] md:right-[unset] md:text-[18px] md:leading-[26px] md:tracking-[-0.54px]
|
||||
lg:bottom-[71px] lg:left-[918px];
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user