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
|
@@ -11,12 +11,118 @@ const { data } = await useAsyncData(
|
|
|
11
11
|
() => getSlugData(slug.value, 'giai-phap'),
|
|
12
12
|
{ server: true }
|
|
13
13
|
)
|
|
14
|
+
|
|
15
|
+
const targetCTA = computed(() => data.value?.productData?.targetCTA)
|
|
16
|
+
const pageSlug = computed(() => data.value?.productData?.slug || slug.value)
|
|
17
|
+
|
|
18
|
+
// Lấy 1 field group (vd goi_san_pham_co_ban) từ fieldResumes, gom children về object phẳng { productid, name, ... }
|
|
19
|
+
const findFieldGroup = (fieldName: string) => {
|
|
20
|
+
const group = data.value?.resumeData?.fieldResumes?.find(
|
|
21
|
+
(field: any) => field?.fieldName === fieldName && field?.type === 'FIELD_GROUP'
|
|
22
|
+
)
|
|
23
|
+
if (!group?.children) return null
|
|
24
|
+
|
|
25
|
+
const result: Record<string, any> = {}
|
|
26
|
+
for (const field of group.children) {
|
|
27
|
+
const key = field?.fieldName?.split('@')?.pop()
|
|
28
|
+
if (key) result[key] = field?.value
|
|
29
|
+
}
|
|
30
|
+
return result
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Gói thuê hạ tầng (goi_san_pham_*): giá theo 3 kỳ hạn 12/24/36 tháng, dùng thẳng text CMS trả về
|
|
34
|
+
const toRentalPlanItem = (fieldName: string, highlight = false) => {
|
|
35
|
+
const group = findFieldGroup(fieldName)
|
|
36
|
+
if (!group) return undefined
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
productId: group.productid,
|
|
40
|
+
name: group.name,
|
|
41
|
+
tagline: group.tagline,
|
|
42
|
+
unit: group.don_vi ? `/${group.don_vi}` : '',
|
|
43
|
+
// short_description là rich text từ CMS, PricingPlansSection render bằng v-html
|
|
44
|
+
short_description: group.short_description,
|
|
45
|
+
prices: {
|
|
46
|
+
12: group.gia_thue_12_thang,
|
|
47
|
+
24: group.gia_thue_24_thang,
|
|
48
|
+
36: group.gia_thue_36_thang,
|
|
49
|
+
},
|
|
50
|
+
url: group.url,
|
|
51
|
+
title_button: group.title_button,
|
|
52
|
+
highlight,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Workshop / khóa đào tạo (goi_workshop_*, goi_dao_tao_proxmox_*): 1 mức giá duy nhất
|
|
57
|
+
const toFlatPriceItem = (fieldName: string, highlight = false) => {
|
|
58
|
+
const group = findFieldGroup(fieldName)
|
|
59
|
+
if (!group) return undefined
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
productId: group.productid,
|
|
63
|
+
name: group.name,
|
|
64
|
+
tagline: group.tagline,
|
|
65
|
+
unit: group.don_vi ? `/${group.don_vi}` : '',
|
|
66
|
+
short_description: group.short_description,
|
|
67
|
+
price: group.hoc_phi,
|
|
68
|
+
url: group.url,
|
|
69
|
+
title_button: group.title_button,
|
|
70
|
+
highlight,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const pricingHci01 = computed(() => toRentalPlanItem('goi_san_pham_co_ban'))
|
|
75
|
+
const pricingHci02 = computed(() => toRentalPlanItem('goi_san_pham_nang_cao', true))
|
|
76
|
+
const pricingHci03 = computed(() => toRentalPlanItem('goi_san_pham_cao_cap'))
|
|
77
|
+
|
|
78
|
+
const pricingWorkshop01 = computed(() => toFlatPriceItem('goi_workshop_co_ban'))
|
|
79
|
+
const pricingWorkshop02 = computed(() => toFlatPriceItem('goi_workshop_nang_cao', true))
|
|
80
|
+
const pricingWorkshop03 = computed(() => toFlatPriceItem('goi_workshop_cao_cap'))
|
|
81
|
+
|
|
82
|
+
const pricingTraining01 = computed(() => toFlatPriceItem('goi_dao_tao_proxmox_co_ban'))
|
|
83
|
+
const pricingTraining02 = computed(() => toFlatPriceItem('goi_dao_tao_proxmox_nang_cao', true))
|
|
84
|
+
const pricingTraining03 = computed(() => toFlatPriceItem('goi_dao_tao_proxmox_cao_cap'))
|
|
85
|
+
|
|
86
|
+
// Đồng bộ dropdown "Khóa học quan tâm" của CtaSection với đúng data đang hiện ở PricingPlansSection
|
|
87
|
+
const pricingPlans = computed(() => ({
|
|
88
|
+
hci: [pricingHci01.value, pricingHci02.value, pricingHci03.value].map(normalizePlanItem).filter(Boolean),
|
|
89
|
+
workshop: [pricingWorkshop01.value, pricingWorkshop02.value, pricingWorkshop03.value].map(normalizePlanItem).filter(Boolean),
|
|
90
|
+
training: [pricingTraining01.value, pricingTraining02.value, pricingTraining03.value].map(normalizePlanItem).filter(Boolean),
|
|
91
|
+
}))
|
|
14
92
|
</script>
|
|
15
93
|
|
|
16
94
|
<template>
|
|
17
|
-
<DynamicPageSections
|
|
95
|
+
<!-- <DynamicPageSections
|
|
18
96
|
:field-resumes="data?.resumeData?.fieldResumes"
|
|
19
97
|
:target-c-t-a="data?.productData?.targetCTA"
|
|
20
98
|
:slug="data?.productData?.slug || slug"
|
|
21
|
-
/>
|
|
99
|
+
/> -->
|
|
100
|
+
|
|
101
|
+
<div>
|
|
102
|
+
<main class="min-h-screen bg-white text-slate-900">
|
|
103
|
+
<TheHeader :target-c-t-a="targetCTA" :slug="pageSlug" />
|
|
104
|
+
<ProxmoxHeroSection />
|
|
105
|
+
<ProxmoxPainPointsSection />
|
|
106
|
+
<ProxmoxVeFeaturesSection />
|
|
107
|
+
<ProxmoxHciOverviewSection />
|
|
108
|
+
<PricingPlansSection
|
|
109
|
+
:item-id01-groups01="pricingHci01"
|
|
110
|
+
:item-id02-groups01="pricingHci02"
|
|
111
|
+
:item-id03-groups01="pricingHci03"
|
|
112
|
+
:item-id01-groups02="pricingWorkshop01"
|
|
113
|
+
:item-id02-groups02="pricingWorkshop02"
|
|
114
|
+
:item-id03-groups02="pricingWorkshop03"
|
|
115
|
+
:item-id01-groups03="pricingTraining01"
|
|
116
|
+
:item-id02-groups03="pricingTraining02"
|
|
117
|
+
:item-id03-groups03="pricingTraining03"
|
|
118
|
+
/>
|
|
119
|
+
<ProxmoxTrainingBenefitsSection />
|
|
120
|
+
<ProxmoxComparisonSection />
|
|
121
|
+
<!-- <CoursesSection :target-c-t-a="targetCTA" :slug="pageSlug" /> -->
|
|
122
|
+
<!-- <ProxmoxTestimonialsSection />
|
|
123
|
+
<ProxmoxFaqSection /> -->
|
|
124
|
+
<CtaSection :pricing-plans="pricingPlans" />
|
|
125
|
+
<TheFooter />
|
|
126
|
+
</main>
|
|
127
|
+
</div>
|
|
22
128
|
</template>
|
|
@@ -1,22 +1,136 @@
|
|
|
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, 'gioi-thieu-template'),
|
|
12
|
-
{ server: true }
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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, 'gioi-thieu-template'),
|
|
12
|
+
{ server: true }
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const targetCTA = computed(() => data.value?.productData?.targetCTA)
|
|
16
|
+
const pageSlug = computed(() => data.value?.productData?.slug || slug.value)
|
|
17
|
+
|
|
18
|
+
// Lấy 1 field group (vd version-khuyen-nghi) từ fieldResumes, gom children về object phẳng { name, type, ... }
|
|
19
|
+
const findFieldGroup = (fieldName: string) => {
|
|
20
|
+
const group = data.value?.resumeData?.fieldResumes?.find(
|
|
21
|
+
(field: any) => field?.fieldName === fieldName && field?.type === 'FIELD_GROUP'
|
|
22
|
+
)
|
|
23
|
+
if (!group?.children) return null
|
|
24
|
+
|
|
25
|
+
const result: Record<string, any> = {}
|
|
26
|
+
for (const field of group.children) {
|
|
27
|
+
const key = field?.fieldName?.split('@')?.pop()
|
|
28
|
+
if (key) result[key] = field?.value
|
|
29
|
+
}
|
|
30
|
+
return result
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 1 phiên bản Ubuntu (version-khuyen-nghi, version_on_dinh, version_lts)
|
|
34
|
+
const toVersionMeta = (fieldName: string, badgeTone: string, recommended = false) => {
|
|
35
|
+
const group = findFieldGroup(fieldName)
|
|
36
|
+
if (!group) return undefined
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
id: group.productid,
|
|
40
|
+
version: String(group.name || '').replace(/^Ubuntu\s+/i, ''),
|
|
41
|
+
codename: '',
|
|
42
|
+
badge: group.type,
|
|
43
|
+
badgeTone,
|
|
44
|
+
recommended,
|
|
45
|
+
releaseDate: group.createdate,
|
|
46
|
+
supportUntil: group.expired ? `Hỗ trợ đến ${group.expired}` : '',
|
|
47
|
+
desc: group.description,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const versionRecommended = computed(() => toVersionMeta('version-khuyen-nghi', 'primary', true))
|
|
52
|
+
const versionStable = computed(() => toVersionMeta('version_on_dinh', 'amber'))
|
|
53
|
+
const versionLts = computed(() => toVersionMeta('version_lts', 'slate'))
|
|
54
|
+
|
|
55
|
+
// short_description là rich text <ul><li>✓ ...</li></ul> -> tách thành mảng text thuần
|
|
56
|
+
// (template tự thêm icon check riêng nên phải bỏ ký tự ✓ có sẵn trong CMS để khỏi lặp)
|
|
57
|
+
const parseFeatureList = (html?: string | null) => {
|
|
58
|
+
if (!html) return []
|
|
59
|
+
|
|
60
|
+
return [...html.matchAll(/<li[^>]*>([\s\S]*?)<\/li>/gi)]
|
|
61
|
+
.map((match) =>
|
|
62
|
+
(match[1] || '')
|
|
63
|
+
.replace(/<[^>]+>/g, '')
|
|
64
|
+
.replace(/&/g, '&')
|
|
65
|
+
.replace(/^[✓\s]+/, '')
|
|
66
|
+
.trim()
|
|
67
|
+
)
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 1 gói dịch vụ (san_pham_01/02/03), gắn với 1 phiên bản Ubuntu qua relate_groups <-> productid của version
|
|
72
|
+
const toPackage = (fieldName: string) => {
|
|
73
|
+
const group = findFieldGroup(fieldName)
|
|
74
|
+
if (!group?.name) return null
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
id: group.productid || group.product_slug || group.name,
|
|
78
|
+
relateGroups: group.relate_groups,
|
|
79
|
+
name: group.name,
|
|
80
|
+
desc: group.tagline,
|
|
81
|
+
price: group.gia_tien,
|
|
82
|
+
unit: group.don_vi ? `/${group.don_vi}` : '',
|
|
83
|
+
features: parseFeatureList(group.short_description),
|
|
84
|
+
url: group.url,
|
|
85
|
+
titleButton: group.title_button,
|
|
86
|
+
highlight: false,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const packages = computed(() =>
|
|
91
|
+
[toPackage('san_pham_01'), toPackage('san_pham_02'), toPackage('san_pham_03')].filter(
|
|
92
|
+
(pkg): pkg is NonNullable<typeof pkg> => !!pkg
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
// Gom các gói thuộc về đúng phiên bản Ubuntu thành 1 nhóm sản phẩm cho phiên bản đó
|
|
97
|
+
const toProductGroup = (versionId?: string) => {
|
|
98
|
+
const items = packages.value.filter((pkg) => pkg.relateGroups === versionId)
|
|
99
|
+
if (!items.length) return undefined
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
id: 'goi-dich-vu',
|
|
103
|
+
name: 'Gói dịch vụ đề xuất',
|
|
104
|
+
icon: 'lucide:server-cog',
|
|
105
|
+
desc: '',
|
|
106
|
+
packages: items,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const groupRecommended = computed(() => toProductGroup(versionRecommended.value?.id))
|
|
111
|
+
const groupStable = computed(() => toProductGroup(versionStable.value?.id))
|
|
112
|
+
const groupLts = computed(() => toProductGroup(versionLts.value?.id))
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<template>
|
|
116
|
+
<!-- <DynamicPageSections
|
|
117
|
+
:field-resumes="data?.resumeData?.fieldResumes"
|
|
118
|
+
:target-c-t-a="data?.productData?.targetCTA"
|
|
119
|
+
:slug="data?.productData?.slug || slug"
|
|
120
|
+
/> -->
|
|
121
|
+
|
|
122
|
+
<div class="bg-ink text-cloud font-body antialiased">
|
|
123
|
+
<TheHeader :target-c-t-a="targetCTA" :slug="pageSlug" />
|
|
124
|
+
<LinuxIntroSection />
|
|
125
|
+
<LinuxVersionsSection
|
|
126
|
+
:groups-item01="versionRecommended"
|
|
127
|
+
:groups-item02="versionStable"
|
|
128
|
+
:groups-item03="versionLts"
|
|
129
|
+
:item-id01-groups01="groupRecommended"
|
|
130
|
+
:item-id01-groups02="groupStable"
|
|
131
|
+
:item-id01-groups03="groupLts"
|
|
132
|
+
/>
|
|
133
|
+
<WhyChooseUbuntuSection />
|
|
134
|
+
<TheFooter />
|
|
135
|
+
</div>
|
|
136
|
+
</template>
|
|
@@ -11,12 +11,67 @@ const { data } = await useAsyncData(
|
|
|
11
11
|
() => getSlugData(slug.value, 'giai-phap'),
|
|
12
12
|
{ server: true }
|
|
13
13
|
)
|
|
14
|
+
|
|
15
|
+
const targetCTA = computed(() => data.value?.productData?.targetCTA)
|
|
16
|
+
const pageSlug = computed(() => data.value?.productData?.slug || slug.value)
|
|
17
|
+
|
|
18
|
+
// Lấy giá trị 1 field top-level (vd name, level) từ fieldResumes
|
|
19
|
+
const findFieldValue = (fieldName: string) => {
|
|
20
|
+
const field = data.value?.resumeData?.fieldResumes?.find(
|
|
21
|
+
(item: any) => item?.fieldName === fieldName && item?.type === 'FIELD'
|
|
22
|
+
)
|
|
23
|
+
return field?.value ?? ''
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Lấy 1 field group (vd hinh_thuc_hoc_online) từ fieldResumes, gom children về object phẳng { name, type }
|
|
27
|
+
const findFieldGroup = (fieldName: string) => {
|
|
28
|
+
const group = data.value?.resumeData?.fieldResumes?.find(
|
|
29
|
+
(field: any) => field?.fieldName === fieldName && field?.type === 'FIELD_GROUP'
|
|
30
|
+
)
|
|
31
|
+
if (!group?.children) return null
|
|
32
|
+
|
|
33
|
+
const result: Record<string, any> = {}
|
|
34
|
+
for (const field of group.children) {
|
|
35
|
+
const key = field?.fieldName?.split('@')?.pop()
|
|
36
|
+
if (key) result[key] = field?.value
|
|
37
|
+
}
|
|
38
|
+
return result
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const plan = computed(() => ({
|
|
42
|
+
id: findFieldValue('productid') || pageSlug.value,
|
|
43
|
+
name: findFieldValue('name'),
|
|
44
|
+
level: findFieldValue('level'),
|
|
45
|
+
duration: findFieldValue('thoi-gian'),
|
|
46
|
+
description: findFieldValue('short_description'),
|
|
47
|
+
icon: 'lucide:server',
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
const toStudyMode = (fieldName: string) => {
|
|
51
|
+
const group = findFieldGroup(fieldName)
|
|
52
|
+
if (!group) return null
|
|
53
|
+
return { name: group.name, type: String(group.type || '').trim() }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const studyModes = computed(() =>
|
|
57
|
+
[
|
|
58
|
+
toStudyMode('hinh_thuc_hoc_online'),
|
|
59
|
+
toStudyMode('hinh_thuc_hoc_tai_lop'),
|
|
60
|
+
toStudyMode('dao_tao_doanh_nghiep'),
|
|
61
|
+
].filter((mode): mode is { name: string; type: string } => !!mode)
|
|
62
|
+
)
|
|
14
63
|
</script>
|
|
15
64
|
|
|
16
65
|
<template>
|
|
17
|
-
<DynamicPageSections
|
|
66
|
+
<!-- <DynamicPageSections
|
|
18
67
|
:field-resumes="data?.resumeData?.fieldResumes"
|
|
19
68
|
:target-c-t-a="data?.productData?.targetCTA"
|
|
20
69
|
:slug="data?.productData?.slug || slug"
|
|
21
|
-
/>
|
|
70
|
+
/> -->
|
|
71
|
+
|
|
72
|
+
<div class="bg-ink text-cloud font-body antialiased">
|
|
73
|
+
<TheHeader :target-c-t-a="targetCTA" :slug="pageSlug" />
|
|
74
|
+
<RegistrationSection :plans="[plan]" :study-modes="studyModes" />
|
|
75
|
+
<TheFooter />
|
|
76
|
+
</div>
|
|
22
77
|
</template>
|