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参数"
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参数"
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地址
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参数"
VITE_ROUTER_HISTORY="hash"
@ -8,7 +8,7 @@ VITE_ROUTER_HISTORY="hash"
VITE_BASE_API=/api
# 跨域代理地址
VITE_APP_URL=http://localhost:8801
VITE_APP_URL=http://localhost:7070
# 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 {
// 超过最大值换行
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,
singleQuote: false,
// 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"<none|es5|all>"默认none
trailingComma: "all",
// 在JSX中使用单引号而不是双引号
jsxSingleQuote: true,
// (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid省略括号 ,always不省略括号
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,
cors: true,
proxy: {
"/api": {
"/admin": {
target: VITE_APP_URL,
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/api/, "/api")
rewrite: (path: string) => path.replace(/^\/admin/, "/admin")
},
"/mock": {
target: VITE_APP_URL,

View File

@ -10,9 +10,10 @@ import type {
RequestMethods
} from "./types";
import { stringify } from "qs";
import NProgress from "../../utils/progress";
import NProgress from "@/utils/progress";
import { formatToken, getToken } from "@/utils/auth";
import { useUserStoreHook } from "@/store/modules/user";
import { message } from "@/utils/message";
// 相关配置请参考www.axios-js.com/zh-cn/docs/#axios-request-config-1
const defaultConfig: AxiosRequestConfig = {
@ -55,7 +56,7 @@ class PureHttp {
private static retryOriginalRequest(config: PureHttpRequestConfig) {
return new Promise(resolve => {
PureHttp.requests.push((token: string) => {
config.headers["Authorization"] = formatToken(token);
config.headers["token"] = formatToken(token);
resolve(config);
});
});
@ -136,7 +137,7 @@ class PureHttp {
// token过期刷新
useUserStoreHook()
.handRefreshToken({ refreshToken: data.refreshToken })
.then(res => {
.then((res: any) => {
const token = res.data.accessToken;
config.headers["Authorization"] = formatToken(token);
PureHttp.requests.forEach(cb => cb(token));
@ -188,8 +189,9 @@ class PureHttp {
$error.isCancelRequest = Axios.isCancel($error);
// 关闭进度条动画
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) {
return new Promise(resolve => {
PureHttp.requests.push((token: string) => {
config.headers["Authorization"] = formatToken(token);
config.headers["token"] = formatToken(token);
resolve(config);
});
});
@ -129,7 +129,7 @@ class PureHttp {
// token过期刷新
useUserStoreHook()
.handRefreshToken({ refreshToken: data.refreshToken })
.then(res => {
.then((res: any) => {
const token = res.data.accessToken;
config.headers["Authorization"] = formatToken(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 = {
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 = {
success: boolean;
data: {
export interface UserResult {
/** 头像 */
avatar: string;
/** 用户名 */
@ -19,27 +18,40 @@ export type UserResult = {
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
};
};
}
export type RefreshTokenResult = {
success: boolean;
data: {
export interface RefreshTokenResult {
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
};
};
}
/** 登录 */
export const getLogin = (data?: object) => {
return http.request<UserResult>("post", "/login", { data });
export const fetchLogin = (data?: object) => {
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` */
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`值 */
const metaAuths = getAuths();
if (!metaAuths) return false;
const isAuths = isString(value)
return isString(value)
? metaAuths.includes(value)
: isIncludeAllChildren(value, metaAuths);
return isAuths ? true : false;
}
function handleTopMenu(route) {

View File

@ -8,16 +8,17 @@ import {
type userType
} from "../utils";
import {
getLogin,
fetchLogin,
fetchPostEmailCode,
refreshTokenApi,
type RefreshTokenResult,
type UserResult
type RefreshTokenResult
} from "@/api/v1/user";
import { useMultiTagsStoreHook } from "./multiTags";
import { type DataInfo, removeToken, setToken, userKey } from "@/utils/auth";
import { message } from "@/utils/message";
export const useUserStore = defineStore({
id: "pure-user",
id: "system-user",
state: (): userType => ({
// 头像
avatar: storageLocal().getItem<DataInfo<number>>(userKey)?.avatar ?? "",
@ -65,33 +66,53 @@ export const useUserStore = defineStore({
this.loginDay = Number(value);
},
/** 登入 */
async loginByUsername(data) {
return new Promise<UserResult>((resolve, reject) => {
getLogin(data)
.then(data => {
if (data?.success) setToken(data.data);
resolve(data);
})
.catch(error => {
reject(error);
});
});
async loginByUsername(data: any) {
const result = await fetchLogin(data);
if (result.code === 200) {
setToken(data.data);
return true;
}
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.roles = [];
this.permissions = [];
removeToken();
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
resetRouter();
router.push("/login");
await router.push("/login");
},
/** 刷新`token` */
/**
* `token`
*/
async handRefreshToken(data) {
return new Promise<RefreshTokenResult>((resolve, reject) => {
refreshTokenApi(data)
.then(data => {
.then((data: any) => {
if (data) {
setToken(data.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 { $t } from "@/plugins/i18n";
import { useLayout } from "@/layout/hooks/useLayout";
import { useUserStoreHook } from "@/store/modules/user";
import { getTopMenu, initRouter } from "@/router/utils";
import { useUserStore } from "@/store/modules/user";
import { avatar, bg, illustration } from "./utils/static";
import { useRenderIcon } from "@/components/CommonIcon/src/hooks";
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 Check from "@iconify-icons/ep/check";
import User from "@iconify-icons/ri/user-3-fill";
import { getTopMenu, initRouter } from "@/router/utils";
defineOptions({
name: "Login"
@ -29,6 +29,8 @@ defineOptions({
const router = useRouter();
const loading = ref(false);
const ruleFormRef = ref<FormInstance>();
const sendSecond = ref(60);
const timer = ref(null);
const { initStorage } = useLayout();
initStorage();
@ -38,32 +40,71 @@ const { dataTheme, overallStyle, dataThemeChange } = useDataThemeChange();
dataThemeChange(overallStyle.value);
const { title, getDropdownItemStyle, getDropdownItemClass } = useNav();
const { locale, translationCh, translationEn } = useTranslationLang();
const userStore = useUserStore();
const ruleForm = reactive({
username: "admin",
password: "admin123"
username: "1319900154@qq.com",
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) => {
if (!formEl) return;
await formEl.validate((valid, fields) => {
await formEl.validate(async valid => {
if (valid) {
loading.value = true;
useUserStoreHook()
.loginByUsername({ username: ruleForm.username, password: "admin123" })
.then(res => {
if (res.success) {
const result = await userStore.loginByUsername(ruleForm);
if (result) {
//
return initRouter().then(() => {
await initRouter();
router.push(getTopMenu(true).path).then(() => {
message(t("login.loginSuccess"), { type: "success" });
});
});
} else {
message(t("login.loginFail"), { type: "error" });
}
})
.finally(() => (loading.value = false));
loading.value = false;
}
});
};
@ -178,6 +219,39 @@ onBeforeUnmount(() => {
</el-form-item>
</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">
<el-button
:loading="loading"

View File

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