nuxt-layer-core 2.0.1 → 2.0.3
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 +42 -43
- package/app/layers/solution/app/components/CtaSection.vue +40 -22
- package/app/layers/solution/app/components/DynamicPageSections.vue +103 -0
- package/app/layers/solution/app/components/PricingPlansSection.vue +108 -146
- package/app/layers/solution/app/components/RegistrationForm.vue +33 -6
- package/app/layers/solution/app/components/RegistrationSection.vue +24 -0
- package/app/layers/solution/app/components/RegistrationSidebar.vue +70 -0
- package/app/layers/solution/app/components/TheHeader.vue +11 -7
- package/app/layers/solution/app/composables/useConsultationRequest.ts +8 -0
- package/app/layers/solution/app/composables/usePlanNormalizer.ts +45 -0
- package/app/layers/solution/app/composables/useSolutionApi.ts +18 -0
- package/app/layers/solution/app/data/pricingPlans.ts +150 -0
- package/app/layers/solution/app/nuxt.config.ts +6 -0
- package/app/layers/solution/app/pages/[slug]/[slug2]/[slug3]/index.vue +22 -0
- package/app/layers/solution/app/pages/[slug]/[slug2]/index.vue +23 -0
- package/app/layers/solution/app/pages/[slug]/index.vue +22 -0
- package/app/layers/solution/app/pages/dang-ky-tu-van/khoa-hoc-proxmox.vue.disabled +39 -0
- package/app/layers/solution/app/pages/giai-phap-proxmox/dang-ky-khoa-hoc/[slug].vue.disabled +56 -0
- package/app/pages/index.vue +2 -1
- package/nuxt.config.ts +1 -0
- package/package.json +1 -1
- package/app/layers/solution/app/pages/giai-phap-proxmox/dang-ky-khoa-hoc/[slug].vue +0 -131
- package/website-longvan-1.1.6.tgz +0 -0
- package/website-longvan-1.1.7.tgz +0 -0
- package/website-longvan-1.1.8.tgz +0 -0
- /package/app/layers/solution/app/pages/cap-nhat-thong-tin/{index.vue → index.vue.disabled} +0 -0
- /package/app/layers/solution/app/pages/dang-ky/{index.vue → index.vue.disabled} +0 -0
- /package/app/layers/solution/app/pages/dang-nhap/{index.vue → index.vue.disabled} +0 -0
- /package/app/layers/solution/app/pages/doi-mat-khau/{index.vue → index.vue.disabled} +0 -0
- /package/app/layers/solution/app/pages/giai-phap-proxmox/[slug]/{index.vue → index.vue.disabled} +0 -0
- /package/app/layers/solution/app/pages/giai-phap-proxmox/[slug]/proxmox-hci/{index.vue → index.vue.disabled} +0 -0
- /package/app/layers/solution/app/pages/quen-mat-khau/{index.vue → index.vue.disabled} +0 -0
- /package/app/layers/solution/app/pages/xac-thuc/{index.vue → index.vue.disabled} +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
plans: Array<{
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
level: string
|
|
7
|
+
duration: string
|
|
8
|
+
icon: string
|
|
9
|
+
}>
|
|
10
|
+
selectedPlanId?: string
|
|
11
|
+
}>()
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div class="min-h-screen bg-[#f4f5f9] px-6 py-[7rem] font-sans text-slate-900">
|
|
16
|
+
<div class="mx-auto grid max-w-[1000px] grid-cols-1 items-start gap-6 md:grid-cols-[340px_1fr]">
|
|
17
|
+
<!-- Left column -->
|
|
18
|
+
<RegistrationSidebar />
|
|
19
|
+
|
|
20
|
+
<!-- Right column: form -->
|
|
21
|
+
<RegistrationForm :plans="plans" :selected-plan-id="selectedPlanId" />
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<aside class="flex flex-col gap-4">
|
|
3
|
+
<div class="rounded-2xl border border-[#e7e9f0] bg-white p-7">
|
|
4
|
+
<h2 class="mb-5 text-[17px] font-bold text-slate-900">Vì sao chọn chúng tôi</h2>
|
|
5
|
+
<ul class="flex flex-col gap-[22px]">
|
|
6
|
+
<li class="flex items-start gap-3">
|
|
7
|
+
<span
|
|
8
|
+
class="flex h-[34px] w-[34px] flex-none items-center justify-center rounded-[10px] bg-[#e8eefc] text-[#2f5fe0]">
|
|
9
|
+
<svg class="h-[17px] w-[17px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
|
|
10
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
11
|
+
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
|
12
|
+
<circle cx="9" cy="7" r="4" />
|
|
13
|
+
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
|
14
|
+
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
|
15
|
+
</svg>
|
|
16
|
+
</span>
|
|
17
|
+
<span class="flex flex-col gap-[3px]">
|
|
18
|
+
<span class="text-sm font-semibold text-slate-900">Lớp nhỏ, kèm sát</span>
|
|
19
|
+
<span class="text-[13px] leading-relaxed text-gray-500">Sĩ số giới hạn, giảng viên hỗ trợ trực tiếp
|
|
20
|
+
từng học viên.</span>
|
|
21
|
+
</span>
|
|
22
|
+
</li>
|
|
23
|
+
<li class="flex items-start gap-3">
|
|
24
|
+
<span
|
|
25
|
+
class="flex h-[34px] w-[34px] flex-none items-center justify-center rounded-[10px] bg-[#e8eefc] text-[#2f5fe0]">
|
|
26
|
+
<svg class="h-[17px] w-[17px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
|
|
27
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
28
|
+
<path d="M22 10 12 5 2 10l10 5 10-5Z" />
|
|
29
|
+
<path d="M6 12v5c0 1.66 2.69 3 6 3s6-1.34 6-3v-5" />
|
|
30
|
+
</svg>
|
|
31
|
+
</span>
|
|
32
|
+
<span class="flex flex-col gap-[3px]">
|
|
33
|
+
<span class="text-sm font-semibold text-slate-900">Thực hành hệ thống thật</span>
|
|
34
|
+
<span class="text-[13px] leading-relaxed text-gray-500">Thao tác trên cụm Proxmox HCI thực tế, không
|
|
35
|
+
chỉ lý thuyết.</span>
|
|
36
|
+
</span>
|
|
37
|
+
</li>
|
|
38
|
+
<li class="flex items-start gap-3">
|
|
39
|
+
<span
|
|
40
|
+
class="flex h-[34px] w-[34px] flex-none items-center justify-center rounded-[10px] bg-[#e8eefc] text-[#2f5fe0]">
|
|
41
|
+
<svg class="h-[17px] w-[17px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
|
|
42
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
43
|
+
<circle cx="12" cy="12" r="10" />
|
|
44
|
+
<path d="m9 12 2 2 4-4" />
|
|
45
|
+
</svg>
|
|
46
|
+
</span>
|
|
47
|
+
<span class="flex flex-col gap-[3px]">
|
|
48
|
+
<span class="text-sm font-semibold text-slate-900">Chứng nhận hoàn thành</span>
|
|
49
|
+
<span class="text-[13px] leading-relaxed text-gray-500">Cấp chứng nhận và hỗ trợ tư vấn nghề nghiệp
|
|
50
|
+
sau khóa học.</span>
|
|
51
|
+
</span>
|
|
52
|
+
</li>
|
|
53
|
+
</ul>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="rounded-2xl border border-[#dbe4fd] bg-[#eef2ff] px-6 py-[22px]">
|
|
57
|
+
<p class="mb-2 flex items-center gap-[9px] text-sm font-bold text-[#2f5fe0]">
|
|
58
|
+
<svg class="h-[17px] w-[17px] flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
59
|
+
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
60
|
+
<path d="M23 7 16 12l7 5V7Z" />
|
|
61
|
+
<rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
|
|
62
|
+
</svg>
|
|
63
|
+
Khai giảng linh hoạt
|
|
64
|
+
</p>
|
|
65
|
+
<p class="text-[13px] leading-relaxed text-gray-600">
|
|
66
|
+
Lịch học buổi tối và cuối tuần, phù hợp cho cả người đi làm. Hỗ trợ ghi hình buổi học để xem lại.
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
</aside>
|
|
70
|
+
</template>
|
|
@@ -1,7 +1,7 @@
|
|
|
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
|
|
4
|
+
<NuxtLink :to="`/giai-phap-proxmox/khoa-hoc-proxmox-hci`" class="flex 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
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
]">
|
|
17
17
|
{{ link.label }}
|
|
18
18
|
</NuxtLink>
|
|
19
|
-
<a v-else :href="`/giai-phap-proxmox
|
|
19
|
+
<a v-else :href="`/giai-phap-proxmox/khoa-hoc-proxmox-hci#${link.id}`" @click.prevent="handleAnchorClick(link)" :class="[
|
|
20
20
|
'pb-1 border-b-2 transition-all duration-200 cursor-pointer',
|
|
21
21
|
activeSection === link.id
|
|
22
22
|
? 'text-[#004AC6] font-bold border-[#004AC6]'
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
{{ link.label }}
|
|
26
26
|
</a>
|
|
27
27
|
</template>
|
|
28
|
-
<a v-else :href="`/giai-phap-proxmox
|
|
28
|
+
<a v-else :href="`/giai-phap-proxmox/khoa-hoc-proxmox-hci#${link.id}`" @click.prevent="handleAnchorClick(link)" :class="[
|
|
29
29
|
'pb-1 border-b-2 transition-all duration-200 cursor-pointer',
|
|
30
30
|
activeSection === link.id
|
|
31
31
|
? 'text-[#004AC6] font-bold border-[#004AC6]'
|
|
@@ -65,16 +65,16 @@
|
|
|
65
65
|
|
|
66
66
|
<!-- Not logged in -->
|
|
67
67
|
<div v-else class="flex items-center gap-3">
|
|
68
|
-
<a v-if="isConsultant" :href="`/giai-phap-proxmox
|
|
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
|
|
71
71
|
<Icon name="lucide:headset" class="text-[16px]" />
|
|
72
72
|
</a>
|
|
73
|
-
<
|
|
73
|
+
<a :href="loginHref"
|
|
74
74
|
class="inline-flex items-center gap-1.5 text-sm font-bold px-5 py-2.5 rounded-[10px] bg-[#004AC6] text-white hover:bg-[#1D4ED8] transition-all active:scale-95 shadow-sm shadow-blue-600/20">
|
|
75
75
|
Đăng nhập
|
|
76
76
|
<Icon name="lucide:log-in" class="text-[16px]" />
|
|
77
|
-
</
|
|
77
|
+
</a>
|
|
78
78
|
</div>
|
|
79
79
|
</div>
|
|
80
80
|
</header>
|
|
@@ -98,7 +98,7 @@ const route = useRoute()
|
|
|
98
98
|
const tokenKey = config.public.ACCESSTOKEN_NAME as string
|
|
99
99
|
|
|
100
100
|
const navLinks = computed(() => [
|
|
101
|
-
{ id: 'san-pham', label: 'Sản phẩm', path: `/
|
|
101
|
+
{ id: 'san-pham', label: 'Sản phẩm', path: `/gioi-thieu-san-pham/proxmox-hci` },
|
|
102
102
|
{ id: 'giai-phap', label: 'Vì sao chọn chúng tôi' },
|
|
103
103
|
{ id: 'chi-phi', label: 'Học phí' },
|
|
104
104
|
{ id: 'khach-hang', label: 'Học viên nói gì' },
|
|
@@ -106,6 +106,10 @@ const navLinks = computed(() => [
|
|
|
106
106
|
|
|
107
107
|
const { scrollTo } = useScrollToSection()
|
|
108
108
|
|
|
109
|
+
// route.fullPath gồm cả query hiện tại (vd ?plan=...&name=...) để sau khi đăng nhập quay lại đúng chỗ
|
|
110
|
+
// Trang đăng nhập nằm ở domain LONGVAN_WEB (vd https://dev.longvan.net), không phải app này
|
|
111
|
+
const loginHref = computed(() => `${config.public.LONGVAN_WEB}/dang-nhap?redirectUrl=${encodeURIComponent(route.fullPath)}`)
|
|
112
|
+
|
|
109
113
|
const activeSection = useActiveSection(navLinks.value.map(l => l.id))
|
|
110
114
|
const user = ref<{ fullName?: string; username?: string } | null>(null)
|
|
111
115
|
const showMenu = ref(false)
|
|
@@ -0,0 +1,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
|
+
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
|
+
}
|
|
@@ -28,6 +28,23 @@ export const useSolutionApi = () => {
|
|
|
28
28
|
return data
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// Tạm thời: dò slug path theo cấp bất kỳ (slug/slug2/slug3), lấy object đầu tiên rồi gọi resume theo profileId
|
|
32
|
+
const getSlugData = async (slug: string) => {
|
|
33
|
+
const list = await handleGetSlugPathView(slug, 'giai-phap')
|
|
34
|
+
console.log("🚀 ~ getSlugData ~ list:", list)
|
|
35
|
+
|
|
36
|
+
const first = list?.[0]
|
|
37
|
+
console.log("🚀 ~ getSlugData ~ first:", first)
|
|
38
|
+
|
|
39
|
+
const profileId = first?.profileId
|
|
40
|
+
if (!profileId) return { productData: first, resumeData: null }
|
|
41
|
+
|
|
42
|
+
const resumeData = await getResumeByIdData(profileId)
|
|
43
|
+
console.log("🚀 ~ getSlugData ~ resumeData:", resumeData)
|
|
44
|
+
|
|
45
|
+
return { productData: first, resumeData }
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
const getSolutionDetail = async (slug: string, type: string) => {
|
|
32
49
|
const productData = await handleGetSlugPathView(slug, type)
|
|
33
50
|
|
|
@@ -51,5 +68,6 @@ export const useSolutionApi = () => {
|
|
|
51
68
|
getPublicProductDynamic,
|
|
52
69
|
getResumeByIdData,
|
|
53
70
|
getSolutionDetail,
|
|
71
|
+
getSlugData,
|
|
54
72
|
}
|
|
55
73
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
export const hciPlans = [
|
|
2
|
+
{
|
|
3
|
+
id: 'hci-2',
|
|
4
|
+
name: 'HCI2 - 3 NODE, 120 CORE, 384GB RAM, 2.88TB NVME',
|
|
5
|
+
desc: 'Cụm 3 node cho doanh nghiệp vừa và nhỏ',
|
|
6
|
+
unit: '/tháng',
|
|
7
|
+
priceLabel: '',
|
|
8
|
+
prices: { 12: 8540000, 24: 8038000, 36: 7535000 },
|
|
9
|
+
tags: ['3 node x86', 'Ceph 3 bản sao', '10 GbE nội cụm', '~20 TB khả dụng'],
|
|
10
|
+
features: [
|
|
11
|
+
'Không chi phí đầu tư thiết bị ban đầu',
|
|
12
|
+
'Triển khai, tinh chỉnh và bật High Availability',
|
|
13
|
+
'Bảo hành, thay thế phần cứng trong suốt thời gian thuê',
|
|
14
|
+
'Đào tạo vận hành 1 buổi cho đội IT',
|
|
15
|
+
],
|
|
16
|
+
highlight: false,
|
|
17
|
+
url: 'https://dev.longvan.net/san-pham/hci2',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'hci-3',
|
|
21
|
+
name: 'HCI3 - 8 NODE, 160 CORE, 1024GB RAM, 7.68TB NVME',
|
|
22
|
+
desc: 'Cụm mở rộng cho hệ thống production',
|
|
23
|
+
unit: '/tháng',
|
|
24
|
+
priceLabel: '',
|
|
25
|
+
prices: { 12: 17847000, 24: 16844000, 36: 15791000 },
|
|
26
|
+
tags: ['5 node NVMe', 'Ceph RBD + CephFS', '25 GbE dự phòng', '~60 TB khả dụng'],
|
|
27
|
+
features: [
|
|
28
|
+
'Proxmox Backup Server riêng đi kèm',
|
|
29
|
+
'SDN, firewall phân tầng cho từng VM',
|
|
30
|
+
'Giám sát, cảnh báo toàn cụm 24/7',
|
|
31
|
+
'Nâng cấp cấu hình giữa kỳ không phát sinh phí ký lại',
|
|
32
|
+
],
|
|
33
|
+
highlight: true,
|
|
34
|
+
url: 'https://dev.longvan.net/san-pham/hci3',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'hci-4',
|
|
38
|
+
name: 'HCI4 - 8 NODE, 320 CORE, 1024GB RAM, 7.68TB NVME',
|
|
39
|
+
desc: 'Cụm mở rộng cho hệ thống production',
|
|
40
|
+
unit: '/tháng',
|
|
41
|
+
priceLabel: '',
|
|
42
|
+
prices: { 12: 22773000, 24: 21433000, 36: 20094000 },
|
|
43
|
+
tags: ['Từ 8 node', 'Ceph đa pool', 'Mạng lõi 100 GbE', 'Dung lượng theo yêu cầu'],
|
|
44
|
+
features: [
|
|
45
|
+
'Nhân bản chéo hai trung tâm dữ liệu',
|
|
46
|
+
'Kịch bản DR và diễn tập định kỳ',
|
|
47
|
+
'Tự động hóa qua API/Terraform',
|
|
48
|
+
'Kỹ sư trực onsite theo SLA cam kết',
|
|
49
|
+
],
|
|
50
|
+
highlight: false,
|
|
51
|
+
url: 'https://dev.longvan.net/san-pham/hci4',
|
|
52
|
+
},
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
export const workshopPlans = [
|
|
56
|
+
{
|
|
57
|
+
id: 'workshop-online',
|
|
58
|
+
name: 'Workshop Trực tuyến',
|
|
59
|
+
desc: 'Tham gia từ xa · lab riêng trên cloud',
|
|
60
|
+
unit: '/học viên',
|
|
61
|
+
priceLabel: '',
|
|
62
|
+
price: 2900000,
|
|
63
|
+
priceNote: '',
|
|
64
|
+
tags: ['1 ngày · 8 giờ', 'Trực tuyến', 'Lab cloud riêng'],
|
|
65
|
+
features: [
|
|
66
|
+
'Cài đặt Proxmox VE từ số 0',
|
|
67
|
+
'Dựng cụm HCI với Ceph, bật High Availability',
|
|
68
|
+
'Ghi hình buổi học để xem lại 30 ngày',
|
|
69
|
+
'Checklist triển khai dùng ngay',
|
|
70
|
+
],
|
|
71
|
+
highlight: false,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'workshop-onsite',
|
|
75
|
+
name: 'Workshop Tại lớp',
|
|
76
|
+
desc: 'Học trực tiếp cùng giảng viên',
|
|
77
|
+
unit: '/học viên',
|
|
78
|
+
priceLabel: '',
|
|
79
|
+
price: 3900000,
|
|
80
|
+
priceNote: '',
|
|
81
|
+
tags: ['1 ngày · 8 giờ', 'Tại lớp', 'Thiết bị thật tại lab'],
|
|
82
|
+
features: [
|
|
83
|
+
'Thao tác trực tiếp trên cụm nhiều node',
|
|
84
|
+
'Mô phỏng sự cố node và tự phục hồi',
|
|
85
|
+
'Hỏi đáp 1-1 với giảng viên',
|
|
86
|
+
'Suất ăn trưa và tài liệu in kèm',
|
|
87
|
+
],
|
|
88
|
+
highlight: true,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'workshop-enterprise',
|
|
92
|
+
name: 'Workshop Nội bộ doanh nghiệp',
|
|
93
|
+
desc: 'Tổ chức riêng cho đội IT của bạn',
|
|
94
|
+
unit: '/lớp tối đa 15 người',
|
|
95
|
+
priceLabel: 'Từ ',
|
|
96
|
+
price: 39000000,
|
|
97
|
+
priceNote: '',
|
|
98
|
+
tags: ['1-2 ngày', 'Tại doanh nghiệp', 'Nội dung tùy chỉnh'],
|
|
99
|
+
features: [
|
|
100
|
+
'Nội dung thiết kế theo hạ tầng hiện có',
|
|
101
|
+
'Giảng viên và thiết bị lab tới tận nơi',
|
|
102
|
+
'Đánh giá năng lực đội IT sau workshop',
|
|
103
|
+
'Ưu đãi khi kết hợp gói thuê HCI',
|
|
104
|
+
],
|
|
105
|
+
highlight: false,
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
export const trainingPlans = [
|
|
110
|
+
{
|
|
111
|
+
id: 'training-deploy',
|
|
112
|
+
name: 'Deployment & Management',
|
|
113
|
+
desc: 'Nền tảng · máy chủ đơn',
|
|
114
|
+
unit: '/học viên',
|
|
115
|
+
priceLabel: 'Từ ',
|
|
116
|
+
price: 9900000,
|
|
117
|
+
priceNote: '',
|
|
118
|
+
tags: ['14 giờ · 2 buổi', 'Trực tuyến hoặc tại lớp', 'Lab thực hành riêng'],
|
|
119
|
+
features: ['Cài đặt, mạng, lưu trữ, VM/LXC', 'Backup, phân quyền, tường lửa', 'Chứng nhận hoàn thành Proxmox'],
|
|
120
|
+
highlight: false,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 'training-cluster',
|
|
124
|
+
name: 'Clustering & Shared Storage',
|
|
125
|
+
desc: 'Chuyên sâu · cụm và Ceph',
|
|
126
|
+
unit: '/học viên',
|
|
127
|
+
priceLabel: 'Từ ',
|
|
128
|
+
price: 11900000,
|
|
129
|
+
priceNote: '',
|
|
130
|
+
tags: ['14 giờ · 2 buổi', 'Trực tuyến hoặc tại lớp', 'Lab cụm nhiều node'],
|
|
131
|
+
features: ['Cluster, pmxcfs, Corosync, SDN', 'Ceph HCI, ZFS replication, HA', 'Kịch bản sự cố thực chiến'],
|
|
132
|
+
highlight: true,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'training-bundle',
|
|
136
|
+
name: 'Proxmox VE Bundle',
|
|
137
|
+
desc: 'Trọn gói cả hai khóa · tiết kiệm hơn',
|
|
138
|
+
unit: '/học viên',
|
|
139
|
+
priceLabel: 'Từ ',
|
|
140
|
+
price: 19900000,
|
|
141
|
+
priceNote: '',
|
|
142
|
+
tags: ['28 giờ · 4-5 buổi', 'Trực tuyến hoặc tại lớp', 'Lộ trình liền mạch'],
|
|
143
|
+
features: [
|
|
144
|
+
'Gồm đủ Module 1 và Module 2',
|
|
145
|
+
'Ưu đãi khi đăng ký theo nhóm',
|
|
146
|
+
'Hỗ trợ hỏi đáp sau khóa học',
|
|
147
|
+
],
|
|
148
|
+
highlight: false,
|
|
149
|
+
},
|
|
150
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
definePageMeta({ layout: 'default' })
|
|
3
|
+
|
|
4
|
+
const route = useRoute()
|
|
5
|
+
const { getSlugData } = useSolutionApi()
|
|
6
|
+
|
|
7
|
+
const slug3 = computed(() => route.params.slug3 as string)
|
|
8
|
+
|
|
9
|
+
const { data } = await useAsyncData(
|
|
10
|
+
`slug3-${slug3.value}`,
|
|
11
|
+
() => getSlugData(slug3.value),
|
|
12
|
+
{ server: true }
|
|
13
|
+
)
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<DynamicPageSections
|
|
18
|
+
:field-resumes="data?.resumeData?.fieldResumes"
|
|
19
|
+
:target-c-t-a="data?.productData?.targetCTA"
|
|
20
|
+
:slug="data?.productData?.slug || slug3"
|
|
21
|
+
/>
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
definePageMeta({ layout: 'default' })
|
|
3
|
+
|
|
4
|
+
const route = useRoute()
|
|
5
|
+
const { getSlugData } = useSolutionApi()
|
|
6
|
+
|
|
7
|
+
const slug2 = computed(() => route.params.slug2 as string)
|
|
8
|
+
|
|
9
|
+
const { data } = await useAsyncData(
|
|
10
|
+
`slug2-${slug2.value}`,
|
|
11
|
+
() => getSlugData(slug2.value),
|
|
12
|
+
{ server: true }
|
|
13
|
+
)
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<DynamicPageSections
|
|
18
|
+
:field-resumes="data?.resumeData?.fieldResumes"
|
|
19
|
+
:target-c-t-a="data?.productData?.targetCTA"
|
|
20
|
+
:slug="data?.productData?.slug || slug2"
|
|
21
|
+
/>
|
|
22
|
+
<!-- {{ data }} -->
|
|
23
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
definePageMeta({ layout: 'default' })
|
|
3
|
+
|
|
4
|
+
const route = useRoute()
|
|
5
|
+
const { getSlugData } = useSolutionApi()
|
|
6
|
+
|
|
7
|
+
const slug = computed(() => route.params.slug as string)
|
|
8
|
+
|
|
9
|
+
const { data } = await useAsyncData(
|
|
10
|
+
`slug-${slug.value}`,
|
|
11
|
+
() => getSlugData(slug.value),
|
|
12
|
+
{ server: true }
|
|
13
|
+
)
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<DynamicPageSections
|
|
18
|
+
:field-resumes="data?.resumeData?.fieldResumes"
|
|
19
|
+
:target-c-t-a="data?.productData?.targetCTA"
|
|
20
|
+
:slug="data?.productData?.slug || slug"
|
|
21
|
+
/>
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
definePageMeta({ layout: 'default' })
|
|
3
|
+
|
|
4
|
+
const route = useRoute()
|
|
5
|
+
|
|
6
|
+
// Gói cần tư vấn được mang theo qua query từ CoursesSection (không fetch lại API)
|
|
7
|
+
const selectedPlan = computed(() => {
|
|
8
|
+
const id = route.query.plan
|
|
9
|
+
if (!id) return null
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
id: String(id),
|
|
13
|
+
name: String(route.query.name || ''),
|
|
14
|
+
level: String(route.query.level || ''),
|
|
15
|
+
duration: String(route.query.duration || ''),
|
|
16
|
+
price: String(route.query.price || ''),
|
|
17
|
+
description: String(route.query.description || ''),
|
|
18
|
+
icon: 'lucide:server',
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const plans = computed(() => (selectedPlan.value ? [selectedPlan.value] : []))
|
|
23
|
+
|
|
24
|
+
useHead({
|
|
25
|
+
title: computed(() =>
|
|
26
|
+
selectedPlan.value?.name ? `Đăng ký tư vấn - ${selectedPlan.value.name}` : 'Đăng ký tư vấn khóa học Proxmox'
|
|
27
|
+
),
|
|
28
|
+
})
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div class="bg-ink text-cloud font-body antialiased">
|
|
33
|
+
<TheHeader />
|
|
34
|
+
|
|
35
|
+
<RegistrationSection :plans="plans" :selected-plan-id="selectedPlan?.id" />
|
|
36
|
+
|
|
37
|
+
<TheFooter />
|
|
38
|
+
</div>
|
|
39
|
+
</template>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
definePageMeta({ layout: 'default' })
|
|
3
|
+
|
|
4
|
+
const route = useRoute()
|
|
5
|
+
const { getSolutionDetail } = useSolutionApi()
|
|
6
|
+
|
|
7
|
+
const slug = computed(() => route.params.slug as string)
|
|
8
|
+
|
|
9
|
+
// SSR data fetch
|
|
10
|
+
const { data, error } = await useAsyncData(
|
|
11
|
+
`dang-ky-${slug.value}`,
|
|
12
|
+
() => getSolutionDetail(slug.value, 'giai-phap'),
|
|
13
|
+
{ server: true }
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if (error.value || !data.value) {
|
|
17
|
+
throw createError({ statusCode: 404, statusMessage: 'Không tìm thấy khóa học', fatal: true })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Format plans from resumeData
|
|
21
|
+
const formatPlans = (resumeData: any) => {
|
|
22
|
+
const fieldResumes = resumeData?.fieldResumes || []
|
|
23
|
+
return fieldResumes
|
|
24
|
+
?.filter((group: any) => group?.type === 'FIELD_GROUP' && Array.isArray(group?.children))
|
|
25
|
+
?.map((group: any) => {
|
|
26
|
+
const fields = Object.fromEntries(
|
|
27
|
+
group.children?.map((field: any) => [field?.fieldName?.split('@')?.pop(), field?.value]) || []
|
|
28
|
+
)
|
|
29
|
+
return {
|
|
30
|
+
id: fields.productid || group.fieldName,
|
|
31
|
+
name: fields.title || '',
|
|
32
|
+
level: fields.level || '',
|
|
33
|
+
duration: fields.time || '',
|
|
34
|
+
icon: fields.icon || 'lucide:server',
|
|
35
|
+
}
|
|
36
|
+
}) || []
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const plans = computed(() => formatPlans(data.value?.resumeData))
|
|
40
|
+
const targetCTA = computed(() => data.value?.productData?.[0]?.targetCTA || '')
|
|
41
|
+
const selectedPlanId = computed(() => (route.query.plan as string) || '')
|
|
42
|
+
|
|
43
|
+
useHead({
|
|
44
|
+
title: computed(() => `Đăng ký khóa học - ${data.value?.productData?.[0]?.product?.title || 'Proxmox HCI'}`),
|
|
45
|
+
})
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<div class="bg-ink text-cloud font-body antialiased">
|
|
50
|
+
<TheHeader :target-c-t-a="targetCTA" :slug="slug" />
|
|
51
|
+
|
|
52
|
+
<RegistrationSection :plans="plans" :selected-plan-id="selectedPlanId" />
|
|
53
|
+
|
|
54
|
+
<TheFooter />
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
package/app/pages/index.vue
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
// import GiaiPhapPage from '../layers/solution/app/pages/giai-phap-proxmox/[slug]/index.vue'
|
|
3
|
-
import GiaiPhapPage from '../layers/solution/app/pages/giai-phap-proxmox/[slug]/proxmox-hci/index.vue'
|
|
3
|
+
// import GiaiPhapPage from '../layers/solution/app/pages/giai-phap-proxmox/[slug]/proxmox-hci/index.vue'
|
|
4
|
+
import GiaiPhapPage from '../layers/solution/app/pages/[slug]/[slug2]/index.vue'
|
|
4
5
|
</script>
|
|
5
6
|
|
|
6
7
|
<template>
|
package/nuxt.config.ts
CHANGED