|
|
|
|
@@ -1,97 +1,63 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
interface props {
|
|
|
|
|
isShowDimmed?: boolean
|
|
|
|
|
isOutsideClose?: boolean
|
|
|
|
|
contentText?: string
|
|
|
|
|
contentTitle: string
|
|
|
|
|
modalName?: string
|
|
|
|
|
areaClass?: string
|
|
|
|
|
closeClass?: string
|
|
|
|
|
tabLength?: number // 탭 총개수
|
|
|
|
|
tabValue?: number // 현재 탭 번호 (1부터 시작)
|
|
|
|
|
import type { ContentParams } from '#layers/types/components/modal'
|
|
|
|
|
|
|
|
|
|
interface TabItem {
|
|
|
|
|
title: string
|
|
|
|
|
desc: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const props = withDefaults(defineProps<props>(), {
|
|
|
|
|
isShowDimmed: true,
|
|
|
|
|
const props = withDefaults(defineProps<ContentParams>(), {
|
|
|
|
|
isOutsideClose: false,
|
|
|
|
|
tabLength: 1,
|
|
|
|
|
tabValue: 1,
|
|
|
|
|
contentTitle: '',
|
|
|
|
|
tabActiveIndex: 0,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description Tab 형태의 모달 컴포넌트 props 전달 예시
|
|
|
|
|
* const tabList = [{ key: 1, text: 'Trigger1' }, { key: 2, text: 'Trigger2' }, { key: 3, text: 'Trigger3' }]
|
|
|
|
|
* <BlocksModalContent
|
|
|
|
|
* :is-show-dimmed="true"
|
|
|
|
|
* :is-outside-close="true"
|
|
|
|
|
* :is-open="true"
|
|
|
|
|
* content-title="타이틀"
|
|
|
|
|
* :tab-length="tabList.length"
|
|
|
|
|
* :tab-value="tabNumber"
|
|
|
|
|
* @update-tab="탭 변경시 처리 함수"
|
|
|
|
|
* >
|
|
|
|
|
* <template v-for="trigger in tabList" :key="trigger" #[`trigger${trigger}`]>
|
|
|
|
|
* <span>{{ trigger.text }}</span>
|
|
|
|
|
* </template>
|
|
|
|
|
*
|
|
|
|
|
* <template #content1>
|
|
|
|
|
* <p>콘텐츠 내용</p>
|
|
|
|
|
* </template>
|
|
|
|
|
* ...
|
|
|
|
|
* </BlocksModalContent>
|
|
|
|
|
*
|
|
|
|
|
* @description 단일 콘텐츠 모달 컴포넌트 props 전달 예시
|
|
|
|
|
* 위의 Tab 형태의 모달 컴포넌트에서 :tab-length와 :tab-value, @updata-tab 제거하고 사용하면 됩니다.
|
|
|
|
|
* 내부에 template 태그를 사용하지 않고 콘텐츠 내용만 추가하면 됩니다.
|
|
|
|
|
* <BlocksModalContent
|
|
|
|
|
* :is-show-dimmed="true"
|
|
|
|
|
* :is-outside-close="true"
|
|
|
|
|
* :is-open="true"
|
|
|
|
|
* content-title="타이틀"
|
|
|
|
|
* >
|
|
|
|
|
* <p>콘텐츠 내용</p>
|
|
|
|
|
* </BlocksModalContent>
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
close: []
|
|
|
|
|
updateTab: [tabNumber: number]
|
|
|
|
|
}>()
|
|
|
|
|
const modalStore = useModalStore()
|
|
|
|
|
const breakpoints = useResponsiveBreakpoints()
|
|
|
|
|
|
|
|
|
|
const isOpen = defineModel<boolean>('isOpen', { default: false })
|
|
|
|
|
const currentTab = ref(props.tabValue)
|
|
|
|
|
const currentTab = ref<number>(props.tabActiveIndex)
|
|
|
|
|
|
|
|
|
|
const responsiveTransition = computed(() =>
|
|
|
|
|
breakpoints.value.isXs ? 'slide-up' : 'fade'
|
|
|
|
|
)
|
|
|
|
|
const tabInfo = computed<TabItem[]>(() => props.tabInfo ?? [])
|
|
|
|
|
const isTab = computed(() => tabInfo.value.length >= 2)
|
|
|
|
|
|
|
|
|
|
const isVisible = (index: number) => currentTab.value === index
|
|
|
|
|
|
|
|
|
|
const handleCloseModal = () => {
|
|
|
|
|
emit('close')
|
|
|
|
|
isOpen.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleOutsideClick = () => {
|
|
|
|
|
if (props.isOutsideClose) {
|
|
|
|
|
handleCloseModal()
|
|
|
|
|
}
|
|
|
|
|
if (props.isOutsideClose) handleCloseModal()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleUpdateTab = (tabNumber: number) => {
|
|
|
|
|
currentTab.value = tabNumber
|
|
|
|
|
emit('updateTab', tabNumber)
|
|
|
|
|
if (currentTab.value !== tabNumber) {
|
|
|
|
|
currentTab.value = tabNumber
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(isOpen, newVal => {
|
|
|
|
|
if (newVal) {
|
|
|
|
|
modalStore.handleControlDimmed(true)
|
|
|
|
|
} else {
|
|
|
|
|
modalStore.handleControlDimmed(false)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<Transition name="fade">
|
|
|
|
|
<Transition :name="responsiveTransition">
|
|
|
|
|
<div
|
|
|
|
|
v-if="isOpen"
|
|
|
|
|
:class="[
|
|
|
|
|
'modal-wrap pt-[80px] pb-0',
|
|
|
|
|
{ dimmed: props.isShowDimmed },
|
|
|
|
|
props.modalName,
|
|
|
|
|
]"
|
|
|
|
|
:class="['modal-wrap', { 'is-open': isOpen }, props.modalName]"
|
|
|
|
|
@click="handleOutsideClick"
|
|
|
|
|
>
|
|
|
|
|
<div class="modal-area" @click.stop>
|
|
|
|
|
<div class="modal-title">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<strong class="title">{{ props.contentTitle }}</strong>
|
|
|
|
|
|
|
|
|
|
<button type="button" class="modal-close" @click="handleCloseModal">
|
|
|
|
|
@@ -100,50 +66,45 @@ const handleUpdateTab = (tabNumber: number) => {
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
<div class="tab-wrap">
|
|
|
|
|
<div v-if="$slots.trigger1" class="tab-trigger-wrap">
|
|
|
|
|
<template v-for="trigger in tabLength" :key="trigger">
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<template v-if="isTab">
|
|
|
|
|
<div class="tab-trigger" role="tablist">
|
|
|
|
|
<template
|
|
|
|
|
v-for="(tab, index) in tabInfo"
|
|
|
|
|
:key="tab.title + index"
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
v-if="$slots[`trigger${trigger}`]"
|
|
|
|
|
type="button"
|
|
|
|
|
:class="[
|
|
|
|
|
'tab-trigger',
|
|
|
|
|
{ 'tab-trigger-active': currentTab === trigger },
|
|
|
|
|
]"
|
|
|
|
|
@click="handleUpdateTab(trigger)"
|
|
|
|
|
:class="['btn-trigger', { 'is-active': isVisible(index) }]"
|
|
|
|
|
role="tab"
|
|
|
|
|
@click="handleUpdateTab(index)"
|
|
|
|
|
>
|
|
|
|
|
<slot :name="`trigger${trigger}`"></slot>
|
|
|
|
|
{{ tab.title }}
|
|
|
|
|
</button>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
:class="[
|
|
|
|
|
'tab-panel-wrap',
|
|
|
|
|
$slots.trigger1
|
|
|
|
|
? 'h-[calc(100%-56px)] sm:max-h-[calc(680px-68px-60px)]'
|
|
|
|
|
: 'sm:pt-[12px] sm:max-h-[calc(680px-60px)]',
|
|
|
|
|
]"
|
|
|
|
|
>
|
|
|
|
|
<template v-for="content in tabLength" :key="content">
|
|
|
|
|
<!-- 패널 겹침(컨테이너를 가장 긴 패널 높이에 맞춤) -->
|
|
|
|
|
<div class="tab-panel grid">
|
|
|
|
|
<template v-for="(tab, index) in tabInfo" :key="tab.desc + index">
|
|
|
|
|
<div
|
|
|
|
|
v-if="$slots[`content${content}`]"
|
|
|
|
|
v-show="currentTab === content"
|
|
|
|
|
class="tab-panel"
|
|
|
|
|
>
|
|
|
|
|
<div class="tab-panel-content">
|
|
|
|
|
<slot :name="`content${content}`"></slot>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else class="tab-panel">
|
|
|
|
|
<div class="tab-panel-content">
|
|
|
|
|
<slot></slot>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
v-dompurify-html="tab.desc"
|
|
|
|
|
:class="[
|
|
|
|
|
'content-tex',
|
|
|
|
|
'use-base',
|
|
|
|
|
{ 'is-visible': isVisible(index) },
|
|
|
|
|
]"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
<div
|
|
|
|
|
v-dompurify-html="tabInfo[0].desc"
|
|
|
|
|
class="content-tex use-base"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -151,53 +112,53 @@ const handleUpdateTab = (tabNumber: number) => {
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.modal-area {
|
|
|
|
|
@apply flex flex-col items-center justify-start max-w-[375px] h-full mb-0 rounded-t-[20px] sm:max-w-[560px] sm:h-auto sm:min-h-0 sm:max-h-[680px] sm:mb-auto sm:rounded-[20px];
|
|
|
|
|
.modal-wrap {
|
|
|
|
|
@apply overflow-hidden flex-col p-0 pt-[80px] sm:p-5;
|
|
|
|
|
}
|
|
|
|
|
.modal-title {
|
|
|
|
|
@apply relative flex items-center justify-between gap-[8px] w-full py-[16px] px-[20px]
|
|
|
|
|
.modal-area {
|
|
|
|
|
@apply overflow-hidden flex flex-col rounded-t-[20px] sm:w-[560px] sm:max-h-[680px] sm:rounded-b-[20px];
|
|
|
|
|
}
|
|
|
|
|
.modal-header {
|
|
|
|
|
@apply flex items-center justify-between gap-[8px] w-full py-[16px] px-[20px]
|
|
|
|
|
sm:pt-[20px] sm:px-[32px];
|
|
|
|
|
}
|
|
|
|
|
.modal-title .title {
|
|
|
|
|
@apply relative overflow-hidden text-ellipsis line-clamp-2 w-full text-[#1F1F1F] text-[16px] font-[700] leading-[24px] tracking-[-0.48px];
|
|
|
|
|
.modal-header .title {
|
|
|
|
|
@apply line-clamp-2 w-full text-[#1F1F1F] text-[16px] font-[700] leading-[24px] tracking-[-0.48px];
|
|
|
|
|
}
|
|
|
|
|
.modal-body {
|
|
|
|
|
@apply flex flex-col flex-1 min-h-0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-content {
|
|
|
|
|
@apply relative flex flex-col items-center justify-start w-full h-full flex-1 min-h-0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab-wrap {
|
|
|
|
|
@apply relative flex flex-col items-center justify-center w-full h-full min-h-0;
|
|
|
|
|
}
|
|
|
|
|
.tab-trigger-wrap {
|
|
|
|
|
@apply relative flex items-center justify-center w-full mb-[12px] sm:mb-[24px];
|
|
|
|
|
}
|
|
|
|
|
.tab-trigger {
|
|
|
|
|
@apply relative w-full h-[44px] before:content-[''] before:absolute before:bottom-0 before:left-0 before:w-full before:h-[1px] before:bg-[rgba(0,0,0,0.15)] text-[#1F1F1F] text-[14px] font-[500] leading-[24px] tracking-[-0.42px];
|
|
|
|
|
@apply relative flex w-full mb-[12px] sm:mb-[24px];
|
|
|
|
|
}
|
|
|
|
|
.tab-trigger-active {
|
|
|
|
|
@apply before:!bg-[#1F1F1F] before:!h-[2px];
|
|
|
|
|
.btn-trigger {
|
|
|
|
|
@apply relative w-full py-2.5 before:content-[''] before:absolute before:bottom-0 before:left-0 before:w-full before:h-[1px] before:bg-[rgba(0,0,0,0.15)] text-[#1F1F1F] text-[14px] font-[500] leading-[24px] tracking-[-0.42px];
|
|
|
|
|
}
|
|
|
|
|
.btn-trigger.is-active {
|
|
|
|
|
@apply before:bg-[#1F1F1F] before:h-[2px];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab-panel-wrap {
|
|
|
|
|
@apply relative flex flex-col items-center justify-start overflow-hidden w-full pl-[20px] pr-[8px] sm:pl-[32px] flex-1 min-h-0;
|
|
|
|
|
}
|
|
|
|
|
.tab-panel {
|
|
|
|
|
@apply relative flex flex-col items-start justify-start w-full h-full min-h-0 overflow-auto;
|
|
|
|
|
@apply overflow-hidden grid w-full h-full;
|
|
|
|
|
}
|
|
|
|
|
.tab-panel-content {
|
|
|
|
|
@apply w-full pb-[16px] pr-[12px] sm:pb-[24px] sm:pr-[24px] text-[#333333] text-[15px] font-[400] leading-[24px] tracking-[-0.45px];
|
|
|
|
|
.tab-panel .content-tex {
|
|
|
|
|
@apply col-start-1 row-start-1 transition-opacity duration-200 ease-in-out;
|
|
|
|
|
}
|
|
|
|
|
.tab-panel .content-tex.is-hidden {
|
|
|
|
|
@apply opacity-0 invisible pointer-events-none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab-panel::-webkit-scrollbar {
|
|
|
|
|
.content-tex {
|
|
|
|
|
@apply overflow-y-auto mb-4 px-6 sm:mb-6 sm:px-8;
|
|
|
|
|
}
|
|
|
|
|
.content-tex::-webkit-scrollbar {
|
|
|
|
|
@apply w-[4px];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab-panel::-webkit-scrollbar-track {
|
|
|
|
|
@apply bg-transparent mb-[16px] sm:mb-[24px];
|
|
|
|
|
.content-tex::-webkit-scrollbar-track {
|
|
|
|
|
@apply bg-transparent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab-panel::-webkit-scrollbar-thumb {
|
|
|
|
|
@apply bg-[#D9D9D9] rounded-full;
|
|
|
|
|
.content-tex::-webkit-scrollbar-thumb {
|
|
|
|
|
@apply bg-[#D9D9D9] rounded-full px-2;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|