feat(login): 集成飞书登录功能及其相关界面和路由支持
Some checks failed
Lint Code / Lint Code (push) Failing after 5m15s

- 新增飞书登录API接口定义及请求方法
- 添加飞书登录相关的类型声明
- 本地多语言文件增加飞书登录文案(中英文)
- 登录页面新增飞书登录视图和样式,支持扫码或授权登录
- 添加飞书登录状态控制、回调处理逻辑,支持token和用户信息存储
- 路由白名单增加飞书登录回调路径,避免权限拦截
- 登录页新增切换账号密码登录和飞书登录的切换按钮
- Vite配置新增本地api代理规则,便于接口联调测试
This commit is contained in:
2026-03-27 17:49:32 +08:00
parent 2b62486364
commit bd809479e6
8 changed files with 340 additions and 5 deletions

View File

@@ -220,6 +220,7 @@ login:
pureAlipayLogin: Alipay Login
pureQQLogin: QQ Login
pureWeiBoLogin: Weibo Login
pureFeishuLogin: Feishu Login
purePhone: Phone
pureSmsVerifyCode: SMS VerifyCode
pureBack: Back

View File

@@ -220,6 +220,7 @@ login:
pureAlipayLogin: 支付宝登录
pureQQLogin: QQ登录
pureWeiBoLogin: 微博登录
pureFeishuLogin: 飞书登录
purePhone: 手机号码
pureSmsVerifyCode: 短信验证码
pureBack: 返回

33
src/api/feishu.ts Normal file
View File

@@ -0,0 +1,33 @@
import { http } from "@/utils/http";
export type FeishuLoginResult = {
code: number;
message: string;
data: {
/** 是否登录成功 */
isLogin: boolean;
/** 用户ID */
userId: string | number;
/** 用户名 */
username: string;
/** 真实姓名 */
realName: string;
/** 头像 */
avatar: string;
/** 手机号 */
phone: string;
/** 邮箱 */
email: string;
/** token */
token: string;
/** token名称 */
tokenName: string;
};
};
/** 飞书OAuth登录接口前端回调后调用 */
export const getFeishuLogin = (code: string) => {
return http.request<FeishuLoginResult>("post", "/api/v1/auth/feishu/login", {
params: { code }
});
};

View File

@@ -0,0 +1,81 @@
{
"openapi": "3.0.1",
"info": {
"title": "默认模块",
"description": "",
"version": "1.0.0"
},
"tags": [],
"paths": {
"/api/v1/auth/feishu/login": {
"post": {
"summary": "飞书OAuth登录接口前端回调后调用",
"deprecated": false,
"description": "前端从飞书回调中获取code然后调用此接口完成登录",
"tags": [],
"parameters": [
{
"name": "code",
"in": "query",
"description": "飞书授权码",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BaseResponseMapObject",
"description": "登录结果包含token和用户信息"
}
}
}
}
},
"security": []
}
}
},
"components": {
"schemas": {
"BaseResponseMapObject": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"description": ""
},
"data": {
"type": "object",
"properties": {
"isLogin": {
"type": "boolean"
},
"userId": {
"$ref": "#/components/schemas/userId"
}
},
"description": ""
},
"message": {
"description": "",
"type": "null"
}
}
},
"userId": {
"type": "object",
"properties": {}
}
},
"responses": {},
"securitySchemes": {}
},
"servers": [],
"security": []
}

View File

@@ -116,7 +116,7 @@ export function resetRouter() {
}
/** 路由白名单 */
const whiteList = ["/login"];
const whiteList = ["/login", "/auth/callback"];
const { VITE_HIDE_HOME } = import.meta.env;

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import Motion from "./utils/motion";
import { useRouter } from "vue-router";
import { useRouter, useRoute } from "vue-router";
import { message } from "@/utils/message";
import { loginRules } from "./utils/rule";
import TypeIt from "@/components/ReTypeit";
@@ -20,10 +20,13 @@ import { useUserStoreHook } from "@/store/modules/user";
import { initRouter, getTopMenu } from "@/router/utils";
import { bg, avatar, illustration } from "./utils/static";
import { ReImageVerify } from "@/components/ReImageVerify";
import { ref, toRaw, reactive, watch, computed } from "vue";
import { ref, toRaw, reactive, watch, computed, onMounted } from "vue";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { useTranslationLang } from "@/layout/hooks/useTranslationLang";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import { getFeishuLogin } from "@/api/feishu";
import { setToken, userKey } from "@/utils/auth";
import { storageLocal } from "@pureadmin/utils";
import dayIcon from "@/assets/svg/day.svg?component";
import darkIcon from "@/assets/svg/dark.svg?component";
@@ -38,9 +41,16 @@ defineOptions({
name: "Login"
});
// 飞书OAuth配置
const FEISHU_APP_ID = "cli_a94c8a7930badcd5";
const FEISHU_REDIRECT_URI = encodeURIComponent(
window.location.origin + "/login"
);
const imgCode = ref("");
const loginDay = ref(7);
const router = useRouter();
const route = useRoute();
const loading = ref(false);
const checked = ref(false);
const disabled = ref(false);
@@ -49,6 +59,12 @@ const currentPage = computed(() => {
return useUserStoreHook().currentPage;
});
// 飞书登录loading状态
const feishuLoading = ref(false);
// 是否显示飞书登录(作为默认登录方式)
const showFeishuLogin = ref(true);
const { t } = useI18n();
const { initStorage } = useLayout();
initStorage();
@@ -116,6 +132,91 @@ watch(checked, bool => {
watch(loginDay, value => {
useUserStoreHook().SET_LOGINDAY(value);
});
/** 跳转到飞书授权页面 */
const handleFeishuLogin = () => {
const feishuAuthUrl = `https://open.feishu.cn/open-apis/authen/v1/index?app_id=${FEISHU_APP_ID}&redirect_uri=${FEISHU_REDIRECT_URI}`;
window.location.href = feishuAuthUrl;
};
/** 处理飞书回调登录 */
const handleFeishuCallback = async (code: string) => {
feishuLoading.value = true;
try {
const res = await getFeishuLogin(code);
if (res.code === 0 && res.data?.isLogin) {
const { data } = res;
// 设置token适配后端返回的字段名
setToken({
accessToken: data.token,
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
refreshToken: data.token,
avatar: data.avatar,
username: data.username,
nickname: data.realName,
roles: ["admin"], // 根据实际业务调整
permissions: []
});
// 保存用户信息到本地存储
storageLocal().setItem(userKey, {
refreshToken: data.token,
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).getTime(),
avatar: data.avatar,
username: data.username,
nickname: data.realName,
roles: ["admin"],
permissions: []
});
// 更新全局状态
const userStore = useUserStoreHook();
userStore.SET_AVATAR(data.avatar);
userStore.SET_USERNAME(data.username);
userStore.SET_NICKNAME(data.realName);
userStore.SET_ROLES(["admin"]);
userStore.SET_PERMS([]);
// 初始化路由
await initRouter();
// 跳转到首页
router.push(getTopMenu(true).path).then(() => {
message(t("login.pureLoginSuccess"), { type: "success" });
});
} else {
message(res.message || "飞书登录失败", { type: "error" });
}
} catch (err) {
console.error("飞书登录失败:", err);
message("飞书登录失败", { type: "error" });
} finally {
feishuLoading.value = false;
}
};
/** 切换到账号密码登录 */
const switchToAccountLogin = () => {
showFeishuLogin.value = false;
};
/** 切换到飞书登录 */
const switchToFeishuLogin = () => {
showFeishuLogin.value = true;
};
// 页面加载时检查是否有飞书回调code
onMounted(() => {
// 从 URL 中解析 code 参数(处理 hash 路由模式下 query 参数在 hash 之前的情况)
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
if (code) {
handleFeishuCallback(code);
// 清除 URL 中的 code 参数,避免重复处理
const newUrl = window.location.pathname + window.location.hash;
window.history.replaceState({}, document.title, newUrl);
}
});
</script>
<template>
@@ -178,8 +279,48 @@ watch(loginDay, value => {
</h2>
</Motion>
<!-- 飞书登录 - 默认登录方式 -->
<div
v-if="currentPage === 0 && showFeishuLogin"
class="feishu-login-container"
>
<Motion :delay="100">
<div class="feishu-login-box">
<div class="feishu-icon">
<svg viewBox="0 0 1024 1024" width="64" height="64">
<path
fill="#3370FF"
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
/>
<path
fill="#3370FF"
d="M696 480H544V328c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v152H328c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h152v152c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V544h152c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z"
/>
</svg>
</div>
<h3 class="feishu-title">飞书登录</h3>
<p class="feishu-desc">使用飞书扫码或授权登录</p>
<el-button
class="w-full mt-4 feishu-login-btn"
size="large"
type="primary"
:loading="feishuLoading"
@click="handleFeishuLogin"
>
<IconifyIconOnline icon="ri:login-circle-line" class="mr-1" />
点击授权登录
</el-button>
<div class="login-switch">
<el-button link type="primary" @click="switchToAccountLogin">
使用账号密码登录
</el-button>
</div>
</div>
</Motion>
</div>
<el-form
v-if="currentPage === 0"
v-if="currentPage === 0 && !showFeishuLogin"
ref="ruleFormRef"
:model="ruleForm"
:rules="loginRules"
@@ -298,6 +439,17 @@ watch(loginDay, value => {
</div>
</el-form-item>
</Motion>
<!-- 切换回飞书登录 -->
<Motion :delay="350">
<el-form-item>
<div class="login-switch mt-4">
<el-button link type="primary" @click="switchToFeishuLogin">
使用飞书登录
</el-button>
</div>
</el-form-item>
</Motion>
</el-form>
<Motion v-if="currentPage === 0" :delay="350">
@@ -372,4 +524,61 @@ watch(loginDay, value => {
left: 20px;
}
}
/* 飞书登录样式 */
.feishu-login-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px 0;
}
.feishu-login-box {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.feishu-icon {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
margin-bottom: 20px;
background: linear-gradient(135deg, #3370ff20, #3370ff10);
border-radius: 20px;
}
.feishu-title {
margin: 0 0 8px;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.feishu-desc {
margin: 0 0 24px;
font-size: 14px;
color: var(--el-text-color-secondary);
}
.feishu-login-btn {
height: 44px;
font-size: 16px;
font-weight: 500;
background: linear-gradient(135deg, #3370ff, #2b5fd9);
border: none;
&:hover {
background: linear-gradient(135deg, #2860d9, #1f4fb3);
}
}
.login-switch {
margin-top: 20px;
text-align: center;
}
</style>

View File

@@ -13,6 +13,10 @@ const operates = [
];
const thirdParty = [
{
title: $t("login.pureFeishuLogin"),
icon: "feishu"
},
{
title: $t("login.pureWeChatLogin"),
icon: "wechat"

View File

@@ -24,7 +24,13 @@ export default async ({ mode }: ConfigEnv): Promise<UserConfigExport> => {
port: VITE_PORT,
host: "0.0.0.0",
// 本地跨域代理 https://cn.vitejs.dev/config/server-options.html#server-proxy
proxy: {},
proxy: {
"/api": {
target: "http://localhost:8080/api",
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, "")
}
},
// 预热文件以提前转换和缓存结果,降低启动期间的初始页面加载时长并防止转换瀑布
warmup: {
clientFiles: ["./index.html", "./src/{views,components}/*"]