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,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
plans: Array<{
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
level: string
|
|
7
|
+
duration: string
|
|
8
|
+
icon: string
|
|
9
|
+
}>
|
|
10
|
+
selectedPlanId?: string
|
|
11
|
+
}>()
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div class="min-h-screen bg-[#f4f5f9] px-6 py-[7rem] font-sans text-slate-900">
|
|
16
|
+
<div class="mx-auto grid max-w-[1000px] grid-cols-1 items-start gap-6 md:grid-cols-[340px_1fr]">
|
|
17
|
+
<!-- Left column -->
|
|
18
|
+
<RegistrationSidebar />
|
|
19
|
+
|
|
20
|
+
<!-- Right column: form -->
|
|
21
|
+
<RegistrationForm :plans="plans" :selected-plan-id="selectedPlanId" />
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<aside class="flex flex-col gap-4">
|
|
3
|
+
<div class="rounded-2xl border border-[#e7e9f0] bg-white p-7">
|
|
4
|
+
<h2 class="mb-5 text-[17px] font-bold text-slate-900">Vì sao chọn chúng tôi</h2>
|
|
5
|
+
<ul class="flex flex-col gap-[22px]">
|
|
6
|
+
<li class="flex items-start gap-3">
|
|
7
|
+
<span
|
|
8
|
+
class="flex h-[34px] w-[34px] flex-none items-center justify-center rounded-[10px] bg-[#e8eefc] text-[#2f5fe0]">
|
|
9
|
+
<svg class="h-[17px] w-[17px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
|
|
10
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
11
|
+
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
|
12
|
+
<circle cx="9" cy="7" r="4" />
|
|
13
|
+
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
|
14
|
+
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
|
15
|
+
</svg>
|
|
16
|
+
</span>
|
|
17
|
+
<span class="flex flex-col gap-[3px]">
|
|
18
|
+
<span class="text-sm font-semibold text-slate-900">Lớp nhỏ, kèm sát</span>
|
|
19
|
+
<span class="text-[13px] leading-relaxed text-gray-500">Sĩ số giới hạn, giảng viên hỗ trợ trực tiếp
|
|
20
|
+
từng học viên.</span>
|
|
21
|
+
</span>
|
|
22
|
+
</li>
|
|
23
|
+
<li class="flex items-start gap-3">
|
|
24
|
+
<span
|
|
25
|
+
class="flex h-[34px] w-[34px] flex-none items-center justify-center rounded-[10px] bg-[#e8eefc] text-[#2f5fe0]">
|
|
26
|
+
<svg class="h-[17px] w-[17px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
|
|
27
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
28
|
+
<path d="M22 10 12 5 2 10l10 5 10-5Z" />
|
|
29
|
+
<path d="M6 12v5c0 1.66 2.69 3 6 3s6-1.34 6-3v-5" />
|
|
30
|
+
</svg>
|
|
31
|
+
</span>
|
|
32
|
+
<span class="flex flex-col gap-[3px]">
|
|
33
|
+
<span class="text-sm font-semibold text-slate-900">Thực hành hệ thống thật</span>
|
|
34
|
+
<span class="text-[13px] leading-relaxed text-gray-500">Thao tác trên cụm Proxmox HCI thực tế, không
|
|
35
|
+
chỉ lý thuyết.</span>
|
|
36
|
+
</span>
|
|
37
|
+
</li>
|
|
38
|
+
<li class="flex items-start gap-3">
|
|
39
|
+
<span
|
|
40
|
+
class="flex h-[34px] w-[34px] flex-none items-center justify-center rounded-[10px] bg-[#e8eefc] text-[#2f5fe0]">
|
|
41
|
+
<svg class="h-[17px] w-[17px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
|
|
42
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
43
|
+
<circle cx="12" cy="12" r="10" />
|
|
44
|
+
<path d="m9 12 2 2 4-4" />
|
|
45
|
+
</svg>
|
|
46
|
+
</span>
|
|
47
|
+
<span class="flex flex-col gap-[3px]">
|
|
48
|
+
<span class="text-sm font-semibold text-slate-900">Chứng nhận hoàn thành</span>
|
|
49
|
+
<span class="text-[13px] leading-relaxed text-gray-500">Cấp chứng nhận và hỗ trợ tư vấn nghề nghiệp
|
|
50
|
+
sau khóa học.</span>
|
|
51
|
+
</span>
|
|
52
|
+
</li>
|
|
53
|
+
</ul>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="rounded-2xl border border-[#dbe4fd] bg-[#eef2ff] px-6 py-[22px]">
|
|
57
|
+
<p class="mb-2 flex items-center gap-[9px] text-sm font-bold text-[#2f5fe0]">
|
|
58
|
+
<svg class="h-[17px] w-[17px] flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
59
|
+
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
60
|
+
<path d="M23 7 16 12l7 5V7Z" />
|
|
61
|
+
<rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
|
|
62
|
+
</svg>
|
|
63
|
+
Khai giảng linh hoạt
|
|
64
|
+
</p>
|
|
65
|
+
<p class="text-[13px] leading-relaxed text-gray-600">
|
|
66
|
+
Lịch học buổi tối và cuối tuần, phù hợp cho cả người đi làm. Hỗ trợ ghi hình buổi học để xem lại.
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
</aside>
|
|
70
|
+
</template>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section id="khach-hang" class="py-24 bg-white border-t border-slate-200">
|
|
3
|
+
<div class="max-w-7xl mx-auto px-6 lg:px-8">
|
|
4
|
+
<div class="text-center mb-16">
|
|
5
|
+
<h2 class="text-[#004AC6] font-bold uppercase tracking-widest text-sm mb-2">Học viên nói gì</h2>
|
|
6
|
+
<h3 class="font-['Montserrat'] font-extrabold text-2xl sm:text-3xl text-slate-900">
|
|
7
|
+
Được tư vấn đúng khóa, học đúng thứ cần dùng
|
|
8
|
+
</h3>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
12
|
+
<blockquote
|
|
13
|
+
v-for="(t, i) in testimonials"
|
|
14
|
+
:key="t.name"
|
|
15
|
+
:data-delay="i * 150"
|
|
16
|
+
class="testimonial-card bg-white p-8 rounded-xl border border-slate-200 relative hover:shadow-lg transition-shadow"
|
|
17
|
+
>
|
|
18
|
+
<Icon name="lucide:quote" class="text-blue-100 text-4xl absolute top-6 right-6" />
|
|
19
|
+
<p class="text-slate-700 leading-relaxed mb-6 relative z-10">{{ t.quote }}</p>
|
|
20
|
+
<footer class="flex items-center gap-3">
|
|
21
|
+
<div class="w-10 h-10 rounded-[10px] bg-gradient-to-br from-[#2563EB] to-[#1D4ED8] flex items-center justify-center text-xs font-bold text-white">
|
|
22
|
+
{{ t.initials }}
|
|
23
|
+
</div>
|
|
24
|
+
<div class="text-sm">
|
|
25
|
+
<p class="font-bold text-slate-900">{{ t.name }}</p>
|
|
26
|
+
<p class="text-slate-500">{{ t.role }}</p>
|
|
27
|
+
</div>
|
|
28
|
+
</footer>
|
|
29
|
+
</blockquote>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</section>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup>
|
|
36
|
+
useScrollAnimate('.testimonial-card', 'slide-left')
|
|
37
|
+
|
|
38
|
+
const testimonials = [
|
|
39
|
+
{
|
|
40
|
+
initials: 'TH',
|
|
41
|
+
name: 'Trần Hải',
|
|
42
|
+
role: 'IT Manager, chuỗi bán lẻ 20 chi nhánh',
|
|
43
|
+
quote: 'Ban đầu định đăng ký gói cao nhất, nhưng buổi tư vấn cho thấy đội em chỉ cần gói Nền tảng. Tiết kiệm được kha khá chi phí mà vẫn đạt mục tiêu.'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
initials: 'NL',
|
|
47
|
+
name: 'Nguyễn Lâm',
|
|
48
|
+
role: 'DevOps Lead, công ty fintech',
|
|
49
|
+
quote: 'Người tư vấn hỏi rất kỹ về hạ tầng hiện tại trước khi gợi ý khóa học, nên nội dung học sát với công việc thật, không lan man.'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
initials: 'PA',
|
|
53
|
+
name: 'Phạm Anh',
|
|
54
|
+
role: 'CTO, startup logistics',
|
|
55
|
+
quote: 'Nhờ được tư vấn đúng lộ trình, công ty chuyển từ VMware sang cluster HCI trên Proxmox và cắt gần 60% chi phí license hàng năm.'
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
</script>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<footer class="bg-white border-t border-slate-200 pt-16 pb-10">
|
|
3
|
+
<div class="max-w-7xl mx-auto px-6 lg:px-8 grid grid-cols-1 md:grid-cols-4 gap-10">
|
|
4
|
+
<div class="space-y-4">
|
|
5
|
+
<div class="flex items-center">
|
|
6
|
+
<img src="/images/logo-lvs-1300x330.png" alt="Long Vân" class="h-8 w-auto" />
|
|
7
|
+
</div>
|
|
8
|
+
<p class="text-slate-500 text-sm leading-relaxed">
|
|
9
|
+
Tư vấn và đào tạo hạ tầng siêu hội tụ Proxmox. Đồng hành cùng bạn chọn đúng khóa học,
|
|
10
|
+
đúng lộ trình nghề nghiệp.
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div>
|
|
15
|
+
<h5 class="font-bold text-slate-900 mb-5 text-sm">Khóa học</h5>
|
|
16
|
+
<ul class="space-y-3">
|
|
17
|
+
<li><NuxtLink to="#dao-tao" @click.prevent="scrollTo('dao-tao')" class="text-slate-500 hover:text-[#004AC6] transition-colors text-sm">Proxmox nền tảng</NuxtLink></li>
|
|
18
|
+
<li><NuxtLink to="#dao-tao" @click.prevent="scrollTo('dao-tao')" class="text-slate-500 hover:text-[#004AC6] transition-colors text-sm">Vận hành production</NuxtLink></li>
|
|
19
|
+
<li><NuxtLink to="#dao-tao" @click.prevent="scrollTo('dao-tao')" class="text-slate-500 hover:text-[#004AC6] transition-colors text-sm">Đào tạo doanh nghiệp</NuxtLink></li>
|
|
20
|
+
</ul>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div>
|
|
24
|
+
<h5 class="font-bold text-slate-900 mb-5 text-sm">Tư vấn</h5>
|
|
25
|
+
<ul class="space-y-3">
|
|
26
|
+
<li><NuxtLink to="#giai-phap" @click.prevent="scrollTo('giai-phap')" class="text-slate-500 hover:text-[#004AC6] transition-colors text-sm">Quy trình tư vấn</NuxtLink></li>
|
|
27
|
+
<li><NuxtLink to="#khach-hang" @click.prevent="scrollTo('khach-hang')" class="text-slate-500 hover:text-[#004AC6] transition-colors text-sm">Học viên nói gì</NuxtLink></li>
|
|
28
|
+
<li><NuxtLink to="#lien-he" @click.prevent="scrollTo('lien-he')" class="text-slate-500 hover:text-[#004AC6] transition-colors text-sm">Đặt lịch tư vấn</NuxtLink></li>
|
|
29
|
+
</ul>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div>
|
|
33
|
+
<h5 class="font-bold text-slate-900 mb-5 text-sm">Liên hệ</h5>
|
|
34
|
+
<ul class="space-y-3 text-sm text-slate-500">
|
|
35
|
+
<li class="flex items-center gap-2">
|
|
36
|
+
<Icon name="lucide:mail" class="text-[16px] text-[#004AC6]" />
|
|
37
|
+
tuvan@longvan.net
|
|
38
|
+
</li>
|
|
39
|
+
<li class="flex items-center gap-2">
|
|
40
|
+
<Icon name="lucide:phone" class="text-[16px] text-[#004AC6]" />
|
|
41
|
+
1800 6070
|
|
42
|
+
</li>
|
|
43
|
+
</ul>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="max-w-7xl mx-auto px-6 lg:px-8 mt-12 pt-8 border-t border-slate-200 flex flex-col sm:flex-row justify-between items-center gap-4">
|
|
48
|
+
<p class="text-slate-400 text-xs">© {{ year }} Long Vân Cloud Solution. Bảo lưu mọi quyền.</p>
|
|
49
|
+
<div class="flex gap-6 text-xs text-slate-500">
|
|
50
|
+
<NuxtLink to="#giai-phap" @click.prevent="scrollTo('giai-phap')" class="hover:text-[#004AC6] transition-colors">Vì sao chọn chúng tôi</NuxtLink>
|
|
51
|
+
<NuxtLink to="#dao-tao" @click.prevent="scrollTo('dao-tao')" class="hover:text-[#004AC6] transition-colors">Khóa học</NuxtLink>
|
|
52
|
+
<NuxtLink to="#lien-he" @click.prevent="scrollTo('lien-he')" class="hover:text-[#004AC6] transition-colors">Tư vấn</NuxtLink>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</footer>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script setup>
|
|
59
|
+
const { scrollTo } = useScrollToSection()
|
|
60
|
+
const year = new Date().getFullYear()
|
|
61
|
+
</script>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<header class="fixed top-0 left-0 right-0 z-50 bg-white/90 backdrop-blur-md border-b border-slate-200/70 shadow-sm">
|
|
3
|
+
<div class="max-w-7xl mx-auto px-6 lg:px-8 flex items-center justify-between h-16">
|
|
4
|
+
<NuxtLink :to="`/giai-phap-proxmox/khoa-hoc-proxmox-hci`" class="flex items-center gap-2.5">
|
|
5
|
+
<img src="/images/logo-lvs-1300x330.png" alt="Long Vân" class="h-8 w-auto" />
|
|
6
|
+
</NuxtLink>
|
|
7
|
+
|
|
8
|
+
<nav class="hidden md:flex items-center gap-8 text-sm font-medium text-slate-600">
|
|
9
|
+
<template v-for="link in navLinks" :key="link.id">
|
|
10
|
+
<template v-if="link.path">
|
|
11
|
+
<NuxtLink v-if="link.path && link.path != route.path" :to="`${link.path}`" :class="[
|
|
12
|
+
'pb-1 border-b-2 transition-all duration-200 cursor-pointer',
|
|
13
|
+
activeSection === link.id
|
|
14
|
+
? 'text-[#004AC6] font-bold border-[#004AC6]'
|
|
15
|
+
: 'border-transparent hover:text-[#004AC6]'
|
|
16
|
+
]">
|
|
17
|
+
{{ link.label }}
|
|
18
|
+
</NuxtLink>
|
|
19
|
+
<a v-else :href="`/giai-phap-proxmox/khoa-hoc-proxmox-hci#${link.id}`" @click.prevent="handleAnchorClick(link)" :class="[
|
|
20
|
+
'pb-1 border-b-2 transition-all duration-200 cursor-pointer',
|
|
21
|
+
activeSection === link.id
|
|
22
|
+
? 'text-[#004AC6] font-bold border-[#004AC6]'
|
|
23
|
+
: 'border-transparent hover:text-[#004AC6]'
|
|
24
|
+
]">
|
|
25
|
+
{{ link.label }}
|
|
26
|
+
</a>
|
|
27
|
+
</template>
|
|
28
|
+
<a v-else :href="`/giai-phap-proxmox/khoa-hoc-proxmox-hci#${link.id}`" @click.prevent="handleAnchorClick(link)" :class="[
|
|
29
|
+
'pb-1 border-b-2 transition-all duration-200 cursor-pointer',
|
|
30
|
+
activeSection === link.id
|
|
31
|
+
? 'text-[#004AC6] font-bold border-[#004AC6]'
|
|
32
|
+
: 'border-transparent hover:text-[#004AC6]'
|
|
33
|
+
]">
|
|
34
|
+
{{ link.label }}
|
|
35
|
+
</a>
|
|
36
|
+
</template>
|
|
37
|
+
</nav>
|
|
38
|
+
|
|
39
|
+
<!-- Logged in -->
|
|
40
|
+
<div v-if="user" class="relative">
|
|
41
|
+
<button @click="showMenu = !showMenu"
|
|
42
|
+
class="flex items-center gap-2.5 text-sm font-medium text-slate-700 hover:text-[#004AC6] transition-colors">
|
|
43
|
+
<span
|
|
44
|
+
class="w-8 h-8 rounded-[10px] bg-gradient-to-br from-[#004AC6] to-[#1D4ED8] flex items-center justify-center text-white text-xs font-bold">
|
|
45
|
+
{{ avatarInitial }}
|
|
46
|
+
</span>
|
|
47
|
+
<span class="hidden sm:block">Xin chào, <strong>{{ user.fullName || user.username }}</strong></span>
|
|
48
|
+
<Icon name="lucide:chevron-down" class="text-[14px] text-slate-400" />
|
|
49
|
+
</button>
|
|
50
|
+
|
|
51
|
+
<!-- Dropdown -->
|
|
52
|
+
<Transition enter-active-class="transition ease-out duration-100" enter-from-class="opacity-0 scale-95"
|
|
53
|
+
enter-to-class="opacity-100 scale-100" leave-active-class="transition ease-in duration-75"
|
|
54
|
+
leave-from-class="opacity-100 scale-100" leave-to-class="opacity-0 scale-95">
|
|
55
|
+
<div v-if="showMenu"
|
|
56
|
+
class="absolute right-0 mt-2 w-48 bg-white rounded-xl shadow-lg border border-slate-200/80 py-1.5 z-50">
|
|
57
|
+
<button @click="logout"
|
|
58
|
+
class="w-full flex items-center gap-2.5 px-4 py-2.5 text-sm text-red-500 hover:bg-red-50 transition-colors">
|
|
59
|
+
<Icon name="lucide:log-out" class="text-[15px]" />
|
|
60
|
+
Đăng xuất
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
</Transition>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- Not logged in -->
|
|
67
|
+
<div v-else class="flex items-center gap-3">
|
|
68
|
+
<a v-if="isConsultant" :href="`/giai-phap-proxmox/khoa-hoc-proxmox-hci#lien-he`" @click.prevent="scrollTo('lien-he')"
|
|
69
|
+
class="inline-flex items-center gap-1.5 text-sm font-bold px-5 py-2.5 rounded-[10px] border border-[#004AC6] text-[#004AC6] hover:bg-blue-50 transition-all active:scale-95 cursor-pointer">
|
|
70
|
+
Liên hệ tư vấn
|
|
71
|
+
<Icon name="lucide:headset" class="text-[16px]" />
|
|
72
|
+
</a>
|
|
73
|
+
<a :href="loginHref"
|
|
74
|
+
class="inline-flex items-center gap-1.5 text-sm font-bold px-5 py-2.5 rounded-[10px] bg-[#004AC6] text-white hover:bg-[#1D4ED8] transition-all active:scale-95 shadow-sm shadow-blue-600/20">
|
|
75
|
+
Đăng nhập
|
|
76
|
+
<Icon name="lucide:log-in" class="text-[16px]" />
|
|
77
|
+
</a>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</header>
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
<script setup lang="ts">
|
|
84
|
+
const props = defineProps({
|
|
85
|
+
targetCTA: { type: String, default: '' },
|
|
86
|
+
slug: { type: String, default: '' },
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const isConsultant = computed(() => props.targetCTA === 'CONSULTANT')
|
|
90
|
+
|
|
91
|
+
const { getCookie, deleteCookie } = useCookieHelper()
|
|
92
|
+
const { getUserByToken } = useAuthApi()
|
|
93
|
+
const { isLoggedIn, userName, setUser, clearUser } = useAuthState()
|
|
94
|
+
const config = useRuntimeConfig()
|
|
95
|
+
const router = useRouter()
|
|
96
|
+
const route = useRoute()
|
|
97
|
+
|
|
98
|
+
const tokenKey = config.public.ACCESSTOKEN_NAME as string
|
|
99
|
+
|
|
100
|
+
const navLinks = computed(() => [
|
|
101
|
+
{ id: 'san-pham', label: 'Sản phẩm', path: `/gioi-thieu-san-pham/proxmox-hci` },
|
|
102
|
+
{ id: 'giai-phap', label: 'Vì sao chọn chúng tôi' },
|
|
103
|
+
{ id: 'chi-phi', label: 'Học phí' },
|
|
104
|
+
{ id: 'khach-hang', label: 'Học viên nói gì' },
|
|
105
|
+
])
|
|
106
|
+
|
|
107
|
+
const { scrollTo } = useScrollToSection()
|
|
108
|
+
|
|
109
|
+
// route.fullPath gồm cả query hiện tại (vd ?plan=...&name=...) để sau khi đăng nhập quay lại đúng chỗ
|
|
110
|
+
// Trang đăng nhập nằm ở domain LONGVAN_WEB (vd https://dev.longvan.net), không phải app này
|
|
111
|
+
const loginHref = computed(() => `${config.public.LONGVAN_WEB}/dang-nhap?redirectUrl=${encodeURIComponent(route.fullPath)}`)
|
|
112
|
+
|
|
113
|
+
const activeSection = useActiveSection(navLinks.value.map(l => l.id))
|
|
114
|
+
const user = ref<{ fullName?: string; username?: string } | null>(null)
|
|
115
|
+
const showMenu = ref(false)
|
|
116
|
+
|
|
117
|
+
function handleNavClick(e: Event, link: any) {
|
|
118
|
+
if (route.path === link.path) {
|
|
119
|
+
e.preventDefault()
|
|
120
|
+
scrollTo(link.id)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function handleAnchorClick(link: any) {
|
|
125
|
+
const el = document.getElementById(link.id)
|
|
126
|
+
if (el) {
|
|
127
|
+
scrollTo(link.id)
|
|
128
|
+
} else {
|
|
129
|
+
router.push(`/giai-phap-proxmox/${props.slug}#${link.id}`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const avatarInitial = computed(() => {
|
|
134
|
+
const name = user.value?.fullName || user.value?.username || ''
|
|
135
|
+
return name.charAt(0).toUpperCase() || '?'
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
async function loadUser() {
|
|
139
|
+
const token = getCookie(tokenKey)
|
|
140
|
+
if (!token) return
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const res = await getUserByToken(token)
|
|
144
|
+
if (res) {
|
|
145
|
+
user.value = {
|
|
146
|
+
fullName: res.fullName,
|
|
147
|
+
username: res.username || res.userLoginId,
|
|
148
|
+
}
|
|
149
|
+
setUser(res.fullName || res.username || '')
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
deleteCookie(tokenKey)
|
|
153
|
+
clearUser()
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Watch auth state changes (when login/register updates state)
|
|
158
|
+
watch(isLoggedIn, (loggedIn) => {
|
|
159
|
+
if (loggedIn && userName.value) {
|
|
160
|
+
user.value = {
|
|
161
|
+
fullName: userName.value,
|
|
162
|
+
username: userName.value,
|
|
163
|
+
}
|
|
164
|
+
} else if (!loggedIn) {
|
|
165
|
+
user.value = null
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
function logout() {
|
|
170
|
+
deleteCookie(tokenKey)
|
|
171
|
+
user.value = null
|
|
172
|
+
clearUser()
|
|
173
|
+
showMenu.value = false
|
|
174
|
+
router.push('/giai-phap-proxmox')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Close menu on outside click
|
|
178
|
+
function onClickOutside(e: MouseEvent) {
|
|
179
|
+
const target = e.target as HTMLElement
|
|
180
|
+
if (!target.closest('.relative')) {
|
|
181
|
+
showMenu.value = false
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
onMounted(() => {
|
|
186
|
+
loadUser()
|
|
187
|
+
document.addEventListener('click', onClickOutside)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
onUnmounted(() => {
|
|
191
|
+
document.removeEventListener('click', onClickOutside)
|
|
192
|
+
})
|
|
193
|
+
</script>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
name: string
|
|
4
|
+
class?: string
|
|
5
|
+
}>()
|
|
6
|
+
|
|
7
|
+
const paths: Record<string, string> = {
|
|
8
|
+
cpu: "M9 3v2M15 3v2M9 19v2M15 19v2M3 9h2M3 15h2M19 9h2M19 15h2M7 7h10v10H7z M10 10h4v4h-4z",
|
|
9
|
+
database:
|
|
10
|
+
"M12 5c4.418 0 8-1.12 8-2.5S16.418 0 12 0 4 1.12 4 2.5 7.582 5 12 5zM4 2.5V19.5C4 20.88 7.582 22 12 22s8-1.12 8-2.5V2.5M4 8.5C4 9.88 7.582 11 12 11s8-1.12 8-2.5M4 14.5c0 1.38 3.582 2.5 8 2.5s8-1.12 8-2.5",
|
|
11
|
+
share: "M6 12a3 3 0 100-6 3 3 0 000 6zM18 6a3 3 0 100-6 3 3 0 000 6zM18 24a3 3 0 100-6 3 3 0 000 6zM8.5 10.5l7-4M8.5 13.5l7 4",
|
|
12
|
+
cluster: "M6 9a3 3 0 100-6 3 3 0 000 6zM18 9a3 3 0 100-6 3 3 0 000 6zM12 21a3 3 0 100-6 3 3 0 000 6zM6 9v3a3 3 0 003 3M18 9v3a3 3 0 01-3 3",
|
|
13
|
+
shield: "M12 2l8 3.5v5.5c0 5-3.4 8.9-8 11-4.6-2.1-8-6-8-11V5.5L12 2z",
|
|
14
|
+
bolt: "M13 2L3 14h7l-1 8 11-14h-8l1-6z",
|
|
15
|
+
chart: "M4 20V10M10 20V4M16 20v-7M22 20H2",
|
|
16
|
+
layers: "M12 2l9 5-9 5-9-5 9-5zM3 12l9 5 9-5M3 17l9 5 9-5",
|
|
17
|
+
book: "M4 4.5A2.5 2.5 0 016.5 2H20v17H6.5A2.5 2.5 0 004 21.5V4.5zM4 19.5A2.5 2.5 0 016.5 17H20",
|
|
18
|
+
users: "M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2M9 11a4 4 0 100-8 4 4 0 000 8zM23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75",
|
|
19
|
+
trending: "M23 6l-9.5 9.5-5-5L1 18M17 6h6v6",
|
|
20
|
+
award: "M12 15a6 6 0 100-12 6 6 0 000 12zM8.21 13.89L7 23l5-3 5 3-1.21-9.12",
|
|
21
|
+
globe: "M12 22a10 10 0 100-20 10 10 0 000 20zM2 12h20M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z",
|
|
22
|
+
activity: "M22 12h-4l-3 9L9 3l-3 9H2",
|
|
23
|
+
check: "M20 6L9 17l-5-5",
|
|
24
|
+
x: "M18 6L6 18M6 6l12 12",
|
|
25
|
+
star: "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z",
|
|
26
|
+
cap: "M22 10L12 5 2 10l10 5 10-5zM6 12v5c0 1.66 2.69 3 6 3s6-1.34 6-3v-5",
|
|
27
|
+
calendar: "M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 012 2v14a2 2 0 01-2 2H5a2 2 0 01-2-2V6a2 2 0 012-2z",
|
|
28
|
+
arrowRight: "M5 12h14M12 5l7 7-7 7",
|
|
29
|
+
chevronDown: "M6 9l6 6 6-6"
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<svg
|
|
35
|
+
:class="$props.class"
|
|
36
|
+
viewBox="0 0 24 24"
|
|
37
|
+
fill="none"
|
|
38
|
+
stroke="currentColor"
|
|
39
|
+
stroke-width="2"
|
|
40
|
+
stroke-linecap="round"
|
|
41
|
+
stroke-linejoin="round"
|
|
42
|
+
>
|
|
43
|
+
<path :d="paths[name] ?? paths.check" />
|
|
44
|
+
</svg>
|
|
45
|
+
</template>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { comparisonRows } from "../../data/proxmox-content"
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<section class="bg-white py-20 sm:py-28">
|
|
7
|
+
<div class="mx-auto max-w-4xl px-6 lg:px-8">
|
|
8
|
+
<div class="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
|
+
So sánh
|
|
11
|
+
</div>
|
|
12
|
+
<h2 v-reveal="{ delay: 60 }" class="mt-5 text-3xl font-extrabold tracking-tight text-slate-900 sm:text-4xl">
|
|
13
|
+
<span class="text-slate-400">Proxmox HCI vs</span>
|
|
14
|
+
Hạ tầng truyền thống
|
|
15
|
+
</h2>
|
|
16
|
+
<p v-reveal="{ delay: 120 }" class="mt-5 leading-relaxed text-slate-600">
|
|
17
|
+
Đặt lên bàn cân để thấy sự khác biệt về hiệu quả đầu tư và khả năng vận hành lâu dài.
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div v-reveal="{ delay: 180 }" class="mt-12 overflow-hidden rounded-2xl border border-slate-200 shadow-sm">
|
|
22
|
+
<table class="w-full border-collapse text-left text-sm">
|
|
23
|
+
<thead>
|
|
24
|
+
<tr class="bg-blue-600 text-white">
|
|
25
|
+
<th class="px-6 py-4 font-semibold">Tiêu chí</th>
|
|
26
|
+
<th class="px-6 py-4 text-center font-semibold">Proxmox HCI</th>
|
|
27
|
+
<th class="px-6 py-4 text-center font-semibold">Hạ tầng truyền thống</th>
|
|
28
|
+
</tr>
|
|
29
|
+
</thead>
|
|
30
|
+
<tbody class="divide-y divide-slate-200 bg-white">
|
|
31
|
+
<tr v-for="row in comparisonRows" :key="row.label">
|
|
32
|
+
<td class="px-6 py-4 font-medium text-slate-700">{{ row.label }}</td>
|
|
33
|
+
<td class="px-6 py-4">
|
|
34
|
+
<span class="mx-auto flex w-fit items-center gap-1.5 font-medium text-emerald-600">
|
|
35
|
+
<ProxmoxAppIcon name="check" class="h-4 w-4 rounded-full bg-emerald-50 p-0.5" />
|
|
36
|
+
{{ row.hci }}
|
|
37
|
+
</span>
|
|
38
|
+
</td>
|
|
39
|
+
<td class="px-6 py-4">
|
|
40
|
+
<span
|
|
41
|
+
class="mx-auto flex w-fit items-center gap-1.5 font-medium"
|
|
42
|
+
:class="row.legacyOk ? 'text-emerald-600' : 'text-rose-500'"
|
|
43
|
+
>
|
|
44
|
+
<ProxmoxAppIcon :name="row.legacyOk ? 'check' : 'x'" class="h-4 w-4 rounded-full p-0.5" :class="row.legacyOk ? 'bg-emerald-50' : 'bg-rose-50'" />
|
|
45
|
+
{{ row.legacy }}
|
|
46
|
+
</span>
|
|
47
|
+
</td>
|
|
48
|
+
</tr>
|
|
49
|
+
</tbody>
|
|
50
|
+
</table>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</section>
|
|
54
|
+
</template>
|
|
@@ -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>
|