phongdt:header footer
This commit is contained in:
@@ -1,237 +1,69 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<footer class="border-t bg-white mt-6">
|
||||||
useCmsPageStore
|
<div id="footer-desktop" class="px-4 mx-auto max-w-7xl 2xl:px-0 pt-4">
|
||||||
|
<div class="grid gap-4 font-semibold md:grid-cols-12 text-sm mb-2">
|
||||||
|
<div class="col-span-8">
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-4 grid gap-4 sm:border-l sm:pl-4 auto-rows-max">
|
||||||
|
<div>
|
||||||
|
<p class="mb-2 uppercase text-xl font-bold">Liên hệ</p>
|
||||||
|
<div class="flex flex-col gap-3 whitespace-nowrap">
|
||||||
|
<div class="flex items-center max-w-full gap-2">
|
||||||
|
<Icon name="fa6-solid:building" />
|
||||||
|
<span class="text-sm hover-underline" title="Trụ sở chính: T.5 93A, Thụy Khuê, TP.Hà Nội">Toà Soạn</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center max-w-full gap-2">
|
||||||
|
<Icon name="fa6-solid:envelope" />
|
||||||
|
<a href="mailto:ktdtonline@gmail.com" class="text-sm hover-underline">
|
||||||
|
contact@vpress.vn
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center max-w-full gap-2">
|
||||||
|
<Icon name="fa6-solid:handshake" />
|
||||||
|
<span class="text-sm">Hợp tác bản quyền</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="mb-2 text-neutral-500">Đường dây nóng</p>
|
||||||
|
<div class="flex flex-col lg:(flex-row justify-between)">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-lg font-bold tracking-wide">0123456789</span>
|
||||||
|
<p class="text-sm text-neutral-500">(Hà Nội)</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-lg font-bold tracking-wide">0123456789</span>
|
||||||
|
<p class="text-sm text-neutral-500">(Hồ Chí Minh)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div class="flex flex-col items-center justify-between gap-4 my-2 sm:flex-row">
|
||||||
|
<div class="flex items-center justify-center sm:order-1">
|
||||||
|
<span>Hệ thống đang chạy thử nghiệm</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center gap-4 sm:order-3">
|
||||||
|
<a href="https://www.facebook.com" title="Theo dõi chúng tôi trên facebook" class="grid duration-300 border rounded-full w-9 h-9 border-neutral-200 text-neutral-500 place-items-center hover:bg-blue-500 hover:text-white hover:border-blue-500">
|
||||||
|
<Icon name="fa6-brands:facebook-f" />
|
||||||
|
</a>
|
||||||
|
<a href="https://www.youtube.com" title="Theo dõi chúng tôi trên youtube" class="grid duration-300 border rounded-full w-9 h-9 border-neutral-200 text-neutral-500 place-items-center hover:bg-black hover:text-white hover:border-black">
|
||||||
|
<Icon name="ion:logo-youtube" />
|
||||||
|
</a>
|
||||||
|
<a href="https://www.tiktok.com" title="Theo dõi chúng tôi trên tiktok" class="grid border rounded-full w-9 h-9 border-neutral-200 text-neutral-500 place-items-center hover:bg-black hover:text-white hover:border-black">
|
||||||
|
<Icon name="fa6-brands:tiktok" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center gap-4 sm:ml-auto sm:order-2">
|
||||||
|
<a href="#!" class="text-sm lg:text-base text-neutral-500">RSS</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.col-span-8 {
|
|
||||||
grid-column: span 8 / span 8;
|
|
||||||
|
|
||||||
@media (max-width: 1150px) {
|
|
||||||
grid-column: span 7 / span 7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.col-span-12 {
|
|
||||||
grid-column: span 12 / span 12 !important;
|
|
||||||
}
|
|
||||||
.mbootom-5 {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
.mbootom-14 {
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
.text-neutral-500 {
|
|
||||||
color: #737373;
|
|
||||||
}
|
|
||||||
.grid-col-2 {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
&.grid-col-1 {
|
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-col {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.text-span {
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.025em;
|
|
||||||
flex: 1;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-a {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
line-height: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.lg-row {
|
|
||||||
@media (min-width: 1300px) {
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer1 {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
background-color: #ffffff;
|
|
||||||
color: black;
|
|
||||||
border-top: 1px solid #bfbfbf;
|
|
||||||
|
|
||||||
&-wrap {
|
|
||||||
max-width: 90%;
|
|
||||||
margin: auto;
|
|
||||||
padding-top: 1rem;
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
|
|
||||||
.section-right {
|
|
||||||
display: grid;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
gap: 1rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
@media (min-width: 950px) {
|
|
||||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-category {
|
|
||||||
display: grid;
|
|
||||||
gap: 1rem;
|
|
||||||
height: 100%;
|
|
||||||
/* grid-template-columns: repeat(5, minmax(0, 1fr)); */
|
|
||||||
&.grid-col-3 {
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
&.grid-col-2 {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
.item-nav {
|
|
||||||
padding: 10px;
|
|
||||||
.text {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.drag-new {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #215486;
|
|
||||||
font-size: 40px;
|
|
||||||
color: #fff;
|
|
||||||
margin: 0 11px;
|
|
||||||
max-width: 200px;
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-4 {
|
|
||||||
display: grid;
|
|
||||||
grid-column: span 4 / span 4;
|
|
||||||
grid-auto-rows: max;
|
|
||||||
gap: 1rem;
|
|
||||||
&.border-top-left-0 {
|
|
||||||
border-left: 0;
|
|
||||||
border-top: 1px solid #bfbfbf;
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
@media (max-width: 1150px) {
|
|
||||||
grid-column: span 5 / span 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 950px) {
|
|
||||||
padding-left: 1rem;
|
|
||||||
border-left: 1px solid #bfbfbf;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
|
|
||||||
&-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
.text-item {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
.text-child {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
flex: 1;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-bottom {
|
|
||||||
display: flex;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ssr {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #737373;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__left {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__right {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.icon1 {
|
|
||||||
color: #737373;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
border-radius: 9999px;
|
|
||||||
border: 1px solid #737373;
|
|
||||||
transition-duration: 300ms;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
order: 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const currentDateTime = ref<string>("");
|
||||||
|
onMounted(() => {
|
||||||
|
currentDateTime.value = dayjs().format("dddd, DD/MM/YYYY");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center text-sm whitespace-nowrap">
|
||||||
|
<Icon name="fa6-regular:clock" />
|
||||||
|
<span class="inline-block text-16px leading-normal ml-1">{{ currentDateTime }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,237 +1,96 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
|
import { CurrentDateTime, LangSwitcher, TopNavigation, Mega } from "./index";
|
||||||
|
const widgetsStore = useWidgetsStore();
|
||||||
|
const layoutstore = useLayoutStore();
|
||||||
|
|
||||||
|
const { weather } = storeToRefs(widgetsStore);
|
||||||
|
const { megaMenuActive } = storeToRefs(layoutstore);
|
||||||
|
|
||||||
|
const navClass = ref("");
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (window.scrollY > 0) {
|
||||||
|
navClass.value = "shadow-md";
|
||||||
|
} else {
|
||||||
|
navClass.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
window.addEventListener("scroll", handleScroll);
|
||||||
|
await widgetsStore.fetchWeatherByLocation();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener("scroll", handleScroll);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
|
<header id="header" class="relative">
|
||||||
|
<div class="w-full mx-auto px-4 max-w-8xl py-1">
|
||||||
|
<div id="top-bar-inner" class="flex items-center justify-between md:justify-center md:divide-x">
|
||||||
|
<NuxtLink to="/" id="logo" class="pr-6">
|
||||||
|
<img src="/images/200.png" alt="logo" class="object-cover w-24" />
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<ClientOnly>
|
||||||
|
<CurrentDateTime class="md:px-4 pt-5px" />
|
||||||
|
</ClientOnly>
|
||||||
|
|
||||||
|
<div class="items-center hidden px-6 ml-auto space-x-8 lg:flex">
|
||||||
<div>
|
<div>
|
||||||
useCmsPageStore
|
<ClientOnly>
|
||||||
|
<div v-if="weather" class="flex items-center space-x-1">
|
||||||
|
<p class="text-l">{{ weather.location.name }}</p>
|
||||||
|
<img :src="weather.current.condition.icon" alt="Weather Icon" class="h-8" />
|
||||||
|
<p class="text-l">{{ weather.current.temp_c }}°C</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<p>Đang tải thông tin thời tiết...</p>
|
||||||
|
</div>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hidden md:flex gap-4 items-center px-4">
|
||||||
|
<button class="outline-none flex py-2 bg-transparent">
|
||||||
|
<Icon name="gg:search" size="18" />
|
||||||
|
</button>
|
||||||
|
<NuxtLink :to="`/subscriptions/paper`">
|
||||||
|
<Icon name="material-symbols:book-4-outline" />
|
||||||
|
</NuxtLink>
|
||||||
|
<!-- <Auth /> -->
|
||||||
|
<button class="outline-none flex py-2 bg-transparent">
|
||||||
|
<Icon name="fa6-regular:circle-user" size="16" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LangSwitcher class="hidden md:block px-4 pt-5px" />
|
||||||
|
|
||||||
|
<div class="xl:hidden block pl-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
v-show="!megaMenuActive"
|
||||||
|
@click="layoutstore.setStatus(true)"
|
||||||
|
class="py-1 duration-300 hover:text-blue-500 bg-transparent"
|
||||||
|
>
|
||||||
|
<Icon name="fa6-solid:bars" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
v-show="megaMenuActive"
|
||||||
|
@click="layoutstore.setStatus(false)"
|
||||||
|
class="py-1 duration-300 hover:text-red-500 bg-transparent"
|
||||||
|
>
|
||||||
|
<Icon name="fa6-solid:xmark" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<TopNavigation />
|
||||||
|
|
||||||
|
<Teleport to="body">
|
||||||
|
<Mega />
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
<style></style>
|
||||||
<style lang="scss" scoped>
|
|
||||||
.col-span-8 {
|
|
||||||
grid-column: span 8 / span 8;
|
|
||||||
|
|
||||||
@media (max-width: 1150px) {
|
|
||||||
grid-column: span 7 / span 7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.col-span-12 {
|
|
||||||
grid-column: span 12 / span 12 !important;
|
|
||||||
}
|
|
||||||
.mbootom-5 {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
.mbootom-14 {
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
.text-neutral-500 {
|
|
||||||
color: #737373;
|
|
||||||
}
|
|
||||||
.grid-col-2 {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
&.grid-col-1 {
|
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-col {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.text-span {
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.025em;
|
|
||||||
flex: 1;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-a {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
line-height: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.lg-row {
|
|
||||||
@media (min-width: 1300px) {
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer1 {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
background-color: #ffffff;
|
|
||||||
color: black;
|
|
||||||
border-top: 1px solid #bfbfbf;
|
|
||||||
|
|
||||||
&-wrap {
|
|
||||||
max-width: 90%;
|
|
||||||
margin: auto;
|
|
||||||
padding-top: 1rem;
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
|
|
||||||
.section-right {
|
|
||||||
display: grid;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
gap: 1rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
@media (min-width: 950px) {
|
|
||||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-category {
|
|
||||||
display: grid;
|
|
||||||
gap: 1rem;
|
|
||||||
height: 100%;
|
|
||||||
/* grid-template-columns: repeat(5, minmax(0, 1fr)); */
|
|
||||||
&.grid-col-3 {
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
&.grid-col-2 {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
.item-nav {
|
|
||||||
padding: 10px;
|
|
||||||
.text {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.drag-new {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #215486;
|
|
||||||
font-size: 40px;
|
|
||||||
color: #fff;
|
|
||||||
margin: 0 11px;
|
|
||||||
max-width: 200px;
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-4 {
|
|
||||||
display: grid;
|
|
||||||
grid-column: span 4 / span 4;
|
|
||||||
grid-auto-rows: max;
|
|
||||||
gap: 1rem;
|
|
||||||
&.border-top-left-0 {
|
|
||||||
border-left: 0;
|
|
||||||
border-top: 1px solid #bfbfbf;
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
@media (max-width: 1150px) {
|
|
||||||
grid-column: span 5 / span 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 950px) {
|
|
||||||
padding-left: 1rem;
|
|
||||||
border-left: 1px solid #bfbfbf;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
|
|
||||||
&-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
.text-item {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
.text-child {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
flex: 1;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-bottom {
|
|
||||||
display: flex;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ssr {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #737373;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__left {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__right {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.icon1 {
|
|
||||||
color: #737373;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
border-radius: 9999px;
|
|
||||||
border: 1px solid #737373;
|
|
||||||
transition-duration: 300ms;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
order: 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onClickOutside } from "@vueuse/core";
|
||||||
|
|
||||||
|
const langSwitcherEl = ref<HTMLDivElement>();
|
||||||
|
|
||||||
|
const selectingLanguages = ref<boolean>(false);
|
||||||
|
const classes = computed(() => ({
|
||||||
|
"pointer-events-auto opacity-100": selectingLanguages.value,
|
||||||
|
"pointer-events-none opacity-0": !selectingLanguages.value,
|
||||||
|
}));
|
||||||
|
onClickOutside(langSwitcherEl, () => selectingLanguages.value = false);
|
||||||
|
|
||||||
|
const languages = ['Tiếng việt']
|
||||||
|
|
||||||
|
const onSelectLanguage = () => selectingLanguages.value = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="langSwitcherEl" id="lang-switcher" class="relative text-sm">
|
||||||
|
<button class="text-sm bg-transparent" @click="selectingLanguages = !selectingLanguages">
|
||||||
|
Tiếng việt
|
||||||
|
</button>
|
||||||
|
<div id="languages-switchable" :class="classes"
|
||||||
|
class="absolute z-50 min-w-36 right-0 top-10 bg-white rounded shadow overflow-hidden shadow-lg flex flex-col duration-300">
|
||||||
|
<div class="relative w-full px-1 py-1">
|
||||||
|
<ul>
|
||||||
|
<li v-for="(l, i) in languages" @click="onSelectLanguage" :key="i">
|
||||||
|
<button class="py-2 w-full rounded duration-300 hover:bg-blue-400 hover:text-white">
|
||||||
|
{{ l }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onClickOutside } from "@vueuse/core";
|
||||||
|
import { useNavigationStoreV2 } from '~/stores/navigation';
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
import { vInterpolate } from '~/directives/v-interpolate';
|
||||||
|
import * as cherrio from 'cheerio'
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const v2NavigationStore = useNavigationStoreV2()
|
||||||
|
const layoutstore = useLayoutStore();
|
||||||
|
|
||||||
|
const { megaMenuActive } = storeToRefs(layoutstore);
|
||||||
|
const {topMenu} = storeToRefs(v2NavigationStore)
|
||||||
|
|
||||||
|
const megaMenuEl = ref<HTMLElement>();
|
||||||
|
|
||||||
|
const computedClass = computed(() =>
|
||||||
|
megaMenuActive.value
|
||||||
|
? ["opacity-100", "pointer-events-auto"]
|
||||||
|
: ["opacity-0", "pointer-events-none"]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const $ = cherrio.load(topMenu.value)
|
||||||
|
|
||||||
|
const html = $('.parent').addClass('xl:(flex items-center justify-center)')
|
||||||
|
html.find('>li').addClass('xl:(relative group xl:mr-3) hover:bg-[#e6f4ff] py-3 px-6 rounded-md')
|
||||||
|
html.find('ul').addClass('pl-4 hidden xl:(gap-0 w-200px shadow group-hover:(block absolute top-full left-0 bg-white z-50))')
|
||||||
|
html.find('>li>a').addClass('xl:(block py-4 hover:(text-blue))')
|
||||||
|
html.find('>li>ul>li>a').addClass('xl:(block py-10px px-15px hover:(bg-blue text-white))')
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
|
||||||
|
<a-drawer
|
||||||
|
v-model:open="megaMenuActive"
|
||||||
|
class="custom-class"
|
||||||
|
root-class-name="root-class-name"
|
||||||
|
title="Tất cả chuyên mục"
|
||||||
|
placement="right"
|
||||||
|
:bodyStyle="{padding:0}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class=" h-full max-h-full flex flex-col gap-y-4 mx-auto"
|
||||||
|
>
|
||||||
|
<div id="mega-menu" v-interpolate v-html="html"></div>
|
||||||
|
</div>
|
||||||
|
</a-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
#mega-menu
|
||||||
|
max-height: 100vh
|
||||||
|
min-height: 100vh
|
||||||
|
|
||||||
|
#mega-list
|
||||||
|
max-height: 100vh
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useNavigationStoreV2 } from '~/stores/navigation';
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
import { vInterpolate } from '~/directives/v-interpolate';
|
||||||
|
import * as cherrio from 'cheerio'
|
||||||
|
|
||||||
|
const v2NavigationStore = useNavigationStoreV2()
|
||||||
|
const {topMenu} = storeToRefs(v2NavigationStore)
|
||||||
|
await v2NavigationStore.fetchNavigation()
|
||||||
|
const $ = cherrio.load(topMenu.value)
|
||||||
|
|
||||||
|
const html = $('.parent').addClass('xl:(flex items-center justify-center)')
|
||||||
|
html.find('>li').addClass('relative group xl:mr-3 hover:text-blue')
|
||||||
|
html.find('ul').addClass('hidden w-200px shadow group-hover:(block absolute top-full left-0 bg-white z-50)')
|
||||||
|
html.find('>li>a').addClass('block py-4')
|
||||||
|
html.find('>li>ul>li>a').addClass('block py-10px px-15px text-black hover:(bg-blue text-white)')
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav class="main-nav text-sm z-40 sticky top-0 bg-white relative border-y border-neutral-200 hidden xl:block" v-interpolate v-html="html">
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export { default as LangSwitcher } from './LangSwitcher.vue'
|
||||||
|
export { default as CurrentDateTime } from './CurrentDateTime.vue'
|
||||||
|
export { default as TopNavigation } from './TopNavigation.vue'
|
||||||
|
export { default as Mega } from './Mega.vue'
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import type { ObjectDirective } from 'vue'
|
||||||
|
|
||||||
|
type InterpolationElement = HTMLElement & {
|
||||||
|
$componentUpdated?: () => void
|
||||||
|
$destroy?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const vInterpolate: ObjectDirective<InterpolationElement> = {
|
||||||
|
mounted(el) {
|
||||||
|
const links = Array.from(el.getElementsByTagName('a')).filter((linkEl) => {
|
||||||
|
const href = linkEl.getAttribute('href')
|
||||||
|
if (!href) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return isInternalLink(href)
|
||||||
|
})
|
||||||
|
|
||||||
|
addListeners(links)
|
||||||
|
// cleanup
|
||||||
|
el.$componentUpdated = () => {
|
||||||
|
removeListeners(links)
|
||||||
|
nextTick(() => addListeners(links))
|
||||||
|
}
|
||||||
|
el.$destroy = () => removeListeners(links)
|
||||||
|
},
|
||||||
|
updated: (el) => el.$componentUpdated?.(),
|
||||||
|
beforeUnmount: (el) => el.$destroy?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function navigate(event: Event) {
|
||||||
|
const target = event.target as HTMLElement
|
||||||
|
const href = target.getAttribute('href')
|
||||||
|
event.preventDefault()
|
||||||
|
return navigateTo(href)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addListeners(links: HTMLAnchorElement[]) {
|
||||||
|
links.forEach((link) => {
|
||||||
|
link.addEventListener('click', navigate, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeListeners(links: HTMLAnchorElement[]) {
|
||||||
|
links.forEach((link) => {
|
||||||
|
link.removeEventListener('click', navigate, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInternalLink(href?: string) {
|
||||||
|
return href?.startsWith('/')
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createRouter, defineEventHandler, useBase } from 'h3'
|
import { createRouter, defineEventHandler, useBase } from 'h3'
|
||||||
import * as DynamicPageCtrl from '~/server/models/dynamic-page'
|
import * as DynamicPageCtrl from '~/server/models/dynamic-page'
|
||||||
|
import * as navigationCtrl from '~/server/models/navigation'
|
||||||
|
|
||||||
const router = createRouter()
|
const router = createRouter()
|
||||||
|
|
||||||
@@ -26,5 +27,6 @@ router.get('/get-by-id/:id', defineEventHandler(async (event : any) => {
|
|||||||
handleError(error);
|
handleError(error);
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
router.get('/navigation', defineEventHandler(navigationCtrl.get))
|
||||||
|
|
||||||
export default useBase('/api/services', router.handler)
|
export default useBase('/api/services', router.handler)
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default interface Base {
|
||||||
|
createdBy?: string | number
|
||||||
|
createdOn?: string
|
||||||
|
updatedBy?: string | number
|
||||||
|
updatedOn?: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import Base from "./base";
|
||||||
|
import {H3Event} from "h3";
|
||||||
|
|
||||||
|
export type Category = {
|
||||||
|
id: number;
|
||||||
|
siteId: number;
|
||||||
|
parentId?: number;
|
||||||
|
title: string;
|
||||||
|
code: string;
|
||||||
|
description?: string;
|
||||||
|
thumbnail?: string;
|
||||||
|
keyword?: string;
|
||||||
|
taxonomy?: string;
|
||||||
|
type: number;
|
||||||
|
layout?: number;
|
||||||
|
template?: string;
|
||||||
|
feature?: string;
|
||||||
|
settings?: string;
|
||||||
|
order?: number;
|
||||||
|
isPublished?: boolean;
|
||||||
|
publishType?: number;
|
||||||
|
publishedBy?: string;
|
||||||
|
publishedOn?: string;
|
||||||
|
status: number;
|
||||||
|
} & Base;
|
||||||
|
|
||||||
|
export const list = async () => {
|
||||||
|
try {
|
||||||
|
const { site, apiUrl } = useRuntimeConfig().public;
|
||||||
|
|
||||||
|
const {items}:any = await $fetch(`${apiUrl}/cms/category/site`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
site: site,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import Base from "./base";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a navigation item.
|
||||||
|
*/
|
||||||
|
export type NavigationItem = {
|
||||||
|
id: number;
|
||||||
|
siteId: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
feature?: string;
|
||||||
|
taxonomy?: string;
|
||||||
|
status: number;
|
||||||
|
} & Base;
|
||||||
|
|
||||||
|
const navigation = `
|
||||||
|
<ul class="parent">
|
||||||
|
<li><a href="/thoi-su" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Thời sự</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/tin-tuc" data-title="data-description=" data-description="data-code=data-keyword=" data-code="data-keyword=data-target=_blank" data-target="_blank" data-type="data-feature=true" data-feature="true">Tin tức</a></li>
|
||||||
|
<li><a href="/thong-tin-doi-ngoai" data-title="data-description=" data-description="data-code=data-keyword=" data-code="data-keyword=data-target=_blank" data-keyword="data-target=_blank" data-type="data-feature=true" data-feature="true">Thông tin đối ngoại</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><a href="/kinh-te" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Kinh tế</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/thi-truong" data-title="data-description=" data-description="data-code=data-keyword=" data-keyword="data-target=_blank" data-type="data-feature=true">Thị trường</a></li>
|
||||||
|
<li><a href="/hang-viet" data-title="data-description=" data-description="data-code=data-keyword=" data-keyword="data-target=_blank" data-type="data-feature=true">Hàng việt</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><a href="/do-thi" data-title="data-description=" data-description="data-code=data-keyword=" data-code="data-keyword=data-target=_blank" data-target="_blank" data-type="data-feature=true" data-feature="true">Đô thị</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/do-thi-24h" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Đô thị 24h</a></li>
|
||||||
|
<li><a href="/giao-thong" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Giao thông</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><a href="/bat-dong-san" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Bất động sản</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/thi-truong" data-title="data-description=" data-description="data-code=data-keyword=" data-keyword="data-target=_blank" data-type="data-feature=true">Thị trường</a></li>
|
||||||
|
<li><a href="/tu-van-dau-tu" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Tư vấn đầu tư</a></li>
|
||||||
|
<li><a href="/du-an" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Dự án</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><a href="/y-te" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Y tế</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/an-toan-thuc-pham" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">An toàn thực phẩm</a></li>
|
||||||
|
<li><a href="/tu-van-suc-khoe" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Tư vấn sức khỏe</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><a href="/giao-duc" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Giáo dục</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/tuyen-sinh" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Tuyển sinh</a></li>
|
||||||
|
<li><a href="/cau-chuyen-hoc-duong" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Câu chuyên học đường</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><a href="/doi-song" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Đời sống</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/viec-lam-an-sinh-xa-hoi" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Việc làm - an sinh xã hội</a></li>
|
||||||
|
<li><a href="/phong-su-ghi-chep" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Phóng sự ghi chép</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><a href="/van-hoa" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Văn hóa</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/van-nghe" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Văn nghệ</a></li>
|
||||||
|
<li><a href="/ha-noi-thanh-lich-van-minh" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Hà Nội thanh lịch văn minh</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><a href="/phap-luat" data-title="data-description=" data-description="data-code=data-keyword=" data-keyword="data-target=_blank" data-type="data-feature=true">Pháp luật</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/pha-an" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Phá án</a></li>
|
||||||
|
<li><a href="/phap-dinh" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Pháp đình</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><a href="/quoc-te" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Quốc tế</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/quoc-te-24h" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Quốc tế 24h</a></li>
|
||||||
|
<li><a href="/kinh-te-tai-chinh-toan-cau" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Kinh tế tài chính toàn cầu</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li> <a href="/multimedia" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Multimedia</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/podcast" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Podcast</a></li>
|
||||||
|
<li><a href="/anh" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Ảnh</a></li>
|
||||||
|
<li><a href="/video-clip" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Video Clip</a></li>
|
||||||
|
<li><a href="/infographics" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Infographics</a></li>
|
||||||
|
<li><a href="/emagazine" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Emagazine</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li> <a href="/chuyen-doi-so" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Chuyển đổi số</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/cong-nghe" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Công nghệ</a></li>
|
||||||
|
<li><a href="/trai-nghiem" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Trải nghiệm</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
|
||||||
|
export const get = () =>{
|
||||||
|
return navigation;
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { Category } from "~/server/models/category";
|
||||||
|
|
||||||
|
export const useCategoryStore = defineStore("category-v2", () => {
|
||||||
|
const categories = ref<Category[]>([]);
|
||||||
|
|
||||||
|
async function fetchCategories() {
|
||||||
|
const { data, error } = await useFetch<Category[]>("/api/v2/categories");
|
||||||
|
if (error.value) {
|
||||||
|
return [] as Category[];
|
||||||
|
}
|
||||||
|
|
||||||
|
categories.value = Object.assign([], data.value);
|
||||||
|
|
||||||
|
return categories.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findByCode(code?: string) {
|
||||||
|
if (code) return categories.value.find((c) => c.code === code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findById(id?: number) {
|
||||||
|
return categories.value.find((c) => c.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findParents(category?: Category) {
|
||||||
|
if (!category) return [];
|
||||||
|
|
||||||
|
const parents = [];
|
||||||
|
let parent = findById(category.parentId);
|
||||||
|
while (parent) {
|
||||||
|
parents.push(parent);
|
||||||
|
parent = findById(parent.parentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parents.reverse().concat(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSubTree(category?: Category) {
|
||||||
|
if (!category) return [];
|
||||||
|
|
||||||
|
let subTree = [] as Category[];
|
||||||
|
|
||||||
|
function findChildren(category: Category) {
|
||||||
|
const children = categories.value.filter((c:Category) => c.parentId === category.id);
|
||||||
|
if (children.length === 0) return;
|
||||||
|
|
||||||
|
subTree.push(...children,category);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(category.parentId === 41){
|
||||||
|
findChildren(category);
|
||||||
|
}else{
|
||||||
|
const parent = findById(category.parentId);
|
||||||
|
if(parent){
|
||||||
|
findChildren(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return subTree.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
function findChildren(category: Category) {
|
||||||
|
const children = categories.value.filter((c:Category) => c.parentId === category.id);
|
||||||
|
if (children.length === 0) return;
|
||||||
|
else return [...children]
|
||||||
|
}
|
||||||
|
|
||||||
|
return { categories, fetchCategories, findByCode, findById, findParents,findSubTree, findChildren };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (import.meta.hot) {
|
||||||
|
import.meta.hot.accept(acceptHMRUpdate(useCategoryStore, import.meta.hot));
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineStore, acceptHMRUpdate } from "pinia";
|
||||||
|
|
||||||
|
export const useLayoutStore = defineStore("layout", () => {
|
||||||
|
const megaMenuActive = ref<boolean>(false);
|
||||||
|
function setStatus(status: boolean) {
|
||||||
|
megaMenuActive.value = status;
|
||||||
|
}
|
||||||
|
return { megaMenuActive, setStatus };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (import.meta.hot) {
|
||||||
|
import.meta.hot.accept(acceptHMRUpdate(useLayoutStore, import.meta.hot));
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { defineStore, acceptHMRUpdate } from 'pinia'
|
||||||
|
|
||||||
|
export const useNavigationStoreV2 = defineStore('navigation-v2', () => {
|
||||||
|
const topMenu = ref('')
|
||||||
|
|
||||||
|
async function fetchNavigation() {
|
||||||
|
const {data, error } = await useFetch('/api/services/navigation')
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if(data.value) {
|
||||||
|
topMenu.value = data.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return topMenu.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return {topMenu, fetchNavigation}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (import.meta.hot) {
|
||||||
|
import.meta.hot.accept(acceptHMRUpdate(useNavigationStoreV2, import.meta.hot))
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { defineStore, acceptHMRUpdate } from 'pinia'
|
||||||
|
|
||||||
|
export const useWidgetsStore = defineStore('widgets', () => {
|
||||||
|
const weather = ref<any>(null)
|
||||||
|
const locations = ref(["Hanoi", "Ho Chi Minh City", "Huế", "Danang", "Hai Phong", "Nha Trang"])
|
||||||
|
const selectedLocation = ref("Hanoi")
|
||||||
|
|
||||||
|
async function fetchWeatherByLocation(location?:string){
|
||||||
|
try {
|
||||||
|
if(!location){
|
||||||
|
location = selectedLocation.value
|
||||||
|
}
|
||||||
|
const response = await $fetch(
|
||||||
|
`https://api.weatherapi.com/v1/current.json?key=56e1a8576f0c482280d84625230905&q=${location}&aqi=yes`
|
||||||
|
);
|
||||||
|
weather.value = response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {locations,selectedLocation,weather,fetchWeatherByLocation}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (import.meta.hot) {
|
||||||
|
import.meta.hot.accept(acceptHMRUpdate(useWidgetsStore, import.meta.hot))
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import * as cherrio from "cheerio";
|
||||||
|
|
||||||
|
export const utils = {
|
||||||
|
toNumber,
|
||||||
|
toString,
|
||||||
|
toBoolean,
|
||||||
|
toNumberArray,
|
||||||
|
toStringArray,
|
||||||
|
dateFormat,
|
||||||
|
generateSlugWithId,
|
||||||
|
formattedTime,
|
||||||
|
formateDate,
|
||||||
|
isDev,
|
||||||
|
domainImage,
|
||||||
|
uid,
|
||||||
|
isExternalUrl,
|
||||||
|
isValidPhone,
|
||||||
|
isTouchDevice,
|
||||||
|
toTitleCase
|
||||||
|
};
|
||||||
|
|
||||||
|
function toNumber(value: any, _default?: number): number {
|
||||||
|
const number = parseInt(String(value));
|
||||||
|
return Number(
|
||||||
|
isNaN(number) ? (_default !== undefined ? _default : 0) : number
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toString(value: any): string {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBoolean(value: any): boolean {
|
||||||
|
const lowercaseValue = String(value).toLowerCase();
|
||||||
|
if (lowercaseValue === "true") {
|
||||||
|
return true;
|
||||||
|
} else if (lowercaseValue === "false") {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid boolean string");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNumberArray(value: any): number[] {
|
||||||
|
return String(value)
|
||||||
|
.split(",")
|
||||||
|
.map((item) => Number(item.trim()))
|
||||||
|
.filter((num) => !isNaN(num));
|
||||||
|
}
|
||||||
|
function toStringArray(value: any): string[] {
|
||||||
|
return String(value)
|
||||||
|
.split(",")
|
||||||
|
.map((item) => item.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
function dateFormat(date: any, format?: string) {
|
||||||
|
const dayjsInstance = dayjs(date);
|
||||||
|
const formatter = format ?? "ddd, D MMM YYYY HH:mm";
|
||||||
|
let d = dayjsInstance.format(formatter);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSlugWithId(prefix?: string, slug?: string, id?: number) {
|
||||||
|
return `${prefix}/${slug}${id ? "-" + id : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formattedTime(seconds: number) {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = Math.floor(seconds % 60);
|
||||||
|
return `${String(minutes).padStart(2, "0")}:${String(
|
||||||
|
remainingSeconds
|
||||||
|
).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formateDate(
|
||||||
|
date: string | Date | undefined,
|
||||||
|
formatter: string = "HH:mm, dddd, D MMMM YYYY"
|
||||||
|
) {
|
||||||
|
if (!date) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const formattedDate = useDateFormat(date, formatter, { locales: "vi-VN" });
|
||||||
|
|
||||||
|
const dateObject = new Date(date);
|
||||||
|
const time = formattedDate.value.slice(0, 5); // Extract HH:mm
|
||||||
|
const day = `0${dateObject.getDate()}`.slice(-2); // Get day with leading zero
|
||||||
|
const month = `0${dateObject.getMonth() + 1}`.slice(-2); // Get month with leading zero
|
||||||
|
const year = dateObject.getFullYear();
|
||||||
|
|
||||||
|
// Creating the desired format "16:21 | 04/10/2022"
|
||||||
|
const result = ` ${day}/${month}/${year}`;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDev() {
|
||||||
|
return process.env.NODE_ENV === "development";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function domainImage(text: string = "", domainImage: string = "") {
|
||||||
|
const replaceDomains = ["http://45.77.168.121:8083"];
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
const $ = cherrio.load(text, null, false);
|
||||||
|
|
||||||
|
$("figure img").each((i, el) => {
|
||||||
|
const src = $(el).attr("src");
|
||||||
|
|
||||||
|
if (src && replaceDomains.some((domain) => src.startsWith(domain))) {
|
||||||
|
const replaceDomain = replaceDomains.find((domain) =>
|
||||||
|
src.startsWith(domain)
|
||||||
|
)!;
|
||||||
|
$(el).attr("src", src.replace(replaceDomain, domainImage));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $.html();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _id = 0
|
||||||
|
|
||||||
|
function uid () {
|
||||||
|
_id = (_id + 1) % Number.MAX_SAFE_INTEGER
|
||||||
|
return `vuid-${_id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExternalUrl(url?: string) {
|
||||||
|
if(!url) return false
|
||||||
|
return /^(http?:|https?:|mailto:|tel:)/.test(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidPhone(phone:string){
|
||||||
|
return /^(0)(3[2-9]|5[6|8|9]|7[0|6-9]|8[0-6|8|9]|9[0-4|6-9])[0-9]{7}$/.test(phone)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTouchDevice() {
|
||||||
|
return 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toTitleCase(str?: string){
|
||||||
|
if (!str) return;
|
||||||
|
return str.replace(/\w\S*/g, function (txt) {
|
||||||
|
return txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase();
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user