Files
smt-used-car-pc/src/views/auth/login/index.vue
T

537 lines
14 KiB
Vue
Raw Normal View History

2026-03-11 11:16:35 +08:00
<template>
<div class="login-page">
<div class="login-container">
<!-- 左侧展示区域 -->
<div class="login-left">
<div class="logo-section">
2026-03-12 16:48:21 +08:00
<h1 style="color: #fff">USED CAR</h1>
<p>smartcar</p>
2026-03-11 11:16:35 +08:00
</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) => {
2026-03-12 16:48:21 +08:00
const roles: string[] = []
2026-03-11 11:16:35 +08:00
// 基础角色
2026-03-12 16:48:21 +08:00
if (role == 1) {
roles.push('business')
} else if (role == 2) {
roles.push('procurement')
}
2026-03-11 11:16:35 +08:00
// 根据 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>