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
|
@@ -1,396 +1,369 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<section id="goi-san-pham" ref="sectionRoot" class="py-24 bg-white overflow-hidden" :class="{ 'pricing-revealed': revealed }">
|
|
3
|
-
<div class="max-w-6xl mx-auto px-6 lg:px-8">
|
|
4
|
-
<div class="text-center mb-12 pricing-header">
|
|
5
|
-
<h2 class="text-[#004AC6] font-bold uppercase tracking-widest text-sm mb-2">Gói sản phẩm</h2>
|
|
6
|
-
<h3 class="font-['Montserrat'] font-extrabold text-2xl sm:text-4xl text-slate-900 mb-4">
|
|
7
|
-
Chọn gói phù hợp<br class="hidden sm:block" />
|
|
8
|
-
với nhu cầu của bạn
|
|
9
|
-
</h3>
|
|
10
|
-
<p class="text-slate-500 max-w-2xl mx-auto leading-relaxed">
|
|
11
|
-
Thuê hạ tầng Proxmox HCI theo tháng, tham gia workshop 1 ngày hoặc chọn khóa đào tạo chuyên sâu. Chi phí
|
|
12
|
-
minh bạch, điều chỉnh theo yêu cầu riêng.
|
|
13
|
-
</p>
|
|
14
|
-
|
|
15
|
-
<div class="flex flex-wrap items-center justify-center gap-3 mt-8">
|
|
16
|
-
<button
|
|
17
|
-
v-for="tab in tabs"
|
|
18
|
-
:key="tab.id"
|
|
19
|
-
type="button"
|
|
20
|
-
@click="activeTab = tab.id"
|
|
21
|
-
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-full text-sm font-bold transition-all duration-300 cursor-pointer"
|
|
22
|
-
:class="activeTab === tab.id
|
|
23
|
-
? 'bg-[#004AC6] text-white shadow-lg shadow-blue-500/25 scale-[1.03]'
|
|
24
|
-
: 'bg-white text-slate-500 border border-slate-200 hover:border-blue-200 hover:text-[#004AC6]'
|
|
25
|
-
"
|
|
26
|
-
>
|
|
27
|
-
<Icon :name="tab.icon" class="w-4 h-4" />
|
|
28
|
-
{{ tab.label }}
|
|
29
|
-
</button>
|
|
30
|
-
</div>
|
|
31
|
-
|
|
32
|
-
<Transition name="fade-swap" mode="out-in">
|
|
33
|
-
<p :key="activeTab" class="text-slate-400 text-sm max-w-xl mx-auto mt-5">
|
|
34
|
-
{{ activeTabData.intro }}
|
|
35
|
-
</p>
|
|
36
|
-
</Transition>
|
|
37
|
-
</div>
|
|
38
|
-
|
|
39
|
-
<Transition name="fade-swap" mode="out-in">
|
|
40
|
-
<div :key="activeTab">
|
|
41
|
-
<!-- Info cards: only for Proxmox HCI -->
|
|
42
|
-
<div v-if="activeTab === 'hci'" class="grid grid-cols-1
|
|
43
|
-
<div
|
|
44
|
-
v-for="(info, i) in hciInfoCards"
|
|
45
|
-
:key="info.title"
|
|
46
|
-
class="pricing-reveal bg-white rounded-2xl border border-slate-200 p-6 hover:border-blue-200 hover:shadow-lg transition-all duration-300"
|
|
47
|
-
:style="{ '--i': i }"
|
|
48
|
-
>
|
|
49
|
-
<div class="w-11 h-11 rounded-[10px] bg-blue-50 flex items-center justify-center mb-5 text-[#004AC6]">
|
|
50
|
-
<Icon :name="info.icon" class="w-5 h-5" />
|
|
51
|
-
</div>
|
|
52
|
-
<h4 class="font-bold text-slate-900 mb-2">{{ info.title }}</h4>
|
|
53
|
-
<p class="text-sm text-slate-500 leading-relaxed">{{ info.desc }}</p>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
|
|
57
|
-
<!-- Billing period switch: only for Proxmox HCI -->
|
|
58
|
-
<div v-if="activeTab === 'hci'" class="flex flex-col items-center gap-4 mb-10">
|
|
59
|
-
<p class="text-slate-500 text-sm">Chọn kỳ hạn thuê, càng dài chi phí hàng tháng càng thấp</p>
|
|
60
|
-
<div class="inline-flex items-center gap-1 bg-white border border-slate-200 rounded-full p-1">
|
|
61
|
-
<button
|
|
62
|
-
v-for="period in billingPeriods"
|
|
63
|
-
:key="period.value"
|
|
64
|
-
type="button"
|
|
65
|
-
@click="activePeriod = period.value"
|
|
66
|
-
class="px-4 py-2 rounded-full text-sm font-semibold transition-all duration-300 cursor-pointer"
|
|
67
|
-
:class="activePeriod === period.value
|
|
68
|
-
? 'bg-[#004AC6] text-white shadow-md shadow-blue-500/25'
|
|
69
|
-
: 'text-slate-500 hover:text-[#004AC6]'
|
|
70
|
-
"
|
|
71
|
-
>
|
|
72
|
-
{{ period.label }}
|
|
73
|
-
</button>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
<!-- Pricing cards -->
|
|
78
|
-
<Transition name="fade-swap" mode="out-in">
|
|
79
|
-
<div :key="activeTab + '-' + activePeriod" class="grid
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
@keyframes pricing-card-in {
|
|
372
|
-
from {
|
|
373
|
-
opacity: 0;
|
|
374
|
-
transform: translateY(24px);
|
|
375
|
-
}
|
|
376
|
-
to {
|
|
377
|
-
opacity: 1;
|
|
378
|
-
transform: translateY(0);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
.fade-swap-enter-active,
|
|
383
|
-
.fade-swap-leave-active {
|
|
384
|
-
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
.fade-swap-enter-from {
|
|
388
|
-
opacity: 0;
|
|
389
|
-
transform: translateY(10px);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
.fade-swap-leave-to {
|
|
393
|
-
opacity: 0;
|
|
394
|
-
transform: translateY(-6px);
|
|
395
|
-
}
|
|
396
|
-
</style>
|
|
1
|
+
<template>
|
|
2
|
+
<section id="goi-san-pham" ref="sectionRoot" class="py-24 bg-white overflow-hidden" :class="{ 'pricing-revealed': revealed }">
|
|
3
|
+
<div class="max-w-6xl mx-auto px-6 lg:px-8">
|
|
4
|
+
<div class="text-center mb-12 pricing-header">
|
|
5
|
+
<h2 class="text-[#004AC6] font-bold uppercase tracking-widest text-sm mb-2">Gói sản phẩm</h2>
|
|
6
|
+
<h3 class="font-['Montserrat'] font-extrabold text-2xl sm:text-4xl text-slate-900 mb-4">
|
|
7
|
+
Chọn gói phù hợp<br class="hidden sm:block" />
|
|
8
|
+
với nhu cầu của bạn
|
|
9
|
+
</h3>
|
|
10
|
+
<p class="text-slate-500 max-w-2xl mx-auto leading-relaxed">
|
|
11
|
+
Thuê hạ tầng Proxmox HCI theo tháng, tham gia workshop 1 ngày hoặc chọn khóa đào tạo chuyên sâu. Chi phí
|
|
12
|
+
minh bạch, điều chỉnh theo yêu cầu riêng.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<div class="flex flex-wrap items-center justify-center gap-3 mt-8">
|
|
16
|
+
<button
|
|
17
|
+
v-for="tab in tabs"
|
|
18
|
+
:key="tab.id"
|
|
19
|
+
type="button"
|
|
20
|
+
@click="activeTab = tab.id"
|
|
21
|
+
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-full text-sm font-bold transition-all duration-300 cursor-pointer"
|
|
22
|
+
:class="activeTab === tab.id
|
|
23
|
+
? 'bg-[#004AC6] text-white shadow-lg shadow-blue-500/25 scale-[1.03]'
|
|
24
|
+
: 'bg-white text-slate-500 border border-slate-200 hover:border-blue-200 hover:text-[#004AC6]'
|
|
25
|
+
"
|
|
26
|
+
>
|
|
27
|
+
<Icon :name="tab.icon" class="w-4 h-4" />
|
|
28
|
+
{{ tab.label }}
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<Transition name="fade-swap" mode="out-in">
|
|
33
|
+
<p :key="activeTab" class="text-slate-400 text-sm max-w-xl mx-auto mt-5">
|
|
34
|
+
{{ activeTabData.intro }}
|
|
35
|
+
</p>
|
|
36
|
+
</Transition>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<Transition name="fade-swap" mode="out-in">
|
|
40
|
+
<div :key="activeTab">
|
|
41
|
+
<!-- Info cards: only for Proxmox HCI -->
|
|
42
|
+
<div v-if="activeTab === 'hci'" class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 mb-16">
|
|
43
|
+
<div
|
|
44
|
+
v-for="(info, i) in hciInfoCards"
|
|
45
|
+
:key="info.title"
|
|
46
|
+
class="pricing-reveal bg-white rounded-2xl border border-slate-200 p-6 hover:border-blue-200 hover:shadow-lg transition-all duration-300"
|
|
47
|
+
:style="{ '--i': i }"
|
|
48
|
+
>
|
|
49
|
+
<div class="w-11 h-11 rounded-[10px] bg-blue-50 flex items-center justify-center mb-5 text-[#004AC6]">
|
|
50
|
+
<Icon :name="info.icon" class="w-5 h-5" />
|
|
51
|
+
</div>
|
|
52
|
+
<h4 class="font-bold text-slate-900 mb-2">{{ info.title }}</h4>
|
|
53
|
+
<p class="text-sm text-slate-500 leading-relaxed">{{ info.desc }}</p>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- Billing period switch: only for Proxmox HCI -->
|
|
58
|
+
<div v-if="activeTab === 'hci'" class="flex flex-col items-center gap-4 mb-10">
|
|
59
|
+
<p class="text-slate-500 text-sm">Chọn kỳ hạn thuê, càng dài chi phí hàng tháng càng thấp</p>
|
|
60
|
+
<div class="inline-flex items-center gap-1 bg-white border border-slate-200 rounded-full p-1">
|
|
61
|
+
<button
|
|
62
|
+
v-for="period in billingPeriods"
|
|
63
|
+
:key="period.value"
|
|
64
|
+
type="button"
|
|
65
|
+
@click="activePeriod = period.value"
|
|
66
|
+
class="px-4 py-2 rounded-full text-sm font-semibold transition-all duration-300 cursor-pointer"
|
|
67
|
+
:class="activePeriod === period.value
|
|
68
|
+
? 'bg-[#004AC6] text-white shadow-md shadow-blue-500/25'
|
|
69
|
+
: 'text-slate-500 hover:text-[#004AC6]'
|
|
70
|
+
"
|
|
71
|
+
>
|
|
72
|
+
{{ period.label }}
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- Pricing cards -->
|
|
78
|
+
<Transition name="fade-swap" mode="out-in">
|
|
79
|
+
<div :key="activeTab + '-' + activePeriod" class="grid gap-6 lg:gap-8"
|
|
80
|
+
:class="activePlans.length === 1 ? 'grid-cols-1 justify-items-center' : 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'">
|
|
81
|
+
<div
|
|
82
|
+
v-for="(plan, i) in activePlans"
|
|
83
|
+
:key="plan.name"
|
|
84
|
+
class="pricing-reveal group relative"
|
|
85
|
+
:style="{ '--i': i }"
|
|
86
|
+
:class="[plan.highlight ? 'lg:-mt-6' : 'lg:mt-0', activePlans.length === 1 ? 'w-full max-w-sm' : '']"
|
|
87
|
+
>
|
|
88
|
+
<div
|
|
89
|
+
v-if="plan.highlight"
|
|
90
|
+
class="absolute -inset-3 bg-gradient-to-br from-[#004AC6]/25 via-blue-400/15 to-transparent rounded-[28px] blur-2xl opacity-70 -z-10"
|
|
91
|
+
></div>
|
|
92
|
+
|
|
93
|
+
<div
|
|
94
|
+
class="relative bg-white rounded-2xl flex flex-col overflow-hidden h-full transition-all duration-300 ease-out"
|
|
95
|
+
:class="plan.highlight
|
|
96
|
+
? 'ring-1 ring-[#004AC6]/15 shadow-[0_8px_30px_-6px_rgba(0,74,198,0.25)] hover:shadow-[0_20px_45px_-10px_rgba(0,74,198,0.35)] hover:-translate-y-1'
|
|
97
|
+
: 'ring-1 ring-slate-200 shadow-[0_2px_10px_-2px_rgba(15,23,42,0.06)] hover:ring-blue-200 hover:shadow-[0_12px_30px_-8px_rgba(15,23,42,0.12)] hover:-translate-y-1'
|
|
98
|
+
"
|
|
99
|
+
>
|
|
100
|
+
<div
|
|
101
|
+
v-if="plan.highlight"
|
|
102
|
+
class="bg-gradient-to-r from-[#004AC6] to-[#1D63E0] text-white text-[11px] font-bold py-2.5 text-center uppercase tracking-widest flex items-center justify-center gap-1.5"
|
|
103
|
+
>
|
|
104
|
+
<Icon name="lucide:star" class="w-3.5 h-3.5" />
|
|
105
|
+
Được chọn nhiều nhất
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div class="p-7 flex flex-col gap-4">
|
|
109
|
+
<div>
|
|
110
|
+
<h4 class="font-bold text-xl text-slate-900 mb-2">{{ plan.name }}</h4>
|
|
111
|
+
<p v-if="plan.tagline" class="text-slate-500 text-sm leading-relaxed">{{ plan.tagline }}</p>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="pb-1">
|
|
115
|
+
<span v-if="plan.contact" class="font-['Montserrat'] font-extrabold text-2xl text-[#004AC6]">
|
|
116
|
+
Liên hệ
|
|
117
|
+
</span>
|
|
118
|
+
<template v-else>
|
|
119
|
+
<span class="font-['Montserrat'] font-extrabold text-2xl sm:text-[28px] text-slate-900">
|
|
120
|
+
{{ plan.price }}
|
|
121
|
+
</span>
|
|
122
|
+
<span class="text-slate-400 text-sm">{{ plan.unit }}</span>
|
|
123
|
+
</template>
|
|
124
|
+
<p class="text-slate-400 text-xs mt-1.5">{{ plan.priceNote }}</p>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<p class="text-slate-500 text-sm leading-relaxed" v-html="plan.desc"></p>
|
|
128
|
+
|
|
129
|
+
<div class="flex flex-wrap gap-1.5">
|
|
130
|
+
<span
|
|
131
|
+
v-for="tag in plan.tags"
|
|
132
|
+
:key="tag"
|
|
133
|
+
class="text-[11px] font-medium text-slate-500 bg-slate-50 border border-slate-100 rounded-md px-2 py-1"
|
|
134
|
+
>
|
|
135
|
+
{{ tag }}
|
|
136
|
+
</span>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<ul class="flex flex-col gap-2.5 pt-4 border-t border-slate-100">
|
|
140
|
+
<li
|
|
141
|
+
v-for="feature in plan.features"
|
|
142
|
+
:key="feature"
|
|
143
|
+
class="flex items-start gap-2 text-sm text-slate-600"
|
|
144
|
+
>
|
|
145
|
+
<Icon name="lucide:check" class="w-4 h-4 text-[#004AC6] mt-0.5 shrink-0" />
|
|
146
|
+
{{ feature }}
|
|
147
|
+
</li>
|
|
148
|
+
</ul>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div class="p-7 pt-2 mt-auto">
|
|
152
|
+
<a
|
|
153
|
+
v-if="plan.url"
|
|
154
|
+
:href="plan.url"
|
|
155
|
+
target="_blank"
|
|
156
|
+
rel="noopener noreferrer"
|
|
157
|
+
class="w-full py-3.5 rounded-xl font-bold text-sm transition-all duration-300 cursor-pointer inline-flex items-center justify-center gap-2"
|
|
158
|
+
:class="plan.highlight
|
|
159
|
+
? 'bg-[#004AC6] text-white shadow-lg shadow-blue-500/25 hover:bg-[#1D4ED8] hover:shadow-blue-500/40'
|
|
160
|
+
: 'border border-slate-200 text-slate-600 hover:border-[#004AC6] hover:text-[#004AC6] hover:bg-blue-50/50'
|
|
161
|
+
"
|
|
162
|
+
>
|
|
163
|
+
{{ plan.titleButton || 'Đăng ký ngay' }}
|
|
164
|
+
<Icon name="lucide:arrow-up-right" class="w-4 h-4" />
|
|
165
|
+
</a>
|
|
166
|
+
<button
|
|
167
|
+
v-else
|
|
168
|
+
type="button"
|
|
169
|
+
disabled
|
|
170
|
+
class="w-full py-3.5 rounded-xl font-bold text-sm cursor-not-allowed opacity-50 border border-slate-200 text-slate-600"
|
|
171
|
+
>
|
|
172
|
+
{{ plan.titleButton || activeTabData.cta }}
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</Transition>
|
|
179
|
+
</div>
|
|
180
|
+
</Transition>
|
|
181
|
+
</div>
|
|
182
|
+
</section>
|
|
183
|
+
</template>
|
|
184
|
+
|
|
185
|
+
<script setup>
|
|
186
|
+
import { hciPlans, workshopPlans, trainingPlans } from '../data/pricingPlans'
|
|
187
|
+
|
|
188
|
+
const props = defineProps({
|
|
189
|
+
groupsItem01: { type: String, default: '' },
|
|
190
|
+
groupsItem02: { type: String, default: '' },
|
|
191
|
+
groupsItem03: { type: String, default: '' },
|
|
192
|
+
itemId01Groups01: { type: Object, default: null },
|
|
193
|
+
itemId02Groups01: { type: Object, default: null },
|
|
194
|
+
itemId03Groups01: { type: Object, default: null },
|
|
195
|
+
itemId01Groups02: { type: Object, default: null },
|
|
196
|
+
itemId02Groups02: { type: Object, default: null },
|
|
197
|
+
itemId03Groups02: { type: Object, default: null },
|
|
198
|
+
itemId01Groups03: { type: Object, default: null },
|
|
199
|
+
itemId02Groups03: { type: Object, default: null },
|
|
200
|
+
itemId03Groups03: { type: Object, default: null },
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
useScrollAnimate('.pricing-header', 'fade-up')
|
|
204
|
+
|
|
205
|
+
const sectionRoot = ref(null)
|
|
206
|
+
const revealed = ref(false)
|
|
207
|
+
|
|
208
|
+
onMounted(() => {
|
|
209
|
+
const observer = new IntersectionObserver(
|
|
210
|
+
(entries) => {
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
if (entry.isIntersecting) {
|
|
213
|
+
revealed.value = true
|
|
214
|
+
observer.disconnect()
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
{ threshold: 0.15 }
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if (sectionRoot.value) observer.observe(sectionRoot.value)
|
|
222
|
+
onUnmounted(() => observer.disconnect())
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
const activeTab = ref('hci')
|
|
226
|
+
const activePeriod = ref(36)
|
|
227
|
+
|
|
228
|
+
const groupLabel = (value, fallback) => (typeof value === 'string' && value.trim()) || fallback
|
|
229
|
+
|
|
230
|
+
const tabs = computed(() => [
|
|
231
|
+
{ id: 'hci', label: groupLabel(props.groupsItem01, 'Proxmox HCI'), icon: 'lucide:server-cog' },
|
|
232
|
+
{ id: 'workshop', label: groupLabel(props.groupsItem02, 'Workshop 1 ngày'), icon: 'lucide:zap' },
|
|
233
|
+
{ id: 'training', label: groupLabel(props.groupsItem03, 'Đào tạo Proxmox'), icon: 'lucide:layers' },
|
|
234
|
+
])
|
|
235
|
+
|
|
236
|
+
const billingPeriods = [
|
|
237
|
+
{ value: 12, label: 'Thuê 12 tháng' },
|
|
238
|
+
{ value: 24, label: 'Thuê 24 tháng' },
|
|
239
|
+
{ value: 36, label: 'Thuê 36 tháng' },
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
const hciInfoCards = [
|
|
243
|
+
{
|
|
244
|
+
icon: 'lucide:server',
|
|
245
|
+
title: 'Thuê trọn gói tại DC của chúng tôi',
|
|
246
|
+
desc: 'Cụm đặt tại trung tâm dữ liệu Tier III, gồm phần cứng, điện, mạng và vận hành. Bạn chỉ dùng và trả phí hàng tháng.',
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
icon: 'lucide:building-2',
|
|
250
|
+
title: 'Thuê phần cứng đặt tại doanh nghiệp',
|
|
251
|
+
desc: 'Chúng tôi mang thiết bị tới đặt tại phòng máy của bạn, triển khai và bảo hành. Dữ liệu nằm trong nội bộ, không cần đầu tư ban đầu.',
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
icon: 'lucide:refresh-cw',
|
|
255
|
+
title: 'Thuê linh hoạt theo node',
|
|
256
|
+
desc: 'Thêm hoặc bớt node theo từng tháng khi nhu cầu thay đổi. Có tùy chọn chuyển quyền sở hữu thiết bị khi hết hạn thuê.',
|
|
257
|
+
},
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
const tabContent = {
|
|
261
|
+
hci: {
|
|
262
|
+
intro: 'Thuê cụm siêu hội tụ theo tháng: không đầu tư ban đầu, đã bao gồm phần cứng, triển khai Ceph, HA và đội ngũ vận hành.',
|
|
263
|
+
cta: 'Nhận báo giá',
|
|
264
|
+
},
|
|
265
|
+
workshop: {
|
|
266
|
+
intro: 'Workshop cài đặt & triển khai Proxmox HCI trong 1 ngày, thực hành 100% trên lab thật.',
|
|
267
|
+
cta: 'Đăng ký workshop',
|
|
268
|
+
},
|
|
269
|
+
training: {
|
|
270
|
+
intro: 'Khóa học do giảng viên được Proxmox chứng nhận đứng lớp, kèm chứng nhận hoàn thành.',
|
|
271
|
+
cta: 'Đăng ký khóa học',
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const activeTabData = computed(() => tabContent[activeTab.value])
|
|
276
|
+
|
|
277
|
+
// CMS đổ item theo shape chung (title/time/short_description/config_product/productId, giống CoursesSection)
|
|
278
|
+
// -> chuẩn hoá qua normalizePlanItem rồi map lại đúng field template pricing card đang cần (desc/unit/tags/features)
|
|
279
|
+
const toPricingPlan = (item) => {
|
|
280
|
+
const normalized = normalizePlanItem(item)
|
|
281
|
+
if (!normalized) return null
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
id: normalized.id,
|
|
285
|
+
name: normalized.name,
|
|
286
|
+
tagline: normalized.tagline,
|
|
287
|
+
desc: normalized.description,
|
|
288
|
+
unit: normalized.duration,
|
|
289
|
+
price: normalized.price,
|
|
290
|
+
prices: normalized.prices,
|
|
291
|
+
priceNote: normalized.priceNote,
|
|
292
|
+
tags: normalized.features,
|
|
293
|
+
features: normalized.features,
|
|
294
|
+
highlight: normalized.highlight,
|
|
295
|
+
url: normalized.url,
|
|
296
|
+
titleButton: normalized.titleButton,
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const hciItems = computed(() =>
|
|
301
|
+
[props.itemId01Groups01, props.itemId02Groups01, props.itemId03Groups01].map(toPricingPlan).filter(Boolean)
|
|
302
|
+
)
|
|
303
|
+
const workshopItems = computed(() =>
|
|
304
|
+
[props.itemId01Groups02, props.itemId02Groups02, props.itemId03Groups02].map(toPricingPlan).filter(Boolean)
|
|
305
|
+
)
|
|
306
|
+
const trainingItems = computed(() =>
|
|
307
|
+
[props.itemId01Groups03, props.itemId02Groups03, props.itemId03Groups03].map(toPricingPlan).filter(Boolean)
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
// Chỉ 1 gói -> tự động highlight (không có gói khác để so sánh nên luôn là gói nổi bật)
|
|
311
|
+
const withSingleHighlight = (plans) => (plans.length === 1 ? [{ ...plans[0], highlight: true }] : plans)
|
|
312
|
+
|
|
313
|
+
const activePlans = computed(() => {
|
|
314
|
+
if (activeTab.value === 'workshop') {
|
|
315
|
+
return withSingleHighlight(workshopItems.value.length > 0 ? workshopItems.value : workshopPlans)
|
|
316
|
+
}
|
|
317
|
+
if (activeTab.value === 'training') {
|
|
318
|
+
return withSingleHighlight(trainingItems.value.length > 0 ? trainingItems.value : trainingPlans)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const source = hciItems.value.length > 0 ? hciItems.value : hciPlans
|
|
322
|
+
return withSingleHighlight(
|
|
323
|
+
source.map((plan) => ({
|
|
324
|
+
...plan,
|
|
325
|
+
price: plan.prices?.[activePeriod.value],
|
|
326
|
+
priceNote: `Giá thuê khi ký hợp đồng ${activePeriod.value} tháng`,
|
|
327
|
+
}))
|
|
328
|
+
)
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
</script>
|
|
332
|
+
|
|
333
|
+
<style scoped>
|
|
334
|
+
.pricing-reveal {
|
|
335
|
+
animation: pricing-card-in 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
336
|
+
animation-delay: calc(var(--i, 0) * 110ms);
|
|
337
|
+
animation-play-state: paused;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.pricing-revealed .pricing-reveal {
|
|
341
|
+
animation-play-state: running;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
@keyframes pricing-card-in {
|
|
345
|
+
from {
|
|
346
|
+
opacity: 0;
|
|
347
|
+
transform: translateY(24px);
|
|
348
|
+
}
|
|
349
|
+
to {
|
|
350
|
+
opacity: 1;
|
|
351
|
+
transform: translateY(0);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.fade-swap-enter-active,
|
|
356
|
+
.fade-swap-leave-active {
|
|
357
|
+
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.fade-swap-enter-from {
|
|
361
|
+
opacity: 0;
|
|
362
|
+
transform: translateY(10px);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.fade-swap-leave-to {
|
|
366
|
+
opacity: 0;
|
|
367
|
+
transform: translateY(-6px);
|
|
368
|
+
}
|
|
369
|
+
</style>
|