nuxt-layer-core 1.0.8 → 1.1.0
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.
|
@@ -1,283 +1,285 @@
|
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
form.
|
|
145
|
-
form.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
+
targetId: selectedPlan.value?.id || '',
|
|
135
|
+
targetType: 'PRODUCT',
|
|
136
|
+
targetUrl: `${hostname}${window.location.pathname}`,
|
|
137
|
+
extSource: 'WEBSITE',
|
|
138
|
+
})
|
|
139
|
+
submitSuccess.value = true
|
|
140
|
+
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
submitSuccess.value = false
|
|
143
|
+
}, 2000)
|
|
144
|
+
form.name = ''
|
|
145
|
+
form.phone = ''
|
|
146
|
+
form.email = ''
|
|
147
|
+
form.note = ''
|
|
148
|
+
Object.keys(touched).forEach((k) => (touched[k as keyof typeof touched] = false))
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error('Error submitting registration:', err)
|
|
151
|
+
errors.name = 'Gửi đăng ký thất bại, vui lòng thử lại.'
|
|
152
|
+
touched.name = true
|
|
153
|
+
} finally {
|
|
154
|
+
submitting.value = false
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
</script>
|
|
158
|
+
|
|
159
|
+
<template>
|
|
160
|
+
<section class="rounded-2xl border border-[#e7e9f0] bg-white p-8">
|
|
161
|
+
<h2 class="mb-5 text-[17px] font-bold text-slate-900">Thông tin đăng ký</h2>
|
|
162
|
+
|
|
163
|
+
<form @submit.prevent="handleSubmit">
|
|
164
|
+
<!-- Course selection -->
|
|
165
|
+
<div class="mb-5">
|
|
166
|
+
<label class="mb-[10px] block text-[13px] font-semibold text-gray-700">
|
|
167
|
+
{{ plans.length === 1 ? 'Gói bạn đăng ký tư vấn' : 'Chọn khóa học' }}
|
|
168
|
+
</label>
|
|
169
|
+
|
|
170
|
+
<!-- Đã 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 -->
|
|
171
|
+
<div v-if="plans.length === 1" class="flex flex-col gap-2 rounded-[10px] border border-[#2f5fe0] bg-[#eef3ff] px-4 py-[14px]">
|
|
172
|
+
<div class="flex items-center gap-3">
|
|
173
|
+
<span class="relative h-[18px] w-[18px] flex-none rounded-[10px] border-2 border-[#2f5fe0]">
|
|
174
|
+
<span class="absolute inset-[3px] rounded-[10px] bg-[#2f5fe0]" />
|
|
175
|
+
</span>
|
|
176
|
+
<span class="flex flex-col gap-[2px]">
|
|
177
|
+
<span class="text-sm font-semibold text-slate-900">{{ plans[0].name }}</span>
|
|
178
|
+
<span v-if="planMetaLine(plans[0])" class="text-[12.5px] text-gray-500">{{ planMetaLine(plans[0]) }}</span>
|
|
179
|
+
</span>
|
|
180
|
+
</div>
|
|
181
|
+
<p v-if="plans[0].price" class="pl-[30px] text-[12.5px] font-semibold text-[#2f5fe0]">{{ plans[0].price }}</p>
|
|
182
|
+
<p v-if="plans[0].description" class="pl-[30px] text-[12.5px] text-gray-500">{{ plans[0].description }}</p>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div v-else class="flex flex-col gap-[10px]">
|
|
186
|
+
<label v-for="course in plans" :key="course.id"
|
|
187
|
+
class="relative flex cursor-pointer items-center gap-3 rounded-[10px] border px-4 py-[14px] transition-colors"
|
|
188
|
+
:class="selectedCourseId === course.id ? 'border-[#2f5fe0] bg-[#eef3ff]' : 'border-[#e2e5ee] bg-white'">
|
|
189
|
+
<input type="radio" name="course" class="absolute h-0 w-0 opacity-0" :value="course.id"
|
|
190
|
+
v-model="selectedCourseId" @change="touch('course')">
|
|
191
|
+
<span class="relative h-[18px] w-[18px] flex-none rounded-[10px] border-2 transition-colors"
|
|
192
|
+
:class="selectedCourseId === course.id ? 'border-[#2f5fe0]' : 'border-[#cbd2e1]'">
|
|
193
|
+
<span v-if="selectedCourseId === course.id" class="absolute inset-[3px] rounded-[10px] bg-[#2f5fe0]" />
|
|
194
|
+
</span>
|
|
195
|
+
<span class="flex flex-col gap-[2px]">
|
|
196
|
+
<span class="text-sm font-semibold text-slate-900">{{ course.name }}</span>
|
|
197
|
+
<span v-if="planMetaLine(course)" class="text-[12.5px] text-gray-500">{{ planMetaLine(course) }}</span>
|
|
198
|
+
</span>
|
|
199
|
+
</label>
|
|
200
|
+
</div>
|
|
201
|
+
<p v-if="touched.course && errors.course" class="mt-1 text-[12px] text-red-500">{{ errors.course }}</p>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<!-- Name & Phone -->
|
|
205
|
+
<div class="mb-5 grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
206
|
+
<div>
|
|
207
|
+
<label class="mb-[10px] block text-[13px] font-semibold text-gray-700" for="fullname">Họ và tên</label>
|
|
208
|
+
<input id="fullname" v-model="form.name" type="text"
|
|
209
|
+
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]"
|
|
210
|
+
: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'"
|
|
211
|
+
placeholder="Nguyễn Văn A" @blur="touch('name')">
|
|
212
|
+
<p v-if="touched.name && errors.name" class="mt-1 text-[12px] text-red-500">{{ errors.name }}</p>
|
|
213
|
+
</div>
|
|
214
|
+
<div>
|
|
215
|
+
<label class="mb-[10px] block text-[13px] font-semibold text-gray-700" for="phone">Số điện thoại</label>
|
|
216
|
+
<input id="phone" v-model="form.phone" type="tel"
|
|
217
|
+
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]"
|
|
218
|
+
: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'"
|
|
219
|
+
placeholder="0912 345 678" @blur="touch('phone')">
|
|
220
|
+
<p v-if="touched.phone && errors.phone" class="mt-1 text-[12px] text-red-500">{{ errors.phone }}</p>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<!-- Email -->
|
|
225
|
+
<div class="mb-5">
|
|
226
|
+
<label class="mb-[10px] block text-[13px] font-semibold text-gray-700" for="email">Email</label>
|
|
227
|
+
<input id="email" v-model="form.email" type="email"
|
|
228
|
+
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]"
|
|
229
|
+
: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'"
|
|
230
|
+
placeholder="example@congty.vn" @blur="touch('email')">
|
|
231
|
+
<p v-if="touched.email && errors.email" class="mt-1 text-[12px] text-red-500">{{ errors.email }}</p>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<!-- Study mode -->
|
|
235
|
+
<div class="mb-5">
|
|
236
|
+
<label class="mb-[10px] block text-[13px] font-semibold text-gray-700">Hình thức học</label>
|
|
237
|
+
<div class="flex flex-wrap gap-[10px]">
|
|
238
|
+
<button type="button" class="rounded-[10px] border px-4 py-[9px] text-[13.5px] font-medium transition-colors"
|
|
239
|
+
:class="studyMode === 'online' ? 'border-[#2f5fe0] bg-[#2f5fe0] text-white' : 'border-[#e2e5ee] bg-white text-gray-700'"
|
|
240
|
+
@click="studyMode = 'online'">
|
|
241
|
+
Học trực tuyến (Online)
|
|
242
|
+
</button>
|
|
243
|
+
<button type="button" class="rounded-[10px] border px-4 py-[9px] text-[13.5px] font-medium transition-colors"
|
|
244
|
+
:class="studyMode === 'offline' ? 'border-[#2f5fe0] bg-[#2f5fe0] text-white' : 'border-[#e2e5ee] bg-white text-gray-700'"
|
|
245
|
+
@click="studyMode = 'offline'">
|
|
246
|
+
Học tại lớp (Offline)
|
|
247
|
+
</button>
|
|
248
|
+
<button type="button" class="rounded-[10px] border px-4 py-[9px] text-[13.5px] font-medium transition-colors"
|
|
249
|
+
:class="studyMode === 'enterprise' ? 'border-[#2f5fe0] bg-[#2f5fe0] text-white' : 'border-[#e2e5ee] bg-white text-gray-700'"
|
|
250
|
+
@click="studyMode = 'enterprise'">
|
|
251
|
+
Đào tạo doanh nghiệp
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<!-- Notes -->
|
|
257
|
+
<div class="mb-5">
|
|
258
|
+
<label class="mb-[10px] block text-[13px] font-semibold text-gray-700" for="note">Ghi chú (tùy chọn)</label>
|
|
259
|
+
<textarea id="note" v-model="form.note" rows="3"
|
|
260
|
+
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"
|
|
261
|
+
placeholder="Kinh nghiệm hiện tại, mục tiêu học tập..." />
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<!-- Submit -->
|
|
265
|
+
<button type="submit" :disabled="submitting"
|
|
266
|
+
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">
|
|
267
|
+
<span v-if="submitting">Đang gửi...</span>
|
|
268
|
+
<template v-else>
|
|
269
|
+
Gửi đăng ký
|
|
270
|
+
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
271
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
272
|
+
<line x1="22" y1="2" x2="11" y2="13" />
|
|
273
|
+
<polygon points="22 2 15 22 11 13 2 9 22 2" />
|
|
274
|
+
</svg>
|
|
275
|
+
</template>
|
|
276
|
+
</button>
|
|
277
|
+
</form>
|
|
278
|
+
|
|
279
|
+
<!-- Success message -->
|
|
280
|
+
<div v-if="submitSuccess"
|
|
281
|
+
class="mb-5 mt-5 rounded-[10px] border border-green-200 bg-green-50 p-4 text-sm text-green-700">
|
|
282
|
+
Đă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.
|
|
283
|
+
</div>
|
|
284
|
+
</section>
|
|
285
|
+
</template>
|