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.
@@ -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 type="button" class="rounded-[10px] border px-4 py-[9px] text-[13.5px] font-medium transition-colors"
242
- :class="studyMode === 'online' ? 'border-[#2f5fe0] bg-[#2f5fe0] text-white' : 'border-[#e2e5ee] bg-white text-gray-700'"
243
- @click="studyMode = 'online'">
244
- Học trực tuyến (Online)
245
- </button>
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 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
- ]
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 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()
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 md:flex items-center gap-8 text-sm font-medium text-slate-600">
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 md:block">
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 md:flex items-center gap-3">
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="md:hidden flex h-9 w-9 items-center justify-center rounded-[10px] text-slate-600 hover:bg-slate-100 transition-colors"
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="md:hidden border-t border-slate-200/70 bg-white px-6 py-4">
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 flex h-full items-center">
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
- level: string
7
- duration: string
8
- description: string
9
- features: string[]
10
- price?: number
11
- prices?: Record<string, number>
12
- priceLabel: string
13
- priceNote: string
14
- highlight: boolean
15
- url?: string
16
- icon: string
17
- }
18
-
19
- export const normalizePlanItem = (item: any): NormalizedPlanItem | null => {
20
- if (!item || typeof item !== 'object') return null
21
-
22
- const features = Array.isArray(item.config_product)
23
- ? item.config_product
24
- : typeof item.config_product === 'string' && item.config_product
25
- ? [item.config_product]
26
- : Array.isArray(item.features)
27
- ? item.features
28
- : []
29
-
30
- return {
31
- id: String(item.productId ?? item.id ?? item.title ?? item.name ?? ''),
32
- name: item.title || item.name || '',
33
- level: item.level || '',
34
- duration: item.time || item.unit || '',
35
- description: item.short_description || item.desc || '',
36
- features,
37
- price: item.price,
38
- prices: item.prices,
39
- priceLabel: item.priceLabel || '',
40
- priceNote: item.priceNote || '',
41
- highlight: !!item.highlight,
42
- url: item.url,
43
- icon: item.icon || 'lucide:server',
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>