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.
@@ -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
- </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>
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(/&amp;/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>
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "nuxt-layer-core",
3
3
  "type": "module",
4
4
  "private": false,
5
- "version": "1.1.2",
5
+ "version": "1.1.4",
6
6
  "scripts": {
7
7
  "build": "env-cmd -f .env nuxt build",
8
8
  "dev": "env-cmd -f .env nuxt dev"