nuxt-layer-core 1.1.2 → 1.1.4
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/app/layers/solution/app/components/CoursesSection.vue +260 -244
- package/app/layers/solution/app/components/CtaSection.vue +3 -6
- package/app/layers/solution/app/components/FeaturesSection.vue +66 -66
- package/app/layers/solution/app/components/HeroSection.vue +3 -3
- package/app/layers/solution/app/components/LinuxVersionsSection.vue +24 -13
- package/app/layers/solution/app/components/PricingPlansSection.vue +369 -396
- package/app/layers/solution/app/components/RegistrationForm.vue +14 -22
- package/app/layers/solution/app/components/RegistrationSection.vue +2 -1
- package/app/layers/solution/app/components/TestimonialsSection.vue +57 -57
- package/app/layers/solution/app/components/TheFooter.vue +60 -60
- package/app/layers/solution/app/components/TheHeader.vue +6 -6
- package/app/layers/solution/app/components/proxmox/HeroSection.vue +3 -3
- package/app/layers/solution/app/composables/usePlanNormalizer.ts +49 -45
- package/app/layers/solution/app/pages/giai-phap-proxmox/[slug]/index.vue +62 -2
- package/app/layers/solution/app/pages/gioi-thieu-san-pham/[slug]/index.vue +108 -2
- package/app/layers/solution/app/pages/template/[slug]/index.vue +136 -22
- package/app/layers/solution/app/pages/tu-van/[slug]/index.vue +57 -2
- package/package.json +1 -1
|
@@ -10,8 +10,14 @@ const props = withDefaults(defineProps<{
|
|
|
10
10
|
description?: string
|
|
11
11
|
}>
|
|
12
12
|
selectedPlanId?: string
|
|
13
|
+
studyModes?: Array<{ name: string; type: string }>
|
|
13
14
|
}>(), {
|
|
14
15
|
plans: () => [],
|
|
16
|
+
studyModes: () => [
|
|
17
|
+
{ name: 'Học trực tuyến (Online)', type: 'online' },
|
|
18
|
+
{ name: 'Học tại lớp (Offline)', type: 'offline' },
|
|
19
|
+
{ name: 'Đào tạo doanh nghiệp', type: 'enterprise' },
|
|
20
|
+
],
|
|
15
21
|
})
|
|
16
22
|
|
|
17
23
|
const { addOpportunity } = useCrmApi()
|
|
@@ -19,7 +25,7 @@ const { addOpportunity } = useCrmApi()
|
|
|
19
25
|
// Form state
|
|
20
26
|
const selectedCourseId = ref(props.selectedPlanId || '')
|
|
21
27
|
const form = reactive({ name: '', phone: '', email: '', note: '' })
|
|
22
|
-
const studyMode = ref('online')
|
|
28
|
+
const studyMode = ref(props.studyModes[0]?.type || 'online')
|
|
23
29
|
const errors = reactive({ name: '', phone: '', email: '', course: '' })
|
|
24
30
|
const touched = reactive({ name: false, phone: false, email: false, course: false })
|
|
25
31
|
const submitting = ref(false)
|
|
@@ -100,12 +106,7 @@ async function handleSubmit() {
|
|
|
100
106
|
|
|
101
107
|
try {
|
|
102
108
|
const hostname = window.location.origin
|
|
103
|
-
const studyModeLabel =
|
|
104
|
-
studyMode.value === 'online'
|
|
105
|
-
? 'Học trực tuyến (Online)'
|
|
106
|
-
: studyMode.value === 'offline'
|
|
107
|
-
? 'Học tại lớp (Offline)'
|
|
108
|
-
: 'Đào tạo doanh nghiệp'
|
|
109
|
+
const studyModeLabel = props.studyModes.find((mode) => mode.type === studyMode.value)?.name || studyMode.value
|
|
109
110
|
|
|
110
111
|
const levelLabel =
|
|
111
112
|
selectedPlan.value?.level === 'basic'
|
|
@@ -237,21 +238,12 @@ async function handleSubmit() {
|
|
|
237
238
|
<!-- Study mode -->
|
|
238
239
|
<div class="mb-5">
|
|
239
240
|
<label class="mb-[10px] block text-[13px] font-semibold text-gray-700">Hình thức học</label>
|
|
240
|
-
<div class="flex flex-wrap gap-[10px]">
|
|
241
|
-
<button
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
<button type="button" class="rounded-[10px] border px-4 py-[9px] text-[13.5px] font-medium transition-colors"
|
|
247
|
-
:class="studyMode === 'offline' ? 'border-[#2f5fe0] bg-[#2f5fe0] text-white' : 'border-[#e2e5ee] bg-white text-gray-700'"
|
|
248
|
-
@click="studyMode = 'offline'">
|
|
249
|
-
Học tại lớp (Offline)
|
|
250
|
-
</button>
|
|
251
|
-
<button type="button" class="rounded-[10px] border px-4 py-[9px] text-[13.5px] font-medium transition-colors"
|
|
252
|
-
:class="studyMode === 'enterprise' ? 'border-[#2f5fe0] bg-[#2f5fe0] text-white' : 'border-[#e2e5ee] bg-white text-gray-700'"
|
|
253
|
-
@click="studyMode = 'enterprise'">
|
|
254
|
-
Đào tạo doanh nghiệp
|
|
241
|
+
<div class="flex flex-wrap justify-center gap-[10px] sm:justify-start">
|
|
242
|
+
<button v-for="mode in studyModes" :key="mode.type" type="button"
|
|
243
|
+
class="rounded-[10px] border px-4 py-[9px] text-[13.5px] font-medium transition-colors"
|
|
244
|
+
:class="studyMode === mode.type ? 'border-[#2f5fe0] bg-[#2f5fe0] text-white' : 'border-[#e2e5ee] bg-white text-gray-700'"
|
|
245
|
+
@click="studyMode = mode.type">
|
|
246
|
+
{{ mode.name }}
|
|
255
247
|
</button>
|
|
256
248
|
</div>
|
|
257
249
|
</div>
|
|
@@ -8,6 +8,7 @@ defineProps<{
|
|
|
8
8
|
icon: string
|
|
9
9
|
}>
|
|
10
10
|
selectedPlanId?: string
|
|
11
|
+
studyModes?: Array<{ name: string; type: string }>
|
|
11
12
|
}>()
|
|
12
13
|
</script>
|
|
13
14
|
|
|
@@ -18,7 +19,7 @@ defineProps<{
|
|
|
18
19
|
<RegistrationSidebar />
|
|
19
20
|
|
|
20
21
|
<!-- Right column: form -->
|
|
21
|
-
<RegistrationForm :plans="plans" :selected-plan-id="selectedPlanId" />
|
|
22
|
+
<RegistrationForm :plans="plans" :selected-plan-id="selectedPlanId" :study-modes="studyModes" />
|
|
22
23
|
</div>
|
|
23
24
|
</div>
|
|
24
25
|
</template>
|
|
@@ -1,58 +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
|
|
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
|
-
]
|
|
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 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
|
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
58
|
</script>
|
|
@@ -1,61 +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
|
|
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()
|
|
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 gap-10 sm:grid-cols-2 lg:grid-cols-4">
|
|
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
61
|
</script>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
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
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">
|
|
4
|
+
<NuxtLink :to="`/giai-phap-proxmox/khoa-hoc-proxmox-hci`" class="flex shrink-0 items-center gap-2.5">
|
|
5
5
|
<img src="/images/logo-lvs-1300x330.png" alt="Long Vân" class="h-8 w-auto" />
|
|
6
6
|
</NuxtLink>
|
|
7
7
|
|
|
8
|
-
<nav class="hidden
|
|
8
|
+
<nav class="hidden lg:flex shrink-0 items-center gap-5 whitespace-nowrap text-sm font-medium text-slate-600 xl:gap-8">
|
|
9
9
|
<template v-for="link in navLinks" :key="link.id">
|
|
10
10
|
<template v-if="link.path">
|
|
11
11
|
<NuxtLink v-if="link.path && link.path != route.path" :to="`${link.path}`" :class="[
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
</nav>
|
|
38
38
|
|
|
39
39
|
<!-- Logged in -->
|
|
40
|
-
<div v-if="user" class="relative hidden
|
|
40
|
+
<div v-if="user" class="relative hidden lg:block">
|
|
41
41
|
<button @click="showMenu = !showMenu"
|
|
42
42
|
class="flex items-center gap-2.5 text-sm font-medium text-slate-700 hover:text-[#004AC6] transition-colors">
|
|
43
43
|
<span
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
</div>
|
|
65
65
|
|
|
66
66
|
<!-- Not logged in -->
|
|
67
|
-
<div v-else class="hidden
|
|
67
|
+
<div v-else class="hidden shrink-0 items-center gap-3 whitespace-nowrap lg:flex">
|
|
68
68
|
<a v-if="isConsultant" :href="`/giai-phap-proxmox/khoa-hoc-proxmox-hci#lien-he`" @click.prevent="scrollTo('lien-he')"
|
|
69
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
70
|
Liên hệ tư vấn
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
</div>
|
|
79
79
|
|
|
80
80
|
<!-- Mobile menu toggle -->
|
|
81
|
-
<button type="button" class="
|
|
81
|
+
<button type="button" class="lg:hidden flex h-9 w-9 items-center justify-center rounded-[10px] text-slate-600 hover:bg-slate-100 transition-colors"
|
|
82
82
|
:aria-expanded="showMobileMenu" aria-label="Mở menu"
|
|
83
83
|
@click="showMobileMenu = !showMobileMenu">
|
|
84
84
|
<Icon :name="showMobileMenu ? 'lucide:x' : 'lucide:menu'" class="text-[22px]" />
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
<Transition enter-active-class="transition ease-out duration-150" enter-from-class="opacity-0 -translate-y-1"
|
|
90
90
|
enter-to-class="opacity-100 translate-y-0" leave-active-class="transition ease-in duration-100"
|
|
91
91
|
leave-from-class="opacity-100 translate-y-0" leave-to-class="opacity-0 -translate-y-1">
|
|
92
|
-
<div v-if="showMobileMenu" class="
|
|
92
|
+
<div v-if="showMobileMenu" class="lg:hidden border-t border-slate-200/70 bg-white px-6 py-4">
|
|
93
93
|
<nav class="flex flex-col gap-1 text-sm font-medium text-slate-600">
|
|
94
94
|
<template v-for="link in navLinks" :key="link.id">
|
|
95
95
|
<NuxtLink v-if="link.path && link.path != route.path" :to="`${link.path}`" class="rounded-[10px] px-3 py-2.5 transition-colors"
|
|
@@ -35,7 +35,7 @@ const { scrollTo } = useScrollToSection()
|
|
|
35
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
36
|
</a>
|
|
37
37
|
|
|
38
|
-
<div v-reveal="{ delay: 280 }" class="mt-8 flex flex-wrap items-center gap-4">
|
|
38
|
+
<div v-reveal="{ delay: 280 }" class="mt-8 flex flex-wrap items-center justify-center gap-4 lg:justify-start">
|
|
39
39
|
<a href="#lien-he" @click.prevent="scrollTo('lien-he')"
|
|
40
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
41
|
Nhận tư vấn triển khai
|
|
@@ -61,8 +61,8 @@ const { scrollTo } = useScrollToSection()
|
|
|
61
61
|
</dl>
|
|
62
62
|
</div>
|
|
63
63
|
|
|
64
|
-
<!-- Right: image with floating cards -->
|
|
65
|
-
<div v-reveal="{ type: 'right', delay: 160 }" class="relative
|
|
64
|
+
<!-- Right: image with floating cards - ẩn trên mobile/tablet để tiết kiệm chiều cao -->
|
|
65
|
+
<div v-reveal="{ type: 'right', delay: 160 }" class="relative hidden h-full items-center lg:flex">
|
|
66
66
|
<div class="w-full overflow-hidden rounded-3xl shadow-2xl shadow-blue-900/10 ring-1 ring-black/5">
|
|
67
67
|
<img src="../../images/Proxmox HCI.png" alt="Datacenter server racks"
|
|
68
68
|
class="block h-auto w-full object-contain" />
|
|
@@ -1,45 +1,49 @@
|
|
|
1
|
-
// Chuẩn hoá 1 item CMS (đến từ item_id_0M_groups_0N của CoursesSection hoặc PricingPlansSection)
|
|
2
|
-
// về cùng 1 shape, để mọi nơi tiêu thụ (CtaSection, PricingPlansSection...) không phải đoán field theo từng nguồn.
|
|
3
|
-
export interface NormalizedPlanItem {
|
|
4
|
-
id: string
|
|
5
|
-
name: string
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
// Chuẩn hoá 1 item CMS (đến từ item_id_0M_groups_0N của CoursesSection hoặc PricingPlansSection)
|
|
2
|
+
// về cùng 1 shape, để mọi nơi tiêu thụ (CtaSection, PricingPlansSection...) không phải đoán field theo từng nguồn.
|
|
3
|
+
export interface NormalizedPlanItem {
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
tagline: string
|
|
7
|
+
level: string
|
|
8
|
+
duration: string
|
|
9
|
+
description: string
|
|
10
|
+
features: string[]
|
|
11
|
+
price?: number
|
|
12
|
+
prices?: Record<string, number>
|
|
13
|
+
priceLabel: string
|
|
14
|
+
priceNote: string
|
|
15
|
+
highlight: boolean
|
|
16
|
+
url?: string
|
|
17
|
+
titleButton: string
|
|
18
|
+
icon: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const normalizePlanItem = (item: any): NormalizedPlanItem | null => {
|
|
22
|
+
if (!item || typeof item !== 'object') return null
|
|
23
|
+
|
|
24
|
+
const features = Array.isArray(item.config_product)
|
|
25
|
+
? item.config_product
|
|
26
|
+
: typeof item.config_product === 'string' && item.config_product
|
|
27
|
+
? [item.config_product]
|
|
28
|
+
: Array.isArray(item.features)
|
|
29
|
+
? item.features
|
|
30
|
+
: []
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
id: String(item.productId ?? item.id ?? item.title ?? item.name ?? ''),
|
|
34
|
+
name: item.title || item.name || '',
|
|
35
|
+
tagline: item.tagline || '',
|
|
36
|
+
level: item.level || '',
|
|
37
|
+
duration: item.time || item.unit || '',
|
|
38
|
+
description: item.short_description || item.desc || '',
|
|
39
|
+
features,
|
|
40
|
+
price: item.price,
|
|
41
|
+
prices: item.prices,
|
|
42
|
+
priceLabel: item.priceLabel || '',
|
|
43
|
+
priceNote: item.priceNote || '',
|
|
44
|
+
highlight: !!item.highlight,
|
|
45
|
+
url: item.url,
|
|
46
|
+
titleButton: item.title_button || item.titleButton || '',
|
|
47
|
+
icon: item.icon || 'lucide:server',
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -11,12 +11,72 @@ const { data } = await useAsyncData(
|
|
|
11
11
|
() => getSlugData(slug.value, 'giai-phap'),
|
|
12
12
|
{ server: true }
|
|
13
13
|
)
|
|
14
|
+
|
|
15
|
+
const targetCTA = computed(() => data.value?.productData?.targetCTA)
|
|
16
|
+
const pageSlug = computed(() => data.value?.productData?.slug || slug.value)
|
|
17
|
+
|
|
18
|
+
// Lấy 1 field group (vd khoa_dao_tao_co_ban) từ fieldResumes, gom children về object phẳng { productid, name, ... }
|
|
19
|
+
const findFieldGroup = (fieldName: string) => {
|
|
20
|
+
const group = data.value?.resumeData?.fieldResumes?.find(
|
|
21
|
+
(field: any) => field?.fieldName === fieldName && field?.type === 'FIELD_GROUP'
|
|
22
|
+
)
|
|
23
|
+
if (!group?.children) return null
|
|
24
|
+
|
|
25
|
+
const result: Record<string, any> = {}
|
|
26
|
+
for (const field of group.children) {
|
|
27
|
+
const key = field?.fieldName?.split('@')?.pop()
|
|
28
|
+
if (key) result[key] = field?.value
|
|
29
|
+
}
|
|
30
|
+
return result
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const toCourseItem = (fieldName: string, level: string) => {
|
|
34
|
+
const group = findFieldGroup(fieldName)
|
|
35
|
+
if (!group) return undefined
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
productId: group.productid,
|
|
39
|
+
name: group.name,
|
|
40
|
+
tagline: group.tagline,
|
|
41
|
+
level,
|
|
42
|
+
time: group.thoi_gian_hoc,
|
|
43
|
+
// mo_ta_ngan là rich text từ CMS, CoursesSection render bằng v-html
|
|
44
|
+
short_description: group.mo_ta_ngan,
|
|
45
|
+
url: group.url,
|
|
46
|
+
title_button: group.title_button,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const courseBasic = computed(() => toCourseItem('khoa_dao_tao_co_ban', 'basic'))
|
|
51
|
+
const courseAdvanced = computed(() => toCourseItem('khoa_dao_tao_nang_cao', 'advanced'))
|
|
52
|
+
const courseExpert = computed(() => toCourseItem('khoa_dao_tao_chuyen_gia', 'expert'))
|
|
53
|
+
|
|
54
|
+
// Đồng bộ dropdown "Khóa học quan tâm" của CtaSection với đúng data đang hiện ở CoursesSection
|
|
55
|
+
const courses = computed(() =>
|
|
56
|
+
[courseBasic.value, courseAdvanced.value, courseExpert.value].map(normalizePlanItem).filter(Boolean)
|
|
57
|
+
)
|
|
14
58
|
</script>
|
|
15
59
|
|
|
16
60
|
<template>
|
|
17
|
-
<DynamicPageSections
|
|
61
|
+
<!-- <DynamicPageSections
|
|
18
62
|
:field-resumes="data?.resumeData?.fieldResumes"
|
|
19
63
|
:target-c-t-a="data?.productData?.targetCTA"
|
|
20
64
|
:slug="data?.productData?.slug || slug"
|
|
21
|
-
/>
|
|
65
|
+
/> -->
|
|
66
|
+
|
|
67
|
+
<div class="bg-ink text-cloud font-body antialiased">
|
|
68
|
+
<TheHeader :target-c-t-a="targetCTA" :slug="pageSlug" />
|
|
69
|
+
<HeroSection />
|
|
70
|
+
<FeaturesSection />
|
|
71
|
+
<CoursesSection
|
|
72
|
+
:target-c-t-a="targetCTA"
|
|
73
|
+
:slug="pageSlug"
|
|
74
|
+
:item-id01-groups01="courseBasic"
|
|
75
|
+
:item-id02-groups01="courseAdvanced"
|
|
76
|
+
:item-id03-groups01="courseExpert"
|
|
77
|
+
/>
|
|
78
|
+
<TestimonialsSection />
|
|
79
|
+
<CtaSection :courses="courses" />
|
|
80
|
+
<TheFooter />
|
|
81
|
+
</div>
|
|
22
82
|
</template>
|