feat: 🚀 用户登录未完成

This commit is contained in:
bunny 2024-09-26 16:58:51 +08:00
parent 25cd96e6d9
commit 33fc048aab
14 changed files with 243 additions and 98 deletions

2
.env
View File

@ -1,5 +1,5 @@
# 平台本地运行端口号 # 平台本地运行端口号
VITE_PORT=8201 VITE_PORT=7000
# 预发布环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数" # 预发布环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY="hash" VITE_ROUTER_HISTORY="hash"

View File

@ -1,14 +1,14 @@
# 平台本地运行端口号 # 平台本地运行端口号
VITE_PORT=8201 VITE_PORT=7000
# 预发布环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数" # 预发布环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY="hash" VITE_ROUTER_HISTORY="hash"
# 基础请求路径 # 基础请求路径
VITE_BASE_API=/api VITE_BASE_API=/admin
# 跨域代理地址 # 跨域代理地址
VITE_APP_URL=http://localhost:8801 VITE_APP_URL=http://localhost:7070
# mock地址 # mock地址
VITE_MOCK_BASE_API=/mock VITE_MOCK_BASE_API=/mock

View File

@ -1,5 +1,5 @@
# 平台本地运行端口号 # 平台本地运行端口号
VITE_PORT=8201 VITE_PORT=7000
# 预发布环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数" # 预发布环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY="hash" VITE_ROUTER_HISTORY="hash"
@ -8,7 +8,7 @@ VITE_ROUTER_HISTORY="hash"
VITE_BASE_API=/api VITE_BASE_API=/api
# 跨域代理地址 # 跨域代理地址
VITE_APP_URL=http://localhost:8801 VITE_APP_URL=http://localhost:7070
# mock地址 # mock地址
VITE_MOCK_BASE_API=/mock VITE_MOCK_BASE_API=/mock

View File

@ -1,9 +1,39 @@
// @ts-check // @see: https://www.prettier.cn
/** @type {import("prettier").Config} */
export default { export default {
// 超过最大值换行
printWidth: 200,
// 缩进字节数
tabWidth: 1,
// 使用制表符而不是空格缩进行
useTabs: true,
// 结尾不用分号(true有false没有)
semi: true,
// 使用单引号(true单引号false双引号)
singleQuote: true,
// 更改引用对象属性的时间 可选值"<as-needed|consistent|preserve>"
quoteProps: "as-needed",
// 在对象,数组括号与文字之间加空格 "{ foo: bar }"
bracketSpacing: true, bracketSpacing: true,
singleQuote: false, // 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"<none|es5|all>"默认none
trailingComma: "all",
// 在JSX中使用单引号而不是双引号
jsxSingleQuote: true,
// (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid省略括号 ,always不省略括号
arrowParens: "avoid", arrowParens: "avoid",
trailingComma: "none" // 如果文件顶部已经有一个 doclock这个选项将新建一行注释并打上@format标记。
insertPragma: false,
// 指定要使用的解析器,不需要写文件开头的 @prettier
requirePragma: false,
// 默认值。因为使用了一些折行敏感型的渲染器如GitHub comment而按照markdown文本样式进行折行
proseWrap: "preserve",
// 在html中空格是否是敏感的 "css" - 遵守CSS显示属性的默认值 "strict" - 空格被认为是敏感的 "ignore" - 空格被认为是不敏感的
htmlWhitespaceSensitivity: "css",
// 换行符使用 lf 结尾是 可选值"<auto|lf|crlf|cr>"
endOfLine: "auto",
// 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码
rangeStart: 0,
rangeEnd: Infinity,
vueIndentScriptAndStyle: false // Vue文件脚本和样式标签缩进
}; };

View File

@ -10,10 +10,10 @@ export const serverOptions = (mode: string) => {
open: true, open: true,
cors: true, cors: true,
proxy: { proxy: {
"/api": { "/admin": {
target: VITE_APP_URL, target: VITE_APP_URL,
changeOrigin: true, changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/api/, "/api") rewrite: (path: string) => path.replace(/^\/admin/, "/admin")
}, },
"/mock": { "/mock": {
target: VITE_APP_URL, target: VITE_APP_URL,

View File

@ -10,9 +10,10 @@ import type {
RequestMethods RequestMethods
} from "./types"; } from "./types";
import { stringify } from "qs"; import { stringify } from "qs";
import NProgress from "../../utils/progress"; import NProgress from "@/utils/progress";
import { formatToken, getToken } from "@/utils/auth"; import { formatToken, getToken } from "@/utils/auth";
import { useUserStoreHook } from "@/store/modules/user"; import { useUserStoreHook } from "@/store/modules/user";
import { message } from "@/utils/message";
// 相关配置请参考www.axios-js.com/zh-cn/docs/#axios-request-config-1 // 相关配置请参考www.axios-js.com/zh-cn/docs/#axios-request-config-1
const defaultConfig: AxiosRequestConfig = { const defaultConfig: AxiosRequestConfig = {
@ -55,7 +56,7 @@ class PureHttp {
private static retryOriginalRequest(config: PureHttpRequestConfig) { private static retryOriginalRequest(config: PureHttpRequestConfig) {
return new Promise(resolve => { return new Promise(resolve => {
PureHttp.requests.push((token: string) => { PureHttp.requests.push((token: string) => {
config.headers["Authorization"] = formatToken(token); config.headers["token"] = formatToken(token);
resolve(config); resolve(config);
}); });
}); });
@ -136,7 +137,7 @@ class PureHttp {
// token过期刷新 // token过期刷新
useUserStoreHook() useUserStoreHook()
.handRefreshToken({ refreshToken: data.refreshToken }) .handRefreshToken({ refreshToken: data.refreshToken })
.then(res => { .then((res: any) => {
const token = res.data.accessToken; const token = res.data.accessToken;
config.headers["Authorization"] = formatToken(token); config.headers["Authorization"] = formatToken(token);
PureHttp.requests.forEach(cb => cb(token)); PureHttp.requests.forEach(cb => cb(token));
@ -188,8 +189,9 @@ class PureHttp {
$error.isCancelRequest = Axios.isCancel($error); $error.isCancelRequest = Axios.isCancel($error);
// 关闭进度条动画 // 关闭进度条动画
NProgress.done(); NProgress.done();
message(error.message, { type: "error" });
// 所有的响应异常 区分来源为取消请求/非取消请求 // 所有的响应异常 区分来源为取消请求/非取消请求
return Promise.reject($error); return $error;
} }
); );
} }

View File

@ -48,7 +48,7 @@ class PureHttp {
private static retryOriginalRequest(config: PureHttpRequestConfig) { private static retryOriginalRequest(config: PureHttpRequestConfig) {
return new Promise(resolve => { return new Promise(resolve => {
PureHttp.requests.push((token: string) => { PureHttp.requests.push((token: string) => {
config.headers["Authorization"] = formatToken(token); config.headers["token"] = formatToken(token);
resolve(config); resolve(config);
}); });
}); });
@ -129,7 +129,7 @@ class PureHttp {
// token过期刷新 // token过期刷新
useUserStoreHook() useUserStoreHook()
.handRefreshToken({ refreshToken: data.refreshToken }) .handRefreshToken({ refreshToken: data.refreshToken })
.then(res => { .then((res: any) => {
const token = res.data.accessToken; const token = res.data.accessToken;
config.headers["Authorization"] = formatToken(token); config.headers["Authorization"] = formatToken(token);
PureHttp.requests.forEach(cb => cb(token)); PureHttp.requests.forEach(cb => cb(token));

View File

@ -1,4 +1,4 @@
import { http } from "@/utils/http"; import { http } from "@/api/service";
type Result = { type Result = {
success: boolean; success: boolean;

View File

@ -1,8 +1,7 @@
import { http } from "@/api/service/mockRequest"; import { http } from "@/api/service";
import type { BaseResult } from "@/types/common/BaseResult";
export type UserResult = { export interface UserResult {
success: boolean;
data: {
/** 头像 */ /** 头像 */
avatar: string; avatar: string;
/** 用户名 */ /** 用户名 */
@ -19,27 +18,40 @@ export type UserResult = {
refreshToken: string; refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */ /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date; expires: Date;
}; }
};
export type RefreshTokenResult = { export interface RefreshTokenResult {
success: boolean;
data: {
/** `token` */ /** `token` */
accessToken: string; accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */ /** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string; refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */ /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date; expires: Date;
}; }
};
/** 登录 */ /** 登录 */
export const getLogin = (data?: object) => { export const fetchLogin = (data?: object) => {
return http.request<UserResult>("post", "/login", { data }); return http.request<BaseResult<UserResult>>("post", "/login", { data });
};
/**
* *
* @param data
*/
export const fetchPostEmailCode = (data: any) => {
return http.request<BaseResult<any>>(
"post",
"/user/noAuth/sendLoginEmail",
{ data },
{ headers: { "Content-Type": "multipart/form-data" } }
);
}; };
/** 刷新`token` */ /** 刷新`token` */
export const refreshTokenApi = (data?: object) => { export const refreshTokenApi = (data?: object) => {
return http.request<RefreshTokenResult>("post", "/refresh-token", { data }); return http.request<BaseResult<RefreshTokenResult>>(
"post",
"/refresh-token",
{ data }
);
}; };

View File

@ -361,10 +361,9 @@ function hasAuth(value: string | Array<string>): boolean {
/** 从当前路由的`meta`字段里获取按钮级别的所有自定义`code`值 */ /** 从当前路由的`meta`字段里获取按钮级别的所有自定义`code`值 */
const metaAuths = getAuths(); const metaAuths = getAuths();
if (!metaAuths) return false; if (!metaAuths) return false;
const isAuths = isString(value) return isString(value)
? metaAuths.includes(value) ? metaAuths.includes(value)
: isIncludeAllChildren(value, metaAuths); : isIncludeAllChildren(value, metaAuths);
return isAuths ? true : false;
} }
function handleTopMenu(route) { function handleTopMenu(route) {

View File

@ -8,16 +8,17 @@ import {
type userType type userType
} from "../utils"; } from "../utils";
import { import {
getLogin, fetchLogin,
fetchPostEmailCode,
refreshTokenApi, refreshTokenApi,
type RefreshTokenResult, type RefreshTokenResult
type UserResult
} from "@/api/v1/user"; } from "@/api/v1/user";
import { useMultiTagsStoreHook } from "./multiTags"; import { useMultiTagsStoreHook } from "./multiTags";
import { type DataInfo, removeToken, setToken, userKey } from "@/utils/auth"; import { type DataInfo, removeToken, setToken, userKey } from "@/utils/auth";
import { message } from "@/utils/message";
export const useUserStore = defineStore({ export const useUserStore = defineStore({
id: "pure-user", id: "system-user",
state: (): userType => ({ state: (): userType => ({
// 头像 // 头像
avatar: storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? "", avatar: storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? "",
@ -65,33 +66,53 @@ export const useUserStore = defineStore({
this.loginDay = Number(value); this.loginDay = Number(value);
}, },
/** 登入 */ /** 登入 */
async loginByUsername(data) { async loginByUsername(data: any) {
return new Promise<UserResult>((resolve, reject) => { const result = await fetchLogin(data);
getLogin(data)
.then(data => { if (result.code === 200) {
if (data?.success) setToken(data.data); setToken(data.data);
resolve(data); return true;
}) }
.catch(error => {
reject(error); message(result.message, { type: "error" });
}); return false;
});
}, },
/** 前端登出(不调用接口) */
logOut() { /**
* *
* @param email
*/
async postEmailCode(email: string) {
const response = await fetchPostEmailCode({ email });
if (response.code === 200) {
message(response.message, { type: "success" });
return true;
}
message(response.message, { type: "error" });
return false;
},
/**
*
*/
async logOut() {
this.username = ""; this.username = "";
this.roles = []; this.roles = [];
this.permissions = []; this.permissions = [];
removeToken(); removeToken();
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]); useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
resetRouter(); resetRouter();
router.push("/login"); await router.push("/login");
}, },
/** 刷新`token` */
/**
* `token`
*/
async handRefreshToken(data) { async handRefreshToken(data) {
return new Promise<RefreshTokenResult>((resolve, reject) => { return new Promise<RefreshTokenResult>((resolve, reject) => {
refreshTokenApi(data) refreshTokenApi(data)
.then(data => { .then((data: any) => {
if (data) { if (data) {
setToken(data.data); setToken(data.data);
resolve(data); resolve(data);

View File

@ -0,0 +1,6 @@
// 基础后端返回内容
export interface BaseResult<T> {
code: number;
data: T;
message: string;
}

View File

@ -8,8 +8,7 @@ import { useNav } from "@/layout/hooks/useNav";
import type { FormInstance } from "element-plus"; import type { FormInstance } from "element-plus";
import { $t } from "@/plugins/i18n"; import { $t } from "@/plugins/i18n";
import { useLayout } from "@/layout/hooks/useLayout"; import { useLayout } from "@/layout/hooks/useLayout";
import { useUserStoreHook } from "@/store/modules/user"; import { useUserStore } from "@/store/modules/user";
import { getTopMenu, initRouter } from "@/router/utils";
import { avatar, bg, illustration } from "./utils/static"; import { avatar, bg, illustration } from "./utils/static";
import { useRenderIcon } from "@/components/CommonIcon/src/hooks"; import { useRenderIcon } from "@/components/CommonIcon/src/hooks";
import { onBeforeUnmount, onMounted, reactive, ref, toRaw } from "vue"; import { onBeforeUnmount, onMounted, reactive, ref, toRaw } from "vue";
@ -22,6 +21,7 @@ import globalization from "@/assets/svg/globalization.svg?component";
import Lock from "@iconify-icons/ri/lock-fill"; import Lock from "@iconify-icons/ri/lock-fill";
import Check from "@iconify-icons/ep/check"; import Check from "@iconify-icons/ep/check";
import User from "@iconify-icons/ri/user-3-fill"; import User from "@iconify-icons/ri/user-3-fill";
import { getTopMenu, initRouter } from "@/router/utils";
defineOptions({ defineOptions({
name: "Login" name: "Login"
@ -29,6 +29,8 @@ defineOptions({
const router = useRouter(); const router = useRouter();
const loading = ref(false); const loading = ref(false);
const ruleFormRef = ref<FormInstance>(); const ruleFormRef = ref<FormInstance>();
const sendSecond = ref(60);
const timer = ref(null);
const { initStorage } = useLayout(); const { initStorage } = useLayout();
initStorage(); initStorage();
@ -38,32 +40,71 @@ const { dataTheme, overallStyle, dataThemeChange } = useDataThemeChange();
dataThemeChange(overallStyle.value); dataThemeChange(overallStyle.value);
const { title, getDropdownItemStyle, getDropdownItemClass } = useNav(); const { title, getDropdownItemStyle, getDropdownItemClass } = useNav();
const { locale, translationCh, translationEn } = useTranslationLang(); const { locale, translationCh, translationEn } = useTranslationLang();
const userStore = useUserStore();
const ruleForm = reactive({ const ruleForm = reactive({
username: "admin", username: "1319900154@qq.com",
password: "admin123" password: "admin123",
emailCode: ""
}); });
/**
* * 发送邮箱验证码
*/
const onSendEmailCode = async () => {
//
if (ruleForm.username === "" || ruleForm.username === void 0) {
message("请先填写邮箱地址", { type: "warning" });
return false;
}
const result = await userStore.postEmailCode(ruleForm.username);
if (result) {
//
onSendEmailTimer();
}
};
/**
* * 发送邮箱倒计时点击
*/
const onSendEmailTimer = () => {
//
timer.value = setInterval(() => {
// 0
if (sendSecond.value <= 0) {
clearInterval(timer.value);
timer.value = null;
sendSecond.value = 60;
return;
}
//
sendSecond.value--;
}, 1000);
};
/**
* 登录
* @param formEl
*/
const onLogin = async (formEl: FormInstance | undefined) => { const onLogin = async (formEl: FormInstance | undefined) => {
if (!formEl) return; if (!formEl) return;
await formEl.validate((valid, fields) => { await formEl.validate(async valid => {
if (valid) { if (valid) {
loading.value = true; loading.value = true;
useUserStoreHook() const result = await userStore.loginByUsername(ruleForm);
.loginByUsername({ username: ruleForm.username, password: "admin123" })
.then(res => { if (result) {
if (res.success) {
// //
return initRouter().then(() => { await initRouter();
router.push(getTopMenu(true).path).then(() => { router.push(getTopMenu(true).path).then(() => {
message(t("login.loginSuccess"), { type: "success" }); message(t("login.loginSuccess"), { type: "success" });
}); });
});
} else { } else {
message(t("login.loginFail"), { type: "error" }); message(t("login.loginFail"), { type: "error" });
} }
})
.finally(() => (loading.value = false)); loading.value = false;
} }
}); });
}; };
@ -178,6 +219,39 @@ onBeforeUnmount(() => {
</el-form-item> </el-form-item>
</Motion> </Motion>
<Motion :delay="150">
<el-form-item prop="emailCode">
<el-input
v-model="ruleForm.emailCode"
:placeholder="t('login.emailCode')"
:prefix-icon="useRenderIcon('ic:outline-email')"
clearable
@keydown.enter="onLogin(ruleFormRef)"
>
<template v-slot:append>
<el-link
v-if="sendSecond === 60"
:underline="false"
class="px-2"
type="primary"
@click="onSendEmailCode"
>
{{ t("login.getEmailCode") }}
</el-link>
<el-link
v-else
:underline="false"
class="px-2"
type="primary"
>
{{ sendSecond }}
{{ t("login.getCodeInfo") }}
</el-link>
</template>
</el-input>
</el-form-item>
</Motion>
<Motion :delay="250"> <Motion :delay="250">
<el-button <el-button
:loading="loading" :loading="loading"

View File

@ -21,7 +21,8 @@ const loginRules = reactive(<FormRules>{
}, },
trigger: "blur" trigger: "blur"
} }
] ],
emailCode: [{ required: true, trigger: "blur", type: "string" }]
}); });
export { loginRules }; export { loginRules };