537 lines
14 KiB
Vue
537 lines
14 KiB
Vue
<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>
|