nuxt-layer-core 1.0.8 → 1.1.0

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.
@@ -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="activeVersionId = 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 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="handleSelectPackage(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
- vấn gói này
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
- // Chuẩn hoá 1 nhóm sản phẩm từ CMS (item_id_0X_groups_0Y)
200
- const normalizeGroup = (raw) => {
201
- if (!raw || typeof raw !== 'object' || !raw.name) return null
202
-
203
- return {
204
- id: raw.id || raw.name,
205
- name: raw.name,
206
- icon: raw.icon || 'lucide:server-cog',
207
- desc: raw.desc || '',
208
- packages: Array.isArray(raw.packages) ? raw.packages : [],
209
- }
210
- }
211
-
212
- // 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)
213
- // Không có data CMS cho slot nào -> bỏ qua slot đó (không dùng data giả để fallback nữa)
214
- const buildVersion = (meta, groupSlots) => {
215
- if (!meta || typeof meta !== 'object' || !meta.id) return null
216
-
217
- const groups = groupSlots.map(normalizeGroup).filter(Boolean)
218
-
219
- return {
220
- ...meta,
221
- groups,
222
- }
223
- }
224
-
225
- const resolvedVersions = computed(() =>
226
- [
227
- buildVersion(props.groupsItem01, [props.itemId01Groups01, props.itemId02Groups01, props.itemId03Groups01]),
228
- buildVersion(props.groupsItem02, [props.itemId01Groups02, props.itemId02Groups02, props.itemId03Groups02]),
229
- buildVersion(props.groupsItem03, [props.itemId01Groups03, props.itemId02Groups03, props.itemId03Groups03]),
230
- ].filter(Boolean)
231
- )
232
-
233
- const isValidVersion = (id) => resolvedVersions.value.some((v) => v.id === id)
234
-
235
- const activeVersionId = ref(
236
- isValidVersion(props.activeVersionId)
237
- ? props.activeVersionId
238
- : resolvedVersions.value.find((v) => v.recommended)?.id || resolvedVersions.value[0]?.id || ''
239
- )
240
-
241
- const emptyVersion = { version: '', codename: '', badge: '', badgeTone: 'slate', releaseDate: '', supportUntil: '', desc: '', groups: [] }
242
-
243
- const activeVersion = computed(
244
- () =>
245
- resolvedVersions.value.find((v) => v.id === activeVersionId.value) ||
246
- resolvedVersions.value[0] ||
247
- emptyVersion
248
- )
249
-
250
- const badgeToneMap = {
251
- primary: { pill: 'bg-[#004AC6]', text: 'text-[#004AC6]', icon: '#004AC6' },
252
- amber: { pill: 'bg-amber-500', text: 'text-amber-600', icon: '#d97706' },
253
- slate: { pill: 'bg-slate-500', text: 'text-slate-600', icon: '#475569' },
254
- }
255
-
256
- const toneOf = (version) => badgeToneMap[version.badgeTone] || badgeToneMap.slate
257
-
258
- function handleSelectPackage(pkg, group) {
259
- emit('select-package', {
260
- id: pkg.id,
261
- name: pkg.name,
262
- level: `Ubuntu ${activeVersion.value.version} · ${group.name}`,
263
- duration: pkg.unit,
264
- icon: 'lucide:server-cog',
265
- })
266
- }
267
-
268
- function formatPrice(value) {
269
- if (!value && value !== 0) return ''
270
- return new Intl.NumberFormat('vi-VN').format(value)
271
- }
272
-
273
- const sectionRoot = ref(null)
274
- const revealed = ref(false)
275
-
276
- onMounted(() => {
277
- const observer = new IntersectionObserver(
278
- (entries) => {
279
- for (const entry of entries) {
280
- if (entry.isIntersecting) {
281
- revealed.value = true
282
- observer.disconnect()
283
- }
284
- }
285
- },
286
- { threshold: 0.15 }
287
- )
288
-
289
- if (sectionRoot.value) observer.observe(sectionRoot.value)
290
- onUnmounted(() => observer.disconnect())
291
- })
292
- </script>
293
-
294
- <style scoped>
295
- .version-reveal {
296
- animation: version-group-in 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
297
- animation-delay: calc(var(--i, 0) * 120ms);
298
- animation-play-state: paused;
299
- }
300
-
301
- .versions-revealed .version-reveal {
302
- animation-play-state: running;
303
- }
304
-
305
- @keyframes version-group-in {
306
- from {
307
- opacity: 0;
308
- transform: translateY(24px);
309
- }
310
-
311
- to {
312
- opacity: 1;
313
- transform: translateY(0);
314
- }
315
- }
316
-
317
- .fade-swap-enter-active,
318
- .fade-swap-leave-active {
319
- transition: opacity 0.3s ease, transform 0.3s ease;
320
- }
321
-
322
- .fade-swap-enter-from {
323
- opacity: 0;
324
- transform: translateY(10px);
325
- }
326
-
327
- .fade-swap-leave-to {
328
- opacity: 0;
329
- transform: translateY(-6px);
330
- }
331
- </style>
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 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 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>