提交 | 用户 | 时间
|
820397
|
1 |
<template> |
H |
2 |
<div |
|
3 |
:class="prefixCls" |
|
4 |
class="relative h-[100%] lt-xl:bg-[var(--login-bg-color)] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px" |
|
5 |
> |
|
6 |
<div class="relative mx-auto h-full flex"> |
|
7 |
<div |
|
8 |
:class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden`" |
|
9 |
> |
|
10 |
<!-- 左上角的 logo + 系统标题 --> |
|
11 |
<div class="relative flex items-center text-white"> |
|
12 |
<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" /> |
|
13 |
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span> |
|
14 |
</div> |
|
15 |
<!-- 左边的背景图 + 欢迎语 --> |
|
16 |
<div class="h-[calc(100%-60px)] flex items-center justify-center"> |
|
17 |
<TransitionGroup |
|
18 |
appear |
|
19 |
enter-active-class="animate__animated animate__bounceInLeft" |
|
20 |
tag="div" |
|
21 |
> |
|
22 |
<img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" /> |
|
23 |
<div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div> |
|
24 |
<div key="3" class="mt-5 text-14px font-normal text-white"> |
|
25 |
{{ t('login.message') }} |
|
26 |
</div> |
|
27 |
</TransitionGroup> |
|
28 |
</div> |
|
29 |
</div> |
|
30 |
<div class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px"> |
|
31 |
<!-- 右上角的主题、语言选择 --> |
|
32 |
<div |
|
33 |
class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end" |
|
34 |
> |
|
35 |
<div class="flex items-center at-2xl:hidden at-xl:hidden"> |
|
36 |
<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" /> |
|
37 |
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span> |
|
38 |
</div> |
|
39 |
<div class="flex items-center justify-end space-x-10px"> |
|
40 |
<ThemeSwitch /> |
|
41 |
<LocaleDropdown class="dark:text-white lt-xl:text-white" /> |
|
42 |
</div> |
|
43 |
</div> |
|
44 |
<!-- 右边的登录界面 --> |
|
45 |
<Transition appear enter-active-class="animate__animated animate__bounceInRight"> |
|
46 |
<div |
|
47 |
class="m-auto h-full w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px" |
|
48 |
> |
|
49 |
<!-- 账号登录 --> |
|
50 |
<el-form |
|
51 |
v-show="getShow" |
|
52 |
ref="formLogin" |
|
53 |
:model="loginData.loginForm" |
|
54 |
:rules="LoginRules" |
|
55 |
class="login-form" |
|
56 |
label-position="top" |
|
57 |
label-width="120px" |
|
58 |
size="large" |
|
59 |
> |
|
60 |
<el-row style="margin-right: -10px; margin-left: -10px"> |
|
61 |
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
|
62 |
<el-form-item> |
|
63 |
<LoginFormTitle style="width: 100%" /> |
|
64 |
</el-form-item> |
|
65 |
</el-col> |
|
66 |
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
|
67 |
<el-form-item v-if="loginData.tenantEnable" prop="tenantName"> |
|
68 |
<el-input |
|
69 |
v-model="loginData.loginForm.tenantName" |
|
70 |
:placeholder="t('login.tenantNamePlaceholder')" |
|
71 |
:prefix-icon="iconHouse" |
|
72 |
link |
|
73 |
type="primary" |
|
74 |
/> |
|
75 |
</el-form-item> |
|
76 |
</el-col> |
|
77 |
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
|
78 |
<el-form-item prop="username"> |
|
79 |
<el-input |
|
80 |
v-model="loginData.loginForm.username" |
|
81 |
:placeholder="t('login.usernamePlaceholder')" |
|
82 |
:prefix-icon="iconAvatar" |
|
83 |
/> |
|
84 |
</el-form-item> |
|
85 |
</el-col> |
|
86 |
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
|
87 |
<el-form-item prop="password"> |
|
88 |
<el-input |
|
89 |
v-model="loginData.loginForm.password" |
|
90 |
:placeholder="t('login.passwordPlaceholder')" |
|
91 |
:prefix-icon="iconLock" |
|
92 |
show-password |
|
93 |
type="password" |
|
94 |
@keyup.enter="getCode()" |
|
95 |
/> |
|
96 |
</el-form-item> |
|
97 |
</el-col> |
|
98 |
<el-col |
|
99 |
:span="24" |
|
100 |
style=" |
|
101 |
padding-right: 10px; |
|
102 |
padding-left: 10px; |
|
103 |
margin-top: -20px; |
|
104 |
margin-bottom: -20px; |
|
105 |
" |
|
106 |
> |
|
107 |
<el-form-item> |
|
108 |
<el-row justify="space-between" style="width: 100%"> |
|
109 |
<el-col :span="6"> |
|
110 |
<el-checkbox v-model="loginData.loginForm.rememberMe"> |
|
111 |
{{ t('login.remember') }} |
|
112 |
</el-checkbox> |
|
113 |
</el-col> |
|
114 |
<el-col :offset="6" :span="12"> |
|
115 |
<el-link style="float: right" type="primary">{{ |
|
116 |
t('login.forgetPassword') |
|
117 |
}}</el-link> |
|
118 |
</el-col> |
|
119 |
</el-row> |
|
120 |
</el-form-item> |
|
121 |
</el-col> |
|
122 |
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
|
123 |
<el-form-item> |
|
124 |
<XButton |
|
125 |
:loading="loginLoading" |
|
126 |
:title="t('login.login')" |
|
127 |
class="w-[100%]" |
|
128 |
type="primary" |
|
129 |
@click="getCode()" |
|
130 |
/> |
|
131 |
</el-form-item> |
|
132 |
</el-col> |
|
133 |
<Verify |
|
134 |
ref="verify" |
|
135 |
:captchaType="captchaType" |
|
136 |
:imgSize="{ width: '400px', height: '200px' }" |
|
137 |
mode="pop" |
|
138 |
@success="handleLogin" |
|
139 |
/> |
|
140 |
</el-row> |
|
141 |
</el-form> |
|
142 |
</div> |
|
143 |
</Transition> |
|
144 |
</div> |
|
145 |
</div> |
|
146 |
</div> |
|
147 |
</template> |
|
148 |
|
|
149 |
<script lang="ts" setup> |
|
150 |
import { underlineToHump } from '@/utils' |
|
151 |
|
|
152 |
import { ElLoading } from 'element-plus' |
|
153 |
|
|
154 |
import { useDesign } from '@/hooks/web/useDesign' |
|
155 |
import { useAppStore } from '@/store/modules/app' |
|
156 |
import { useIcon } from '@/hooks/web/useIcon' |
|
157 |
import { usePermissionStore } from '@/store/modules/permission' |
|
158 |
|
|
159 |
import * as LoginApi from '@/api/login' |
|
160 |
import * as authUtil from '@/utils/auth' |
|
161 |
import { ThemeSwitch } from '@/layout/components/ThemeSwitch' |
|
162 |
import { LocaleDropdown } from '@/layout/components/LocaleDropdown' |
|
163 |
import { LoginStateEnum, useFormValid, useLoginState } from './components/useLogin' |
|
164 |
import LoginFormTitle from './components/LoginFormTitle.vue' |
|
165 |
import router from '@/router' |
|
166 |
|
|
167 |
defineOptions({ name: 'SocialLogin' }) |
|
168 |
|
|
169 |
const { t } = useI18n() |
|
170 |
const route = useRoute() |
|
171 |
|
|
172 |
const appStore = useAppStore() |
|
173 |
const { getPrefixCls } = useDesign() |
|
174 |
const prefixCls = getPrefixCls('login') |
|
175 |
const iconHouse = useIcon({ icon: 'ep:house' }) |
|
176 |
const iconAvatar = useIcon({ icon: 'ep:avatar' }) |
|
177 |
const iconLock = useIcon({ icon: 'ep:lock' }) |
|
178 |
const formLogin = ref<any>() |
|
179 |
const { validForm } = useFormValid(formLogin) |
|
180 |
const { getLoginState } = useLoginState() |
|
181 |
const { push } = useRouter() |
|
182 |
const permissionStore = usePermissionStore() |
|
183 |
const loginLoading = ref(false) |
|
184 |
const verify = ref() |
|
185 |
const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字 |
|
186 |
|
|
187 |
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN) |
|
188 |
|
|
189 |
const LoginRules = { |
|
190 |
tenantName: [required], |
|
191 |
username: [required], |
|
192 |
password: [required] |
|
193 |
} |
|
194 |
const loginData = reactive({ |
|
195 |
isShowPassword: false, |
|
196 |
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE !== 'false', |
|
197 |
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE !== 'false', |
|
198 |
loginForm: { |
|
199 |
tenantName: 'iailab', |
|
200 |
username: 'admin', |
|
201 |
password: 'admin123', |
|
202 |
captchaVerification: '', |
|
203 |
rememberMe: false |
|
204 |
} |
|
205 |
}) |
|
206 |
|
|
207 |
// 获取验证码 |
|
208 |
const getCode = async () => { |
|
209 |
// 情况一,未开启:则直接登录 |
|
210 |
if (!loginData.captchaEnable) { |
|
211 |
await handleLogin({}) |
|
212 |
} else { |
|
213 |
// 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录 |
|
214 |
// 弹出验证码 |
|
215 |
verify.value.show() |
|
216 |
} |
|
217 |
} |
|
218 |
//获取租户ID |
|
219 |
const getTenantId = async () => { |
|
220 |
if (loginData.tenantEnable) { |
|
221 |
const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName) |
|
222 |
authUtil.setTenantId(res) |
|
223 |
} |
|
224 |
} |
|
225 |
// 记住我 |
|
226 |
const getCookie = () => { |
|
227 |
const loginForm = authUtil.getLoginForm() |
|
228 |
if (loginForm) { |
|
229 |
loginData.loginForm = { |
|
230 |
...loginData.loginForm, |
|
231 |
username: loginForm.username ? loginForm.username : loginData.loginForm.username, |
|
232 |
password: loginForm.password ? loginForm.password : loginData.loginForm.password, |
|
233 |
rememberMe: loginForm.rememberMe ? true : false, |
|
234 |
tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName |
|
235 |
} |
|
236 |
} |
|
237 |
} |
|
238 |
const loading = ref() // ElLoading.service 返回的实例 |
|
239 |
|
|
240 |
// tricky: 配合LoginForm.vue中redirectUri需要对参数进行encode,需要在回调后进行decode |
|
241 |
function getUrlValue(key: string): string { |
|
242 |
const url = new URL(decodeURIComponent(location.href)) |
|
243 |
return url.searchParams.get(key) ?? '' |
|
244 |
} |
|
245 |
|
|
246 |
// 尝试登录: 当账号已经绑定,socialLogin会直接获得token |
|
247 |
const tryLogin = async () => { |
|
248 |
try { |
|
249 |
const type = getUrlValue('type') |
|
250 |
const redirect = getUrlValue('redirect') |
|
251 |
const code = route?.query?.code as string |
|
252 |
const state = route?.query?.state as string |
|
253 |
|
|
254 |
const res = await LoginApi.socialLogin(type, code, state) |
|
255 |
authUtil.setToken(res) |
|
256 |
|
|
257 |
router.push({ path: redirect || '/' }) |
|
258 |
} catch (err) {} |
|
259 |
} |
|
260 |
|
|
261 |
// 登录 |
|
262 |
const handleLogin = async (params) => { |
|
263 |
loginLoading.value = true |
|
264 |
try { |
|
265 |
await getTenantId() |
|
266 |
const data = await validForm() |
|
267 |
if (!data) { |
|
268 |
return |
|
269 |
} |
|
270 |
|
|
271 |
let redirect = getUrlValue('redirect') |
|
272 |
|
|
273 |
const type = getUrlValue('type') |
|
274 |
const code = route?.query?.code as string |
|
275 |
const state = route?.query?.state as string |
|
276 |
|
|
277 |
const res = await LoginApi.login({ |
|
278 |
// 账号密码登录 |
|
279 |
username: loginData.loginForm.username, |
|
280 |
password: loginData.loginForm.password, |
|
281 |
captchaVerification: params.captchaVerification, |
|
282 |
// 社交登录 |
|
283 |
socialCode: code, |
|
284 |
socialState: state, |
|
285 |
socialType: type |
|
286 |
}) |
|
287 |
if (!res) { |
|
288 |
return |
|
289 |
} |
|
290 |
loading.value = ElLoading.service({ |
|
291 |
lock: true, |
|
292 |
text: '正在加载系统中...', |
|
293 |
background: 'rgba(0, 0, 0, 0.7)' |
|
294 |
}) |
|
295 |
if (loginData.loginForm.rememberMe) { |
|
296 |
authUtil.setLoginForm(loginData.loginForm) |
|
297 |
} else { |
|
298 |
authUtil.removeLoginForm() |
|
299 |
} |
|
300 |
authUtil.setToken(res) |
|
301 |
if (!redirect) { |
|
302 |
redirect = '/' |
|
303 |
} |
|
304 |
// 判断是否为SSO登录 |
|
305 |
if (redirect.indexOf('sso') !== -1) { |
|
306 |
window.location.href = window.location.href.replace('/login?redirect=', '') |
|
307 |
} else { |
|
308 |
push({ path: redirect || permissionStore.addRouters[0].path }) |
|
309 |
} |
|
310 |
} finally { |
|
311 |
loginLoading.value = false |
|
312 |
loading.value.close() |
|
313 |
} |
|
314 |
} |
|
315 |
|
|
316 |
onMounted(() => { |
|
317 |
getCookie() |
|
318 |
tryLogin() |
|
319 |
}) |
|
320 |
</script> |
|
321 |
|
|
322 |
<style lang="scss" scoped> |
|
323 |
$prefix-cls: #{$namespace}-login; |
|
324 |
|
|
325 |
.#{$prefix-cls} { |
|
326 |
overflow: auto; |
|
327 |
|
|
328 |
&__left { |
|
329 |
&::before { |
|
330 |
position: absolute; |
|
331 |
top: 0; |
|
332 |
left: 0; |
|
333 |
z-index: -1; |
|
334 |
width: 100%; |
|
335 |
height: 100%; |
|
336 |
background-image: url('@/assets/svgs/login-bg.svg'); |
|
337 |
background-position: center; |
|
338 |
background-repeat: no-repeat; |
|
339 |
content: ''; |
|
340 |
} |
|
341 |
} |
|
342 |
} |
|
343 |
</style> |