117 Commits

Author SHA1 Message Date
nguyen van thai b4aa3e45d1 thainv-dev: Fix emagazine 2024-07-18 17:11:42 +07:00
nguyen van thai a63155a782 thainv-dev: Fix 2024-07-17 14:27:21 +07:00
nguyen van thai 45f21ba187 Fix Footer 2024-07-17 11:00:01 +07:00
nguyen van thai 795cd47e41 thainv-dev: Fix 2024-07-17 10:35:35 +07:00
nguyen van thai 5f9525371d Poll 2024-07-16 22:54:15 +07:00
nguyen van thai 6f571d9549 fix 2024-07-16 13:06:34 +07:00
nguyen van thai 174a596db9 Fix 2024-07-16 11:37:38 +07:00
Duong Truong Phong 5f72a107ce phongdt:add id component 2024-07-16 11:19:46 +07:00
nguyen van thai 5a041acd54 Fix 2024-07-16 09:58:54 +07:00
nguyen van thai 9cc998e0bf Fix 2024-07-16 09:23:44 +07:00
MoreStrive 7565a37d60 feat: create by site 2024-07-15 21:02:22 +07:00
MoreStrive 043f97743c Merge branch 'main' of http://work.gct.com.vn/minhnt/NSG_PORTAL_V2 2024-07-12 19:14:35 +07:00
MoreStrive dffbe39fa6 minhnt-dev: fix page template 2024-07-12 19:13:34 +07:00
nguyen van thai 7151e7d311 . 2024-07-11 08:38:56 +07:00
nguyen van thai 7da82c9101 . 2024-07-10 17:39:38 +07:00
nguyen van thai 212e6d357c . 2024-07-09 16:17:48 +07:00
nguyen van thai 7cb6199610 fix 2024-07-09 15:27:08 +07:00
nguyen van thai 0dba7790b1 Fix 2024-07-09 15:15:22 +07:00
nguyen van thai 776a3cf2c7 fix 2024-07-09 09:41:47 +07:00
nguyen van thai 2e49529934 Fix Responsive 2024-07-09 09:23:17 +07:00
nguyen van thai 28ce3d42a0 Fix Responsive 2024-07-09 08:33:33 +07:00
nguyen van thai 08ad924483 Responsive 2024-07-08 17:38:54 +07:00
MoreStrive 76b2fa4771 minhnt-dev: scroll smooth 2024-07-08 17:03:33 +07:00
MoreStrive 81bfa351e8 Merge branch 'main' of http://work.gct.com.vn/minhnt/NSG_PORTAL_V2 2024-07-06 18:29:42 +07:00
MoreStrive a3e20c9445 feat: select tag 2024-07-06 18:29:38 +07:00
nguyen van thai 6806201085 thainv-dev: Fix 2024-07-06 18:19:38 +07:00
MoreStrive 4ec2e425df Merge branch 'main' of http://work.gct.com.vn/minhnt/NSG_PORTAL_V2 2024-07-06 16:34:43 +07:00
MoreStrive 31175ade27 minhnt-dev: fix lodash 2024-07-06 16:34:25 +07:00
nguyen van thai ccd92c86ee thainv-dev: Nhúng 2024-07-06 16:28:45 +07:00
phongdt a47229f44e Merge pull request 'phongdt:fix paging' (#9) from phongdt into main
Reviewed-on: http://work.gct.com.vn/minhnt/NSG_PORTAL_V2/pulls/9
2024-07-05 15:32:49 +07:00
Duong Truong Phong 0ad19bbcfd phongdt:fix paging 2024-07-05 15:30:58 +07:00
phongdt 11ea05de83 Merge pull request 'phongdt' (#8) from phongdt into main
Reviewed-on: http://work.gct.com.vn/minhnt/NSG_PORTAL_V2/pulls/8
2024-07-05 15:13:26 +07:00
Duong Truong Phong e738cca263 Merge branch 'phongdt' of http://work.gct.com.vn/minhnt/NSG_PORTAL_V2 into phongdt 2024-07-05 15:13:00 +07:00
Duong Truong Phong b93f0218a5 phongdt:paging 2024-07-05 15:12:49 +07:00
Duong Truong Phong 3fe4da0ecb phongdt: paging component 2024-07-05 15:12:49 +07:00
Duong Truong Phong a9d6bea337 phongdt:paging 2024-07-05 15:12:16 +07:00
nguyen van thai df31b7bdef . 2024-07-05 15:03:50 +07:00
MoreStrive a1c6e2872f feat: full size 2024-07-05 15:01:01 +07:00
Duong Truong Phong 46b808cf9c phongdt: paging component 2024-07-05 14:51:25 +07:00
MoreStrive 367374863e Merge branch 'main' of http://work.gct.com.vn/minhnt/NSG_PORTAL_V2 2024-07-05 14:29:53 +07:00
MoreStrive 66b5a8ce6a feat: widget 2024-07-05 14:29:49 +07:00
nguyen van thai adecec9041 Layout Page Component 2024-07-05 14:17:41 +07:00
MoreStrive 984ec50a39 Merge branch 'main' of http://work.gct.com.vn/minhnt/NSG_PORTAL_V2 2024-07-05 13:05:28 +07:00
MoreStrive a756c91bd0 feat: widget 2024-07-05 13:05:21 +07:00
nguyen van thai cf64f11e72 thainv-dev 2024-07-05 11:57:41 +07:00
nguyen van thai 780474bcb3 Merge branch 'main' of http://work.gct.com.vn/minhnt/NSG_PORTAL_V2 2024-07-05 11:41:49 +07:00
nguyen van thai be1393b7da thainv-dev: Layout Page Section 2024-07-05 11:41:38 +07:00
MoreStrive a5f9ff7bac fix: null component 2024-07-05 11:02:04 +07:00
nguyen van thai 5889e9af0e thainv-dev 2024-07-05 10:39:07 +07:00
MoreStrive 17036b77af feat: layout none bugs 2024-07-05 10:29:08 +07:00
MoreStrive ac218aeac5 fea: navigation 2024-07-05 10:03:28 +07:00
MoreStrive 815ce88d95 Merge branch 'main' of http://work.gct.com.vn/minhnt/NSG_PORTAL_V2 2024-07-05 09:48:47 +07:00
MoreStrive 76d4628100 feat: fix type 2024-07-05 09:48:34 +07:00
nguyen van thai 7bf902041e thainv-dev 2024-07-05 09:45:00 +07:00
nguyen van thai 554ceab3c6 thainv-dev: Fix 2024-07-03 15:33:51 +07:00
MoreStrive ee5c6f40f1 deploy 2024-07-02 16:03:24 +07:00
MoreStrive 4bf217c207 Merge branch 'main' of http://work.gct.com.vn/minhnt/NSG_PORTAL_V2 2024-07-01 16:06:28 +07:00
MoreStrive 0adb6fca36 minhnt-dev: Video Card 2024-07-01 16:05:16 +07:00
nguyen van thai 35f069a776 thainv-dev: 2024-07-01 16:04:16 +07:00
MoreStrive a01eedc2bc minhnt-dev: fix bugs 2024-07-01 15:14:17 +07:00
MoreStrive 03ca9c6603 minhnt-dev: fix bugs 2024-07-01 14:51:57 +07:00
MoreStrive 5a207435ce feat: bugs 2024-06-28 17:25:58 +07:00
MoreStrive 40622caf61 feat: fix lodash import 2024-06-28 16:41:36 +07:00
nguyen van thai 26e4a289d7 a 2024-06-28 16:27:39 +07:00
phongdt cbc0f8b7c0 Merge pull request 'phongdt:fix ui' (#7) from phongdt into main
Reviewed-on: http://work.gct.com.vn/minhnt/NSG_PORTAL_V2/pulls/7
2024-06-28 16:15:35 +07:00
Duong Truong Phong 80a5aae4e6 phongdt:fix ui 2024-06-28 16:14:06 +07:00
MoreStrive a46ef56e07 feat: scss 2024-06-28 16:04:13 +07:00
MoreStrive 1f60621995 feat: scss 2024-06-28 15:44:12 +07:00
MoreStrive 36e39fa0d5 Merge branch 'main' of http://work.gct.com.vn/minhnt/NSG_PORTAL_V2 2024-06-28 15:40:33 +07:00
MoreStrive ad962eda86 feat: new layout 2024-06-28 15:39:26 +07:00
nguyen van thai 24ecc2195d thainv-dev: Fix bài viết liên quan 2024-06-25 14:52:45 +07:00
nguyen van thai fcb826a7c6 thainv-dev: Fix Ui 2024-06-25 09:08:44 +07:00
MoreStrive ab3419bd5f minhnt-dev: oonly ssr fix 2024-06-24 16:46:59 +07:00
MoreStrive e151dda2ad minhnt-dev: SEO 2024-06-21 17:51:36 +07:00
MoreStrive a447a8a7aa minhnt-dev: fix log in server 2024-06-21 09:59:14 +07:00
MoreStrive 6a275c354e minhnt-dev: only ssr 2024-06-21 09:56:34 +07:00
MoreStrive 229155b24a minhnt-dev: footer 2024-06-19 16:13:42 +07:00
MoreStrive 3b613faccf Merge branch 'main' of http://work.gct.com.vn/minhnt/NSG_PORTAL_V2 2024-06-18 14:26:06 +07:00
MoreStrive f9c2d748d5 minhnt-dev: loadash import 2024-06-18 14:25:58 +07:00
nguyen van thai d103f4bbf7 thainv-dev: UI 2024-06-18 14:04:24 +07:00
MoreStrive f17e28472c minhnt-dev: navigation 2024-06-17 13:20:31 +07:00
nguyen van thai 3c75c89a8b thainv: ghép navigation 2024-06-17 11:48:00 +07:00
MoreStrive c2b9208746 minhnt-dev: add build node with arr 2024-06-17 09:55:33 +07:00
nguyen van thai ecf4512cd3 thainv-dev: sửa lại cấu trúc folder 2024-06-13 17:24:46 +07:00
nguyen van thai 8818c73cec thainv-dev: fix css 2024-06-12 17:56:44 +07:00
nguyen van thai 339d370f22 thainv-dev: Xóa bớt file, folder thừa 2024-06-12 17:36:38 +07:00
nguyen van thai 5b1e0af397 thainv-dev: tạo lại cấu trúc folder và làm UI 2024-06-12 17:17:49 +07:00
nguyen van thai c217ed82c9 thainnv-dev: Nhúng 2024-06-06 13:29:22 +07:00
nguyen van thai 0a7774b7f4 Merge branch 'main' of http://work.gct.com.vn/minhnt/NSG_PORTAL_V2 into thainv-dev 2024-06-04 15:04:05 +07:00
nguyen van thai a0a5651ac0 thainv-dev: fix ui 2024-06-04 15:03:54 +07:00
MoreStrive 4d239e9f32 minhnt-dev: dynamic section 2024-06-04 09:59:12 +07:00
phongdt 45cd7780d6 Merge pull request 'phongdt:sort' (#6) from phongdt into main
Reviewed-on: http://work.gct.com.vn/minhnt/NSG_PORTAL_V2/pulls/6
2024-06-04 09:41:26 +07:00
Duong Truong Phong 343ac29a57 phongdt:sort 2024-06-04 09:40:47 +07:00
nguyen van thai 0729e021bd thainv-dev: fix ui 2024-06-04 08:41:38 +07:00
nguyen van thai 58b5c67d0c thainv-dev: fix ui 2024-06-03 21:53:23 +07:00
phongdt d95d648687 Merge pull request 'phongdt:fix layout article' (#5) from phongdt into main
Reviewed-on: http://work.gct.com.vn/minhnt/NSG_PORTAL_V2/pulls/5
2024-06-03 15:20:56 +07:00
Duong Truong Phong 6bf22ee747 phongdt:fix layout article 2024-06-03 15:20:13 +07:00
nguyen van thai 27c4917697 Merge branch 'main' of http://work.gct.com.vn/minhnt/NSG_PORTAL_V2 into thainv-dev 2024-06-03 12:30:08 +07:00
phongdt f744c90969 Merge pull request 'phongdt:page video' (#4) from phongdt into main
Reviewed-on: http://work.gct.com.vn/minhnt/NSG_PORTAL_V2/pulls/4
2024-06-03 12:28:40 +07:00
nguyen van thai f22a16d42a thainv-dev: Fix UI 2024-06-03 12:27:58 +07:00
Duong Truong Phong 0bfbfa7711 phongdt:page video 2024-06-03 12:27:55 +07:00
MoreStrive 94ea03f189 feat: Seo meta tag 2024-06-03 10:24:28 +07:00
MoreStrive e126b2ab40 feat: merge env 2024-06-03 09:27:47 +07:00
MoreStrive 66ac28b468 fix: scss 2024-05-31 17:08:43 +07:00
MoreStrive 27b57f394f Merge branch 'main' of http://work.gct.com.vn/minhnt/NSG_PORTAL_V2 2024-05-31 17:06:04 +07:00
MoreStrive 57a8a5e15d minhnt-dev: fix build 2024-05-31 17:05:23 +07:00
phongdt 0364939e00 Merge pull request 'phongdt:fix collection' (#3) from phongdt into main
Reviewed-on: http://work.gct.com.vn/minhnt/NSG_PORTAL_V2/pulls/3
2024-05-31 17:00:05 +07:00
Duong Truong Phong cfb0592ce3 phongdt:fix collection 2024-05-31 16:59:14 +07:00
phongdt dc94e7c3b3 Merge pull request 'phongdt' (#2) from phongdt into main
Reviewed-on: http://work.gct.com.vn/minhnt/NSG_PORTAL_V2/pulls/2
2024-05-31 16:39:05 +07:00
Duong Truong Phong 2f8c9b9cb8 phongdt:podcast details 2024-05-31 16:38:12 +07:00
Duong Truong Phong 9e72b00aa7 phongdt:audio podcast 2024-05-31 15:32:58 +07:00
Duong Truong Phong 3a7132ea98 phongdt:podcast 2024-05-31 15:32:58 +07:00
nguyen van thai c5887d911f thainv-dev: fix 2024-05-31 15:31:05 +07:00
MoreStrive 892bddde2f minhnt-dev: get by slug 2024-05-31 15:04:22 +07:00
MoreStrive 7b01ce6170 minhnt-dev: design 2024-05-31 13:49:36 +07:00
MoreStrive 709a2c3232 Merge branch 'main' of http://work.gct.com.vn/minhnt/NSG_PORTAL_V2 2024-05-31 12:50:46 +07:00
MoreStrive 94abf5ce61 minhnt-dev: api service 2024-05-31 12:49:28 +07:00
220 changed files with 10637 additions and 2353 deletions
+1 -1
View File
@@ -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
+6 -3
View File
@@ -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
+19 -5
View File
@@ -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>
<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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

+4 -6
View File
@@ -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
+6 -3
View File
@@ -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{
+210 -12
View File
@@ -1,22 +1,220 @@
.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;
> div {
article {
background: red;
h3 {
@apply text-xl #{!important}
$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;
scroll-behavior: smooth;
}
// %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;
}
}
@media (max-width: 640px) {
.collection-container {
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
& > article.basic-article {
flex-direction: row!important;
& > .basic-article_thumbnail {
width: 50%!important;
}
}
}
}
.emagazine, .infographic, .image {
p {
@apply w-full max-w-660px mx-auto text-18px font-raleway leading-180%;
}
figure {
@apply w-full;
}
img {
@apply mx-auto;
}
}
.emagazine {
h1,h2,h3,h4,h5,h6,span,em {
@apply w-full max-w-660px mx-auto;
}
}
.detail-default {
p {
@apply text-18px font-raleway leading-180% my-10px;
}
}
+14
View File
@@ -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>
+23
View File
@@ -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>
+15
View File
@@ -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>
+14
View File
@@ -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>
+14
View File
@@ -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>
+154
View File
@@ -0,0 +1,154 @@
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;
}
await loadData();
// onBeforeMount(async () => {
// });
// const selectedOption = ref<any>(-1);
// const showResult = ref(false);
const alreadyVoted = ref(false);
const showResult = 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 toggleResults = () => {
// if (canShowResult.value) showResult.value = !showResult.value;
// };
const singleSelect = ref<number>(-1)
const multipleSelect = ref<number []>([])
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;
// }
// }
switch (poll.type) {
case 1:
if(singleSelect.value >= 0) {
const result = await store.pollResponse.create({ optionId: singleSelect.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 === singleSelect.value) {
option.responseCount = (option.responseCount ?? 0) + 1;
}
option.percentage = Number(((option.responseCount / totalResponses.value) * 100).toFixed(1));
alreadyVoted.value = true
});
}
}
}
}
</script>
<template>
<div class="inline-block px-4 py-2 mb-5">
<h3 class="text-#000 font-raleway font-italic text-16px font-500 leading-140% mb-2">{{ poll?.title }}</h3>
<ul class="flex flex-col gap-3">
<template v-if="poll.type === 1">
<li v-for="(option, index) in options" :key="index">
<input :id="`option-${index}`" :disabled="singleSelect >= 0 && alreadyVoted ? true : false" type="radio" class="peer opacity-0" :value="option.id" v-model="singleSelect">
<label :for="`option-${index}`" class="font-raleway text-16px relative text-#000 font-400 leading-140% pl-16px before:content-[''] before:inline-block before:w-14px before:h-14px before:rounded-50% before:border-1 before:absolute before:left--3 before:top-10px before:border-1px before:border-#000 before:translate-y--50% after:absolute after:left--10px after:top-10px after:translate-y--50% after:border-#000 after:content-[''] after:inline-block after:w-10px after:h-10px after:rounded-full peer-checked:after:bg-primary peer-checked:before:border-primary ">{{ option?.title }}</label>
</li>
</template>
</ul>
<div class="flex items-center justify-center mt-3">
<button @click="submitVote()" v-if="!alreadyVoted" :disabled="alreadyVoted && singleSelect >= 0 ? true : false" class="px-5 py-10px rounded-6px bg-primary text-#fff font-raleway text-14px font-400">Gửi kết quả</button>
<button @click="showResult = true" v-if="alreadyVoted" class="px-5 py-10px rounded-6px bg-primary text-#fff font-raleway text-14px font-400">Xem kết quả</button>
</div>
<div class="mt-5" v-if="showResult && canShowResult">
<h3 class="text-#000 font-raleway font-italic text-16px font-500 leading-140% mb-2">{{ poll?.title }}</h3>
<ul>
<li v-for="(answer, index) in options" :key="index" class="flex gap-4 items-center text-#000 font-raleway text-12px leading-140%">
{{ answer.percentage}}%
<div :style="{ width: `${answer.percentage}%` }" :class="answer.id === singleSelect ? 'bg-primary' : 'bg-#AEAEAE'" class="h-1.5 rounded-full"></div>
</li>
</ul>
</div>
</div>
<!-- <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>
+320
View File
@@ -0,0 +1,320 @@
<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 !text-black-500">
<h5 class="underline decoration-gray-500 font-bold mb-2">Câu đố: {{ data?.title }}</h5>
<ul class="px-3 flex flex-col gap-3">
<li v-for="(question, questionIndex) in data.questionGeneral" :key="questionIndex" class="mb-2">
<h5 class="text-#000 font-raleway font-italic text-16px font-500 leading-140% mb-2">{{ `${questionIndex + 1}. ${question.title}` }}</h5>
<ul>
<template v-if="question.type === 1">
<li v-for="(answer, answerIndex) in question.answers" :key="answerIndex" class="flex items-center gap-1 py-1">
<input :id="`answer-${questionIndex}-${answerIndex}`" type="radio" :value="answerIndex" class="peer opacity-0" v-model="selectQuizAnswer[questionIndex]" />
<label :for="`answer-${questionIndex}-${answerIndex}`" class="font-raleway text-16px relative text-#000 font-400 leading-140% pl-16px before:content-[''] before:inline-block before:w-14px before:h-14px before:rounded-50% before:border-1 before:absolute before:left--3 before:top-10px before:border-1px before:border-#000 before:translate-y--50% after:absolute after:left--10px after:top-10px after:translate-y--50% after:border-#000 after:content-[''] after:inline-block after:w-10px after:h-10px after:rounded-full peer-checked:after:bg-primary peer-checked:before:border-primary ">{{ answer.title }}</label>
</li>
</template>
<template v-else>
<li v-for="(answer, answerIndex) in question.answers" :key="answerIndex" class="flex items-center gap-1 py-1">
<input :id="`answer-${questionIndex}-${answerIndex}`" type="checkbox" :value="answerIndex" class="peer opacity-0" v-model="selectQuizAnswer[questionIndex]" />
<label :for="`answer-${questionIndex}-${answerIndex}`" class="font-raleway text-16px relative text-#000 font-400 leading-140% pl-16px before:content-[''] before:inline-block before:w-14px before:h-14px before:rounded-2px before:border-1 before:absolute before:left--3 before:top-10px before:border-1px before:border-#000 before:translate-y--50% after:absolute after:left--10px after:top-6px after:translate-y--50% after:border-#000 after:content-['✔'] after:text-12px after:text-transparent after:inline-block after:w-14px after:h-14px peer-checked:after:text-white peer-checked:before:border-primary peer-checked:before:bg-primary ">{{ answer.title }}</label>
</li>
</template>
</ul>
</li>
</ul>
<div class="flex justify-center">
<button @click="submitSend" class="mx-auto px-5 py-10px rounded-6px bg-primary text-#fff font-raleway text-14px font-400">Gửi câu trả lời</button>
</div>
</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>
+316
View File
@@ -0,0 +1,316 @@
<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 !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="text-#000 font-raleway font-italic text-16px font-500 leading-140% mb-2">{{ `${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> -->
<ul>
<template v-if="question.type === 1">
<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="radio" :value="answerIndex" class="peer opacity-0" v-model="selectSurveyAnswer[questionIndex]" />
<label :for="`answer-survey-${questionIndex}-${answerIndex}`" class="font-raleway text-16px relative text-#000 font-400 leading-140% pl-16px before:content-[''] before:inline-block before:w-14px before:h-14px before:rounded-50% before:border-1 before:absolute before:left--3 before:top-10px before:border-1px before:border-#000 before:translate-y--50% after:absolute after:left--10px after:top-10px after:translate-y--50% after:border-#000 after:content-[''] after:inline-block after:w-10px after:h-10px after:rounded-full peer-checked:after:bg-primary peer-checked:before:border-primary ">{{ answer.title }}</label>
</li>
</template>
<template v-else>
<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="checkbox" :value="answerIndex" class="peer opacity-0" v-model="selectSurveyAnswer[questionIndex]" />
<label :for="`answer-survey-${questionIndex}-${answerIndex}`" class="font-raleway text-16px relative text-#000 font-400 leading-140% pl-16px before:content-[''] before:inline-block before:w-14px before:h-14px before:rounded-2px before:border-1 before:absolute before:left--3 before:top-10px before:border-1px before:border-#000 before:translate-y--50% after:absolute after:left--10px after:top-6px after:translate-y--50% after:border-#000 after:content-['✔'] after:text-12px after:text-transparent after:inline-block after:w-14px after:h-14px peer-checked:after:text-white peer-checked:before:border-primary peer-checked:before:bg-primary ">{{ answer.title }}</label>
</li>
</template>
</ul>
</li>
</ul>
<div class="flex justify-center">
<button @click="submitSend" class="mx-auto px-5 py-10px rounded-6px bg-primary text-#fff font-raleway text-14px font-400">Gửi câu trả lời</button>
</div>
<!-- <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>
+15
View File
@@ -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,61 @@
<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 contentParse = computed(() => (currentPage.value.content ? JSON.parse(currentPage.value.content) : {}));
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:
if (contentParse.value.navigationTop) {
result =
currentPage.value.components &&
currentPage.value.components.find((component: any) => {
return component.id === contentParse.value.navigationTop;
});
}
break;
case defineTypeRecusive.BOTTOM_NAVIGATION:
if (contentParse.value.navigationBottom) {
result =
currentPage.value.components &&
currentPage.value.components.find((component: any) => {
return component.id === contentParse.value.navigationBottom;
});
}
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,173 @@
<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;
component?: 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 :id="`cpn_${props.component.id}`" class="card-audio" :class="LAYOUT_PARSE['article_Class']" :style="LAYOUT_PARSE['article']">
<nuxt-link :to="`/bai-viet/${parseData?.slug}`" class="article-thumbnail">
<img :src="parseData?.thumbnail ? parseData?.thumbnail : 'https://indiaeducationdiary.in/wp-content/uploads/2021/02/SD-default-image.png'" :alt="parseData?.title?.replace(/<[^>]+>/g, '')" />
</nuxt-link>
<div class="card-audio__content">
<span class="flex justify-center">
<template v-if="['Image', 'Infographics', 'Emagazine'].includes(type)">
<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>
<template v-if="['Postcard', 'Video'].includes(type)">
<svg width="28" height="18" viewBox="0 0 28 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.5 2.25V15.75C18.5 17.0156 17.4688 18 16.25 18H2.75C1.48438 18 0.5 17.0156 0.5 15.75V2.25C0.5 1.03125 1.48438 0 2.75 0H16.25C17.4688 0 18.5 1.03125 18.5 2.25ZM27.5 3V15.0469C27.5 16.2188 26.0938 16.9219 25.1094 16.2656L20 12.7031V5.34375L25.1094 1.78125C26.0938 1.125 27.5 1.82812 27.5 3Z" fill="white"/>
</svg>
</template>
</span>
<div class="card-audio__type-category" >
<div class="card-audio__type" v-if="type">{{ type }}</div>
<nuxt-link v-if="parseData?.category" 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>
</template>
<style lang="scss">
.card-audio {
position: relative;
width: 100%;
padding-bottom: calc((16 / 9) * 100%);
overflow: hidden;
.article-thumbnail {
position: absolute;
height: 100%;
width: 100%;
z-index: 1;
& img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.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,256 @@
<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;
component?: 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"
:id="`cpn_${props.component.id}`"
class="basic-article border-custom"
:class="LAYOUT_PARSE['article_Class']"
:style="LAYOUT_PARSE['article']"
>
<div class="basic-article_thumbnail 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'], 'article-time']">{{
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-5 article-intro" :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'], 'article-time']">{{
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;
}
@media (max-width: 640px) {
padding-left: 0px!important;
padding-right: 0px!important;
border: 0px solid transparent!important;
flex-direction: column !important;
& .basic-article_thumbnail {
width: 100% !important;
}
}
}
</style>
@@ -0,0 +1,114 @@
<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;
component?: any;
}>();
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 :id="`cpn_${props.component.id}`" class="basic-article border-custom" :class="LAYOUT_PARSE['article_Class']" :style="LAYOUT_PARSE['article']">
<div class="article_miss">
<template v-if="parseData">
<nuxt-link :to="`/bai-viet/${parseData.slug}`">
<div class="article_miss_thumb custom-thumb" :style="{ backgroundImage: `url('${parseData.thumbnail ? parseData.thumbnail : '/images/default-thumbnail.jpg'}')` }"></div>
</nuxt-link>
<div class="article_miss_content" :style="LAYOUT_PARSE['content']">
<nuxt-link :to="`/bai-viet/${parseData.slug}`">
<h3 class="line-clamp text-white" :class="LAYOUT_PARSE['title_Class']" :style="LAYOUT_PARSE['h3.title']">
{{ parseData.title?.replace(/<[^>]+>/g, "") }}
</h3>
</nuxt-link>
</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,166 @@
<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;
component?: 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
:id="`cpn_${props.component.id}`"
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,138 @@
<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?: string;
component?: any;
}>();
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 :id="`cpn_${props.component?.id}`" class="basic-article border-custom" :class="LAYOUT_PARSE['article_Class']" @click="selectComponent" @dragover.prevent @drop.stop.prevent="drop" :style="LAYOUT_PARSE['article']">
<div class="article_video">
<template v-if="parseData">
<div class="article_video_thumb article-thumbnail" :style="{ backgroundImage: `url('${parseData.thumbnail ? parseData.thumbnail : '/images/default-thumbnail.jpg'}')` }">
<div></div>
<div class="article_video_content">
<span>
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M23.5 18.5C24.375 17.875 25.625 17.875 26.5 18.5L44.5 29.5C45.375 30 46 31 46 32C46 33.125 45.375 34.125 44.5 34.625L26.5 45.625C25.625 46.125 24.375 46.25 23.5 45.625C22.5 45.125 22 44.125 22 43V21C22 20 22.5 19 23.5 18.5ZM64 32C64 49.75 49.625 64 32 64C14.25 64 0 49.75 0 32C0 14.375 14.25 0 32 0C49.625 0 64 14.375 64 32ZM32 6C17.625 6 6 17.75 6 32C6 46.375 17.625 58 32 58C46.25 58 58 46.375 58 32C58 17.75 46.25 6 32 6Z"
fill="#ED1C24"
/>
</svg>
</span>
<h3 class="line-clamp article-title" :class="LAYOUT_PARSE['title_Class']" :style="LAYOUT_PARSE['h3.title']">
{{ parseData.title?.replace(/<[^>]+>/g, "") }}
</h3>
<p class="mb-0 line-clamp article-intro" :class="LAYOUT_PARSE['paragraph_Class']" :style="LAYOUT_PARSE['p.paragraph']">
{{ parseData.intro?.replace(/<[^>]+>/g, "") }}
</p>
</div>
</div>
</template>
<div v-else class="empty-box">
<div class="d-flex justify-content-center align-items-center flex-column">
<i class="ri-play-circle-line"></i>
</div>
</div>
</div>
<div v-html="LAYOUT_PARSE.styleClasses" v-if="LAYOUT_PARSE.styles"></div>
</article>
</template>
<style lang="scss" scoped>
.article_video {
.article_video_thumb {
background-size: cover;
background-repeat: no-repeat;
background-position: center;
position: relative;
z-index: 0;
padding: 120px 85px 60px 85px;
border-radius: 2px;
margin: 10px;
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;
color: white;
}
p {
font-size: 14px;
font-weight: 400;
color: white;
}
i {
font-size: 80px;
color: #ed1c24;
}
}
}
.empty-box {
background-color: #409eff;
margin: 10px;
min-height: 60px;
i {
font-size: 60px;
}
}
}
</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,95 @@
<script setup lang="ts">
const emit = defineEmits(["dropData", "selectComponent"]);
const _props = defineProps<{
dataResult?: any;
dataType?: any;
dataQuery?: any;
layout?: string;
label?: string;
}>();
const { currentArticle } = storeToRefs(useArticleStore())
console.log(currentArticle.value, 'currentArticle')
</script>
<template>
<div class="overflow-hidden emagazine">
<h2 class="font-gelasio text-center text-44px font-bold leading-130%" v-if="currentArticle?.title" v-html="currentArticle?.title"></h2>
<div class="article-detail" v-html="currentArticle.detail"></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;
}
}
}
.content {
width: auto;
}
</style>
@@ -0,0 +1,236 @@
<script setup lang="ts">
import { enumPageComponentTemplates } from "@/definitions/enum";
import { DEFAULT_QUERY_DROP, getInputValue } from "@/utils/parseSQL";
const emit = defineEmits(["dropData", "selectComponent"]);
const _props = defineProps<{
dataResult?: any;
dataType?: any;
dataQuery?: any;
layout?: string;
label?: string;
}>();
const SETTING_OPTIONS = {
BREADCRUMB_MAX_ELEMENT: 3,
};
const LAYOUT_PARSE = computed(() => {
const designObject = _props.label ? getInputValue(_props.label, "OBJECT") : {};
return Object.assign({}, designObject);
});
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>
<div @click="selectComponent" class="overflow-hidden" @dragover.prevent @drop.stop.prevent="drop">
<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"
>
<p 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,196 @@
<script setup lang="ts">
const emit = defineEmits(["dropData", "selectComponent"]);
const { categoryTree } = storeToRefs(useCategoryStore());
const { currentArticle } = storeToRefs(useArticleStore());
if (categoryTree.value) {
await useCategoryStore().fetchBySiteId();
}
const _props = defineProps<{
dataResult?: any[];
}>();
const SETTING_OPTIONS = {
BREADCRUMB_MAX_ELEMENT: 3,
};
const currentCategoryTree = findElementPathById(categoryTree.value, currentArticle.value.categoryId);
function findElementPathById(categories: any[], targetId: number, path: any[] = []) {
for (const category of categories) {
const currentPath = [...path, { title: category.title, code: category.code }];
if (category.id === targetId) {
return currentPath;
}
if (category.children) {
const result: any = findElementPathById(category.children, targetId, currentPath);
if (result) {
return result;
}
}
}
return null;
}
console.log(currentArticle.value, "currentArticle");
</script>
<template>
<div class="overflow-hidden w-full max-w-1385px mx-auto px-30px image">
<div class="">
<div class="category flex justify-between flex-wrap items-center mb-10px">
<ul class="flex gap-32px">
<li v-for="(category, index) in currentCategoryTree" :key="index" class="first:text-#000 text-#929292 last:after:content-[''] relative after:absolute after:content-['/'] after:text-20px after:right--20px">
<nuxt-link class="font-raleway text-18px font-500 leading-180% uppercase" :to="`/${category.code}`">{{ category.title }}</nuxt-link>
</li>
</ul>
<div v-if="currentArticle.topics" class="pl-20px relative bg-primary inline-block">
<nuxt-link class="h-30px block py-4px px-16px border-1 border-#000 bg-white text-12px leading-180% font-raleway font-400" :to="`/topic/${currentArticle.topics[0].code}`">{{ currentArticle.topics[0].title }}</nuxt-link>
</div>
</div>
<h2 class="font-gelasio text-44px font-bold leading-130%" v-if="currentArticle.title" v-html="currentArticle.title"></h2>
<!-- <div class="grid grid-cols-12 gap-20px">
<div class="col-span-3"></div>
</div> -->
<div class="author flex gap-12px my-20px" v-if="currentArticle.authors">
<ul class="flex">
<li :style="{ 'z-index': index + 1 }" class="relative ml--12px first:ml-0" v-for="(author, index) in currentArticle.authors" :key="index">
<nuxt-link :to="`/tac-gia/${author.code}`">
<img :src="author.thumbnail || `http://picsum.photos/1024/600?random=1`" alt="" class="w-64px p-1px border-1px border-white h-64px object-cover rounded-full" />
</nuxt-link>
</li>
</ul>
<div>
<div class="mt-10px">
<nuxt-link class="font-raleway text-#000" v-for="(author, index) in currentArticle.authors" :key="index" :to="`/tac-gia/${author.code}`">{{ author.title + (index < currentArticle.authors.length - 1 ? ", " : "") }}</nuxt-link>
</div>
<div class="text-12px">Xuất bản vào {{ formatDate(currentArticle.publishedOn, "DD/MM/YYYY | hh:mm") }}</div>
</div>
</div>
<div v-html="currentArticle.detail"></div>
</div>
</div>
</template>
<style scoped lang="scss">
$max-width: 1276px;
.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,101 @@
<script setup lang="ts">
const emit = defineEmits(["dropData", "selectComponent"]);
const _props = defineProps<{
dataResult?: any;
dataType?: any;
dataQuery?: any;
layout?: string;
label?: string;
}>();
const { currentArticle } = storeToRefs(useArticleStore())
console.log(currentArticle.value, 'currentArticle')
</script>
<template>
<div class="overflow-hidden infographic">
<!-- bổ sung sau -->
<!-- <img :src="currentArticle.thumbnail" alt="" class="w-full object-cover">
<div class="px-44px pb-30px my-30px max-w-660px mx-auto border-b-1px border-#000">
</div> -->
<h2 class="font-gelasio text-center text-44px font-bold leading-130%" v-if="currentArticle?.title" v-html="currentArticle?.title"></h2>
<div v-html="currentArticle.detail"></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;
}
}
}
.center-y {
width: auto;
}
</style>
@@ -0,0 +1,479 @@
<script setup lang="ts">
import { useArticleStore } from "~/stores/articles";
import Poll from "~/components/article/immerse/Poll.vue";
import Quiz from "~/components/article/immerse/Quiz.vue";
import Survey from "~/components/article/immerse/Survey.vue";
import Document from "~/components/article/immerse/Document.vue";
import Attachment from "@/components/article/immerse/Attachment.vue";
import Tag from "@/components/article/immerse/Tag.vue";
const { currentArticle } = storeToRefs(useArticleStore());
import { useDynamicPageStore } from "~/stores/dynamic-page";
import { useCategoryStore } from "~/stores/category";
const store = reactive({
dynamicPage: useDynamicPageStore(),
article: useArticleStore(),
category: useCategoryStore(),
});
const { categoryTree } = storeToRefs(store.category);
await store.category.fetchBySiteId();
const currentCategoryTree = (store.category.currentCategoryTree = findElementPathById(categoryTree.value, currentArticle.value.categoryId));
function findElementPathById(categories: any[], targetId: number, path: any[] = []) {
for (const category of categories) {
const currentPath = [...path, { title: category.title, code: category.code }];
if (category.id === targetId) {
return currentPath;
}
if (category.children) {
const result: any = findElementPathById(category.children, targetId, currentPath);
if (result) {
return result;
}
}
}
return null;
}
onMounted(async () => {
clickElement("figure", "custom-figure", "data-code");
clickElement("author", "author", "data-code");
let detailEmagazine = document.querySelector('div[layout="ARTICLE_DETAIL_EMAGAZINE"]');
let breadcrumb = document.querySelector('div[layout="BREADCRUM_DEFAULT"]');
if (detailEmagazine && breadcrumb) {
breadcrumb.classList.add("lg:max-w-640px", "mx-auto");
}
});
function clickElement(type: string, selector: string, attribute: string) {
const elements = document.querySelectorAll(selector);
elements.forEach((element) => {
element.addEventListener("click", (event) => {
event.preventDefault();
const url = `${window.location.protocol}//${window.location.host}/${type}/${element.getAttribute(attribute)}`;
const a = document.createElement("a");
a.href = url;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
});
}
const isBookmark = ref(false);
const onClickBookmark = () => {
isBookmark.value = !isBookmark.value;
};
async function copyLink() {
try {
const url = window.location.href;
await navigator.clipboard.writeText(url);
alert("copy link thành công");
} catch (error) {
alert(error);
}
}
const getSrc = (htmlString: string) => {
const srcRegex = /src="([^"]+)"/;
return htmlString?.match(srcRegex);
};
const isMoreControl = ref(false);
const isPlayed = ref(true);
const isVolume = ref(true);
const speedList = ref<{ [key: number]: string }>({
1: "0.5x",
2: "0.75x",
3: "1.0x",
4: "1.25x",
5: "1.50x",
});
const speedIndexDefault = ref(3);
const speedDefault = ref(speedList.value[speedIndexDefault.value]);
const volume = ref(1.0);
const audioPlayer = ref<HTMLAudioElement | null>(null);
const currentTime = ref(0);
const duration = ref(0);
function setUpVolums() {
isVolume.value = !isVolume.value;
if (audioPlayer.value) {
if (isVolume.value) {
audioPlayer.value.volume = 1;
} else {
audioPlayer.value.volume = 0;
}
}
}
const updateVolume = (num?: number) => {
if (audioPlayer.value) {
if(num) {
volume.value += num
}
// console.log('1231321')
audioPlayer.value.volume = volume.value;
}
};
function chanageSpeed() {
if (speedIndexDefault.value < 5) {
speedIndexDefault.value += 1;
if (audioPlayer.value) {
audioPlayer.value.playbackRate += 0.25;
}
speedDefault.value = speedList.value[speedIndexDefault.value];
} else {
if (audioPlayer.value) {
audioPlayer.value.playbackRate = 0.5;
}
speedIndexDefault.value = 1;
speedDefault.value = speedList.value[1];
}
}
function togglePlayer() {
isPlayed.value = !isPlayed.value;
if (audioPlayer.value) {
if (isPlayed.value) {
audioPlayer.value.pause();
} else {
audioPlayer.value.play();
}
}
}
function replayAndForward(time: number) {
if (audioPlayer.value) {
if (audioPlayer.value.currentTime == audioPlayer.value.duration) {
isPlayed.value = true;
} else {
audioPlayer.value.currentTime = audioPlayer.value.currentTime + time;
}
}
}
const seekToTime = () => {
if (audioPlayer.value) {
audioPlayer.value.currentTime = currentTime.value;
}
};
const updateCurrentTime = () => {
if (audioPlayer.value) {
currentTime.value = audioPlayer.value.currentTime;
}
};
const updateDuration = () => {
if (audioPlayer.value) {
duration.value = audioPlayer.value.duration;
}
};
const currrentTimeComputed = computed(() => {
return utils.formattedTime(currentTime.value);
});
const durationComputed = computed(() => {
return utils.formattedTime(duration.value);
});
</script>
<template>
<div class="lg:p-40px md:p-30px p-5 border-1px border-solid border-black/10 rounded-8px">
<div class="flex md:flex-row flex-col md:gap-6 gap-2 justify-between mb-10px">
<p class="text-#9f9f9f text-14px mb-2 md:hidden block text-center">
{{ utils.dateFormat(currentArticle?.publishedOn, "dddd, DD/MM/YYYY - HH:mm") }}
</p>
<figure class="!w-auto"><img class="w-150px h-150px rounded-8px shadow-md cursor-pointer" :src="currentArticle?.thumbnail" alt="Ảnh podcast" title="Ảnh podcast" /></figure>
<div class="flex-1 text-#222 m-0 md:text-left text-center">
<p class="text-#9f9f9f text-14px mb-2 md:block hidden">
{{ utils.dateFormat(currentArticle?.publishedOn, "dddd, DD/MM/YYYY - HH:mm") }}
</p>
<h1 class="text-24px md:mb-4 mb-2 font-bold" v-html="currentArticle?.title"></h1>
<p class="hidden md:line-clamp-3" v-html="currentArticle?.intro"></p>
</div>
<ul class="items-start gap-2 m-0 p-0 md:flex hidden">
<li class="w-9 h-9 bg-white border-1 border-solid border-[rgb(229, 231, 235)] cursor-pointer shadow-md rounded-50px relative hover:bg-primary-100 hover:text-primary-600">
<Icon class="text-18px absolute top-50% left-50% translate-x--50% translate-y--50%" name="mdi:bookmark-outline" />
</li>
<li class="w-9 h-9 bg-white border-1 border-solid border-[rgb(229, 231, 235)] cursor-pointer shadow-md rounded-50px relative hover:bg-primary-100 hover:text-primary-600">
<Icon class="text-18px absolute top-50% left-50% translate-x--50% translate-y--50%" name="material-symbols:mode-comment-outline" />
</li>
</ul>
</div>
<audio :src="getSrc(currentArticle?.detail)?.[1]" preload="auto" ref="audioPlayer" @timeupdate="updateCurrentTime" @loadedmetadata="updateDuration" />
<div class="p-2">
<input class="w-full accent-primary-600 cursor-pointer" type="range" v-model="currentTime" @input="seekToTime" :max="duration" />
<div class="flex justify-between">
<span>{{ currrentTimeComputed }}</span>
<span>{{ durationComputed }}</span>
</div>
<div class="flex justify-between items-center">
<div class="md:w-150px text-left">
<div class="text-28px text-primary-600 md:hidden block">
<Icon name="material-symbols:skip-previous" />
</div>
<div class="md:inline-flex hidden items-center gap-2 ml--10px h9 text-primary-600 rounded-8px text-28px cursor-pointer hover:bg-primary-100">
<Icon @click="updateVolume(-0.1)" name="material-symbols:volume-mute"></Icon>
<input v-if="isVolume" class="accent-primary-600 h-1 w-12 lg:w-20 cursor-pointer" type="range" v-model="volume" @input="updateVolume()" min="0.1" max="1" step="0.1" />
<Icon @click="updateVolume(0.1)" name="material-symbols:volume-up"></Icon>
</div>
</div>
<div class="flex items-center justify-center gap-4 flex-1 text-28px text-primary-600">
<Icon @click="replayAndForward(-10)" name="fluent:skip-back-10-48-filled" />
<button @click="togglePlayer" class="bg-transparent">
<Icon v-if="isPlayed" name="material-symbols:play-arrow-rounded" class="text-64px" />
<Icon v-if="!isPlayed" name="material-symbols:pause" class="text-64px" />
</button>
<Icon @click="replayAndForward(10)" name="fluent:skip-forward-10-48-filled" />
</div>
<div class="md:w-150px text-right">
<div class="text-28px text-primary-600 md:hidden block">
<Icon name="material-symbols:skip-next" />
</div>
<div class="text-14px text-primary-600 md:block hidden cursor-pointer" @click="chanageSpeed">
<span class="font-300">Tốc độ phát: </span>
<strong class="font-bold text-20px ml-1">{{ speedDefault }}</strong>
</div>
</div>
</div>
</div>
<p class="md:hidden block" v-html="currentArticle?.intro"></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,93 @@
<script setup lang="ts">
const { categoryTree } = storeToRefs(useCategoryStore());
const { currentArticle } = storeToRefs(useArticleStore());
if (categoryTree.value) {
await useCategoryStore().fetchBySiteId();
}
const currentCategoryTree = findElementPathById(categoryTree.value, currentArticle.value.categoryId);
function findElementPathById(categories: any[], targetId: number, path: any[] = []) {
for (const category of categories) {
const currentPath = [...path, { title: category.title, code: category.code }];
if (category.id === targetId) {
return currentPath;
}
if (category.children) {
const result: any = findElementPathById(category.children, targetId, currentPath);
if (result) {
return result;
}
}
}
return null;
}
</script>
<template>
<div class="video-container">
<ul class="flex gap-32px lg:ml-80px md:ml-40px sm:ml-20px">
<li v-for="(category, index) in currentCategoryTree" :key="index" class="first:text-#000 text-#929292 last:after:content-[''] relative after:absolute after:content-['/'] after:text-20px after:right--20px">
<nuxt-link class="font-raleway text-18px font-500 leading-180% uppercase" :to="`/${category.code}`">{{ category.title }}</nuxt-link>
</li>
</ul>
<h2 class="font-gelasio text-center text-44px font-bold leading-130%" v-if="currentArticle?.title" v-html="currentArticle?.title"></h2>
<div class="video-content" v-html="currentArticle.detail"></div>
</div>
</template>
<style scoped lang="scss">
.video-container {
width: 100%;
max-width: 1080px;
margin: auto;
.category-list {
display: flex;
gap: 32px;
margin-bottom: 26px;
list-style: none;
padding: 0;
margin: 0 0 0 80px;
.category-item {
color: #929292;
position: relative;
font-size: 18px;
& > span {
font-size: 18px;
font-weight: 500;
line-height: 180%;
text-transform: uppercase;
}
&::after {
position: absolute;
content: "/";
font-size: 20px;
right: -20px;
}
&:first-child {
color: #000;
}
&:last-child {
&::after {
content: "";
}
}
}
}
.video-content {
width: 100%;
// max-width: 1080px;
margin: 26px 0 26px 0px;
// background-color: #eee;
// height: 500px;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>
@@ -0,0 +1,8 @@
// 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'
export { default as Article_Detail_Emagazine } from './Emagazine.vue'
export { default as Article_Detail_Infographic } from './Infographic.vue'
@@ -0,0 +1,43 @@
<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, Article_Detail_Emagazine, Article_Detail_Infographic } 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,
[enumPageComponentLayouts[enumPageComponentTemplate[enumPageComponentKey.ARTICLE]["ARTICLE_DETAIL"]]["DETAIL_EMAGAZINE"]]: Article_Detail_Emagazine,
[enumPageComponentLayouts[enumPageComponentTemplate[enumPageComponentKey.ARTICLE]["ARTICLE_DETAIL"]]["DETAIL_INFOGRAPHIC"]]: Article_Detail_Infographic,
};
const getCurrentComponent = computed(() => `${_props.settings.layout}`);
const GET_PROPS = computed(() => {
return () => {
let props: any = {};
if (_props.settings) {
for (const [key, value] of Object.entries(_props.settings)) {
props = {
...props,
[key]: value,
};
}
return props;
}
};
});
</script>
<template>
<component
v-if="definedDynamicComponent[getCurrentComponent]"
:is="definedDynamicComponent[getCurrentComponent]"
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings }"
/>
</template>
@@ -0,0 +1,2 @@
export { default as Article_Card } from './cards/index.vue'
export { default as Article_Detail } from './details/index.vue'
@@ -0,0 +1,40 @@
<script lang="ts" setup>
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
import { Article_Card, Article_Detail } from "./index";
const _props = defineProps<{
settings: any;
component?: any;
content?: any;
}>();
const definedDynamicComponent: Record<string, any> = {
[enumPageComponentTemplate[enumPageComponentKey.ARTICLE]["ARTICLE_CARD"]]: Article_Card,
[enumPageComponentTemplate[enumPageComponentKey.ARTICLE]["ARTICLE_DETAIL"]]: Article_Detail,
};
const getCurrentComponent = computed(() => _props.settings.template);
const GET_PROPS = computed(() => {
return () => {
let props: any = {};
if (_props.settings) {
for (const [key, value] of Object.entries(_props.settings)) {
props = {
...props,
[key]: value,
};
}
return props;
}
};
});
</script>
<template>
<component
v-if="definedDynamicComponent[getCurrentComponent]"
:is="definedDynamicComponent[getCurrentComponent]"
v-bind="{ ...GET_PROPS(), component: _props.component, settings: _props.settings }"
/>
</template>
@@ -1,124 +0,0 @@
<script lang="ts" setup>
import { enumPageComponentTemplates } from "@/definitions/enum";
import { DEFAULT_QUERY_DROP, getInputValue } from '@/utils/parseSQL';
const props = defineProps<{
dataResult?: any
dataType?: any
dataQuery?: any
layout?: string
}>()
const LAYOUT_PARSE = computed(() => {
const parseLayout = props.layout?.split('-')?.map((_layout : any) => {
const parseItem = _layout.split(':')
return {
[parseItem[0]]: parseItem[0] === 'HIDE' ? parseItem[1].split(',') : parseItem[1],
};
}) || [];
return Object.assign({}, ...parseLayout);
})
const emit = defineEmits(['selectComponent', 'dropData']);
const selectComponent = () => {
emit('selectComponent');
}
const parseData = computed(() => {
if(!props.dataResult) return
const result = getInputValue(props.dataResult, 'OBJECT');
return result
})
const drop = (e: any) => {
if (e.dataTransfer.getData(`${enumPageComponentTemplates.ARTICLE}`)) {
const data = e.dataTransfer.getData(`${enumPageComponentTemplates.ARTICLE}`);
const { dataType, dataResult } = JSON.parse(data);
const dataQuery = DEFAULT_QUERY_DROP(dataType, dataResult.id);
emit('dropData', {
dataType,
dataResult,
dataQuery: dataQuery,
});
}
}
</script>
<template>
<article class="basic-article gap-x-4" :class="[LAYOUT_PARSE['LAYOUT'] || 'horizontal', !parseData && 'no-data', LAYOUT_PARSE['REVERSE'] ? 'reverse' : '']">
<div v-if="!LAYOUT_PARSE['HIDE'] || !LAYOUT_PARSE['HIDE'].includes('thumbnail')" class="basic-article_thumbnail">
<template v-if="parseData">
<img class="object-fit-cover" :src="parseData.thumbnail ? parseData.thumbnail : '/images/default-thumbnail.jpg'" :alt="parseData.title?.replace(/<[^>]+>/g, '')" />
</template>
</div>
<div class="basic-article_content" :class="[!parseData && 'no-data']">
<div>
<template v-if="parseData">
<nuxt-link :to="`/bai-viet/${parseData.id}`">
<h3 v-if="!LAYOUT_PARSE['HIDE'] || !LAYOUT_PARSE['HIDE'].includes('title')" class="mb-1 line-clamp-2 text-base font-700 hover:text-primary-100 transition-all duration-300">
{{ parseData.title?.replace(/<[^>]+>/g, '') }}
</h3>
</nuxt-link>
</template>
<p v-if="!LAYOUT_PARSE['HIDE'] || !LAYOUT_PARSE['HIDE'].includes('paragraph')" class="mb-0 line-clamp-3 sm:line-clamp-3 text-[14px]">
<template v-if="parseData">
<template v-if="parseData.intro">
{{ parseData.intro?.replace(/<[^>]+>/g, '') }}
</template>
<template v-if="parseData.sub">
{{ parseData.sub?.replace(/<[^>]+>/g, '') }}
</template>
</template>
</p>
</div>
</div>
</article>
</template>
<style lang="scss" scoped>
.basic-article {
display: grid;
height: 100%;
&.vertical {
@apply lg:grid-cols-1 sm:grid-cols-2;
.basic-article_content {
padding: 10px 0px;
}
}
&.horizontal {
grid-template-columns: 1fr 1fr;
.basic-article_content {
padding: 0px 0px;
}
&.reverse {
.basic-article_thumbnail {
grid-column: 2;
}
.basic-article_content {
grid-row: 1;
}
}
}
&_thumbnail {
flex: 1;
img {
width: 100%;
border-radius: 6px;
aspect-ratio: 16/10;
}
}
.empty-block {
background-color: #409eff;
height: 100px;
display: block;
}
}
</style>
@@ -1,38 +0,0 @@
<script setup lang="ts">
import { COLLECTION_QUERY_DROP, getValueStringWithKeyAndColon, getInputValue } from '@/utils/parseSQL';
const _props = defineProps<{
dataResult?: any[];
dataQuery?: string;
}>();
const SETTING_OPTIONS = {
MAX_ELEMENT: 5,
};
const _dataResult = computed(() => {
let _components = Array(SETTING_OPTIONS.MAX_ELEMENT).fill(null);
const result = getInputValue(_props.dataResult, 'ARRAY');
result && result.length > 0 && _components.map((_ : any, index : any) => {
_components[index] = result[index] || null;
})
return _components;
});
</script>
<template>
<div>
<div class="flex gap-4 items-end">
<template v-for="(component, index) in _dataResult">
<nuxt-link v-if="component" :key="index" :to="`/${component.code}`" class=" py-1 font-400 text-[16px] first:font-600 first:text-[20px] sm:block hidden first:block">
<h3 class="m-0 leading-none hover:text-primary-100 transition-all duration-300">{{ component.title }}</h3>
</nuxt-link>
</template>
</div>
</div>
</template>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,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;
component?: 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 :id="`cpn_${_props.component.id}`" 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.code}`">{{ 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,101 @@
<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;
component?: 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 :id="`cpn_${_props.component.id}`" class="categories-container border-custom flex-wrap" :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']" class="whitespace-nowrap">
<nuxt-link :to="`/${component.code}`">{{ component.title }}</nuxt-link>
</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,91 @@
<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;
component?: 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 :id="`cpn_${_props.component.id}`" 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,91 @@
<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;
component?: 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 :id="`cpn_${_props.component.id}`" 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,247 @@
<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 emit = defineEmits(["dropComponent", "dropData", "selectComponent"]);
// const store = reactive({
// page: useCmsPageStore(),
// section: usePageSectionStore(),
// });
// const { currentScreenMode } = storeToRefs(useCmsPageStore());
const _props = defineProps<{
dataResult?: any[];
dataQuery?: string;
layout?: string;
label?: string;
content?: any;
component?: any;
}>();
const SETTING_OPTIONS = {
MAX_ELEMENT: 9,
TEMPLATE: "TYPE:Card",
LAYOUT: "TYPE:Card_VideoHightLight",
};
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;
});
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>
<div
:id="`cpn_${_props.component?.id}`"
class="collection-video-container border-custom"
:class="[LAYOUT_PARSE['div.collection-container_Class'], LAYOUT_PARSE['collection_Class']]"
@click="selectComponent"
:style="LAYOUT_PARSE['div.collection-container']"
>
<div v-for="(component, index) in _dataResult" :key="index">
<div class="wrap">
<!-- {{ index }} -->
<DynamicComponent
:settings="{
template: SETTING_OPTIONS.TEMPLATE,
layout: SETTING_OPTIONS.LAYOUT,
label: mapActivesToItems(Number(index)),
dataResult: !isEmpty(component) ? { ...component } : null,
}"
:component="COMPONENT"
@drop-data="dropData"
/>
</div>
</div>
</div>
<div v-html="LAYOUT_PARSE.styleClasses" style="display: none" v-if="LAYOUT_PARSE.styles"></div>
</template>
<style lang="scss">
.collection-video-container {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-rows: repeat(3, minmax(0, 1fr));
gap: 20px;
& > div {
background-color: #eee;
position: relative;
width: 100%;
padding-top: calc((9 / 16) * 100%);
& > .wrap {
position: absolute;
top: 0;
width: 100%;
height: 100%;
& > .basic-article {
height: 100%;
& > .article_video {
height: 100%;
& > .article_video_thumb {
height: 100%;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
justify-content: flex-end;
overflow: hidden;
& > .article_video_content {
padding: 0 24px 8px 24px;
display: flex;
flex-direction: column;
align-items: center;
& > span {
margin-bottom: 10px;
}
& > .article-title {
text-align: center;
font-size: 18px;
font-weight: 700;
line-height: 130%;
margin: 0;
}
& > .article-intro {
display: none;
}
}
}
& > .empty-box {
width: 100%;
height: 100%;
box-sizing: border-box;
margin: 0px;
& > div {
width: 100%;
height: 100%;
}
}
}
}
}
&:nth-child(1) {
grid-column: span 2 / span 2;
grid-row: span 2 / span 2;
order: 6;
background-color: aqua;
& > .wrap {
& > .basic-article {
& > .article_video {
& > .article_video_thumb {
& > .article_video_content {
padding: 0 120px 24px 120px;
}
}
}
}
}
}
&:nth-child(2) {
order: 2;
background-color: red;
}
&:nth-child(3) {
order: 3;
background-color: green;
}
&:nth-child(4) {
order: 4;
background-color: orange;
}
&:nth-child(5) {
order: 5;
background-color: orangered;
}
&:nth-child(6) {
order: 6;
background-color: brown;
}
&:nth-child(7) {
order: 7;
background-color: blueviolet;
}
&:nth-child(8) {
order: 8;
background-color: darkred;
}
&:nth-child(9) {
order: 9;
background-color: darkcyan;
}
}
// &.column-phone {
// grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
// }
// &.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,3 @@
export { default as Default_Collection } from './Default.vue'
export { default as Audio_Collection } from './Audio.vue'
export { default as Video_Collection } from './Video.vue'
@@ -0,0 +1,39 @@
<script lang="ts" setup>
import { enumPageComponentTemplate, enumPageComponentKey, enumPageComponentLayouts } from "@/definitions/enum";
import { Default_Collection, Audio_Collection, Video_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,
[enumPageComponentLayouts[`${enumPageComponentTemplate[enumPageComponentKey.COLLECTION]["ARTICLE"]}`]["ARTICLE_COLLECTION_VIDEO"]]: Video_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,195 @@
<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 emit = defineEmits(["dropComponent", "dropData", "selectComponent"]);
const _props = defineProps<{
dataResult?: any;
dataQuery?: string;
layout?: string;
label?: string;
content?: any;
component?: 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 :id="`cpn_${_props.component.id}`" 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="box"
: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;
@media (min-width: 640px) and (max-width: 1024px) {
column-count: 2;
-webkit-column-count: 2;
-moz-column-count: 2;
}
@media (max-width: 640px) {
column-count: 1;
-webkit-column-count: 1;
-moz-column-count: 1;
}
.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;
}
& > .box {
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 hồn, vốn 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 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,10 +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 Dynamic_Other } from './other/index.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,26 +1,29 @@
<script lang="ts" setup>
import { enumPageComponentTemplates } from "@/definitions/enum";
import { Article_BasicCard, BasicCategories, Article_BasicCollection, Dynamic_Other } 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.OTHER]: Dynamic_Other
[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,
@@ -30,11 +33,12 @@ const GET_PROPS = computed(() => {
}
};
});
console.log(_props.component, '12')
</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, settings: _props.settings }" />
<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,76 @@
<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">
<nuxt-link :to="`/${item.slug}`"
><h4 class="font-raleway">{{ item.title }}</h4></nuxt-link
>
<div class="ml-2">
<nuxt-link v-for="(_item, _index) in item.childs ? item.childs : []" :key="_index" :to="`/${_item.slug}`"
><h5 class="font-raleway">
{{ _item.title }}
</h5></nuxt-link
>
</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>
@@ -1,17 +0,0 @@
<script setup lang="ts">
</script>
<template>
<div class="content">
<span>Quảng cáo đây</span>
</div>
</template>
<style scoped lang="scss">
.content {
font-size: 18px;
background-color: rgb(245, 245, 245);
height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
@@ -1,105 +0,0 @@
<script setup lang="ts">
import { isEmpty } from 'lodash';
const emit = defineEmits(['dropData', 'selectComponent'])
const _props = defineProps<{
dataResult?: any[]
}>()
const SETTING_OPTIONS = {
MAX_ELEMENT: 3
}
</script>
<template>
<div class="breadcrumb">
<ul class="breadcrumb__list">
<li class="breadcrumb__list__item">
<p class="breadcrumb__list__item__title">
<nuxt-link to="/">Trang chủ</nuxt-link>
</p>
</li>
</ul>
<div class="button">
<div class="button__increase">
<i class="ri-font-size"></i>
<i class="ri-add-line"></i>
</div>
<div class="button__decrease">
<i class="ri-font-size"></i>
<i class="ri-subtract-line"></i>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.breadcrumb {
display: flex;
justify-content: space-between;
&__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;
}
}
}
.button {
display: flex;
gap: 4px;
&__increase, &__decrease {
width: 32px;
height: 32px;
background-color: rgb(243, 244, 246);
border-radius: 50px;
position: relative;
& .ri-font-size {
font-size: 17px;
position: absolute;
top: 50%;
left: 40%;
transform: translateY(-50%) translateX(-45%);
}
& .ri-add-line, & .ri-subtract-line {
position: absolute;
right: 2px;
}
}
}
}
.empty {
border-radius: 6px;
background: #409eff;
width: 40px;
min-height: 20px;
}
</style>
@@ -1,48 +0,0 @@
<template>
<div class="btn-wrap">
<div class="center-y">
<p title="Quay trở lại" class="class-default">
<Icon name="fa6-solid:arrow-left" />
</p>
<button class="defaultClasses">
<Icon name="fa6-regular:bookmark" />
</button>
</div>
<div class="center-y">
<button title="Copy link" class="class-default text-2xl">
<Icon name="bi:link-45deg" />
</button>
</div>
</div>
</template>
<style scoped lang="scss">
.btn-wrap {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
@media (min-width: 768px) {
flex-direction: row;
}
.class-default {
display: grid;
place-items: center;
border-radius: 9999px;
border-width: 1px;
width: 3rem;
height: 3rem;
background-color: #ffffff;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
&:hover {
color: #2563eb;
}
}
}
.center-y {
display: flex;
gap: 1rem;
align-items: center;
}
</style>
@@ -1,41 +0,0 @@
<script setup lang="ts">
import { isEmpty } from 'lodash';
const emit = defineEmits(['dropData', 'selectComponent'])
const _props = defineProps<{
dataResult?: any[]
}>()
</script>
<template>
<div class="content">
<h3 class="title">Tiêu đề bài viết đây</h3>
<span class="intro">intro bài viết đây</span>
<div class="detail">Nội dung bài viết đây <br>
Lorem ipsum dolor sit amet, consectetur adipisci elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum</div>
</div>
</template>
<style scoped lang="scss">
.content {
font-size: 14px;
height: 200px;
padding: 8px;
overflow: hidden;
}
.title {
white-space: normal;
}
.intro {
white-space: normal;
padding-bottom: 10px;
display: block;
}
.detail {
white-space: normal;
}
</style>
@@ -1,16 +0,0 @@
<script setup lang="ts">
import { useArticleStore } from '~/stores/articles';
const emit = defineEmits(['dropData', 'selectComponent'])
const { currentArticle } = storeToRefs(useArticleStore());
</script>
<template>
<div class="content" v-if="currentArticle">
<h1 v-html="currentArticle?.sub" class="text-xl font-bold opacity-60 pb-1"></h1>
<h3 class="text-2xl font-bold sm:text-3xl xl:text-4xl pb-1" v-html="currentArticle?.title"></h3>
<div v-if="currentArticle?.intro" v-html="currentArticle?.intro" class="font-semibold text-xl tracking-widest pb-1"></div>
<div id="article-detail" :class="'text-[15px] tracking-wider'" v-html="currentArticle.detail"
class="[&_img]:mx-auto">
</div>
</div>
</template>
@@ -1,42 +0,0 @@
<script setup lang="ts">
import { isEmpty } from 'lodash';
const emit = defineEmits(['dropData', 'selectComponent'])
const _props = defineProps<{
dataResult?: any[]
}>()
</script>
<template>
<div class="content">
<h3 class="title">Tiêu đề bài viết đây</h3>
<div class="author">Tác giả - Thời gian tạo</div>
<span class="intro">intro bài viết đây</span>
<div class="detail">Nội dung bài viết đây <br>
Lorem ipsum dolor sit amet, consectetur adipisci elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum</div>
</div>
</template>
<style scoped lang="scss">
.content {
font-size: 14px;
height: 200px;
padding: 8px;
overflow: hidden;
}
.title, .author, .intro {
white-space: normal;
}
span {
white-space: normal;
padding-bottom: 10px;
display: block;
}
.detail {
white-space: normal;
}
</style>
@@ -1,5 +0,0 @@
export { default as Article_Button } from './copyLinks/ArticleButton.vue'
export { default as Article_Detail_Emagazine } from './details/emagazine.vue'
export { default as Article_Detail_Default } from './details/default.vue'
export { default as Article_Detail_Infographics } from './details/infographics.vue'
export { default as Default_Breadcrumb} from './breadcrumb/default.vue'
@@ -1,42 +0,0 @@
<script lang="ts" setup>
import { enumPageComponentTemplates } from "@/definitions/enum";
// import { Default_Breadcrumb, Comment, Podcast, Video, Article_Detail_Default, ADS_Default, Article_Button, Article_Detail_Infographics, Article_Detail_Emagazine} from "./index";
import { Article_Button, Article_Detail_Emagazine, Article_Detail_Default, Article_Detail_Infographics, Default_Breadcrumb} from "./index";
const _props = defineProps<{
settings: any;
component?: any;
}>();
const definedDynamicComponent: Record<string, any> = {
'BREADCRUM_DEFAULT': Default_Breadcrumb,
'ARTICLE_DETAIL_DEFAULT': Article_Detail_Default,
'ARTICLE_DETAIL_INFOGRAPHICS': Article_Detail_Infographics,
'ARTICLE_DETAIL_EMAGAZINE': Article_Detail_Emagazine,
// 'ADS_DEFAULT': ADS_Default,
'ARTICLE_BUTTON': Article_Button,
// COMMENT: Comment,
// POCAST: Podcast,
// VIDEO: Video
};
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 :is="definedDynamicComponent[getCurrentComponent]" v-bind="GET_PROPS()" />
</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>

Some files were not shown because too many files have changed in this diff Show More