nuxt-layer-core 1.0.8 → 1.1.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/app/layers/solution/app/components/LinuxVersionsSection.vue +349 -331
- package/app/layers/solution/app/components/RegistrationForm.vue +285 -283
- package/app/layers/solution/app/components/proxmox/ComparisonSection.vue +1 -1
- package/app/layers/solution/app/components/proxmox/HciOverviewSection.vue +1 -1
- package/app/layers/solution/app/components/proxmox/PainPointsSection.vue +43 -0
- package/app/layers/solution/app/components/proxmox/TrainingBenefitsSection.vue +1 -1
- package/app/layers/solution/app/composables/useCrmApi.ts +35 -32
- package/app/layers/solution/app/data/proxmox-content.ts +196 -158
- package/app/layers/solution/app/pages/giai-phap-proxmox/[slug]/proxmox-hci/index.vue.disabled +1 -0
- package/package.json +1 -1
|
@@ -1,331 +1,349 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<section id="phien-ban" ref="sectionRoot" class="py-24 bg-[#F7F9FB] overflow-hidden"
|
|
3
|
-
:class="{ 'versions-revealed': revealed }">
|
|
4
|
-
<div class="max-w-6xl mx-auto px-6 lg:px-8">
|
|
5
|
-
<div class="text-center mb-12 versions-header">
|
|
6
|
-
<h2 class="text-[#004AC6] font-bold uppercase tracking-widest text-sm mb-2">Phiên bản hỗ trợ</h2>
|
|
7
|
-
<h3 class="font-['Montserrat'] font-extrabold text-2xl sm:text-4xl text-slate-900 mb-4">
|
|
8
|
-
Chọn phiên bản Ubuntu<br class="hidden sm:block" />
|
|
9
|
-
phù hợp với hệ thống của bạn
|
|
10
|
-
</h3>
|
|
11
|
-
<p class="text-slate-500 max-w-2xl mx-auto leading-relaxed">
|
|
12
|
-
Mỗi phiên bản Ubuntu có vòng đời hỗ trợ khác nhau. Chọn một phiên bản để triển khai,
|
|
13
|
-
sau đó chọn gói dịch vụ tương ứng bên dưới.
|
|
14
|
-
</p>
|
|
15
|
-
</div>
|
|
16
|
-
|
|
17
|
-
<!-- Version cards -->
|
|
18
|
-
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16">
|
|
19
|
-
<button
|
|
20
|
-
v-for="version in resolvedVersions"
|
|
21
|
-
:key="version.id"
|
|
22
|
-
type="button"
|
|
23
|
-
@click="
|
|
24
|
-
class="version-reveal relative text-left bg-white rounded-2xl border p-6 flex flex-col gap-4 transition-all duration-300 cursor-pointer"
|
|
25
|
-
:style="{ '--i': 0 }"
|
|
26
|
-
:class="activeVersionId === version.id
|
|
27
|
-
? 'border-[#004AC6] ring-1 ring-[#004AC6]/25 shadow-[0_12px_30px_-8px_rgba(0,74,198,0.25)]'
|
|
28
|
-
: 'border-slate-200 hover:border-blue-200 hover:shadow-lg'
|
|
29
|
-
"
|
|
30
|
-
>
|
|
31
|
-
<span
|
|
32
|
-
v-if="activeVersionId === version.id"
|
|
33
|
-
class="absolute -top-2.5 -right-2.5 w-7 h-7 rounded-full bg-[#004AC6] text-white flex items-center justify-center shadow-md"
|
|
34
|
-
>
|
|
35
|
-
<Icon name="lucide:check" class="w-4 h-4" />
|
|
36
|
-
</span>
|
|
37
|
-
|
|
38
|
-
<div class="flex items-center justify-between">
|
|
39
|
-
<UbuntuMark :size="32" :stroke="toneOf(version).icon" :accent="toneOf(version).icon" />
|
|
40
|
-
<span
|
|
41
|
-
class="text-[11px] font-bold uppercase tracking-wide px-2.5 py-1 rounded-full text-white"
|
|
42
|
-
:class="toneOf(version).pill"
|
|
43
|
-
>
|
|
44
|
-
{{ version.badge }}
|
|
45
|
-
</span>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
<div>
|
|
49
|
-
<h4 class="font-bold text-lg text-slate-900">Ubuntu {{ version.version }}</h4>
|
|
50
|
-
<p class="text-sm font-semibold" :class="toneOf(version).text">{{ version.codename }}</p>
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
|
-
<p class="text-sm text-slate-500 leading-relaxed grow">{{ version.desc }}</p>
|
|
54
|
-
|
|
55
|
-
<div class="text-xs text-slate-400 space-y-1 pt-3 border-t border-slate-100">
|
|
56
|
-
<p>Ra mắt: {{ version.releaseDate }}</p>
|
|
57
|
-
<p class="font-semibold text-slate-600">{{ version.supportUntil }}</p>
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
<span
|
|
61
|
-
class="mt-1 inline-flex items-center justify-center gap-2 w-full py-2.5 rounded-xl font-bold text-sm transition-all duration-300"
|
|
62
|
-
:class="activeVersionId === version.id
|
|
63
|
-
? 'bg-[#004AC6] text-white shadow-lg shadow-blue-500/25'
|
|
64
|
-
: 'border border-slate-200 text-slate-600'
|
|
65
|
-
"
|
|
66
|
-
>
|
|
67
|
-
<Icon name="lucide:arrow-right" class="w-4 h-4" />
|
|
68
|
-
Triển khai
|
|
69
|
-
</span>
|
|
70
|
-
</button>
|
|
71
|
-
</div>
|
|
72
|
-
|
|
73
|
-
<!-- Product groups for the selected version -->
|
|
74
|
-
<div class="text-center mb-10">
|
|
75
|
-
<h4 class="font-['Montserrat'] font-extrabold text-xl sm:text-2xl text-slate-900">
|
|
76
|
-
Nhóm sản phẩm hỗ trợ trên Ubuntu {{ activeVersion.version }}
|
|
77
|
-
</h4>
|
|
78
|
-
</div>
|
|
79
|
-
|
|
80
|
-
<Transition name="fade-swap" mode="out-in">
|
|
81
|
-
<div :key="activeVersionId" class="flex flex-col gap-14">
|
|
82
|
-
<div
|
|
83
|
-
v-for="(group, gi) in activeVersion.groups"
|
|
84
|
-
:key="group.id"
|
|
85
|
-
class="version-reveal"
|
|
86
|
-
:style="{ '--i': gi + 1 }"
|
|
87
|
-
>
|
|
88
|
-
<div class="flex items-start gap-3 mb-6">
|
|
89
|
-
<div class="w-10 h-10 rounded-[10px] bg-blue-50 flex items-center justify-center text-[#004AC6] shrink-0">
|
|
90
|
-
<Icon :name="group.icon" class="w-5 h-5" />
|
|
91
|
-
</div>
|
|
92
|
-
<div>
|
|
93
|
-
<h4 class="font-bold text-lg text-slate-900">{{ group.name }}</h4>
|
|
94
|
-
<p class="text-sm text-slate-500 mt-0.5">{{ group.desc }}</p>
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
|
-
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
99
|
-
<div
|
|
100
|
-
v-for="pkg in group.packages"
|
|
101
|
-
:key="pkg.id"
|
|
102
|
-
class="relative bg-white rounded-2xl flex flex-col overflow-hidden transition-all duration-300 ease-out"
|
|
103
|
-
:class="pkg.highlight
|
|
104
|
-
? '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'
|
|
105
|
-
: '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'
|
|
106
|
-
"
|
|
107
|
-
>
|
|
108
|
-
<div
|
|
109
|
-
v-if="pkg.highlight"
|
|
110
|
-
class="bg-gradient-to-r from-[#004AC6] to-[#1D63E0] text-white text-[11px] font-bold py-2 text-center uppercase tracking-widest flex items-center justify-center gap-1.5"
|
|
111
|
-
>
|
|
112
|
-
<Icon name="lucide:star" class="w-3.5 h-3.5" />
|
|
113
|
-
Phổ biến nhất
|
|
114
|
-
</div>
|
|
115
|
-
|
|
116
|
-
<div class="p-6 flex flex-col gap-4 grow">
|
|
117
|
-
<div>
|
|
118
|
-
<h5 class="font-bold text-slate-900 mb-1.5">{{ pkg.name }}</h5>
|
|
119
|
-
<p class="text-slate-500 text-sm leading-relaxed">{{ pkg.desc }}</p>
|
|
120
|
-
</div>
|
|
121
|
-
|
|
122
|
-
<div class="pb-1">
|
|
123
|
-
<span class="font-['Montserrat'] font-extrabold text-xl text-slate-900">
|
|
124
|
-
{{ formatPrice(pkg.price) }}đ
|
|
125
|
-
</span>
|
|
126
|
-
<span class="text-slate-400 text-sm">{{ pkg.unit }}</span>
|
|
127
|
-
</div>
|
|
128
|
-
|
|
129
|
-
<div class="flex flex-wrap gap-1.5">
|
|
130
|
-
<span
|
|
131
|
-
v-for="tag in pkg.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 pt-3 border-t border-slate-100">
|
|
140
|
-
<li
|
|
141
|
-
v-for="feature in pkg.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-6 pt-2">
|
|
152
|
-
<button
|
|
153
|
-
type="button"
|
|
154
|
-
@click="
|
|
155
|
-
class="w-full py-3 rounded-xl font-bold text-sm transition-all duration-300 cursor-pointer"
|
|
156
|
-
:class="pkg.highlight
|
|
157
|
-
? 'bg-[#004AC6] text-white shadow-lg shadow-blue-500/25 hover:bg-[#1D4ED8] hover:shadow-blue-500/40'
|
|
158
|
-
: 'border border-slate-200 text-slate-600 hover:border-[#004AC6] hover:text-[#004AC6] hover:bg-blue-50/50'
|
|
159
|
-
"
|
|
160
|
-
>
|
|
161
|
-
|
|
162
|
-
</button>
|
|
163
|
-
</div>
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
168
|
-
</Transition>
|
|
169
|
-
</div>
|
|
170
|
-
</section>
|
|
171
|
-
</template>
|
|
172
|
-
|
|
173
|
-
<script setup>
|
|
174
|
-
// Data giả dùng khi chưa cấu hình CMS - đã tắt vì CMS đã có data thật, giữ lại để bật lại khi cần
|
|
175
|
-
// import { ubuntuVersions as fallbackVersions } from '../data/linuxVersions'
|
|
176
|
-
|
|
177
|
-
const props = defineProps({
|
|
178
|
-
activeVersionId: { type: String, default: '' },
|
|
179
|
-
// groupsItem0X: JSON thông tin 1 phiên bản Ubuntu (id, version, codename, badge, badgeTone, recommended, releaseDate, supportUntil, desc)
|
|
180
|
-
groupsItem01: { type: Object, default: null },
|
|
181
|
-
groupsItem02: { type: Object, default: null },
|
|
182
|
-
groupsItem03: { type: Object, default: null },
|
|
183
|
-
// itemId0X_groups_0Y: JSON 1 nhóm sản phẩm (id, name, icon, desc, packages[]) thuộc phiên bản groupsItem0Y
|
|
184
|
-
itemId01Groups01: { type: Object, default: null },
|
|
185
|
-
itemId02Groups01: { type: Object, default: null },
|
|
186
|
-
itemId03Groups01: { type: Object, default: null },
|
|
187
|
-
itemId01Groups02: { type: Object, default: null },
|
|
188
|
-
itemId02Groups02: { type: Object, default: null },
|
|
189
|
-
itemId03Groups02: { type: Object, default: null },
|
|
190
|
-
itemId01Groups03: { type: Object, default: null },
|
|
191
|
-
itemId02Groups03: { type: Object, default: null },
|
|
192
|
-
itemId03Groups03: { type: Object, default: null },
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
const emit = defineEmits(['select-package'])
|
|
196
|
-
|
|
197
|
-
useScrollAnimate('.versions-header', 'fade-up')
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
buildVersion(props.
|
|
230
|
-
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const
|
|
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
|
-
|
|
1
|
+
<template>
|
|
2
|
+
<section id="phien-ban" ref="sectionRoot" class="py-24 bg-[#F7F9FB] overflow-hidden"
|
|
3
|
+
:class="{ 'versions-revealed': revealed }">
|
|
4
|
+
<div class="max-w-6xl mx-auto px-6 lg:px-8">
|
|
5
|
+
<div class="text-center mb-12 versions-header">
|
|
6
|
+
<h2 class="text-[#004AC6] font-bold uppercase tracking-widest text-sm mb-2">Phiên bản hỗ trợ</h2>
|
|
7
|
+
<h3 class="font-['Montserrat'] font-extrabold text-2xl sm:text-4xl text-slate-900 mb-4">
|
|
8
|
+
Chọn phiên bản Ubuntu<br class="hidden sm:block" />
|
|
9
|
+
phù hợp với hệ thống của bạn
|
|
10
|
+
</h3>
|
|
11
|
+
<p class="text-slate-500 max-w-2xl mx-auto leading-relaxed">
|
|
12
|
+
Mỗi phiên bản Ubuntu có vòng đời hỗ trợ khác nhau. Chọn một phiên bản để triển khai,
|
|
13
|
+
sau đó chọn gói dịch vụ tương ứng bên dưới.
|
|
14
|
+
</p>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<!-- Version cards -->
|
|
18
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16">
|
|
19
|
+
<button
|
|
20
|
+
v-for="version in resolvedVersions"
|
|
21
|
+
:key="version.id"
|
|
22
|
+
type="button"
|
|
23
|
+
@click="selectVersion(version.id)"
|
|
24
|
+
class="version-reveal relative text-left bg-white rounded-2xl border p-6 flex flex-col gap-4 transition-all duration-300 cursor-pointer"
|
|
25
|
+
:style="{ '--i': 0 }"
|
|
26
|
+
:class="activeVersionId === version.id
|
|
27
|
+
? 'border-[#004AC6] ring-1 ring-[#004AC6]/25 shadow-[0_12px_30px_-8px_rgba(0,74,198,0.25)]'
|
|
28
|
+
: 'border-slate-200 hover:border-blue-200 hover:shadow-lg'
|
|
29
|
+
"
|
|
30
|
+
>
|
|
31
|
+
<span
|
|
32
|
+
v-if="activeVersionId === version.id"
|
|
33
|
+
class="absolute -top-2.5 -right-2.5 w-7 h-7 rounded-full bg-[#004AC6] text-white flex items-center justify-center shadow-md"
|
|
34
|
+
>
|
|
35
|
+
<Icon name="lucide:check" class="w-4 h-4" />
|
|
36
|
+
</span>
|
|
37
|
+
|
|
38
|
+
<div class="flex items-center justify-between">
|
|
39
|
+
<UbuntuMark :size="32" :stroke="toneOf(version).icon" :accent="toneOf(version).icon" />
|
|
40
|
+
<span
|
|
41
|
+
class="text-[11px] font-bold uppercase tracking-wide px-2.5 py-1 rounded-full text-white"
|
|
42
|
+
:class="toneOf(version).pill"
|
|
43
|
+
>
|
|
44
|
+
{{ version.badge }}
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div>
|
|
49
|
+
<h4 class="font-bold text-lg text-slate-900">Ubuntu {{ version.version }}</h4>
|
|
50
|
+
<p class="text-sm font-semibold" :class="toneOf(version).text">{{ version.codename }}</p>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<p class="text-sm text-slate-500 leading-relaxed grow">{{ version.desc }}</p>
|
|
54
|
+
|
|
55
|
+
<div class="text-xs text-slate-400 space-y-1 pt-3 border-t border-slate-100">
|
|
56
|
+
<p>Ra mắt: {{ version.releaseDate }}</p>
|
|
57
|
+
<p class="font-semibold text-slate-600">{{ version.supportUntil }}</p>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<span
|
|
61
|
+
class="mt-1 inline-flex items-center justify-center gap-2 w-full py-2.5 rounded-xl font-bold text-sm transition-all duration-300"
|
|
62
|
+
:class="activeVersionId === version.id
|
|
63
|
+
? 'bg-[#004AC6] text-white shadow-lg shadow-blue-500/25'
|
|
64
|
+
: 'border border-slate-200 text-slate-600'
|
|
65
|
+
"
|
|
66
|
+
>
|
|
67
|
+
<Icon name="lucide:arrow-right" class="w-4 h-4" />
|
|
68
|
+
Triển khai
|
|
69
|
+
</span>
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<!-- Product groups for the selected version -->
|
|
74
|
+
<div id="nhom-san-pham" class="text-center mb-10 scroll-mt-24">
|
|
75
|
+
<h4 class="font-['Montserrat'] font-extrabold text-xl sm:text-2xl text-slate-900">
|
|
76
|
+
Nhóm sản phẩm hỗ trợ trên Ubuntu {{ activeVersion.version }}
|
|
77
|
+
</h4>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<Transition name="fade-swap" mode="out-in">
|
|
81
|
+
<div :key="activeVersionId" class="flex flex-col gap-14">
|
|
82
|
+
<div
|
|
83
|
+
v-for="(group, gi) in activeVersion.groups"
|
|
84
|
+
:key="group.id"
|
|
85
|
+
class="version-reveal"
|
|
86
|
+
:style="{ '--i': gi + 1 }"
|
|
87
|
+
>
|
|
88
|
+
<div class="flex items-start gap-3 mb-6">
|
|
89
|
+
<div class="w-10 h-10 rounded-[10px] bg-blue-50 flex items-center justify-center text-[#004AC6] shrink-0">
|
|
90
|
+
<Icon :name="group.icon" class="w-5 h-5" />
|
|
91
|
+
</div>
|
|
92
|
+
<div>
|
|
93
|
+
<h4 class="font-bold text-lg text-slate-900">{{ group.name }}</h4>
|
|
94
|
+
<p class="text-sm text-slate-500 mt-0.5">{{ group.desc }}</p>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
99
|
+
<div
|
|
100
|
+
v-for="pkg in group.packages"
|
|
101
|
+
:key="pkg.id"
|
|
102
|
+
class="relative bg-white rounded-2xl flex flex-col overflow-hidden transition-all duration-300 ease-out"
|
|
103
|
+
:class="pkg.highlight
|
|
104
|
+
? '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'
|
|
105
|
+
: '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'
|
|
106
|
+
"
|
|
107
|
+
>
|
|
108
|
+
<div
|
|
109
|
+
v-if="pkg.highlight"
|
|
110
|
+
class="bg-gradient-to-r from-[#004AC6] to-[#1D63E0] text-white text-[11px] font-bold py-2 text-center uppercase tracking-widest flex items-center justify-center gap-1.5"
|
|
111
|
+
>
|
|
112
|
+
<Icon name="lucide:star" class="w-3.5 h-3.5" />
|
|
113
|
+
Phổ biến nhất
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div class="p-6 flex flex-col gap-4 grow">
|
|
117
|
+
<div>
|
|
118
|
+
<h5 class="font-bold text-slate-900 mb-1.5">{{ pkg.name }}</h5>
|
|
119
|
+
<p class="text-slate-500 text-sm leading-relaxed">{{ pkg.desc }}</p>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="pb-1">
|
|
123
|
+
<span class="font-['Montserrat'] font-extrabold text-xl text-slate-900">
|
|
124
|
+
{{ formatPrice(pkg.price) }}đ
|
|
125
|
+
</span>
|
|
126
|
+
<span class="text-slate-400 text-sm">{{ pkg.unit }}</span>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div class="flex flex-wrap gap-1.5">
|
|
130
|
+
<span
|
|
131
|
+
v-for="tag in pkg.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 pt-3 border-t border-slate-100">
|
|
140
|
+
<li
|
|
141
|
+
v-for="feature in pkg.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-6 pt-2">
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
@click="handlePackageClick(pkg, group)"
|
|
155
|
+
class="w-full py-3 rounded-xl font-bold text-sm transition-all duration-300 cursor-pointer"
|
|
156
|
+
:class="pkg.highlight
|
|
157
|
+
? 'bg-[#004AC6] text-white shadow-lg shadow-blue-500/25 hover:bg-[#1D4ED8] hover:shadow-blue-500/40'
|
|
158
|
+
: 'border border-slate-200 text-slate-600 hover:border-[#004AC6] hover:text-[#004AC6] hover:bg-blue-50/50'
|
|
159
|
+
"
|
|
160
|
+
>
|
|
161
|
+
Đăng ký ngay
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</Transition>
|
|
169
|
+
</div>
|
|
170
|
+
</section>
|
|
171
|
+
</template>
|
|
172
|
+
|
|
173
|
+
<script setup>
|
|
174
|
+
// Data giả dùng khi chưa cấu hình CMS - đã tắt vì CMS đã có data thật, giữ lại để bật lại khi cần
|
|
175
|
+
// import { ubuntuVersions as fallbackVersions } from '../data/linuxVersions'
|
|
176
|
+
|
|
177
|
+
const props = defineProps({
|
|
178
|
+
activeVersionId: { type: String, default: '' },
|
|
179
|
+
// groupsItem0X: JSON thông tin 1 phiên bản Ubuntu (id, version, codename, badge, badgeTone, recommended, releaseDate, supportUntil, desc)
|
|
180
|
+
groupsItem01: { type: Object, default: null },
|
|
181
|
+
groupsItem02: { type: Object, default: null },
|
|
182
|
+
groupsItem03: { type: Object, default: null },
|
|
183
|
+
// itemId0X_groups_0Y: JSON 1 nhóm sản phẩm (id, name, icon, desc, packages[]) thuộc phiên bản groupsItem0Y
|
|
184
|
+
itemId01Groups01: { type: Object, default: null },
|
|
185
|
+
itemId02Groups01: { type: Object, default: null },
|
|
186
|
+
itemId03Groups01: { type: Object, default: null },
|
|
187
|
+
itemId01Groups02: { type: Object, default: null },
|
|
188
|
+
itemId02Groups02: { type: Object, default: null },
|
|
189
|
+
itemId03Groups02: { type: Object, default: null },
|
|
190
|
+
itemId01Groups03: { type: Object, default: null },
|
|
191
|
+
itemId02Groups03: { type: Object, default: null },
|
|
192
|
+
itemId03Groups03: { type: Object, default: null },
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const emit = defineEmits(['select-package'])
|
|
196
|
+
|
|
197
|
+
useScrollAnimate('.versions-header', 'fade-up')
|
|
198
|
+
|
|
199
|
+
const { scrollTo } = useScrollToSection()
|
|
200
|
+
|
|
201
|
+
// Chuẩn hoá 1 nhóm sản phẩm từ CMS (item_id_0X_groups_0Y)
|
|
202
|
+
const normalizeGroup = (raw) => {
|
|
203
|
+
if (!raw || typeof raw !== 'object' || !raw.name) return null
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
id: raw.id || raw.name,
|
|
207
|
+
name: raw.name,
|
|
208
|
+
icon: raw.icon || 'lucide:server-cog',
|
|
209
|
+
desc: raw.desc || '',
|
|
210
|
+
packages: Array.isArray(raw.packages) ? raw.packages : [],
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Ghép 1 phiên bản Ubuntu (groups_item_0X) + 3 nhóm sản phẩm của nó (item_id_0X_groups_0Y)
|
|
215
|
+
// Không có data CMS cho slot nào -> bỏ qua slot đó (không dùng data giả để fallback nữa)
|
|
216
|
+
const buildVersion = (meta, groupSlots) => {
|
|
217
|
+
if (!meta || typeof meta !== 'object' || !meta.id) return null
|
|
218
|
+
|
|
219
|
+
const groups = groupSlots.map(normalizeGroup).filter(Boolean)
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
...meta,
|
|
223
|
+
groups,
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const resolvedVersions = computed(() =>
|
|
228
|
+
[
|
|
229
|
+
buildVersion(props.groupsItem01, [props.itemId01Groups01, props.itemId02Groups01, props.itemId03Groups01]),
|
|
230
|
+
buildVersion(props.groupsItem02, [props.itemId01Groups02, props.itemId02Groups02, props.itemId03Groups02]),
|
|
231
|
+
buildVersion(props.groupsItem03, [props.itemId01Groups03, props.itemId02Groups03, props.itemId03Groups03]),
|
|
232
|
+
].filter(Boolean)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
const isValidVersion = (id) => resolvedVersions.value.some((v) => v.id === id)
|
|
236
|
+
|
|
237
|
+
const activeVersionId = ref(
|
|
238
|
+
isValidVersion(props.activeVersionId)
|
|
239
|
+
? props.activeVersionId
|
|
240
|
+
: resolvedVersions.value.find((v) => v.recommended)?.id || resolvedVersions.value[0]?.id || ''
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
const emptyVersion = { version: '', codename: '', badge: '', badgeTone: 'slate', releaseDate: '', supportUntil: '', desc: '', groups: [] }
|
|
244
|
+
|
|
245
|
+
const activeVersion = computed(
|
|
246
|
+
() =>
|
|
247
|
+
resolvedVersions.value.find((v) => v.id === activeVersionId.value) ||
|
|
248
|
+
resolvedVersions.value[0] ||
|
|
249
|
+
emptyVersion
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
const badgeToneMap = {
|
|
253
|
+
primary: { pill: 'bg-[#004AC6]', text: 'text-[#004AC6]', icon: '#004AC6' },
|
|
254
|
+
amber: { pill: 'bg-amber-500', text: 'text-amber-600', icon: '#d97706' },
|
|
255
|
+
slate: { pill: 'bg-slate-500', text: 'text-slate-600', icon: '#475569' },
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const toneOf = (version) => badgeToneMap[version.badgeTone] || badgeToneMap.slate
|
|
259
|
+
|
|
260
|
+
// Chọn phiên bản rồi cuộn xuống đúng khu vực nhóm sản phẩm bên dưới để người dùng thấy ngay kết quả
|
|
261
|
+
function selectVersion(id) {
|
|
262
|
+
activeVersionId.value = id
|
|
263
|
+
nextTick(() => scrollTo('nhom-san-pham'))
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Gói có sẵn url (đăng ký/mua trực tiếp) -> điều hướng thẳng tới đó; không có thì báo lên trang cha để mở form tư vấn
|
|
267
|
+
function handlePackageClick(pkg, group) {
|
|
268
|
+
if (pkg.url) {
|
|
269
|
+
window.location.href = pkg.url
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
handleSelectPackage(pkg, group)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function handleSelectPackage(pkg, group) {
|
|
277
|
+
emit('select-package', {
|
|
278
|
+
id: pkg.id,
|
|
279
|
+
name: pkg.name,
|
|
280
|
+
level: `Ubuntu ${activeVersion.value.version} · ${group.name}`,
|
|
281
|
+
duration: pkg.unit,
|
|
282
|
+
icon: 'lucide:server-cog',
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function formatPrice(value) {
|
|
287
|
+
if (!value && value !== 0) return ''
|
|
288
|
+
return new Intl.NumberFormat('vi-VN').format(value)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const sectionRoot = ref(null)
|
|
292
|
+
const revealed = ref(false)
|
|
293
|
+
|
|
294
|
+
onMounted(() => {
|
|
295
|
+
const observer = new IntersectionObserver(
|
|
296
|
+
(entries) => {
|
|
297
|
+
for (const entry of entries) {
|
|
298
|
+
if (entry.isIntersecting) {
|
|
299
|
+
revealed.value = true
|
|
300
|
+
observer.disconnect()
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
{ threshold: 0.15 }
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
if (sectionRoot.value) observer.observe(sectionRoot.value)
|
|
308
|
+
onUnmounted(() => observer.disconnect())
|
|
309
|
+
})
|
|
310
|
+
</script>
|
|
311
|
+
|
|
312
|
+
<style scoped>
|
|
313
|
+
.version-reveal {
|
|
314
|
+
animation: version-group-in 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
315
|
+
animation-delay: calc(var(--i, 0) * 120ms);
|
|
316
|
+
animation-play-state: paused;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.versions-revealed .version-reveal {
|
|
320
|
+
animation-play-state: running;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
@keyframes version-group-in {
|
|
324
|
+
from {
|
|
325
|
+
opacity: 0;
|
|
326
|
+
transform: translateY(24px);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
to {
|
|
330
|
+
opacity: 1;
|
|
331
|
+
transform: translateY(0);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.fade-swap-enter-active,
|
|
336
|
+
.fade-swap-leave-active {
|
|
337
|
+
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.fade-swap-enter-from {
|
|
341
|
+
opacity: 0;
|
|
342
|
+
transform: translateY(10px);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.fade-swap-leave-to {
|
|
346
|
+
opacity: 0;
|
|
347
|
+
transform: translateY(-6px);
|
|
348
|
+
}
|
|
349
|
+
</style>
|