front/src/views/login/components/pwd-login/index.vue
2025-12-03 19:26:02 +08:00

522 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<a-form ref="loginFormRef" :model="loginForm" :rules="loginRules" layout="vertical">
<a-form-item v-if="tenantOption === 'CHOSE'" field="tenantId">
<a-select
v-model="loginForm.tenantId"
:options="tenantOptions"
placeholder="请选择租户"
allow-clear
>
<template #option="{ option }">
{{ option.name }}
</template>
</a-select>
</a-form-item>
<a-form-item field="orgId">
<a-select
v-model="loginForm.orgId"
:options="orgOptions"
placeholder="请选择组织"
allow-clear
:loading="orgLoading"
size="large"
/>
</a-form-item>
<a-form-item field="account">
<a-input
v-model="loginForm.account"
placeholder="请输入用户名"
size="large"
>
<template #prefix>
<icon-user />
</template>
</a-input>
</a-form-item>
<a-form-item field="password">
<a-input-password
v-model="loginForm.password"
placeholder="请输入密码"
size="large"
allow-clear
>
<template #prefix>
<icon-lock />
</template>
</a-input-password>
</a-form-item>
<a-form-item v-if="captchaOpen" field="validCode">
<div class="captcha-container">
<a-input
v-model="loginForm.validCode"
placeholder="请输入验证码"
size="large"
>
<template #prefix>
<icon-safe />
</template>
</a-input>
<div class="captcha-img-wrapper">
<img
:src="validCodeBase64"
alt="验证码"
class="captcha-img"
@click="loginCaptcha"
/>
</div>
</div>
</a-form-item>
</a-form>
<div class="login-btn">
<a-button type="primary" size="large" :loading="loading" @click="handleSubmit">
登录
</a-button>
</div>
<!-- 模块选择弹窗 -->
<ModuleSelector
v-model:visible="moduleSelectorVisible"
:module-list="moduleList"
:loading="loading"
@confirm="handleModuleConfirm"
/>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import { IconUser, IconLock, IconSafe } from '@arco-design/web-vue/es/icon';
import { sysAuthBLoginPost, sysAuthBGetPicCaptchaGet } from '@/api/SimpleAdmin/bEndPermissionVerify';
import { sysLoginPolicyGet, sysTenantListGet } from '@/api/SimpleAdmin/common';
import { setToken } from '@/utils/auth';
import { useUserStore } from '@/store';
import smCrypto from '@/utils/smCrypto';
import ModuleSelector from '../module-selector.vue';
const router = useRouter();
const userStore = useUserStore();
// props
interface PwdProps {
tenantOption?: string;
tenantOptions?: any[];
}
const props = withDefaults(defineProps<PwdProps>(), {
tenantOption: 'CLOSE',
tenantOptions: () => [],
});
const loginFormRef = ref();
const loading = ref(false);
const captchaOpen = ref(false);
const validCodeBase64 = ref('');
const validCodeReqNo = ref('');
const orgLoading = ref(false);
const orgOptions = ref<Array<{ label: string; value: number }>>([]);
const moduleSelectorVisible = ref(false);
const moduleList = ref<any[]>([]);
const selectedModuleId = ref<number | null>(null);
// 表单数据
const loginForm = reactive({
account: 'superAdmin',
password: '123456',
validCode: '',
validCodeReqNo: '',
tenantId: undefined as number | undefined,
orgId: undefined as number | undefined,
});
// 表单验证规则
const loginRules = reactive({
account: [{ required: true, message: '请输入用户名' }],
password: [{ required: true, message: '请输入密码' }],
tenantId: [{ required: props.tenantOption === 'CHOSE', message: '请选择租户' }],
orgId: [{ required: false, message: '请选择组织' }],
validCode: [{ required: false, message: '请输入验证码' }],
});
// 获取租户列表(组织列表)
const getOrgTree = async () => {
try {
orgLoading.value = true;
const res = await sysTenantListGet();
// request.ts 已经处理了响应格式,如果响应是 { code: 200, data: [...] },会直接返回 data
// 所以这里需要兼容两种情况:
// 1. res 直接是数组(request.ts 已经提取了 data)
// 2. res 是对象,包含 data 或 list 字段
let list: any[] = [];
if (res) {
if (Array.isArray(res)) {
// 如果 res 直接是数组,说明 request.ts 已经提取了 data
list = res;
} else if (res && typeof res === 'object') {
// 如果 res 是对象,检查是否有 data 或 list 字段
if (Array.isArray(res.data)) {
list = res.data;
} else if (Array.isArray(res.list)) {
list = res.list;
} else if (Array.isArray(res)) {
list = res;
}
}
}
// 转换为下拉选项格式
orgOptions.value = list.map((item: any) => ({
label: item.name || item.tenantName || item.title || item.label || '',
value: item.id || item.tenantId || 0,
name: item.name || item.tenantName || item.title || '', // 保留原始 name
}));
// 默认选择第一个组织(如果未选择且有数据)
if (!loginForm.orgId && orgOptions.value.length > 0) {
loginForm.orgId = orgOptions.value[0].value as number;
}
} catch (error) {
console.error('获取组织列表失败', error);
} finally {
orgLoading.value = false;
}
};
// 加载验证码
const loginCaptcha = async () => {
try {
const res = await sysAuthBGetPicCaptchaGet();
// request.ts 已经处理了响应格式,如果响应是 { code: 200, data: {...} },会直接返回 data
const data = res?.data || res || {};
validCodeBase64.value = data.validCodeBase64 || '';
validCodeReqNo.value = data.validCodeReqNo || '';
} catch (error) {
console.error('获取验证码失败', error);
}
};
// 检查验证码策略
const checkCaptchaPolicy = async () => {
try {
const res = await sysLoginPolicyGet();
// request.ts 已经处理了响应格式,如果响应是 { code: 200, data: [...] },会直接返回 data
const policies = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : []);
const captchaPolicy = policies.find((item: any) => item.configKey === 'LOGIN_CAPTCHA_OPEN');
captchaOpen.value = captchaPolicy?.configValue === 'true';
if (captchaOpen.value) {
await loginCaptcha();
}
} catch (error) {
console.error('获取登录策略失败', error);
}
};
// 提交登录
const handleSubmit = async () => {
try {
await loginFormRef.value?.validate();
loading.value = true;
// 构建登录数据,确保字段格式正确
const loginData: API.LoginInput = {
account: loginForm.account,
password: smCrypto.doSm2Encrypt(loginForm.password), // SM2加密密码
device: 'PC',
validCode: captchaOpen.value ? loginForm.validCode : '', // 没有验证码时发送空字符串
validCodeReqNo: captchaOpen.value ? validCodeReqNo.value : '', // 没有验证码时发送空字符串
tenantId: props.tenantOption === 'CHOSE' ? (loginForm.tenantId || 0) : 0, // 没有租户时发送 0
};
const res = await sysAuthBLoginPost(loginData);
// request.ts 已经处理了响应格式,如果响应是 { code: 200, data: {...} },会直接返回 data
const loginResult = res?.data || res || {};
if (loginResult && (loginResult.token || loginResult.userInfo)) {
// 保存 token(从响应数据或响应头中获取)
if (loginResult.token) {
setToken(loginResult.token);
}
// 保存用户信息
let userInfo: any = null;
if (loginResult.userInfo) {
userInfo = loginResult.userInfo;
userStore.setInfo(userInfo);
} else {
// 如果没有用户信息,调用获取用户信息接口
try {
const { sysAuthBGetLoginUserGet } = await import('@/api/SimpleAdmin/bEndPermissionVerify');
const userRes = await sysAuthBGetLoginUserGet();
// request.ts 已经处理了响应格式,如果响应是 { code: 200, data: {...} },会直接返回 data
userInfo = userRes?.data || userRes || null;
if (userInfo) {
userStore.setInfo(userInfo);
}
} catch (error) {
console.error('获取用户信息失败', error);
}
}
// 检查是否需要选择模块
if (userInfo?.moduleList && Array.isArray(userInfo.moduleList) && userInfo.moduleList.length > 1) {
// 如果用户有多个模块,显示模块选择弹窗
moduleList.value = userInfo.moduleList;
moduleSelectorVisible.value = true;
} else {
// 如果只有一个模块或没有模块,直接跳转
const moduleId = userInfo?.moduleList?.[0]?.id || userInfo?.defaultModule || 0;
selectedModuleId.value = moduleId;
// 设置当前选择的模块(类似 SimpleAdmin)
if (moduleId) {
userStore.setModule(moduleId);
}
// 调用模块确认处理(会初始化动态路由)
await handleModuleConfirm(moduleId);
}
} else {
Message.error('登录失败,请检查账号密码');
if (captchaOpen.value) {
await loginCaptcha();
loginForm.validCode = '';
}
}
} catch (err: any) {
if (err?.errors) {
// 表单验证错误
return;
}
console.error('登录失败', err);
const errorMessage = err?.message || err?.response?.data?.message || err?.response?.data?.msg || '登录失败';
Message.error(errorMessage);
// 如果验证码开启,刷新验证码
if (captchaOpen.value) {
await loginCaptcha();
loginForm.validCode = '';
}
} finally {
loading.value = false;
}
};
// 取消重置按钮,保留方法以防引用
const resetForm = () => {};
// 确认选择模块
const handleModuleConfirm = async (moduleId: number) => {
try {
loading.value = true;
selectedModuleId.value = moduleId;
moduleSelectorVisible.value = false;
// 保存选择的模块到用户信息中
if (userStore.userInfo) {
userStore.setInfo({
...userStore.userInfo,
defaultModule: moduleId,
});
// 设置当前选择的模块(类似 SimpleAdmin)
userStore.setModule(moduleId);
}
// 初始化动态路由(获取授权菜单并添加路由)
const { initDynamicRouter } = await import('@/router/dynamicRouter');
const homePath = await initDynamicRouter();
// 跳转
const { redirect, ...othersQuery } = router.currentRoute.value.query;
// 构建跳转参数
const redirectParams: any = {
query: {
...othersQuery,
moduleId: moduleId.toString(),
},
};
// 判断 redirect 是路径还是路由名称
if (redirect) {
const redirectStr = redirect as string;
if (redirectStr.startsWith('/')) {
// 如果是路径,使用 path
redirectParams.path = redirectStr;
} else {
// 如果是路由名称,使用 name
redirectParams.name = redirectStr;
}
} else {
// 使用动态路由返回的首页路径
if (homePath.startsWith('/')) {
redirectParams.path = homePath;
} else {
redirectParams.name = homePath;
}
}
// 先显示成功消息,再跳转
Message.success('登录成功');
// 使用 nextTick 确保消息显示后再跳转,提升用户体验
await nextTick();
await router.push(redirectParams);
} catch (error) {
console.error('模块选择失败', error);
Message.error('模块选择失败');
} finally {
loading.value = false;
}
};
onMounted(() => {
// 监听 Enter 键
document.onkeydown = (e: KeyboardEvent) => {
if ((e.code === 'Enter' || e.code === 'NumpadEnter') && !loading.value) {
handleSubmit();
}
};
// 检查验证码策略
checkCaptchaPolicy();
// 如果有租户选项,默认选中第一个
if (props.tenantOption === 'CHOSE' && props.tenantOptions.length > 0 && !loginForm.tenantId) {
loginForm.tenantId = props.tenantOptions[0].id;
}
// 加载组织树
getOrgTree();
});
onBeforeUnmount(() => {
document.onkeydown = null;
});
</script>
<style lang="less" scoped>
:deep(.arco-form-item) {
margin-bottom: 24px;
// 完全隐藏 label-col 和所有星号相关元素
.arco-form-item-label-col {
display: none !important;
}
// 隐藏所有可能的星号元素
.arco-form-item-label-required,
.arco-form-item-label-asterisk,
.arco-form-item-label-required-symbol {
display: none !important;
}
.arco-input-wrapper {
border-radius: 8px;
transition: all 0.3s;
border-color: #d9d9d9;
background-color: #ffffff;
&:hover {
border-color: #2D8C8C;
}
&.arco-input-focus {
border-color: #2D8C8C;
box-shadow: 0 0 0 2px rgba(45, 140, 140, 0.15);
}
.arco-input {
font-size: 14px;
color: #262626;
&::placeholder {
color: #bfbfbf;
}
}
.arco-input-prefix {
color: #8c8c8c;
transition: color 0.3s;
}
&:hover .arco-input-prefix,
&.arco-input-focus .arco-input-prefix {
color: #2D8C8C;
}
}
.arco-select {
border-radius: 8px;
transition: all 0.3s;
border-color: #d9d9d9;
background-color: #ffffff;
&:hover {
border-color: #2D8C8C;
}
&.arco-select-focus {
border-color: #2D8C8C;
box-shadow: 0 0 0 2px rgba(45, 140, 140, 0.15);
}
.arco-select-view-value {
color: #262626;
}
.arco-select-view-placeholder {
color: #bfbfbf;
}
}
}
.captcha-container {
display: flex;
align-items: center;
gap: 8px;
.captcha-img-wrapper {
flex-shrink: 0;
.captcha-img {
height: 32px;
cursor: pointer;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
&:hover {
border-color: #2D8C8C;
}
}
}
}
.login-btn {
width: 100%;
margin-top: 30px;
.arco-btn {
width: 100%;
height: 48px;
line-height: 46px;
border-radius: 12px;
font-weight: 600;
transition: all 0.3s;
&.arco-btn-primary {
background: #1890ff;
border-color: #1890ff;
&:hover {
background: #40a9ff;
border-color: #40a9ff;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
}
}
}
}
</style>