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.
- package/.claude/settings.local.json +8 -0
- package/app/app.config.ts +5 -0
- package/app/app.vue +11 -0
- package/app/assets/css/main.css +113 -0
- package/app/components/.gitkeep +0 -0
- package/app/components/ui/sonner/Sonner.vue +62 -0
- package/app/components/ui/sonner/index.ts +1 -0
- package/app/composables/.gitkeep +0 -0
- package/app/layers/solution/app/assets/proxmox-css/main.css +89 -0
- package/app/layers/solution/app/assets/proxmox-css/proxmox-utils.css +68 -0
- package/app/layers/solution/app/components/ClusterDiagram.vue +72 -0
- package/app/layers/solution/app/components/CoursesSection.vue +245 -0
- package/app/layers/solution/app/components/CtaSection.vue +347 -0
- package/app/layers/solution/app/components/DynamicPageSections.vue +103 -0
- package/app/layers/solution/app/components/FeaturesSection.vue +67 -0
- package/app/layers/solution/app/components/HeroSection.vue +144 -0
- package/app/layers/solution/app/components/PricingPlansSection.vue +396 -0
- package/app/layers/solution/app/components/RegistrationForm.vue +283 -0
- 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/TestimonialsSection.vue +58 -0
- package/app/layers/solution/app/components/TheFooter.vue +61 -0
- package/app/layers/solution/app/components/TheHeader.vue +193 -0
- package/app/layers/solution/app/components/proxmox/AppIcon.vue +45 -0
- package/app/layers/solution/app/components/proxmox/ComparisonSection.vue +54 -0
- package/app/layers/solution/app/components/proxmox/FaqSection.vue +58 -0
- package/app/layers/solution/app/components/proxmox/HciOverviewSection.vue +37 -0
- package/app/layers/solution/app/components/proxmox/HeroSection.vue +74 -0
- package/app/layers/solution/app/components/proxmox/TestimonialsSection.vue +44 -0
- package/app/layers/solution/app/components/proxmox/TrainingBenefitsSection.vue +37 -0
- package/app/layers/solution/app/components/proxmox/VeFeaturesSection.vue +55 -0
- package/app/layers/solution/app/composables/useActiveSection.ts +47 -0
- package/app/layers/solution/app/composables/useAuthApi.ts +244 -0
- package/app/layers/solution/app/composables/useAuthState.ts +16 -0
- package/app/layers/solution/app/composables/useConsultationRequest.ts +8 -0
- package/app/layers/solution/app/composables/useCookie.ts +79 -0
- package/app/layers/solution/app/composables/useCookieHelper.ts +81 -0
- package/app/layers/solution/app/composables/useCrmApi.ts +32 -0
- package/app/layers/solution/app/composables/useDynamicCollection.ts +23 -0
- package/app/layers/solution/app/composables/usePlanNormalizer.ts +45 -0
- package/app/layers/solution/app/composables/useScrollAnimate.ts +40 -0
- package/app/layers/solution/app/composables/useScrollToSection.ts +13 -0
- package/app/layers/solution/app/composables/useSolutionApi.ts +73 -0
- package/app/layers/solution/app/composables/useToast.ts +14 -0
- package/app/layers/solution/app/data/pricingPlans.ts +150 -0
- package/app/layers/solution/app/data/proxmox-content.ts +158 -0
- package/app/layers/solution/app/images/Proxmox HCI.png +0 -0
- package/app/layers/solution/app/images/image_from_https_hercules_cdn.com_file_6dztkvkfnip0ud23tzzvu57c.png +0 -0
- package/app/layers/solution/app/layouts/default.vue +5 -0
- package/app/layers/solution/app/nuxt.config.ts +15 -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/cap-nhat-thong-tin/index.vue.disabled +302 -0
- package/app/layers/solution/app/pages/dang-ky/index.vue.disabled +627 -0
- package/app/layers/solution/app/pages/dang-ky-tu-van/khoa-hoc-proxmox.vue.disabled +39 -0
- package/app/layers/solution/app/pages/dang-nhap/index.vue.disabled +208 -0
- package/app/layers/solution/app/pages/doi-mat-khau/index.vue.disabled +237 -0
- package/app/layers/solution/app/pages/giai-phap-proxmox/[slug]/index.vue.disabled +37 -0
- package/app/layers/solution/app/pages/giai-phap-proxmox/[slug]/proxmox-hci/index.vue.disabled +45 -0
- package/app/layers/solution/app/pages/giai-phap-proxmox/dang-ky-khoa-hoc/[slug].vue.disabled +56 -0
- package/app/layers/solution/app/pages/quen-mat-khau/index.vue.disabled +136 -0
- package/app/layers/solution/app/pages/xac-thuc/index.vue.disabled +320 -0
- package/app/layers/solution/app/plugins/reveal.ts +56 -0
- package/app/layers/solution/app/public/images/image_from_https_hercules_cdn.com_file_6dztkvkfnip0ud23tzzvu57c.png +0 -0
- package/app/layers/solution/app/public/images/logo-lvs-1300x330.png +0 -0
- package/app/layouts/auth.vue +71 -0
- package/app/lib/utils.ts +7 -0
- package/app/middleware/.gitkeep +0 -0
- package/app/modules/layer-route-priority.ts +60 -0
- package/app/pages/index.vue +10 -0
- package/app/pages/san-pham-proxmox.vue +8 -0
- package/app/plugins/.gitkeep +0 -0
- package/app/plugins/sdk.ts +29 -0
- package/app/stores/.gitkeep +0 -0
- package/app/types/sdk.d.ts +15 -0
- package/components.json +24 -0
- package/nuxt.config.ts +105 -0
- package/package.json +52 -0
- package/public/.gitkeep +0 -0
- package/server/api/.gitkeep +0 -0
- package/server/utils/.gitkeep +0 -0
- package/tailwind.config.js +96 -0
- package/tsconfig.base.json +18 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section id="goi-san-pham" ref="sectionRoot" class="py-24 bg-white overflow-hidden" :class="{ 'pricing-revealed': revealed }">
|
|
3
|
+
<div class="max-w-6xl mx-auto px-6 lg:px-8">
|
|
4
|
+
<div class="text-center mb-12 pricing-header">
|
|
5
|
+
<h2 class="text-[#004AC6] font-bold uppercase tracking-widest text-sm mb-2">Gói sản phẩm</h2>
|
|
6
|
+
<h3 class="font-['Montserrat'] font-extrabold text-2xl sm:text-4xl text-slate-900 mb-4">
|
|
7
|
+
Chọn gói phù hợp<br class="hidden sm:block" />
|
|
8
|
+
với nhu cầu của bạn
|
|
9
|
+
</h3>
|
|
10
|
+
<p class="text-slate-500 max-w-2xl mx-auto leading-relaxed">
|
|
11
|
+
Thuê hạ tầng Proxmox HCI theo tháng, tham gia workshop 1 ngày hoặc chọn khóa đào tạo chuyên sâu. Chi phí
|
|
12
|
+
minh bạch, điều chỉnh theo yêu cầu riêng.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<div class="flex flex-wrap items-center justify-center gap-3 mt-8">
|
|
16
|
+
<button
|
|
17
|
+
v-for="tab in tabs"
|
|
18
|
+
:key="tab.id"
|
|
19
|
+
type="button"
|
|
20
|
+
@click="activeTab = tab.id"
|
|
21
|
+
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-full text-sm font-bold transition-all duration-300 cursor-pointer"
|
|
22
|
+
:class="activeTab === tab.id
|
|
23
|
+
? 'bg-[#004AC6] text-white shadow-lg shadow-blue-500/25 scale-[1.03]'
|
|
24
|
+
: 'bg-white text-slate-500 border border-slate-200 hover:border-blue-200 hover:text-[#004AC6]'
|
|
25
|
+
"
|
|
26
|
+
>
|
|
27
|
+
<Icon :name="tab.icon" class="w-4 h-4" />
|
|
28
|
+
{{ tab.label }}
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<Transition name="fade-swap" mode="out-in">
|
|
33
|
+
<p :key="activeTab" class="text-slate-400 text-sm max-w-xl mx-auto mt-5">
|
|
34
|
+
{{ activeTabData.intro }}
|
|
35
|
+
</p>
|
|
36
|
+
</Transition>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<Transition name="fade-swap" mode="out-in">
|
|
40
|
+
<div :key="activeTab">
|
|
41
|
+
<!-- Info cards: only for Proxmox HCI -->
|
|
42
|
+
<div v-if="activeTab === 'hci'" class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16">
|
|
43
|
+
<div
|
|
44
|
+
v-for="(info, i) in hciInfoCards"
|
|
45
|
+
:key="info.title"
|
|
46
|
+
class="pricing-reveal bg-white rounded-2xl border border-slate-200 p-6 hover:border-blue-200 hover:shadow-lg transition-all duration-300"
|
|
47
|
+
:style="{ '--i': i }"
|
|
48
|
+
>
|
|
49
|
+
<div class="w-11 h-11 rounded-[10px] bg-blue-50 flex items-center justify-center mb-5 text-[#004AC6]">
|
|
50
|
+
<Icon :name="info.icon" class="w-5 h-5" />
|
|
51
|
+
</div>
|
|
52
|
+
<h4 class="font-bold text-slate-900 mb-2">{{ info.title }}</h4>
|
|
53
|
+
<p class="text-sm text-slate-500 leading-relaxed">{{ info.desc }}</p>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- Billing period switch: only for Proxmox HCI -->
|
|
58
|
+
<div v-if="activeTab === 'hci'" class="flex flex-col items-center gap-4 mb-10">
|
|
59
|
+
<p class="text-slate-500 text-sm">Chọn kỳ hạn thuê, càng dài chi phí hàng tháng càng thấp</p>
|
|
60
|
+
<div class="inline-flex items-center gap-1 bg-white border border-slate-200 rounded-full p-1">
|
|
61
|
+
<button
|
|
62
|
+
v-for="period in billingPeriods"
|
|
63
|
+
:key="period.value"
|
|
64
|
+
type="button"
|
|
65
|
+
@click="activePeriod = period.value"
|
|
66
|
+
class="px-4 py-2 rounded-full text-sm font-semibold transition-all duration-300 cursor-pointer"
|
|
67
|
+
:class="activePeriod === period.value
|
|
68
|
+
? 'bg-[#004AC6] text-white shadow-md shadow-blue-500/25'
|
|
69
|
+
: 'text-slate-500 hover:text-[#004AC6]'
|
|
70
|
+
"
|
|
71
|
+
>
|
|
72
|
+
{{ period.label }}
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- Pricing cards -->
|
|
78
|
+
<Transition name="fade-swap" mode="out-in">
|
|
79
|
+
<div :key="activeTab + '-' + activePeriod" class="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8">
|
|
80
|
+
<div
|
|
81
|
+
v-for="(plan, i) in activePlans"
|
|
82
|
+
:key="plan.name"
|
|
83
|
+
class="pricing-reveal group relative"
|
|
84
|
+
:style="{ '--i': i }"
|
|
85
|
+
:class="plan.highlight ? 'md:-mt-6' : 'md:mt-0'"
|
|
86
|
+
>
|
|
87
|
+
<div
|
|
88
|
+
v-if="plan.highlight"
|
|
89
|
+
class="absolute -inset-3 bg-gradient-to-br from-[#004AC6]/25 via-blue-400/15 to-transparent rounded-[28px] blur-2xl opacity-70 -z-10"
|
|
90
|
+
></div>
|
|
91
|
+
|
|
92
|
+
<div
|
|
93
|
+
class="relative bg-white rounded-2xl flex flex-col overflow-hidden h-full transition-all duration-300 ease-out"
|
|
94
|
+
:class="plan.highlight
|
|
95
|
+
? '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'
|
|
96
|
+
: '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'
|
|
97
|
+
"
|
|
98
|
+
>
|
|
99
|
+
<div
|
|
100
|
+
v-if="plan.highlight"
|
|
101
|
+
class="bg-gradient-to-r from-[#004AC6] to-[#1D63E0] text-white text-[11px] font-bold py-2.5 text-center uppercase tracking-widest flex items-center justify-center gap-1.5"
|
|
102
|
+
>
|
|
103
|
+
<Icon name="lucide:star" class="w-3.5 h-3.5" />
|
|
104
|
+
Được chọn nhiều nhất
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="p-7 flex flex-col gap-4">
|
|
108
|
+
<div>
|
|
109
|
+
<h4 class="font-bold text-xl text-slate-900 mb-2">{{ plan.name }}</h4>
|
|
110
|
+
<p class="text-slate-500 text-sm leading-relaxed">{{ plan.desc }}</p>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div class="pb-1">
|
|
114
|
+
<span v-if="plan.contact" class="font-['Montserrat'] font-extrabold text-2xl text-[#004AC6]">
|
|
115
|
+
Liên hệ
|
|
116
|
+
</span>
|
|
117
|
+
<template v-else>
|
|
118
|
+
<span class="font-['Montserrat'] font-extrabold text-2xl sm:text-[28px] text-slate-900">
|
|
119
|
+
{{ plan.priceLabel }}{{ formatPrice(plan.price) }}đ
|
|
120
|
+
</span>
|
|
121
|
+
<span class="text-slate-400 text-sm">{{ plan.unit }}</span>
|
|
122
|
+
</template>
|
|
123
|
+
<p class="text-slate-400 text-xs mt-1.5">{{ plan.priceNote }}</p>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="flex flex-wrap gap-1.5">
|
|
127
|
+
<span
|
|
128
|
+
v-for="tag in plan.tags"
|
|
129
|
+
:key="tag"
|
|
130
|
+
class="text-[11px] font-medium text-slate-500 bg-slate-50 border border-slate-100 rounded-md px-2 py-1"
|
|
131
|
+
>
|
|
132
|
+
{{ tag }}
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<ul class="flex flex-col gap-2.5 pt-4 border-t border-slate-100">
|
|
137
|
+
<li
|
|
138
|
+
v-for="feature in plan.features"
|
|
139
|
+
:key="feature"
|
|
140
|
+
class="flex items-start gap-2 text-sm text-slate-600"
|
|
141
|
+
>
|
|
142
|
+
<Icon name="lucide:check" class="w-4 h-4 text-[#004AC6] mt-0.5 shrink-0" />
|
|
143
|
+
{{ feature }}
|
|
144
|
+
</li>
|
|
145
|
+
</ul>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div class="p-7 pt-2 mt-auto">
|
|
149
|
+
<a
|
|
150
|
+
v-if="plan.url"
|
|
151
|
+
:href="plan.url"
|
|
152
|
+
target="_blank"
|
|
153
|
+
rel="noopener noreferrer"
|
|
154
|
+
class="w-full py-3.5 rounded-xl font-bold text-sm transition-all duration-300 cursor-pointer inline-flex items-center justify-center gap-2"
|
|
155
|
+
:class="plan.highlight
|
|
156
|
+
? 'bg-[#004AC6] text-white shadow-lg shadow-blue-500/25 hover:bg-[#1D4ED8] hover:shadow-blue-500/40'
|
|
157
|
+
: 'border border-slate-200 text-slate-600 hover:border-[#004AC6] hover:text-[#004AC6] hover:bg-blue-50/50'
|
|
158
|
+
"
|
|
159
|
+
>
|
|
160
|
+
Đăng ký ngay
|
|
161
|
+
<Icon name="lucide:arrow-up-right" class="w-4 h-4" />
|
|
162
|
+
</a>
|
|
163
|
+
<button
|
|
164
|
+
v-else
|
|
165
|
+
type="button"
|
|
166
|
+
@click="handleContactClick(plan)"
|
|
167
|
+
class="w-full py-3.5 rounded-xl font-bold text-sm transition-all duration-300 cursor-pointer"
|
|
168
|
+
:class="plan.highlight
|
|
169
|
+
? 'bg-[#004AC6] text-white shadow-lg shadow-blue-500/25 hover:bg-[#1D4ED8] hover:shadow-blue-500/40'
|
|
170
|
+
: 'border border-slate-200 text-slate-600 hover:border-[#004AC6] hover:text-[#004AC6] hover:bg-blue-50/50'
|
|
171
|
+
"
|
|
172
|
+
>
|
|
173
|
+
{{ activeTabData.cta }}
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</Transition>
|
|
180
|
+
</div>
|
|
181
|
+
</Transition>
|
|
182
|
+
</div>
|
|
183
|
+
</section>
|
|
184
|
+
</template>
|
|
185
|
+
|
|
186
|
+
<script setup>
|
|
187
|
+
import { hciPlans, workshopPlans, trainingPlans } from '../data/pricingPlans'
|
|
188
|
+
|
|
189
|
+
const props = defineProps({
|
|
190
|
+
groupsItem01: { type: String, default: '' },
|
|
191
|
+
groupsItem02: { type: String, default: '' },
|
|
192
|
+
groupsItem03: { type: String, default: '' },
|
|
193
|
+
itemId01Groups01: { type: Object, default: null },
|
|
194
|
+
itemId02Groups01: { type: Object, default: null },
|
|
195
|
+
itemId03Groups01: { type: Object, default: null },
|
|
196
|
+
itemId01Groups02: { type: Object, default: null },
|
|
197
|
+
itemId02Groups02: { type: Object, default: null },
|
|
198
|
+
itemId03Groups02: { type: Object, default: null },
|
|
199
|
+
itemId01Groups03: { type: Object, default: null },
|
|
200
|
+
itemId02Groups03: { type: Object, default: null },
|
|
201
|
+
itemId03Groups03: { type: Object, default: null },
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
useScrollAnimate('.pricing-header', 'fade-up')
|
|
205
|
+
|
|
206
|
+
const router = useRouter()
|
|
207
|
+
|
|
208
|
+
// Không có url riêng -> mở trang đăng ký tư vấn, mang theo thông tin gói qua query (giống CoursesSection.handleSelectPlan)
|
|
209
|
+
const categoryLevelLabels = {
|
|
210
|
+
hci: 'Gói thuê hạ tầng',
|
|
211
|
+
workshop: 'Workshop',
|
|
212
|
+
training: 'Khóa đào tạo',
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function handleContactClick(plan) {
|
|
216
|
+
const priceText = plan.priceLabel || typeof plan.price === 'number'
|
|
217
|
+
? `${plan.priceLabel || ''}${formatPrice(plan.price)}đ${plan.unit ? ' ' + plan.unit : ''}`
|
|
218
|
+
: ''
|
|
219
|
+
|
|
220
|
+
router.push({
|
|
221
|
+
path: `/dang-ky-tu-van/khoa-hoc-proxmox`,
|
|
222
|
+
query: {
|
|
223
|
+
plan: plan.id || plan.name,
|
|
224
|
+
name: plan.name,
|
|
225
|
+
level: categoryLevelLabels[activeTab.value] || '',
|
|
226
|
+
// plan.unit là hậu tố giá ("/tháng", "/học viên"), không phải thời lượng -> ưu tiên tags[0] ("1 ngày · 8 giờ")
|
|
227
|
+
duration: plan.tags?.[0] || '',
|
|
228
|
+
price: priceText,
|
|
229
|
+
description: plan.desc || '',
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const sectionRoot = ref(null)
|
|
235
|
+
const revealed = ref(false)
|
|
236
|
+
|
|
237
|
+
onMounted(() => {
|
|
238
|
+
const observer = new IntersectionObserver(
|
|
239
|
+
(entries) => {
|
|
240
|
+
for (const entry of entries) {
|
|
241
|
+
if (entry.isIntersecting) {
|
|
242
|
+
revealed.value = true
|
|
243
|
+
observer.disconnect()
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{ threshold: 0.15 }
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if (sectionRoot.value) observer.observe(sectionRoot.value)
|
|
251
|
+
onUnmounted(() => observer.disconnect())
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const activeTab = ref('hci')
|
|
255
|
+
const activePeriod = ref(36)
|
|
256
|
+
|
|
257
|
+
const groupLabel = (value, fallback) => (typeof value === 'string' && value.trim()) || fallback
|
|
258
|
+
|
|
259
|
+
const tabs = computed(() => [
|
|
260
|
+
{ id: 'hci', label: groupLabel(props.groupsItem01, 'Proxmox HCI'), icon: 'lucide:server-cog' },
|
|
261
|
+
{ id: 'workshop', label: groupLabel(props.groupsItem02, 'Workshop 1 ngày'), icon: 'lucide:zap' },
|
|
262
|
+
{ id: 'training', label: groupLabel(props.groupsItem03, 'Đào tạo Proxmox'), icon: 'lucide:layers' },
|
|
263
|
+
])
|
|
264
|
+
|
|
265
|
+
const billingPeriods = [
|
|
266
|
+
{ value: 12, label: 'Thuê 12 tháng' },
|
|
267
|
+
{ value: 24, label: 'Thuê 24 tháng' },
|
|
268
|
+
{ value: 36, label: 'Thuê 36 tháng' },
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
const hciInfoCards = [
|
|
272
|
+
{
|
|
273
|
+
icon: 'lucide:server',
|
|
274
|
+
title: 'Thuê trọn gói tại DC của chúng tôi',
|
|
275
|
+
desc: 'Cụm đặt tại trung tâm dữ liệu Tier III, gồm phần cứng, điện, mạng và vận hành. Bạn chỉ dùng và trả phí hàng tháng.',
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
icon: 'lucide:building-2',
|
|
279
|
+
title: 'Thuê phần cứng đặt tại doanh nghiệp',
|
|
280
|
+
desc: 'Chúng tôi mang thiết bị tới đặt tại phòng máy của bạn, triển khai và bảo hành. Dữ liệu nằm trong nội bộ, không cần đầu tư ban đầu.',
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
icon: 'lucide:refresh-cw',
|
|
284
|
+
title: 'Thuê linh hoạt theo node',
|
|
285
|
+
desc: 'Thêm hoặc bớt node theo từng tháng khi nhu cầu thay đổi. Có tùy chọn chuyển quyền sở hữu thiết bị khi hết hạn thuê.',
|
|
286
|
+
},
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
const tabContent = {
|
|
290
|
+
hci: {
|
|
291
|
+
intro: 'Thuê cụm siêu hội tụ theo tháng: không đầu tư ban đầu, đã bao gồm phần cứng, triển khai Ceph, HA và đội ngũ vận hành.',
|
|
292
|
+
cta: 'Nhận báo giá',
|
|
293
|
+
},
|
|
294
|
+
workshop: {
|
|
295
|
+
intro: 'Workshop cài đặt & triển khai Proxmox HCI trong 1 ngày, thực hành 100% trên lab thật.',
|
|
296
|
+
cta: 'Đăng ký workshop',
|
|
297
|
+
},
|
|
298
|
+
training: {
|
|
299
|
+
intro: 'Khóa học do giảng viên được Proxmox chứng nhận đứng lớp, kèm chứng nhận hoàn thành.',
|
|
300
|
+
cta: 'Đăng ký khóa học',
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const activeTabData = computed(() => tabContent[activeTab.value])
|
|
305
|
+
|
|
306
|
+
// CMS đổ item theo shape chung (title/time/short_description/config_product/productId, giống CoursesSection)
|
|
307
|
+
// -> chuẩn hoá qua normalizePlanItem rồi map lại đúng field template pricing card đang cần (desc/unit/tags/features)
|
|
308
|
+
const toPricingPlan = (item) => {
|
|
309
|
+
const normalized = normalizePlanItem(item)
|
|
310
|
+
if (!normalized) return null
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
id: normalized.id,
|
|
314
|
+
name: normalized.name,
|
|
315
|
+
desc: normalized.description,
|
|
316
|
+
unit: normalized.duration,
|
|
317
|
+
priceLabel: normalized.priceLabel,
|
|
318
|
+
price: normalized.price,
|
|
319
|
+
prices: normalized.prices,
|
|
320
|
+
priceNote: normalized.priceNote,
|
|
321
|
+
tags: normalized.features,
|
|
322
|
+
features: normalized.features,
|
|
323
|
+
highlight: normalized.highlight,
|
|
324
|
+
url: normalized.url,
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const hciItems = computed(() =>
|
|
329
|
+
[props.itemId01Groups01, props.itemId02Groups01, props.itemId03Groups01].map(toPricingPlan).filter(Boolean)
|
|
330
|
+
)
|
|
331
|
+
const workshopItems = computed(() =>
|
|
332
|
+
[props.itemId01Groups02, props.itemId02Groups02, props.itemId03Groups02].map(toPricingPlan).filter(Boolean)
|
|
333
|
+
)
|
|
334
|
+
const trainingItems = computed(() =>
|
|
335
|
+
[props.itemId01Groups03, props.itemId02Groups03, props.itemId03Groups03].map(toPricingPlan).filter(Boolean)
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
const activePlans = computed(() => {
|
|
339
|
+
if (activeTab.value === 'workshop') {
|
|
340
|
+
return workshopItems.value.length > 0 ? workshopItems.value : workshopPlans
|
|
341
|
+
}
|
|
342
|
+
if (activeTab.value === 'training') {
|
|
343
|
+
return trainingItems.value.length > 0 ? trainingItems.value : trainingPlans
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const source = hciItems.value.length > 0 ? hciItems.value : hciPlans
|
|
347
|
+
return source.map((plan) => ({
|
|
348
|
+
...plan,
|
|
349
|
+
price: plan.prices?.[activePeriod.value],
|
|
350
|
+
priceNote: `Giá thuê khi ký hợp đồng ${activePeriod.value} tháng`,
|
|
351
|
+
}))
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
function formatPrice(value) {
|
|
355
|
+
if (!value && value !== 0) return ''
|
|
356
|
+
return new Intl.NumberFormat('vi-VN').format(value)
|
|
357
|
+
}
|
|
358
|
+
</script>
|
|
359
|
+
|
|
360
|
+
<style scoped>
|
|
361
|
+
.pricing-reveal {
|
|
362
|
+
animation: pricing-card-in 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
363
|
+
animation-delay: calc(var(--i, 0) * 110ms);
|
|
364
|
+
animation-play-state: paused;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.pricing-revealed .pricing-reveal {
|
|
368
|
+
animation-play-state: running;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
@keyframes pricing-card-in {
|
|
372
|
+
from {
|
|
373
|
+
opacity: 0;
|
|
374
|
+
transform: translateY(24px);
|
|
375
|
+
}
|
|
376
|
+
to {
|
|
377
|
+
opacity: 1;
|
|
378
|
+
transform: translateY(0);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.fade-swap-enter-active,
|
|
383
|
+
.fade-swap-leave-active {
|
|
384
|
+
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.fade-swap-enter-from {
|
|
388
|
+
opacity: 0;
|
|
389
|
+
transform: translateY(10px);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.fade-swap-leave-to {
|
|
393
|
+
opacity: 0;
|
|
394
|
+
transform: translateY(-6px);
|
|
395
|
+
}
|
|
396
|
+
</style>
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = withDefaults(defineProps<{
|
|
3
|
+
plans?: Array<{
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
level: string
|
|
7
|
+
duration: string
|
|
8
|
+
icon: string
|
|
9
|
+
price?: string
|
|
10
|
+
description?: string
|
|
11
|
+
}>
|
|
12
|
+
selectedPlanId?: string
|
|
13
|
+
}>(), {
|
|
14
|
+
plans: () => [],
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const { addOpportunity } = useCrmApi()
|
|
18
|
+
|
|
19
|
+
// Form state
|
|
20
|
+
const selectedCourseId = ref(props.selectedPlanId || '')
|
|
21
|
+
const form = reactive({ name: '', phone: '', email: '', note: '' })
|
|
22
|
+
const studyMode = ref('online')
|
|
23
|
+
const errors = reactive({ name: '', phone: '', email: '', course: '' })
|
|
24
|
+
const touched = reactive({ name: false, phone: false, email: false, course: false })
|
|
25
|
+
const submitting = ref(false)
|
|
26
|
+
const submitSuccess = ref(false)
|
|
27
|
+
|
|
28
|
+
// Auto-select first plan if none selected
|
|
29
|
+
watch(() => props.plans, (list: any) => {
|
|
30
|
+
if (!selectedCourseId.value && list.length > 0) {
|
|
31
|
+
selectedCourseId.value = list[0].id
|
|
32
|
+
}
|
|
33
|
+
}, { immediate: true })
|
|
34
|
+
|
|
35
|
+
// Sync when selectedPlanId prop changes
|
|
36
|
+
watch(() => props.selectedPlanId, (id) => {
|
|
37
|
+
if (id) selectedCourseId.value = id
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const selectedPlan = computed(() => props.plans.find((p) => p.id === selectedCourseId.value))
|
|
41
|
+
|
|
42
|
+
// Validation
|
|
43
|
+
function touch(field: keyof typeof touched) {
|
|
44
|
+
touched[field] = true
|
|
45
|
+
validateField(field)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function validateField(field: keyof typeof touched) {
|
|
49
|
+
if (field === 'name') {
|
|
50
|
+
if (!form.name) errors.name = 'Vui lòng nhập họ tên'
|
|
51
|
+
else if (form.name.length < 2) errors.name = 'Họ tên phải có ít nhất 2 ký tự'
|
|
52
|
+
else errors.name = ''
|
|
53
|
+
}
|
|
54
|
+
if (field === 'phone') {
|
|
55
|
+
if (!form.phone) errors.phone = 'Vui lòng nhập số điện thoại'
|
|
56
|
+
else if (!/^(0|\+84)\d{9,10}$/.test(form.phone.replace(/\s/g, ''))) errors.phone = 'Số điện thoại không hợp lệ'
|
|
57
|
+
else errors.phone = ''
|
|
58
|
+
}
|
|
59
|
+
if (field === 'email') {
|
|
60
|
+
if (form.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) errors.email = 'Email không hợp lệ'
|
|
61
|
+
else errors.email = ''
|
|
62
|
+
}
|
|
63
|
+
if (field === 'course') {
|
|
64
|
+
if (!selectedCourseId.value) errors.course = 'Vui lòng chọn khóa học'
|
|
65
|
+
else errors.course = ''
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatLevel(level?: string) {
|
|
70
|
+
const labels: Record<string, string> = {
|
|
71
|
+
basic: 'Cơ bản',
|
|
72
|
+
advanced: 'Nâng cao',
|
|
73
|
+
expert: 'Chuyên gia',
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return labels[level || ''] || level || ''
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Ghép level + duration, bỏ qua phần nào rỗng thay vì để dính dấu "·" trơ
|
|
80
|
+
function planMetaLine(plan: { level?: string; duration?: string }) {
|
|
81
|
+
return [formatLevel(plan.level), plan.duration].filter(Boolean).join(' · ')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function validateAll() {
|
|
85
|
+
; (['name', 'phone', 'email', 'course'] as const).forEach((f) => {
|
|
86
|
+
touched[f] = true
|
|
87
|
+
validateField(f)
|
|
88
|
+
})
|
|
89
|
+
return !errors.name && !errors.phone && !errors.email && !errors.course
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Submit
|
|
93
|
+
async function handleSubmit() {
|
|
94
|
+
if (!validateAll()) return
|
|
95
|
+
submitting.value = true
|
|
96
|
+
submitSuccess.value = false
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const hostname = window.location.origin
|
|
100
|
+
const studyModeLabel =
|
|
101
|
+
studyMode.value === 'online'
|
|
102
|
+
? 'Học trực tuyến (Online)'
|
|
103
|
+
: studyMode.value === 'offline'
|
|
104
|
+
? 'Học tại lớp (Offline)'
|
|
105
|
+
: 'Đào tạo doanh nghiệp'
|
|
106
|
+
|
|
107
|
+
const levelLabel =
|
|
108
|
+
selectedPlan.value?.level === 'basic'
|
|
109
|
+
? 'Cơ bản'
|
|
110
|
+
: selectedPlan.value?.level === 'advanced'
|
|
111
|
+
? 'Nâng cao'
|
|
112
|
+
: selectedPlan.value?.level === 'expert'
|
|
113
|
+
? 'Chuyên gia'
|
|
114
|
+
: selectedPlan.value?.level || ''
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
const description = [
|
|
118
|
+
`<strong>Khóa học:</strong> ${selectedPlan.value?.name || ''}`,
|
|
119
|
+
`<strong>Trình độ:</strong> ${levelLabel}`,
|
|
120
|
+
`<strong>Thời lượng:</strong> ${selectedPlan.value?.duration || ''}`,
|
|
121
|
+
`<strong>Hình thức học:</strong> ${studyModeLabel}`,
|
|
122
|
+
`<strong>Họ và tên:</strong> ${form.name}`,
|
|
123
|
+
`<strong>Số điện thoại:</strong> ${form.phone}`,
|
|
124
|
+
`<strong>Email:</strong> ${form.email || 'Không cung cấp'}`,
|
|
125
|
+
`<strong>Ghi chú:</strong> ${form.note || 'Không có'}`,
|
|
126
|
+
].join('<br>')
|
|
127
|
+
|
|
128
|
+
await addOpportunity({
|
|
129
|
+
name: `Yêu cầu đăng ký khóa học ${selectedPlan.value?.name}`,
|
|
130
|
+
description: description,
|
|
131
|
+
referName: form.name,
|
|
132
|
+
referPhone: form.phone,
|
|
133
|
+
referEmail: form.email,
|
|
134
|
+
targetUrl: `${hostname}${window.location.pathname}`,
|
|
135
|
+
extSource: 'WEBSITE',
|
|
136
|
+
})
|
|
137
|
+
submitSuccess.value = true
|
|
138
|
+
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
submitSuccess.value = false
|
|
141
|
+
}, 2000)
|
|
142
|
+
form.name = ''
|
|
143
|
+
form.phone = ''
|
|
144
|
+
form.email = ''
|
|
145
|
+
form.note = ''
|
|
146
|
+
Object.keys(touched).forEach((k) => (touched[k as keyof typeof touched] = false))
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error('Error submitting registration:', err)
|
|
149
|
+
errors.name = 'Gửi đăng ký thất bại, vui lòng thử lại.'
|
|
150
|
+
touched.name = true
|
|
151
|
+
} finally {
|
|
152
|
+
submitting.value = false
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
<template>
|
|
158
|
+
<section class="rounded-2xl border border-[#e7e9f0] bg-white p-8">
|
|
159
|
+
<h2 class="mb-5 text-[17px] font-bold text-slate-900">Thông tin đăng ký</h2>
|
|
160
|
+
|
|
161
|
+
<form @submit.prevent="handleSubmit">
|
|
162
|
+
<!-- Course selection -->
|
|
163
|
+
<div class="mb-5">
|
|
164
|
+
<label class="mb-[10px] block text-[13px] font-semibold text-gray-700">
|
|
165
|
+
{{ plans.length === 1 ? 'Gói bạn đăng ký tư vấn' : 'Chọn khóa học' }}
|
|
166
|
+
</label>
|
|
167
|
+
|
|
168
|
+
<!-- Đã chọn sẵn 1 gói từ trang trước -> chỉ hiển thị thông tin, không cần chọn lại -->
|
|
169
|
+
<div v-if="plans.length === 1" class="flex flex-col gap-2 rounded-[10px] border border-[#2f5fe0] bg-[#eef3ff] px-4 py-[14px]">
|
|
170
|
+
<div class="flex items-center gap-3">
|
|
171
|
+
<span class="relative h-[18px] w-[18px] flex-none rounded-[10px] border-2 border-[#2f5fe0]">
|
|
172
|
+
<span class="absolute inset-[3px] rounded-[10px] bg-[#2f5fe0]" />
|
|
173
|
+
</span>
|
|
174
|
+
<span class="flex flex-col gap-[2px]">
|
|
175
|
+
<span class="text-sm font-semibold text-slate-900">{{ plans[0].name }}</span>
|
|
176
|
+
<span v-if="planMetaLine(plans[0])" class="text-[12.5px] text-gray-500">{{ planMetaLine(plans[0]) }}</span>
|
|
177
|
+
</span>
|
|
178
|
+
</div>
|
|
179
|
+
<p v-if="plans[0].price" class="pl-[30px] text-[12.5px] font-semibold text-[#2f5fe0]">{{ plans[0].price }}</p>
|
|
180
|
+
<p v-if="plans[0].description" class="pl-[30px] text-[12.5px] text-gray-500">{{ plans[0].description }}</p>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div v-else class="flex flex-col gap-[10px]">
|
|
184
|
+
<label v-for="course in plans" :key="course.id"
|
|
185
|
+
class="relative flex cursor-pointer items-center gap-3 rounded-[10px] border px-4 py-[14px] transition-colors"
|
|
186
|
+
:class="selectedCourseId === course.id ? 'border-[#2f5fe0] bg-[#eef3ff]' : 'border-[#e2e5ee] bg-white'">
|
|
187
|
+
<input type="radio" name="course" class="absolute h-0 w-0 opacity-0" :value="course.id"
|
|
188
|
+
v-model="selectedCourseId" @change="touch('course')">
|
|
189
|
+
<span class="relative h-[18px] w-[18px] flex-none rounded-[10px] border-2 transition-colors"
|
|
190
|
+
:class="selectedCourseId === course.id ? 'border-[#2f5fe0]' : 'border-[#cbd2e1]'">
|
|
191
|
+
<span v-if="selectedCourseId === course.id" class="absolute inset-[3px] rounded-[10px] bg-[#2f5fe0]" />
|
|
192
|
+
</span>
|
|
193
|
+
<span class="flex flex-col gap-[2px]">
|
|
194
|
+
<span class="text-sm font-semibold text-slate-900">{{ course.name }}</span>
|
|
195
|
+
<span v-if="planMetaLine(course)" class="text-[12.5px] text-gray-500">{{ planMetaLine(course) }}</span>
|
|
196
|
+
</span>
|
|
197
|
+
</label>
|
|
198
|
+
</div>
|
|
199
|
+
<p v-if="touched.course && errors.course" class="mt-1 text-[12px] text-red-500">{{ errors.course }}</p>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<!-- Name & Phone -->
|
|
203
|
+
<div class="mb-5 grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
204
|
+
<div>
|
|
205
|
+
<label class="mb-[10px] block text-[13px] font-semibold text-gray-700" for="fullname">Họ và tên</label>
|
|
206
|
+
<input id="fullname" v-model="form.name" type="text"
|
|
207
|
+
class="w-full rounded-[10px] border px-[14px] py-[11px] text-sm text-slate-900 placeholder:text-gray-400 focus:outline-none focus:ring-[3px]"
|
|
208
|
+
:class="touched.name && errors.name ? 'border-red-400 focus:border-red-400 focus:ring-red-400/15' : 'border-[#e2e5ee] focus:border-[#2f5fe0] focus:ring-[#2f5fe0]/15'"
|
|
209
|
+
placeholder="Nguyễn Văn A" @blur="touch('name')">
|
|
210
|
+
<p v-if="touched.name && errors.name" class="mt-1 text-[12px] text-red-500">{{ errors.name }}</p>
|
|
211
|
+
</div>
|
|
212
|
+
<div>
|
|
213
|
+
<label class="mb-[10px] block text-[13px] font-semibold text-gray-700" for="phone">Số điện thoại</label>
|
|
214
|
+
<input id="phone" v-model="form.phone" type="tel"
|
|
215
|
+
class="w-full rounded-[10px] border px-[14px] py-[11px] text-sm text-slate-900 placeholder:text-gray-400 focus:outline-none focus:ring-[3px]"
|
|
216
|
+
:class="touched.phone && errors.phone ? 'border-red-400 focus:border-red-400 focus:ring-red-400/15' : 'border-[#e2e5ee] focus:border-[#2f5fe0] focus:ring-[#2f5fe0]/15'"
|
|
217
|
+
placeholder="0912 345 678" @blur="touch('phone')">
|
|
218
|
+
<p v-if="touched.phone && errors.phone" class="mt-1 text-[12px] text-red-500">{{ errors.phone }}</p>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<!-- Email -->
|
|
223
|
+
<div class="mb-5">
|
|
224
|
+
<label class="mb-[10px] block text-[13px] font-semibold text-gray-700" for="email">Email</label>
|
|
225
|
+
<input id="email" v-model="form.email" type="email"
|
|
226
|
+
class="w-full rounded-[10px] border px-[14px] py-[11px] text-sm text-slate-900 placeholder:text-gray-400 focus:outline-none focus:ring-[3px]"
|
|
227
|
+
:class="touched.email && errors.email ? 'border-red-400 focus:border-red-400 focus:ring-red-400/15' : 'border-[#e2e5ee] focus:border-[#2f5fe0] focus:ring-[#2f5fe0]/15'"
|
|
228
|
+
placeholder="example@congty.vn" @blur="touch('email')">
|
|
229
|
+
<p v-if="touched.email && errors.email" class="mt-1 text-[12px] text-red-500">{{ errors.email }}</p>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<!-- Study mode -->
|
|
233
|
+
<div class="mb-5">
|
|
234
|
+
<label class="mb-[10px] block text-[13px] font-semibold text-gray-700">Hình thức học</label>
|
|
235
|
+
<div class="flex flex-wrap gap-[10px]">
|
|
236
|
+
<button type="button" class="rounded-[10px] border px-4 py-[9px] text-[13.5px] font-medium transition-colors"
|
|
237
|
+
:class="studyMode === 'online' ? 'border-[#2f5fe0] bg-[#2f5fe0] text-white' : 'border-[#e2e5ee] bg-white text-gray-700'"
|
|
238
|
+
@click="studyMode = 'online'">
|
|
239
|
+
Học trực tuyến (Online)
|
|
240
|
+
</button>
|
|
241
|
+
<button type="button" class="rounded-[10px] border px-4 py-[9px] text-[13.5px] font-medium transition-colors"
|
|
242
|
+
:class="studyMode === 'offline' ? 'border-[#2f5fe0] bg-[#2f5fe0] text-white' : 'border-[#e2e5ee] bg-white text-gray-700'"
|
|
243
|
+
@click="studyMode = 'offline'">
|
|
244
|
+
Học tại lớp (Offline)
|
|
245
|
+
</button>
|
|
246
|
+
<button type="button" class="rounded-[10px] border px-4 py-[9px] text-[13.5px] font-medium transition-colors"
|
|
247
|
+
:class="studyMode === 'enterprise' ? 'border-[#2f5fe0] bg-[#2f5fe0] text-white' : 'border-[#e2e5ee] bg-white text-gray-700'"
|
|
248
|
+
@click="studyMode = 'enterprise'">
|
|
249
|
+
Đào tạo doanh nghiệp
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<!-- Notes -->
|
|
255
|
+
<div class="mb-5">
|
|
256
|
+
<label class="mb-[10px] block text-[13px] font-semibold text-gray-700" for="note">Ghi chú (tùy chọn)</label>
|
|
257
|
+
<textarea id="note" v-model="form.note" rows="3"
|
|
258
|
+
class="w-full min-h-[84px] resize-y rounded-[10px] border border-[#e2e5ee] bg-[#fafafb] px-[14px] py-[11px] text-sm text-slate-900 placeholder:text-gray-400 focus:border-[#2f5fe0] focus:outline-none focus:ring-[3px] focus:ring-[#2f5fe0]/15"
|
|
259
|
+
placeholder="Kinh nghiệm hiện tại, mục tiêu học tập..." />
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<!-- Submit -->
|
|
263
|
+
<button type="submit" :disabled="submitting"
|
|
264
|
+
class="flex w-full items-center justify-center gap-2 rounded-[10px] bg-[#2f5fe0] px-5 py-[13px] text-[14.5px] font-semibold text-white transition-colors hover:bg-[#274ec2] disabled:opacity-60">
|
|
265
|
+
<span v-if="submitting">Đang gửi...</span>
|
|
266
|
+
<template v-else>
|
|
267
|
+
Gửi đăng ký
|
|
268
|
+
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
269
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
270
|
+
<line x1="22" y1="2" x2="11" y2="13" />
|
|
271
|
+
<polygon points="22 2 15 22 11 13 2 9 22 2" />
|
|
272
|
+
</svg>
|
|
273
|
+
</template>
|
|
274
|
+
</button>
|
|
275
|
+
</form>
|
|
276
|
+
|
|
277
|
+
<!-- Success message -->
|
|
278
|
+
<div v-if="submitSuccess"
|
|
279
|
+
class="mb-5 mt-5 rounded-[10px] border border-green-200 bg-green-50 p-4 text-sm text-green-700">
|
|
280
|
+
Đăng ký thành công! Chúng tôi sẽ liên hệ với bạn trong thời gian sớm nhất.
|
|
281
|
+
</div>
|
|
282
|
+
</section>
|
|
283
|
+
</template>
|