nuxt-layer-core 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +8 -0
- package/app/app.config.ts +5 -0
- package/app/app.vue +11 -0
- package/app/assets/css/main.css +113 -0
- package/app/components/.gitkeep +0 -0
- package/app/components/ui/sonner/Sonner.vue +62 -0
- package/app/components/ui/sonner/index.ts +1 -0
- package/app/composables/.gitkeep +0 -0
- package/app/layers/solution/app/assets/proxmox-css/main.css +89 -0
- package/app/layers/solution/app/assets/proxmox-css/proxmox-utils.css +68 -0
- package/app/layers/solution/app/components/ClusterDiagram.vue +72 -0
- package/app/layers/solution/app/components/CoursesSection.vue +245 -0
- package/app/layers/solution/app/components/CtaSection.vue +347 -0
- package/app/layers/solution/app/components/DynamicPageSections.vue +103 -0
- package/app/layers/solution/app/components/FeaturesSection.vue +67 -0
- package/app/layers/solution/app/components/HeroSection.vue +144 -0
- package/app/layers/solution/app/components/PricingPlansSection.vue +396 -0
- package/app/layers/solution/app/components/RegistrationForm.vue +283 -0
- package/app/layers/solution/app/components/RegistrationSection.vue +24 -0
- package/app/layers/solution/app/components/RegistrationSidebar.vue +70 -0
- package/app/layers/solution/app/components/TestimonialsSection.vue +58 -0
- package/app/layers/solution/app/components/TheFooter.vue +61 -0
- package/app/layers/solution/app/components/TheHeader.vue +193 -0
- package/app/layers/solution/app/components/proxmox/AppIcon.vue +45 -0
- package/app/layers/solution/app/components/proxmox/ComparisonSection.vue +54 -0
- package/app/layers/solution/app/components/proxmox/FaqSection.vue +58 -0
- package/app/layers/solution/app/components/proxmox/HciOverviewSection.vue +37 -0
- package/app/layers/solution/app/components/proxmox/HeroSection.vue +74 -0
- package/app/layers/solution/app/components/proxmox/TestimonialsSection.vue +44 -0
- package/app/layers/solution/app/components/proxmox/TrainingBenefitsSection.vue +37 -0
- package/app/layers/solution/app/components/proxmox/VeFeaturesSection.vue +55 -0
- package/app/layers/solution/app/composables/useActiveSection.ts +47 -0
- package/app/layers/solution/app/composables/useAuthApi.ts +244 -0
- package/app/layers/solution/app/composables/useAuthState.ts +16 -0
- package/app/layers/solution/app/composables/useConsultationRequest.ts +8 -0
- package/app/layers/solution/app/composables/useCookie.ts +79 -0
- package/app/layers/solution/app/composables/useCookieHelper.ts +81 -0
- package/app/layers/solution/app/composables/useCrmApi.ts +32 -0
- package/app/layers/solution/app/composables/useDynamicCollection.ts +23 -0
- package/app/layers/solution/app/composables/usePlanNormalizer.ts +45 -0
- package/app/layers/solution/app/composables/useScrollAnimate.ts +40 -0
- package/app/layers/solution/app/composables/useScrollToSection.ts +13 -0
- package/app/layers/solution/app/composables/useSolutionApi.ts +73 -0
- package/app/layers/solution/app/composables/useToast.ts +14 -0
- package/app/layers/solution/app/data/pricingPlans.ts +150 -0
- package/app/layers/solution/app/data/proxmox-content.ts +158 -0
- package/app/layers/solution/app/images/Proxmox HCI.png +0 -0
- package/app/layers/solution/app/images/image_from_https_hercules_cdn.com_file_6dztkvkfnip0ud23tzzvu57c.png +0 -0
- package/app/layers/solution/app/layouts/default.vue +5 -0
- package/app/layers/solution/app/nuxt.config.ts +15 -0
- package/app/layers/solution/app/pages/[slug]/[slug2]/[slug3]/index.vue +22 -0
- package/app/layers/solution/app/pages/[slug]/[slug2]/index.vue +23 -0
- package/app/layers/solution/app/pages/[slug]/index.vue +22 -0
- package/app/layers/solution/app/pages/cap-nhat-thong-tin/index.vue.disabled +302 -0
- package/app/layers/solution/app/pages/dang-ky/index.vue.disabled +627 -0
- package/app/layers/solution/app/pages/dang-ky-tu-van/khoa-hoc-proxmox.vue.disabled +39 -0
- package/app/layers/solution/app/pages/dang-nhap/index.vue.disabled +208 -0
- package/app/layers/solution/app/pages/doi-mat-khau/index.vue.disabled +237 -0
- package/app/layers/solution/app/pages/giai-phap-proxmox/[slug]/index.vue.disabled +37 -0
- package/app/layers/solution/app/pages/giai-phap-proxmox/[slug]/proxmox-hci/index.vue.disabled +45 -0
- package/app/layers/solution/app/pages/giai-phap-proxmox/dang-ky-khoa-hoc/[slug].vue.disabled +56 -0
- package/app/layers/solution/app/pages/quen-mat-khau/index.vue.disabled +136 -0
- package/app/layers/solution/app/pages/xac-thuc/index.vue.disabled +320 -0
- package/app/layers/solution/app/plugins/reveal.ts +56 -0
- package/app/layers/solution/app/public/images/image_from_https_hercules_cdn.com_file_6dztkvkfnip0ud23tzzvu57c.png +0 -0
- package/app/layers/solution/app/public/images/logo-lvs-1300x330.png +0 -0
- package/app/layouts/auth.vue +71 -0
- package/app/lib/utils.ts +7 -0
- package/app/middleware/.gitkeep +0 -0
- package/app/modules/layer-route-priority.ts +60 -0
- package/app/pages/index.vue +10 -0
- package/app/pages/san-pham-proxmox.vue +8 -0
- package/app/plugins/.gitkeep +0 -0
- package/app/plugins/sdk.ts +29 -0
- package/app/stores/.gitkeep +0 -0
- package/app/types/sdk.d.ts +15 -0
- package/components.json +24 -0
- package/nuxt.config.ts +105 -0
- package/package.json +52 -0
- package/public/.gitkeep +0 -0
- package/server/api/.gitkeep +0 -0
- package/server/utils/.gitkeep +0 -0
- package/tailwind.config.js +96 -0
- package/tsconfig.base.json +18 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { heroStats } from "../../data/proxmox-content"
|
|
3
|
+
const { scrollTo } = useScrollToSection()
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<section id="san-pham" class="relative overflow-hidden bg-gradient-to-b from-blue-50/60 via-white to-white">
|
|
8
|
+
<div
|
|
9
|
+
class="bg-grid pointer-events-none absolute inset-0 [mask-image:radial-gradient(ellipse_60%_60%_at_50%_0%,black,transparent)]" />
|
|
10
|
+
|
|
11
|
+
<div class="relative mx-auto max-w-7xl px-6 pt-20 pb-16 sm:pt-28 sm:pb-24 lg:px-8">
|
|
12
|
+
<div class="grid grid-cols-1 items-center gap-10 lg:grid-cols-2 lg:gap-12">
|
|
13
|
+
<!-- Left: copy -->
|
|
14
|
+
<div>
|
|
15
|
+
<div v-reveal
|
|
16
|
+
class="inline-flex items-center rounded-full bg-blue-50 px-4 py-1.5 text-sm font-medium text-blue-600 ring-1 ring-inset ring-blue-100">
|
|
17
|
+
Giải pháp Hạ tầng Siêu hội tụ hàng đầu
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<h1 v-reveal="{ delay: 80 }"
|
|
21
|
+
class="mt-6 text-4xl font-extrabold tracking-tight text-slate-900 sm:text-5xl lg:text-[3.4rem] lg:leading-[1.1]">
|
|
22
|
+
Proxmox HCI —
|
|
23
|
+
<span class="block text-blue-600">Hạ tầng siêu hội tụ</span>
|
|
24
|
+
toàn diện
|
|
25
|
+
</h1>
|
|
26
|
+
|
|
27
|
+
<p v-reveal="{ delay: 160 }" class="mt-6 max-w-xl text-lg leading-relaxed text-slate-600">
|
|
28
|
+
Tích hợp KVM, Ceph Storage, SDN và HA Cluster vào một nền tảng duy nhất. Giải pháp hoàn hảo giúp doanh
|
|
29
|
+
nghiệp tối ưu chi phí, nâng cao hiệu suất và đảm bảo tính sẵn sàng cao.
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
<a v-reveal="{ delay: 220 }" href="#lien-he" @click.prevent="scrollTo('lien-he')"
|
|
33
|
+
class="mt-5 inline-flex items-center gap-2 text-[15px] font-medium text-blue-600 hover:text-blue-700">
|
|
34
|
+
<ProxmoxAppIcon name="calendar" class="h-4 w-4" />
|
|
35
|
+
Triển khai dễ dàng, mở rộng linh hoạt. Nhận tư vấn giải pháp từ chuyên gia Long Vân.
|
|
36
|
+
</a>
|
|
37
|
+
|
|
38
|
+
<div v-reveal="{ delay: 280 }" class="mt-8 flex flex-wrap items-center gap-4">
|
|
39
|
+
<a href="#lien-he" @click.prevent="scrollTo('lien-he')"
|
|
40
|
+
class="inline-flex items-center gap-2 rounded-[10px] bg-blue-600 px-6 py-3.5 text-[15px] font-semibold text-white shadow-sm shadow-blue-600/20 transition hover:bg-blue-700">
|
|
41
|
+
Nhận tư vấn triển khai
|
|
42
|
+
<ProxmoxAppIcon name="arrowRight" class="h-4 w-4" />
|
|
43
|
+
</a>
|
|
44
|
+
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<dl class="mt-9 grid grid-cols-2 border-t border-slate-200 pt-6 sm:grid-cols-4">
|
|
48
|
+
<div v-for="(stat, i) in heroStats" :key="stat.label" class="px-3 first:pl-0 sm:px-5" :class="[
|
|
49
|
+
i === 2 ? 'mt-6 sm:mt-0' : '',
|
|
50
|
+
i === 3 ? 'mt-6 sm:mt-0' : '',
|
|
51
|
+
i > 0 ? 'sm:border-l sm:border-slate-200' : ''
|
|
52
|
+
]">
|
|
53
|
+
<dt class="text-2xl font-extrabold tracking-tight text-slate-900">
|
|
54
|
+
{{ stat.value }}
|
|
55
|
+
</dt>
|
|
56
|
+
|
|
57
|
+
<dd class="mt-1 max-w-[110px] text-xs leading-5 text-slate-500">
|
|
58
|
+
{{ stat.label }}
|
|
59
|
+
</dd>
|
|
60
|
+
</div>
|
|
61
|
+
</dl>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- Right: image with floating cards -->
|
|
65
|
+
<div v-reveal="{ type: 'right', delay: 160 }" class="relative flex h-full items-center">
|
|
66
|
+
<div class="w-full overflow-hidden rounded-3xl shadow-2xl shadow-blue-900/10 ring-1 ring-black/5">
|
|
67
|
+
<img src="../../images/Proxmox HCI.png" alt="Datacenter server racks"
|
|
68
|
+
class="block h-auto w-full object-contain" />
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</section>
|
|
74
|
+
</template>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { testimonials } from "../../data/proxmox-content"
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<section id="khach-hang" class="bg-white py-20 sm:py-28">
|
|
7
|
+
<div class="mx-auto max-w-6xl px-6 lg:px-8">
|
|
8
|
+
<div class="mx-auto max-w-3xl text-center">
|
|
9
|
+
<div v-reveal class="inline-flex items-center rounded-full bg-blue-50 px-4 py-1.5 text-sm font-medium text-blue-600 ring-1 ring-inset ring-blue-100">
|
|
10
|
+
Học viên nói gì
|
|
11
|
+
</div>
|
|
12
|
+
<h2 v-reveal="{ delay: 60 }" class="mt-5 text-3xl font-extrabold tracking-tight text-slate-900 sm:text-4xl">
|
|
13
|
+
Được tư vấn đúng khóa,
|
|
14
|
+
<span class="text-blue-600">học đúng thứ cần dùng</span>
|
|
15
|
+
</h2>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="mt-14 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
19
|
+
<figure
|
|
20
|
+
v-for="(t, i) in testimonials"
|
|
21
|
+
:key="t.name + i"
|
|
22
|
+
v-reveal="{ delay: 100 + i * 90 }"
|
|
23
|
+
class="rounded-2xl border border-slate-200 bg-white p-7"
|
|
24
|
+
>
|
|
25
|
+
<div class="flex gap-0.5 text-amber-400">
|
|
26
|
+
<ProxmoxAppIcon v-for="n in 5" :key="n" name="star" class="h-4 w-4 fill-current" />
|
|
27
|
+
</div>
|
|
28
|
+
<blockquote class="mt-4 text-[15px] italic leading-relaxed text-slate-600">
|
|
29
|
+
“{{ t.quote }}”
|
|
30
|
+
</blockquote>
|
|
31
|
+
<figcaption class="mt-6 flex items-center gap-3 border-t border-slate-100 pt-5">
|
|
32
|
+
<span class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-blue-100 text-xs font-bold text-blue-600">
|
|
33
|
+
HV
|
|
34
|
+
</span>
|
|
35
|
+
<div class="text-sm">
|
|
36
|
+
<p class="font-semibold text-slate-900">{{ t.name }}</p>
|
|
37
|
+
<p class="text-slate-500">{{ t.role }}</p>
|
|
38
|
+
</div>
|
|
39
|
+
</figcaption>
|
|
40
|
+
</figure>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</section>
|
|
44
|
+
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { trainingBenefits } from "../../data/proxmox-content"
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<section class="bg-[#F7F9FB] py-20 sm:py-28">
|
|
7
|
+
<div class="mx-auto max-w-6xl px-6 lg:px-8">
|
|
8
|
+
<div class="mx-auto max-w-3xl text-center">
|
|
9
|
+
<div v-reveal class="inline-flex items-center rounded-full bg-blue-50 px-4 py-1.5 text-sm font-medium text-blue-600 ring-1 ring-inset ring-blue-100">
|
|
10
|
+
Lợi ích đào tạo
|
|
11
|
+
</div>
|
|
12
|
+
<h2 v-reveal="{ delay: 60 }" class="mt-5 text-3xl font-extrabold tracking-tight text-slate-900 sm:text-4xl">
|
|
13
|
+
Vì sao nên tham gia
|
|
14
|
+
<span class="text-blue-600">đào tạo Proxmox VE?</span>
|
|
15
|
+
</h2>
|
|
16
|
+
<p v-reveal="{ delay: 120 }" class="mt-5 leading-relaxed text-slate-600">
|
|
17
|
+
Đầu tư một lần vào đào tạo bài bản giúp bạn tối ưu năng lực vận hành, cắt giảm chi phí hạ tầng và tạo bước đệm bứt phá trên con đường sự nghiệp.
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="mt-14 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
|
22
|
+
<div
|
|
23
|
+
v-for="(item, i) in trainingBenefits"
|
|
24
|
+
:key="item.title"
|
|
25
|
+
v-reveal="{ delay: 100 + i * 70 }"
|
|
26
|
+
class="rounded-2xl border border-slate-200 bg-white p-7 transition hover:-translate-y-1 hover:border-blue-200 hover:shadow-lg hover:shadow-blue-900/5"
|
|
27
|
+
>
|
|
28
|
+
<span class="flex h-11 w-11 items-center justify-center rounded-xl bg-blue-50 text-blue-600">
|
|
29
|
+
<ProxmoxAppIcon :name="item.icon" class="h-5 w-5" />
|
|
30
|
+
</span>
|
|
31
|
+
<p class="mt-4 font-semibold text-slate-900">{{ item.title }}</p>
|
|
32
|
+
<p class="mt-2 text-sm leading-relaxed text-slate-500">{{ item.desc }}</p>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</section>
|
|
37
|
+
</template>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { veFeatureList, veFeatureCards } from "../../data/proxmox-content"
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<section id="giai-phap" class="bg-white mx-auto max-w-7xl px-6 py-20 sm:py-28 lg:px-8">
|
|
7
|
+
<div class="grid grid-cols-1 items-start gap-16 lg:grid-cols-2">
|
|
8
|
+
<!-- Left copy -->
|
|
9
|
+
<div>
|
|
10
|
+
<div v-reveal class="inline-flex items-center rounded-full bg-blue-50 px-4 py-1.5 text-sm font-medium text-blue-600 ring-1 ring-inset ring-blue-100">
|
|
11
|
+
Nền tảng ảo hóa
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<h2 v-reveal="{ delay: 60 }" class="mt-5 text-3xl font-extrabold tracking-tight text-slate-900 sm:text-4xl sm:leading-tight">
|
|
15
|
+
Proxmox VE — Nền tảng ảo hóa
|
|
16
|
+
<span class="text-blue-600">hàng đầu thế giới</span>
|
|
17
|
+
</h2>
|
|
18
|
+
|
|
19
|
+
<p v-reveal="{ delay: 120 }" class="mt-5 leading-relaxed text-slate-600">
|
|
20
|
+
Proxmox VE là nền tảng ảo hóa mã nguồn mở mạnh mẽ, cho phép doanh nghiệp triển khai và quản lý VM & Container tập trung trên một giao diện web duy nhất. Được hàng nghìn doanh nghiệp toàn cầu tin dùng thay thế VMware vSphere với chi phí gần như bằng 0.
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<ul class="mt-7 space-y-4">
|
|
24
|
+
<li
|
|
25
|
+
v-for="(item, i) in veFeatureList"
|
|
26
|
+
:key="item.text"
|
|
27
|
+
v-reveal="{ delay: 160 + i * 60 }"
|
|
28
|
+
class="flex items-center gap-3"
|
|
29
|
+
>
|
|
30
|
+
<span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blue-50 text-blue-600">
|
|
31
|
+
<ProxmoxAppIcon :name="item.icon" class="h-4 w-4" />
|
|
32
|
+
</span>
|
|
33
|
+
<span class="text-[15px] text-slate-700">{{ item.text }}</span>
|
|
34
|
+
</li>
|
|
35
|
+
</ul>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<!-- Right card grid -->
|
|
39
|
+
<div class="grid grid-cols-2 gap-5">
|
|
40
|
+
<div
|
|
41
|
+
v-for="(card, i) in veFeatureCards"
|
|
42
|
+
:key="card.title"
|
|
43
|
+
v-reveal="{ type: 'scale', delay: 100 + i * 70 }"
|
|
44
|
+
class="rounded-2xl border border-slate-200 bg-slate-50/60 p-6 transition hover:-translate-y-1 hover:border-blue-200 hover:bg-white hover:shadow-lg hover:shadow-blue-900/5"
|
|
45
|
+
>
|
|
46
|
+
<span class="flex h-11 w-11 items-center justify-center rounded-xl bg-white text-blue-600 shadow-sm ring-1 ring-slate-200">
|
|
47
|
+
<ProxmoxAppIcon :name="card.icon" class="h-5 w-5" />
|
|
48
|
+
</span>
|
|
49
|
+
<p class="mt-4 font-semibold text-slate-900">{{ card.title }}</p>
|
|
50
|
+
<p class="mt-1 text-sm text-slate-500">{{ card.desc }}</p>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</section>
|
|
55
|
+
</template>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function useActiveSection(sectionIds: string[]) {
|
|
2
|
+
const activeSection = ref(sectionIds[0] || '')
|
|
3
|
+
|
|
4
|
+
onMounted(() => {
|
|
5
|
+
const sections = sectionIds
|
|
6
|
+
.map(id => document.getElementById(id))
|
|
7
|
+
.filter(Boolean) as HTMLElement[]
|
|
8
|
+
|
|
9
|
+
if (!sections.length) return
|
|
10
|
+
|
|
11
|
+
function update() {
|
|
12
|
+
let maxVisibleHeight = 0
|
|
13
|
+
let maxId = activeSection.value
|
|
14
|
+
|
|
15
|
+
const viewportHeight = window.innerHeight
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < sections.length; i++) {
|
|
18
|
+
const el = sections[i]
|
|
19
|
+
const rect = el.getBoundingClientRect()
|
|
20
|
+
|
|
21
|
+
const visibleTop = Math.max(0, rect.top)
|
|
22
|
+
const visibleBottom = Math.min(viewportHeight, rect.bottom)
|
|
23
|
+
const visibleHeight = Math.max(0, visibleBottom - visibleTop)
|
|
24
|
+
|
|
25
|
+
if (visibleHeight > maxVisibleHeight) {
|
|
26
|
+
maxVisibleHeight = visibleHeight
|
|
27
|
+
maxId = el.id
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (window.scrollY < 50 && sections[0]) {
|
|
32
|
+
activeSection.value = sections[0].id
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (maxVisibleHeight > 0) {
|
|
37
|
+
activeSection.value = maxId
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
update()
|
|
42
|
+
window.addEventListener('scroll', update, { passive: true })
|
|
43
|
+
onUnmounted(() => window.removeEventListener('scroll', update))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return activeSection
|
|
47
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
export const useAuthApi = () => {
|
|
2
|
+
const { $sdk } = useNuxtApp()
|
|
3
|
+
|
|
4
|
+
// ==========================
|
|
5
|
+
// LOGIN / REGISTER
|
|
6
|
+
// ==========================
|
|
7
|
+
|
|
8
|
+
const login = (data: {
|
|
9
|
+
username: string
|
|
10
|
+
password: string
|
|
11
|
+
type?: string
|
|
12
|
+
otpCode?: string
|
|
13
|
+
}) => {
|
|
14
|
+
return $sdk.auth.login(data)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const register = (data: {
|
|
18
|
+
username: string
|
|
19
|
+
fullName: string
|
|
20
|
+
}) => {
|
|
21
|
+
return $sdk.auth.register(data as any)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const logout = (accessToken: string, partyId: string) => {
|
|
25
|
+
return $sdk.auth.logout(accessToken, partyId)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const getUserByToken = (token: string) => {
|
|
29
|
+
return $sdk.auth.getUserDetail(token)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const getUserLoginByToken = (token: string) => {
|
|
33
|
+
return $sdk.auth.getUserLoginByToken(token)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const updateInfo = (
|
|
37
|
+
accessToken: string,
|
|
38
|
+
updateUserRequest: { fullName: string; phone: string; email?: string },
|
|
39
|
+
type: string,
|
|
40
|
+
password: string
|
|
41
|
+
) => {
|
|
42
|
+
return $sdk.auth.updateInfo(accessToken, updateUserRequest, type, password)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const getAccessTokenByOTP = (
|
|
46
|
+
code: string,
|
|
47
|
+
phone: string,
|
|
48
|
+
method: string
|
|
49
|
+
) => {
|
|
50
|
+
return $sdk.auth.getAccessTokenByOTP(code, phone, method)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ==========================
|
|
54
|
+
// USER LOGIN
|
|
55
|
+
// ==========================
|
|
56
|
+
|
|
57
|
+
const getUserLoginByUserLoginId = (userLoginId: string) => {
|
|
58
|
+
return $sdk.auth.getUserLoginByUserLoginId(userLoginId)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const checkUsernameExisted = (username: string) => {
|
|
62
|
+
return $sdk.auth.checkUsernameExisted(username)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const createUserLogin = (phone: string) => {
|
|
66
|
+
return $sdk.auth.createUserLogin(phone)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const linkingUserLoginAndUserDetail = (
|
|
70
|
+
userLoginId: string,
|
|
71
|
+
partyId: string
|
|
72
|
+
) => {
|
|
73
|
+
return $sdk.auth.linkingUserLoginAndUserDetail(
|
|
74
|
+
userLoginId,
|
|
75
|
+
partyId
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const createUserDetail = (userLoginId: string) => {
|
|
80
|
+
return $sdk.auth.createUserDetail(userLoginId)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ==========================
|
|
84
|
+
// OTP
|
|
85
|
+
// ==========================
|
|
86
|
+
|
|
87
|
+
const sendOTP = (
|
|
88
|
+
phone: string,
|
|
89
|
+
method: "sms" | "zalo"
|
|
90
|
+
) => {
|
|
91
|
+
return $sdk.auth.sendOTP(phone, method)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const validateOTP = (
|
|
95
|
+
code: string,
|
|
96
|
+
phone: string,
|
|
97
|
+
method: string
|
|
98
|
+
) => {
|
|
99
|
+
return $sdk.auth.validateOTP(code, phone, method)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ==========================
|
|
103
|
+
// RESET PASSWORD
|
|
104
|
+
// ==========================
|
|
105
|
+
|
|
106
|
+
const createResetKey = (userLoginId: string) => {
|
|
107
|
+
return $sdk.auth.createResetKey(userLoginId)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const checkResetKey = (key: string) => {
|
|
111
|
+
return $sdk.auth.checkResetKey(key)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const resetPassword = (
|
|
115
|
+
username: string,
|
|
116
|
+
newPassword: string,
|
|
117
|
+
accessToken: string
|
|
118
|
+
) => {
|
|
119
|
+
return $sdk.auth.resetPassword(username, newPassword, accessToken)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ==========================
|
|
123
|
+
// USER
|
|
124
|
+
// ==========================
|
|
125
|
+
|
|
126
|
+
const updateCustomer = (
|
|
127
|
+
partyId: string,
|
|
128
|
+
body: any,
|
|
129
|
+
updatedBy: string
|
|
130
|
+
) => {
|
|
131
|
+
return $sdk.user.updateCustomer(
|
|
132
|
+
partyId,
|
|
133
|
+
body,
|
|
134
|
+
updatedBy
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const updateProfile = (
|
|
139
|
+
userLoginId: string,
|
|
140
|
+
fullName: string,
|
|
141
|
+
phone: string,
|
|
142
|
+
email: string
|
|
143
|
+
) => {
|
|
144
|
+
return $sdk.auth.updateProfile(
|
|
145
|
+
userLoginId,
|
|
146
|
+
fullName,
|
|
147
|
+
phone,
|
|
148
|
+
email
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const getPhoneByPartyId = (partyId: string) => {
|
|
153
|
+
return $sdk.auth.getPhoneByPartyId(partyId)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ==========================
|
|
157
|
+
// SOCIAL LOGIN
|
|
158
|
+
// ==========================
|
|
159
|
+
|
|
160
|
+
const loginGoogle = (redirectUrl: string, redirectUrlError: string, serviceOperator: string, appId: string, deviceId: string) => {
|
|
161
|
+
return $sdk.auth.loginGoogle(redirectUrl, redirectUrlError, serviceOperator, appId, deviceId)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const loginFacebook = (redirectUrl: string, redirectUrlError: string, serviceOperator: string, appId: string, deviceId: string) => {
|
|
165
|
+
return $sdk.auth.loginFacebook(redirectUrl, redirectUrlError, serviceOperator, appId, deviceId)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const loginZalo = (redirectUrl: string, redirectUrlError: string, serviceOperator: string, appId: string, deviceId: string) => {
|
|
169
|
+
return $sdk.auth.loginZalo(redirectUrl, redirectUrlError, serviceOperator, appId, deviceId)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ==========================
|
|
173
|
+
// ORDER
|
|
174
|
+
// ==========================
|
|
175
|
+
|
|
176
|
+
const setOrderToken = (token: string) => {
|
|
177
|
+
return $sdk.order.setToken(token)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const findCampaignActionGiftVoucher = () => {
|
|
181
|
+
return $sdk.order.findCampaignActionGiftVoucher()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ==========================
|
|
185
|
+
// CRM CAMPAIGN
|
|
186
|
+
// ==========================
|
|
187
|
+
|
|
188
|
+
const setCampaignToken = (token: string) => {
|
|
189
|
+
return $sdk.crm_camping.setToken(token)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const addCustomerToVoucher = (
|
|
193
|
+
userId: string,
|
|
194
|
+
voucherCode: string
|
|
195
|
+
) => {
|
|
196
|
+
return $sdk.crm_camping.addCustomerToVoucher(
|
|
197
|
+
userId,
|
|
198
|
+
voucherCode
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
// login / register
|
|
204
|
+
login,
|
|
205
|
+
register,
|
|
206
|
+
logout,
|
|
207
|
+
getUserByToken,
|
|
208
|
+
getUserLoginByToken,
|
|
209
|
+
updateInfo,
|
|
210
|
+
getAccessTokenByOTP,
|
|
211
|
+
|
|
212
|
+
// user login
|
|
213
|
+
getUserLoginByUserLoginId,
|
|
214
|
+
checkUsernameExisted,
|
|
215
|
+
createUserLogin,
|
|
216
|
+
linkingUserLoginAndUserDetail,
|
|
217
|
+
createUserDetail,
|
|
218
|
+
|
|
219
|
+
// otp
|
|
220
|
+
sendOTP,
|
|
221
|
+
validateOTP,
|
|
222
|
+
|
|
223
|
+
// reset
|
|
224
|
+
createResetKey,
|
|
225
|
+
checkResetKey,
|
|
226
|
+
resetPassword,
|
|
227
|
+
|
|
228
|
+
// user
|
|
229
|
+
updateCustomer,
|
|
230
|
+
updateProfile,
|
|
231
|
+
getPhoneByPartyId,
|
|
232
|
+
|
|
233
|
+
// social
|
|
234
|
+
loginGoogle,
|
|
235
|
+
loginFacebook,
|
|
236
|
+
loginZalo,
|
|
237
|
+
|
|
238
|
+
// voucher
|
|
239
|
+
setOrderToken,
|
|
240
|
+
findCampaignActionGiftVoucher,
|
|
241
|
+
setCampaignToken,
|
|
242
|
+
addCustomerToVoucher,
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const useAuthState = () => {
|
|
2
|
+
const isLoggedIn = useState('auth-logged-in', () => false)
|
|
3
|
+
const userName = useState('auth-user-name', () => '')
|
|
4
|
+
|
|
5
|
+
function setUser(name: string) {
|
|
6
|
+
isLoggedIn.value = true
|
|
7
|
+
userName.value = name
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function clearUser() {
|
|
11
|
+
isLoggedIn.value = false
|
|
12
|
+
userName.value = ''
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return { isLoggedIn, userName, setUser, clearUser }
|
|
16
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract root domain from hostname.
|
|
3
|
+
* e.g. "dev.longvan.net" → ".longvan.net"
|
|
4
|
+
* "longvan.net" → ".longvan.net"
|
|
5
|
+
* "localhost" → "localhost"
|
|
6
|
+
*/
|
|
7
|
+
function getCookieDomain(): string {
|
|
8
|
+
if (typeof window === 'undefined') return ''
|
|
9
|
+
const hostname = window.location.hostname
|
|
10
|
+
|
|
11
|
+
// localhost / IP → không set domain (browser tự xử lý)
|
|
12
|
+
if (hostname === 'localhost' || /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
|
|
13
|
+
return hostname
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const parts = hostname.split('.')
|
|
17
|
+
// Lấy 2 phần cuối (root domain): longvan.net, example.com, ...
|
|
18
|
+
if (parts.length >= 2) {
|
|
19
|
+
return '.' + parts.slice(-2).join('.')
|
|
20
|
+
}
|
|
21
|
+
return hostname
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cookie helper composable — set, get, delete cookies theo domain hiện tại.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* const { setCookie, getCookie, deleteCookie } = useCookie()
|
|
29
|
+
* setCookie('token', 'abc123', 7) // expires in 7 days
|
|
30
|
+
* getCookie('token') // → 'abc123'
|
|
31
|
+
* deleteCookie('token')
|
|
32
|
+
*/
|
|
33
|
+
export function useCookie() {
|
|
34
|
+
/**
|
|
35
|
+
* Lưu cookie.
|
|
36
|
+
* @param key - Tên cookie
|
|
37
|
+
* @param value - Giá trị cookie
|
|
38
|
+
* @param days - Số ngày hết hạn (mặc định 365)
|
|
39
|
+
*/
|
|
40
|
+
function setCookie(key: string, value: string, days = 365): void {
|
|
41
|
+
const date = new Date()
|
|
42
|
+
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
|
|
43
|
+
const expires = 'expires=' + date.toUTCString()
|
|
44
|
+
const domain = getCookieDomain()
|
|
45
|
+
const domainPart = domain ? `; domain=${domain}` : ''
|
|
46
|
+
document.cookie = `${key}=${encodeURIComponent(value)}; ${expires}; path=/${domainPart}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Lấy giá trị cookie theo key.
|
|
51
|
+
* @param key - Tên cookie
|
|
52
|
+
* @returns Giá trị cookie hoặc null nếu không tìm thấy
|
|
53
|
+
*/
|
|
54
|
+
function getCookie(key: string): string | null {
|
|
55
|
+
const name = key + '='
|
|
56
|
+
const decodedCookie = decodeURIComponent(document.cookie)
|
|
57
|
+
const cookies = decodedCookie.split(';')
|
|
58
|
+
|
|
59
|
+
for (let cookie of cookies) {
|
|
60
|
+
cookie = cookie.trim()
|
|
61
|
+
if (cookie.startsWith(name)) {
|
|
62
|
+
return cookie.substring(name.length)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Xóa cookie theo key.
|
|
70
|
+
* @param key - Tên cookie cần xóa
|
|
71
|
+
*/
|
|
72
|
+
function deleteCookie(key: string): void {
|
|
73
|
+
const domain = getCookieDomain()
|
|
74
|
+
const domainPart = domain ? `; domain=${domain}` : ''
|
|
75
|
+
document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/${domainPart}`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { setCookie, getCookie, deleteCookie }
|
|
79
|
+
}
|