Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ad19bbcfd | |||
| 11ea05de83 | |||
| e738cca263 | |||
| b93f0218a5 | |||
| 3fe4da0ecb | |||
| a9d6bea337 | |||
| df31b7bdef | |||
| a1c6e2872f | |||
| 46b808cf9c | |||
| 367374863e | |||
| 66b5a8ce6a | |||
| adecec9041 | |||
| 984ec50a39 | |||
| a756c91bd0 | |||
| cf64f11e72 | |||
| 780474bcb3 | |||
| be1393b7da | |||
| a5f9ff7bac | |||
| 5889e9af0e | |||
| 17036b77af | |||
| ac218aeac5 | |||
| 815ce88d95 | |||
| 76d4628100 | |||
| 7bf902041e | |||
| 554ceab3c6 | |||
| ee5c6f40f1 | |||
| 4bf217c207 | |||
| 0adb6fca36 | |||
| 35f069a776 | |||
| a01eedc2bc | |||
| 03ca9c6603 | |||
| 5a207435ce | |||
| 40622caf61 | |||
| 26e4a289d7 | |||
| cbc0f8b7c0 | |||
| 80a5aae4e6 | |||
| a46ef56e07 | |||
| 1f60621995 | |||
| 36e39fa0d5 | |||
| ad962eda86 | |||
| 24ecc2195d | |||
| fcb826a7c6 | |||
| ab3419bd5f | |||
| e151dda2ad | |||
| a447a8a7aa | |||
| 6a275c354e | |||
| 229155b24a | |||
| 3b613faccf | |||
| f9c2d748d5 | |||
| d103f4bbf7 | |||
| f17e28472c | |||
| 3c75c89a8b | |||
| c2b9208746 | |||
| ecf4512cd3 | |||
| 8818c73cec | |||
| 339d370f22 | |||
| 5b1e0af397 | |||
| c217ed82c9 | |||
| 0a7774b7f4 | |||
| a0a5651ac0 | |||
| 4d239e9f32 | |||
| 45cd7780d6 | |||
| 343ac29a57 | |||
| 0729e021bd | |||
| 58b5c67d0c | |||
| d95d648687 | |||
| 6bf22ee747 | |||
| 27c4917697 | |||
| f744c90969 | |||
| f22a16d42a | |||
| 0bfbfa7711 | |||
| 94ea03f189 | |||
| e126b2ab40 | |||
| 66ac28b468 | |||
| 27b57f394f | |||
| 57a8a5e15d | |||
| 0364939e00 | |||
| cfb0592ce3 | |||
| dc94e7c3b3 | |||
| 2f8c9b9cb8 | |||
| 9e72b00aa7 | |||
| 3a7132ea98 | |||
| c5887d911f | |||
| 892bddde2f | |||
| 7b01ce6170 | |||
| 709a2c3232 | |||
| 94abf5ce61 | |||
| 51ab320799 | |||
| 6ff759f22a | |||
| 86bebfd66e |
@@ -1,4 +1,4 @@
|
|||||||
NUXT_PUBLIC_BASE_API=https://api.vpress.vn/api-v1
|
NUXT_PUBLIC_BASE_API=http://api-portal.vpress.vn/api-v1
|
||||||
NUXT_PUBLIC_SITE_DEFAULT=1
|
NUXT_PUBLIC_SITE_DEFAULT=1
|
||||||
PUBLIC_BASE_SERVER_RESOURCE=https://acp-api.vpress.vn
|
PUBLIC_BASE_SERVER_RESOURCE=https://acp-api.vpress.vn
|
||||||
PUBLIC_PAGING_PAGE=1
|
PUBLIC_PAGING_PAGE=1
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
NUXT_PUBLIC_BASE_API=https://api.vpress.vn/api-v1
|
NUXT_PUBLIC_BASE_API=http://api-portal.vpress.vn/api-v1
|
||||||
NUXT_PUBLIC_SITE_DEFAULT=1
|
NUXT_PUBLIC_SITE_DEFAULT=1
|
||||||
PUBLIC_BASE_SERVER_RESOURCE=https://api.vpress.vn
|
PUBLIC_BASE_SERVER_RESOURCE=http://api-portal.vpress.vn
|
||||||
PUBLIC_PAGING_PAGE=1
|
PUBLIC_PAGING_PAGE=1
|
||||||
PUBLIC_PAGING_LIMIT=10
|
PUBLIC_PAGING_LIMIT=10
|
||||||
AUTH_SECRET=vpress
|
AUTH_SECRET=vpress
|
||||||
AUTH_ORIGIN=https://vpress.vn
|
GOOGLE_CLIENT_ID=410090780886-odisqirb9ghresjoop8rg3ad0fn8jl0s.apps.googleusercontent.com
|
||||||
|
GOOGLE_CLIENT_SECRET=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
|
||||||
|
FACEBOOK_CLIENT_ID=280456401372340
|
||||||
|
FACEBOOK_CLIENT_SECRET=86d6272c3a03d25442ecd7ccbf0c204c
|
||||||
@@ -26,9 +26,7 @@ useHead({
|
|||||||
<div>
|
<div>
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
<NuxtLoadingIndicator />
|
<NuxtLoadingIndicator />
|
||||||
<ErrorBoundary>
|
<NuxtErrorBoundary>
|
||||||
|
|
||||||
|
|
||||||
<template #error="{ error }">
|
<template #error="{ error }">
|
||||||
<div class="text-center my-8">
|
<div class="text-center my-8">
|
||||||
<h2 class="mb-2">404</h2>
|
<h2 class="mb-2">404</h2>
|
||||||
@@ -38,12 +36,28 @@ useHead({
|
|||||||
trang chủ</button>
|
trang chủ</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ErrorBoundary>
|
</NuxtErrorBoundary>
|
||||||
<ScrollToTop />
|
<KeepAlive>
|
||||||
|
<NuxtPage />
|
||||||
|
</KeepAlive>
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</div>
|
</div>
|
||||||
</a-config-provider> -->
|
</a-config-provider> -->
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
<NuxtPage />
|
<NuxtLoadingIndicator />
|
||||||
</NuxtLayout>
|
<NuxtErrorBoundary>
|
||||||
|
<template #error="{ error }">
|
||||||
|
<div class="text-center my-8">
|
||||||
|
<h2 class="mb-2">404</h2>
|
||||||
|
<p class="mb-3">Trang không tồn tại.</p>
|
||||||
|
<p v-if="utils.isDev()">{{ error }}</p>
|
||||||
|
<button @click="resolveError(error)" type="button" class=" p-2 border focus:outline-none border-blue text-blue-7 hover:(bg-blue text-white) rounded-lg transition-colors">Về
|
||||||
|
trang chủ</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NuxtErrorBoundary>
|
||||||
|
<KeepAlive>
|
||||||
|
<NuxtPage />
|
||||||
|
</KeepAlive>
|
||||||
|
</NuxtLayout>
|
||||||
</template>
|
</template>
|
||||||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
@@ -1,11 +1,9 @@
|
|||||||
@import custom.css
|
@import custom.css
|
||||||
body
|
|
||||||
font-family: 'Nunito', sans-serif
|
|
||||||
|
|
||||||
video
|
// video
|
||||||
max-width: 100% !important
|
// max-width: 100% !important
|
||||||
width: unset !important
|
// width: unset !important
|
||||||
height: unset !important
|
// height: unset !important
|
||||||
|
|
||||||
iframe
|
iframe
|
||||||
width: 100% !important
|
width: 100% !important
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap');
|
/* @import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,100..900;1,100..900&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,100..900;1,100..900&family=Merriweather:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400;1,700;1,900&display=swap'); */
|
||||||
|
|
||||||
.custom_scrollbar {
|
.custom_scrollbar {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -40,7 +43,7 @@ img.wide {
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
width: 70%;
|
width: 70%;
|
||||||
transform: translateX(20%);
|
/* transform: translateX(20%); */
|
||||||
}
|
}
|
||||||
|
|
||||||
figure.image.wide {
|
figure.image.wide {
|
||||||
@@ -146,7 +149,7 @@ span.boxRelation .relationBoxText{
|
|||||||
}
|
}
|
||||||
|
|
||||||
span.boxRelation .relationText{
|
span.boxRelation .relationText{
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
span.boxRelation .relationDay{
|
span.boxRelation .relationDay{
|
||||||
|
|||||||
@@ -1,22 +1,182 @@
|
|||||||
.style_layout {
|
@import url("https://fonts.googleapis.com/css2?family=Gelasio:ital,wght@0,400..700;1,400..700&display=swap");
|
||||||
> .section-container {
|
@import url("https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100..900;1,100..900&display=swap");
|
||||||
> .layout_define {
|
|
||||||
> .section_layout {
|
|
||||||
@apply gap-x-14
|
|
||||||
|
|
||||||
> div {
|
// Variables
|
||||||
background: red;
|
$font-gelasio: "Gelasio", serif;
|
||||||
|
$font-raleway: "Raleway", sans-serif;
|
||||||
|
|
||||||
|
$color-primary: #ED1C24;
|
||||||
|
$color-brown: #9E1E0F;
|
||||||
|
$color-text: #151411;
|
||||||
|
$color-paragraph: #AFADB5;
|
||||||
|
$color-placeholder: #F9F9F9;
|
||||||
|
$color-line: #EDEDED;
|
||||||
|
|
||||||
|
|
||||||
|
// Mixins
|
||||||
|
|
||||||
|
// extends
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: $font-raleway;
|
||||||
|
}
|
||||||
|
|
||||||
|
// %headings {
|
||||||
|
// color: $color-text;
|
||||||
|
// font-family: $font-gelasio;
|
||||||
|
// line-height: 130%;
|
||||||
|
// font-weight: 700;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// %label {
|
||||||
|
// color: $color-text;
|
||||||
|
// font-family: $font-raleway;
|
||||||
|
// line-height: 130%;
|
||||||
|
// font-weight: 700;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// %paragraph {
|
||||||
|
// color: $color-paragraph;
|
||||||
|
// font-family: $font-raleway;
|
||||||
|
// line-height: 180%;
|
||||||
|
// font-weight: 500;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// h1, h2, h3, h4, h5, h6 {
|
||||||
|
// font-family: $font-gelasio;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// h1 a {
|
||||||
|
// @extend %headings;
|
||||||
|
// font-size: 64px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// h2 a{
|
||||||
|
// @extend %headings;
|
||||||
|
// font-size: 44px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// h3 a{
|
||||||
|
// @extend %headings;
|
||||||
|
// font-size: 24px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// h4 a{
|
||||||
|
// @extend %headings;
|
||||||
|
// font-size: 20px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// h5 a{
|
||||||
|
// @extend %headings;
|
||||||
|
// font-size: 18px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// h6 a{
|
||||||
|
// @extend %headings;
|
||||||
|
// font-size: 16px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// h6.h6-plus a {
|
||||||
|
// @extend %headings;
|
||||||
|
// font-size: 14px;
|
||||||
|
// font-weight: 500;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// label {
|
||||||
|
// &.label-l1 {
|
||||||
|
// @extend %label;
|
||||||
|
// font-size: 18px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// &.label-l2 {
|
||||||
|
// @extend %label;
|
||||||
|
// font-size: 16px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// &.label-l3 {
|
||||||
|
// @extend %label;
|
||||||
|
// font-size: 14px;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// p {
|
||||||
|
// &.paragraph-p1 {
|
||||||
|
// font-size: 18px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// &.paragraph-p2 {
|
||||||
|
// font-size: 16px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// &.paragraph-p2 {
|
||||||
|
// font-size: 14px;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
a {
|
||||||
|
@apply hover:text-primary font-gelasio font-700 leading-130%;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
object-fit: cover!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// .content {
|
||||||
|
// & p {
|
||||||
|
// @apply mb-2 font-arial leading-160%;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & #title {
|
||||||
|
// @apply font-merriweather font-bold leading-150%;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & #intro, & #sub {
|
||||||
|
// @apply font-arial font-medium leading-160%;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & audio {
|
||||||
|
// @apply w-full;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// & document, & a, & custom-figure, & author {
|
||||||
|
// @apply cursor-pointer text-primary;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
div[layout="TYPE:Detail-LAYOUT:image"] {
|
||||||
|
& p,& figure.align-center-image, & #sub, & #title, & #intro, & #breadcrumb, & #navigation__bottom {
|
||||||
|
@apply lg:max-w-640px mx-auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div[layout="ARTICLE_PAGE"] {
|
||||||
|
& figure{
|
||||||
|
@apply w-full items-center flex justify-center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-long {
|
||||||
|
& .section_layout.grid {
|
||||||
|
@apply md:gap-20px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1385px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout_container {
|
||||||
|
|
||||||
|
& > .section_layout {
|
||||||
|
@apply mt-12 first:mt-0;
|
||||||
|
|
||||||
> div {
|
|
||||||
article {
|
|
||||||
background: red;
|
|
||||||
h3 {
|
|
||||||
@apply text-xl #{!important}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{ events: any[] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="events?.length" class="mt-6">
|
||||||
|
<h3 class="text-xl font-semibold after:content-[':']">Sự kiện</h3>
|
||||||
|
<ul class="flex flex-col gap-2 list-disc ml-4 my-2 pl-3 flex-wrap">
|
||||||
|
<li v-for="(event, index) in events" :key="index" class="font-semibold text-primary">
|
||||||
|
<nuxt-link :to="{ name: 'event-eventSlug', params: { eventSlug: event.code } }">{{ event.title }}</nuxt-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{ tags?: any[] }>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="tags?.length"
|
||||||
|
class="flex flex-col items-center justify-between gap-6 sm:flex-row mt-6">
|
||||||
|
<div id="article-tags" class="flex order-2 gap-4 xl:order-1">
|
||||||
|
<h3 class="text-xl font-semibold after:content-[':']">Tag</h3>
|
||||||
|
<div class="flex items-center gap-4 flex-wrap">
|
||||||
|
<template v-for="(item, index) in tags" :key="index">
|
||||||
|
<div v-if="item.code">
|
||||||
|
<nuxt-link :to="{ name: 'tag-tagSlug', params:{ tagSlug: item.code } }" class="text-blue-500 hover:text-blue-600">
|
||||||
|
{{ item?.title }}
|
||||||
|
</nuxt-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{topics?: any[]}>()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div v-if="topics?.length" class="flex flex-col items-center justify-between gap-6 sm:flex-row mt-6">
|
||||||
|
<div class="flex order-2 gap-4 xl:order-1">
|
||||||
|
<h3 class="text-xl font-semibold after:content-[':'] whitespace-nowrap">Chủ đề</h3>
|
||||||
|
<div class="flex items-center gap-4 flex-wrap">
|
||||||
|
<NuxtLink v-for="(item, index) in topics" :key="index" :to="{ name:'topic-topicSlug', params:{ topicSlug: item.code } }" class="text-blue-500 hover:text-blue-600">
|
||||||
|
{{ item?.title }},
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useArticleStore } from '~/stores/articles';
|
||||||
|
const { currentArticle } = storeToRefs(useArticleStore());
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
dataId?: string,
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const store = reactive({
|
||||||
|
article: useArticleStore()
|
||||||
|
})
|
||||||
|
|
||||||
|
// onBeforeMount(async () => {
|
||||||
|
// await store.article.getArticleById(Number(props.dataId))
|
||||||
|
// })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
12
|
||||||
|
<!-- {{ currentArticle }} 12 -->
|
||||||
|
<!-- <a href="#" :style="style" class="!no-underline !px-2">{{ title }} 1</a> -->
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
dataResource?: string,
|
||||||
|
dataTitle?: string,
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const resource = computed(() => props.dataResource ?? '')
|
||||||
|
const title = computed(() => props.dataTitle ?? '')
|
||||||
|
const fileName = computed(() => `${title.value}.${resource.value.split('.').pop()}`)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a :href="resource" :download="fileName">{{fileName}}</a>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
dataResource?: string,
|
||||||
|
dataTitle?: string,
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const resource = computed(() => props.dataResource ?? '')
|
||||||
|
const title = computed(() => props.dataTitle ?? '')
|
||||||
|
const fileName = computed(() => `${title.value}.${resource.value.split('.').pop()}`)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a :href="resource" :download="fileName">{{fileName}}</a>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
span
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { usePollStore } from "~/stores/poll";
|
||||||
|
import { usePollOptionStore } from "~/stores/poll-option";
|
||||||
|
import { usePollResponseStore } from "~/stores/poll-response";
|
||||||
|
import type { Poll } from "~/server/models/poll";
|
||||||
|
import type { PollResponse } from "~/server/models/poll-response";
|
||||||
|
import type { PollOption } from "~/server/models/poll-option";
|
||||||
|
const props = defineProps<{ dataId?: string }>();
|
||||||
|
|
||||||
|
const store = reactive({
|
||||||
|
poll: usePollStore(),
|
||||||
|
pollOptions: usePollOptionStore(),
|
||||||
|
pollResponse: usePollResponseStore(),
|
||||||
|
});
|
||||||
|
const { currentPoll } = storeToRefs(store.poll);
|
||||||
|
const { currentPollOptions } = storeToRefs(store.pollOptions);
|
||||||
|
const { currentPollResponses } = storeToRefs(store.pollResponse);
|
||||||
|
const poll = reactive<Poll | any>({});
|
||||||
|
const options = ref<PollOption[] | any[]>([]);
|
||||||
|
async function loadData() {
|
||||||
|
await store.poll.fetchById(String(props.dataId));
|
||||||
|
await store.pollOptions.fetchByPollId(String(props.dataId));
|
||||||
|
await store.pollResponse.fetchByPollId(String(props.dataId));
|
||||||
|
assignData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignData() {
|
||||||
|
Object.assign(poll, currentPoll.value);
|
||||||
|
options.value = currentPollOptions.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
await loadData();
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedOption = ref<any>(-1);
|
||||||
|
const showResult = ref(false);
|
||||||
|
const alreadyVoted = ref(false);
|
||||||
|
const canShowResult = computed(() => {
|
||||||
|
switch (poll.settings?.resultPublication) {
|
||||||
|
case 0:
|
||||||
|
return false;
|
||||||
|
case 1:
|
||||||
|
return true;
|
||||||
|
case 2:
|
||||||
|
return alreadyVoted.value;
|
||||||
|
case 3:
|
||||||
|
return alreadyVoted.value && (!poll.endTime || new Date() > new Date(poll.endTime));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const totalResponses = ref(0);
|
||||||
|
async function submitVote() {
|
||||||
|
if (selectedOption.value >= 0 && !alreadyVoted.value) {
|
||||||
|
const result = await store.pollResponse.create({ optionId: selectedOption.value });
|
||||||
|
if (result?.id) {
|
||||||
|
totalResponses.value = options.value?.reduce((sum, option) => sum + Number(option.responseCount ?? 0), 1);
|
||||||
|
options?.value?.forEach((option: PollOption | any) => {
|
||||||
|
if (option.id === selectedOption.value) {
|
||||||
|
option.responseCount = (option.responseCount ?? 0) + 1;
|
||||||
|
}
|
||||||
|
option.percentage = Number(((option.responseCount / totalResponses.value) * 100).toFixed(1));
|
||||||
|
});
|
||||||
|
alreadyVoted.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleResults = () => {
|
||||||
|
if (canShowResult.value) showResult.value = !showResult.value;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<span class="inline-block px-4 py-2 shadow-xl rounded-lg bg-[#f5f5f5]">
|
||||||
|
<span class="block">
|
||||||
|
<span class="underline decoration-gray-500 font-bold">
|
||||||
|
{{ poll?.title }}
|
||||||
|
</span>
|
||||||
|
<button v-if="showResult && canShowResult" type="button" class="underline text-blue-400" @click="toggleResults">Câu hỏi</button>
|
||||||
|
<button class="underline text-blue-400" v-if="!showResult && canShowResult" type="button" @click="toggleResults">Kết quả</button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="!showResult" class="p-1 block">
|
||||||
|
<span v-for="(option, index) in options" :key="index" class="block">
|
||||||
|
<label class="flex gap-2 m-2">
|
||||||
|
<input type="radio" :value="option.id" v-model="selectedOption" :disabled="selectedOption >= 0 && alreadyVoted ? true : false" />
|
||||||
|
<span class="font-semibold">{{ option?.title }}</span>
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
<button @click="submitVote" class="bg-primary-500 text-white py-1 px-3 rounded-4px cursor-pointer hover:bg-primary-600 float-right">Bình chọn</button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else class="flex flex-col my-5 gap-4">
|
||||||
|
<span v-for="(answer, index) in options" :key="index" class="flex gap-2">
|
||||||
|
<span class="w-50px">{{ answer.percentage }}%</span>
|
||||||
|
<div class="w-full">
|
||||||
|
<b class="mb-0.5 block">{{ answer.title }}</b>
|
||||||
|
<div :style="{ width: `${answer.percentage}%` }" :class="answer.id === selectedOption ? 'bg-green-600' : 'bg-primary-500'" class="h-1.5 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<b>Tổng số lượt binh chọn: {{ totalResponses }}</b>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useQuizStore } from "~/stores/quiz";
|
||||||
|
import type { Quiz } from "~/server/models/quiz";
|
||||||
|
|
||||||
|
const props = defineProps<{ dataId?: string }>();
|
||||||
|
|
||||||
|
const store = reactive({
|
||||||
|
quiz: useQuizStore(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { currentQuiz } = storeToRefs(store.quiz);
|
||||||
|
|
||||||
|
const quiz = reactive<Quiz>({});
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
await store.quiz.fetchById(Number(props.dataId));
|
||||||
|
|
||||||
|
assignData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignData() {
|
||||||
|
Object.assign(quiz, currentQuiz.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
await loadData();
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevQuestion = () => {
|
||||||
|
if (step.value) {
|
||||||
|
step.value--;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextQuestion = () => {
|
||||||
|
if (step.value < 3) {
|
||||||
|
step.value++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
articles: null,
|
||||||
|
questionGeneral: [
|
||||||
|
{
|
||||||
|
answers: [
|
||||||
|
{
|
||||||
|
id: 260,
|
||||||
|
siteId: 1,
|
||||||
|
quizId: 4,
|
||||||
|
questionId: 511,
|
||||||
|
title: "Con ếch 1",
|
||||||
|
thumbnail: "",
|
||||||
|
description: "Con ếch 1",
|
||||||
|
type: 0,
|
||||||
|
isCorrect: true,
|
||||||
|
order: 1,
|
||||||
|
status: 6,
|
||||||
|
createdBy: 3,
|
||||||
|
createdOn: "2024-06-04T15:27:05.641243",
|
||||||
|
updatedBy: null,
|
||||||
|
updatedOn: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 259,
|
||||||
|
siteId: 1,
|
||||||
|
quizId: 4,
|
||||||
|
questionId: 511,
|
||||||
|
title: "Con ếch 2",
|
||||||
|
thumbnail: "",
|
||||||
|
description: "Con ếch 2",
|
||||||
|
type: 0,
|
||||||
|
isCorrect: false,
|
||||||
|
order: 1,
|
||||||
|
status: 6,
|
||||||
|
createdBy: 3,
|
||||||
|
createdOn: "2024-06-04T15:27:05.641243",
|
||||||
|
updatedBy: null,
|
||||||
|
updatedOn: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 258,
|
||||||
|
siteId: 1,
|
||||||
|
quizId: 4,
|
||||||
|
questionId: 511,
|
||||||
|
title: "Con ếch 3",
|
||||||
|
thumbnail: "",
|
||||||
|
description: "Con ếch 3",
|
||||||
|
type: 0,
|
||||||
|
isCorrect: false,
|
||||||
|
order: 3,
|
||||||
|
status: 6,
|
||||||
|
createdBy: 3,
|
||||||
|
createdOn: "2024-06-04T15:27:05.641243",
|
||||||
|
updatedBy: null,
|
||||||
|
updatedOn: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
responses: null,
|
||||||
|
id: 511,
|
||||||
|
siteId: 1,
|
||||||
|
quizId: 4,
|
||||||
|
title: "Con ếch bạn chọn sẽ tiết lộ bí quyết làm giàu",
|
||||||
|
thumbnail: "https://resource.vpress.vn/resources/1/private/13cee27a2bd93915479f049378cffdd3/caudo1-1717486185.jpg",
|
||||||
|
description: "Con ếch bạn chọn sẽ tiết lộ bí quyết làm giàu",
|
||||||
|
type: 1,
|
||||||
|
order: 1,
|
||||||
|
status: 6,
|
||||||
|
createdBy: 3,
|
||||||
|
createdOn: "2024-06-04T15:27:05.641243",
|
||||||
|
updatedBy: null,
|
||||||
|
updatedOn: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
answers: [
|
||||||
|
{
|
||||||
|
id: 257,
|
||||||
|
siteId: 1,
|
||||||
|
quizId: 4,
|
||||||
|
questionId: 510,
|
||||||
|
title: "Băng zôn",
|
||||||
|
thumbnail: "",
|
||||||
|
description: "Băng zôn",
|
||||||
|
type: 1,
|
||||||
|
isCorrect: true,
|
||||||
|
order: 1,
|
||||||
|
status: 6,
|
||||||
|
createdBy: 3,
|
||||||
|
createdOn: "2024-06-04T15:27:05.641243",
|
||||||
|
updatedBy: null,
|
||||||
|
updatedOn: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 256,
|
||||||
|
siteId: 1,
|
||||||
|
quizId: 4,
|
||||||
|
questionId: 510,
|
||||||
|
title: "Người đàn ông",
|
||||||
|
thumbnail: "",
|
||||||
|
description: "Người đàn ông",
|
||||||
|
type: 1,
|
||||||
|
isCorrect: true,
|
||||||
|
order: 2,
|
||||||
|
status: 6,
|
||||||
|
createdBy: 3,
|
||||||
|
createdOn: "2024-06-04T15:27:05.641243",
|
||||||
|
updatedBy: null,
|
||||||
|
updatedOn: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 255,
|
||||||
|
siteId: 1,
|
||||||
|
quizId: 4,
|
||||||
|
questionId: 510,
|
||||||
|
title: "Bánh sinh nhật",
|
||||||
|
thumbnail: "",
|
||||||
|
description: "Bánh sinh nhật",
|
||||||
|
type: 1,
|
||||||
|
isCorrect: false,
|
||||||
|
order: 3,
|
||||||
|
status: 6,
|
||||||
|
createdBy: 3,
|
||||||
|
createdOn: "2024-06-04T15:27:05.641243",
|
||||||
|
updatedBy: null,
|
||||||
|
updatedOn: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 254,
|
||||||
|
siteId: 1,
|
||||||
|
quizId: 4,
|
||||||
|
questionId: 510,
|
||||||
|
title: "Khác",
|
||||||
|
thumbnail: "",
|
||||||
|
description: "Khác",
|
||||||
|
type: 2,
|
||||||
|
isCorrect: false,
|
||||||
|
order: 4,
|
||||||
|
status: 6,
|
||||||
|
createdBy: 3,
|
||||||
|
createdOn: "2024-06-04T15:27:05.641243",
|
||||||
|
updatedBy: null,
|
||||||
|
updatedOn: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
responses: null,
|
||||||
|
id: 510,
|
||||||
|
siteId: 1,
|
||||||
|
quizId: 4,
|
||||||
|
title: "Những điều khả nghi nào trong bức hình này?",
|
||||||
|
thumbnail: "https://resource.vpress.vn/resources/1/private/13cee27a2bd93915479f049378cffdd3/câu-đố-2-1717486529.jpg",
|
||||||
|
description: "Đâu là điều khả nghi nhất trong bức hình này",
|
||||||
|
type: 2,
|
||||||
|
order: 2,
|
||||||
|
status: 6,
|
||||||
|
createdBy: 3,
|
||||||
|
createdOn: "2024-06-04T15:27:05.641243",
|
||||||
|
updatedBy: null,
|
||||||
|
updatedOn: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
responses: null,
|
||||||
|
id: 4,
|
||||||
|
siteId: 1,
|
||||||
|
title: "câu đố tháng 6",
|
||||||
|
code: "cau-do-thang-6",
|
||||||
|
type: 0,
|
||||||
|
startTime: "2024-06-04T15:00:00",
|
||||||
|
endTime: "2024-06-10T00:00:00",
|
||||||
|
settings: {
|
||||||
|
participantType: 3,
|
||||||
|
resultPublication: 1,
|
||||||
|
},
|
||||||
|
features: "Important;Feature",
|
||||||
|
taxonomy: "Biên tập",
|
||||||
|
keywords: "câu đố;tháng 6;e",
|
||||||
|
thumbnail: "https://resource.vpress.vn/resources/1/private/13cee27a2bd93915479f049378cffdd3/ret-20240603042609106.jpg",
|
||||||
|
description: "câu đố tháng 6 e",
|
||||||
|
order: 1,
|
||||||
|
status: 6,
|
||||||
|
createdBy: 3,
|
||||||
|
createdOn: "2024-06-04T14:40:08.617253",
|
||||||
|
updatedBy: 3,
|
||||||
|
updatedOn: "2024-06-04T15:23:59.964931",
|
||||||
|
};
|
||||||
|
|
||||||
|
const step = ref(0);
|
||||||
|
const beforeWidth = computed(() => (100 / Number(data.questionGeneral.length - 1)) * step.value);
|
||||||
|
|
||||||
|
const selectQuizAnswer = ref<any>([]);
|
||||||
|
|
||||||
|
data.questionGeneral.forEach((question) => {
|
||||||
|
switch (question.type) {
|
||||||
|
case 0:
|
||||||
|
selectQuizAnswer.value.push([]);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
selectQuizAnswer.value.push(0);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
selectQuizAnswer.value.push([]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function submitSend() {}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="inline-block px-4 py-2 shadow-xl rounded-lg bg-[#f5f5f5] !text-black-500">
|
||||||
|
<h5 class="underline decoration-gray-500 font-bold mb-2">Câu đố: {{ data?.title }}</h5>
|
||||||
|
|
||||||
|
<ul class="px-3">
|
||||||
|
<li v-for="(question, questionIndex) in data.questionGeneral" :key="questionIndex" class="mb-2">
|
||||||
|
<h5 class="mb-1 font-700 text-black-500">{{ `${questionIndex + 1}. ${question.title}` }}</h5>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li v-for="(answer, answerIndex) in question.answers" :key="answerIndex" class="flex items-center gap-1 py-1">
|
||||||
|
<input :id="`answer-${questionIndex}-${answerIndex}`" :type="question.type === 1 ? 'radio' : 'checkbox'" :value="answerIndex" v-model="selectQuizAnswer[questionIndex]" />
|
||||||
|
<label :for="`answer-${questionIndex}-${answerIndex}`" class="font-semibold">{{ answer.title }}</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button @click="submitSend" class="bg-primary-500 text-white py-1 px-3 rounded-4px cursor-pointer hover:bg-primary-600 float-right">Gửi câu trả lời</button>
|
||||||
|
</div>
|
||||||
|
<!-- <div>
|
||||||
|
<h5 class="text-black text-18px font-700">{{ data?.title }}</h5>
|
||||||
|
<template v-if="data.questionGeneral.length > 1">
|
||||||
|
<ul
|
||||||
|
:style="{ '--before-width': beforeWidth + '%' }"
|
||||||
|
class="progress flex items-center justify-between relative after:content-[''] after:absolute after:top-50% after:translate-y--50% after:w-full after:h-1 after:bg-gray-200 before:content-[''] before:absolute before:top-50% before:translate-y--50% before:h-1 before:bg-primary-500 before:z-2 before:transition-all before:ease-linear before:duration-300"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="(index, item) in data.questionGeneral.length"
|
||||||
|
:key="index"
|
||||||
|
:class="step >= index - 1 ? 'bg-primary-500 text-white transition-all delay-300' : 'bg-white text-primary'"
|
||||||
|
class="relative z-3 w-7 h-7 rounded-full flex items-center justify-center border-2 border-solid border-primary-500"
|
||||||
|
>
|
||||||
|
<template template v-if="step > index - 1"><Icon name="material-symbols:check-rounded" class="text-22px" /></template>
|
||||||
|
<template v-else>{{ item }}</template>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<template v-for="(item, index) in data.questionGeneral" :key="index">
|
||||||
|
<div v-show="step === index">
|
||||||
|
{{ item.title }} => {{ index }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="bg-primary-500 text-white px-2 py-2 rounded-4px" @click="prevQuestion()">Câu trước</button>
|
||||||
|
<button class="bg-primary-500 text-white px-2 py-2 rounded-4px" @click="nextQuestion()">Câu tiếp theo</button>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:root {
|
||||||
|
--before-width: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
&::before {
|
||||||
|
width: var(--before-width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useSurveyStore } from "~/stores/survey";
|
||||||
|
import type { Survey } from "~/server/models/survey";
|
||||||
|
|
||||||
|
const props = defineProps<{ dataId?: string }>();
|
||||||
|
|
||||||
|
const store = reactive({
|
||||||
|
survey: useSurveyStore(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { currentSurvey } = storeToRefs(store.survey);
|
||||||
|
|
||||||
|
const survey = reactive<Survey>({});
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
await store.survey.fetchById(Number(props.dataId));
|
||||||
|
|
||||||
|
assignData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignData() {
|
||||||
|
Object.assign(survey, currentSurvey.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
await loadData();
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataSurvey = {
|
||||||
|
"articles": null,
|
||||||
|
"questionGeneral": [
|
||||||
|
{
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"id": 85,
|
||||||
|
"siteId": 1,
|
||||||
|
"surveyId": 10,
|
||||||
|
"questionId": 84,
|
||||||
|
"title": "Không",
|
||||||
|
"thumbnail": "",
|
||||||
|
"description": "",
|
||||||
|
"type": 1,
|
||||||
|
"isCorrect": false,
|
||||||
|
"order": 2,
|
||||||
|
"status": 6,
|
||||||
|
"createdBy": 3,
|
||||||
|
"createdOn": "2024-06-04T17:13:45.794056",
|
||||||
|
"updatedBy": null,
|
||||||
|
"updatedOn": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 84,
|
||||||
|
"siteId": 1,
|
||||||
|
"surveyId": 10,
|
||||||
|
"questionId": 84,
|
||||||
|
"title": "Có",
|
||||||
|
"thumbnail": "",
|
||||||
|
"description": "",
|
||||||
|
"type": 1,
|
||||||
|
"isCorrect": true,
|
||||||
|
"order": 1,
|
||||||
|
"status": 6,
|
||||||
|
"createdBy": 3,
|
||||||
|
"createdOn": "2024-06-04T17:13:45.794056",
|
||||||
|
"updatedBy": null,
|
||||||
|
"updatedOn": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": null,
|
||||||
|
"id": 84,
|
||||||
|
"siteId": 1,
|
||||||
|
"surveyId": 10,
|
||||||
|
"title": "Bạn có chọn xe công nghệ để di chuyển trong giờ cao điểm không?",
|
||||||
|
"thumbnail": "",
|
||||||
|
"description": "",
|
||||||
|
"type": 0,
|
||||||
|
"order": 3,
|
||||||
|
"status": 6,
|
||||||
|
"createdBy": 3,
|
||||||
|
"createdOn": "2024-06-04T17:13:45.794056",
|
||||||
|
"updatedBy": null,
|
||||||
|
"updatedOn": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"id": 83,
|
||||||
|
"siteId": 1,
|
||||||
|
"surveyId": 10,
|
||||||
|
"questionId": 83,
|
||||||
|
"title": "Xe bus",
|
||||||
|
"thumbnail": "",
|
||||||
|
"description": "",
|
||||||
|
"type": 1,
|
||||||
|
"isCorrect": false,
|
||||||
|
"order": 3,
|
||||||
|
"status": 6,
|
||||||
|
"createdBy": 3,
|
||||||
|
"createdOn": "2024-06-04T17:13:45.794056",
|
||||||
|
"updatedBy": null,
|
||||||
|
"updatedOn": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 82,
|
||||||
|
"siteId": 1,
|
||||||
|
"surveyId": 10,
|
||||||
|
"questionId": 83,
|
||||||
|
"title": "Xe đạp",
|
||||||
|
"thumbnail": "",
|
||||||
|
"description": "",
|
||||||
|
"type": 1,
|
||||||
|
"isCorrect": false,
|
||||||
|
"order": 2,
|
||||||
|
"status": 6,
|
||||||
|
"createdBy": 3,
|
||||||
|
"createdOn": "2024-06-04T17:13:45.794056",
|
||||||
|
"updatedBy": null,
|
||||||
|
"updatedOn": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 81,
|
||||||
|
"siteId": 1,
|
||||||
|
"surveyId": 10,
|
||||||
|
"questionId": 83,
|
||||||
|
"title": "Xe máy",
|
||||||
|
"thumbnail": "",
|
||||||
|
"description": "",
|
||||||
|
"type": 1,
|
||||||
|
"isCorrect": true,
|
||||||
|
"order": 1,
|
||||||
|
"status": 6,
|
||||||
|
"createdBy": 3,
|
||||||
|
"createdOn": "2024-06-04T17:13:45.794056",
|
||||||
|
"updatedBy": null,
|
||||||
|
"updatedOn": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": null,
|
||||||
|
"id": 83,
|
||||||
|
"siteId": 1,
|
||||||
|
"surveyId": 10,
|
||||||
|
"title": "Bạn thường di chuyển bằng phương tiện gì?",
|
||||||
|
"thumbnail": "",
|
||||||
|
"description": "",
|
||||||
|
"type": 1,
|
||||||
|
"order": 2,
|
||||||
|
"status": 6,
|
||||||
|
"createdBy": 3,
|
||||||
|
"createdOn": "2024-06-04T17:13:45.794056",
|
||||||
|
"updatedBy": null,
|
||||||
|
"updatedOn": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"id": 80,
|
||||||
|
"siteId": 1,
|
||||||
|
"surveyId": 10,
|
||||||
|
"questionId": 82,
|
||||||
|
"title": "21 lần trở lên",
|
||||||
|
"thumbnail": "",
|
||||||
|
"description": "",
|
||||||
|
"type": 1,
|
||||||
|
"isCorrect": false,
|
||||||
|
"order": 3,
|
||||||
|
"status": 6,
|
||||||
|
"createdBy": 3,
|
||||||
|
"createdOn": "2024-06-04T17:13:45.794056",
|
||||||
|
"updatedBy": null,
|
||||||
|
"updatedOn": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 79,
|
||||||
|
"siteId": 1,
|
||||||
|
"surveyId": 10,
|
||||||
|
"questionId": 82,
|
||||||
|
"title": "14 - 21 lần",
|
||||||
|
"thumbnail": "",
|
||||||
|
"description": "",
|
||||||
|
"type": 1,
|
||||||
|
"isCorrect": false,
|
||||||
|
"order": 0,
|
||||||
|
"status": 6,
|
||||||
|
"createdBy": 3,
|
||||||
|
"createdOn": "2024-06-04T17:13:45.794056",
|
||||||
|
"updatedBy": null,
|
||||||
|
"updatedOn": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 78,
|
||||||
|
"siteId": 1,
|
||||||
|
"surveyId": 10,
|
||||||
|
"questionId": 82,
|
||||||
|
"title": "7 lần",
|
||||||
|
"thumbnail": "",
|
||||||
|
"description": "",
|
||||||
|
"type": 1,
|
||||||
|
"isCorrect": true,
|
||||||
|
"order": 1,
|
||||||
|
"status": 6,
|
||||||
|
"createdBy": 3,
|
||||||
|
"createdOn": "2024-06-04T17:13:45.794056",
|
||||||
|
"updatedBy": null,
|
||||||
|
"updatedOn": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": null,
|
||||||
|
"id": 82,
|
||||||
|
"siteId": 1,
|
||||||
|
"surveyId": 10,
|
||||||
|
"title": "Mỗi tuần bạn di chuyển với tần suất bao nhiêu lần?",
|
||||||
|
"thumbnail": "",
|
||||||
|
"description": "Mỗi tuần bạn di chuyển với tần suất bao nhiêu lần?",
|
||||||
|
"type": 1,
|
||||||
|
"order": 1,
|
||||||
|
"status": 6,
|
||||||
|
"createdBy": 3,
|
||||||
|
"createdOn": "2024-06-04T17:13:45.794056",
|
||||||
|
"updatedBy": null,
|
||||||
|
"updatedOn": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": null,
|
||||||
|
"id": 10,
|
||||||
|
"siteId": 1,
|
||||||
|
"title": "Thói quen di chuyển trong giờ cao điểm",
|
||||||
|
"code": "thoi-quen-di-chuyen-trong-gio-cao-diem",
|
||||||
|
"type": 0,
|
||||||
|
"startTime": "2024-06-04T17:18:00",
|
||||||
|
"endTime": "2024-06-20T00:00:00",
|
||||||
|
"settings": {
|
||||||
|
"participantType": 3,
|
||||||
|
"resultPublication": 2
|
||||||
|
},
|
||||||
|
"features": "Feature",
|
||||||
|
"taxonomy": "Biên tập",
|
||||||
|
"keywords": "thoiquendichuyen;giocaodiem",
|
||||||
|
"thumbnail": "https://resource.vpress.vn/resources/1/private/13cee27a2bd93915479f049378cffdd3/thoiquendichuyentronggiocaodiem-20240604100659862.png",
|
||||||
|
"description": "Thói quen di chuyển trong giờ cao điểm",
|
||||||
|
"order": 1,
|
||||||
|
"status": 6,
|
||||||
|
"createdBy": 3,
|
||||||
|
"createdOn": "2024-06-04T17:13:45.653177",
|
||||||
|
"updatedBy": null,
|
||||||
|
"updatedOn": null
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const selectSurveyAnswer = ref<any>([])
|
||||||
|
|
||||||
|
dataSurvey.questionGeneral.forEach((question) => {
|
||||||
|
switch (question.type) {
|
||||||
|
case 0:
|
||||||
|
selectSurveyAnswer.value.push([])
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
selectSurveyAnswer.value.push(0)
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
selectSurveyAnswer.value.push([])
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function submitSend() {
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="inline-block px-4 py-2 shadow-xl rounded-lg bg-[#f5f5f5] !text-black-500">
|
||||||
|
<h5 class="underline decoration-gray-500 font-bold mb-2">Khảo sát: {{ dataSurvey?.title }}</h5>
|
||||||
|
|
||||||
|
<ul class="px-3">
|
||||||
|
<li v-for="(question, questionIndex) in dataSurvey.questionGeneral" :key="questionIndex" class="mb-2">
|
||||||
|
<h5 class="mb-1 font-700 text-black-500">{{ `${questionIndex + 1}. ${question.title}` }}</h5>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li v-for="(answer, answerIndex) in question.answers" :key="answerIndex" class="flex items-center gap-1 py-1">
|
||||||
|
<input :id="`answer-survey-${questionIndex}-${answerIndex}`" :type="question.type === 1 ? 'radio' : 'checkbox'" :value="answerIndex" v-model="selectSurveyAnswer[questionIndex]">
|
||||||
|
<label :for="`answer-survey-${questionIndex}-${answerIndex}`" class="font-semibold">{{ answer.title }}</label>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button @click="submitSend" class="bg-primary-500 text-white py-1 px-3 rounded-4px cursor-pointer hover:bg-primary-600 float-right">Gửi câu trả lời</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:root {
|
||||||
|
--before-width: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
dataCode?: string,
|
||||||
|
dataTitle?: string,
|
||||||
|
style?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const title = computed(() => props.dataTitle ?? '')
|
||||||
|
const code = computed(() => props.dataCode ?? '')
|
||||||
|
const style = computed(() => props.style ?? '')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a href="#" :style="style" class="!no-underline !px-2">{{ title }} 1</a>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { enumPageComponentLayouts, enumPageComponentTemplate, enumPageComponentKey } from "@/definitions/enum";
|
||||||
|
import DynamicComponent from "~/components/dynamic-page/page-component/templates/index.vue";
|
||||||
|
import { useDynamicPageStore } from '~/stores/dynamic-page';
|
||||||
|
const { currentPage } = storeToRefs(useDynamicPageStore());
|
||||||
|
const props = defineProps<{
|
||||||
|
type?: any; // [TOP_NAVIGATION, BOTTOM_NAVIGATION]
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const defineTypeRecusive = {
|
||||||
|
TOP_NAVIGATION: enumPageComponentLayouts[`${enumPageComponentTemplate[enumPageComponentKey.NAVIGATION]['TOP']}`]['NAVIGATION_TOP_DEFAULT'],
|
||||||
|
BOTTOM_NAVIGATION: enumPageComponentLayouts[`${enumPageComponentTemplate[enumPageComponentKey.NAVIGATION]['BOTTOM']}`]['NAVIGATION_BOTTOM_DEFAULT'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const findDataPosition = computed<any>(() => {
|
||||||
|
let result = {};
|
||||||
|
switch (props.type) {
|
||||||
|
case defineTypeRecusive.TOP_NAVIGATION:
|
||||||
|
result = currentPage.value.components && currentPage.value.components.find((component: any) => {
|
||||||
|
return component.taxonomy === enumPageComponentKey.NAVIGATION && component.settings?.layout === defineTypeRecusive.TOP_NAVIGATION
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case defineTypeRecusive.BOTTOM_NAVIGATION:
|
||||||
|
result = currentPage.value.components && currentPage.value.components.find((component: any) => {
|
||||||
|
return component.taxonomy === enumPageComponentKey.NAVIGATION && component.settings?.layout === defineTypeRecusive.BOTTOM_NAVIGATION
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result = {};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<DynamicComponent
|
||||||
|
v-if="findDataPosition && findDataPosition?.id"
|
||||||
|
:settings="findDataPosition?.settings"
|
||||||
|
:component="findDataPosition"
|
||||||
|
:content="findDataPosition?.content"
|
||||||
|
/>
|
||||||
|
<div v-else class="text-center">
|
||||||
|
<span>Hãy tạo thành phần "Thanh điều hướng ở đầu trang" để hiển thị thành điều hướng tại đây</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
<template>
|
||||||
|
<div class="pt-5">
|
||||||
|
<div class="content p-3">
|
||||||
|
<span class="text-12px text-[#AFADB5] text-end block">Quảng cáo</span>
|
||||||
|
<img class="block w-full h-full" src="/assets/images/tienphong/main-ads-2.jpg" alt="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.content {
|
||||||
|
font-size: 18px;
|
||||||
|
background-color: #eeeeee;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
<template>
|
||||||
|
<div class="pt-5">
|
||||||
|
<div class="content p-3 border-y-1px border-solid border-#000">
|
||||||
|
<img class="mx-auto max-h-[300px] object-cover" src="/assets/images/tienphong/ads_full.png" alt="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.content {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Default_Ads } from './Default.vue'
|
||||||
|
export { default as Main_Ads } from './Main.vue'
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { Default_Ads, Main_Ads } from "./index";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
content?: any;
|
||||||
|
}>();
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentLayouts[`${enumPageComponentTemplate[enumPageComponentKey.ADVERTISING]['ADVERTISING']}`]['DEFAULT']]: Default_Ads,
|
||||||
|
[enumPageComponentLayouts[`${enumPageComponentTemplate[enumPageComponentKey.ADVERTISING]['ADVERTISING']}`]['MAIN']]: Main_Ads,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = computed(() => _props.settings.layout);
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings, content: _props.content }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as Advertisings } from './advertisings/index.vue'
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { Advertisings } from "./index";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentTemplate[enumPageComponentKey.ADVERTISING]["ADVERTISING"]]: Advertisings,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = computed(() => `${_props.settings.template}`);
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// console.log(getCurrentComponent.value, 'quảng caosd ád')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const type = ref("");
|
||||||
|
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||||
|
import { DEFAULT_QUERY_DROP, getInputValue } from "@/utils/parseSQL";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
dataResult?: any;
|
||||||
|
dataType?: any;
|
||||||
|
dataQuery?: any;
|
||||||
|
layout?: string;
|
||||||
|
label?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const LAYOUT_PARSE = computed(() => {
|
||||||
|
const designObject = props.label ? getInputValue(props.label, "OBJECT") : {};
|
||||||
|
return Object.assign({}, designObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseData = computed(() => {
|
||||||
|
if (!props.dataResult) return;
|
||||||
|
const result = getInputValue(props.dataResult, "OBJECT");
|
||||||
|
switch (result?.contentType) {
|
||||||
|
case 1:
|
||||||
|
type.value = "";
|
||||||
|
// type.value = "Image";
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
type.value = "Image";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
type.value = "Postcard";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
type.value = "Video";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
if (result?.layoutType === 4) {
|
||||||
|
type.value = "Emagazine";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (result?.layoutType === 3) {
|
||||||
|
type.value = "Infographics";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article class="card-audio" :class="LAYOUT_PARSE['article_Class']" :style="LAYOUT_PARSE['article']">
|
||||||
|
<img :src="parseData?.thumbnail ? parseData?.thumbnail : 'https://indiaeducationdiary.in/wp-content/uploads/2021/02/SD-default-image.png'" :alt="parseData?.title?.replace(/<[^>]+>/g, '')" />
|
||||||
|
<div class="card-audio__content">
|
||||||
|
<span>
|
||||||
|
<template v-if="type === 'Image'">
|
||||||
|
<svg width="28" height="22" viewBox="0 0 28 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M25.25 0.5H7.25C5.98438 0.5 5 1.53125 5 2.75V14.75C5 16.0156 5.98438 17 7.25 17H25.25C26.4688 17 27.5 16.0156 27.5 14.75V2.75C27.5 1.53125 26.4688 0.5 25.25 0.5ZM10.9531 3.5C11.75 3.5 12.4531 4.20312 12.4531 5C12.4531 5.84375 11.7969 6.5 10.9531 6.5C10.1094 6.5 9.45312 5.84375 9.45312 5C9.45312 4.20312 10.1562 3.5 10.9531 3.5ZM23.6562 13.625C23.5156 13.8594 23.2344 14 23 14H9.5C9.17188 14 8.9375 13.8594 8.79688 13.625C8.70312 13.3438 8.70312 13.0625 8.89062 12.8281L12.1719 8.32812C12.3125 8.14062 12.5 8 12.7812 8C13.0156 8 13.2031 8.14062 13.3438 8.32812L14.4219 9.78125L17.375 5.375C17.4688 5.14062 17.7031 5 17.9844 5C18.2188 5 18.4531 5.14062 18.5938 5.375L23.6094 12.875C23.75 13.0625 23.75 13.3906 23.6562 13.625ZM21.875 19.25H6.125C4.25 19.25 2.75 17.75 2.75 15.875V4.625C2.75 4.01562 2.23438 3.5 1.625 3.5C0.96875 3.5 0.5 4.01562 0.5 4.625V15.875C0.5 19.0156 2.98438 21.5 6.125 21.5H21.875C22.4844 21.5 23 21.0312 23 20.375C23 19.7656 22.4844 19.25 21.875 19.25Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="card-audio__type-category">
|
||||||
|
<div class="card-audio__type" v-if="type">{{ type }}</div>
|
||||||
|
<nuxt-link to="#" class="card-audio__category" :style="LAYOUT_PARSE['category-article']" :class="LAYOUT_PARSE['category-article_Class']">{{
|
||||||
|
parseData?.category?.title
|
||||||
|
}}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 :class="LAYOUT_PARSE['title_Class']" :style="LAYOUT_PARSE['h3.title']">
|
||||||
|
<nuxt-link :to="`/bai-viet/${parseData?.code}`"><span v-html="parseData?.title"></span> </nuxt-link>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<div v-if="LAYOUT_PARSE.styleClasses" v-html="LAYOUT_PARSE.styleClasses" style="display:none;"></div>
|
||||||
|
<!-- <div>
|
||||||
|
aaaaaaa
|
||||||
|
</div> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.card-audio {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: calc((16 / 9) * 100%);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-audio__content {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 20px 30px;
|
||||||
|
z-index: 3;
|
||||||
|
text-align: center;
|
||||||
|
h2 {
|
||||||
|
color: #fff;
|
||||||
|
margin: 12px 0 20px 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 130%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #9e1e0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-audio__type-category {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
.card-audio__type,
|
||||||
|
.card-audio__category {
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 180%;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-audio__type {
|
||||||
|
background-color: #ed1c24;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-audio__category {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.empty-block {
|
||||||
|
background-color: #409eff;
|
||||||
|
height: 100px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||||
|
import { DEFAULT_QUERY_DROP } from "@/utils/parseSQL";
|
||||||
|
import { getInputValue } from "@/utils/parseSQL";
|
||||||
|
import { formatDate } from "@/utils/filters";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
dataResult?: any;
|
||||||
|
dataType?: any;
|
||||||
|
dataQuery?: any;
|
||||||
|
layout?: string;
|
||||||
|
label?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const LAYOUT_PARSE = computed(() => {
|
||||||
|
const designObject = props.label ? getInputValue(props.label, "OBJECT") : {};
|
||||||
|
return Object.assign({}, designObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseData = computed(() => {
|
||||||
|
if (!props.dataResult) return;
|
||||||
|
const result = getInputValue(props.dataResult, "OBJECT");
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article
|
||||||
|
v-if="parseData"
|
||||||
|
class="basic-article border-custom"
|
||||||
|
:class="LAYOUT_PARSE['article_Class']"
|
||||||
|
:style="LAYOUT_PARSE['article']"
|
||||||
|
>
|
||||||
|
<div class="basic-article_thumbnail" :class="LAYOUT_PARSE['thumbnail_Class']" :style="LAYOUT_PARSE['div.basic-article_thumbnail']">
|
||||||
|
<template v-if="parseData">
|
||||||
|
<nuxt-link :to="`${parseData.code}`">
|
||||||
|
<img class="object-fit-cover" :src="parseData.thumbnail ? parseData.thumbnail : '/images/default-thumbnail.jpg'" :alt="parseData.title?.replace(/<[^>]+>/g, '')" />
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
<span v-else class="empty-block" style="width: 100%; height: 100%; min-height: 50px"></span>
|
||||||
|
</div>
|
||||||
|
<div class="basic-article_content" :class="[!parseData && 'no-data']">
|
||||||
|
<template v-if="parseData?.topics && parseData?.topics.length > 0">
|
||||||
|
<nuxt-link class="article-card-default__topic" :to="`/${parseData?.topics[0].code}`" :style="LAYOUT_PARSE['topic']">
|
||||||
|
<h5><nuxt-link :to="`/topic/${parseData?.topics[0].code}`">
|
||||||
|
{{ parseData?.topics[0].title }}</nuxt-link></h5>
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
<h3 class="line-clamp" :class="LAYOUT_PARSE['title_Class']" :style="LAYOUT_PARSE['h3.title']">
|
||||||
|
<template v-if="parseData">
|
||||||
|
<nuxt-link :to="`/bai-viet/${parseData.code}`">
|
||||||
|
{{ parseData.title?.replace(/<[^>]+>/g, "") }}
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
<span v-else class="empty-block" style="height: 8px"></span>
|
||||||
|
</h3>
|
||||||
|
<div class="article-card-default__bottom" v-if="LAYOUT_PARSE.layout === 'row'">
|
||||||
|
<span :style="LAYOUT_PARSE['time']" style="margin-right: 5px" :class="LAYOUT_PARSE['time_Class']">{{
|
||||||
|
formatDate(String(parseData?.createdOn), "DD/MM/YYYY | HH:mm")
|
||||||
|
}}</span>
|
||||||
|
<nuxt-link :style="LAYOUT_PARSE['category-article']" :class="LAYOUT_PARSE['category-article_Class']">{{ parseData?.category?.title }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<p class="mb-0 line-clamp" :class="LAYOUT_PARSE['paragraph_Class']" :style="LAYOUT_PARSE['p.paragraph']">
|
||||||
|
<template v-if="parseData">
|
||||||
|
{{ parseData.intro?.replace(/<[^>]+>/g, "") }}
|
||||||
|
</template>
|
||||||
|
<span v-else class="empty-block" style="height: 5px"></span>
|
||||||
|
</p>
|
||||||
|
<div class="article-card-default__bottom" v-if="LAYOUT_PARSE?.layout !== 'row'" :style="LAYOUT_PARSE['metadata']">
|
||||||
|
<span :style="LAYOUT_PARSE['time']" style="margin-right: 5px" :class="LAYOUT_PARSE['time_Class']">{{
|
||||||
|
formatDate(String(parseData?.createdOn), "DD/MM/YYYY | HH:mm")
|
||||||
|
}}</span>
|
||||||
|
<nuxt-link :style="LAYOUT_PARSE['category-article']" :class="LAYOUT_PARSE['category-article_Class']">{{ parseData?.category?.title }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<div v-html="LAYOUT_PARSE.styleClasses" style="display:none;"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.article-card-default {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.article-card-default__title {
|
||||||
|
color: #000;
|
||||||
|
h2 {
|
||||||
|
display: inline-block;
|
||||||
|
/* margin: 12px 0 20px 0; */
|
||||||
|
font-size: 24px;
|
||||||
|
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 130%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #9e1e0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 150%;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__thumbnail {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-column {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-card-default__topic {
|
||||||
|
position: relative;
|
||||||
|
/* margin: 0 0 12px 12px; */
|
||||||
|
|
||||||
|
// background-color: #151411;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
// color: #fff;
|
||||||
|
padding: 0 12px;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid #000;
|
||||||
|
line-height: 180%;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 12px;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #ed1c24;
|
||||||
|
left: -12px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-card-default__bottom {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.35);
|
||||||
|
/* margin-top: 10px; */
|
||||||
|
a {
|
||||||
|
color: #ed1c24;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
h3,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.basic-article {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
height: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
background-size: cover;
|
||||||
|
flex-direction: column;
|
||||||
|
&.no-data {
|
||||||
|
gap: 5px !important;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.line-clamp {
|
||||||
|
display: -webkit-box;
|
||||||
|
/* -webkit-line-clamp: 3; */
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basic-article_thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
&.border-custom {
|
||||||
|
border-color: #e5e5e5 !important;
|
||||||
|
}
|
||||||
|
/* &.horizontal {
|
||||||
|
flex-direction: row;
|
||||||
|
.basic-article_thumbnail {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
&.reverse {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
&_thumbnail {
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
aspect-ratio: 16/10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&_content {
|
||||||
|
/* padding: 10px 0px; */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
flex: 1;
|
||||||
|
&.no-data {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
/* margin-top: 10px; */
|
||||||
|
opacity: 85%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-block {
|
||||||
|
background-color: #409eff;
|
||||||
|
height: 100px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||||
|
import { DEFAULT_QUERY_DROP } from "@/utils/parseSQL";
|
||||||
|
import { getInputValue } from "@/utils/parseSQL";
|
||||||
|
import { formatDate } from "@/utils/filters";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
dataResult?: any;
|
||||||
|
dataType?: any;
|
||||||
|
dataQuery?: any;
|
||||||
|
layout?: string;
|
||||||
|
label?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const LAYOUT_PARSE = computed(() => {
|
||||||
|
const designObject = props.label ? getInputValue(props.label, "OBJECT") : {};
|
||||||
|
return Object.assign({}, designObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["selectComponent", "dropData"]);
|
||||||
|
|
||||||
|
const selectComponent = () => {
|
||||||
|
emit("selectComponent");
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseData = computed(() => {
|
||||||
|
if (!props.dataResult) return;
|
||||||
|
const result = getInputValue(props.dataResult, "OBJECT");
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const drop = (e: any) => {
|
||||||
|
if (e.dataTransfer.getData(`${enumPageComponentTemplates.ARTICLE}`)) {
|
||||||
|
const data = e.dataTransfer.getData(`${enumPageComponentTemplates.ARTICLE}`);
|
||||||
|
const { dataType, dataResult } = JSON.parse(data);
|
||||||
|
const dataQuery = DEFAULT_QUERY_DROP(dataType, dataResult.id);
|
||||||
|
emit("dropData", {
|
||||||
|
dataType,
|
||||||
|
dataResult,
|
||||||
|
dataQuery: dataQuery,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article class="basic-article border-custom" :class="LAYOUT_PARSE['article_Class']" :style="LAYOUT_PARSE['article']">
|
||||||
|
<div class="article_miss">
|
||||||
|
<template v-if="parseData">
|
||||||
|
<div class="article_miss_thumb custom-thumb" :style="{ backgroundImage: `url('${parseData.thumbnail ? parseData.thumbnail : '/images/default-thumbnail.jpg'}')` }"></div>
|
||||||
|
<div class="article_miss_content" :style="LAYOUT_PARSE['content']">
|
||||||
|
<h3 class="line-clamp text-white" :class="LAYOUT_PARSE['title_Class']" :style="LAYOUT_PARSE['h3.title']">
|
||||||
|
{{ parseData.title?.replace(/<[^>]+>/g, "") }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="empty-box"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-html="LAYOUT_PARSE.styleClasses" v-if="LAYOUT_PARSE.styles" style="display: none"></div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.article_miss {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
.article_miss_thumb {
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article_miss_content {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
bottom: -30px;
|
||||||
|
background-color: rgba(255, 93, 2, 0.7);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
width: 80%;
|
||||||
|
left: 10%;
|
||||||
|
padding: 16px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 130%;
|
||||||
|
text-align: center;
|
||||||
|
// margin-bottom: 12px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.empty-box {
|
||||||
|
background-color: #409eff;
|
||||||
|
min-height: 60px;
|
||||||
|
height: 100%;
|
||||||
|
i {
|
||||||
|
font-size: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||||
|
import { DEFAULT_QUERY_DROP, getInputValue } from "@/utils/parseSQL";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
dataResult?: any;
|
||||||
|
dataType?: any;
|
||||||
|
dataQuery?: any;
|
||||||
|
layout?: string;
|
||||||
|
label?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const LAYOUT_PARSE = computed(() => {
|
||||||
|
const designObject = props.label ? getInputValue(props.label, "OBJECT") : {};
|
||||||
|
return Object.assign({}, designObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseData = computed(() => {
|
||||||
|
if (!props.dataResult) return;
|
||||||
|
const result = getInputValue(props.dataResult, "OBJECT");
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article
|
||||||
|
class="basic-article border-custom"
|
||||||
|
:class="LAYOUT_PARSE['article_Class']"
|
||||||
|
:style="LAYOUT_PARSE['article']"
|
||||||
|
>
|
||||||
|
<!-- <div class="basic-article_thumbnail" :class="LAYOUT_PARSE['thumbnail_Class']" :style="LAYOUT_PARSE['div.basic-article_thumbnail']">
|
||||||
|
<template v-if="parseData">
|
||||||
|
<img class="object-fit-cover" :src="parseData.thumbnail ? parseData.thumbnail : '/images/default-thumbnail.jpg'" :alt="parseData.title?.replace(/<[^>]+>/g, '')" />
|
||||||
|
</template>
|
||||||
|
<span v-else class="empty-block" style="width: 100%; height: 100%; min-height: 50px"></span>
|
||||||
|
</div>
|
||||||
|
<div class="basic-article_content" :class="[!parseData && 'no-data']">
|
||||||
|
<template v-if="parseData?.topics && parseData?.topics.length > 0">
|
||||||
|
<nuxt-link class="article-card-default__topic" :to="`/${parseData?.topics[0].code}`" :style="LAYOUT_PARSE['topic']">
|
||||||
|
<h5>{{ parseData?.topics[0].title }}</h5>
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
<h3 class="line-clamp" :class="LAYOUT_PARSE['title_Class']" :style="LAYOUT_PARSE['h3.title']">
|
||||||
|
<template v-if="parseData">
|
||||||
|
{{ parseData.title?.replace(/<[^>]+>/g, "") }}
|
||||||
|
</template>
|
||||||
|
<span v-else class="empty-block" style="height: 8px"></span>
|
||||||
|
</h3>
|
||||||
|
<div class="article-card-default__bottom" v-if="LAYOUT_PARSE.layout === 'row'">
|
||||||
|
<span :style="LAYOUT_PARSE['time']" style="margin-right: 5px" :class="LAYOUT_PARSE['time_Class']">{{
|
||||||
|
formatDate(String(parseData?.createdOn), "DD/MM/YYYY | HH:mm")
|
||||||
|
}}</span>
|
||||||
|
<nuxt-link :style="LAYOUT_PARSE['category-article']" :class="LAYOUT_PARSE['category-article_Class']">{{ parseData?.category?.title }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<p class="mb-0 line-clamp" :class="LAYOUT_PARSE['paragraph_Class']" :style="LAYOUT_PARSE['p.paragraph']">
|
||||||
|
<template v-if="parseData">
|
||||||
|
{{ parseData.intro?.replace(/<[^>]+>/g, "") }}
|
||||||
|
</template>
|
||||||
|
<span v-else class="empty-block" style="height: 5px"></span>
|
||||||
|
</p>
|
||||||
|
<div class="article-card-default__bottom" v-if="LAYOUT_PARSE?.layout !== 'row'" :style="LAYOUT_PARSE['metadata']">
|
||||||
|
<span :style="LAYOUT_PARSE['time']" style="margin-right: 5px" :class="LAYOUT_PARSE['time_Class']">{{
|
||||||
|
formatDate(String(parseData?.createdOn), "DD/MM/YYYY | HH:mm")
|
||||||
|
}}</span>
|
||||||
|
<nuxt-link :style="LAYOUT_PARSE['category-article']" :class="LAYOUT_PARSE['category-article_Class']">{{ parseData?.category?.title }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="article_video">
|
||||||
|
<template v-if="parseData">
|
||||||
|
<div class="article_video_container">
|
||||||
|
<div
|
||||||
|
class="article_video_thumb"
|
||||||
|
:style="{ backgroundImage: `url('${parseData.thumbnail ? parseData.thumbnail : '/images/default-thumbnail.jpg'}')` }"
|
||||||
|
>
|
||||||
|
<Icon name="ri:play-circle-line" />
|
||||||
|
</div>
|
||||||
|
<div class="article_video_content">
|
||||||
|
<div>
|
||||||
|
<h3 class="line-clamp" :class="LAYOUT_PARSE['title_Class']" :style="LAYOUT_PARSE['h3.title']">
|
||||||
|
{{ parseData.title?.replace(/<[^>]+>/g, "") }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="LAYOUT_PARSE.styleClasses" v-html="LAYOUT_PARSE.styleClasses"></div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.article_video {
|
||||||
|
.article_video_container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.article_video_thumb {
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
width: 250px;
|
||||||
|
height: 140px;
|
||||||
|
border-radius: 2px;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 1;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
font-size: 40px;
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article_video_content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 10px;
|
||||||
|
> div {
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 24px;
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.empty-box {
|
||||||
|
margin: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
.empty-block {
|
||||||
|
width: 50%;
|
||||||
|
> div {
|
||||||
|
background-color: #409eff;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i {
|
||||||
|
font-size: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||||
|
import { DEFAULT_QUERY_DROP, getInputValue } from "@/utils/parseSQL";
|
||||||
|
import { getResource } from "@/utils/resourceHandler";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
dataResult?: any;
|
||||||
|
dataType?: any;
|
||||||
|
dataQuery?: any;
|
||||||
|
layout?: string;
|
||||||
|
label?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const LAYOUT_PARSE = computed(() => {
|
||||||
|
const designObject = props.label ? getInputValue(props.label, "OBJECT") : {};
|
||||||
|
return Object.assign({}, designObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseData = computed(() => {
|
||||||
|
if (!props.dataResult) return;
|
||||||
|
const result = getInputValue(props.dataResult, "OBJECT");
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const playVideo = ref<boolean>(false)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getResource(JSON.parse(props.dataResult).detail)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article
|
||||||
|
class="basic-article border-custom"
|
||||||
|
:class="LAYOUT_PARSE['article_Class']"
|
||||||
|
:style="LAYOUT_PARSE['article']"
|
||||||
|
>
|
||||||
|
<div class="article_video">
|
||||||
|
<template v-if="parseData">
|
||||||
|
<div
|
||||||
|
v-if="!playVideo"
|
||||||
|
class="article_video_thumb h-full"
|
||||||
|
:style="{ backgroundImage: `url('${parseData.thumbnail ? parseData.thumbnail : '/images/default-thumbnail.jpg'}')` }"
|
||||||
|
>
|
||||||
|
<div></div>
|
||||||
|
<div class="article_video_content">
|
||||||
|
<span><Icon name="ri:play-circle-line" class="text-white" /></span>
|
||||||
|
<h3 class="line-clamp text-white" :class="LAYOUT_PARSE['title_Class']" :style="LAYOUT_PARSE['h3.title']">
|
||||||
|
{{ parseData.title?.replace(/<[^>]+>/g, "") }}
|
||||||
|
</h3>
|
||||||
|
<p class="mb-0 line-clamp text-white" :class="LAYOUT_PARSE['paragraph_Class']" :style="LAYOUT_PARSE['p.paragraph']">
|
||||||
|
{{ parseData.intro?.replace(/<[^>]+>/g, "") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<video src=""></video>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="LAYOUT_PARSE.styleClasses" v-html="LAYOUT_PARSE.styleClasses"></div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.article_video {
|
||||||
|
@apply min-h-465px;
|
||||||
|
.article_video_thumb {
|
||||||
|
@apply flex flex-col justify-end min-h-465px;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
padding: 120px 85px 60px 85px;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 1;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
.article_video_content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 44px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 57.2px;
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-size: 80px;
|
||||||
|
color: #ed1c24;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export { default as Article_Card_Default } from './Card.vue'
|
||||||
|
export { default as Article_Card_Audio } from './Audio.vue'
|
||||||
|
export { default as Article_Card_Video } from './Video.vue'
|
||||||
|
export { default as Article_Card_Video_Hightlight } from './VideoBackground.vue'
|
||||||
|
export { default as Article_Card_Miss_Hightlight } from './MissBackground.vue'
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { Article_Card_Default, Article_Card_Audio, Article_Card_Video, Article_Card_Video_Hightlight,Article_Card_Miss_Hightlight } from "./index";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
content?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentLayouts[enumPageComponentTemplate[enumPageComponentKey.ARTICLE]["ARTICLE_CARD"]]["CARD_DEFAULT"]]: Article_Card_Default,
|
||||||
|
[enumPageComponentLayouts[enumPageComponentTemplate[enumPageComponentKey.ARTICLE]["ARTICLE_CARD"]]["CARD_AUDIO"]]: Article_Card_Audio,
|
||||||
|
[enumPageComponentLayouts[enumPageComponentTemplate[enumPageComponentKey.ARTICLE]["ARTICLE_CARD"]]["CARD_VIDEO"]]: Article_Card_Video,
|
||||||
|
[enumPageComponentLayouts[enumPageComponentTemplate[enumPageComponentKey.ARTICLE]["ARTICLE_CARD"]]["CARD_VIDEO_HIGHLIGHT"]]: Article_Card_Video_Hightlight,
|
||||||
|
[enumPageComponentLayouts[enumPageComponentTemplate[enumPageComponentKey.ARTICLE]["ARTICLE_CARD"]]["CARD_MISS_HIGHLIGHT"]]: Article_Card_Miss_Hightlight,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = computed(() => {
|
||||||
|
return _props.settings.layout;
|
||||||
|
});
|
||||||
|
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import DynamicComponent from "~/components/dynamic-page/page-component/templates/index.vue";
|
|
||||||
import { COLLECTION_QUERY_DROP, getValueStringWithKeyAndColon, getInputValue } from '@/utils/parseSQL';
|
|
||||||
import { isEmpty } from "lodash";
|
|
||||||
|
|
||||||
const _props = defineProps<{
|
|
||||||
dataResult?: any[];
|
|
||||||
dataQuery?: string;
|
|
||||||
layout?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const SETTING_OPTIONS = {
|
|
||||||
MAX_ELEMENT: 5,
|
|
||||||
TEMPLATE: "Article",
|
|
||||||
LAYOUT: "LAYOUT:vertical"
|
|
||||||
};
|
|
||||||
|
|
||||||
const LAYOUT_PARSE = computed(() => {
|
|
||||||
const parseLayout = _props.layout?.split("-")?.map((_layout: any) => {
|
|
||||||
const parseItem = _layout.split(":");
|
|
||||||
return {
|
|
||||||
[parseItem[0]]: parseItem[1],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return Object.assign({}, ...parseLayout);
|
|
||||||
});
|
|
||||||
|
|
||||||
const _dataResult = computed(() => {
|
|
||||||
let _components = Array(Number(LAYOUT_PARSE.value.MAX) || SETTING_OPTIONS.MAX_ELEMENT).fill(null);
|
|
||||||
const result = getInputValue(_props.dataResult, 'ARRAY');
|
|
||||||
result && result.length > 0 && _components.map((_ : any, index : any) => {
|
|
||||||
_components[index] = result[index] || null;
|
|
||||||
})
|
|
||||||
return _components;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="collection-container grid gap-5" :class="LAYOUT_PARSE['LAYOUT'] || 'horizontal'">
|
|
||||||
<div v-for="(component, index) in _dataResult" :key="index">
|
|
||||||
<template v-if="!isEmpty(component)">
|
|
||||||
<DynamicComponent
|
|
||||||
:settings="{
|
|
||||||
template: LAYOUT_PARSE.TYPE || SETTING_OPTIONS.TEMPLATE,
|
|
||||||
layout: `LAYOUT:${LAYOUT_PARSE.DATA.toLowerCase()}` || SETTING_OPTIONS.LAYOUT,
|
|
||||||
dataResult: { ...component },
|
|
||||||
}"
|
|
||||||
@drop-data="dropData"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<DynamicComponent
|
|
||||||
:settings="{
|
|
||||||
template: LAYOUT_PARSE.TYPE || SETTING_OPTIONS.TEMPLATE,
|
|
||||||
layout: `LAYOUT:${LAYOUT_PARSE.DATA.toLowerCase()}` || SETTING_OPTIONS.LAYOUT,
|
|
||||||
}"
|
|
||||||
@drop-data="dropData"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.collection-container {
|
|
||||||
&.vertical {
|
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
&.horizontal {
|
|
||||||
grid-template-rows: auto;
|
|
||||||
grid-auto-flow: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||||
|
import { DEFAULT_QUERY_DROP } from "@/utils/parseSQL";
|
||||||
|
import { isEmpty } from "@/utils/lodash";
|
||||||
|
import { getInputValue } from "@/utils/parseSQL";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
dataResult?: any;
|
||||||
|
dataType?: any;
|
||||||
|
dataQuery?: any;
|
||||||
|
layout?: string;
|
||||||
|
label?: any;
|
||||||
|
}>();
|
||||||
|
const SETTING_OPTIONS = {
|
||||||
|
BREADCRUMB_MAX_ELEMENT: 3,
|
||||||
|
};
|
||||||
|
const LAYOUT_PARSE = computed(() => {
|
||||||
|
const designObject = _props.label ? getInputValue(_props.label, "OBJECT") : {};
|
||||||
|
return Object.assign({}, designObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseData = computed(() => {
|
||||||
|
if (!_props.dataResult) return;
|
||||||
|
const result = getInputValue(_props.dataResult, "OBJECT");
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const articleStore = useArticleStore();
|
||||||
|
const currentArticle = computed(() => articleStore.currentArticleGeneral);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<div class="breadcrumb" v-if="!LAYOUT_PARSE['HideBreadcrumb']">
|
||||||
|
<ul class="breadcrumb__list">
|
||||||
|
<li
|
||||||
|
class="breadcrumb__list__item"
|
||||||
|
v-for="(item, index) in _props.dataResult && _props.dataResult?.length > 0 ? _props.dataResult : Array(SETTING_OPTIONS.BREADCRUMB_MAX_ELEMENT).fill(null)"
|
||||||
|
:key="index"
|
||||||
|
:class="isEmpty(item) && 'empty'"
|
||||||
|
>
|
||||||
|
<p v-if="!isEmpty(item)" class="breadcrumb__list__item__title">
|
||||||
|
{{ item?.title }}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<nuxt-link class="article-card-default__topic" :to="`#`">
|
||||||
|
<h5>Topic</h5>
|
||||||
|
</nuxt-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">Nội dung bài viết sẽ ở đây</div>
|
||||||
|
<!-- <div class="btn-wrap" v-if="!LAYOUT_PARSE['HideCopylink']">
|
||||||
|
<div class="center-y">
|
||||||
|
<p title="Quay trở lại" class="button--back">
|
||||||
|
<Icon name="fa6-solid:arrow-left" />
|
||||||
|
</p>
|
||||||
|
<button class="button--bookmark">
|
||||||
|
<Icon name="fa6-regular:bookmark" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="center-y">
|
||||||
|
<button title="Copy link" class="button--back">
|
||||||
|
<Icon name="mdi:link-variant" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0px;
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #000;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 180%;
|
||||||
|
}
|
||||||
|
// &:first-child {
|
||||||
|
// color: blue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
&:not(:first-child):before {
|
||||||
|
content: "\\";
|
||||||
|
position: absolute;
|
||||||
|
left: -18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-card-default__topic {
|
||||||
|
position: relative;
|
||||||
|
// background-color: #151411;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
// color: #fff;
|
||||||
|
padding: 0 12px;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid #000;
|
||||||
|
line-height: 180%;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 12px;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #ed1c24;
|
||||||
|
left: -12px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #409eff;
|
||||||
|
width: 40px;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
font-size: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 300px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #eeeeee;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
white-space: normal;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// .btn-wrap {
|
||||||
|
// display: flex;
|
||||||
|
// flex-wrap: wrap;
|
||||||
|
// justify-content: space-between;
|
||||||
|
// align-items: center;
|
||||||
|
|
||||||
|
// @media (min-width: 768px) {
|
||||||
|
// flex-direction: row;
|
||||||
|
// }
|
||||||
|
// .class-default,
|
||||||
|
// .button--back {
|
||||||
|
// border-radius: 9999px;
|
||||||
|
// border-width: 1px;
|
||||||
|
// width: 3rem;
|
||||||
|
// height: 3rem;
|
||||||
|
// margin: 0;
|
||||||
|
// font-size: 17px;
|
||||||
|
// display: flex;
|
||||||
|
// align-items: center;
|
||||||
|
// justify-content: center;
|
||||||
|
// background-color: #ffffff;
|
||||||
|
// border: 1px solid rgb(229, 231, 235);
|
||||||
|
// box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
// &:hover {
|
||||||
|
// background-color: #e6f4ff;
|
||||||
|
// color: #3c7abc;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .button--bookmark {
|
||||||
|
// width: 32px;
|
||||||
|
// height: 32px;
|
||||||
|
// border-radius: 200px;
|
||||||
|
// background-color: white;
|
||||||
|
// display: flex;
|
||||||
|
// align-items: center;
|
||||||
|
// justify-content: center;
|
||||||
|
// border: none;
|
||||||
|
// &:hover {
|
||||||
|
// background-color: #e6f4ff;
|
||||||
|
// color: #3c7abc;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
.center-y {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { isEmpty } from "@/utils/lodash";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
dataResult?: any;
|
||||||
|
}>();
|
||||||
|
const SETTING_OPTIONS = {
|
||||||
|
BREADCRUMB_MAX_ELEMENT: 3,
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<div class="breadcrumb">
|
||||||
|
<ul class="breadcrumb__list">
|
||||||
|
<li
|
||||||
|
class="breadcrumb__list__item"
|
||||||
|
v-for="(item, index) in _props.dataResult && _props.dataResult?.length > 0 ? _props.dataResult : Array(SETTING_OPTIONS.BREADCRUMB_MAX_ELEMENT).fill(null)"
|
||||||
|
:key="index"
|
||||||
|
:class="isEmpty(item) && 'empty'"
|
||||||
|
>
|
||||||
|
<p v-if="!isEmpty(item)" class="breadcrumb__list__item__title">
|
||||||
|
{{ item?.title }}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="breakcrumb__time">Ngày tạo image</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">Nội dung bài viết sẽ ở đây</div>
|
||||||
|
|
||||||
|
<div class="btn-wrap w-100 max-w">
|
||||||
|
<div class="center-y">
|
||||||
|
<p title="Quay trở lại" class="button--back">
|
||||||
|
<Icon name="fa6-solid:arrow-left" />
|
||||||
|
</p>
|
||||||
|
<button class="button--bookmark">
|
||||||
|
<Icon name="fa6-regular:bookmark" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="center-y">
|
||||||
|
<button title="Copy link" class="button--back copy-link">
|
||||||
|
<Icon name="mdi:link-variant" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
$max-width: 680px;
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
padding: 0px;
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
&:first-child {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:first-child):before {
|
||||||
|
content: "";
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-top: 1px solid #bdbdbd;
|
||||||
|
border-right: 1px solid #bdbdbd;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
position: absolute;
|
||||||
|
left: -18px;
|
||||||
|
top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time {
|
||||||
|
color: #9f9f9f;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #409eff;
|
||||||
|
width: 40px;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
font-size: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 300px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #eeeeee;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
white-space: normal;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.class-default,
|
||||||
|
.button--back,
|
||||||
|
.button--bookmark {
|
||||||
|
border-radius: 8px;
|
||||||
|
border-width: 1px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 17px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid rgb(229, 231, 235);
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
&:hover {
|
||||||
|
background-color: #e6f4ff;
|
||||||
|
color: #3c7abc;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.copy-link {
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb,
|
||||||
|
.btn-wrap {
|
||||||
|
max-width: $max-width;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-y {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="podcast__wrapper overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="podcast"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="podcast__content__time"
|
||||||
|
>
|
||||||
|
Ngày tạo podcast
|
||||||
|
</p>
|
||||||
|
<figure><img src="http://picsum.photos/1024/600?random=1'" alt="Ảnh podcast" title="Ảnh podcast" /></figure>
|
||||||
|
<div
|
||||||
|
class="podcast__content"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="podcast__content__time"
|
||||||
|
>
|
||||||
|
Ngày tạo podcast
|
||||||
|
</p>
|
||||||
|
<h1 class="podcast__content__title">Tiêu đề podcast</h1>
|
||||||
|
<p
|
||||||
|
class="podcast__content__text"
|
||||||
|
>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||||
|
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
class="buttons"
|
||||||
|
>
|
||||||
|
<li><Icon name="mdi:bookmark-outline" /></li>
|
||||||
|
<li><Icon name="material-symbols:mode-comment-outline" /></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="playlist">
|
||||||
|
<div class="playlist__time">
|
||||||
|
<span>5:00</span>
|
||||||
|
<span>10:00</span>
|
||||||
|
</div>
|
||||||
|
<div class="playlist__buttons">
|
||||||
|
<div class="playlist__buttons__left">
|
||||||
|
<div
|
||||||
|
class="button__prev"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:skip-previous" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="sound"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:volume-mute"></Icon>
|
||||||
|
<div></div>
|
||||||
|
<Icon name="material-symbols:volume-up"></Icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="play">
|
||||||
|
<Icon name="fluent:skip-back-10-48-filled" />
|
||||||
|
<Icon name="material-symbols:play-arrow" class="button" />
|
||||||
|
<Icon name="fluent:skip-forward-10-48-filled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="playlist__buttons__right">
|
||||||
|
<div
|
||||||
|
class="button__next"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:skip-next" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="speed"
|
||||||
|
>
|
||||||
|
<span>Tốc độ phát: </span>
|
||||||
|
<strong>1x</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="podcast__content__text"
|
||||||
|
>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||||
|
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss">
|
||||||
|
:root {
|
||||||
|
--podcast-wrapper-padding: 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podcast-padding-tablet {
|
||||||
|
--podcast-wrapper-padding: 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podcast-padding-smartphone {
|
||||||
|
--podcast-wrapper-padding: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podcast__wrapper {
|
||||||
|
padding: calc(var(--podcast-wrapper-padding) * 1px);
|
||||||
|
border: 1px solid #eeeeee;
|
||||||
|
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.podcast {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
& > figure > img {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content__text {
|
||||||
|
font-size: 18px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
flex: 1;
|
||||||
|
color: #222222;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&__time {
|
||||||
|
color: #9f9f9f;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
text-align: center;
|
||||||
|
&__time {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .buttons {
|
||||||
|
display: flex;
|
||||||
|
align-self: start;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
& li {
|
||||||
|
list-style: none;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid rgb(229, 231, 235);
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||||
|
border-radius: 50px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e6f4ff;
|
||||||
|
color: #3c7abc;
|
||||||
|
}
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
font-size: 18px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateY(-50%) translateX(-55%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
& .buttons {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
&__time {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-top: 12px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after,
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
height: 4px;
|
||||||
|
top: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #e6f4ff;
|
||||||
|
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: 50%;
|
||||||
|
background-color: #3c7abc;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
& span {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #3c7abc;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
& .button__prev,
|
||||||
|
& .button__next {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #3c7abc;
|
||||||
|
}
|
||||||
|
& .sound {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: -10px;
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 36px;
|
||||||
|
color: #3c7abc;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: #e6f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
width: 50px;
|
||||||
|
height: 2px;
|
||||||
|
position: relative;
|
||||||
|
background-color: #dcf0ff;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
top: 0;
|
||||||
|
height: 2px;
|
||||||
|
width: 50%;
|
||||||
|
background-color: #3c7abc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .play {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #3c7abc;
|
||||||
|
|
||||||
|
&.button {
|
||||||
|
font-size: 64px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .speed {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #3c7abc;
|
||||||
|
|
||||||
|
& span {
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
& strong {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 20px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Comment from "@/components/dynamic-page/page-component/templates/others/comments/Default.vue";
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="container overflow-hidden">
|
||||||
|
<div class="video row">
|
||||||
|
<div
|
||||||
|
class="video__left"
|
||||||
|
>
|
||||||
|
<video controls="controls" width="100%" height="100%" data-file-id="149" data-resource="https://acp-api.vpress.vn/Resources/Video/983d2f57-7743-472f-b22d-fc73085af6d5.mp4" data-title="Download.mp4">
|
||||||
|
<source src="" type="video/mp4" />
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="video__right bg-body-tertiary"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class=""
|
||||||
|
>
|
||||||
|
Tiêu đề video
|
||||||
|
</h1>
|
||||||
|
<p class="line-clamp-3 fs-5 fw-light">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||||
|
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||||
|
</p>
|
||||||
|
<h5 class="text-end fs-4 opacity-75">Tác giả</h5>
|
||||||
|
|
||||||
|
<p><b class="text-primary fw-semibold">Danh mục</b> <span class="ms-2 opacity-25 fw-semibold">Ngày đăng video</span></p>
|
||||||
|
<Comment />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.line-clamp-3 {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// export { default as Article_Card } from './cards/Card.vue'
|
||||||
|
|
||||||
|
export { default as Article_Detail_General } from './General.vue'
|
||||||
|
export { default as Article_Detail_Podcast } from './Podcast.vue'
|
||||||
|
export { default as Article_Detail_Video } from './Video.vue'
|
||||||
|
export { default as Article_Detail_Image } from './Image.vue'
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
|
||||||
|
// import { Article_Card, Article_Detail_Video, Article_Detail_Podcast, Article_Detail_General, Article_Detail_Image } from "./index";
|
||||||
|
import { Article_Detail_General, Article_Detail_Podcast, Article_Detail_Video, Article_Detail_Image } from "./index";
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
content?: any;
|
||||||
|
}>();
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentLayouts[enumPageComponentTemplate[enumPageComponentKey.ARTICLE]["ARTICLE_DETAIL"]]["DETAIL_GENERAL"]]: Article_Detail_General,
|
||||||
|
[enumPageComponentLayouts[enumPageComponentTemplate[enumPageComponentKey.ARTICLE]["ARTICLE_DETAIL"]]["DETAIL_PODCAST"]]: Article_Detail_Podcast,
|
||||||
|
[enumPageComponentLayouts[enumPageComponentTemplate[enumPageComponentKey.ARTICLE]["ARTICLE_DETAIL"]]["DETAIL_VIDEO"]]: Article_Detail_Video,
|
||||||
|
[enumPageComponentLayouts[enumPageComponentTemplate[enumPageComponentKey.ARTICLE]["ARTICLE_DETAIL"]]["DETAIL_IMAGE"]]: Article_Detail_Image,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = computed(() => `${_props.settings.layout}`);
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Article_Card } from './cards/index.vue'
|
||||||
|
export { default as Article_Detail } from './details/index.vue'
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { Article_Card, Article_Detail } from "./index";
|
||||||
|
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
content?: any;
|
||||||
|
}>();
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentTemplate[enumPageComponentKey.ARTICLE]["ARTICLE_CARD"]]: Article_Card,
|
||||||
|
[enumPageComponentTemplate[enumPageComponentKey.ARTICLE]["ARTICLE_DETAIL"]]: Article_Detail,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = computed(() => _props.settings.template);
|
||||||
|
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { enumPageComponentTemplates } from "@/definitions/enum";
|
|
||||||
import { DEFAULT_QUERY_DROP, getInputValue } from '@/utils/parseSQL';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
dataResult?: any
|
|
||||||
dataType?: any
|
|
||||||
dataQuery?: any
|
|
||||||
layout?: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const LAYOUT_PARSE = computed(() => {
|
|
||||||
const parseLayout = props.layout?.split('-')?.map((_layout : any) => {
|
|
||||||
const parseItem = _layout.split(':')
|
|
||||||
return {
|
|
||||||
[parseItem[0]]: parseItem[0] === 'HIDE' ? parseItem[1].split(',') : parseItem[1],
|
|
||||||
};
|
|
||||||
}) || [];
|
|
||||||
return Object.assign({}, ...parseLayout);
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['selectComponent', 'dropData']);
|
|
||||||
|
|
||||||
const selectComponent = () => {
|
|
||||||
emit('selectComponent');
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseData = computed(() => {
|
|
||||||
if(!props.dataResult) return
|
|
||||||
const result = getInputValue(props.dataResult, 'OBJECT');
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
|
|
||||||
const drop = (e: any) => {
|
|
||||||
if (e.dataTransfer.getData(`${enumPageComponentTemplates.ARTICLE}`)) {
|
|
||||||
const data = e.dataTransfer.getData(`${enumPageComponentTemplates.ARTICLE}`);
|
|
||||||
const { dataType, dataResult } = JSON.parse(data);
|
|
||||||
const dataQuery = DEFAULT_QUERY_DROP(dataType, dataResult.id);
|
|
||||||
emit('dropData', {
|
|
||||||
dataType,
|
|
||||||
dataResult,
|
|
||||||
dataQuery: dataQuery,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<article class="basic-article gap-x-4" :class="[LAYOUT_PARSE['LAYOUT'] || 'horizontal', !parseData && 'no-data', LAYOUT_PARSE['REVERSE'] ? 'reverse' : '']">
|
|
||||||
<div v-if="!LAYOUT_PARSE['HIDE'] || !LAYOUT_PARSE['HIDE'].includes('thumbnail')" class="basic-article_thumbnail">
|
|
||||||
<template v-if="parseData">
|
|
||||||
<img class="object-fit-cover" :src="parseData.thumbnail ? parseData.thumbnail : '/images/default-thumbnail.jpg'" :alt="parseData.title?.replace(/<[^>]+>/g, '')" />
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="basic-article_content" :class="[!parseData && 'no-data']">
|
|
||||||
<div>
|
|
||||||
<template v-if="parseData">
|
|
||||||
<nuxt-link :to="`/bai-viet/${parseData.id}`">
|
|
||||||
<h3 v-if="!LAYOUT_PARSE['HIDE'] || !LAYOUT_PARSE['HIDE'].includes('title')" class="mb-1 line-clamp-2 text-base font-700 hover:text-primary-100 transition-all duration-300">
|
|
||||||
{{ parseData.title?.replace(/<[^>]+>/g, '') }}
|
|
||||||
</h3>
|
|
||||||
</nuxt-link>
|
|
||||||
</template>
|
|
||||||
<p v-if="!LAYOUT_PARSE['HIDE'] || !LAYOUT_PARSE['HIDE'].includes('paragraph')" class="mb-0 line-clamp-3 sm:line-clamp-3 text-[14px]">
|
|
||||||
<template v-if="parseData">
|
|
||||||
<template v-if="parseData.intro">
|
|
||||||
{{ parseData.intro?.replace(/<[^>]+>/g, '') }}
|
|
||||||
</template>
|
|
||||||
<template v-if="parseData.sub">
|
|
||||||
{{ parseData.sub?.replace(/<[^>]+>/g, '') }}
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.basic-article {
|
|
||||||
display: grid;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
&.vertical {
|
|
||||||
@apply lg:grid-cols-1 sm:grid-cols-2;
|
|
||||||
.basic-article_content {
|
|
||||||
padding: 10px 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.horizontal {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
.basic-article_content {
|
|
||||||
padding: 0px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.reverse {
|
|
||||||
.basic-article_thumbnail {
|
|
||||||
grid-column: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.basic-article_content {
|
|
||||||
grid-row: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&_thumbnail {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 6px;
|
|
||||||
aspect-ratio: 16/10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-block {
|
|
||||||
background-color: #409eff;
|
|
||||||
height: 100px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import { COLLECTION_QUERY_DROP, getValueStringWithKeyAndColon, getInputValue } from '@/utils/parseSQL';
|
|
||||||
|
|
||||||
const _props = defineProps<{
|
|
||||||
dataResult?: any[];
|
|
||||||
dataQuery?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const SETTING_OPTIONS = {
|
|
||||||
MAX_ELEMENT: 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
const _dataResult = computed(() => {
|
|
||||||
let _components = Array(SETTING_OPTIONS.MAX_ELEMENT).fill(null);
|
|
||||||
const result = getInputValue(_props.dataResult, 'ARRAY');
|
|
||||||
result && result.length > 0 && _components.map((_ : any, index : any) => {
|
|
||||||
_components[index] = result[index] || null;
|
|
||||||
})
|
|
||||||
return _components;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="flex gap-4 items-end py-2">
|
|
||||||
<template v-for="(component, index) in _dataResult">
|
|
||||||
<nuxt-link v-if="component" :key="index" :to="`/${component.code}`" class=" py-1 font-400 text-[16px] first:font-600 first:text-[20px] sm:block hidden first:block">
|
|
||||||
<h3 class="m-0 leading-none hover:text-primary-100 transition-all duration-300">{{ component.title }}</h3>
|
|
||||||
</nuxt-link>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { isEmpty } from "@/utils/lodash";
|
||||||
|
import { COLLECTION_QUERY_DROP, getValueStringWithKeyAndColon, getInputValue } from "@/utils/parseSQL";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
dataResult?: any;
|
||||||
|
dataQuery?: string;
|
||||||
|
label?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const designObject = computed(() => {
|
||||||
|
return _props.label ? getInputValue(_props.label, "OBJECT") : {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapActivesToItems = (index: number) => {
|
||||||
|
if (designObject.value && designObject.value.listCss) {
|
||||||
|
return designObject.value.listCss[index] || {};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="categories-container border-custom" :class="designObject['categories_Class']" :style="designObject['div.categories-container']">
|
||||||
|
<div v-for="(component, index) in _dataResult" :key="index" :class="['border-custom', isEmpty(component) ? 'empty' : 'category', designObject['category_Class']]" :style="mapActivesToItems(index)['category']">
|
||||||
|
<template v-if="!isEmpty(component)">
|
||||||
|
<div>
|
||||||
|
<h3 :style="mapActivesToItems(index)['h3.categories']">
|
||||||
|
<nuxt-link to="#">{{ component.title }}</nuxt-link>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div v-html="designObject.styleClasses"></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.border-pri {
|
||||||
|
.categories-container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.categories-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
width: fit-content;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.category {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
h3 {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #409eff;
|
||||||
|
width: 50px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.border-custom {
|
||||||
|
border-color: #e5e5e5 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { isEmpty } from "@/utils/lodash";
|
||||||
|
import { COLLECTION_QUERY_DROP, getValueStringWithKeyAndColon, getInputValue } from "@/utils/parseSQL";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
dataResult?: any;
|
||||||
|
dataQuery?: string;
|
||||||
|
label?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const SETTING_OPTIONS = {
|
||||||
|
MAX_ELEMENT: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const _dataResult = computed(() => {
|
||||||
|
const designObject = _props.label ? getInputValue(_props.label, "OBJECT") : {};
|
||||||
|
let _components = Array(Number(designObject.MAX) || SETTING_OPTIONS.MAX_ELEMENT).fill(null);
|
||||||
|
const result = getInputValue(_props.dataResult, "ARRAY");
|
||||||
|
result &&
|
||||||
|
result.length > 0 &&
|
||||||
|
_components.map((_: any, index: any) => {
|
||||||
|
_components[index] = result[index] || null;
|
||||||
|
});
|
||||||
|
return Object.assign({}, _components);
|
||||||
|
});
|
||||||
|
|
||||||
|
const designObject = computed(() => {
|
||||||
|
return _props.label ? getInputValue(_props.label, "OBJECT") : {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapActivesToItems = (index: number) => {
|
||||||
|
if (designObject.value && designObject.value.listCss) {
|
||||||
|
return designObject.value.listCss[index] || {};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="categories-container border-custom" :class="designObject['categories_Class']" :style="designObject['div.categories-container']">
|
||||||
|
<div v-for="(component, index) in _dataResult" :key="index" :class="['border-custom', isEmpty(component) ? 'empty' : 'category', designObject['category_Class']]" :style="mapActivesToItems(index)['category']">
|
||||||
|
<template v-if="!isEmpty(component)">
|
||||||
|
<div class="category-content">
|
||||||
|
<svg width="6" height="6" viewBox="0 0 6 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5.984 2.456V4.184H4.336V5.992H2.4V4.184H0.752V2.456H2.4V0.648H4.336V2.456H5.984Z" fill="black" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<h3 :style="mapActivesToItems(index)['h3.categories']">
|
||||||
|
{{ component.title }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div v-if="designObject.styleClasses" v-html="designObject.styleClasses"></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.border-pri {
|
||||||
|
.categories-container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.categories-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: fit-content;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.category {
|
||||||
|
&-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: #000;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 180%;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #409eff;
|
||||||
|
width: 100px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.border-custom {
|
||||||
|
border-color: #e5e5e5 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Default_Collection } from './Default.vue'
|
||||||
|
export { default as Vertical_Collection } from './Vertical.vue'
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { Default_Collection, Vertical_Collection } from "./index";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
content?: any;
|
||||||
|
}>();
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentLayouts[`${enumPageComponentTemplate[enumPageComponentKey.CATEGORY]["CATEGORY"]}`]["DEFAULT"]]: Default_Collection,
|
||||||
|
[enumPageComponentLayouts[`${enumPageComponentTemplate[enumPageComponentKey.CATEGORY]["CATEGORY"]}`]["CATEGORY_VERTICAL"]]: Vertical_Collection,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = computed(() => _props.settings.layout);
|
||||||
|
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings, content: _props.content }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as Categories } from './categories/index.vue'
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { Categories } from "./index";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentTemplate[enumPageComponentKey.CATEGORY]['CATEGORY']]: Categories,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = computed(() => _props.settings.template);
|
||||||
|
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import DynamicComponent from "~/components/dynamic-page/page-component/templates/index.vue";
|
||||||
|
import { COLLECTION_QUERY_DROP, getValueStringWithKeyAndColon, getInputValue } from "@/utils/parseSQL";
|
||||||
|
import { isEmpty } from "@/utils/lodash";
|
||||||
|
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
dataResult?: any;
|
||||||
|
dataQuery?: string;
|
||||||
|
layout?: string;
|
||||||
|
label?: any;
|
||||||
|
content?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const SETTING_OPTIONS = {
|
||||||
|
MAX_ELEMENT: 5,
|
||||||
|
TEMPLATE: "TYPE:Card",
|
||||||
|
LAYOUT: "TYPE:Card_Audio",
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMPONENT = {
|
||||||
|
taxonomy: enumPageComponentTemplates.ARTICLE,
|
||||||
|
};
|
||||||
|
|
||||||
|
const LAYOUT_PARSE = computed(() => {
|
||||||
|
return _props.label ? getInputValue(_props.label, "OBJECT") : {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const _dataResult = computed(() => {
|
||||||
|
let _components = Array(Number(LAYOUT_PARSE.value.MAX) || SETTING_OPTIONS.MAX_ELEMENT).fill(null);
|
||||||
|
const result = getInputValue(_props.dataResult, "ARRAY");
|
||||||
|
result &&
|
||||||
|
result.length > 0 &&
|
||||||
|
_components.map((_: any, index: any) => {
|
||||||
|
_components[index] = result[index] || null;
|
||||||
|
});
|
||||||
|
return _components;
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapActivesToItems = (index: number) => {
|
||||||
|
if (LAYOUT_PARSE.value && LAYOUT_PARSE.value.listCss) {
|
||||||
|
return LAYOUT_PARSE.value.listCss[index] || {};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="collection-container border-custom" :class="[LAYOUT_PARSE['div.collection-container_Class'], LAYOUT_PARSE['collection_Class']]" :style="LAYOUT_PARSE['div.collection-container']">
|
||||||
|
<DynamicComponent
|
||||||
|
v-for="(component, index) in _dataResult"
|
||||||
|
:key="index"
|
||||||
|
:settings="{
|
||||||
|
template: SETTING_OPTIONS.TEMPLATE,
|
||||||
|
layout: SETTING_OPTIONS.LAYOUT,
|
||||||
|
label: mapActivesToItems(Number(index)),
|
||||||
|
dataResult: !isEmpty(component) ? { ...component } : null,
|
||||||
|
}"
|
||||||
|
:component="COMPONENT"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-html="LAYOUT_PARSE.styleClasses"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.collection-container {
|
||||||
|
display: grid;
|
||||||
|
&.column {
|
||||||
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
&.row {
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
}
|
||||||
|
&.border-pri {
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
&.border-custom {
|
||||||
|
border-color: #e5e5e5 !important;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
min-height: 100px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #409eff;
|
||||||
|
}
|
||||||
|
&.noData {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import DynamicComponent from "~/components/dynamic-page/page-component/templates/index.vue";
|
||||||
|
import { COLLECTION_QUERY_DROP, getValueStringWithKeyAndColon, getInputValue } from "@/utils/parseSQL";
|
||||||
|
import { isEmpty } from "@/utils/lodash";
|
||||||
|
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
dataResult?: any;
|
||||||
|
dataQuery?: string;
|
||||||
|
layout?: string;
|
||||||
|
label?: any;
|
||||||
|
content?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const SETTING_OPTIONS = {
|
||||||
|
MAX_ELEMENT: 5,
|
||||||
|
TEMPLATE: "TYPE:Card",
|
||||||
|
LAYOUT: "TYPE:Card_Default",
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMPONENT = {
|
||||||
|
taxonomy: enumPageComponentTemplates.ARTICLE,
|
||||||
|
};
|
||||||
|
|
||||||
|
const LAYOUT_PARSE = computed(() => {
|
||||||
|
return _props.label ? getInputValue(_props.label, "OBJECT") : {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const _dataResult = computed(() => {
|
||||||
|
let _components = Array(Number(LAYOUT_PARSE.value.MAX) || SETTING_OPTIONS.MAX_ELEMENT).fill(null);
|
||||||
|
const result = getInputValue(_props.dataResult, "ARRAY");
|
||||||
|
result &&
|
||||||
|
result.length > 0 &&
|
||||||
|
_components.map((_: any, index: any) => {
|
||||||
|
_components[index] = result[index] || null;
|
||||||
|
});
|
||||||
|
return _components;
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapActivesToItems = (index: number) => {
|
||||||
|
if (LAYOUT_PARSE.value && LAYOUT_PARSE.value.listCss) {
|
||||||
|
return LAYOUT_PARSE.value.listCss[index] || {};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="collection-container border-custom overflow-hidden" :class="[LAYOUT_PARSE['div.collection-container_Class'], LAYOUT_PARSE['collection_Class']]" :style="LAYOUT_PARSE['div.collection-container']">
|
||||||
|
<DynamicComponent
|
||||||
|
v-for="(component, index) in _dataResult"
|
||||||
|
:key="index"
|
||||||
|
:settings="{
|
||||||
|
template: SETTING_OPTIONS.TEMPLATE,
|
||||||
|
layout: SETTING_OPTIONS.LAYOUT,
|
||||||
|
label: mapActivesToItems(Number(index)),
|
||||||
|
dataResult: !isEmpty(component) ? { ...component } : null,
|
||||||
|
}"
|
||||||
|
:component="COMPONENT"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-html="LAYOUT_PARSE.styleClasses"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.collection-container {
|
||||||
|
display: grid;
|
||||||
|
&.column {
|
||||||
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
&.row {
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
}
|
||||||
|
&.border-pri {
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
&.border-custom {
|
||||||
|
border-color: #e5e5e5 !important;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
min-height: 100px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #409eff;
|
||||||
|
}
|
||||||
|
&.noData {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Default_Collection } from './Default.vue'
|
||||||
|
export { default as Audio_Collection } from './Audio.vue'
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { Default_Collection, Audio_Collection } from "./index";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
content?: any;
|
||||||
|
}>();
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentLayouts[`${enumPageComponentTemplate[enumPageComponentKey.COLLECTION]["ARTICLE"]}`]["ARTICLE_COLLECTION_DEFAULT"]]: Default_Collection,
|
||||||
|
[enumPageComponentLayouts[`${enumPageComponentTemplate[enumPageComponentKey.COLLECTION]["ARTICLE"]}`]["ARTICLE_COLLECTION_AUDIO"]]: Audio_Collection,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = computed(() => _props.settings.layout);
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings, content: _props.content }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as Misses_Default } from './misses/Default.vue'
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { Misses_Default } from "./index";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
content?: any;
|
||||||
|
}>();
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentLayouts[`${enumPageComponentTemplate[enumPageComponentKey.COLLECTION]["CATEGORY"]}`]["MISSES_COLLECTION_DEFAULT"]]: Misses_Default,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = computed(() => _props.settings.layout);
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings, content: _props.content }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import DynamicComponent from "~/components/dynamic-page/page-component/templates/index.vue";
|
||||||
|
import { COLLECTION_QUERY_DROP, getValueStringWithKeyAndColon, getInputValue } from "@/utils/parseSQL";
|
||||||
|
import { isEmpty, groupBy } from "lodash";
|
||||||
|
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||||
|
const emit = defineEmits(["dropComponent", "dropData", "selectComponent"]);
|
||||||
|
const _props = defineProps<{
|
||||||
|
dataResult?: any;
|
||||||
|
dataQuery?: string;
|
||||||
|
layout?: string;
|
||||||
|
label?: string;
|
||||||
|
content?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const SETTING_OPTIONS = {
|
||||||
|
MAX_ELEMENT: 6,
|
||||||
|
TEMPLATE: "TYPE:Card",
|
||||||
|
LAYOUT: "TYPE:Card_MissHightLight",
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMPONENT = {
|
||||||
|
taxonomy: enumPageComponentTemplates.ARTICLE,
|
||||||
|
};
|
||||||
|
|
||||||
|
const LAYOUT_PARSE = computed(() => {
|
||||||
|
// console.log(_props.label);
|
||||||
|
return _props.label ? getInputValue(_props.label, "OBJECT") : {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const _dataResult = computed(() => {
|
||||||
|
let _components = Array(Number(LAYOUT_PARSE.value.MAX) || SETTING_OPTIONS.MAX_ELEMENT).fill(null);
|
||||||
|
const result = getInputValue(_props.dataResult, "ARRAY");
|
||||||
|
result &&
|
||||||
|
result.length > 0 &&
|
||||||
|
_components.map((_: any, index: any) => {
|
||||||
|
_components[index] = result[index] || null;
|
||||||
|
});
|
||||||
|
return _components;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function dropData(data: any) {
|
||||||
|
if (data) {
|
||||||
|
const { dataResult, dataType } = data;
|
||||||
|
const checkDataResult = getInputValue(_props.dataResult, "ARRAY");
|
||||||
|
const result = _props.dataResult ? [...checkDataResult, { ...dataResult }] : [{ ...dataResult }];
|
||||||
|
const getDataQuery = _props.dataQuery ? COLLECTION_QUERY_DROP(dataType, getValueStringWithKeyAndColon(_props.dataQuery) + "," + dataResult.id) : COLLECTION_QUERY_DROP(dataType, dataResult.id);
|
||||||
|
|
||||||
|
emit("dropData", {
|
||||||
|
dataResult: result,
|
||||||
|
dataType,
|
||||||
|
dataQuery: getDataQuery,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectComponent = () => {
|
||||||
|
emit("selectComponent");
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapActivesToItems = (index: number) => {
|
||||||
|
if (LAYOUT_PARSE.value && LAYOUT_PARSE.value.listCss) {
|
||||||
|
return LAYOUT_PARSE.value.listCss[index] || {};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="gallery" :class="[LAYOUT_PARSE['div.collection-container_Class'], LAYOUT_PARSE['collection_Class']]" @click="selectComponent" :style="LAYOUT_PARSE['div.collection-container']">
|
||||||
|
<div class="wrap" v-for="(component, index) in _dataResult" :key="index">
|
||||||
|
<DynamicComponent
|
||||||
|
class="abc"
|
||||||
|
:settings="{
|
||||||
|
template: SETTING_OPTIONS.TEMPLATE,
|
||||||
|
layout: SETTING_OPTIONS.LAYOUT,
|
||||||
|
label: { ...mapActivesToItems(Number(index)) },
|
||||||
|
dataResult: !isEmpty(component) ? { ...component } : null,
|
||||||
|
}"
|
||||||
|
:component="COMPONENT"
|
||||||
|
@drop-data="dropData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- <conllection
|
||||||
|
class="collection-container border-custom overflow-hidden"
|
||||||
|
:class="[LAYOUT_PARSE['div.collection-container_Class'], LAYOUT_PARSE['collection_Class']]"
|
||||||
|
@click="selectComponent"
|
||||||
|
:style="LAYOUT_PARSE['div.collection-container']"
|
||||||
|
>
|
||||||
|
<DynamicComponent
|
||||||
|
v-for="(component, index) in _dataResult"
|
||||||
|
:key="index"
|
||||||
|
:class="[index === 0 || index === 1 ? 'row-span-3' : index === 2 || index === 3 ? 'row-span-2' : 'row-span-1']"
|
||||||
|
:settings="{
|
||||||
|
template: SETTING_OPTIONS.TEMPLATE,
|
||||||
|
layout: SETTING_OPTIONS.LAYOUT,
|
||||||
|
label: { ...mapActivesToItems(Number(index)) },
|
||||||
|
dataResult: !isEmpty(component) ? { ...component } : null,
|
||||||
|
}"
|
||||||
|
:component="COMPONENT"
|
||||||
|
@drop-data="dropData"
|
||||||
|
/>
|
||||||
|
</conllection> -->
|
||||||
|
<div v-if="LAYOUT_PARSE.styleClasses" v-html="LAYOUT_PARSE.styleClasses" style="display: none"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.gallery {
|
||||||
|
column-count: 4;
|
||||||
|
-webkit-column-count: 4;
|
||||||
|
-moz-column-count: 4;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
&:nth-child(1),
|
||||||
|
&:nth-child(2) {
|
||||||
|
padding-top: 615px;
|
||||||
|
}
|
||||||
|
&:nth-child(3),
|
||||||
|
&:nth-child(5) {
|
||||||
|
padding-top: 358px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(4),
|
||||||
|
&:nth-child(6) {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 241px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .abc {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
// margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.row-span-3 {
|
||||||
|
// grid-row: span 3 / span 3;
|
||||||
|
// height: 585px;
|
||||||
|
|
||||||
|
// margin: 10px 0;
|
||||||
|
// &:nth-child(1) {
|
||||||
|
// background-color: red;
|
||||||
|
// }
|
||||||
|
// &:nth-child(2) {
|
||||||
|
// background-color: yellow;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
.row-span-2 {
|
||||||
|
// margin: 10px 0;
|
||||||
|
// grid-row: span 2 / span 2;
|
||||||
|
// height: 328px;
|
||||||
|
// background-color: aqua;
|
||||||
|
// .basic-article {
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
.row-span-1 {
|
||||||
|
// grid-row: span 1 / span 1;
|
||||||
|
// height: 211px;
|
||||||
|
// background-color: green;
|
||||||
|
|
||||||
|
// .basic-article {
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image img {
|
||||||
|
height: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.collection-container {
|
||||||
|
// display: grid;
|
||||||
|
// grid-template-columns: repeat(4, 1fr);
|
||||||
|
// grid-template-rows: repeat(3, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
column-count: 4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Article_Collection } from './articles/index.vue'
|
||||||
|
export { default as Category_Collection } from './categories/index.vue'
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { Article_Collection, Category_Collection } from "./index";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
content?: any;
|
||||||
|
}>();
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentTemplate[enumPageComponentKey.COLLECTION]["ARTICLE"]]: Article_Collection,
|
||||||
|
[enumPageComponentTemplate[enumPageComponentKey.COLLECTION]["CATEGORY"]]: Category_Collection,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = computed(() => _props.settings.template);
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings, content: _props.content }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{}>();
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="player">
|
|
||||||
<div class="player__track">
|
|
||||||
<input class="player__track-range" type="range" disabled />
|
|
||||||
<div class="player__time">
|
|
||||||
<span class="player__time-current">00:00</span>
|
|
||||||
<span class="player__time-duration">00:00</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="player__controls">
|
|
||||||
<div class="player__speed">
|
|
||||||
<button class="player__speed-button">
|
|
||||||
<span class="player__speed-label">Tốc độ phát</span>
|
|
||||||
<span class="player__speed-value">1.0x</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="player__actions">
|
|
||||||
<button class="player__actions-button player__actions-button--replay">
|
|
||||||
<Icon name="ri:replay-5-fill" class="player__icon player__icon--replay" />
|
|
||||||
</button>
|
|
||||||
<button class="player__actions-button player__actions-button--pause">
|
|
||||||
<Icon name="ri:play-circle-fill" class="player__icon player__icon--pause" />
|
|
||||||
</button>
|
|
||||||
<button class="player__actions-button player__actions-button--forward">
|
|
||||||
<Icon name="ri:forward-5-line" class="player__icon player__icon--forward" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="player__volume">
|
|
||||||
<button class="player__volume-button">
|
|
||||||
<div class="player__volume-control">
|
|
||||||
<Icon name="ri:volume-up-fill" class="player__icon player__icon--volume" />
|
|
||||||
<input class="player__volume-range" type="range" disabled />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.player {
|
|
||||||
&__track {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__track-range {
|
|
||||||
width: 100%;
|
|
||||||
height: 5px;
|
|
||||||
accent-color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__time {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 1rem;
|
|
||||||
|
|
||||||
&-current,
|
|
||||||
&-duration {
|
|
||||||
font-size: 10px;
|
|
||||||
font-family: 'Raleway', sans-serif;
|
|
||||||
font-weight: normal;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__controls {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0 1rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
& > div {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__speed {
|
|
||||||
&-button {
|
|
||||||
color: #fff;
|
|
||||||
background-color: transparent;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
display: flex;
|
|
||||||
gap: 0.25rem;
|
|
||||||
|
|
||||||
&-value {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__actions {
|
|
||||||
&-button {
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 100%;
|
|
||||||
color: white;
|
|
||||||
width: fit-content;
|
|
||||||
height: fit-content;
|
|
||||||
&--replay:hover,
|
|
||||||
&--forward:hover {
|
|
||||||
background-color: #d6d3d1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&__icon {
|
|
||||||
&--replay,
|
|
||||||
&--forward,
|
|
||||||
&--pause {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--pause {
|
|
||||||
font-size: 44px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&__volume {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&-button {
|
|
||||||
background-color: transparent;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-control {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
|
|
||||||
& .player__icon--volume {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .player__volume-range {
|
|
||||||
accent-color: #fff;
|
|
||||||
width: 3rem;
|
|
||||||
height: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
button{
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import AudioPlayer from './AudioPlayer.vue'
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div class="banner">
|
|
||||||
<div class="banner__background" style="background-image: url('https://acp-api.vpress.vn/Resources/%E1%BA%A2nh/0bf02739-de1e-4899-9a2e-287c5d949250.jpg')">
|
|
||||||
<div class="banner__overlay"></div>
|
|
||||||
<Wrap class="banner__content">
|
|
||||||
<div class="banner__inner">
|
|
||||||
<div class="article">
|
|
||||||
<div class="article__image-container">
|
|
||||||
<div class="article__image-wrapper" style="background-image: url('https://acp-api.vpress.vn/Resources/%E1%BA%A2nh/0bf02739-de1e-4899-9a2e-287c5d949250.jpg')">
|
|
||||||
<img src="https://acp-api.vpress.vn/Resources/%E1%BA%A2nh/0bf02739-de1e-4899-9a2e-287c5d949250.jpg" alt="" class="article__image" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="article__content">
|
|
||||||
<div class="article__header">
|
|
||||||
<div class="article__header-text">
|
|
||||||
<h1 class="article__title">Podcast Truyện ngắn: Như cơi đựng trầu</h1>
|
|
||||||
<time class="article__date">T2, 29 Th01 2024 16:57</time>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="article__intro">
|
|
||||||
<div class="article__intro-text">Tình cảm vợ chồng êm ấm 12 năm, tối nay được định đoạt bằng tờ giấy vô hồn, vốn là người dễ xúc động nên trong lúc viết, Ngân Thương để mấy giọt nước mắt rơi xuống làm đôi chỗ bị nhòe đi.</div>
|
|
||||||
</div>
|
|
||||||
<div class="article__audio">
|
|
||||||
<AudioPlayer />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Wrap>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.banner {
|
|
||||||
&__background {
|
|
||||||
width: 100%;
|
|
||||||
height: 60px;
|
|
||||||
background-size: cover;
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
height: 25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
background-position: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__overlay {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background-color: black;
|
|
||||||
opacity: 0.8;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__inner {
|
|
||||||
width: 100%;
|
|
||||||
height: 40px;
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
height: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 2;
|
|
||||||
.article {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(10, 1fr);
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&__image-container {
|
|
||||||
grid-column: span 3;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 15rem;
|
|
||||||
min-width: 100px;
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
height: 20rem;
|
|
||||||
margin: 0 2rem;
|
|
||||||
}
|
|
||||||
margin: 0 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__image-wrapper {
|
|
||||||
height: 10rem;
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
height: 15rem;
|
|
||||||
}
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 1.5rem 0 0 1.5rem;
|
|
||||||
padding: 0.5rem;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
background-size: cover;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
background-color: #000;
|
|
||||||
opacity: 0.3;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__image {
|
|
||||||
position: relative;
|
|
||||||
z-index: 3;
|
|
||||||
height: 10rem;
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
height: 15rem;
|
|
||||||
}
|
|
||||||
width: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
grid-column: span 7;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(12, 1fr);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__header {
|
|
||||||
grid-column: span 12;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(12, 1fr);
|
|
||||||
margin-top: 2rem;
|
|
||||||
|
|
||||||
&-text {
|
|
||||||
grid-column: span 11;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__subtitle {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
font-size: 19px;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: bold;
|
|
||||||
font-family: "SFD";
|
|
||||||
}
|
|
||||||
|
|
||||||
&__date {
|
|
||||||
margin-top: 0.125rem;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__intro {
|
|
||||||
grid-column: span 12;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
display: none;
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__intro-text {
|
|
||||||
text-align: left;
|
|
||||||
font-size: 16px;
|
|
||||||
color: #fff;
|
|
||||||
font-family: "SFD";
|
|
||||||
}
|
|
||||||
|
|
||||||
&__audio {
|
|
||||||
grid-column: span 10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
<template>
|
|
||||||
<article class="article">
|
|
||||||
<div id="article-detail" class="article__detail">
|
|
||||||
<div>
|
|
||||||
<video controls="controls" width="100%" height="auto" data-file-id="149" data-resource="https://acp-api.vpress.vn/Resources/Video/983d2f57-7743-472f-b22d-fc73085af6d5.mp4" data-title="Download.mp4">
|
|
||||||
<source src="https://acp-api.vpress.vn/Resources/Video/983d2f57-7743-472f-b22d-fc73085af6d5.mp4" type="video/mp4" />
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="article__sidebar">
|
|
||||||
<div class="article__sidebar-content">
|
|
||||||
<h1 class="article__title">Tranh cãi chuyện 'quán không nhận chuyển khoản'</h1>
|
|
||||||
<div class="article__author-info">
|
|
||||||
<div class="article__author">
|
|
||||||
<p class="article__author-name">Thanh Huệ</p>
|
|
||||||
</div>
|
|
||||||
<span class="article__separator">-</span>
|
|
||||||
<p class="article__date">T4, 15 Th05 2024 10:55</p>
|
|
||||||
</div>
|
|
||||||
<div id="article-brief" class="article__brief">
|
|
||||||
<div class="article__intro-text">Những ngày cận Tết tại Hà Nội, các hội thi hoa đào, quất cảnh với đa dạng các sản phẩm độc đáo, bắt mắt đ đuợc các nghệ nhân đem đến cho khách tham quan chiêm ngưỡng.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</template>
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.article {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
/* flex-direction: column; */
|
|
||||||
gap: 1rem; // Equivalent to gap-4
|
|
||||||
margin-top: 1rem; // Equivalent to mt-4
|
|
||||||
background-color: #f7f7f7;
|
|
||||||
|
|
||||||
&__detail {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
iframe,
|
|
||||||
video {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
&.iframe {
|
|
||||||
max-height: 13rem; // Equivalent to max-h-52
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__sidebar {
|
|
||||||
width: 50%;
|
|
||||||
&-content {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
font-size: 17px; // Equivalent to text-2xl
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__author-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
video {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Article
|
export { default as Articles } from './articles/index.vue'
|
||||||
export { default as Article_BasicCard } from './articles/individuals/Card.vue'
|
export { default as Navigations } from './navigations/index.vue'
|
||||||
export { default as Article_BasicCollection } from './articles/collections/BasicCollection.vue'
|
export { default as Collections } from './collections/index.vue'
|
||||||
|
export { default as Sections } from './sections/index.vue'
|
||||||
// Category
|
export { default as Categories } from './categories/index.vue'
|
||||||
export { default as BasicCategories } from './categories/BasicCategories.vue'
|
export { default as Advertisings } from './advertisings/index.vue'
|
||||||
export { default as CollectionPaging } from './pageCategories/collection_page.vue'
|
export { default as Others } from './others/index.vue'
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { enumPageComponentTemplates } from "@/definitions/enum";
|
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||||
import { Article_BasicCard, BasicCategories, Article_BasicCollection } from "./index";
|
import { Articles, Navigations, Collections, Sections, Categories, Advertisings, Others } from "./index";
|
||||||
|
|
||||||
const _props = defineProps<{
|
const _props = defineProps<{
|
||||||
settings: any;
|
settings: any;
|
||||||
component?: any;
|
component?: any;
|
||||||
|
content?: any;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const definedDynamicComponent: Record<string, any> = {
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
[enumPageComponentTemplates.ARTICLE]: Article_BasicCard,
|
[enumPageComponentTemplates.ARTICLE]: Articles,
|
||||||
[enumPageComponentTemplates.CATEGORY]: BasicCategories,
|
[enumPageComponentTemplates.NAVIGATION]: Navigations,
|
||||||
[enumPageComponentTemplates.COLLECTION]: Article_BasicCollection,
|
[enumPageComponentTemplates.COLLECTION]: Collections,
|
||||||
|
[enumPageComponentTemplates.SECTION]: Sections,
|
||||||
|
[enumPageComponentTemplates.CATEGORY]: Categories,
|
||||||
|
[enumPageComponentTemplates.ADVERTISING]: Advertisings,
|
||||||
|
[enumPageComponentTemplates.OTHER]: Others,
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentComponent = computed(() => `${_props.settings.template}`);
|
const getCurrentComponent = computed(() => _props.component?.taxonomy);
|
||||||
|
|
||||||
const GET_PROPS = computed(() => {
|
const GET_PROPS = computed(() => {
|
||||||
return () => {
|
return () => {
|
||||||
let props: any = {};
|
let props: any = {};
|
||||||
if (_props.settings) {
|
if (_props.settings) {
|
||||||
for (const [key, value] of _props.settings ? Object.entries(_props.settings) : []) {
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
props = {
|
props = {
|
||||||
...props,
|
...props,
|
||||||
[key]: value,
|
[key]: value,
|
||||||
@@ -32,6 +36,9 @@ const GET_PROPS = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- <component :is="definedDynamicComponent[getCurrentComponent]" v-bind="{ ...(GET_PROPS()), component: _props.component, settings: _props.settings }" /> -->
|
<component
|
||||||
<component :is="definedDynamicComponent[getCurrentComponent]" v-bind="{ ...(GET_PROPS()), component: _props.component }" />
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings, content: _props.content }"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { isEmpty } from "@/utils/lodash";
|
||||||
|
import { nanoid } from "nanoid"
|
||||||
|
import DynamicComponent from "~/components/dynamic-page/page-component/templates/index.vue";
|
||||||
|
import RecusiveNavItem from "@/components/dynamic-page/page-component/templates/navigations/components/RecusiveNavItem.vue";
|
||||||
|
import { buildTree } from "@/utils/recusive";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
content?: any;
|
||||||
|
component?: any;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="px-4 mt-4">
|
||||||
|
<div class="nav-container">
|
||||||
|
<template v-if="_props.content">
|
||||||
|
<div v-for="item, index in buildTree(_props.content)" :key="index" class="nav-items-box">
|
||||||
|
<div class="submenu-container">
|
||||||
|
<h4 class="" >{{ item.title }}</h4>
|
||||||
|
<div class="ml-2">
|
||||||
|
<h5
|
||||||
|
v-for="_item, _index in item.childs ? item.childs : []"
|
||||||
|
:key="_index"
|
||||||
|
>
|
||||||
|
{{ _item.title }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.empty {
|
||||||
|
width: 100px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #409eff;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-container {
|
||||||
|
display: flex;
|
||||||
|
.nav-items-box {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-container {
|
||||||
|
> div {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
h5 {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Navigation
|
||||||
|
export { default as Navigation_Default } from './Default.vue'
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { Navigation_Default } from "./index";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
content?: any
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentLayouts[enumPageComponentTemplate[enumPageComponentKey.NAVIGATION]['BOTTOM']]['NAVIGATION_BOTTOM_DEFAULT']]: Navigation_Default,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = computed(() => _props.settings.layout);
|
||||||
|
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings, content: _props.content }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import RecusiveNavItem from "@/components/dynamic-page/page-component/templates/navigations/components/RecusiveNavItem.vue";
|
||||||
|
import RecusiveSection from "@/components/dynamic-page/page-section/RecusiveSection.vue";
|
||||||
|
import { enumPageComponentStaticChild } from "@/definitions/enum";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
records?: any[]
|
||||||
|
component?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const globalState = ref<any>({})
|
||||||
|
const setGlobalState = (id: any) => {
|
||||||
|
globalState.value[id] = !globalState.value[id];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="navigation-container flex gap-4 justify-center items-center">
|
||||||
|
<div v-for="(record) in props.records" :key="record.id" class="navigation-branch cursor-pointer">
|
||||||
|
<template v-if="record && record.childs && record.childs.length > 0 && record.typeChild === enumPageComponentStaticChild.DEFAULT">
|
||||||
|
<div class="navigation-submenu">
|
||||||
|
<div class="navigation_title">
|
||||||
|
<nuxt-link :to="record?.slug" class="!font-arial !font-400">{{ record?.title }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<div class="navigation-item submenu-container dropdown-container">
|
||||||
|
<RecusiveNavItem :records="record.childs" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="record.typeChild === enumPageComponentStaticChild.LAYOUT">
|
||||||
|
<div class="navigation-submenu">
|
||||||
|
<div class="position-relative ps-3">
|
||||||
|
<div class="navigation_title ">
|
||||||
|
<nuxt-link :to="record?.slug" class="!font-arial !font-400">{{ record?.title }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="full-layout dropdown-container">
|
||||||
|
<template v-if="record.data">
|
||||||
|
<div class="p-1">
|
||||||
|
<RecusiveSection type="section" :id="record.data" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="navigation_title navigation-item" >
|
||||||
|
<nuxt-link :to="record?.slug" class="!font-arial !font-400">{{ record?.title }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.navigation-branch {
|
||||||
|
.navigation_title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.navigation-submenu {
|
||||||
|
position: relative;
|
||||||
|
padding: 15px 5px;
|
||||||
|
&:hover {
|
||||||
|
> .dropdown-container {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, 0%);
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.submenu-container {
|
||||||
|
width: 200px;
|
||||||
|
display: flex;
|
||||||
|
> div {
|
||||||
|
justify-content: start !important;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
gap: 0px !important;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.navigation-item {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 10px 20px;
|
||||||
|
&:hover {
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigation-branch {
|
||||||
|
padding: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dropdown-container {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, 10%);
|
||||||
|
left: 50%;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all .3s;
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
top: 100%;
|
||||||
|
}
|
||||||
|
.full-layout {
|
||||||
|
width: 1200px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-menu {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, 0%);
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { isEmpty } from "@/utils/lodash";
|
||||||
|
import DynamicComponent from "~/components/dynamic-page/page-component/templates/index.vue";
|
||||||
|
import { getInputValue } from "@/utils/parseSQL";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
dataResult?: any;
|
||||||
|
dataQuery?: string;
|
||||||
|
component?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const SETTING_OPTIONS = {
|
||||||
|
MAX_ELEMENT: 10,
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<div v-for="navItem, index in Array(SETTING_OPTIONS.MAX_ELEMENT).fill({})" :key="index">
|
||||||
|
<div class="empty"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.empty {
|
||||||
|
width: 120px;
|
||||||
|
min-height: 100px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #409eff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as Navigation_Default } from './Default.vue'
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { Navigation_Default } from "./index";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
content?: any
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentLayouts[enumPageComponentTemplate[enumPageComponentKey.NAVIGATION]['DIRECTION']]['NAVIGATION_BOTTOM_DEFAULT']]: Navigation_Default,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = computed(() => _props.settings.layout);
|
||||||
|
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings, content: _props.content }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// Navigation
|
||||||
|
export { default as Top_Navigation } from './tops/index.vue'
|
||||||
|
export { default as Bottom_Navigation } from './bottoms/index.vue'
|
||||||
|
export { default as Direction_Navigation } from './directions/index.vue'
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { Top_Navigation, Bottom_Navigation, Direction_Navigation } from "./index";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
content?: any
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentTemplate[enumPageComponentKey.NAVIGATION]['DIRECTION']]: Direction_Navigation,
|
||||||
|
[enumPageComponentTemplate[enumPageComponentKey.NAVIGATION]['BOTTOM']]: Bottom_Navigation,
|
||||||
|
[enumPageComponentTemplate[enumPageComponentKey.NAVIGATION]['TOP']]: Top_Navigation,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = computed(() => _props.settings.template);
|
||||||
|
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings, content: _props.content }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { buildTree } from "@/utils/recusive";
|
||||||
|
import RecusiveNavItem from "@/components/dynamic-page/page-component/templates/navigations/components/RecusiveNavItem.vue";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
content?: any;
|
||||||
|
component?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const SETTING_OPTIONS = {
|
||||||
|
MAX_ELEMENT: 10,
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav>
|
||||||
|
<div class="flex gap-3 justify-end items-center">
|
||||||
|
<RecusiveNavItem :records="content && buildTree(content)" :component="_props.component" />
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.empty {
|
||||||
|
width: 100px;
|
||||||
|
min-height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #409eff;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
margin: 5px 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Navigation
|
||||||
|
export { default as Navigation_Default } from './Default.vue'
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { Navigation_Default } from "./index";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
content?: any
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentLayouts[enumPageComponentTemplate[enumPageComponentKey.NAVIGATION]['TOP']]['NAVIGATION_TOP_DEFAULT']]: Navigation_Default,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = computed(() => _props.settings.layout);
|
||||||
|
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings, content: _props.content }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div class="comment">
|
||||||
|
<div class="input_comment width_common mb-2">
|
||||||
|
<div class="box-area-input width_common">
|
||||||
|
<textarea id="txtComment" class="block_input" placeholder="* Bình luận của bạn sẽ được biên tập trước khi đăng. Xin vui lòng gõ tiếng Việt có dấu"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<button type="button" class="send-comment">
|
||||||
|
Gửi bình luận
|
||||||
|
<Icon name="ri:send-plane-2-fill"></Icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.comment {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.mb-2 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.send-comment {
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
background-color: #409eff;
|
||||||
|
border: 1px solid;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 80rem;
|
||||||
|
&.h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.input_comment {
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 10px;
|
||||||
|
background: #fff;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 5px;
|
||||||
|
border-top: 1px solid #dedede;
|
||||||
|
border-right: 1px solid #dedede;
|
||||||
|
border-bottom: 1px solid #dedede;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-area-input {
|
||||||
|
/* background: #f3f6f9; */
|
||||||
|
border-radius: 4px;
|
||||||
|
position: relative;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-left: 2px solid rgba(59, 130, 246, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input_comment textarea.block_input {
|
||||||
|
height: 30px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
textarea::placeholder {
|
||||||
|
color: #878a99;
|
||||||
|
}
|
||||||
|
.input_comment textarea.block_input {
|
||||||
|
height: 58px;
|
||||||
|
overflow: hidden;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .box-area-input .block_input {
|
||||||
|
background: #f7f7f7;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.input_comment textarea {
|
||||||
|
background: #fff;
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 58px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// export { default as Weather_Day } from './weathers/WeatherDay.vue'
|
||||||
|
// export { default as Comment_Default } from './comments/Default.vue'
|
||||||
|
export { default as Other_Weather } from './weathers/index.vue'
|
||||||
|
export { default as Other_Stock } from './stocks/index.vue'
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { Other_Weather, Other_Stock } from "./index";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
content?: any;
|
||||||
|
}>();
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentTemplate[enumPageComponentKey.OTHER]["WEATHER"]]: Other_Weather,
|
||||||
|
[enumPageComponentTemplate[enumPageComponentKey.OTHER]['STOCK']]: Other_Stock,
|
||||||
|
// [enumPageComponentTemplate[enumPageComponentKey.ARTICLE]["ARTICLE_DETAIL"]]: Article_Detail,
|
||||||
|
};
|
||||||
|
const getCurrentComponent = computed(() => _props.settings.template);
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import JSWidget from '@/components/widget/JSwidget.vue';
|
||||||
|
|
||||||
|
const widgetOptions = {
|
||||||
|
"locale": "vi",
|
||||||
|
"width": "334px",
|
||||||
|
"height": "250px",
|
||||||
|
"price_line_color": "#71BDDF",
|
||||||
|
"grid_color": "#999999",
|
||||||
|
"label_color": "#999999",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<JSWidget
|
||||||
|
:CONTAINER_ID="nanoid(10)"
|
||||||
|
:SCRIPT_ID="nanoid(10)"
|
||||||
|
SCRIPT_SRC="https://www.fireant.vn/Scripts/web/widgets.js"
|
||||||
|
:options="widgetOptions"
|
||||||
|
:inside="false"
|
||||||
|
widgetKey="FireAnt"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
div {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import JSWidget from '@/components/widget/JSwidget.vue';
|
||||||
|
|
||||||
|
const widgetOptions = {
|
||||||
|
"symbols": [
|
||||||
|
{
|
||||||
|
"proName": "FOREXCOM:SPXUSD",
|
||||||
|
"title": "S&P 500 Index"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"proName": "FOREXCOM:NSXUSD",
|
||||||
|
"title": "US 100 Cash CFD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"proName": "FX_IDC:EURUSD",
|
||||||
|
"title": "EUR to USD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"proName": "BITSTAMP:BTCUSD",
|
||||||
|
"title": "Bitcoin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"proName": "BITSTAMP:ETHUSD",
|
||||||
|
"title": "Ethereum"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isTransparent": false,
|
||||||
|
"showSymbolLogo": true,
|
||||||
|
"colorTheme": "dark",
|
||||||
|
"locale": "en"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<JSWidget
|
||||||
|
:CONTAINER_ID="nanoid(10)"
|
||||||
|
:SCRIPT_ID="nanoid(10)"
|
||||||
|
SCRIPT_SRC="https://s3.tradingview.com/external-embedding/embed-widget-tickers.js"
|
||||||
|
:options="widgetOptions"
|
||||||
|
:inside="true"
|
||||||
|
widgetKey="TradingView"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
div {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Stock_Default } from './334x641.vue'
|
||||||
|
export { default as Stock_FullSize } from './Full.vue'
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { Stock_Default, Stock_FullSize } from "./index";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
content?: any;
|
||||||
|
}>();
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentLayouts[enumPageComponentTemplate[enumPageComponentKey.OTHER]['STOCK']]['STOCK_DEFAULT']]: Stock_Default,
|
||||||
|
[enumPageComponentLayouts[enumPageComponentTemplate[enumPageComponentKey.OTHER]["STOCK"]]["STOCK_FULLSIZE"]]: Stock_FullSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = computed(() => `${_props.settings.layout}`);
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// import axios from "axios";
|
||||||
|
// async function getAllWeatherCity() {
|
||||||
|
// try {
|
||||||
|
// const res = await axios.get();
|
||||||
|
// console.log('res',res)
|
||||||
|
// } catch (err) {
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// onBeforeMount(async()=>{
|
||||||
|
// await getAllWeatherCity()
|
||||||
|
// })
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="box-info-weather flexbox" id="overview">
|
||||||
|
<div class="box-info-weather__left">
|
||||||
|
<span class="weather-day-current">Hiện tại</span>
|
||||||
|
|
||||||
|
<div class="weather-day">
|
||||||
|
<img data-v-b1c9218b="" src="https://cdn.weatherapi.com/weather/64x64/day/116.png" alt="Weather Icon">
|
||||||
|
<div class="big-temp">28°</div>
|
||||||
|
<div class="name">Mưa nhẹ</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="color-gray-2">
|
||||||
|
<p>Cao: 28° Thấp: 24°</p>
|
||||||
|
<div>
|
||||||
|
<span>Không khí:</span>
|
||||||
|
<div class="weather-tooltip">
|
||||||
|
<span class="weather-tooltip-group">
|
||||||
|
<span class="quality-2">Trung bình</span>
|
||||||
|
<Icon class="ic ic-help" name="material-symbols:help-outline"></Icon>
|
||||||
|
</span>
|
||||||
|
<div class="box-info-hover" data-left="-71px">
|
||||||
|
<div class="title">
|
||||||
|
<span class="header_tooltip">
|
||||||
|
<Icon class="ic ic-help" name="material-symbols-light:airware"></Icon>
|
||||||
|
<span>Chất lượng không khí</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid__2 chat-luong mb15">
|
||||||
|
<div class="quality-2">
|
||||||
|
<div class="lbl">Trung bình</div>
|
||||||
|
</div>
|
||||||
|
<div class="quality-2">
|
||||||
|
<p>
|
||||||
|
Nồng độ bụi mịn<br />
|
||||||
|
<span class="lbl">PM2.5: 30.42</span> <br />(μg/m3)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>Chất lượng không khí có thể chấp nhận được, tuy nhiên một số người nhạy cảm vẫn nên lưu ý.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
<!-- <div class="box-info-weather__right text-right color-gray-2">
|
||||||
|
<div class="weather-tooltip mb40">
|
||||||
|
<span class="weather-tooltip-group">
|
||||||
|
<span>Cảm giác như 33° </span>
|
||||||
|
<Icon class="ic ic-help" name="material-symbols:help-outline"></Icon>
|
||||||
|
</span>
|
||||||
|
<div class="box-info-hover">
|
||||||
|
<div class="title">
|
||||||
|
<span class="header_tooltip">
|
||||||
|
<Icon class="ic ic-help" name="fxemoji:thermometer"></Icon>
|
||||||
|
<span>Nhiệt độ cảm nhận</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="scroll-height">
|
||||||
|
<p>Nhiệt độ cảm nhận (heat index) là nhiệt độ cơ thể con người cảm thấy trong thực tế, được tính dựa trên dữ liệu nhiệt độ kết hợp với độ ẩm.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Độ ẩm: 86%</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span>UV: </span>
|
||||||
|
<div class="weather-tooltip">
|
||||||
|
<span class="weather-tooltip-group">
|
||||||
|
<span> 5 / 11 </span>
|
||||||
|
<Icon class="ic ic-help" name="material-symbols:help-outline"></Icon>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="box-info-hover">
|
||||||
|
<div class="title">
|
||||||
|
<span class="header_tooltip">
|
||||||
|
<Icon name="material-symbols:sunny-outline"></Icon>
|
||||||
|
<span>Chỉ số UV</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="scroll-height">
|
||||||
|
<ul class="chi-so">
|
||||||
|
<li class="item quality-1">
|
||||||
|
<div class="lbl d-flex justify-content-between">
|
||||||
|
<div>1 → 2</div>
|
||||||
|
<div>Thấp</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="item quality-2">
|
||||||
|
<div class="lbl d-flex justify-content-between">
|
||||||
|
<div>3 → 5</div>
|
||||||
|
<div>Trung bình</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="item quality-3">
|
||||||
|
<div class="lbl d-flex justify-content-between">
|
||||||
|
<div>6 → 7</div>
|
||||||
|
<div>Cao</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="item quality-4">
|
||||||
|
<div class="lbl d-flex justify-content-between">
|
||||||
|
<div>8 → 10</div>
|
||||||
|
<div>Rất cao</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="item quality-5">
|
||||||
|
<div class="lbl d-flex justify-content-between">
|
||||||
|
<div>11+</div>
|
||||||
|
<div>Cực điểm</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Theo Cơ quan Bảo vệ Môi trường Mỹ (EPA), chỉ số UV dao động 0-2 được xem là thấp, chỉ số 8-10 có thời gian tiếp xúc gây bỏng là 25 phút. Chỉ số UV từ 11 trở lên được xem là cực kỳ cao, rất nguy hiểm, nguy cơ làm tổn thương
|
||||||
|
da, mắt bị bỏng nếu tiếp xúc ánh nắng mặt trời trong khoảng 15 phút mà không được bảo vệ.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Tiếp xúc quá mức với ánh sáng mặt trời trong thời gian ngắn sẽ gây bỏng nắng, tổn thương mắt như đục thủy tinh thể, da bị bỏng, khô, sạm, tạo nếp nhăn, lão hóa nhanh. Tiếp xúc tia UV lâu dài, tích lũy có thể gây ung thư da.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.box-info-weather {
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
// min-height: 240px;
|
||||||
|
&.flexbox {
|
||||||
|
align-items: flex-end;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
&__left {
|
||||||
|
width: 50%;
|
||||||
|
.weather-day {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 19px;
|
||||||
|
.ic-weather {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
.big-temp {
|
||||||
|
font: 400 48px "Merriweather", serif;
|
||||||
|
font-feature-settings: "pnum" on, "lnum" on;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
width: 100%;
|
||||||
|
font: 400 20px "Merriweather", serif;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color-gray-2 {
|
||||||
|
color: #757575;
|
||||||
|
}
|
||||||
|
span.weather-day-current {
|
||||||
|
color: #9f9f9f;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__right {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ic {
|
||||||
|
fill: #757575;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.ic-help {
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
.mb40 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.weather-tooltip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
margin-bottom: -15px;
|
||||||
|
display: inline-block;
|
||||||
|
.quality-2 {
|
||||||
|
color: #c6990c;
|
||||||
|
}
|
||||||
|
&:hover .box-info-hover {
|
||||||
|
top: 30px;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.box-info-hover {
|
||||||
|
font-family: Arial;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: normal;
|
||||||
|
position: absolute;
|
||||||
|
top: 50px;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 16px;
|
||||||
|
background: #ffffff;
|
||||||
|
width: 300px;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition-duration: 300ms;
|
||||||
|
transition-property: all;
|
||||||
|
transition-timing-function: cubic-bezier(0.7, 1, 0.7, 1);
|
||||||
|
filter: drop-shadow(0px 2px 12px rgba(0, 0, 0, 0.2));
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
color: #222;
|
||||||
|
.header_tooltip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.lbl {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.scroll-height {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-color: #e5e5e5 #fff;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
.item {
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.quality-1 {
|
||||||
|
background: rgba(36, 161, 72, 0.1);
|
||||||
|
color: #24a148;
|
||||||
|
}
|
||||||
|
.quality-2 {
|
||||||
|
background: rgba(198, 153, 12, 0.1);
|
||||||
|
color: #c6990c;
|
||||||
|
}
|
||||||
|
.quality-3 {
|
||||||
|
background: rgba(224, 120, 47, 0.1);
|
||||||
|
color: #e0782f;
|
||||||
|
}
|
||||||
|
.quality-4 {
|
||||||
|
background: rgba(220, 78, 85, 0.1);
|
||||||
|
color: #dc4e55;
|
||||||
|
}
|
||||||
|
.quality-5 {
|
||||||
|
background: rgba(164, 78, 201, 0.1);
|
||||||
|
color: #a44ec9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.mb15 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.grid__2 {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
column-gap: 8px;
|
||||||
|
row-gap: 8px;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.chat-luong [class*='quality-'] {
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
font-size: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 80px;
|
||||||
|
background: rgba(198, 153, 12, 0.1);
|
||||||
|
color: #C6990C;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Navigation
|
||||||
|
export { default as WeatherDay } from './WeatherDay.vue'
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { WeatherDay } from "./index";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
content?: any
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentLayouts[enumPageComponentTemplate[enumPageComponentKey.OTHER]["WEATHER"]]["WEATHER_DEFAULT"]]: WeatherDay,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = computed(() => _props.settings.layout);
|
||||||
|
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings, content: _props.content }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { isEmpty } from "lodash";
|
|
||||||
import DynamicComponent from "~/components/dynamic-page/page-component/templates/index.vue";
|
|
||||||
import { COLLECTION_PAGING_QUERY_DROP } from '@/utils/parseSQL';
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const emit = defineEmits(["dropData", "selectComponent"]);
|
|
||||||
|
|
||||||
const _props = defineProps<{
|
|
||||||
dataResult?: any[];
|
|
||||||
dataQuery?: string;
|
|
||||||
component?: any;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const SETTING_OPTIONS = {
|
|
||||||
MAX_ELEMENT: 5,
|
|
||||||
TEMPLATE: "Article",
|
|
||||||
LAYOUT: "LAYOUT:horizontal",
|
|
||||||
};
|
|
||||||
|
|
||||||
// const page = ref(1);
|
|
||||||
const limit = ref(1);
|
|
||||||
const totals = ref(2);
|
|
||||||
const category = ref(0);
|
|
||||||
const listArticleByCategory = ref([]);
|
|
||||||
const type = "Article";
|
|
||||||
|
|
||||||
// watch(
|
|
||||||
// () => _props.dataResult,
|
|
||||||
// (newValue) => {
|
|
||||||
// const result = getInputValue(newValue, "ARRAY");
|
|
||||||
// listArticleByCategory.value = result;
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
|
|
||||||
const dropData = (event: any) => {
|
|
||||||
const queryBy = {
|
|
||||||
Category: "Categories",
|
|
||||||
};
|
|
||||||
const { dataResult, dataType } = JSON.parse(event.dataTransfer.getData("category"));
|
|
||||||
// const getDataQuery = `Get[${type}] Top[20] With[${queryBy[dataType]}:${dataResult.id}]`;
|
|
||||||
const getDataQuery = COLLECTION_PAGING_QUERY_DROP(type, { key: queryBy[dataType], value: dataResult.id })
|
|
||||||
category.value = dataResult.id;
|
|
||||||
emit("dropData", {
|
|
||||||
dataResult: [],
|
|
||||||
dataType,
|
|
||||||
dataQuery: getDataQuery,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//?cpn_1=page:2&cpn_2=page:1
|
|
||||||
// Get[Article] Top[5] With[Categories:1]
|
|
||||||
const select = (page: number) => {
|
|
||||||
const componentId = _props.component?.id;
|
|
||||||
if (componentId) {
|
|
||||||
router.push({
|
|
||||||
query: {
|
|
||||||
...route.query,
|
|
||||||
[`cpn_${componentId}`]: `page:${page}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleRouteChange = (query: any) => {
|
|
||||||
const [_, value] = query[`cpn_${_props.component?.id}`]?.split(":");
|
|
||||||
if (value) {
|
|
||||||
loadPage(Number(value));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onBeforeMount(()=>{
|
|
||||||
const result = getInputValue( _props.dataResult, "ARRAY");
|
|
||||||
listArticleByCategory.value = result;
|
|
||||||
handleRouteChange(route.query)
|
|
||||||
})
|
|
||||||
|
|
||||||
const loadPage = (page: string | number) => {
|
|
||||||
console.log(`Loading page ${page}`);
|
|
||||||
// listArticleByCategory.value =
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.query,
|
|
||||||
(newQuery) => {
|
|
||||||
handleRouteChange(newQuery);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<section>
|
|
||||||
<div class="section-container" @dragover.prevent @drop.stop.prevent="dropData($event)"
|
|
||||||
:class="[listArticleByCategory && listArticleByCategory?.length > 0 ? '' : 'noData']">
|
|
||||||
<div class="collection-container">
|
|
||||||
<template v-if="category">
|
|
||||||
<template v-if="listArticleByCategory?.length > 0">
|
|
||||||
<template v-for="(component, index) in listArticleByCategory" :key="index">
|
|
||||||
<DynamicComponent
|
|
||||||
v-if="!isEmpty(component)"
|
|
||||||
:settings="{
|
|
||||||
template: SETTING_OPTIONS.TEMPLATE,
|
|
||||||
layout: SETTING_OPTIONS.LAYOUT,
|
|
||||||
dataResult: { ...component },
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<el-result icon="success" title="Success" sub-title="Nội dung danh sách bài viết sẽ ở đây"> </el-result>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<template v-else><el-empty image-size="90px" description="Kéo Category vào đây" /></template>
|
|
||||||
<div class="button-page flex">
|
|
||||||
<a class="btn-page prev-page">
|
|
||||||
<i class="el-icon">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M609.408 149.376 277.76 489.6a32 32 0 0 0 0 44.672l331.648 340.352a29.12 29.12 0 0 0 41.728 0 30.592 30.592 0 0 0 0-42.752L339.264 511.936l311.872-319.872a30.592 30.592 0 0 0 0-42.688 29.12 29.12 0 0 0-41.728 0z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</i>
|
|
||||||
</a>
|
|
||||||
<a class="btn-page" @click="() => select(index + 1)" v-for="(_, index) in totals">{{ index + 1 }}</a>
|
|
||||||
<a class="btn-page next-page">
|
|
||||||
<i class="el-icon">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M340.864 149.312a30.592 30.592 0 0 0 0 42.752L652.736 512 340.864 831.872a30.592 30.592 0 0 0 0 42.752 29.12 29.12 0 0 0 41.728 0L714.24 534.336a32 32 0 0 0 0-44.672L382.592 149.376a29.12 29.12 0 0 0-41.728 0z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.section-container {
|
|
||||||
.empty {
|
|
||||||
min-height: 100px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #409eff;
|
|
||||||
}
|
|
||||||
.collection-container {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
.basic-article {
|
|
||||||
&.article {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
display: flex;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.noData {
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
margin-top: 10px;
|
|
||||||
justify-content: center;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-page {
|
|
||||||
-webkit-box-align: center;
|
|
||||||
-ms-flex-align: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-page {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 36px;
|
|
||||||
border: 1px solid #409eff;
|
|
||||||
border-radius: 3px;
|
|
||||||
margin-left: 10px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.el-empty {
|
|
||||||
padding: 12px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import DynamicComponent from "~/components/dynamic-page/page-component/templates/index.vue";
|
||||||
|
import { getInputValue } from "@/utils/parseSQL";
|
||||||
|
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const store = reactive({
|
||||||
|
component: useDynamicPageStore(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
dataResult?: any[];
|
||||||
|
dataQuery?: string;
|
||||||
|
component?: any;
|
||||||
|
label?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const SETTING_OPTIONS = {
|
||||||
|
MAX_ELEMENT: 5,
|
||||||
|
TEMPLATE: "TYPE:Card",
|
||||||
|
LAYOUT: "TYPE:Card_Default",
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMPONENT = {
|
||||||
|
taxonomy: enumPageComponentTemplates.ARTICLE,
|
||||||
|
};
|
||||||
|
|
||||||
|
const page = ref(1);
|
||||||
|
const limit = ref(5);
|
||||||
|
const totals = ref(2);
|
||||||
|
const type = "Article";
|
||||||
|
const listArticlePaging = ref([]);
|
||||||
|
|
||||||
|
const listArticleByCategory = computed(() => {
|
||||||
|
const data = getInputValue(_props.dataResult, "OBJECT");
|
||||||
|
if (data && Object.keys(data)?.length > 0) {
|
||||||
|
totals.value = data.Total;
|
||||||
|
return data?.Data;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const designObject = computed(() => {
|
||||||
|
return _props.label ? getInputValue(_props.label, "OBJECT") : {};
|
||||||
|
});
|
||||||
|
|
||||||
|
//?cpn_1=page:2&cpn_2=page:1
|
||||||
|
// Get[Article] Top[5] With[Categories:1]
|
||||||
|
const select = async (pageActive: number) => {
|
||||||
|
const componentId = _props.component?.id;
|
||||||
|
if (componentId) {
|
||||||
|
router.push({
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
[`cpn_${componentId}`]: `page:${pageActive}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
page.value = Number(pageActive);
|
||||||
|
await loadPage(Number(pageActive));
|
||||||
|
};
|
||||||
|
const handleRouteChange = async (query: any) => {
|
||||||
|
const param = query[`cpn_${_props.component?.id}`];
|
||||||
|
if (param) {
|
||||||
|
const [_, value] = param.split(":") || [];
|
||||||
|
page.value = Number(value);
|
||||||
|
await loadPage(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const loadPage = async (page: number) => {
|
||||||
|
let newDataQuery = "";
|
||||||
|
const regex = /Page\[(.*?)\]/;
|
||||||
|
const match = _props.component?.settings?.dataQuery?.match(regex);
|
||||||
|
if (match) {
|
||||||
|
const [pageData] = match;
|
||||||
|
newDataQuery = _props.component?.settings?.dataQuery.replace(pageData, `Page[${page}]`);
|
||||||
|
} else {
|
||||||
|
newDataQuery = _props.component?.settings?.dataQuery + ` Page[${page}]`;
|
||||||
|
}
|
||||||
|
const {item} = await store.component.getOverviewPageComponentById(Number(_props.component?.id), newDataQuery);
|
||||||
|
const data = getInputValue(item?.settings?.dataResult, "OBJECT");
|
||||||
|
|
||||||
|
if (Object.keys(data).length > 0) {
|
||||||
|
totals.value = data.Total;
|
||||||
|
listArticlePaging.value = data?.Data || [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (route.query[`cpn_${_props.component?.id}`]) handleRouteChange(route.query);
|
||||||
|
|
||||||
|
const handleNextPrev = (type: "+" | "-") => {
|
||||||
|
if (listArticleByCategory.value?.length > 0) {
|
||||||
|
if (type === "+") {
|
||||||
|
page.value += 1;
|
||||||
|
} else if (type === "-") {
|
||||||
|
page.value -= 1;
|
||||||
|
}
|
||||||
|
select(page.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapActivesToItems = (index: number) => {
|
||||||
|
if (designObject.value && designObject.value.listCss) {
|
||||||
|
return designObject.value.listCss[index] || {};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section v-if="listArticleByCategory?.length > 0">
|
||||||
|
<div class="section-container">
|
||||||
|
<div class="section-layout" :style="designObject['div.section']">
|
||||||
|
<template v-for="(component, index) in listArticlePaging?.length > 0 ? listArticlePaging : listArticleByCategory" :key="index">
|
||||||
|
<DynamicComponent
|
||||||
|
:settings="{
|
||||||
|
template: SETTING_OPTIONS.TEMPLATE,
|
||||||
|
layout: SETTING_OPTIONS.LAYOUT,
|
||||||
|
dataResult: { ...component },
|
||||||
|
label: mapActivesToItems(Number(index)),
|
||||||
|
}"
|
||||||
|
:component="COMPONENT"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div class="button-page flex">
|
||||||
|
<span class="btn-page prev-page" @click.stop="() => handleNextPrev('-')" v-if="page > 1">
|
||||||
|
<Icon name="ooui:previous-ltr"></Icon>
|
||||||
|
</span>
|
||||||
|
<span :class="['btn-page', page === index + 1 && 'active']" @click.stop="() => select(index + 1)" v-for="(_, index) in Math.ceil(totals / limit)">{{ index + 1 }}</span>
|
||||||
|
<span class="btn-page next-page" @click.stop="() => handleNextPrev('+')" v-if="page < Math.ceil(totals / limit)">
|
||||||
|
<Icon name="ooui:previous-rtl"></Icon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-html="designObject.styleClasses" style="display: none" v-if="designObject.styles"></div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.section-container {
|
||||||
|
.section-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* gap: 10px; */
|
||||||
|
&.border-custom {
|
||||||
|
border-color: #e5e5e5 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.basic-article {
|
||||||
|
&.article {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-page {
|
||||||
|
-webkit-box-align: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-page {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 9px 16px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 36px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-left: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #f2f2f2;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #222222;
|
||||||
|
&.active {
|
||||||
|
background: #ed1c24;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-empty {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as Default_Pagination } from './Default.vue'
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { Default_Pagination } from "./index";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
[enumPageComponentLayouts[`${enumPageComponentTemplate[enumPageComponentKey.SECTION]['ARTICLE']}`]['ARTICLE_SECTION_DEFAULT']]: Default_Pagination,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = computed(() => `${_props.settings.layout}`);
|
||||||
|
|
||||||
|
const GET_PROPS = computed(() => {
|
||||||
|
return () => {
|
||||||
|
let props: any = {};
|
||||||
|
if (_props.settings) {
|
||||||
|
for (const [key, value] of Object.entries(_props.settings)) {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
v-if="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
:is="definedDynamicComponent[getCurrentComponent]"
|
||||||
|
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||