Skip to content

Commit

Permalink
feat: 登录模块
Browse files Browse the repository at this point in the history
  • Loading branch information
pany-ang committed Feb 18, 2025
1 parent f093329 commit 05f4fff
Show file tree
Hide file tree
Showing 19 changed files with 239 additions and 17 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@vant/touch-emulator": "1.4.0",
"axios": "1.7.9",
"dayjs": "1.11.13",
"js-cookie": "3.0.5",
"lodash-es": "4.17.21",
"normalize.css": "8.0.1",
"pinia": "3.0.1",
Expand All @@ -28,6 +29,7 @@
},
"devDependencies": {
"@antfu/eslint-config": "4.2.0",
"@types/js-cookie": "3.0.6",
"@types/lodash-es": "4.17.12",
"@types/node": "22.13.4",
"@types/nprogress": "0.2.3",
Expand Down
11 changes: 11 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
<script setup lang="ts">
import Layout from "@/layout/index.vue"
import { useUserStore } from "@/pinia/stores/user"
const userStore = useUserStore()
watch(
() => userStore.token,
(newVal) => {
newVal && userStore.getInfo()
},
{
immediate: true
}
)
</script>

<template>
Expand Down
Empty file removed src/common/apis/.gitkeep
Empty file.
10 changes: 10 additions & 0 deletions src/common/apis/users/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type * as Users from "./type"
import { request } from "@/http/axios"

/** 获取当前登录用户详情 */
export function getCurrentUserApi() {
return request<Users.CurrentUserResponseData>({
url: "users/me",
method: "get"
})
}
1 change: 1 addition & 0 deletions src/common/apis/users/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type CurrentUserResponseData = ApiResponseData<{ username: string, roles: string[] }>
Empty file removed src/common/constants/.gitkeep
Empty file.
6 changes: 6 additions & 0 deletions src/common/constants/cache-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const SYSTEM_NAME = "mobvue"

/** 缓存数据时用到的 Key */
export class CacheKey {
static readonly TOKEN = `${SYSTEM_NAME}-token-key`
}
16 changes: 16 additions & 0 deletions src/common/utils/cache/cookies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 统一处理 Cookie

import { CacheKey } from "@@/constants/cache-key"
import Cookies from "js-cookie"

export function getToken() {
return Cookies.get(CacheKey.TOKEN)
}

export function setToken(token: string) {
Cookies.set(CacheKey.TOKEN, token)
}

export function removeToken() {
Cookies.remove(CacheKey.TOKEN)
}
14 changes: 13 additions & 1 deletion src/http/axios.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import type { AxiosInstance, AxiosRequestConfig } from "axios"
import { useUserStore } from "@/pinia/stores/user"
import { getToken } from "@@/utils/cache/cookies"
import axios from "axios"
import { get, merge } from "lodash-es"

/** 退出登录并强制刷新页面(会重定向到登录页) */
function logout() {
useUserStore().resetToken()
location.reload()
}

/** 创建请求实例 */
function createInstance() {
// 创建一个 axios 实例命名为 instance
Expand Down Expand Up @@ -33,7 +41,7 @@ function createInstance() {
return apiData
case 401:
// 登录过期
return Promise.reject(new Error("登录过期"))
return logout()
default:
// 不是正确的 code
return Promise.reject(new Error(apiData.message || "Error"))
Expand All @@ -50,6 +58,7 @@ function createInstance() {
case 401:
// 登录过期
error.message = message || "登录过期"
logout()
break
case 403:
error.message = message || "拒绝访问"
Expand Down Expand Up @@ -88,12 +97,15 @@ function createInstance() {
/** 创建请求方法 */
function createRequest(instance: AxiosInstance) {
return <T>(config: AxiosRequestConfig): Promise<T> => {
const token = getToken()
// 默认配置
const defaultConfig: AxiosRequestConfig = {
// 接口地址
baseURL: import.meta.env.VITE_BASE_URL,
// 请求头
headers: {
// 携带 Token
"Authorization": token ? `Bearer ${token}` : undefined,
"Content-Type": "application/json"
},
// 请求体
Expand Down
19 changes: 19 additions & 0 deletions src/pages/login/apis/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type * as Auth from "./type"
import { request } from "@/http/axios"

/** 获取登录验证码 */
export function getCaptchaApi() {
return request<Auth.CaptchaResponseData>({
url: "auth/captcha",
method: "get"
})
}

/** 登录并返回 Token */
export function loginApi(data: Auth.LoginRequestData) {
return request<Auth.LoginResponseData>({
url: "auth/login",
method: "post",
data
})
}
10 changes: 10 additions & 0 deletions src/pages/login/apis/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface LoginRequestData {
/** admin 或 editor */
username: "admin" | "editor"
/** 密码 */
password: string
}

export type CaptchaResponseData = ApiResponseData<string>

export type LoginResponseData = ApiResponseData<{ token: string }>
63 changes: 63 additions & 0 deletions src/pages/login/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<script setup lang="ts">
import type { LoginRequestData } from "./apis/type"
import { useUserStore } from "@/pinia/stores/user"
import Description from "@@/components/Description.vue"
import { loginApi } from "./apis"
const router = useRouter()
const userStore = useUserStore()
const loading = ref(false)
const loginFormData: LoginRequestData = reactive({
username: "admin",
password: "12345678"
})
function onSubmit() {
loading.value = true
loginApi(loginFormData).then(({ data }) => {
userStore.setToken(data.token)
router.push("/")
}).catch(() => {
loginFormData.password = ""
}).finally(() => {
loading.value = false
})
}
</script>

<template>
<div un-h-full un-flex-center un-flex-col un-select-none>
<Description un-mb-20px />
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="loginFormData.username"
name="username"
label="用户名"
:rules="[{ required: true, message: '请填写用户名' }]"
/>
<van-field
v-model="loginFormData.password"
type="password"
name="password"
label="密码"
:rules="[{ required: true, message: '请填写密码' }]"
/>
</van-cell-group>
<div un-mx-16px un-my-24px>
<van-button
:loading="loading"
type="primary"
native-type="submit"
round
block
>
登 录
</van-button>
</div>
</van-form>
</div>
</template>
15 changes: 0 additions & 15 deletions src/pinia/stores/demo.ts

This file was deleted.

40 changes: 40 additions & 0 deletions src/pinia/stores/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { pinia } from "@/pinia"
import { getCurrentUserApi } from "@@/apis/users"
import { setToken as _setToken, getToken, removeToken } from "@@/utils/cache/cookies"

export const useUserStore = defineStore("user", () => {
const token = ref<string>(getToken() || "")

const roles = ref<string[]>([])

const username = ref<string>("")

// 设置 Token
const setToken = (value: string) => {
_setToken(value)
token.value = value
}

// 获取用户详情
const getInfo = async () => {
const { data } = await getCurrentUserApi()
username.value = data.username
}

// 重置 Token
const resetToken = () => {
removeToken()
token.value = ""
roles.value = []
}

return { token, roles, username, setToken, getInfo, resetToken }
})

/**
* @description 在 SPA 应用中可用于在 pinia 实例被激活前使用 store
* @description 在 SSR 应用中可用于在 setup 外使用 store
*/
export function useUserStoreOutside() {
return useUserStore(pinia)
}
14 changes: 13 additions & 1 deletion src/router/guard.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Router } from "vue-router"
import { isWhiteList } from "@/router/whitelist"
import { useTitle } from "@@/composables/useTitle"
import { getToken } from "@@/utils/cache/cookies"
import NProgress from "nprogress"

NProgress.configure({ showSpinner: false })
Expand All @@ -8,8 +10,18 @@ const { setTitle } = useTitle()

export function registerNavigationGuard(router: Router) {
// 全局前置守卫
router.beforeEach((_to, _from) => {
router.beforeEach((to, _from) => {
NProgress.start()
// 如果没有登录
if (!getToken()) {
// 如果在免登录的白名单中,则直接进入
if (isWhiteList(to)) return true
// 其他没有访问权限的页面将被重定向到登录页面
return "/login"
}
// 如果已经登录,并准备进入 Login 页面,则重定向到主页
if (to.path === "/login") return "/"
// 无限制访问
return true
})
// 全局后置钩子
Expand Down
8 changes: 8 additions & 0 deletions src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ const VITE_PUBLIC_PATH = import.meta.env.VITE_PUBLIC_PATH
const VITE_ROUTER_HISTORY = import.meta.env.VITE_ROUTER_HISTORY

export const routes: RouteRecordRaw[] = [
{
path: "/login",
component: () => import("@/pages/login/index.vue"),
name: "Login",
meta: {
title: "登录"
}
},
{
path: "/",
component: () => import("@/pages/home/index.vue"),
Expand Down
13 changes: 13 additions & 0 deletions src/router/whitelist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { RouteLocationNormalized, RouteRecordNameGeneric } from "vue-router"

/** 免登录白名单(匹配路由 path) */
const whiteListByPath: string[] = ["/login"]

/** 免登录白名单(匹配路由 name) */
const whiteListByName: RouteRecordNameGeneric[] = []

/** 判断是否在白名单 */
export function isWhiteList(to: RouteLocationNormalized) {
// path 和 name 任意一个匹配上即可
return whiteListByPath.includes(to.path) || whiteListByName.includes(to.name)
}
1 change: 1 addition & 0 deletions types/auto/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ declare module 'vue' {
VanCellGroup: typeof import('vant/es')['CellGroup']
VanDivider: typeof import('vant/es')['Divider']
VanField: typeof import('vant/es')['Field']
VanForm: typeof import('vant/es')['Form']
VanIcon: typeof import('vant/es')['Icon']
VanNavBar: typeof import('vant/es')['NavBar']
VanNoticeBar: typeof import('vant/es')['NoticeBar']
Expand Down

0 comments on commit 05f4fff

Please sign in to comment.