init
This commit is contained in:
+86
@@ -0,0 +1,86 @@
|
||||
<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 p-2" :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 {
|
||||
display: grid;
|
||||
gap: 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>
|
||||
@@ -0,0 +1,136 @@
|
||||
<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" :class="[LAYOUT_PARSE['LAYOUT'] || 'horizontal', !parseData && 'no-data', LAYOUT_PARSE['REVERSE'] ? 'reverse' : '']" @click="selectComponent" @dragover.prevent @drop.stop.prevent="drop">
|
||||
<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>
|
||||
<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']">
|
||||
<div>
|
||||
<h3 v-if="!LAYOUT_PARSE['HIDE'] || !LAYOUT_PARSE['HIDE'].includes('title')" class="mb-1 text-truncate-two-lines">
|
||||
<template v-if="parseData">
|
||||
{{ parseData.title?.replace(/<[^>]+>/g, '') }}
|
||||
</template>
|
||||
<span v-else class="empty-block" style="height: 8px;"></span>
|
||||
</h3>
|
||||
<p v-if="!LAYOUT_PARSE['HIDE'] || !LAYOUT_PARSE['HIDE'].includes('paragraph')" class="mb-0 text-truncate-two-lines">
|
||||
<template v-if="parseData">
|
||||
{{ parseData.intro?.replace(/<[^>]+>/g, '') }}
|
||||
</template>
|
||||
<span v-else class="empty-block" style="height: 5px;"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.basic-article {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
|
||||
&.no-data {
|
||||
gap: 5px !important;
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
|
||||
&_content {
|
||||
padding: 10px 0px;
|
||||
|
||||
&.no-data {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-block {
|
||||
background-color: #409eff;
|
||||
height: 100px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { isEmpty } from "lodash";
|
||||
import { COLLECTION_QUERY_DROP, getValueStringWithKeyAndColon, getInputValue } from '@/utils/parseSQL';
|
||||
|
||||
const emit = defineEmits(["dropData", "selectComponent"]);
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
async function dropData(event: any) {
|
||||
const { dataResult, dataType } = JSON.parse(event.dataTransfer.getData("category"));
|
||||
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>
|
||||
<div>
|
||||
<div class="categories-container p-2" @click="selectComponent">
|
||||
<div
|
||||
v-for="(component, index) in _dataResult"
|
||||
:key="index"
|
||||
:class="isEmpty(component) ? 'empty' : 'category'"
|
||||
>
|
||||
<template v-if="!isEmpty(component)">
|
||||
<h3>{{ component.title }}</h3>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
@dragover.prevent
|
||||
@drop.stop.prevent="dropData($event)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.categories-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
.category {
|
||||
height: 100%;
|
||||
h3 {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
margin: 0px !important;
|
||||
}
|
||||
&:first-child {
|
||||
h3 {
|
||||
font-weight: 600;
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.empty {
|
||||
border-radius: 6px;
|
||||
background: #409eff;
|
||||
width: 50px;
|
||||
> div {
|
||||
min-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,156 @@
|
||||
<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>
|
||||
@@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
import AudioPlayer from './AudioPlayer.vue'
|
||||
</script>
|
||||
<template>
|
||||
<div class="banner">
|
||||
<div class="banner__background" style="background-image: url('https://acp-api.vpress.vn/Resources/%E1%BA%A2nh/0bf02739-de1e-4899-9a2e-287c5d949250.jpg')">
|
||||
<div class="banner__overlay"></div>
|
||||
<Wrap class="banner__content">
|
||||
<div class="banner__inner">
|
||||
<div class="article">
|
||||
<div class="article__image-container">
|
||||
<div class="article__image-wrapper" style="background-image: url('https://acp-api.vpress.vn/Resources/%E1%BA%A2nh/0bf02739-de1e-4899-9a2e-287c5d949250.jpg')">
|
||||
<img src="https://acp-api.vpress.vn/Resources/%E1%BA%A2nh/0bf02739-de1e-4899-9a2e-287c5d949250.jpg" alt="" class="article__image" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="article__content">
|
||||
<div class="article__header">
|
||||
<div class="article__header-text">
|
||||
<h1 class="article__title">Podcast Truyện ngắn: Như cơi đựng trầu</h1>
|
||||
<time class="article__date">T2, 29 Th01 2024 16:57</time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="article__intro">
|
||||
<div class="article__intro-text">Tình cảm vợ chồng êm ấm 12 năm, tối nay được định đoạt bằng tờ giấy vô hồn, vốn là người dễ xúc động nên trong lúc viết, Ngân Thương để mấy giọt nước mắt rơi xuống làm đôi chỗ bị nhòe đi.</div>
|
||||
</div>
|
||||
<div class="article__audio">
|
||||
<AudioPlayer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Wrap>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.banner {
|
||||
&__background {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
background-size: cover;
|
||||
@media (min-width: 768px) {
|
||||
height: 25rem;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
&__overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: black;
|
||||
opacity: 0.8;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__inner {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
.article {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, 1fr);
|
||||
width: 100%;
|
||||
|
||||
&__image-container {
|
||||
grid-column: span 3;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 15rem;
|
||||
min-width: 100px;
|
||||
@media (min-width: 768px) {
|
||||
height: 20rem;
|
||||
margin: 0 2rem;
|
||||
}
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
&__image-wrapper {
|
||||
height: 10rem;
|
||||
@media (min-width: 768px) {
|
||||
height: 15rem;
|
||||
}
|
||||
width: 100%;
|
||||
border-radius: 1.5rem 0 0 1.5rem;
|
||||
padding: 0.5rem;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
z-index: 1;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #000;
|
||||
opacity: 0.3;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
&__image {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
height: 10rem;
|
||||
@media (min-width: 768px) {
|
||||
height: 15rem;
|
||||
}
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&__content {
|
||||
grid-column: span 7;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__header {
|
||||
grid-column: span 12;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
margin-top: 2rem;
|
||||
|
||||
&-text {
|
||||
grid-column: span 11;
|
||||
}
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 19px;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-family: "SFD";
|
||||
}
|
||||
|
||||
&__date {
|
||||
margin-top: 0.125rem;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&__intro {
|
||||
grid-column: span 12;
|
||||
margin-bottom: 1rem;
|
||||
display: none;
|
||||
@media (min-width: 768px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&__intro-text {
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
font-family: "SFD";
|
||||
}
|
||||
|
||||
&__audio {
|
||||
grid-column: span 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<article class="article">
|
||||
<div id="article-detail" class="article__detail">
|
||||
<div>
|
||||
<video controls="controls" width="100%" height="auto" data-file-id="149" data-resource="https://acp-api.vpress.vn/Resources/Video/983d2f57-7743-472f-b22d-fc73085af6d5.mp4" data-title="Download.mp4">
|
||||
<source src="https://acp-api.vpress.vn/Resources/Video/983d2f57-7743-472f-b22d-fc73085af6d5.mp4" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
<div class="article__sidebar">
|
||||
<div class="article__sidebar-content">
|
||||
<h1 class="article__title">Tranh cãi chuyện 'quán không nhận chuyển khoản'</h1>
|
||||
<div class="article__author-info">
|
||||
<div class="article__author">
|
||||
<p class="article__author-name">Thanh Huệ</p>
|
||||
</div>
|
||||
<span class="article__separator">-</span>
|
||||
<p class="article__date">T4, 15 Th05 2024 10:55</p>
|
||||
</div>
|
||||
<div id="article-brief" class="article__brief">
|
||||
<div class="article__intro-text">Những ngày cận Tết tại Hà Nội, các hội thi hoa đào, quất cảnh với đa dạng các sản phẩm độc đáo, bắt mắt đ đuợc các nghệ nhân đem đến cho khách tham quan chiêm ngưỡng.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.article {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
/* flex-direction: column; */
|
||||
gap: 1rem; // Equivalent to gap-4
|
||||
margin-top: 1rem; // Equivalent to mt-4
|
||||
background-color: #f7f7f7;
|
||||
|
||||
&__detail {
|
||||
flex: 1;
|
||||
|
||||
iframe,
|
||||
video {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
&.iframe {
|
||||
max-height: 13rem; // Equivalent to max-h-52
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__sidebar {
|
||||
width: 50%;
|
||||
&-content {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 17px; // Equivalent to text-2xl
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__author-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
video {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +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'
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts" setup>
|
||||
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||
import { Article_BasicCard, BasicCategories, Article_BasicCollection } from "./index";
|
||||
|
||||
const _props = defineProps<{
|
||||
settings: any;
|
||||
component?: any;
|
||||
}>();
|
||||
|
||||
const definedDynamicComponent: Record<string, any> = {
|
||||
[enumPageComponentTemplates.ARTICLE]: Article_BasicCard,
|
||||
[enumPageComponentTemplates.CATEGORY]: BasicCategories,
|
||||
[enumPageComponentTemplates.COLLECTION]: Article_BasicCollection,
|
||||
};
|
||||
|
||||
const getCurrentComponent = computed(() => `${_props.settings.template}`);
|
||||
|
||||
const GET_PROPS = computed(() => {
|
||||
return () => {
|
||||
let props: any = {};
|
||||
if (_props.settings) {
|
||||
for (const [key, value] of _props.settings ? 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 }" /> -->
|
||||
<component :is="definedDynamicComponent[getCurrentComponent]" v-bind="{ ...(GET_PROPS()), component: _props.component }" />
|
||||
</template>
|
||||
@@ -0,0 +1,194 @@
|
||||
<script setup lang="ts">
|
||||
import { isEmpty } from "lodash";
|
||||
import DynamicComponent from "~/components/dynamic-page/page-component/templates/index.vue";
|
||||
import { COLLECTION_PAGING_QUERY_DROP } from '@/utils/parseSQL';
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const emit = defineEmits(["dropData", "selectComponent"]);
|
||||
|
||||
const _props = defineProps<{
|
||||
dataResult?: any[];
|
||||
dataQuery?: string;
|
||||
component?: any;
|
||||
}>();
|
||||
|
||||
const SETTING_OPTIONS = {
|
||||
MAX_ELEMENT: 5,
|
||||
TEMPLATE: "Article",
|
||||
LAYOUT: "LAYOUT:horizontal",
|
||||
};
|
||||
|
||||
// const page = ref(1);
|
||||
const limit = ref(1);
|
||||
const totals = ref(2);
|
||||
const category = ref(0);
|
||||
const listArticleByCategory = ref([]);
|
||||
const type = "Article";
|
||||
|
||||
// watch(
|
||||
// () => _props.dataResult,
|
||||
// (newValue) => {
|
||||
// const result = getInputValue(newValue, "ARRAY");
|
||||
// listArticleByCategory.value = result;
|
||||
// }
|
||||
// );
|
||||
|
||||
|
||||
const dropData = (event: any) => {
|
||||
const queryBy = {
|
||||
Category: "Categories",
|
||||
};
|
||||
const { dataResult, dataType } = JSON.parse(event.dataTransfer.getData("category"));
|
||||
// const getDataQuery = `Get[${type}] Top[20] With[${queryBy[dataType]}:${dataResult.id}]`;
|
||||
const getDataQuery = COLLECTION_PAGING_QUERY_DROP(type, { key: queryBy[dataType], value: dataResult.id })
|
||||
category.value = dataResult.id;
|
||||
emit("dropData", {
|
||||
dataResult: [],
|
||||
dataType,
|
||||
dataQuery: getDataQuery,
|
||||
});
|
||||
};
|
||||
|
||||
//?cpn_1=page:2&cpn_2=page:1
|
||||
// Get[Article] Top[5] With[Categories:1]
|
||||
const select = (page: number) => {
|
||||
const componentId = _props.component?.id;
|
||||
if (componentId) {
|
||||
router.push({
|
||||
query: {
|
||||
...route.query,
|
||||
[`cpn_${componentId}`]: `page:${page}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleRouteChange = (query: any) => {
|
||||
const [_, value] = query[`cpn_${_props.component?.id}`]?.split(":");
|
||||
if (value) {
|
||||
loadPage(Number(value));
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeMount(()=>{
|
||||
const result = getInputValue( _props.dataResult, "ARRAY");
|
||||
listArticleByCategory.value = result;
|
||||
handleRouteChange(route.query)
|
||||
})
|
||||
|
||||
const loadPage = (page: string | number) => {
|
||||
console.log(`Loading page ${page}`);
|
||||
// listArticleByCategory.value =
|
||||
};
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
(newQuery) => {
|
||||
handleRouteChange(newQuery);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<div class="section-container" @dragover.prevent @drop.stop.prevent="dropData($event)"
|
||||
:class="[listArticleByCategory && listArticleByCategory?.length > 0 ? '' : 'noData']">
|
||||
<div class="collection-container">
|
||||
<template v-if="category">
|
||||
<template v-if="listArticleByCategory?.length > 0">
|
||||
<template v-for="(component, index) in listArticleByCategory" :key="index">
|
||||
<DynamicComponent
|
||||
v-if="!isEmpty(component)"
|
||||
:settings="{
|
||||
template: SETTING_OPTIONS.TEMPLATE,
|
||||
layout: SETTING_OPTIONS.LAYOUT,
|
||||
dataResult: { ...component },
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-result icon="success" title="Success" sub-title="Nội dung danh sách bài viết sẽ ở đây"> </el-result>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else><el-empty image-size="90px" description="Kéo Category vào đây" /></template>
|
||||
<div class="button-page flex">
|
||||
<a class="btn-page prev-page">
|
||||
<i class="el-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M609.408 149.376 277.76 489.6a32 32 0 0 0 0 44.672l331.648 340.352a29.12 29.12 0 0 0 41.728 0 30.592 30.592 0 0 0 0-42.752L339.264 511.936l311.872-319.872a30.592 30.592 0 0 0 0-42.688 29.12 29.12 0 0 0-41.728 0z"
|
||||
></path>
|
||||
</svg>
|
||||
</i>
|
||||
</a>
|
||||
<a class="btn-page" @click="() => select(index + 1)" v-for="(_, index) in totals">{{ index + 1 }}</a>
|
||||
<a class="btn-page next-page">
|
||||
<i class="el-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M340.864 149.312a30.592 30.592 0 0 0 0 42.752L652.736 512 340.864 831.872a30.592 30.592 0 0 0 0 42.752 29.12 29.12 0 0 0 41.728 0L714.24 534.336a32 32 0 0 0 0-44.672L382.592 149.376a29.12 29.12 0 0 0-41.728 0z"
|
||||
></path>
|
||||
</svg>
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.section-container {
|
||||
.empty {
|
||||
min-height: 100px;
|
||||
border-radius: 6px;
|
||||
background: #409eff;
|
||||
}
|
||||
.collection-container {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
.basic-article {
|
||||
&.article {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
&.noData {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
justify-content: center;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.button-page {
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-page {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
line-height: 36px;
|
||||
border: 1px solid #409eff;
|
||||
border-radius: 3px;
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.el-empty {
|
||||
padding: 12px 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user