nuxt-layer-core 1.1.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/layers/solution/app/components/CtaSection.vue +347 -347
- package/app/layers/solution/app/components/HeroSection.vue +143 -143
- package/app/layers/solution/app/components/RegistrationForm.vue +9 -6
- package/app/layers/solution/app/components/RegistrationSection.vue +24 -24
- package/app/layers/solution/app/components/RegistrationSidebar.vue +70 -70
- package/app/layers/solution/app/components/TheHeader.vue +255 -192
- package/app/layers/solution/app/components/proxmox/ComparisonSection.vue +8 -8
- package/package.json +52 -52
|
@@ -1,193 +1,256 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<header class="fixed top-0 left-0 right-0 z-50 bg-white/90 backdrop-blur-md border-b border-slate-200/70 shadow-sm">
|
|
3
|
-
<div class="max-w-7xl mx-auto px-6 lg:px-8 flex items-center justify-between h-16">
|
|
4
|
-
<NuxtLink :to="`/giai-phap-proxmox/khoa-hoc-proxmox-hci`" class="flex items-center gap-2.5">
|
|
5
|
-
<img src="/images/logo-lvs-1300x330.png" alt="Long Vân" class="h-8 w-auto" />
|
|
6
|
-
</NuxtLink>
|
|
7
|
-
|
|
8
|
-
<nav class="hidden md:flex items-center gap-8 text-sm font-medium text-slate-600">
|
|
9
|
-
<template v-for="link in navLinks" :key="link.id">
|
|
10
|
-
<template v-if="link.path">
|
|
11
|
-
<NuxtLink v-if="link.path && link.path != route.path" :to="`${link.path}`" :class="[
|
|
12
|
-
'pb-1 border-b-2 transition-all duration-200 cursor-pointer',
|
|
13
|
-
activeSection === link.id
|
|
14
|
-
? 'text-[#004AC6] font-bold border-[#004AC6]'
|
|
15
|
-
: 'border-transparent hover:text-[#004AC6]'
|
|
16
|
-
]">
|
|
17
|
-
{{ link.label }}
|
|
18
|
-
</NuxtLink>
|
|
19
|
-
<a v-else :href="`/giai-phap-proxmox/khoa-hoc-proxmox-hci#${link.id}`" @click.prevent="handleAnchorClick(link)" :class="[
|
|
20
|
-
'pb-1 border-b-2 transition-all duration-200 cursor-pointer',
|
|
21
|
-
activeSection === link.id
|
|
22
|
-
? 'text-[#004AC6] font-bold border-[#004AC6]'
|
|
23
|
-
: 'border-transparent hover:text-[#004AC6]'
|
|
24
|
-
]">
|
|
25
|
-
{{ link.label }}
|
|
26
|
-
</a>
|
|
27
|
-
</template>
|
|
28
|
-
<a v-else :href="`/giai-phap-proxmox/khoa-hoc-proxmox-hci#${link.id}`" @click.prevent="handleAnchorClick(link)" :class="[
|
|
29
|
-
'pb-1 border-b-2 transition-all duration-200 cursor-pointer',
|
|
30
|
-
activeSection === link.id
|
|
31
|
-
? 'text-[#004AC6] font-bold border-[#004AC6]'
|
|
32
|
-
: 'border-transparent hover:text-[#004AC6]'
|
|
33
|
-
]">
|
|
34
|
-
{{ link.label }}
|
|
35
|
-
</a>
|
|
36
|
-
</template>
|
|
37
|
-
</nav>
|
|
38
|
-
|
|
39
|
-
<!-- Logged in -->
|
|
40
|
-
<div v-if="user" class="relative">
|
|
41
|
-
<button @click="showMenu = !showMenu"
|
|
42
|
-
class="flex items-center gap-2.5 text-sm font-medium text-slate-700 hover:text-[#004AC6] transition-colors">
|
|
43
|
-
<span
|
|
44
|
-
class="w-8 h-8 rounded-[10px] bg-gradient-to-br from-[#004AC6] to-[#1D4ED8] flex items-center justify-center text-white text-xs font-bold">
|
|
45
|
-
{{ avatarInitial }}
|
|
46
|
-
</span>
|
|
47
|
-
<span class="hidden sm:block">Xin chào, <strong>{{ user.fullName || user.username }}</strong></span>
|
|
48
|
-
<Icon name="lucide:chevron-down" class="text-[14px] text-slate-400" />
|
|
49
|
-
</button>
|
|
50
|
-
|
|
51
|
-
<!-- Dropdown -->
|
|
52
|
-
<Transition enter-active-class="transition ease-out duration-100" enter-from-class="opacity-0 scale-95"
|
|
53
|
-
enter-to-class="opacity-100 scale-100" leave-active-class="transition ease-in duration-75"
|
|
54
|
-
leave-from-class="opacity-100 scale-100" leave-to-class="opacity-0 scale-95">
|
|
55
|
-
<div v-if="showMenu"
|
|
56
|
-
class="absolute right-0 mt-2 w-48 bg-white rounded-xl shadow-lg border border-slate-200/80 py-1.5 z-50">
|
|
57
|
-
<button @click="logout"
|
|
58
|
-
class="w-full flex items-center gap-2.5 px-4 py-2.5 text-sm text-red-500 hover:bg-red-50 transition-colors">
|
|
59
|
-
<Icon name="lucide:log-out" class="text-[15px]" />
|
|
60
|
-
Đăng xuất
|
|
61
|
-
</button>
|
|
62
|
-
</div>
|
|
63
|
-
</Transition>
|
|
64
|
-
</div>
|
|
65
|
-
|
|
66
|
-
<!-- Not logged in -->
|
|
67
|
-
<div v-else class="flex items-center gap-3">
|
|
68
|
-
<a v-if="isConsultant" :href="`/giai-phap-proxmox/khoa-hoc-proxmox-hci#lien-he`" @click.prevent="scrollTo('lien-he')"
|
|
69
|
-
class="inline-flex items-center gap-1.5 text-sm font-bold px-5 py-2.5 rounded-[10px] border border-[#004AC6] text-[#004AC6] hover:bg-blue-50 transition-all active:scale-95 cursor-pointer">
|
|
70
|
-
Liên hệ tư vấn
|
|
71
|
-
<Icon name="lucide:headset" class="text-[16px]" />
|
|
72
|
-
</a>
|
|
73
|
-
<a :href="loginHref"
|
|
74
|
-
class="inline-flex items-center gap-1.5 text-sm font-bold px-5 py-2.5 rounded-[10px] bg-[#004AC6] text-white hover:bg-[#1D4ED8] transition-all active:scale-95 shadow-sm shadow-blue-600/20">
|
|
75
|
-
Đăng nhập
|
|
76
|
-
<Icon name="lucide:log-in" class="text-[16px]" />
|
|
77
|
-
</a>
|
|
78
|
-
</div>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
})
|
|
1
|
+
<template>
|
|
2
|
+
<header class="fixed top-0 left-0 right-0 z-50 bg-white/90 backdrop-blur-md border-b border-slate-200/70 shadow-sm">
|
|
3
|
+
<div class="max-w-7xl mx-auto px-6 lg:px-8 flex items-center justify-between h-16">
|
|
4
|
+
<NuxtLink :to="`/giai-phap-proxmox/khoa-hoc-proxmox-hci`" class="flex items-center gap-2.5">
|
|
5
|
+
<img src="/images/logo-lvs-1300x330.png" alt="Long Vân" class="h-8 w-auto" />
|
|
6
|
+
</NuxtLink>
|
|
7
|
+
|
|
8
|
+
<nav class="hidden md:flex items-center gap-8 text-sm font-medium text-slate-600">
|
|
9
|
+
<template v-for="link in navLinks" :key="link.id">
|
|
10
|
+
<template v-if="link.path">
|
|
11
|
+
<NuxtLink v-if="link.path && link.path != route.path" :to="`${link.path}`" :class="[
|
|
12
|
+
'pb-1 border-b-2 transition-all duration-200 cursor-pointer',
|
|
13
|
+
activeSection === link.id
|
|
14
|
+
? 'text-[#004AC6] font-bold border-[#004AC6]'
|
|
15
|
+
: 'border-transparent hover:text-[#004AC6]'
|
|
16
|
+
]">
|
|
17
|
+
{{ link.label }}
|
|
18
|
+
</NuxtLink>
|
|
19
|
+
<a v-else :href="`/giai-phap-proxmox/khoa-hoc-proxmox-hci#${link.id}`" @click.prevent="handleAnchorClick(link)" :class="[
|
|
20
|
+
'pb-1 border-b-2 transition-all duration-200 cursor-pointer',
|
|
21
|
+
activeSection === link.id
|
|
22
|
+
? 'text-[#004AC6] font-bold border-[#004AC6]'
|
|
23
|
+
: 'border-transparent hover:text-[#004AC6]'
|
|
24
|
+
]">
|
|
25
|
+
{{ link.label }}
|
|
26
|
+
</a>
|
|
27
|
+
</template>
|
|
28
|
+
<a v-else :href="`/giai-phap-proxmox/khoa-hoc-proxmox-hci#${link.id}`" @click.prevent="handleAnchorClick(link)" :class="[
|
|
29
|
+
'pb-1 border-b-2 transition-all duration-200 cursor-pointer',
|
|
30
|
+
activeSection === link.id
|
|
31
|
+
? 'text-[#004AC6] font-bold border-[#004AC6]'
|
|
32
|
+
: 'border-transparent hover:text-[#004AC6]'
|
|
33
|
+
]">
|
|
34
|
+
{{ link.label }}
|
|
35
|
+
</a>
|
|
36
|
+
</template>
|
|
37
|
+
</nav>
|
|
38
|
+
|
|
39
|
+
<!-- Logged in -->
|
|
40
|
+
<div v-if="user" class="relative hidden md:block">
|
|
41
|
+
<button @click="showMenu = !showMenu"
|
|
42
|
+
class="flex items-center gap-2.5 text-sm font-medium text-slate-700 hover:text-[#004AC6] transition-colors">
|
|
43
|
+
<span
|
|
44
|
+
class="w-8 h-8 rounded-[10px] bg-gradient-to-br from-[#004AC6] to-[#1D4ED8] flex items-center justify-center text-white text-xs font-bold">
|
|
45
|
+
{{ avatarInitial }}
|
|
46
|
+
</span>
|
|
47
|
+
<span class="hidden sm:block">Xin chào, <strong>{{ user.fullName || user.username }}</strong></span>
|
|
48
|
+
<Icon name="lucide:chevron-down" class="text-[14px] text-slate-400" />
|
|
49
|
+
</button>
|
|
50
|
+
|
|
51
|
+
<!-- Dropdown -->
|
|
52
|
+
<Transition enter-active-class="transition ease-out duration-100" enter-from-class="opacity-0 scale-95"
|
|
53
|
+
enter-to-class="opacity-100 scale-100" leave-active-class="transition ease-in duration-75"
|
|
54
|
+
leave-from-class="opacity-100 scale-100" leave-to-class="opacity-0 scale-95">
|
|
55
|
+
<div v-if="showMenu"
|
|
56
|
+
class="absolute right-0 mt-2 w-48 bg-white rounded-xl shadow-lg border border-slate-200/80 py-1.5 z-50">
|
|
57
|
+
<button @click="logout"
|
|
58
|
+
class="w-full flex items-center gap-2.5 px-4 py-2.5 text-sm text-red-500 hover:bg-red-50 transition-colors">
|
|
59
|
+
<Icon name="lucide:log-out" class="text-[15px]" />
|
|
60
|
+
Đăng xuất
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
</Transition>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- Not logged in -->
|
|
67
|
+
<div v-else class="hidden md:flex items-center gap-3">
|
|
68
|
+
<a v-if="isConsultant" :href="`/giai-phap-proxmox/khoa-hoc-proxmox-hci#lien-he`" @click.prevent="scrollTo('lien-he')"
|
|
69
|
+
class="inline-flex items-center gap-1.5 text-sm font-bold px-5 py-2.5 rounded-[10px] border border-[#004AC6] text-[#004AC6] hover:bg-blue-50 transition-all active:scale-95 cursor-pointer">
|
|
70
|
+
Liên hệ tư vấn
|
|
71
|
+
<Icon name="lucide:headset" class="text-[16px]" />
|
|
72
|
+
</a>
|
|
73
|
+
<a :href="loginHref"
|
|
74
|
+
class="inline-flex items-center gap-1.5 text-sm font-bold px-5 py-2.5 rounded-[10px] bg-[#004AC6] text-white hover:bg-[#1D4ED8] transition-all active:scale-95 shadow-sm shadow-blue-600/20">
|
|
75
|
+
Đăng nhập
|
|
76
|
+
<Icon name="lucide:log-in" class="text-[16px]" />
|
|
77
|
+
</a>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<!-- Mobile menu toggle -->
|
|
81
|
+
<button type="button" class="md:hidden flex h-9 w-9 items-center justify-center rounded-[10px] text-slate-600 hover:bg-slate-100 transition-colors"
|
|
82
|
+
:aria-expanded="showMobileMenu" aria-label="Mở menu"
|
|
83
|
+
@click="showMobileMenu = !showMobileMenu">
|
|
84
|
+
<Icon :name="showMobileMenu ? 'lucide:x' : 'lucide:menu'" class="text-[22px]" />
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<!-- Mobile menu panel -->
|
|
89
|
+
<Transition enter-active-class="transition ease-out duration-150" enter-from-class="opacity-0 -translate-y-1"
|
|
90
|
+
enter-to-class="opacity-100 translate-y-0" leave-active-class="transition ease-in duration-100"
|
|
91
|
+
leave-from-class="opacity-100 translate-y-0" leave-to-class="opacity-0 -translate-y-1">
|
|
92
|
+
<div v-if="showMobileMenu" class="md:hidden border-t border-slate-200/70 bg-white px-6 py-4">
|
|
93
|
+
<nav class="flex flex-col gap-1 text-sm font-medium text-slate-600">
|
|
94
|
+
<template v-for="link in navLinks" :key="link.id">
|
|
95
|
+
<NuxtLink v-if="link.path && link.path != route.path" :to="`${link.path}`" class="rounded-[10px] px-3 py-2.5 transition-colors"
|
|
96
|
+
:class="activeSection === link.id ? 'bg-blue-50 text-[#004AC6] font-bold' : 'hover:bg-slate-50'"
|
|
97
|
+
@click="showMobileMenu = false">
|
|
98
|
+
{{ link.label }}
|
|
99
|
+
</NuxtLink>
|
|
100
|
+
<a v-else :href="`/giai-phap-proxmox/khoa-hoc-proxmox-hci#${link.id}`" class="rounded-[10px] px-3 py-2.5 transition-colors"
|
|
101
|
+
:class="activeSection === link.id ? 'bg-blue-50 text-[#004AC6] font-bold' : 'hover:bg-slate-50'"
|
|
102
|
+
@click.prevent="handleAnchorClick(link); showMobileMenu = false">
|
|
103
|
+
{{ link.label }}
|
|
104
|
+
</a>
|
|
105
|
+
</template>
|
|
106
|
+
</nav>
|
|
107
|
+
|
|
108
|
+
<div class="mt-4 flex flex-col gap-3 border-t border-slate-100 pt-4">
|
|
109
|
+
<template v-if="user">
|
|
110
|
+
<div class="flex items-center gap-2.5 px-3 text-sm text-slate-700">
|
|
111
|
+
<span class="w-8 h-8 rounded-[10px] bg-gradient-to-br from-[#004AC6] to-[#1D4ED8] flex items-center justify-center text-white text-xs font-bold">
|
|
112
|
+
{{ avatarInitial }}
|
|
113
|
+
</span>
|
|
114
|
+
Xin chào, <strong>{{ user.fullName || user.username }}</strong>
|
|
115
|
+
</div>
|
|
116
|
+
<button @click="logout(); showMobileMenu = false"
|
|
117
|
+
class="flex items-center justify-center gap-2.5 px-4 py-2.5 rounded-[10px] text-sm font-bold text-red-500 border border-red-200 hover:bg-red-50 transition-colors">
|
|
118
|
+
<Icon name="lucide:log-out" class="text-[15px]" />
|
|
119
|
+
Đăng xuất
|
|
120
|
+
</button>
|
|
121
|
+
</template>
|
|
122
|
+
<template v-else>
|
|
123
|
+
<a v-if="isConsultant" :href="`/giai-phap-proxmox/khoa-hoc-proxmox-hci#lien-he`"
|
|
124
|
+
@click.prevent="scrollTo('lien-he'); showMobileMenu = false"
|
|
125
|
+
class="inline-flex items-center justify-center gap-1.5 text-sm font-bold px-5 py-2.5 rounded-[10px] border border-[#004AC6] text-[#004AC6] hover:bg-blue-50 transition-all cursor-pointer">
|
|
126
|
+
Liên hệ tư vấn
|
|
127
|
+
<Icon name="lucide:headset" class="text-[16px]" />
|
|
128
|
+
</a>
|
|
129
|
+
<a :href="loginHref"
|
|
130
|
+
class="inline-flex items-center justify-center gap-1.5 text-sm font-bold px-5 py-2.5 rounded-[10px] bg-[#004AC6] text-white hover:bg-[#1D4ED8] transition-all shadow-sm shadow-blue-600/20">
|
|
131
|
+
Đăng nhập
|
|
132
|
+
<Icon name="lucide:log-in" class="text-[16px]" />
|
|
133
|
+
</a>
|
|
134
|
+
</template>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</Transition>
|
|
138
|
+
</header>
|
|
139
|
+
</template>
|
|
140
|
+
|
|
141
|
+
<script setup lang="ts">
|
|
142
|
+
const props = defineProps({
|
|
143
|
+
targetCTA: { type: String, default: '' },
|
|
144
|
+
slug: { type: String, default: '' },
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const isConsultant = computed(() => props.targetCTA === 'CONSULTANT')
|
|
148
|
+
|
|
149
|
+
const { getCookie, deleteCookie } = useCookieHelper()
|
|
150
|
+
const { getUserByToken } = useAuthApi()
|
|
151
|
+
const { isLoggedIn, userName, setUser, clearUser } = useAuthState()
|
|
152
|
+
const config = useRuntimeConfig()
|
|
153
|
+
const router = useRouter()
|
|
154
|
+
const route = useRoute()
|
|
155
|
+
|
|
156
|
+
const tokenKey = config.public.ACCESSTOKEN_NAME as string
|
|
157
|
+
|
|
158
|
+
const navLinks = computed(() => [
|
|
159
|
+
{ id: 'san-pham', label: 'Sản phẩm', path: `/gioi-thieu-san-pham/proxmox-hci` },
|
|
160
|
+
{ id: 'giai-phap', label: 'Vì sao chọn chúng tôi' },
|
|
161
|
+
{ id: 'chi-phi', label: 'Học phí' },
|
|
162
|
+
{ id: 'khach-hang', label: 'Học viên nói gì' },
|
|
163
|
+
])
|
|
164
|
+
|
|
165
|
+
const { scrollTo } = useScrollToSection()
|
|
166
|
+
|
|
167
|
+
// route.fullPath gồm cả query hiện tại (vd ?plan=...&name=...) để sau khi đăng nhập quay lại đúng chỗ
|
|
168
|
+
// Trang đăng nhập nằm ở domain LONGVAN_WEB (vd https://dev.longvan.net), không phải app này
|
|
169
|
+
const loginHref = computed(() => `${config.public.LONGVAN_WEB}/dang-nhap?redirectUrl=${encodeURIComponent(route.fullPath)}`)
|
|
170
|
+
|
|
171
|
+
const activeSection = useActiveSection(navLinks.value.map(l => l.id))
|
|
172
|
+
const user = ref<{ fullName?: string; username?: string } | null>(null)
|
|
173
|
+
const showMenu = ref(false)
|
|
174
|
+
const showMobileMenu = ref(false)
|
|
175
|
+
|
|
176
|
+
watch(() => route.fullPath, () => {
|
|
177
|
+
showMobileMenu.value = false
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
function handleNavClick(e: Event, link: any) {
|
|
181
|
+
if (route.path === link.path) {
|
|
182
|
+
e.preventDefault()
|
|
183
|
+
scrollTo(link.id)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function handleAnchorClick(link: any) {
|
|
188
|
+
const el = document.getElementById(link.id)
|
|
189
|
+
if (el) {
|
|
190
|
+
scrollTo(link.id)
|
|
191
|
+
} else {
|
|
192
|
+
router.push(`/giai-phap-proxmox/${props.slug}#${link.id}`)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const avatarInitial = computed(() => {
|
|
197
|
+
const name = user.value?.fullName || user.value?.username || ''
|
|
198
|
+
return name.charAt(0).toUpperCase() || '?'
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
async function loadUser() {
|
|
202
|
+
const token = getCookie(tokenKey)
|
|
203
|
+
if (!token) return
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const res = await getUserByToken(token)
|
|
207
|
+
if (res) {
|
|
208
|
+
user.value = {
|
|
209
|
+
fullName: res.fullName,
|
|
210
|
+
username: res.username || res.userLoginId,
|
|
211
|
+
}
|
|
212
|
+
setUser(res.fullName || res.username || '')
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
deleteCookie(tokenKey)
|
|
216
|
+
clearUser()
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Watch auth state changes (when login/register updates state)
|
|
221
|
+
watch(isLoggedIn, (loggedIn) => {
|
|
222
|
+
if (loggedIn && userName.value) {
|
|
223
|
+
user.value = {
|
|
224
|
+
fullName: userName.value,
|
|
225
|
+
username: userName.value,
|
|
226
|
+
}
|
|
227
|
+
} else if (!loggedIn) {
|
|
228
|
+
user.value = null
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
function logout() {
|
|
233
|
+
deleteCookie(tokenKey)
|
|
234
|
+
user.value = null
|
|
235
|
+
clearUser()
|
|
236
|
+
showMenu.value = false
|
|
237
|
+
router.push('/giai-phap-proxmox')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Close menu on outside click
|
|
241
|
+
function onClickOutside(e: MouseEvent) {
|
|
242
|
+
const target = e.target as HTMLElement
|
|
243
|
+
if (!target.closest('.relative')) {
|
|
244
|
+
showMenu.value = false
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
onMounted(() => {
|
|
249
|
+
loadUser()
|
|
250
|
+
document.addEventListener('click', onClickOutside)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
onUnmounted(() => {
|
|
254
|
+
document.removeEventListener('click', onClickOutside)
|
|
255
|
+
})
|
|
193
256
|
</script>
|
|
@@ -18,25 +18,25 @@ import { comparisonRows } from "../../data/proxmox-content"
|
|
|
18
18
|
</p>
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
|
-
<div v-reveal="{ delay: 180 }" class="mt-12 overflow-
|
|
22
|
-
<table class="w-full border-collapse text-left text-sm">
|
|
21
|
+
<div v-reveal="{ delay: 180 }" class="mt-12 overflow-x-auto rounded-2xl border border-slate-200 shadow-sm">
|
|
22
|
+
<table class="w-full min-w-[520px] border-collapse text-left text-sm">
|
|
23
23
|
<thead>
|
|
24
24
|
<tr class="bg-blue-600 text-white">
|
|
25
|
-
<th class="px-
|
|
26
|
-
<th class="px-
|
|
27
|
-
<th class="px-
|
|
25
|
+
<th class="px-4 py-4 font-semibold sm:px-6">Tiêu chí</th>
|
|
26
|
+
<th class="px-4 py-4 text-center font-semibold sm:px-6">Proxmox HCI</th>
|
|
27
|
+
<th class="px-4 py-4 text-center font-semibold sm:px-6">Hạ tầng truyền thống</th>
|
|
28
28
|
</tr>
|
|
29
29
|
</thead>
|
|
30
30
|
<tbody class="divide-y divide-slate-200 bg-white">
|
|
31
31
|
<tr v-for="row in comparisonRows" :key="row.label">
|
|
32
|
-
<td class="px-
|
|
33
|
-
<td class="px-
|
|
32
|
+
<td class="px-4 py-4 font-medium text-slate-700 sm:px-6">{{ row.label }}</td>
|
|
33
|
+
<td class="px-4 py-4 sm:px-6">
|
|
34
34
|
<span class="mx-auto flex w-fit items-center gap-1.5 font-medium text-emerald-600">
|
|
35
35
|
<ProxmoxAppIcon name="check" class="h-4 w-4 rounded-full bg-emerald-50 p-0.5" />
|
|
36
36
|
{{ row.hci }}
|
|
37
37
|
</span>
|
|
38
38
|
</td>
|
|
39
|
-
<td class="px-
|
|
39
|
+
<td class="px-4 py-4 sm:px-6">
|
|
40
40
|
<span
|
|
41
41
|
class="mx-auto flex w-fit items-center gap-1.5 font-medium"
|
|
42
42
|
:class="row.legacyOk ? 'text-emerald-600' : 'text-rose-500'"
|
package/package.json
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "nuxt-layer-core",
|
|
3
|
-
"type": "module",
|
|
4
|
-
"private": false,
|
|
5
|
-
"version": "1.1.
|
|
6
|
-
"scripts": {
|
|
7
|
-
"build": "env-cmd -f .env nuxt build",
|
|
8
|
-
"dev": "env-cmd -f .env nuxt dev"
|
|
9
|
-
},
|
|
10
|
-
"dependencies": {
|
|
11
|
-
"@longvansoftware/service-js-client": "2.8.6",
|
|
12
|
-
"@lucide/vue": "^1.28.0",
|
|
13
|
-
"@nuxt/icon": "^2.1.0",
|
|
14
|
-
"@nuxt/image": "^2.0.0",
|
|
15
|
-
"@nuxtjs/sitemap": "^7.4.7",
|
|
16
|
-
"@nuxtjs/strapi": "^2.1.1",
|
|
17
|
-
"@nuxtjs/tailwindcss": "^6.14.0",
|
|
18
|
-
"@pinia/nuxt": "^0.11.3",
|
|
19
|
-
"@splidejs/vue-splide": "^0.6.12",
|
|
20
|
-
"@vueuse/core": "^14.4.0",
|
|
21
|
-
"aos": "^2.3.4",
|
|
22
|
-
"class-variance-authority": "^0.7.1",
|
|
23
|
-
"clsx": "^2.1.1",
|
|
24
|
-
"crypto-js": "^4.2.0",
|
|
25
|
-
"env-cmd": "^11.0.0",
|
|
26
|
-
"feather-icons": "^4.29.2",
|
|
27
|
-
"ipx": "^3.1.1",
|
|
28
|
-
"js-cookie": "^3.0.5",
|
|
29
|
-
"nuxt": "4.2.1",
|
|
30
|
-
"nuxt-gtag": "^4.1.0",
|
|
31
|
-
"pinia": "^3.0.4",
|
|
32
|
-
"qs": "^6.14.0",
|
|
33
|
-
"tailwind-merge": "^3.6.0",
|
|
34
|
-
"vue": "^3.5.24",
|
|
35
|
-
"vue-router": "^4.6.3",
|
|
36
|
-
"vue-sonner": "^2.0.9"
|
|
37
|
-
},
|
|
38
|
-
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610",
|
|
39
|
-
"devDependencies": {
|
|
40
|
-
"@iconify-json/lucide": "^1.2.120",
|
|
41
|
-
"@types/js-cookie": "^3.0.6",
|
|
42
|
-
"@types/node": "^24.10.1",
|
|
43
|
-
"@types/qs": "^6.14.0",
|
|
44
|
-
"autoprefixer": "10.4.20",
|
|
45
|
-
"postcss": "8.4.35",
|
|
46
|
-
"sass": "^1.94.2",
|
|
47
|
-
"shadcn-vue": "^2.8.1",
|
|
48
|
-
"tailwindcss": "3.4.15",
|
|
49
|
-
"tw-animate-css": "^1.4.0",
|
|
50
|
-
"vite-tsconfig-paths": "^5.1.4"
|
|
51
|
-
}
|
|
52
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "nuxt-layer-core",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"private": false,
|
|
5
|
+
"version": "1.1.2",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "env-cmd -f .env nuxt build",
|
|
8
|
+
"dev": "env-cmd -f .env nuxt dev"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@longvansoftware/service-js-client": "2.8.6",
|
|
12
|
+
"@lucide/vue": "^1.28.0",
|
|
13
|
+
"@nuxt/icon": "^2.1.0",
|
|
14
|
+
"@nuxt/image": "^2.0.0",
|
|
15
|
+
"@nuxtjs/sitemap": "^7.4.7",
|
|
16
|
+
"@nuxtjs/strapi": "^2.1.1",
|
|
17
|
+
"@nuxtjs/tailwindcss": "^6.14.0",
|
|
18
|
+
"@pinia/nuxt": "^0.11.3",
|
|
19
|
+
"@splidejs/vue-splide": "^0.6.12",
|
|
20
|
+
"@vueuse/core": "^14.4.0",
|
|
21
|
+
"aos": "^2.3.4",
|
|
22
|
+
"class-variance-authority": "^0.7.1",
|
|
23
|
+
"clsx": "^2.1.1",
|
|
24
|
+
"crypto-js": "^4.2.0",
|
|
25
|
+
"env-cmd": "^11.0.0",
|
|
26
|
+
"feather-icons": "^4.29.2",
|
|
27
|
+
"ipx": "^3.1.1",
|
|
28
|
+
"js-cookie": "^3.0.5",
|
|
29
|
+
"nuxt": "4.2.1",
|
|
30
|
+
"nuxt-gtag": "^4.1.0",
|
|
31
|
+
"pinia": "^3.0.4",
|
|
32
|
+
"qs": "^6.14.0",
|
|
33
|
+
"tailwind-merge": "^3.6.0",
|
|
34
|
+
"vue": "^3.5.24",
|
|
35
|
+
"vue-router": "^4.6.3",
|
|
36
|
+
"vue-sonner": "^2.0.9"
|
|
37
|
+
},
|
|
38
|
+
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610",
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@iconify-json/lucide": "^1.2.120",
|
|
41
|
+
"@types/js-cookie": "^3.0.6",
|
|
42
|
+
"@types/node": "^24.10.1",
|
|
43
|
+
"@types/qs": "^6.14.0",
|
|
44
|
+
"autoprefixer": "10.4.20",
|
|
45
|
+
"postcss": "8.4.35",
|
|
46
|
+
"sass": "^1.94.2",
|
|
47
|
+
"shadcn-vue": "^2.8.1",
|
|
48
|
+
"tailwindcss": "3.4.15",
|
|
49
|
+
"tw-animate-css": "^1.4.0",
|
|
50
|
+
"vite-tsconfig-paths": "^5.1.4"
|
|
51
|
+
}
|
|
52
|
+
}
|