feat: ✨ 添加登录和获取用户信息接口,整合pinia实现用户状态全局共享
This commit is contained in:
44
src/api/auth.ts
Normal file
44
src/api/auth.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
class AuthAPI {
|
||||
/**
|
||||
* 登录接口
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param password 密码
|
||||
* @returns 返回 token
|
||||
*/
|
||||
static login(username: string, password: string): Promise<LoginResult> {
|
||||
return request<LoginResult>({
|
||||
url: "/api/v1/auth/login",
|
||||
method: "POST",
|
||||
data: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
header: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出接口
|
||||
*/
|
||||
static logout(): Promise<void> {
|
||||
return request<void>({
|
||||
url: "/api/v1/auth/logout",
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthAPI;
|
||||
|
||||
/** 登录响应 */
|
||||
export interface LoginResult {
|
||||
/** 访问token */
|
||||
accessToken?: string;
|
||||
/** token 类型 */
|
||||
tokenType?: string;
|
||||
}
|
||||
39
src/api/user.ts
Normal file
39
src/api/user.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
const USER_BASE_URL = "/api/v1/users";
|
||||
|
||||
class UserAPI {
|
||||
/**
|
||||
* 获取当前登录用户信息
|
||||
*
|
||||
* @returns 登录用户昵称、头像信息,包括角色和权限
|
||||
*/
|
||||
static getUserInfo(): Promise<UserInfo> {
|
||||
return request<UserInfo>({
|
||||
url: `${USER_BASE_URL}/me`,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
}
|
||||
export default UserAPI;
|
||||
|
||||
/** 登录用户信息 */
|
||||
export interface UserInfo {
|
||||
/** 用户ID */
|
||||
userId?: number;
|
||||
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
|
||||
/** 昵称 */
|
||||
nickname?: string;
|
||||
|
||||
/** 头像URL */
|
||||
avatar?: string;
|
||||
|
||||
/** 角色 */
|
||||
roles: string[];
|
||||
|
||||
/** 权限 */
|
||||
perms: string[];
|
||||
}
|
||||
@@ -2,8 +2,13 @@ import { createSSRApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import "uno.css";
|
||||
|
||||
import { setupStore } from "@/store";
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App);
|
||||
|
||||
setupStore(app);
|
||||
|
||||
return {
|
||||
app,
|
||||
};
|
||||
|
||||
@@ -13,10 +13,16 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/index",
|
||||
"path": "pages/profile/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
@@ -43,7 +49,7 @@
|
||||
"selectedIconPath": "static/tabbar/workbench-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/my/index",
|
||||
"pagePath": "pages/profile/index",
|
||||
"text": "我的",
|
||||
"iconPath": "static/tabbar/my.png",
|
||||
"selectedIconPath": "static/tabbar/my-active.png"
|
||||
|
||||
40
src/pages/login/index.vue
Normal file
40
src/pages/login/index.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<view class="flex-col items-center">
|
||||
<input v-model="username" placeholder="请输入用户名" />
|
||||
<input v-model="password" placeholder="请输入密码" type="password" />
|
||||
<button class="mt-5" @click="handleLogin">登录</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
|
||||
// 登录表单
|
||||
const username = ref("admin");
|
||||
const password = ref("123456");
|
||||
|
||||
// 使用 pinia
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 登录处理
|
||||
const handleLogin = async () => {
|
||||
await userStore.login(username.value, password.value);
|
||||
|
||||
if (!!userStore.token) {
|
||||
await userStore.getUserInfo(); // 登录成功后获取用户信息
|
||||
uni.showToast({ title: "登录成功", icon: "success" });
|
||||
uni.navigateBack(); // 登录成功后返回上一页
|
||||
} else {
|
||||
uni.showToast({ title: "登录失败", icon: "none" });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
input {
|
||||
width: 80%;
|
||||
padding: 10px;
|
||||
margin-top: 16px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<view class="flex-center flex-col">
|
||||
<text class="text-blue font-bold text-lg">我的</text>
|
||||
</view>
|
||||
</template>
|
||||
39
src/pages/profile/index.vue
Normal file
39
src/pages/profile/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<view class="flex-center flex-col">
|
||||
<text class="text-blue font-bold text-lg">我的</text>
|
||||
|
||||
<!-- 判断是否已登录 -->
|
||||
<template v-if="isLoggedIn">
|
||||
<image :src="userInfo?.avatar" class="w100 h100 mb-5 rounded-full" />
|
||||
<text class="text-lg font-bold">{{ userInfo?.nickname }}</text>
|
||||
<button @click="handleLogout" class="mt-5">退出登录</button>
|
||||
</template>
|
||||
|
||||
<!-- 未登录时显示去登录按钮 -->
|
||||
<template v-else>
|
||||
<text>您还未登录,请先登录</text>
|
||||
<button @click="goToLoginPage" class="mt-5">去登录</button>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
|
||||
// 使用 pinia
|
||||
const userStore = useUserStore();
|
||||
|
||||
const isLoggedIn = computed(() => userStore.token);
|
||||
const userInfo = computed(() => userStore.userInfo);
|
||||
|
||||
// 跳转到登录页面
|
||||
const goToLoginPage = () => {
|
||||
uni.navigateTo({ url: "/pages/login/index" });
|
||||
};
|
||||
|
||||
// 退出登录处理
|
||||
const handleLogout = async () => {
|
||||
await userStore.logout();
|
||||
uni.showToast({ title: "已退出登录", icon: "success" });
|
||||
};
|
||||
</script>
|
||||
12
src/store/index.ts
Normal file
12
src/store/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { App } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
const store = createPinia();
|
||||
|
||||
// 全局注册 store
|
||||
export function setupStore(app: App<Element>) {
|
||||
app.use(store);
|
||||
}
|
||||
|
||||
export * from "./modules/user";
|
||||
export { store };
|
||||
38
src/store/modules/user.ts
Normal file
38
src/store/modules/user.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { defineStore } from "pinia";
|
||||
import AuthAPI from "@/api/auth";
|
||||
import UserAPI, { UserInfo } from "@/api/user";
|
||||
|
||||
export const useUserStore = defineStore("user", () => {
|
||||
// 确保 token 是响应式的
|
||||
const token = ref<string>(uni.getStorageSync("token") || "");
|
||||
const userInfo = ref<UserInfo | null>(null);
|
||||
|
||||
// 登录
|
||||
const login = async (username: string, password: string) => {
|
||||
const { tokenType, accessToken } = await AuthAPI.login(username, password);
|
||||
token.value = `${tokenType} ${accessToken}`; // Bearer token
|
||||
uni.setStorageSync("token", token.value);
|
||||
};
|
||||
|
||||
// 获取用户信息
|
||||
const getUserInfo = async () => {
|
||||
const info = await UserAPI.getUserInfo();
|
||||
userInfo.value = info;
|
||||
};
|
||||
|
||||
// 登出
|
||||
const logout = async () => {
|
||||
await AuthAPI.logout();
|
||||
userInfo.value = null;
|
||||
token.value = ""; // 清空 token
|
||||
uni.removeStorageSync("token"); // 从本地缓存移除 token
|
||||
};
|
||||
|
||||
return {
|
||||
token,
|
||||
userInfo,
|
||||
login,
|
||||
logout,
|
||||
getUserInfo,
|
||||
};
|
||||
});
|
||||
100
src/types/auto-imports.d.ts
vendored
Normal file
100
src/types/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onAddToFavorites: typeof import('@dcloudio/uni-app')['onAddToFavorites']
|
||||
const onBackPress: typeof import('@dcloudio/uni-app')['onBackPress']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onError: typeof import('@dcloudio/uni-app')['onError']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onHide: typeof import('@dcloudio/uni-app')['onHide']
|
||||
const onLaunch: typeof import('@dcloudio/uni-app')['onLaunch']
|
||||
const onLoad: typeof import('@dcloudio/uni-app')['onLoad']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onNavigationBarButtonTap: typeof import('@dcloudio/uni-app')['onNavigationBarButtonTap']
|
||||
const onNavigationBarSearchInputChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputChanged']
|
||||
const onNavigationBarSearchInputClicked: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputClicked']
|
||||
const onNavigationBarSearchInputConfirmed: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputConfirmed']
|
||||
const onNavigationBarSearchInputFocusChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputFocusChanged']
|
||||
const onPageNotFound: typeof import('@dcloudio/uni-app')['onPageNotFound']
|
||||
const onPageScroll: typeof import('@dcloudio/uni-app')['onPageScroll']
|
||||
const onPullDownRefresh: typeof import('@dcloudio/uni-app')['onPullDownRefresh']
|
||||
const onReachBottom: typeof import('@dcloudio/uni-app')['onReachBottom']
|
||||
const onReady: typeof import('@dcloudio/uni-app')['onReady']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onResize: typeof import('@dcloudio/uni-app')['onResize']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onShareAppMessage: typeof import('@dcloudio/uni-app')['onShareAppMessage']
|
||||
const onShareTimeline: typeof import('@dcloudio/uni-app')['onShareTimeline']
|
||||
const onShow: typeof import('@dcloudio/uni-app')['onShow']
|
||||
const onTabItemTap: typeof import('@dcloudio/uni-app')['onTabItemTap']
|
||||
const onThemeChange: typeof import('@dcloudio/uni-app')['onThemeChange']
|
||||
const onUnhandledRejection: typeof import('@dcloudio/uni-app')['onUnhandledRejection']
|
||||
const onUnload: typeof import('@dcloudio/uni-app')['onUnload']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useLink: typeof import('vue-router')['useLink']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
41
src/types/global.d.ts
vendored
Normal file
41
src/types/global.d.ts
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
declare global {
|
||||
/**
|
||||
* 分页查询参数
|
||||
*/
|
||||
interface PageQuery {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应对象
|
||||
*/
|
||||
interface PageResult<T> {
|
||||
/** 数据列表 */
|
||||
list: T;
|
||||
/** 总数 */
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件数据源
|
||||
*/
|
||||
interface OptionType {
|
||||
/** 值 */
|
||||
value: string | number;
|
||||
/** 文本 */
|
||||
label: string;
|
||||
/** 子列表 */
|
||||
children?: OptionType[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应数据
|
||||
*/
|
||||
interface ResponseData<T = any> {
|
||||
code: string;
|
||||
data: T;
|
||||
msg: string;
|
||||
}
|
||||
}
|
||||
export {};
|
||||
44
src/utils/request.ts
Normal file
44
src/utils/request.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
const API_HOST = "https://api.youlai.tech";
|
||||
|
||||
export default function request<T>(options: UniApp.RequestOptions): Promise<T> {
|
||||
const token = uni.getStorageSync("token"); // 从本地缓存获取 token
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
...options,
|
||||
url: `${API_HOST}${options.url}`,
|
||||
header: {
|
||||
...options.header,
|
||||
Authorization: token,
|
||||
},
|
||||
success: (response) => {
|
||||
const resData = response.data as ResponseData<T>;
|
||||
// 业务状态码 00000 表示成功
|
||||
if (resData.code === "00000") {
|
||||
resolve(resData.data);
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: resData.msg || "业务处理失败",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
reject({
|
||||
message: resData.msg || "业务处理失败",
|
||||
code: resData.code,
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
uni.showToast({
|
||||
title: "网络请求失败",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
reject({
|
||||
message: "网络请求失败",
|
||||
error,
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user