From 33fc048aab1005a89737f3e0a8dbba991f194961 Mon Sep 17 00:00:00 2001 From: bunny <1319900154@qq.com> Date: Thu, 26 Sep 2024 16:58:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=9A=80=20=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=9C=AA=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- .env.development | 6 +- .env.production | 4 +- .prettierrc.js | 38 +++++++++-- build/server.ts | 4 +- src/api/service/index.ts | 10 +-- src/api/service/mockRequest.ts | 4 +- src/api/v1/system.ts | 2 +- src/api/v1/user.ts | 84 +++++++++++++----------- src/router/utils.ts | 3 +- src/store/modules/user.ts | 61 ++++++++++++------ src/types/common/BaseResult.ts | 6 ++ src/views/login/index.vue | 114 +++++++++++++++++++++++++++------ src/views/login/utils/rule.ts | 3 +- 14 files changed, 243 insertions(+), 98 deletions(-) create mode 100644 src/types/common/BaseResult.ts diff --git a/.env b/.env index 28a09df..713ddeb 100644 --- a/.env +++ b/.env @@ -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" diff --git a/.env.development b/.env.development index 28a09df..22b39b8 100644 --- a/.env.development +++ b/.env.development @@ -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 diff --git a/.env.production b/.env.production index 28a09df..7dd8fec 100644 --- a/.env.production +++ b/.env.production @@ -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 diff --git a/.prettierrc.js b/.prettierrc.js index 775d970..ff6d01d 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -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, + // 更改引用对象属性的时间 可选值"" + quoteProps: "as-needed", + // 在对象,数组括号与文字之间加空格 "{ foo: bar }" bracketSpacing: true, - singleQuote: false, + // 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"",默认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 结尾是 可选值"" + endOfLine: "auto", + // 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码 + rangeStart: 0, + rangeEnd: Infinity, + + vueIndentScriptAndStyle: false // Vue文件脚本和样式标签缩进 }; diff --git a/build/server.ts b/build/server.ts index f33e143..b184485 100644 --- a/build/server.ts +++ b/build/server.ts @@ -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, diff --git a/src/api/service/index.ts b/src/api/service/index.ts index d07cad6..16ad8b4 100644 --- a/src/api/service/index.ts +++ b/src/api/service/index.ts @@ -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; } ); } diff --git a/src/api/service/mockRequest.ts b/src/api/service/mockRequest.ts index 8eebe92..4ac382c 100644 --- a/src/api/service/mockRequest.ts +++ b/src/api/service/mockRequest.ts @@ -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)); diff --git a/src/api/v1/system.ts b/src/api/v1/system.ts index e4db238..26d25fa 100644 --- a/src/api/v1/system.ts +++ b/src/api/v1/system.ts @@ -1,4 +1,4 @@ -import { http } from "@/utils/http"; +import { http } from "@/api/service"; type Result = { success: boolean; diff --git a/src/api/v1/user.ts b/src/api/v1/user.ts index 3d044a9..a4a8831 100644 --- a/src/api/v1/user.ts +++ b/src/api/v1/user.ts @@ -1,45 +1,57 @@ -import { http } from "@/api/service/mockRequest"; +import { http } from "@/api/service"; +import type { BaseResult } from "@/types/common/BaseResult"; -export type UserResult = { - success: boolean; - data: { - /** 头像 */ - avatar: string; - /** 用户名 */ - username: string; - /** 昵称 */ - nickname: string; - /** 当前登录用户的角色 */ - roles: Array; - /** 按钮级别权限 */ - permissions: Array; - /** `token` */ - accessToken: string; - /** 用于调用刷新`accessToken`的接口时所需的`token` */ - refreshToken: string; - /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */ - expires: Date; - }; -}; +export interface UserResult { + /** 头像 */ + avatar: string; + /** 用户名 */ + username: string; + /** 昵称 */ + nickname: string; + /** 当前登录用户的角色 */ + roles: Array; + /** 按钮级别权限 */ + permissions: Array; + /** `token` */ + accessToken: string; + /** 用于调用刷新`accessToken`的接口时所需的`token` */ + refreshToken: string; + /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */ + expires: Date; +} -export type RefreshTokenResult = { - success: boolean; - data: { - /** `token` */ - accessToken: string; - /** 用于调用刷新`accessToken`的接口时所需的`token` */ - refreshToken: string; - /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */ - expires: Date; - }; -}; +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("post", "/login", { data }); +export const fetchLogin = (data?: object) => { + return http.request>("post", "/login", { data }); +}; + +/** + * * 发送邮件 + * @param data + */ +export const fetchPostEmailCode = (data: any) => { + return http.request>( + "post", + "/user/noAuth/sendLoginEmail", + { data }, + { headers: { "Content-Type": "multipart/form-data" } } + ); }; /** 刷新`token` */ export const refreshTokenApi = (data?: object) => { - return http.request("post", "/refresh-token", { data }); + return http.request>( + "post", + "/refresh-token", + { data } + ); }; diff --git a/src/router/utils.ts b/src/router/utils.ts index 435b0d3..bba08d5 100644 --- a/src/router/utils.ts +++ b/src/router/utils.ts @@ -361,10 +361,9 @@ function hasAuth(value: string | Array): 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) { diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index 7ed0ef1..0bc98ca 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -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>(userKey)?.avatar ?? "", @@ -65,33 +66,53 @@ export const useUserStore = defineStore({ this.loginDay = Number(value); }, /** 登入 */ - async loginByUsername(data) { - return new Promise((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((resolve, reject) => { refreshTokenApi(data) - .then(data => { + .then((data: any) => { if (data) { setToken(data.data); resolve(data); diff --git a/src/types/common/BaseResult.ts b/src/types/common/BaseResult.ts new file mode 100644 index 0000000..b8e4f79 --- /dev/null +++ b/src/types/common/BaseResult.ts @@ -0,0 +1,6 @@ +// 基础后端返回内容 +export interface BaseResult { + code: number; + data: T; + message: string; +} diff --git a/src/views/login/index.vue b/src/views/login/index.vue index c4f68b5..40ac046 100644 --- a/src/views/login/index.vue +++ b/src/views/login/index.vue @@ -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(); +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) { - // 获取后端路由 - return initRouter().then(() => { - router.push(getTopMenu(true).path).then(() => { - message(t("login.loginSuccess"), { type: "success" }); - }); - }); - } else { - message(t("login.loginFail"), { type: "error" }); - } - }) - .finally(() => (loading.value = false)); + const result = await userStore.loginByUsername(ruleForm); + + if (result) { + // 获取后端路由 + await initRouter(); + router.push(getTopMenu(true).path).then(() => { + message(t("login.loginSuccess"), { type: "success" }); + }); + } else { + message(t("login.loginFail"), { type: "error" }); + } + + loading.value = false; } }); }; @@ -178,6 +219,39 @@ onBeforeUnmount(() => { + + + + + + + + { }, trigger: "blur" } - ] + ], + emailCode: [{ required: true, trigger: "blur", type: "string" }] }); export { loginRules };