feat(login): 集成飞书登录功能及其相关界面和路由支持
Some checks failed
Lint Code / Lint Code (push) Failing after 5m15s
Some checks failed
Lint Code / Lint Code (push) Failing after 5m15s
- 新增飞书登录API接口定义及请求方法 - 添加飞书登录相关的类型声明 - 本地多语言文件增加飞书登录文案(中英文) - 登录页面新增飞书登录视图和样式,支持扫码或授权登录 - 添加飞书登录状态控制、回调处理逻辑,支持token和用户信息存储 - 路由白名单增加飞书登录回调路径,避免权限拦截 - 登录页新增切换账号密码登录和飞书登录的切换按钮 - Vite配置新增本地api代理规则,便于接口联调测试
This commit is contained in:
@@ -220,6 +220,7 @@ login:
|
||||
pureAlipayLogin: Alipay Login
|
||||
pureQQLogin: QQ Login
|
||||
pureWeiBoLogin: Weibo Login
|
||||
pureFeishuLogin: Feishu Login
|
||||
purePhone: Phone
|
||||
pureSmsVerifyCode: SMS VerifyCode
|
||||
pureBack: Back
|
||||
|
||||
@@ -220,6 +220,7 @@ login:
|
||||
pureAlipayLogin: 支付宝登录
|
||||
pureQQLogin: QQ登录
|
||||
pureWeiBoLogin: 微博登录
|
||||
pureFeishuLogin: 飞书登录
|
||||
purePhone: 手机号码
|
||||
pureSmsVerifyCode: 短信验证码
|
||||
pureBack: 返回
|
||||
|
||||
33
src/api/feishu.ts
Normal file
33
src/api/feishu.ts
Normal 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 }
|
||||
});
|
||||
};
|
||||
81
src/api/默认模块.openapi.json
Normal file
81
src/api/默认模块.openapi.json
Normal 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": []
|
||||
}
|
||||
@@ -116,7 +116,7 @@ export function resetRouter() {
|
||||
}
|
||||
|
||||
/** 路由白名单 */
|
||||
const whiteList = ["/login"];
|
||||
const whiteList = ["/login", "/auth/callback"];
|
||||
|
||||
const { VITE_HIDE_HOME } = import.meta.env;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -13,6 +13,10 @@ const operates = [
|
||||
];
|
||||
|
||||
const thirdParty = [
|
||||
{
|
||||
title: $t("login.pureFeishuLogin"),
|
||||
icon: "feishu"
|
||||
},
|
||||
{
|
||||
title: $t("login.pureWeChatLogin"),
|
||||
icon: "wechat"
|
||||
|
||||
@@ -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}/*"]
|
||||
|
||||
Reference in New Issue
Block a user