nuxt-layer-core 1.0.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.
Files changed (85) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/app/app.config.ts +5 -0
  3. package/app/app.vue +11 -0
  4. package/app/assets/css/main.css +113 -0
  5. package/app/components/.gitkeep +0 -0
  6. package/app/components/ui/sonner/Sonner.vue +62 -0
  7. package/app/components/ui/sonner/index.ts +1 -0
  8. package/app/composables/.gitkeep +0 -0
  9. package/app/layers/solution/app/assets/proxmox-css/main.css +89 -0
  10. package/app/layers/solution/app/assets/proxmox-css/proxmox-utils.css +68 -0
  11. package/app/layers/solution/app/components/ClusterDiagram.vue +72 -0
  12. package/app/layers/solution/app/components/CoursesSection.vue +245 -0
  13. package/app/layers/solution/app/components/CtaSection.vue +347 -0
  14. package/app/layers/solution/app/components/DynamicPageSections.vue +103 -0
  15. package/app/layers/solution/app/components/FeaturesSection.vue +67 -0
  16. package/app/layers/solution/app/components/HeroSection.vue +144 -0
  17. package/app/layers/solution/app/components/PricingPlansSection.vue +396 -0
  18. package/app/layers/solution/app/components/RegistrationForm.vue +283 -0
  19. package/app/layers/solution/app/components/RegistrationSection.vue +24 -0
  20. package/app/layers/solution/app/components/RegistrationSidebar.vue +70 -0
  21. package/app/layers/solution/app/components/TestimonialsSection.vue +58 -0
  22. package/app/layers/solution/app/components/TheFooter.vue +61 -0
  23. package/app/layers/solution/app/components/TheHeader.vue +193 -0
  24. package/app/layers/solution/app/components/proxmox/AppIcon.vue +45 -0
  25. package/app/layers/solution/app/components/proxmox/ComparisonSection.vue +54 -0
  26. package/app/layers/solution/app/components/proxmox/FaqSection.vue +58 -0
  27. package/app/layers/solution/app/components/proxmox/HciOverviewSection.vue +37 -0
  28. package/app/layers/solution/app/components/proxmox/HeroSection.vue +74 -0
  29. package/app/layers/solution/app/components/proxmox/TestimonialsSection.vue +44 -0
  30. package/app/layers/solution/app/components/proxmox/TrainingBenefitsSection.vue +37 -0
  31. package/app/layers/solution/app/components/proxmox/VeFeaturesSection.vue +55 -0
  32. package/app/layers/solution/app/composables/useActiveSection.ts +47 -0
  33. package/app/layers/solution/app/composables/useAuthApi.ts +244 -0
  34. package/app/layers/solution/app/composables/useAuthState.ts +16 -0
  35. package/app/layers/solution/app/composables/useConsultationRequest.ts +8 -0
  36. package/app/layers/solution/app/composables/useCookie.ts +79 -0
  37. package/app/layers/solution/app/composables/useCookieHelper.ts +81 -0
  38. package/app/layers/solution/app/composables/useCrmApi.ts +32 -0
  39. package/app/layers/solution/app/composables/useDynamicCollection.ts +23 -0
  40. package/app/layers/solution/app/composables/usePlanNormalizer.ts +45 -0
  41. package/app/layers/solution/app/composables/useScrollAnimate.ts +40 -0
  42. package/app/layers/solution/app/composables/useScrollToSection.ts +13 -0
  43. package/app/layers/solution/app/composables/useSolutionApi.ts +73 -0
  44. package/app/layers/solution/app/composables/useToast.ts +14 -0
  45. package/app/layers/solution/app/data/pricingPlans.ts +150 -0
  46. package/app/layers/solution/app/data/proxmox-content.ts +158 -0
  47. package/app/layers/solution/app/images/Proxmox HCI.png +0 -0
  48. package/app/layers/solution/app/images/image_from_https_hercules_cdn.com_file_6dztkvkfnip0ud23tzzvu57c.png +0 -0
  49. package/app/layers/solution/app/layouts/default.vue +5 -0
  50. package/app/layers/solution/app/nuxt.config.ts +15 -0
  51. package/app/layers/solution/app/pages/[slug]/[slug2]/[slug3]/index.vue +22 -0
  52. package/app/layers/solution/app/pages/[slug]/[slug2]/index.vue +23 -0
  53. package/app/layers/solution/app/pages/[slug]/index.vue +22 -0
  54. package/app/layers/solution/app/pages/cap-nhat-thong-tin/index.vue.disabled +302 -0
  55. package/app/layers/solution/app/pages/dang-ky/index.vue.disabled +627 -0
  56. package/app/layers/solution/app/pages/dang-ky-tu-van/khoa-hoc-proxmox.vue.disabled +39 -0
  57. package/app/layers/solution/app/pages/dang-nhap/index.vue.disabled +208 -0
  58. package/app/layers/solution/app/pages/doi-mat-khau/index.vue.disabled +237 -0
  59. package/app/layers/solution/app/pages/giai-phap-proxmox/[slug]/index.vue.disabled +37 -0
  60. package/app/layers/solution/app/pages/giai-phap-proxmox/[slug]/proxmox-hci/index.vue.disabled +45 -0
  61. package/app/layers/solution/app/pages/giai-phap-proxmox/dang-ky-khoa-hoc/[slug].vue.disabled +56 -0
  62. package/app/layers/solution/app/pages/quen-mat-khau/index.vue.disabled +136 -0
  63. package/app/layers/solution/app/pages/xac-thuc/index.vue.disabled +320 -0
  64. package/app/layers/solution/app/plugins/reveal.ts +56 -0
  65. package/app/layers/solution/app/public/images/image_from_https_hercules_cdn.com_file_6dztkvkfnip0ud23tzzvu57c.png +0 -0
  66. package/app/layers/solution/app/public/images/logo-lvs-1300x330.png +0 -0
  67. package/app/layouts/auth.vue +71 -0
  68. package/app/lib/utils.ts +7 -0
  69. package/app/middleware/.gitkeep +0 -0
  70. package/app/modules/layer-route-priority.ts +60 -0
  71. package/app/pages/index.vue +10 -0
  72. package/app/pages/san-pham-proxmox.vue +8 -0
  73. package/app/plugins/.gitkeep +0 -0
  74. package/app/plugins/sdk.ts +29 -0
  75. package/app/stores/.gitkeep +0 -0
  76. package/app/types/sdk.d.ts +15 -0
  77. package/components.json +24 -0
  78. package/nuxt.config.ts +105 -0
  79. package/package.json +52 -0
  80. package/public/.gitkeep +0 -0
  81. package/server/api/.gitkeep +0 -0
  82. package/server/utils/.gitkeep +0 -0
  83. package/tailwind.config.js +96 -0
  84. package/tsconfig.base.json +18 -0
  85. package/tsconfig.json +18 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Extract root domain from hostname.
3
+ * e.g. "dev.longvan.net" → ".longvan.net"
4
+ * "longvan.net" → ".longvan.net"
5
+ * "localhost" → "localhost"
6
+ */
7
+ function getCookieDomain(): string {
8
+ if (typeof window === 'undefined') return ''
9
+ const hostname = window.location.hostname
10
+
11
+ // localhost / IP → không set domain (browser tự xử lý)
12
+ if (hostname === 'localhost' || /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
13
+ return hostname
14
+ }
15
+
16
+ const parts = hostname.split('.')
17
+ // Lấy 2 phần cuối (root domain): longvan.net, example.com, ...
18
+ if (parts.length >= 2) {
19
+ return '.' + parts.slice(-2).join('.')
20
+ }
21
+ return hostname
22
+ }
23
+
24
+ /**
25
+ * Cookie helper composable — set, get, delete cookies theo domain hiện tại.
26
+ *
27
+ * Renamed from `useCookie` to avoid collision with Nuxt's built-in auto-import.
28
+ *
29
+ * Usage:
30
+ * const { setCookie, getCookie, deleteCookie } = useCookieHelper()
31
+ * setCookie('token', 'abc123', 7) // expires in 7 days
32
+ * getCookie('token') // → 'abc123'
33
+ * deleteCookie('token')
34
+ */
35
+ export function useCookieHelper() {
36
+ /**
37
+ * Lưu cookie.
38
+ * @param key - Tên cookie
39
+ * @param value - Giá trị cookie
40
+ * @param days - Số ngày hết hạn (mặc định 365)
41
+ */
42
+ function setCookie(key: string, value: string, days = 365): void {
43
+ const date = new Date()
44
+ date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
45
+ const expires = 'expires=' + date.toUTCString()
46
+ const domain = getCookieDomain()
47
+ const domainPart = domain ? `; domain=${domain}` : ''
48
+ document.cookie = `${key}=${encodeURIComponent(value)}; ${expires}; path=/${domainPart}`
49
+ }
50
+
51
+ /**
52
+ * Lấy giá trị cookie theo key.
53
+ * @param key - Tên cookie
54
+ * @returns Giá trị cookie hoặc null nếu không tìm thấy
55
+ */
56
+ function getCookie(key: string): string | null {
57
+ const name = key + '='
58
+ const decodedCookie = decodeURIComponent(document.cookie)
59
+ const cookies = decodedCookie.split(';')
60
+
61
+ for (let cookie of cookies) {
62
+ cookie = cookie.trim()
63
+ if (cookie.startsWith(name)) {
64
+ return cookie.substring(name.length)
65
+ }
66
+ }
67
+ return null
68
+ }
69
+
70
+ /**
71
+ * Xóa cookie theo key.
72
+ * @param key - Tên cookie cần xóa
73
+ */
74
+ function deleteCookie(key: string): void {
75
+ const domain = getCookieDomain()
76
+ const domainPart = domain ? `; domain=${domain}` : ''
77
+ document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/${domainPart}`
78
+ }
79
+
80
+ return { setCookie, getCookie, deleteCookie }
81
+ }
@@ -0,0 +1,32 @@
1
+ export const useCrmApi = () => {
2
+ const { $sdk } = useNuxtApp()
3
+
4
+ const addOpportunity = async (payload: {
5
+ name: string
6
+ description: string
7
+ referName: string
8
+ referPhone: string
9
+ referEmail: string
10
+ priorityName?: string
11
+ targetUrl?: string
12
+ extSource?: string
13
+ ownerId?: string
14
+ }) => {
15
+ const addOpportunityRequest = {
16
+ name: payload.name,
17
+ description: payload.description,
18
+ referName: payload.referName,
19
+ referPhone: payload.referPhone,
20
+ referEmail: payload.referEmail,
21
+ priorityName: payload.priorityName || 'MEDIUM',
22
+ targetId: '',
23
+ targetUrl: payload.targetUrl || '',
24
+ extSource: payload.extSource || '',
25
+ ownerId: payload.ownerId || '',
26
+ }
27
+
28
+ return $sdk.crm.addOpportunity(addOpportunityRequest, 'ANONYMOUS')
29
+ }
30
+
31
+ return { addOpportunity }
32
+ }
@@ -0,0 +1,23 @@
1
+ export const useDynamicCollection = () => {
2
+ const { $sdk } = useNuxtApp()
3
+
4
+ const handleGetSlugPathView = (slug: string, type: string) => {
5
+ try {
6
+ const response = $sdk.dynamic_collection.getSlugPathView(slug, type);
7
+
8
+ if (response) {
9
+ return response
10
+ } else {
11
+ return null;
12
+ }
13
+
14
+ } catch (error) {
15
+ console.log("🚀 ~ handleGetSlugPathView ~ error:", error);
16
+ return null;
17
+ }
18
+ }
19
+
20
+ return {
21
+ handleGetSlugPathView
22
+ }
23
+ }
@@ -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
+ }
@@ -0,0 +1,40 @@
1
+ const presets: Record<string, string> = {
2
+ 'fade-up': 'opacity-0 translate-y-8',
3
+ 'fade-in': 'opacity-0',
4
+ 'slide-left': 'opacity-0 -translate-x-8',
5
+ 'slide-right': 'opacity-0 translate-x-8',
6
+ 'scale-up': 'opacity-0 scale-95',
7
+ }
8
+
9
+ export function useScrollAnimate(selector: string, animation = 'fade-up') {
10
+ onMounted(() => {
11
+ const els = document.querySelectorAll<HTMLElement>(selector)
12
+ if (!els.length) return
13
+
14
+ const base = 'transition-all duration-700 ease-out'
15
+ const initial = presets[animation] || presets['fade-up']
16
+
17
+ const observer = new IntersectionObserver(
18
+ (entries) => {
19
+ for (const entry of entries) {
20
+ if (entry.isIntersecting) {
21
+ const el = entry.target as HTMLElement
22
+ const delay = el.dataset.delay || '0'
23
+ el.style.transitionDelay = `${delay}ms`
24
+ el.classList.remove(...initial.split(' '))
25
+ el.classList.add('opacity-100', 'translate-y-0', 'translate-x-0', 'scale-100')
26
+ observer.unobserve(el)
27
+ }
28
+ }
29
+ },
30
+ { threshold: 0.1 }
31
+ )
32
+
33
+ els.forEach((el) => {
34
+ el.classList.add(...base.split(' '), ...initial.split(' '))
35
+ observer.observe(el)
36
+ })
37
+
38
+ onUnmounted(() => observer.disconnect())
39
+ })
40
+ }
@@ -0,0 +1,13 @@
1
+ export const useScrollToSection = () => {
2
+ const scrollTo = (id: string, behavior: ScrollBehavior = 'smooth') => {
3
+ if (!id) return
4
+ const el = document.getElementById(id)
5
+ if (el) {
6
+ el.scrollIntoView({ behavior })
7
+ }
8
+ }
9
+
10
+ return {
11
+ scrollTo,
12
+ }
13
+ }
@@ -0,0 +1,73 @@
1
+ export const useSolutionApi = () => {
2
+ const { $sdk } = useNuxtApp()
3
+ const { handleGetSlugPathView } = useDynamicCollection()
4
+
5
+ const getPublicProductDynamic = (categoryId: string) => {
6
+ return $sdk.marketplace.getPublicProductDynamic(categoryId, '', [
7
+ "supplierId",
8
+ "categoryId",
9
+ "storeChannel",
10
+ "productContent { resumeId resumeName resumeDesc }",
11
+ "product { partyId id title status }",
12
+ "slugPath { id slug basePath domain publicStatus type templateId contentId platform providerDomain profileId fullUrl url targetCTA }"
13
+ ])
14
+ }
15
+
16
+ const getResumeByIdData = (resumeId: string) => {
17
+ console.log("🚀 ~ getSolutionDetail ~ resumeId:", resumeId)
18
+ const data = $sdk.crm.getResumeById(resumeId, [
19
+ "resumeId",
20
+ "resumeName",
21
+ "status",
22
+ "statusCode",
23
+ "level",
24
+ "fieldResumes { fieldName value fieldType type children { fieldName value fieldType type children { fieldName value fieldType type children { fieldName value fieldType type } } } }"
25
+ ])
26
+ console.log("🚀 ~ getResumeByIdData ~ data:", data)
27
+
28
+ return data
29
+ }
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
+
48
+ const getSolutionDetail = async (slug: string, type: string) => {
49
+ const productData = await handleGetSlugPathView(slug, type)
50
+
51
+ if(productData.length > 0){
52
+ const getData = productData?.find((c:any)=> c.type == type) ?? productData?.[0];
53
+
54
+ // console.log("🚀 ~ getSolutionDetail ~ productData:", productData)
55
+ const resumeId = getData?.profileId
56
+ // console.log("🚀 ~ getSolutionDetail ~ resumeId:", resumeId)
57
+ if (!resumeId) return { productData, resumeData: null }
58
+ const resumeData = await getResumeByIdData(resumeId)
59
+ // console.log("🚀 ~ getSolutionDetail ~ resumeData:", resumeData)
60
+ return { productData, resumeData }
61
+ }else{
62
+ return { productData: null, resumeData: null }
63
+ }
64
+
65
+ }
66
+
67
+ return {
68
+ getPublicProductDynamic,
69
+ getResumeByIdData,
70
+ getSolutionDetail,
71
+ getSlugData,
72
+ }
73
+ }
@@ -0,0 +1,14 @@
1
+ import { toast } from 'vue-sonner'
2
+
3
+ export function useToast() {
4
+ return {
5
+ success: (message: string) => toast.success(message),
6
+ error: (message: string) => toast.error(message),
7
+ info: (message: string) => toast.info(message),
8
+ warning: (message: string) => toast.warning(message),
9
+ promise: <T>(
10
+ promise: Promise<T>,
11
+ msgs: { loading: string; success: string | ((data: T) => string); error: string | ((err: unknown) => string) }
12
+ ) => toast.promise(promise, msgs),
13
+ }
14
+ }
@@ -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,158 @@
1
+ // Toàn bộ nội dung trong file này là dữ liệu giả (mock data) dùng để dựng giao diện demo.
2
+
3
+ export const heroStats = [
4
+ { value: "1.000+", label: "Doanh nghiệp tin dùng" },
5
+ { value: "50%", label: "Tiết kiệm chi phí bản quyền" },
6
+ { value: "99.99%", label: "Uptime đảm bảo (HA)" },
7
+ { value: "24/7", label: "Hỗ trợ kỹ thuật chuyên gia" }
8
+ ]
9
+
10
+ export const veFeatureList = [
11
+ { icon: "cpu", text: "Quản lý VM (KVM) & Container (LXC)" },
12
+ { icon: "globe", text: "Giao diện web trực quan, dễ vận hành" },
13
+ { icon: "layers", text: "Snapshot, Backup & Restore tích hợp" },
14
+ { icon: "activity", text: "Monitoring tài nguyên theo thời gian thực" },
15
+ { icon: "share", text: "Software Defined Network (SDN)" },
16
+ { icon: "cluster", text: "Hỗ trợ cluster HA tự động failover" }
17
+ ]
18
+
19
+ export const veFeatureCards = [
20
+ { icon: "cpu", title: "KVM Virtualization", desc: "Hiệu năng gần native" },
21
+ { icon: "database", title: "Ceph Storage", desc: "Lưu trữ phân tán HA" },
22
+ { icon: "share", title: "SDN / Networking", desc: "VLAN, VXLAN tích hợp" },
23
+ { icon: "shield", title: "Proxmox Backup", desc: "Dedup & incremental" },
24
+ { icon: "bolt", title: "HA Cluster", desc: "Failover tự động" },
25
+ { icon: "chart", title: "Monitoring", desc: "Real-time dashboard" }
26
+ ]
27
+
28
+ export const hciOverviewCards = [
29
+ {
30
+ icon: "layers",
31
+ title: "Hợp nhất Compute – Storage – Network",
32
+ desc: "Tích hợp toàn bộ thành phần hạ tầng vào một platform duy nhất, giảm thiểu độ phức tạp vận hành."
33
+ },
34
+ {
35
+ icon: "database",
36
+ title: "Ceph Distributed Storage",
37
+ desc: "Lưu trữ phân tán hiệu năng cao với deduplication, không cần SAN/NAS riêng biệt."
38
+ },
39
+ {
40
+ icon: "share",
41
+ title: "Software Defined Network",
42
+ desc: "Quản lý VLAN, VXLAN, SDN ngay trong giao diện web Proxmox, không cần thiết bị mạng phức tạp."
43
+ },
44
+ {
45
+ icon: "shield",
46
+ title: "Backup & Disaster Recovery",
47
+ desc: "Proxmox Backup Server tích hợp incremental backup, khôi phục nhanh, tiết kiệm dung lượng."
48
+ },
49
+ {
50
+ icon: "bolt",
51
+ title: "High Availability Cluster",
52
+ desc: "Tự động failover khi node gặp sự cố, duy trì uptime 99.99% không gián đoạn dịch vụ."
53
+ },
54
+ {
55
+ icon: "trending",
56
+ title: "Mở rộng không giới hạn",
57
+ desc: "Thêm node mới vào cluster không gián đoạn hệ thống, scale linh hoạt theo nhu cầu doanh nghiệp."
58
+ }
59
+ ]
60
+
61
+ export const trainingBenefits = [
62
+ {
63
+ icon: "book",
64
+ title: "Làm chủ toàn diện Proxmox VE",
65
+ desc: "Nắm vững trọn vẹn các khái niệm cốt lõi, từ triển khai ảo hóa cơ bản đến quản trị các tính năng cao cấp."
66
+ },
67
+ {
68
+ icon: "users",
69
+ title: "Học từ chuyên gia thực chiến",
70
+ desc: "Tiếp thu kinh nghiệm từ đội ngũ giảng viên được chứng nhận, đã xử lý hàng ngàn bài toán hệ thống."
71
+ },
72
+ {
73
+ icon: "layers",
74
+ title: "Thực hành Lab môi trường thật",
75
+ desc: "Trực tiếp thao tác và giải quyết tình huống giả lập sát với thực tế doanh nghiệp, không chỉ lý thuyết."
76
+ },
77
+ {
78
+ icon: "bolt",
79
+ title: "Troubleshooting thần tốc",
80
+ desc: "Trang bị kỹ năng nhận diện 'bệnh' hệ thống nhạy bén, xử lý sự cố chính xác và dứt điểm."
81
+ },
82
+ {
83
+ icon: "trending",
84
+ title: "Thăng tiến & Khẳng định vị thế",
85
+ desc: "Nâng tầm giá trị bản thân, tạo dựng sự tín nhiệm với cấp trên nhờ năng lực chuyên môn vượt trội."
86
+ },
87
+ {
88
+ icon: "award",
89
+ title: "Chứng nhận chính quy",
90
+ desc: "Nhận chứng chỉ hoàn thành khóa học danh giá, minh chứng rõ nét cho năng lực và sự đầu tư nghiêm túc."
91
+ }
92
+ ]
93
+
94
+ export const comparisonRows = [
95
+ { label: "Hiệu suất", hci: "Vượt trội", legacy: "Đạt", legacyOk: true },
96
+ { label: "Chi phí ban đầu", hci: "Vượt trội", legacy: "Hạn chế" },
97
+ { label: "Triển khai ban đầu", hci: "Vượt trội", legacy: "Hạn chế" },
98
+ { label: "Mở rộng tài nguyên", hci: "Vượt trội", legacy: "Hạn chế" },
99
+ { label: "Quản lý & vận hành", hci: "Vượt trội", legacy: "Hạn chế" },
100
+ { label: "Quản lý ngân sách", hci: "Vượt trội", legacy: "Hạn chế" },
101
+ { label: "Uptime", hci: "Vượt trội", legacy: "Hạn chế" },
102
+ { label: "Sao lưu dự phòng", hci: "Vượt trội", legacy: "Hạn chế" },
103
+ { label: "Chuyển đổi mô hình", hci: "Vượt trội", legacy: "Hạn chế" }
104
+ ]
105
+
106
+ export const testimonials = [
107
+ {
108
+ quote:
109
+ "Ban đầu định đăng ký gói cao nhất, nhưng buổi tư vấn cho thấy đội em chỉ cần gói Nền tảng. Tiết kiệm được kha khá chi phí mà vẫn đạt mục tiêu.",
110
+ name: "Học viên",
111
+ role: "System Admin"
112
+ },
113
+ {
114
+ quote:
115
+ "Người tư vấn hỏi rất kỹ về hạ tầng hiện tại trước khi gợi ý khóa học, nên nội dung học sát với công việc thật, không lan man.",
116
+ name: "Học viên",
117
+ role: "DevOps Engineer"
118
+ },
119
+ {
120
+ quote:
121
+ "Nhờ được tư vấn đúng lộ trình, công ty chuyển từ VMware sang cluster HCI trên Proxmox và cắt gần 60% chi phí license hàng năm.",
122
+ name: "Học viên",
123
+ role: "IT Manager"
124
+ }
125
+ ]
126
+
127
+ export const faqItems = [
128
+ {
129
+ question: "Khóa học phù hợp với đối tượng nào?",
130
+ answer:
131
+ "Khóa học phù hợp với System Admin, DevOps Engineer, IT Manager và bất kỳ ai đang vận hành hạ tầng máy chủ, muốn chuyển đổi sang nền tảng ảo hóa mã nguồn mở tiết kiệm chi phí."
132
+ },
133
+ {
134
+ question: "Học xong có thể làm được gì ngay?",
135
+ answer:
136
+ "Bạn có thể tự triển khai cluster Proxmox HCI hoàn chỉnh với Ceph Storage, SDN và HA, đồng thời xử lý các sự cố vận hành thường gặp trong môi trường doanh nghiệp."
137
+ },
138
+ {
139
+ question: "Môi trường lab thực hành như thế nào?",
140
+ answer:
141
+ "Học viên được cấp quyền truy cập lab thật, mô phỏng sát hạ tầng doanh nghiệp thực tế, thao tác trực tiếp trên cluster nhiều node thay vì chỉ xem demo."
142
+ },
143
+ {
144
+ question: "Thời gian học và hình thức học?",
145
+ answer:
146
+ "Khóa học tổ chức linh hoạt cả online và offline, với lịch học buổi tối hoặc cuối tuần để phù hợp với người đang đi làm."
147
+ },
148
+ {
149
+ question: "Có hỗ trợ sau khi hoàn thành khóa học không?",
150
+ answer:
151
+ "Học viên được tham gia nhóm hỗ trợ kỹ thuật riêng, tư vấn thêm khi triển khai thực tế tại doanh nghiệp sau khi kết thúc khóa học."
152
+ },
153
+ {
154
+ question: "Tôi chưa biết nên chọn khóa nào?",
155
+ answer:
156
+ "Đặt lịch tư vấn miễn phí 15 phút, đội ngũ chuyên gia sẽ khảo sát hạ tầng hiện tại và đề xuất lộ trình học phù hợp nhất với bạn."
157
+ }
158
+ ]
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <div class="layer-theme pt-16">
3
+ <slot />
4
+ </div>
5
+ </template>
@@ -0,0 +1,15 @@
1
+ export default defineNuxtConfig({
2
+ imports: {
3
+ dirs: [
4
+ "composables/**",
5
+ "services",
6
+ "types",
7
+ ],
8
+ },
9
+ components: [
10
+ {
11
+ path: "./components",
12
+ global: true,
13
+ },
14
+ ],
15
+ })