Files
smt-used-car-pc/src/views/auth/login/index.vue
T
2026-03-12 16:48:21 +08:00

537 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>
<div class="login-page">
<div class="login-container">
<!-- 左侧展示区域 -->
<div class="login-left">
<div class="logo-section">
<h1 style="color: #fff">USED CAR</h1>
<p>smartcar</p>
</div>
</div>
<!-- 右侧登录表单 -->
<div class="login-right">
<div class="login-form-wrapper">
<!-- 登录方式切换 -->
<div class="login-type-tabs">
<span
:class="{ active: activeLoginType === 'password' }"
@click="switchLoginType('password')"
>账号密码登录</span>
<span
:class="{ active: activeLoginType === 'qrcode' }"
@click="switchLoginType('qrcode')"
>飞书登录</span>
</div>
<!-- 账号密码登录表单 -->
<div v-if="activeLoginType === 'password'" class="password-login">
<el-form
ref="formRef"
:model="formData"
:rules="rules"
@keyup.enter="handlePasswordLogin"
>
<el-form-item prop="username">
<el-input
v-model="formData.username"
placeholder="请输入账号"
prefix-icon="User"
size="large"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="formData.password"
type="password"
placeholder="请输入密码"
prefix-icon="Lock"
size="large"
show-password
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
style="width: 100%"
:loading="loading"
@click="handlePasswordLogin"
>
登录
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 飞书扫码登录 -->
<div v-else class="qrcode-login">
<el-form :model="formData">
<el-form-item prop="company">
<el-select
v-model="formData.company"
placeholder="请选择公司"
size="large"
style="width: 100%"
@change="handleCompanyChange"
>
<el-option
v-for="item in companyList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
<div v-if="!isInFeishu && formData.company" class="qrcode-container">
<div id="feishu_qr_login_container"></div>
<p class="qrcode-tip">请使用飞书App扫码登录</p>
</div>
<div v-if="isInFeishu && formData.company" class="feishu-auto-login">
<p>正在跳转飞书授权...</p>
</div>
<div v-if="!formData.company" class="qrcode-placeholder">
<p>请先选择公司</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { UserService } from '@/api/usersApi'
import { useUserStore } from '@/store/modules/user'
import AppConfig from '@/config'
defineOptions({ name: 'Login' })
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
// 系统名称
const systemName = AppConfig.systemInfo.name || '二手车管理系统'
// 登录方式
const activeLoginType = ref<'password' | 'qrcode'>('password')
// 是否在飞书环境中
const isInFeishu = /Lark|Feishu/i.test(navigator.userAgent)
// 表单引用
const formRef = ref<FormInstance>()
// 加载状态
const loading = ref(false)
// 公司列表
const companyList = ref<any[]>([])
// 飞书配置
const feishuConfig = ref<any>(null)
// 表单数据
const formData = reactive({
username: '',
password: '',
company: undefined as number | undefined
})
// 表单验证规则
const rules = computed<FormRules>(() => ({
username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
// 公司选择仅在飞书登录时必填,账号密码登录时可选
company: activeLoginType.value === 'qrcode'
? [{ required: true, message: '请选择公司', trigger: 'change' }]
: []
}))
// 角色设置辅助函数
const getRoles = (role: number, role_wp: number | null) => {
const roles: string[] = []
// 基础角色
if (role == 1) {
roles.push('business')
} else if (role == 2) {
roles.push('procurement')
}
// 根据 role_wp 添加额外角色
if (role_wp == 1) {
roles.push('businesspro')
} else if (role_wp == 2) {
roles.push('supplier')
} else if (role_wp == 3) {
roles.push('adminpro')
}
return roles
}
// 切换登录方式
const switchLoginType = (type: 'password' | 'qrcode') => {
activeLoginType.value = type
// 保存登录类型到 userStore
if (type === 'qrcode') {
userStore.setLoginType('feishu')
} else {
userStore.setLoginType('upwd')
}
if (type === 'qrcode' && formData.company) {
initFeishuLogin()
}
}
// 获取公司列表
const getCompanyList = async () => {
try {
const response = await UserService.getCompanyList()
companyList.value = response || []
} catch (error) {
console.error('获取公司列表失败:', error)
}
}
// 公司变更处理
const handleCompanyChange = (companyId: number) => {
const company = companyList.value.find(c => c.id === companyId)
if (company) {
userStore.setCompany(String(companyId), company.abbr || '')
// 构建飞书配置,确保 r_url 存在
const defaultRUrl = 'http://web-usedcar.dev.hnxinyouhe.com/auth/feishu-callback'
feishuConfig.value = {
...company,
r_url: company.r_url || defaultRUrl,
state: company.state || 'xinyouhe'
}
// 保存 app_id 供回调页面使用
if (company.app_id) {
localStorage.setItem('app_id', company.app_id)
}
}
if (activeLoginType.value === 'qrcode' && companyId) {
initFeishuLogin()
}
}
// 初始化飞书登录
const initFeishuLogin = async () => {
if (!formData.company || !feishuConfig.value) return
// 飞书配置已在 handleCompanyChange 中从公司列表获取
if (isInFeishu) {
// 飞书内环境,自动跳转授权
await handleFeishuLogin()
} else {
// 普通浏览器环境,显示二维码
await nextTick()
await loadFeishuQRCode()
}
}
// 加载飞书二维码
const loadFeishuQRCode = async () => {
// 等待飞书SDK加载
const waitForSDK = () =>
new Promise<void>((resolve) => {
const check = () => {
// @ts-ignore
if (window.QRLogin) resolve()
else setTimeout(check, 100)
}
check()
})
try {
await waitForSDK()
const container = document.getElementById('feishu_qr_login_container')
if (!container) {
ElMessage.error('未找到二维码容器')
return
}
container.innerHTML = ''
const { app_id, r_url, state } = feishuConfig.value || {}
if (!app_id || !r_url || !state) {
ElMessage.error('飞书配置不完整,请重新选择公司')
return
}
// URL编码,确保redirect_uri正确传递
const encodedRedirectUri = encodeURIComponent(r_url)
const gotoUrl = `https://passport.feishu.cn/suite/passport/oauth/authorize?client_id=${app_id}&redirect_uri=${encodedRedirectUri}&response_type=code&state=${state}`
// @ts-ignore
const QRLoginObj = window.QRLogin({
id: 'feishu_qr_login_container',
goto: gotoUrl,
width: '250',
height: '250',
style: 'width:280px;height:280px;border:none'
})
// 监听扫码消息
const handleMessage = (event: MessageEvent) => {
// @ts-ignore
if (
QRLoginObj.matchOrigin &&
QRLoginObj.matchData &&
QRLoginObj.matchOrigin(event.origin) &&
QRLoginObj.matchData(event.data)
) {
const loginTmpCode = event.data.tmp_code
// 使用编码后的跳转URL
window.location.href = `${gotoUrl}&tmp_code=${loginTmpCode}`
}
}
window.addEventListener('message', handleMessage, false)
} catch {
ElMessage.error('加载飞书二维码失败')
}
}
// 飞书登录(飞书内环境)
const handleFeishuLogin = async () => {
if (!formData.company) {
ElMessage.warning('请先选择公司')
return
}
try {
const { auth_url } = await UserService.getfeishuscope({
app_id: feishuConfig.value?.app_id,
is_pc: 1
})
window.location.href = auth_url
} catch (error) {
ElMessage.error('获取飞书授权失败')
}
}
// 账号密码登录
const handlePasswordLogin = async () => {
if (!formRef.value) return
try {
const valid = await formRef.value.validate()
if (!valid) return
loading.value = true
const { data } = await UserService.login({
account: formData.username,
password: formData.password
})
// 保存token(union_id为token)
if (data.union_id) {
userStore.setToken(data.union_id)
}
// 设置角色(支持多角色数组)
const roles = getRoles(data.role, data.role_wp)
localStorage.setItem('role', JSON.stringify(roles))
// 保存用户信息
userStore.setUserInfo(data)
userStore.setLoginStatus(true)
ElMessage.success('登录成功')
// 跳转首页
const redirect = (route.query.redirect as string) || '/'
// router.push(redirect)
router.push(data.home)
} catch (error: any) {
ElMessage.error(error?.message || '登录失败,请检查账号密码')
} finally {
loading.value = false
}
}
// 监听登录方式切换
watch(activeLoginType, (newType) => {
if (newType === 'qrcode' && formData.company) {
initFeishuLogin()
}
})
onMounted(() => {
getCompanyList()
// 设置默认登录类型为账号密码登录
userStore.setLoginType('upwd')
// 检查URL参数中的公司ID
const companyId = route.query.corp_id as string
if (companyId) {
formData.company = Number(companyId)
handleCompanyChange(Number(companyId))
}
})
</script>
<style lang="scss" scoped>
.login-page {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-container {
width: 900px;
height: 550px;
background: #fff;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
display: flex;
overflow: hidden;
}
.login-left {
flex: 1;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #fff;
padding: 40px;
.logo-section {
text-align: center;
h1 {
font-size: 32px;
margin-bottom: 12px;
font-weight: 600;
}
p {
font-size: 16px;
opacity: 0.9;
}
}
}
.login-right {
flex: 1;
padding: 40px;
display: flex;
flex-direction: column;
justify-content: center;
}
.login-form-wrapper {
width: 100%;
max-width: 360px;
margin: 0 auto;
}
.login-type-tabs {
display: flex;
margin-bottom: 32px;
border-bottom: 2px solid #eee;
span {
flex: 1;
text-align: center;
padding: 12px 0;
cursor: pointer;
font-size: 16px;
color: #666;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: all 0.3s;
&.active {
color: #667eea;
border-bottom-color: #667eea;
font-weight: 500;
}
&:hover {
color: #667eea;
}
}
}
.password-login,
.qrcode-login {
:deep(.el-form-item) {
margin-bottom: 24px;
}
:deep(.el-input__wrapper) {
border-radius: 8px;
}
:deep(.el-button) {
border-radius: 8px;
height: 44px;
font-size: 16px;
}
}
.qrcode-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 0;
#feishu_qr_login_container {
margin-bottom: 16px;
}
.qrcode-tip {
color: #999;
font-size: 14px;
}
}
.feishu-btn-login {
padding: 20px 0;
}
.feishu-auto-login {
display: flex;
align-items: center;
justify-content: center;
height: 280px;
color: #667eea;
font-size: 16px;
}
.qrcode-placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 280px;
background: #f5f7fa;
border-radius: 8px;
color: #999;
}
</style>