532 lines
14 KiB
Vue
532 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>二手车管理系统</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 baseRole = role == 1 ? 'business' : role == 2 ? 'procurement' : 'alls'
|
|||
|
|
const roles = [baseRole]
|
|||
|
|
|
|||
|
|
// 根据 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>
|