feat: add login system backend
This commit is contained in:
28
src/apis/system/oauth.ts
Normal file
28
src/apis/system/oauth.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { httpWithCustomReturn } from "@/utils/http";
|
||||
|
||||
export const loginByEmailPasswordWithOauthApi = ({ email, password }) => {
|
||||
return httpWithCustomReturn<{
|
||||
code: number;
|
||||
msg: string;
|
||||
data: {
|
||||
scope: string;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
};
|
||||
}>({
|
||||
url: "/admin-api/system/oauth2/token",
|
||||
method: "POST",
|
||||
params: {
|
||||
grant_type: "password",
|
||||
username: email,
|
||||
password,
|
||||
client_id: "rc",
|
||||
client_secret: "rc",
|
||||
},
|
||||
headers: {
|
||||
"tenant-id": 1,
|
||||
},
|
||||
});
|
||||
};
|
||||
26
src/apis/system/user.ts
Normal file
26
src/apis/system/user.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { httpWithCustomReturn } from "@/utils/http";
|
||||
|
||||
export const getUserInfoApi = () => {
|
||||
return httpWithCustomReturn<{
|
||||
code: number;
|
||||
msg: string;
|
||||
data: {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
mobile: string;
|
||||
sex: number;
|
||||
avatar: string;
|
||||
loginIp: string;
|
||||
loginDate: null;
|
||||
createTime: number;
|
||||
roles: object[];
|
||||
dept: null;
|
||||
posts: null;
|
||||
};
|
||||
}>({
|
||||
url: "/admin-api/system/user/profile/get",
|
||||
method: "GET",
|
||||
});
|
||||
};
|
||||
17
src/apis/temp/auth-qrcode.ts
Normal file
17
src/apis/temp/auth-qrcode.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { httpWithCustomReturn } from "@/utils/http";
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns 直接返回图片二进制数据
|
||||
*/
|
||||
export const getAuthQrcodeApi = () => {
|
||||
return httpWithCustomReturn<Blob>({
|
||||
url: "https://api.qrtool.cn",
|
||||
method: "get",
|
||||
responseType: "blob",
|
||||
params: {
|
||||
text: "测试",
|
||||
size: 600,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import http from "../utils/http";
|
||||
|
||||
/**
|
||||
* This is an example, please remove if not needed
|
||||
*/
|
||||
export const getUserList = () => {
|
||||
return http({
|
||||
url: "/user/list",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
export const insertUser = (data: { account: string; passowrd: string }) => {
|
||||
return http({
|
||||
url: "/user/insert",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { renderAuthDialog } from "@/hooks/dialog/renderAuthDialog";
|
||||
import { useUserStore } from "@/stores/UserStore";
|
||||
|
||||
// 用户 store
|
||||
const userStore = useUserStore();
|
||||
|
||||
const dropdownVisible = ref(true);
|
||||
|
||||
@@ -9,19 +13,39 @@ const closeDropdown = () => {
|
||||
dropdownVisible.value = true;
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
userStore.clearUserInfo();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown dropdown-hover dropdown-end">
|
||||
<div
|
||||
tabindex="0"
|
||||
v-if="!userStore.checkUserLogin()"
|
||||
role="button"
|
||||
class="btn btn-circle btn-sm btn-ghost px-3 w-16"
|
||||
@click="
|
||||
renderAuthDialog('login');
|
||||
closeDropdown();
|
||||
"
|
||||
>
|
||||
<span class="text-sm text-base-content/70">{{ $t("nav.login") }}</span>
|
||||
<span class="text-sm text-base-content/70">
|
||||
{{ $t("nav.login") }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
role="button"
|
||||
class="text-sm text-base-content/70 px-3 cursor-pointer"
|
||||
>
|
||||
{{ userStore.userInfo?.nickname || userStore.userInfo?.username }}
|
||||
</div>
|
||||
|
||||
<!-- 未登录用户下拉框 -->
|
||||
<ul
|
||||
v-show="dropdownVisible"
|
||||
v-if="dropdownVisible && !userStore.checkUserLogin()"
|
||||
tabindex="-1"
|
||||
class="dropdown-content z-1 w-100 p-2"
|
||||
>
|
||||
@@ -53,6 +77,25 @@ const closeDropdown = () => {
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<!-- 已登录用户下拉框 -->
|
||||
<ul
|
||||
v-if="dropdownVisible && userStore.checkUserLogin()"
|
||||
tabindex="-1"
|
||||
class="dropdown-content z-1 w-100 p-2"
|
||||
>
|
||||
<div class="card bg-base-200 w-96 mt-4 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">退出登录</h2>
|
||||
<p>切换您的账号</p>
|
||||
<div class="card-actions justify-end">
|
||||
<button class="btn btn-primary btn-sm" @click="handleLogout">
|
||||
{{ $t("nav.logout") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<!-- 登录弹窗 -->
|
||||
<dialog id="my_modal" class="modal">
|
||||
<div class="modal-box">
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { loginByEmailPasswordWithOauthApi } from "@/apis/system/oauth";
|
||||
import { getUserInfoApi } from "@/apis/system/user";
|
||||
import { getAuthQrcodeApi } from "@/apis/temp/auth-qrcode";
|
||||
import { closeAuthDialog } from "@/hooks/dialog/renderAuthDialog";
|
||||
import { useUserStore } from "@/stores/UserStore";
|
||||
import { AnimatePresence, motion } from "motion-v";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
@@ -12,10 +16,12 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const authFormType = ref<"login" | "register">(props.defaultAuthFormType);
|
||||
const authFormType = ref<"login" | "register" | "transition">(
|
||||
props.defaultAuthFormType,
|
||||
);
|
||||
|
||||
const changeAuthFormType = (type: "login" | "register") => {
|
||||
authFormType.value = "";
|
||||
authFormType.value = "transition";
|
||||
setTimeout(() => {
|
||||
authFormType.value = type;
|
||||
}, 300);
|
||||
@@ -28,6 +34,49 @@ const closeAuthDialogWaitAnim = () => {
|
||||
closeAuthDialog();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// 登录方式
|
||||
const loginType = ref<"password" | "phone">("password");
|
||||
|
||||
// 登录二维码
|
||||
const authQrcode = ref("");
|
||||
|
||||
onMounted(async () => {
|
||||
const qrImg = await getAuthQrcodeApi();
|
||||
authQrcode.value = URL.createObjectURL(qrImg);
|
||||
});
|
||||
|
||||
// 控制登录表单宽度
|
||||
const loginFormWidth = computed(() => {
|
||||
return authFormType.value === "login" ? "900px" : "550px";
|
||||
});
|
||||
|
||||
// 登录表单数据绑定
|
||||
const loginForm = reactive({
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
// 登录按钮点击事件
|
||||
const clickLoginButtonEvent = async () => {
|
||||
const { email, password } = loginForm;
|
||||
if (!email || !password) {
|
||||
return;
|
||||
}
|
||||
const res = await loginByEmailPasswordWithOauthApi({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
const store = useUserStore();
|
||||
store.setAccessToken(res.data.access_token);
|
||||
store.setRefreshToken(res.data.refresh_token);
|
||||
// 进一步获取用户信息
|
||||
const userInfoRes = await getUserInfoApi();
|
||||
store.setUserInfo(userInfoRes.data);
|
||||
closeAuthDialogWaitAnim();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -38,7 +87,7 @@ const closeAuthDialogWaitAnim = () => {
|
||||
v-show="isNotClose"
|
||||
class="wrapper"
|
||||
:initial="{ opacity: 0, scale: 0 }"
|
||||
:animate="{ opacity: 1, scale: 1 }"
|
||||
:animate="{ opacity: 1, scale: 1, width: loginFormWidth }"
|
||||
:transition="{
|
||||
duration: 0.4,
|
||||
scale: { type: 'spring', visualDuration: 0.4, bounce: 0.5 },
|
||||
@@ -77,50 +126,106 @@ const closeAuthDialogWaitAnim = () => {
|
||||
opacity: 0,
|
||||
}"
|
||||
>
|
||||
<h2>
|
||||
{{ t("auth.login") }}
|
||||
</h2>
|
||||
<form action="#">
|
||||
<div class="input-box">
|
||||
<span class="icon">
|
||||
<span class="icon-[material-symbols--mail-outline]" />
|
||||
</span>
|
||||
<input type="text" required />
|
||||
<label>{{ t("auth.email") }}</label>
|
||||
</div>
|
||||
<div class="input-box">
|
||||
<span class="icon">
|
||||
<span class="icon-[material-symbols--lock-outline]" />
|
||||
</span>
|
||||
<input type="password" required />
|
||||
<label>{{ t("auth.password") }}</label>
|
||||
<div class="flex items-center gap-16">
|
||||
<!-- 二维码登录区域 -->
|
||||
<div class="flex flex-col items-center gap-4 w-[200px]">
|
||||
<div class="text-2xl select-none">扫描二维码登录</div>
|
||||
<img
|
||||
v-if="authQrcode"
|
||||
:src="authQrcode"
|
||||
alt="登录二维码"
|
||||
class="h-48 w-48"
|
||||
/>
|
||||
<div v-if="!authQrcode" class="skeleton h-48 w-48" />
|
||||
<div class="text-sm text-gray-500 select-none">
|
||||
请打开手机微信 APP 扫码登录
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="remember-forgot">
|
||||
<label class="label">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" />
|
||||
{{ t("auth.remember_me") }}
|
||||
</label>
|
||||
<a class="label" href="#">{{ t("auth.forget_password") }}</a>
|
||||
<div class="h-48 w-[1px] bg-gray-300/70" />
|
||||
|
||||
<!-- 账号登录区域 -->
|
||||
<div>
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<div
|
||||
class="text-2xl cursor-pointer transition duration-300"
|
||||
:class="{ 'text-gray-400': loginType === 'phone' }"
|
||||
:transition="{
|
||||
duration: 0.3,
|
||||
}"
|
||||
@click="loginType = 'password'"
|
||||
>
|
||||
{{ t("auth.login_password") }}
|
||||
</div>
|
||||
<div class="h-8 w-[1px] bg-gray-300/70" />
|
||||
|
||||
<!-- 注册暂未开放 -->
|
||||
<div class="tooltip">
|
||||
<div class="tooltip-content">
|
||||
<div class="font-black">暂未开放</div>
|
||||
</div>
|
||||
<div
|
||||
class="text-2xl cursor-pointer transition duration-300"
|
||||
:class="{ 'text-gray-400': loginType === 'password' }"
|
||||
:transition="{
|
||||
duration: 0.3,
|
||||
}"
|
||||
>
|
||||
{{ t("auth.login_phone") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<form action="#">
|
||||
<div class="input-box">
|
||||
<span class="icon">
|
||||
<span class="icon-[material-symbols--mail-outline]" />
|
||||
</span>
|
||||
<input v-model="loginForm.email" type="text" required />
|
||||
<label>{{ t("auth.email") }}</label>
|
||||
</div>
|
||||
<div class="input-box">
|
||||
<span class="icon">
|
||||
<span class="icon-[material-symbols--lock-outline]" />
|
||||
</span>
|
||||
<input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
required
|
||||
/>
|
||||
<label>{{ t("auth.password") }}</label>
|
||||
</div>
|
||||
|
||||
<div class="remember-forgot">
|
||||
<label class="label">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" />
|
||||
{{ t("auth.remember_me") }}
|
||||
</label>
|
||||
<a class="label" href="#">{{
|
||||
t("auth.forget_password")
|
||||
}}</a>
|
||||
</div>
|
||||
<button
|
||||
class="btn bg-primary text-primary-content"
|
||||
@click="clickLoginButtonEvent"
|
||||
>
|
||||
{{ t("auth.login") }}
|
||||
</button>
|
||||
<div
|
||||
class="login-register"
|
||||
@click="changeAuthFormType('register')"
|
||||
>
|
||||
<p>
|
||||
{{ t("auth.no_account") }}
|
||||
<a class="register-link cursor-pointer">
|
||||
{{ t("auth.register") }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn bg-primary text-primary-content"
|
||||
>
|
||||
{{ t("auth.login") }}
|
||||
</button>
|
||||
<div
|
||||
class="login-register"
|
||||
@click="changeAuthFormType('register')"
|
||||
>
|
||||
<p>
|
||||
{{ t("auth.no_account") }}
|
||||
<a class="register-link cursor-pointer">
|
||||
{{ t("auth.register") }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -146,7 +251,11 @@ const closeAuthDialogWaitAnim = () => {
|
||||
opacity: 0,
|
||||
}"
|
||||
>
|
||||
<h2>{{ t("auth.register") }}</h2>
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<div class="text-2xl cursor-pointer">
|
||||
{{ t("auth.register") }}
|
||||
</div>
|
||||
</div>
|
||||
<form action="#">
|
||||
<div class="input-box">
|
||||
<span class="icon">
|
||||
@@ -197,19 +306,9 @@ const closeAuthDialogWaitAnim = () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800;900&display=swap");
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
height: 430px;
|
||||
position: relative;
|
||||
width: 400px;
|
||||
height: 440px;
|
||||
background: transparent;
|
||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||
border-radius: 20px;
|
||||
@@ -222,8 +321,7 @@ const closeAuthDialogWaitAnim = () => {
|
||||
}
|
||||
|
||||
.wrapper .form-box {
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
padding: 40px 80px;
|
||||
}
|
||||
|
||||
.wrapper .icon-close {
|
||||
@@ -241,14 +339,9 @@ const closeAuthDialogWaitAnim = () => {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.form-box h2 {
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
width: 350px;
|
||||
height: 50px;
|
||||
border-bottom: 2px solid;
|
||||
margin: 30px 0;
|
||||
@@ -314,8 +407,6 @@ const closeAuthDialogWaitAnim = () => {
|
||||
outline: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-register {
|
||||
|
||||
@@ -8,9 +8,12 @@ export const en_USMessages: messagesInterface = {
|
||||
locale: "Locale",
|
||||
login: "Login",
|
||||
register: "Register",
|
||||
logout: "Logout",
|
||||
},
|
||||
auth: {
|
||||
login: "Login",
|
||||
login_password: "Password Login",
|
||||
login_phone: "Phone Login",
|
||||
register: "Register",
|
||||
email: "Email",
|
||||
password: "Password",
|
||||
@@ -19,5 +22,6 @@ export const en_USMessages: messagesInterface = {
|
||||
no_account: "Don't have an account?",
|
||||
agree_terms: "Agree to the Terms & Conditions",
|
||||
have_account: "Already have an account?",
|
||||
logout: "Logout",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,9 +10,12 @@ export interface messagesInterface {
|
||||
locale: string;
|
||||
login: string;
|
||||
register: string;
|
||||
logout: string;
|
||||
};
|
||||
auth: {
|
||||
login: string;
|
||||
login_password: string;
|
||||
login_phone: string;
|
||||
register: string;
|
||||
email: string;
|
||||
password: string;
|
||||
@@ -21,6 +24,7 @@ export interface messagesInterface {
|
||||
no_account: string;
|
||||
agree_terms: string;
|
||||
have_account: string;
|
||||
logout: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,16 +8,20 @@ export const zh_CNMessages: messagesInterface = {
|
||||
locale: "语言",
|
||||
login: "登录",
|
||||
register: "注册",
|
||||
logout: "退出登录",
|
||||
},
|
||||
auth: {
|
||||
login: "登录",
|
||||
login_password: "密码登录",
|
||||
login_phone: "短信登录",
|
||||
register: "注册",
|
||||
email: "邮箱",
|
||||
password: "密码",
|
||||
remember_me: "记住我",
|
||||
forget_password: "忘记密码",
|
||||
no_account: "没有账号?",
|
||||
agree_terms: "同意条款和条件",
|
||||
agree_terms: "我同意《用户协议》相关条款和条件",
|
||||
have_account: "已经有账号?",
|
||||
logout: "退出登录",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,18 +1,89 @@
|
||||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export interface UserInfo {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
mobile: string;
|
||||
sex: number;
|
||||
avatar: string;
|
||||
loginIp: string;
|
||||
loginDate: null;
|
||||
createTime: number;
|
||||
roles: object[];
|
||||
dept: null;
|
||||
posts: null;
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore(
|
||||
"user",
|
||||
() => {
|
||||
const token = ref("");
|
||||
const accessToken = ref("");
|
||||
const refreshToken = ref("");
|
||||
|
||||
const setToken = (newToken: string) => {
|
||||
token.value = newToken;
|
||||
const setAccessToken = (newToken: string) => {
|
||||
accessToken.value = newToken;
|
||||
};
|
||||
|
||||
const setRefreshToken = (newToken: string) => {
|
||||
refreshToken.value = newToken;
|
||||
};
|
||||
|
||||
const userInfo = ref<UserInfo>({
|
||||
id: 0,
|
||||
username: "",
|
||||
nickname: "",
|
||||
email: "",
|
||||
mobile: "",
|
||||
sex: 0,
|
||||
avatar: "",
|
||||
loginIp: "",
|
||||
loginDate: null,
|
||||
createTime: 0,
|
||||
roles: [],
|
||||
dept: null,
|
||||
posts: null,
|
||||
});
|
||||
|
||||
const setUserInfo = (newInfo: UserInfo) => {
|
||||
userInfo.value = newInfo;
|
||||
};
|
||||
|
||||
const checkUserLogin = () => {
|
||||
return accessToken.value !== "";
|
||||
};
|
||||
|
||||
const clearUserInfo = () => {
|
||||
accessToken.value = "";
|
||||
refreshToken.value = "";
|
||||
userInfo.value = {
|
||||
id: 0,
|
||||
username: "",
|
||||
nickname: "",
|
||||
email: "",
|
||||
mobile: "",
|
||||
sex: 0,
|
||||
avatar: "",
|
||||
loginIp: "",
|
||||
loginDate: null,
|
||||
createTime: 0,
|
||||
roles: [],
|
||||
dept: null,
|
||||
posts: null,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
token,
|
||||
setToken,
|
||||
accessToken,
|
||||
setAccessToken,
|
||||
refreshToken,
|
||||
setRefreshToken,
|
||||
userInfo,
|
||||
setUserInfo,
|
||||
checkUserLogin,
|
||||
clearUserInfo,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
themes: all;
|
||||
};
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: "light";
|
||||
--color-primary: blue;
|
||||
--color-secondary: teal;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar Styles */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
|
||||
@@ -5,22 +5,30 @@ const instance = axios.create({
|
||||
baseURL: import.meta.env.VITE_SERVER,
|
||||
});
|
||||
|
||||
// 所有请求自动加入 token
|
||||
instance.interceptors.request.use((config) => {
|
||||
const store = useUserStore();
|
||||
if (!store.token) {
|
||||
if (!store.accessToken) {
|
||||
return config;
|
||||
}
|
||||
const token = store.token;
|
||||
const token = store.accessToken;
|
||||
if (!token) {
|
||||
return config;
|
||||
}
|
||||
config.headers["token"] = token;
|
||||
config.headers["Authorization"] = `Bearer ${token}`;
|
||||
return config;
|
||||
});
|
||||
|
||||
const http = async <T>(config: AxiosRequestConfig): Promise<Result<T>> => {
|
||||
export const http = async <T>(
|
||||
config: AxiosRequestConfig,
|
||||
): Promise<Result<T>> => {
|
||||
const { data } = await instance.request<Result<T>>(config);
|
||||
return data;
|
||||
};
|
||||
|
||||
export default http;
|
||||
export const httpWithCustomReturn = async <T>(
|
||||
config: AxiosRequestConfig,
|
||||
): Promise<T> => {
|
||||
const { data } = await instance.request<T>(config);
|
||||
return data;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user