Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 059ab5463c | |||
| 7a92ca829f | |||
| a7d4781a81 | |||
| c36a985b88 |
@@ -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
|
||||
PUBLIC_BASE_SERVER_RESOURCE=https://acp-api.vpress.vn
|
||||
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
|
||||
PUBLIC_BASE_SERVER_RESOURCE=https://api.vpress.vn
|
||||
PUBLIC_BASE_SERVER_RESOURCE=http://api-portal.vpress.vn
|
||||
PUBLIC_PAGING_PAGE=1
|
||||
PUBLIC_PAGING_LIMIT=10
|
||||
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>
|
||||
<NuxtLayout>
|
||||
<NuxtLoadingIndicator />
|
||||
<ErrorBoundary>
|
||||
|
||||
|
||||
<NuxtErrorBoundary>
|
||||
<template #error="{ error }">
|
||||
<div class="text-center my-8">
|
||||
<h2 class="mb-2">404</h2>
|
||||
@@ -38,12 +36,28 @@ useHead({
|
||||
trang chủ</button>
|
||||
</div>
|
||||
</template>
|
||||
</ErrorBoundary>
|
||||
<ScrollToTop />
|
||||
</NuxtErrorBoundary>
|
||||
<KeepAlive>
|
||||
<NuxtPage />
|
||||
</KeepAlive>
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
</a-config-provider> -->
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
<NuxtLoadingIndicator />
|
||||
<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>
|
||||
|
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
|
||||
body
|
||||
font-family: 'Nunito', sans-serif
|
||||
|
||||
video
|
||||
max-width: 100% !important
|
||||
width: unset !important
|
||||
height: unset !important
|
||||
// video
|
||||
// max-width: 100% !important
|
||||
// width: unset !important
|
||||
// height: unset !important
|
||||
|
||||
iframe
|
||||
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 {
|
||||
white-space: nowrap;
|
||||
@@ -40,7 +43,7 @@ img.wide {
|
||||
margin-left: 0;
|
||||
max-width: 70%;
|
||||
width: 70%;
|
||||
transform: translateX(20%);
|
||||
/* transform: translateX(20%); */
|
||||
}
|
||||
|
||||
figure.image.wide {
|
||||
@@ -146,7 +149,7 @@ span.boxRelation .relationBoxText{
|
||||
}
|
||||
|
||||
span.boxRelation .relationText{
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
span.boxRelation .relationDay{
|
||||
|
||||
@@ -1,22 +1,182 @@
|
||||
.style_layout {
|
||||
> .section-container {
|
||||
> .layout_define {
|
||||
> .section_layout {
|
||||
@apply gap-x-14
|
||||
@import url("https://fonts.googleapis.com/css2?family=Gelasio:ital,wght@0,400..700;1,400..700&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100..900;1,100..900&display=swap");
|
||||
|
||||
> div {
|
||||
background: red;
|
||||
// Variables
|
||||
$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.slug}`">
|
||||
<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-5 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,36 +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" @click="selectComponent">
|
||||
<nuxt-link :to="`/${component.code}`" v-for="(component, index) in _dataResult" :key="index" class=" py-1 font-400 text-[16px] first:font-600 first:text-[20px] sm:block hidden first:block first:border-b-[1px] first:border-b-solid first:border-b-[#409eff]">
|
||||
<h3 class="m-0 leading-none hover:text-primary-100 transition-all duration-300">{{ component.title }}</h3>
|
||||
</nuxt-link>
|
||||
</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 Article_BasicCard } from './articles/individuals/Card.vue'
|
||||
export { default as Article_BasicCollection } from './articles/collections/BasicCollection.vue'
|
||||
|
||||
// Category
|
||||
export { default as BasicCategories } from './categories/BasicCategories.vue'
|
||||
export { default as CollectionPaging } from './pageCategories/collection_page.vue'
|
||||
export { default as Articles } from './articles/index.vue'
|
||||
export { default as Navigations } from './navigations/index.vue'
|
||||
export { default as Collections } from './collections/index.vue'
|
||||
export { default as Sections } from './sections/index.vue'
|
||||
export { default as Categories } from './categories/index.vue'
|
||||
export { default as Advertisings } from './advertisings/index.vue'
|
||||
export { default as Others } from './others/index.vue'
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
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<{
|
||||
settings: any;
|
||||
component?: any;
|
||||
content?: any;
|
||||
}>();
|
||||
|
||||
const definedDynamicComponent: Record<string, any> = {
|
||||
[enumPageComponentTemplates.ARTICLE]: Article_BasicCard,
|
||||
[enumPageComponentTemplates.CATEGORY]: BasicCategories,
|
||||
[enumPageComponentTemplates.COLLECTION]: Article_BasicCollection,
|
||||
[enumPageComponentTemplates.ARTICLE]: Articles,
|
||||
[enumPageComponentTemplates.NAVIGATION]: Navigations,
|
||||
[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(() => {
|
||||
return () => {
|
||||
let props: any = {};
|
||||
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,
|
||||
[key]: value,
|
||||
@@ -32,6 +36,9 @@ const GET_PROPS = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <component :is="definedDynamicComponent[getCurrentComponent]" v-bind="{ ...(GET_PROPS()), component: _props.component, settings: _props.settings }" /> -->
|
||||
<component :is="definedDynamicComponent[getCurrentComponent]" v-bind="{ ...(GET_PROPS()), component: _props.component }" />
|
||||
<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,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,34 @@
|
||||
<script setup lang="ts">
|
||||
import JSWidget from '@/components/widget/JSwidget.vue';
|
||||
|
||||
const widgetOptions = {
|
||||
"locale": "vi",
|
||||
"price_line_color": "#71BDDF",
|
||||
"grid_color": "#999999",
|
||||
"label_color": "#999999",
|
||||
"width": "350px",
|
||||
"height": "250px"
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<JSWidget
|
||||
CONTAINER_ID="default_widget_[123123]"
|
||||
SCRIPT_ID="12312312"
|
||||
SCRIPT_SRC="https://www.fireant.vn/Scripts/web/widgets.js"
|
||||
:options="widgetOptions"
|
||||
widgetKey="FireAnt"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
div {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-color: #ededed;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Stock_Default } from './334x641.vue'
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts" setup>
|
||||
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
|
||||
import { Stock_Default } 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,
|
||||
};
|
||||
|
||||
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,239 @@
|
||||
<script setup lang="ts">
|
||||
import { isEmpty } from "@/utils/lodash";
|
||||
import DynamicComponent from "~/components/dynamic-page/page-component/templates/index.vue";
|
||||
import { COLLECTION_PAGING_QUERY_DROP, getInputValue } from "@/utils/parseSQL";
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// const store = reactive({
|
||||
// component: usePageComponentStore(),
|
||||
// });
|
||||
|
||||
const _props = defineProps<{
|
||||
dataResult?: any;
|
||||
dataQuery?: string;
|
||||
component?: any;
|
||||
label?: any;
|
||||
}>();
|
||||
|
||||
const SETTING_OPTIONS = {
|
||||
MAX_ELEMENT: 5,
|
||||
TEMPLATE: "Article",
|
||||
LAYOUT: "TYPE:Card",
|
||||
};
|
||||
|
||||
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 = value;
|
||||
await loadPage(value);
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (route.query[`cpn_${_props.component?.id}`]) handleRouteChange(route.query);
|
||||
});
|
||||
|
||||
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 res = await store.component.getOverviewPageComponentById(Number(_props.component?.id), newDataQuery);
|
||||
// const data = getInputValue(res?.settings?.dataResult, "OBJECT");
|
||||
// if (Object.keys(data).length > 0) {
|
||||
// totals.value = data.Total;
|
||||
// listArticlePaging.value = data?.Data || [];
|
||||
// }
|
||||
};
|
||||
|
||||
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>
|
||||
<div class="section-container" :class="[listArticleByCategory && listArticleByCategory?.length > 0 ? '' : 'noData']">
|
||||
<div class="section-layout" :style="designObject['div.section']">
|
||||
<template v-if="listArticleByCategory?.length > 0">
|
||||
<template v-for="(component, index) in listArticlePaging?.length > 0 ? listArticlePaging : listArticleByCategory">
|
||||
<DynamicComponent
|
||||
:key="index"
|
||||
v-if="!isEmpty(component)"
|
||||
:settings="{
|
||||
template: SETTING_OPTIONS.TEMPLATE,
|
||||
layout: SETTING_OPTIONS.LAYOUT,
|
||||
dataResult: { ...component },
|
||||
label: mapActivesToItems(Number(index)),
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="empty"><h6 class="px-2 text-center">Nội dung danh sách bài viết của danh mục sẽ ở đây</h6></div>
|
||||
</template>
|
||||
<div class="button-page flex">
|
||||
<a class="btn-page prev-page" @click.stop="() => handleNextPrev('-')" v-if="page > 1">
|
||||
<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 v-if="listArticleByCategory?.length > 0" :class="['btn-page', page === index + 1 && 'active']" @click.stop="() => select(index + 1)" v-for="(_, index) in Math.ceil(totals / limit)">{{ index + 1 }}</a>
|
||||
<a class="btn-page next-page" @click.stop="() => handleNextPrev('+')" v-if="page < Math.ceil(totals / limit)">
|
||||
<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 {
|
||||
.section-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* gap: 10px; */
|
||||
overflow-x: scroll;
|
||||
&.border-custom {
|
||||
border-color: #e5e5e5 !important;
|
||||
}
|
||||
&.borderLeft {
|
||||
border-left: 1px solid;
|
||||
}
|
||||
&.borderRight {
|
||||
border-right: 1px solid;
|
||||
}
|
||||
&.borderTop {
|
||||
border-top: 1px solid;
|
||||
}
|
||||
&.borderBottom {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
}
|
||||
.empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 50px;
|
||||
background-color: #409eff;
|
||||
display: flex;
|
||||
white-space: normal;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
h6 {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.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;
|
||||
cursor: pointer;
|
||||
&.active {
|
||||
background: #409eff;
|
||||
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>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Article_Pagination } from './articles/index.vue'
|
||||