thainv-dev: sửa lại cấu trúc folder
This commit is contained in:
@@ -17,6 +17,7 @@ const definedDynamicComponent: Record<string, any> = {
|
|||||||
'TYPE:Detail-LAYOUT:image': Article_Detail_Image,
|
'TYPE:Detail-LAYOUT:image': Article_Detail_Image,
|
||||||
'TYPE:Detail-LAYOUT:video': Article_Detail_Video,
|
'TYPE:Detail-LAYOUT:video': Article_Detail_Video,
|
||||||
'TYPE:Detail-LAYOUT:podcast': Article_Detail_Podcast,
|
'TYPE:Detail-LAYOUT:podcast': Article_Detail_Podcast,
|
||||||
|
'TYPE:Card': Article_Card
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentComponent = computed(() => `${_props.settings.layout}`);
|
const getCurrentComponent = computed(() => `${_props.settings.layout}`);
|
||||||
@@ -37,5 +38,5 @@ const GET_PROPS = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<component :is="definedDynamicComponent[getCurrentComponent]" v-bind="GET_PROPS()" />
|
<component :is="definedDynamicComponent[getCurrentComponent]" v-bind="GET_PROPS()" class="h-full"/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,24 +1,94 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||||
|
import { DEFAULT_QUERY_DROP } from "@/utils/parseSQL";
|
||||||
|
import { getInputValue } from "@/utils/parseSQL";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
dataResult?: any;
|
dataResult?: any;
|
||||||
dataType?: any;
|
dataType?: any;
|
||||||
dataQuery?: any;
|
dataQuery?: any;
|
||||||
layout?: string;
|
layout?: string;
|
||||||
design?: string;
|
label?: 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],
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
const designObject = props.label ? getInputValue(props.label, "OBJECT") : {};
|
||||||
|
|
||||||
|
return Object.assign({}, ...parseLayout, 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<article
|
||||||
vào r</div>
|
class="basic-article border-custom"
|
||||||
|
@click="selectComponent"
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.stop.prevent="drop"
|
||||||
|
:class="[LAYOUT_PARSE['LAYOUT'] || 'horizontal', !parseData && 'no-data', LAYOUT_PARSE['REVERSE'] ? 'reverse' : '', ...(LAYOUT_PARSE['border']?.length > 0 ? LAYOUT_PARSE['border'] : [])]"
|
||||||
|
:style="[LAYOUT_PARSE['background'] && `background: ${LAYOUT_PARSE['background']}`]"
|
||||||
|
>
|
||||||
|
<div v-if="!LAYOUT_PARSE['HIDE'] || !LAYOUT_PARSE['HIDE'].includes('thumbnail')" class="basic-article_thumbnail" :style="[LAYOUT_PARSE['LAYOUT'] === 'horizontal' && LAYOUT_PARSE['WidthImg'] && `width: ${LAYOUT_PARSE['WidthImg']}%`]">
|
||||||
|
<template v-if="parseData">
|
||||||
|
<nuxt-link :to="`bai-viet/${parseData?.slug}`">
|
||||||
|
<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 !py-0" :class="[!parseData && 'no-data']">
|
||||||
|
<div>
|
||||||
|
<nuxt-link :to="`bai-viet/${parseData?.slug}`" v-if="!LAYOUT_PARSE['HIDE'] || !LAYOUT_PARSE['HIDE'].includes('title')" class="mb-1 text-truncate-two-lines font-bold line-clamp-2 hover:text-primary-600">
|
||||||
|
<template v-if="parseData">
|
||||||
|
{{ parseData.title?.replace(/<[^>]+>/g, "") }}
|
||||||
|
</template>
|
||||||
|
<span v-else class="empty-block" style="height: 8px"></span>
|
||||||
|
</nuxt-link>
|
||||||
|
<p v-if="!LAYOUT_PARSE['HIDE'] || !LAYOUT_PARSE['HIDE'].includes('paragraph')" class="mb-0 line-clamp-3">
|
||||||
|
<template v-if="parseData">
|
||||||
|
{{ parseData.intro ? parseData.intro?.replace(/<[^>]+>/g, "") : parseData.detail?.replace(/<[^>]+>/g, "") }}
|
||||||
|
</template>
|
||||||
|
<span v-else class="empty-block" style="height: 5px"></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.basic-article {
|
.basic-article {
|
||||||
display: grid;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
@@ -27,44 +97,45 @@ const props = defineProps<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.vertical {
|
&.vertical {
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
flex-direction: column;
|
||||||
|
.basic-article_thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
&.reverse {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&.border-custom {
|
&.border-custom {
|
||||||
border-color: #e5e5e5 !important;
|
border-color: #e5e5e5 !important;
|
||||||
}
|
}
|
||||||
&.borderLeft {
|
&.borderLeft {
|
||||||
border-left: 2px solid;
|
border-left: 1px solid;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
}
|
}
|
||||||
&.borderRight {
|
&.borderRight {
|
||||||
border-right: 2px solid;
|
border-right: 1px solid;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
}
|
}
|
||||||
&.borderTop {
|
&.borderTop {
|
||||||
border-top: 2px solid;
|
border-top: 1px solid;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
&.borderBottom {
|
&.borderBottom {
|
||||||
border-bottom: 2px solid;
|
border-bottom: 1px solid;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
&.horizontal {
|
&.horizontal {
|
||||||
grid-template-columns: 1fr 1fr;
|
flex-direction: row;
|
||||||
|
.basic-article_thumbnail {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
&.reverse {
|
&.reverse {
|
||||||
.basic-article_thumbnail {
|
flex-direction: row-reverse;
|
||||||
grid-column: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.basic-article_content {
|
|
||||||
grid-row: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_thumbnail {
|
&_thumbnail {
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -74,7 +145,7 @@ const props = defineProps<{
|
|||||||
|
|
||||||
&_content {
|
&_content {
|
||||||
padding: 10px 0px;
|
padding: 10px 0px;
|
||||||
|
flex: 1;
|
||||||
&.no-data {
|
&.no-data {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
@@ -89,10 +160,10 @@ const props = defineProps<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-block {
|
// .empty-block {
|
||||||
background-color: #409eff;
|
// background-color: #409eff;
|
||||||
height: 100px;
|
// height: 100px;
|
||||||
display: block;
|
// display: block;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+477
-9
@@ -1,10 +1,478 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
<template>
|
import { useArticleStore } from "~/stores/articles";
|
||||||
<div>
|
import Poll from "~/components/article/immerse/Poll.vue";
|
||||||
postcart</div>
|
import Quiz from "~/components/article/immerse/Quiz.vue";
|
||||||
</template>
|
import Survey from "~/components/article/immerse/Survey.vue";
|
||||||
<style lang="scss" scoped>
|
import Document from "~/components/article/immerse/Document.vue";
|
||||||
div {
|
import Attachment from "@/components/article/immerse/Attachment.vue";
|
||||||
padding: 0;
|
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;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
onMounted(async () => {
|
||||||
|
clickElement("figure", "custom-figure", "data-code");
|
||||||
|
clickElement("author", "author", "data-code");
|
||||||
|
|
||||||
|
let detailEmagazine = document.querySelector('div[layout="ARTICLE_DETAIL_EMAGAZINE"]');
|
||||||
|
let breakcrumb = document.querySelector('div[layout="BREADCRUM_DEFAULT"]');
|
||||||
|
if (detailEmagazine && breakcrumb) {
|
||||||
|
breakcrumb.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
|
||||||
|
}
|
||||||
|
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" 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>
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ const store = reactive({
|
|||||||
article: useArticleStore(),
|
article: useArticleStore(),
|
||||||
category: useCategoryStore(),
|
category: useCategoryStore(),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(currentArticle.value ,'curenta')
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3">
|
<div class="grid grid-cols-1 md:grid-cols-3">
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as Category_Default } from './layouts/Default.vue'
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||||
|
import {
|
||||||
|
Category_Default
|
||||||
|
} from "./index";
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
'TYPE:Category-MAX:5': Category_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 :is="definedDynamicComponent[getCurrentComponent]" v-bind="GET_PROPS()" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<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-[22px] first:border-b first:border-primary-600 sm:block hidden first:block">
|
||||||
|
<h3 class="m-0 leading-none hover:text-primary-600 transition-all duration-300">{{ component.title }}</h3>
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,127 @@
|
|||||||
<script setup lang="ts"></script>
|
<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 emit = defineEmits(["dropComponent", "dropData", "selectComponent"]);
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
dataResult?: any[];
|
||||||
|
dataQuery?: string;
|
||||||
|
layout?: string;
|
||||||
|
label?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const SETTING_OPTIONS = {
|
||||||
|
MAX_ELEMENT: 5,
|
||||||
|
TEMPLATE: "Article",
|
||||||
|
LAYOUT: "TYPE:Card",
|
||||||
|
};
|
||||||
|
|
||||||
|
const layoutParse = computed(() => {
|
||||||
|
const parseLayout = _props.layout?.split("-")?.map((_layout: any) => {
|
||||||
|
const parseItem = _layout.split(":");
|
||||||
|
return {
|
||||||
|
[parseItem[0]]: parseItem[1],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const designObject = _props.label ? getInputValue(_props.label, "OBJECT") : {};
|
||||||
|
return Object.assign({}, ...parseLayout, designObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
const _dataResult = computed(() => {
|
||||||
|
let _components = Array(Number(layoutParse.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");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
collection
|
<div
|
||||||
|
class="collection-container p-2 border-custom"
|
||||||
|
:class="[layoutParse['LAYOUT_WRAP'] || 'vertical', ...(layoutParse['borderWrap']?.length > 0 ? layoutParse['borderWrap'] : [])]"
|
||||||
|
@click="selectComponent"
|
||||||
|
:style="[`grid-template-columns: repeat(${currentScreenMode === 'smartphone' ? 1 : layoutParse['COLUMN']}, minmax(0, 1fr))`, layoutParse['background'] && `background: ${layoutParse['background']}`]"
|
||||||
|
>
|
||||||
|
<DynamicComponent
|
||||||
|
class="h-full"
|
||||||
|
v-for="(component, index) in _dataResult"
|
||||||
|
:key="index"
|
||||||
|
:settings="{
|
||||||
|
template: SETTING_OPTIONS.TEMPLATE,
|
||||||
|
layout: SETTING_OPTIONS.LAYOUT,
|
||||||
|
label,
|
||||||
|
dataResult: !isEmpty(component) ? { ...component } : null,
|
||||||
|
}"
|
||||||
|
@drop-data="dropData"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.collection-container {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
&.border-custom {
|
||||||
|
border-color: #e5e5e5 !important;
|
||||||
|
}
|
||||||
|
&.borderLeft {
|
||||||
|
border-left: 1px solid;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
&.borderRight {
|
||||||
|
border-right: 1px solid;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
&.borderTop {
|
||||||
|
border-top: 1px solid;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
&.borderBottom {
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
&.vertical {
|
||||||
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
&.horizontal {
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
min-height: 100px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #409eff;
|
||||||
|
}
|
||||||
|
&.noData {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ export { default as CollectionPaging } from './pageCategories/collection_page.vu
|
|||||||
|
|
||||||
|
|
||||||
export { default as Dynamic_Other } from './other/index.vue'
|
export { default as Dynamic_Other } from './other/index.vue'
|
||||||
|
export { default as Dynamic_Section } from './sections/index.vue';
|
||||||
export { default as Dynamic_Advertising } from './advertising/index.vue'
|
export { default as Dynamic_Advertising } from './advertising/index.vue'
|
||||||
|
export { default as Dynamic_Category } from './categories/index.vue'
|
||||||
export { default as Dynamic_Article } from './articles/index.vue'
|
export { default as Dynamic_Article } from './articles/index.vue'
|
||||||
export { default as Dynamic_Collection } from './collections/index.vue'
|
export { default as Dynamic_Collection } from './collections/index.vue'
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { enumPageComponentTemplates } from "@/definitions/enum";
|
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||||
import { BasicCategories, Dynamic_Collection, CollectionPaging, Dynamic_Other, Dynamic_Advertising, Dynamic_Article } from "./index";
|
import { Dynamic_Section, Dynamic_Category, Dynamic_Collection, CollectionPaging, Dynamic_Other, Dynamic_Advertising, Dynamic_Article } from "./index";
|
||||||
const _props = defineProps<{
|
const _props = defineProps<{
|
||||||
settings: any;
|
settings: any;
|
||||||
component?: any;
|
component?: any;
|
||||||
@@ -8,9 +8,9 @@ const _props = defineProps<{
|
|||||||
|
|
||||||
const definedDynamicComponent: Record<string, any> = {
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
[enumPageComponentTemplates.ARTICLE]: Dynamic_Article,
|
[enumPageComponentTemplates.ARTICLE]: Dynamic_Article,
|
||||||
[enumPageComponentTemplates.CATEGORY]: BasicCategories,
|
[enumPageComponentTemplates.CATEGORY]: Dynamic_Category,
|
||||||
[enumPageComponentTemplates.COLLECTION]: Dynamic_Collection,
|
[enumPageComponentTemplates.COLLECTION]: Dynamic_Collection,
|
||||||
[enumPageComponentTemplates.SECTION]: CollectionPaging,
|
[enumPageComponentTemplates.SECTION]: Dynamic_Section,
|
||||||
[enumPageComponentTemplates.OTHER]: Dynamic_Other,
|
[enumPageComponentTemplates.OTHER]: Dynamic_Other,
|
||||||
[enumPageComponentTemplates.ADVERTISING]: Dynamic_Advertising
|
[enumPageComponentTemplates.ADVERTISING]: Dynamic_Advertising
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as Article_Pagination } from './layouts/Article.vue'
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { enumPageComponentTemplates, enumPageComponentLayouts } from "@/definitions/enum";
|
||||||
|
import { Article_Pagination } from "./index";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
settings: any;
|
||||||
|
component?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const definedDynamicComponent: Record<string, any> = {
|
||||||
|
'TYPE:Article-LAYOUT:horizontal-DATA:HORIZONTAL': Article_Pagination
|
||||||
|
};
|
||||||
|
console.log('đã vào')
|
||||||
|
const getCurrentComponent = computed(() => `${_props.settings.layout}`);
|
||||||
|
console.log(getCurrentComponent.value, 'getcomponent')
|
||||||
|
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(), component: _props.component, settings: _props.settings }" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { isEmpty } from "lodash";
|
||||||
|
import DynamicComponent from "~/components/dynamic-page/page-component/templates/index.vue";
|
||||||
|
import { COLLECTION_PAGING_QUERY_DROP, getInputValue } from "@/utils/parseSQL";
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const emit = defineEmits(["dropData", "selectComponent"]);
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
dataResult?: any[];
|
||||||
|
dataQuery?: string;
|
||||||
|
component?: any;
|
||||||
|
label?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const SETTING_OPTIONS = {
|
||||||
|
MAX_ELEMENT: 5,
|
||||||
|
TEMPLATE: "Article",
|
||||||
|
LAYOUT: "TYPE:Card",
|
||||||
|
};
|
||||||
|
|
||||||
|
// const page = ref(1);
|
||||||
|
const limit = ref(1);
|
||||||
|
const totals = ref(1);
|
||||||
|
const type = "Article";
|
||||||
|
|
||||||
|
const listArticleByCategory = computed(() => {
|
||||||
|
return getInputValue(_props.dataResult, "ARRAY");
|
||||||
|
});
|
||||||
|
|
||||||
|
const designObject = computed(() => {
|
||||||
|
return _props.label ? getInputValue(_props.label, "OBJECT") : {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const dropData = (event: any) => {
|
||||||
|
const queryBy = {
|
||||||
|
Category: "Categories",
|
||||||
|
};
|
||||||
|
const { dataResult, dataType } = JSON.parse(event.dataTransfer.getData("category"));
|
||||||
|
// const getDataQuery = `Get[${type}] Top[20] With[${queryBy[dataType]}:${dataResult.id}]`;
|
||||||
|
const getDataQuery = COLLECTION_PAGING_QUERY_DROP(type, { key: queryBy[dataType], value: dataResult.id });
|
||||||
|
emit("dropData", {
|
||||||
|
dataResult: [],
|
||||||
|
dataType,
|
||||||
|
dataQuery: getDataQuery,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//?cpn_1=page:2&cpn_2=page:1
|
||||||
|
// Get[Article] Top[5] With[Categories:1]
|
||||||
|
const select = (page: number) => {
|
||||||
|
const componentId = _props.component?.id;
|
||||||
|
if (componentId) {
|
||||||
|
router.push({
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
[`cpn_${componentId}`]: `page:${page}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleRouteChange = (query: any) => {
|
||||||
|
const [_, value] = query[`cpn_${_props.component?.id}`]?.split(":");
|
||||||
|
if (value) {
|
||||||
|
loadPage(Number(value));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
if (route.query[`cpn_${_props.component?.id}`]) handleRouteChange(route.query);
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadPage = (page: string | number) => {
|
||||||
|
console.log(`Loading page ${page}`);
|
||||||
|
// listArticleByCategory.value =
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectComponent = () => {
|
||||||
|
emit("selectComponent");
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query,
|
||||||
|
(newQuery) => {
|
||||||
|
handleRouteChange(newQuery);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<div class="section-container" @click="selectComponent" @dragover.prevent @drop.stop.prevent="dropData($event)" :class="[listArticleByCategory && listArticleByCategory?.length > 0 ? '' : 'noData']">
|
||||||
|
<div
|
||||||
|
class="collection-container"
|
||||||
|
:class="[designObject['LAYOUT_WRAP'] || 'vertical', ...(designObject['borderWrap']?.length > 0 ? designObject['borderWrap'] : [])]"
|
||||||
|
:style="[designObject['background'] && `background: ${designObject['background']}`]"
|
||||||
|
>
|
||||||
|
<template v-if="listArticleByCategory?.length > 0">
|
||||||
|
<template v-for="(component, index) in listArticleByCategory">
|
||||||
|
<DynamicComponent
|
||||||
|
:key="index"
|
||||||
|
v-if="!isEmpty(component)"
|
||||||
|
:settings="{
|
||||||
|
template: SETTING_OPTIONS.TEMPLATE,
|
||||||
|
layout: SETTING_OPTIONS.LAYOUT,
|
||||||
|
dataResult: { ...component },
|
||||||
|
label,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="empty"><h6 class="px-2 text-center">Nội dung danh sách bài viết của danh mục sẽ ở đây</h6></div>
|
||||||
|
</template>
|
||||||
|
<div class="button-page flex">
|
||||||
|
<a class="btn-page prev-page cursor-pointer" >
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" class="w-7 h-6 text-primary-600">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M609.408 149.376 277.76 489.6a32 32 0 0 0 0 44.672l331.648 340.352a29.12 29.12 0 0 0 41.728 0 30.592 30.592 0 0 0 0-42.752L339.264 511.936l311.872-319.872a30.592 30.592 0 0 0 0-42.688 29.12 29.12 0 0 0-41.728 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a class="btn-page" @click="() => select(index + 1)" v-for="(_, index) in totals">{{ index + 1 }}</a>
|
||||||
|
<a class="btn-page next-page cursor-pointer">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" class="w-7 h-6 text-primary-600">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M340.864 149.312a30.592 30.592 0 0 0 0 42.752L652.736 512 340.864 831.872a30.592 30.592 0 0 0 0 42.752 29.12 29.12 0 0 0 41.728 0L714.24 534.336a32 32 0 0 0 0-44.672L382.592 149.376a29.12 29.12 0 0 0-41.728 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.section-container {
|
||||||
|
.collection-container {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
&.border-custom {
|
||||||
|
border-color: #e5e5e5 !important;
|
||||||
|
}
|
||||||
|
&.borderLeft {
|
||||||
|
border-left: 2px solid;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
&.borderRight {
|
||||||
|
border-right: 2px solid;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
&.borderTop {
|
||||||
|
border-top: 2px solid;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
&.borderBottom {
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
&.vertical {
|
||||||
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
&.horizontal {
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 50px;
|
||||||
|
background-color: #409eff;
|
||||||
|
display: flex;
|
||||||
|
white-space: normal;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
h6 {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.basic-article {
|
||||||
|
&.article {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.noData {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-page {
|
||||||
|
-webkit-box-align: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-page {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 36px;
|
||||||
|
border: 1px solid #409eff;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-left: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-empty {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -147,15 +147,24 @@ const CLASS_FOR_SECTION = computed(() => {
|
|||||||
:key="index"
|
:key="index"
|
||||||
:class="[CLASS_FOR_SECTION[index]]"
|
:class="[CLASS_FOR_SECTION[index]]"
|
||||||
>
|
>
|
||||||
<RecusiveSection :type="position.type" :id="position.data" :section="props.section" />
|
|
||||||
|
<RecusiveSection :type="position.type" :id="position.data" :section="props.section" class="h-full"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss">
|
||||||
.section_layout {
|
.section_layout {
|
||||||
&.basic_column {
|
&.basic_column {
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
|
||||||
|
// & > div:first-child {
|
||||||
|
// .basic-article {
|
||||||
|
// h3 {
|
||||||
|
// @apply text-24px;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
&.two_col_layout {
|
&.two_col_layout {
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ const definedDynamicSection: Record<string, any> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentSection = computed(() => _props?.layout || "");
|
const getCurrentSection = computed(() => _props?.layout || "");
|
||||||
|
|
||||||
const GET_PROPS = computed(() => {
|
const GET_PROPS = computed(() => {
|
||||||
return () => {
|
return () => {
|
||||||
let props: any = {};
|
let props: any = {};
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ watch(currentPage, () => {
|
|||||||
useHead({
|
useHead({
|
||||||
title: () => currentPage.value.title || ''
|
title: () => currentPage.value.title || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ watch(currentArticle, async () => {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
isContentType = 'trang-chi-tiet-bai-viet-postcart'
|
isContentType = 'trang-chi-tiet-podcast'
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ const store = reactive({
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
watch(currentPage, () => {
|
watch(currentPage, () => {
|
||||||
console.log(currentPage.value)
|
|
||||||
store.dynamicPage.setSectionPublished();
|
store.dynamicPage.setSectionPublished();
|
||||||
store.dynamicPage.setComponentPublished()
|
store.dynamicPage.setComponentPublished()
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ export type CategoryTree = Category & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const list = async () => {
|
export const list = async () => {
|
||||||
console.log('vào category service')
|
|
||||||
try {
|
try {
|
||||||
const { site, apiUrl } = useRuntimeConfig().public;
|
const { site, apiUrl } = useRuntimeConfig().public;
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const create = async (event: H3Event) => {
|
|||||||
},
|
},
|
||||||
body: payload
|
body: payload
|
||||||
})
|
})
|
||||||
console.log(payload, 'payload')
|
|
||||||
return item
|
return item
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export const useArticleStore = defineStore("article", () => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: condition
|
body: condition
|
||||||
})
|
})
|
||||||
console.log(articles, 'data')
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,6 +232,8 @@ const getInputValue = (inputValue: any, typeGet: 'OBJECT' | 'ARRAY') => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
parseDataQueryFormString,
|
parseDataQueryFormString,
|
||||||
parseDataQueryFormObject,
|
parseDataQueryFormObject,
|
||||||
|
|||||||
Reference in New Issue
Block a user