nuxt-layer-core 2.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 +246 -0
- package/app/layers/solution/app/components/CtaSection.vue +329 -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 +434 -0
- package/app/layers/solution/app/components/RegistrationForm.vue +256 -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 +189 -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/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/useScrollAnimate.ts +40 -0
- package/app/layers/solution/app/composables/useScrollToSection.ts +13 -0
- package/app/layers/solution/app/composables/useSolutionApi.ts +55 -0
- package/app/layers/solution/app/composables/useToast.ts +14 -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 +9 -0
- package/app/layers/solution/app/pages/cap-nhat-thong-tin/index.vue +302 -0
- package/app/layers/solution/app/pages/dang-ky/index.vue +627 -0
- package/app/layers/solution/app/pages/dang-nhap/index.vue +208 -0
- package/app/layers/solution/app/pages/doi-mat-khau/index.vue +237 -0
- package/app/layers/solution/app/pages/giai-phap-proxmox/[slug]/index.vue +37 -0
- package/app/layers/solution/app/pages/giai-phap-proxmox/[slug]/proxmox-hci/index.vue +45 -0
- package/app/layers/solution/app/pages/giai-phap-proxmox/dang-ky-khoa-hoc/[slug].vue +131 -0
- package/app/layers/solution/app/pages/quen-mat-khau/index.vue +136 -0
- package/app/layers/solution/app/pages/xac-thuc/index.vue +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 +9 -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 +104 -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
- package/website-longvan-1.1.6.tgz +0 -0
- package/website-longvan-1.1.7.tgz +0 -0
- package/website-longvan-1.1.8.tgz +0 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { faqItems } from "../../data/proxmox-content"
|
|
3
|
+
|
|
4
|
+
const openIndex = ref<number | null>(0)
|
|
5
|
+
|
|
6
|
+
function toggle(i: number) {
|
|
7
|
+
openIndex.value = openIndex.value === i ? null : i
|
|
8
|
+
}
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<section class="bg-[#F7F9FB] py-20 sm:py-28">
|
|
13
|
+
<div class="mx-auto max-w-3xl px-6 lg:px-8">
|
|
14
|
+
<div class="text-center">
|
|
15
|
+
<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">
|
|
16
|
+
FAQ
|
|
17
|
+
</div>
|
|
18
|
+
<h2 v-reveal="{ delay: 60 }" class="mt-5 text-3xl font-extrabold tracking-tight text-slate-900 sm:text-4xl">
|
|
19
|
+
Câu hỏi thường gặp
|
|
20
|
+
</h2>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="mt-12 space-y-3">
|
|
24
|
+
<div
|
|
25
|
+
v-for="(item, i) in faqItems"
|
|
26
|
+
:key="item.question"
|
|
27
|
+
v-reveal="{ delay: 80 + i * 60 }"
|
|
28
|
+
class="overflow-hidden rounded-xl border border-slate-200 bg-white"
|
|
29
|
+
>
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
class="flex w-full items-center justify-between gap-4 px-6 py-4 text-left text-[15px] font-semibold text-slate-800"
|
|
33
|
+
:aria-expanded="openIndex === i"
|
|
34
|
+
@click="toggle(i)"
|
|
35
|
+
>
|
|
36
|
+
{{ item.question }}
|
|
37
|
+
<ProxmoxAppIcon
|
|
38
|
+
name="chevronDown"
|
|
39
|
+
class="h-4 w-4 shrink-0 text-slate-400 transition-transform duration-300"
|
|
40
|
+
:class="{ 'rotate-180 text-blue-600': openIndex === i }"
|
|
41
|
+
/>
|
|
42
|
+
</button>
|
|
43
|
+
|
|
44
|
+
<div
|
|
45
|
+
class="grid transition-all duration-300 ease-in-out"
|
|
46
|
+
:style="{ gridTemplateRows: openIndex === i ? '1fr' : '0fr' }"
|
|
47
|
+
>
|
|
48
|
+
<div class="overflow-hidden">
|
|
49
|
+
<p class="px-6 pb-5 text-sm leading-relaxed text-slate-500">
|
|
50
|
+
{{ item.answer }}
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</section>
|
|
58
|
+
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { hciOverviewCards } 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
|
+
HCI là 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
|
+
Proxmox HCI — Hạ tầng hội tụ
|
|
14
|
+
<span class="text-blue-600">dựa trên Proxmox VE</span>
|
|
15
|
+
</h2>
|
|
16
|
+
<p v-reveal="{ delay: 120 }" class="mt-5 leading-relaxed text-slate-600">
|
|
17
|
+
HCI (Hyper-Converged Infrastructure) hợp nhất compute, storage và network vào một platform phần mềm duy nhất — loại bỏ SAN riêng biệt, đơn giản hóa vận hành và giảm chi phí đáng kể.
|
|
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="(card, i) in hciOverviewCards"
|
|
24
|
+
:key="card.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="card.icon" class="h-5 w-5" />
|
|
30
|
+
</span>
|
|
31
|
+
<p class="mt-4 font-semibold text-slate-900">{{ card.title }}</p>
|
|
32
|
+
<p class="mt-2 text-sm leading-relaxed text-slate-500">{{ card.desc }}</p>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</section>
|
|
37
|
+
</template>
|
|
@@ -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
|
+
}
|