Merge branch 'main' of http://work.gct.com.vn/minhnt/NSG_PORTAL_V2 into thainv-dev

This commit is contained in:
nguyen van thai
2024-06-03 12:30:08 +07:00
36 changed files with 893 additions and 604 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
+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-100">
<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>
@@ -1,7 +1,7 @@
<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";
import { getInputValue } from '@/utils/parseSQL';
import isEmpty from "lodash/isEmpty";
const _props = defineProps<{
dataResult?: any[];
@@ -37,7 +37,8 @@ const _dataResult = computed(() => {
<template>
<div>
<div class="collection-container grid gap-5" :class="LAYOUT_PARSE['LAYOUT'] || 'horizontal'">
<div class="collection-container grid gap-5" :class="LAYOUT_PARSE['LAYOUT'] || 'horizontal'"
:style="`grid-template-columns: repeat(${LAYOUT_PARSE['COLUMN']}, minmax(0, 1fr))`">
<div v-for="(component, index) in _dataResult" :key="index">
<template v-if="!isEmpty(component)">
<DynamicComponent
@@ -46,16 +47,6 @@ const _dataResult = computed(() => {
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>
@@ -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,5 +1,5 @@
<script setup lang="ts">
import { isEmpty } from 'lodash';
import isEmpty from "lodash/isEmpty";
const emit = defineEmits(['dropData', 'selectComponent'])
const _props = defineProps<{
@@ -1,91 +1,101 @@
<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 outline-0 outline-none outline-offset-0" 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 class="mt-4">
<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>
</template>
<style scoped lang="scss">
<div class="mb-2">
<button type="button" class="mr-2 p-2 bg-blue-500 text-[#fff] rounded text-xs">
Gửi bình luận
<Icon name="ri:send-plane-2-fill"></Icon>
</button>
</div>
.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;
}
</div>
</template>
<style scoped lang="scss">
.box-area-input {
/* background: #f3f6f9; */
border-radius: 4px;
position: relative;
padding: 5px 10px;
border-left: 2px solid rgba(59, 130, 246, 1);
}
.input_comment {
padding: 0;
margin-top: 10px;
background: #f5f5f5;
position: relative;
width: 100%;
border-top: 1px solid #dedede;
border-right: 1px solid #dedede;
border-bottom: 1px solid #dedede;
border-radius: 5px;
}
.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;
} */
.box-area-input {
background: #f7f7f7;
border-radius: 4px;
position: relative;
padding: 10px 0 10px 0;
border-left: 2px solid rgba(59, 130, 246, 1);
}
.input_comment textarea {
background: #fff;
border: none;
width: 100%;
height: 58px;
overflow: hidden;
}
</style>
.input_comment textarea.block_input {
height: 30px;
-webkit-transition-duration: 200ms;
transition-duration: 200ms;
-webkit-transition-property: all;
transition-property: all;
-webkit-transition-timing-function: cubic-bezier(0.7, 1, 0.7, 1);
transition-timing-function: cubic-bezier(0.7, 1, 0.7, 1);
}
.input_comment textarea.block_input {
height: 76px;
overflow: hidden;
resize: none;
}
.box-area-input .block_input {
background: #f7f7f7;
}
.input_comment textarea {
font: 400 16px/150% arial;
background: #fff;
border: none;
width: 100%;
height: 58px;
color: #4f4f4f !important;
overflow: hidden;
padding: 5px 37px 0 15px;
}
input[type="text"],
input[type="password"],
input[type="email"],
input[type="tel"],
textarea,
select {
background: #fff;
width: 100%;
height: 30px;
line-height: 30px;
border: 1px solid #ccc;
font-size: 14px;
margin: 3px 0;
padding: 0 5px;
outline: none;
}
input,
textarea {
font-family: arial;
font-size: 11px;
border: none;
background: none;
}
</style>
@@ -1,8 +1,6 @@
<script setup lang="ts">
import { useArticleStore } from '~/stores/articles';
const emit = defineEmits(['dropData', 'selectComponent'])
const { currentArticle } = storeToRefs(useArticleStore());
console.log(currentArticle.value ,'12')
</script>
<template>
<div class="content" v-if="currentArticle">
@@ -1,6 +1,5 @@
<script setup lang="ts">
import { useArticleStore } from '~/stores/articles';
const emit = defineEmits(['dropData', 'selectComponent'])
const { currentArticle } = storeToRefs(useArticleStore());
</script>
@@ -0,0 +1,149 @@
<script setup lang="ts">
import AudioPlayer from "~/organisms/audioPlayer/AudioPlayer.vue";
const { currentArticle } = storeToRefs(useArticleStore());
import Topic from "@/components/article/Topic.vue";
import Event from "@/components/article/Event.vue";
import Tag from "@/components/article/Tag.vue";
const getSrc = (htmlString: string) => {
const srcRegex = /src="([^"]+)"/;
return htmlString?.match(srcRegex);
};
// const getArticleById = async (articleId: number) => {
// try {
// const { apiUrl } = useRuntimeConfig().public;
// const { item }: any = await $fetch(`${apiUrl}/cms/digital-article/${articleId}`, {
// headers: {
// Site: "1",
// },
// });
// return item;
// } catch (error) {
// handleError(error);
// }
// };
const store = reactive({
tag: useTagStore(),
topic: useTopicStore(),
event: useEventStore()
});
// const listTag = ref([]);
// const listTopic = ref([]);
// const listEvent = ref([])
// const getTagsAndTopicsAndEvents = async () => {
// if (!currentArticle) return;
// const fetchData = async (ids, fetchFn, list) => {
// if (!ids) return;
// const data = await Promise.all(ids.split(",").map(fetchFn));
// if (data.length > 0) list.value = data;
// };
// await Promise.all([
// fetchData(currentArticle.tagIds, store.tag.fetchById, listTag),
// fetchData(currentArticle.topicIds, store.topic.fetchById, listTopic),
// fetchData(currentArticle.eventIds, store.event.fetchById, listEvent)
// ]);
// };
// getTagsAndTopicsAndEvents();
const listArticle = ref([]);
const audioPlay = ref({});
const defaultClass = {
article: ["group", "max-w-full", "grid", "gap-3", "overflow-hidden", "p-4"],
thumbnail: ["rounded-3xl", "shadow-md", "max-w-full", "w-full", "aspect-5/3", "group-hover:scale-[1.05]", "duration-500", "ease-in-out", "object-cover"],
title: ["font-bold", "px-4", "md:px-0", "xl:text-xl", "text-base"],
brief: ["text-sm", "sm:text-base", "mx-4", "pb-4", "md:pb-0", "md:mx-0", "border-b", "border-stone-400", "md:border-none"],
};
// const getListArticle = async () => {
// if (currentArticle && currentArticle.audioIds) {
// const audioIds = currentArticle.audioIds.split(",").map(Number);
// const articles = await Promise.all(audioIds.map(async (audioId) => await getArticleById(audioId)));
// if (articles.length > 0) {
// listArticle.value = articles;
// audioPlay.value = articles[0];
// }
// }
// // const test = "8,9";
// };
// getListArticle();
</script>
<template>
<div class="w-full grid grid-cols-12 gap-4" v-if="currentArticle">
<div class="col-span-12 h-60 md:h-100 relative bg-center" :style="'background-image: url(' + currentArticle?.thumbnail + '); background-size: cover;'">
<div class="absolute inset-0 bg-black opacity-80 z-1"></div>
<div class="w-full mx-auto px-4 max-w-6xl relative flex items-center justify-center">
<div class="w-full h-40 md:h-80 absolute inset-0 z-2">
<div class="grid grid-cols-10 w-full">
<div class="col-span-3 flex justify-center items-center h-60 md:h-80 mx-2 md:mx-8">
<div
class="h-40 md:h-60 w-full rounded-tl-3xl rounded-br-3xl border-double px-2 overflow-x-hidden relative overflow-y-hidden bg-cover z-1 after:z-2 after:content-[''] after:w-full after:h-full after:top-0 after:left-0 after:bg-#000 after:opacity-30 after:absolute"
:style="{ backgroundImage: `url(${currentArticle?.thumbnail})` }"
>
<img :src="currentArticle?.thumbnail" alt="" class="relative z-3 h-40 md:h-60 w-full object-contain" />
</div>
</div>
<div class="col-span-7 grid grid-cols-12 relative">
<div class="col-span-12 w-full grid grid-cols-12 mt-8 md:mb-4">
<div class="col-span-11">
<h1 class="text-md md:text-3xl text-[#fff] font-bold font-['SFD']" v-html="currentArticle?.title"></h1>
<time class="xs:mt-0.5 text-[10px] md:text-sm text-[#fff]">
{{ utils.dateFormat(currentArticle?.createdOn, "dddd, DD/MM/YYYY - HH:mm") }}
</time>
</div>
</div>
<div class="col-span-12 w-full mb-4 hidden md:block">
<div v-html="currentArticle?.intro" class="text-left text-xl text-[#fff] font-['SFD']"></div>
</div>
<div class="col-span-11">
<AudioPlayer :src="getSrc(currentArticle?.detail)?.[1]" />
<!-- <Topic :topics="listTopic" />
<Event :events="listEvent" />
<Tag :tags="listTag" /> -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- <div class="md:col-span-3 col-span-12" v-if="listArticle?.length > 1">
<div class="flex items-center mb-5">
<ul class="bg-red-500 text-white text-sm font-semibold hover:bg-red-400 font-medium inline-block rounded-tl-lg rounded-br-lg">
<li class="inline-block uppercase rounded-l-lg border-radius border-red-500 border-r-0 px-2 py-1 text-center block transition-transform duration-300 transform hover:scale-105">Podcast Hôm nay</li>
</ul>
<div class="border border-slate-7 flex-grow ml-4"></div>
</div>
<div class="grid w-full">
<div class="" v-for="(item, index) in listArticle.filter((item) => item.id !== audioPlay.id)" :key="index">
<article mode="basic" :class="defaultClass.article" @click="audioPlay = { ...item }">
<div class="rounded-sm overflow-hidden relative">
<NuxtImg :src="item?.thumbnail || '/images/default-thumbnail.jpg'" placeholder fit="cover" :class="defaultClass.thumbnail" loading="lazy" />
<div class="absolute bottom-2 left-0 bg-stone-200/[.56] h-10 flex justify-center items-center w-20 rounded-full">
<span class="icon">
<svg width="30" height="30" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12.0131 0.5C5.38869 0.5 0 5.88331 0 12.5C0 19.1167 5.38869 24.5 12.0131 24.5C18.6376 24.5 24.0263 19.1167 24.0263 12.5C24.0263 5.88331 18.6376 0.5 12.0131 0.5ZM16.7889 12.9204L9.78122 17.4204C9.6991 17.4736 9.60426 17.5 9.51041 17.5C9.42829 17.5 9.34518 17.4795 9.2709 17.439C9.10957 17.3511 9.00985 17.1831 9.00985 17V8C9.00985 7.81691 9.10957 7.64891 9.2709 7.56102C9.42927 7.47411 9.62773 7.47945 9.78122 7.57958L16.7889 12.0796C16.9316 12.1714 17.0186 12.3301 17.0186 12.5C17.0186 12.6699 16.9316 12.8286 16.7889 12.9204Z"
fill="#FF0000"
></path>
</svg>
</span>
</div>
</div>
<p class="xs:mt-0.5 xs:text-sm text-sm">{{ utils.dateFormat(item?.createdOn) }}</p>
<h3 :class="defaultClass.title" v-html="item?.title"></h3>
</article>
</div>
</div>
</div> -->
</div>
</template>
<style lang="scss" scoped>
.name {
text-align: center;
line-height: 100px;
}
</style>
@@ -0,0 +1,70 @@
<script setup lang="ts">
import Comment from "@/components/dynamic-page/page-component/templates/other/comments/default.vue";
import Topic from "@/components/article/Topic.vue";
import Event from "@/components/article/Event.vue";
import Tag from "@/components/article/Tag.vue";
const { currentArticle } = storeToRefs(useArticleStore());
const store = reactive({
tag: useTagStore(),
topic: useTopicStore(),
event: useEventStore()
});
// const listTag = ref([]);
// const listTopic = ref([]);
// const listEvent = ref([])
// const getTagsAndTopicsAndEvents = async () => {
// if (!currentArticle) return;
// const fetchData = async (ids, fetchFn, list) => {
// if (!ids) return;
// const data = await Promise.all(ids.split(",").map(fetchFn));
// if (data.length > 0) list.value = data;
// };
// await Promise.all([
// fetchData(currentArticle.value.tagIds, store.tag.fetchById, listTag),
// fetchData(currentArticle.value.topicIds, store.topic.fetchById, listTopic),
// fetchData(currentArticle.value.eventIds, store.event.fetchById, listEvent)
// ]);
// };
// getTagsAndTopicsAndEvents();
</script>
<template>
<div class="max-w-1500px mx-auto">
<article class="w-full flex flex-col lg:flex-row gap-4 overflow-x-hidden mt-4 bg-#f7f7f7">
<div id="article-detail" class="flex-1 [&_iframe]:w-full [&_iframe]:max-w-full [&_iframe]:max-h-52 md:[&_iframe]:max-h-full [&_video]:max-w-full [&_video]:w-full">
<div v-html="currentArticle?.detail" />
</div>
<div class="lg:w-[480px] overflow-y-auto lg:max-h-560px">
<div class="w-full pt-6 pr-3">
<h1 v-html="currentArticle?.sub" class="text-xl font-bold opacity-60"></h1>
<h1 v-html="currentArticle?.title" class="text-2xl font-bold text-left sm:text-3xl xl:text-4xl" />
<!-- <ArticleMeta class="!justify-start items-center gap-x-2" :authors="article?.authors" :createdOn="article?.createdOn" :createdBy="article?.createdBy" /> -->
<div id="article-brief" class="mx-auto xl:max-w-6xl text-balance">
<div v-html="currentArticle?.intro" class="font-semibold text-left" />
</div>
<!-- <section>
<article class="mb-[1rem] py-[1rem] border-y-[1px] border-solid border-[#e0e0e0] flex items-center">
<iframe
:src="`https://www.facebook.com/plugins/like.php?href=${ORIGIN}/${category?.code}/${article?.code}&amp;width=160&amp;layout=button&amp;action=like&amp;size=small&amp;share=true&amp;height=65&amp;appId`"
width="140" height="20" style="border:none;overflow:hidden" scrolling="no" frameborder="0"
allowfullscreen="true"
allow="autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"></iframe>
</article>
</section> -->
<!-- <Topic :topics="listTopic" />
<Event :events="listEvent" />
<Tag :tags="listTag" /> -->
<section id="comment-section" class="grid">
<Comment :articleId="currentArticle?.articleId" />
</section>
</div>
</div>
</article>
<div class="w-full border-t-2 border-dashed mt-4" />
</div>
</template>
@@ -2,6 +2,8 @@ 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 Article_Detail_Podcast } from './details/podcast.vue'
export { default as Article_Detail_Video } from './details/video.vue'
export { default as Default_Breadcrumb} from './breadcrumb/default.vue'
export { default as ADS_Default } from './ads/default.vue';
export { default as Comment } from './comments/default.vue'
@@ -1,7 +1,8 @@
<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, ADS_Default, Comment} from "./index";
import { Article_Button, Article_Detail_Emagazine, Article_Detail_Default, Article_Detail_Infographics,
Default_Breadcrumb, ADS_Default, Comment, Article_Detail_Podcast, Article_Detail_Video
} from "./index";
const _props = defineProps<{
settings: any;
component?: any;
@@ -15,8 +16,8 @@ const definedDynamicComponent: Record<string, any> = {
'ADS_DEFAULT': ADS_Default,
'ARTICLE_BUTTON': Article_Button,
COMMENT: Comment,
// POCAST: Podcast,
// VIDEO: Video
PODCAST: Article_Detail_Podcast,
VIDEO: Article_Detail_Video
};
const getCurrentComponent = computed(() => `${_props.settings.layout}`);
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { isEmpty } from "lodash";
import isEmpty from "lodash/isEmpty";
import DynamicComponent from "~/components/dynamic-page/page-component/templates/index.vue";
import { COLLECTION_PAGING_QUERY_DROP, getInputValue } from "@/utils/parseSQL";
const router = useRouter();
@@ -44,7 +44,6 @@ const findDataPosition = computed(() => {
:settings="findDataPosition.settings"
:component="findDataPosition"
/>
<div v-else class="empty"></div>
</template>
<template v-else-if="props.type === defineTypeRecusive.SECTION">
<DynamicSection
@@ -53,18 +52,18 @@ const findDataPosition = computed(() => {
:content="findDataPosition.content ? JSON.parse(findDataPosition.content) : null"
:section="findDataPosition"
/>
<div v-else class="empty"></div>
</template>
<template v-else>
<div class="empty"></div>
</template>
</div>
</template>
<style lang="scss" scoped>
.empty {
min-height: 100px;
border-radius: 6px;
background: #409eff;
.collection-container {
&.vertical {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
&.horizontal {
grid-template-rows: auto;
grid-auto-flow: column;
}
}
</style>
@@ -8,25 +8,25 @@ const CLASS_FOR_LAYOUT = computed(() => {
switch (props.layout) {
case 'Full_Page':
_classForLayout = {
page_container: 'page_container w-full px-2',
page_container: 'page_container mx-auto w-full px-2',
layout_container: 'layout_container px-5',
};
break;
case 'Center_Page':
_classForLayout = {
page_container: 'page_container w-full px-2 container ',
page_container: 'page_container mx-auto w-full px-2 container ',
layout_container: 'layout_container container-xxl mx-auto',
};
break;
case 'Background_Page':
_classForLayout = {
page_container: 'page_container w-full background-container ',
page_container: 'page_container mx-auto w-full background-container px-2',
layout_container: 'layout_container mx-auto',
};
break;
default:
_classForLayout = {
page_container: 'page_container px-2',
page_container: 'page_container mx-auto px-2',
layout_container: 'layout_container',
};
break;
+1 -6
View File
@@ -18,20 +18,15 @@ export default defineNuxtConfig({
"@vueuse/nuxt",
"@pinia/nuxt",
"nuxt-delay-hydration",
// "@nuxtjs/critters",
"nuxt-icon",
// "nuxt-custom-elements",
"dayjs-nuxt",
"nuxt-swiper",
"nuxt-lodash",
// "nuxt-headlessui",
'@ant-design-vue/nuxt',
// "@sidebase/nuxt-auth",
],
runtimeConfig: {
public: {
apiUrl: process.env.NUXT_PUBLIC_BASE_API || "http://api-portal.vpress.vn/api-v1",
apiUrl: "http://api-portal.vpress.vn/api-v1",
site: process.env.NUXT_PUBLIC_SITE_DEFAULT || "1",
},
authSecret: process.env.AUTH_SECRET||"vpress"
+162
View File
@@ -0,0 +1,162 @@
<script setup lang="ts">
import { ref } from 'vue';
const props = defineProps<{
src?: string
}>()
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 = () => {
if (audioPlayer.value) {
audioPlayer.value.volume = volume.value;
if (volume.value == 0) {
isVolume.value = false
} else {
isVolume.value = true
}
}
};
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>
<audio :src="src" preload="auto" ref="audioPlayer" @timeupdate="updateCurrentTime" @loadedmetadata="updateDuration" />
<div class="relative">
<div class="w-full">
<input class="w-full accent-[#fff] cursor-pointer" type="range" v-model="currentTime" @input="seekToTime" :max="duration" />
<div class="flex items-center justify-between px-4 ">
<span class="text-[10px] sm:text-lg font-normal font-[Raleway] text-[#fff]">{{ currrentTimeComputed }}</span>
<span class="text-[10px] sm:text-lg font-normal font-[Raleway] text-[#fff]">{{ durationComputed }}</span>
</div>
</div>
<div class="w-full px-4 ">
<div class="flex items-center justify-between">
<div>
<button @click="chanageSpeed"
class="text-[#fff] bg-transparent text-sm sm:text-sm lg:text-lg flex gap-1"><span
class="hidden lg:block">Tốc độ phát </span> <span
class="font-bold text-[12px] sm:text-sm lg:text-lg">{{
speedDefault
}}</span> </button>
</div>
<div>
<button @click="replayAndForward(-5)" class="hover:bg-stone-200 bg-transparent p-2 rounded-full">
<Icon name="ri:replay-5-fill" color="fff" class="text-lg sm:text-2xl md:text-4xl" style="" />
</button>
<button @click="togglePlayer" class="bg-transparent">
<Icon v-if="isPlayed" name="ri:play-circle-fill" color="fff"
class="text-lg sm:text-3xl md:text-6xl" />
<Icon v-if="!isPlayed" name="ri:pause-circle-fill" color="fff"
class="text-lg sm:text-3xl md:text-6xl" />
</button>
<button @click="replayAndForward(5)" class="hover:bg-stone-200 bg-transparent p-2 rounded-full">
<Icon name="ri:forward-5-line" color="fff" class="text-lg sm:text-2xl md:text-4xl" />
</button>
</div>
<div class="flex items-center">
<button class="bg-transparent">
<div class="flex gap-2 ">
<Icon v-if="isVolume" @click="setUpVolums()" name="ri:volume-up-fill" color="fff"
class="text-lg md:text-2xl lg:text-3xl" />
<Icon v-if="!isVolume" @click="setUpVolums()" name="ri:volume-mute-fill" color="fff"
class="text-lg md:text-2xl lg:text-3xl" />
<input v-if="isVolume" class="accent-[#fff] w-12 lg:w-20 cursor-pointer" type="range" v-model="volume"
@input="updateVolume" min="0" max="1" step="0.1" />
</div>
</button>
</div>
</div>
</div>
</div></template>
+4 -26
View File
@@ -5,56 +5,34 @@
"build": "nuxt build --dotenv .env.production",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
"preview": "nuxt preview"
},
"devDependencies": {
"@ant-design-vue/nuxt": "^1.4.1",
"@nuxt/devtools": "1.0.0",
"@nuxt/image": "^1.1.0",
"@pinia/nuxt": "latest",
"@remix-run/web-file": "^3.1.0",
"@sidebase/nuxt-auth": "^0.6.4",
"@types/glidejs__glide": "latest",
"@unocss/postcss": "latest",
"cva": "beta",
"dayjs-nuxt": "^2.1.8",
"nuxt": "latest",
"nuxt-custom-elements": "beta",
"nuxt-headlessui": "^1.1.4",
"nuxt-icon": "latest",
"nuxt-lodash": "latest",
"typescript": "^5.3.3",
"vue-tsc": "^1.8.27"
},
"dependencies": {
"@glidejs/glide": "^3.6.0",
"@types/lodash": "^4.17.4",
"@unocss/nuxt": "latest",
"@unocss/reset": "latest",
"@vueuse/core": "^10.8.0",
"@vueuse/nuxt": "10.5.0",
"aos": "latest",
"axios": "^1.5.1",
"cheerio": "^1.0.0-rc.12",
"clsx": "^2.1.0",
"defu": "^6.1.4",
"ipx": "^3.0.1",
"mitt": "^3.0.1",
"next-auth": "4.21.1",
"node-html-parser": "latest",
"lodash": "^4.17.21",
"nuxt-delay-hydration": "latest",
"nuxt-swiper": "latest",
"parse-nested-form-data": "^1.0.0",
"request": "^2.88.2",
"request-promise": "^0.0.1",
"require": "^0.4.4",
"sass": "latest",
"sass-loader": "latest",
"tailwind-merge": "latest",
"vite-svg-loader": "latest",
"vue-advanced-cropper": "^2.8.8",
"winston": "^3.11.0",
"zod": "^3.22.4"
"tailwind-merge": "latest"
},
"overrides": {
"vue": "latest"
+10 -9
View File
@@ -16,6 +16,7 @@ import { useDynamicPageStore } from '~/stores/dynamic-page';
import { useArticleStore } from '~/stores/articles';
const { currentPage, sectionPublished, componentPublished } = storeToRefs(useDynamicPageStore());
const { currentArticle } = storeToRefs(useArticleStore());
const store = reactive({
dynamicPage: useDynamicPageStore(),
article: useArticleStore(),
@@ -37,7 +38,6 @@ const loadPage = async (contentType: string | number) => {
watch(currentArticle, async () => {
let isContentType : string = '';
console.log(currentArticle.value, 'type')
switch (currentArticle.value?.contentType) {
case 1:
isContentType = 'trang-doi-song'
@@ -47,11 +47,11 @@ watch(currentArticle, async () => {
break;
case 3:
isContentType = 'ArticleLayoutPodcast'
isContentType = 'trang-chi-tiet-podcast'
break;
case 4:
isContentType = 'ArticleLayoutVideo'
isContentType = 'trang-chi-tiet-video-clip'
break;
case 5:
@@ -71,15 +71,16 @@ watch(currentArticle, async () => {
isContentType = 'trang-chi-tiet-emagazine'
break;
}
await loadPage(isContentType)
await loadPage(isContentType);
}, { deep: true })
useSeoMeta({
title: currentArticle.value?.title?.replace(/<[^>]+>/g, ''),
ogTitle: currentArticle.value?.title,
description: currentArticle.value?.intro,
ogDescription: currentArticle.value?.intro,
ogImage: currentArticle.value?.thumbnail,
title: () => currentArticle.value?.title?.replace(/<[^>]+>/g, ''),
description: () => currentArticle.value?.intro,
ogTitle: () => currentArticle.value?.title,
ogImage: () => currentArticle.value?.thumbnail,
ogDescription: () => currentArticle.value?.intro,
twitterCard: 'summary_large_image',
})
</script>
-1
View File
@@ -3,7 +3,6 @@ import _cloneDeep from 'lodash/cloneDeep';
import DynamicTemplate from "~/components/dynamic-page/page/templates/index.vue";
import DynamicSection from "~/components/dynamic-page/page-section/templates/index.vue";
import { useDynamicPageStore } from '~/stores/dynamic-page';
const { currentPage, sectionPublished, componentPublished } = storeToRefs(useDynamicPageStore());
+6
View File
@@ -1,8 +1,14 @@
import { createRouter, defineEventHandler, useBase } from 'h3'
import * as navigationCtrl from '~/server/models/navigation'
import * as eventCtrl from '~/server/models/event'
import * as tagCtrl from '~/server/models/tag'
import * as topicCtrl from '~/server/models/topic'
const router = createRouter()
router.get('/navigation', defineEventHandler(navigationCtrl.get))
router.get('/tag', defineEventHandler(tagCtrl.fetchById))
router.get('/topic', defineEventHandler(topicCtrl.fetchById))
router.get('/event', defineEventHandler(eventCtrl.fetchById))
export default useBase('/api/services', router.handler)
+31
View File
@@ -0,0 +1,31 @@
import { utils } from "~/utils/utilities";
import Base from "./base";
import { H3Event } from "h3";
export type Author = {
id:number;
siteId:number;
userId:number;
title:string
code:string;
description?:string;
thumbnail?:string;
feature?:string;
type?:number;
}
export const fetchByCode = async (event: H3Event) => {
try {
const { apiUrl } = useRuntimeConfig().public
const { authorCode }: any = getQuery(event)
const { items }: any = await $fetch(`${apiUrl}/cms/author/code:${authorCode}`, {
method: 'GET',
headers: {
site: 1
}
})
return items[0]
} catch (error) {
handleError(error)
}
}
+50
View File
@@ -0,0 +1,50 @@
import { utils } from "~/utils/utilities";
import Base from "./base";
import { H3Event } from "h3";
export const listPaging = async(event: H3Event) => {
try {
const { apiUrl } = useRuntimeConfig().public
const { siteId, page, fetch } = getQuery(event)
const { items, total }: any = await $fetch(`${apiUrl}/cms/event/condition/paging:${page}-${fetch}`, {
method: 'POST',
body: {siteIds: [siteId]}
})
return {items, total}
} catch (error) {
handleError(error);
}
}
export const fetchByCode = async(event: H3Event) => {
try {
const { apiUrl } = useRuntimeConfig().public
const { eventCode }: any = getQuery(event)
const { item }: any = await $fetch(`${apiUrl}/cms/event/code:${eventCode}`, {
method: 'GET',
headers: {
Site: 1
}
})
return item
} catch (error) {
handleError(error)
}
}
export const fetchById = async(event: H3Event) => {
try {
const { apiUrl } = useRuntimeConfig().public
const { eventId }: any = getQuery(event)
const { item }: any = await $fetch(`${apiUrl}/cms/event/${eventId}`, {
method: 'GET',
headers: {
Site: 1
}
})
return item
} catch (error) {
handleError(error)
}
}
+36
View File
@@ -0,0 +1,36 @@
import { utils } from "~/utils/utilities";
import { Author } from "./author";
import Base from "./base";
import { H3Event } from "h3";
export const get = async(event: H3Event) => {
try {
const { apiUrl } = useRuntimeConfig().public
const { code } = getQuery(event)
const { items }: any = await $fetch(`${apiUrl}/cms/tag/code:${code}`, {
headers: {
site: 1
}
})
return items
} catch(error) {
handleError(error);
}
}
export const fetchById = async(event: H3Event) => {
try {
const { apiUrl } = useRuntimeConfig().public
const { tagId }: any = getQuery(event)
const { item }: any = await $fetch(`${apiUrl}/cms/tag/${tagId}`, {
method: 'GET',
headers: {
Site: 1
}
})
return item
} catch (error) {
handleError(error)
}
}
+60
View File
@@ -0,0 +1,60 @@
import { utils } from "~/utils/utilities";
import { Author } from "./author";
import Base from "./base";
import { ref } from "vue"
import { H3Event } from "h3";
export const listPaging = async (event: H3Event) => {
try {
const { apiUrl } = useRuntimeConfig().public
const { categoryId, page, limit, sort } = getQuery(event)
const query = ref({})
if(categoryId) {
query.value = { categoryId }
}
const { items, total }: any = await $fetch(`${apiUrl}/cms/topic/condition/paging:${page}-${limit}/sorting:${sort}`,{
method: 'POST',
headers: {site: 1},
body:{ ...query.value }
})
return { items, total };
} catch (error) {
handleError(error)
}
}
export const fetchByCode = async(event: H3Event) => {
try {
const { apiUrl } = useRuntimeConfig().public
const { topicCode }: any = getQuery(event)
const { item }: any = await $fetch(`${apiUrl}/cms/topic/code:${topicCode}`, {
method: 'GET',
headers: {
site: 1
}
})
return item
} catch (error) {
handleError(error)
}
}
export const fetchById = async(event: H3Event) => {
try {
const { apiUrl } = useRuntimeConfig().public
const { topicId }: any = getQuery(event)
const { item }: any = await $fetch(`${apiUrl}/cms/topic/${topicId}`, {
method: 'GET',
headers: {
site: 1
}
})
return item
} catch (error) {
handleError(error)
}
}
+2 -2
View File
@@ -12,9 +12,9 @@ export const useArticleStore = defineStore("article", () => {
const getArticleBySlug = async (slug: string) => {
try {
const { data} = await useFetch(`/api/articles/get-by-slug/${slug}`)
const { data: article } = await useAsyncData('article', () => $fetch(`/api/articles/get-by-slug/${slug}`))
currentArticle.value = {}
currentArticle.value = data.value.item
currentArticle.value = article.value?.item
} catch (error: any) {}
}
+45
View File
@@ -0,0 +1,45 @@
import { LocationQuery } from "vue-router";
import { defineStore, acceptHMRUpdate } from "pinia";
export const useEventStore = defineStore('event', () => {
const pagination = ref({
page: utils.toNumber(useRuntimeConfig().public.pagingDefault),
limit: utils.toNumber(useRuntimeConfig().public.pagingLimit),
total: 0,
});
function setStateFromRoute(query: LocationQuery) {
if (query.page) pagination.value.page = utils.toNumber(query.page)
else pagination.value.page = utils.toNumber(useRuntimeConfig().public.pagingDefault);
if (query.limit) pagination.value.limit = utils.toNumber(query.limit)
else pagination.value.limit = utils.toNumber(useRuntimeConfig().public.pagingLimit);
}
async function listPaging(siteId: number, page: number, fetch: number) {
const { data, error } = await useFetch<any>(`/api/services/events-paging?siteId=${siteId}&page=${page}&fetch=${fetch}`)
if (error.value) {
return null
}
return data.value
}
async function fetchById(id: string) {
const { data, error } = await useFetch<any>(`/api/services/event`, {
query: {
eventId: id
}
})
if(error.value) {
return null
}
return data.value
}
return { listPaging, fetchById, setStateFromRoute, pagination }
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useEventStore, import.meta.hot))
}
+28
View File
@@ -0,0 +1,28 @@
export const useTagStore = defineStore('tag', () => {
// async function fetchByCode(code: string) {
// const { data, error } = await useFetch<any>(`/api/services/tag`, { query: {code: code}})
// if(error.value) {
// return null
// }
// return data.value[0]
// }
async function fetchById(id: string) {
const { data, error } = await useFetch<any>(`/api/services/tag`, {
query: {
tagId: id
}
})
if(error.value) {
return null
}
return data.value
}
return { fetchById }
})
if(import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useTagStore, import.meta.hot))
}
+55
View File
@@ -0,0 +1,55 @@
import { LocationQuery } from "vue-router";
import { defineStore, acceptHMRUpdate } from "pinia";
export const useTopicStore = defineStore('topic', () => {
const pagination = ref({
page: utils.toNumber(useRuntimeConfig().public.pagingDefault),
limit: utils.toNumber(useRuntimeConfig().public.pagingLimit),
total: 0,
});
function setStateFromRoute(query: LocationQuery) {
if (query.page) pagination.value.page = utils.toNumber(query.page)
else pagination.value.page = utils.toNumber(useRuntimeConfig().public.pagingDefault);
if (query.limit) pagination.value.limit = utils.toNumber(query.limit)
else pagination.value.limit = utils.toNumber(useRuntimeConfig().public.pagingLimit);
}
async function fetchById(topicId: string) {
const { data, error } = await useFetch<any>(`/api/services/topic`, {
query: {
topicId: topicId
}
})
if(error.value) {
return null
}
return data.value
}
async function fetchByCategoryId(categoryId: number, limit?: number) {
if(limit) {
pagination.value.limit = limit
}
const { data, error } = await useFetch<any>(`/api/services/topics-paging`, {
query: {
categoryId: categoryId,
limit: pagination.value.limit,
page: pagination.value.page,
sort: 'createdon desc'
}
})
if(error.value) {
return null
}
return data.value.items
}
return { pagination, setStateFromRoute, fetchById, fetchByCategoryId }
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useTopicStore, import.meta.hot))
}
-5
View File
@@ -70,16 +70,11 @@ export default defineConfig({
extractors: [],
presets: [
presetUno(),
presetMini(),
presetWebFonts({
provider: "google",
fonts: {
nunito: "Nunito",
playfair: ['Playfair Display', 'sans-serif'],
'playfair-display': ['Playfair Display', 'serif'],
'bai-jamjuree': ['Bai Jamjuree', 'Arial', 'sans-serif'],
sans: ['Raleway', 'Arial', "Helvetica Neue", 'Helvetica', 'sans-serif'],
arial: ['Arial', 'sans-serif'],
},
}),
],
+1 -6
View File
@@ -1,9 +1,4 @@
/* Bộ query mẫu */
// Sql[SELECT * FROM Table WHERE Id=1] Key[xxx]
// Uri[link-api] Method[Post] Params[{"param1":"value1","param2":"value2"}] Headers[{"Authorization":"12345678","Content-Type":"application/json"}] Content[{"data1":"value1","data2":"value2"}] Key[xxx]
// Get[Article] Top[10] With[Topics:1,2,3] Sort[Views-,Shares+]
import { isEmpty } from "lodash";
import isEmpty from "lodash/isEmpty";
const keyMapping = {
// 3 query key để phân loại