feat: 添加登录和获取用户信息接口,整合pinia实现用户状态全局共享

This commit is contained in:
ray
2024-09-23 08:29:44 +08:00
parent f106f84646
commit 737f6a3ee5
21 changed files with 6997 additions and 5061 deletions

View File

@@ -0,0 +1,95 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"PropType": true,
"Ref": true,
"VNode": true,
"WritableComputedRef": true,
"computed": true,
"createApp": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"effectScope": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onAddToFavorites": true,
"onBackPress": true,
"onBeforeMount": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onError": true,
"onErrorCaptured": true,
"onHide": true,
"onLaunch": true,
"onLoad": true,
"onMounted": true,
"onNavigationBarButtonTap": true,
"onNavigationBarSearchInputChanged": true,
"onNavigationBarSearchInputClicked": true,
"onNavigationBarSearchInputConfirmed": true,
"onNavigationBarSearchInputFocusChanged": true,
"onPageNotFound": true,
"onPageScroll": true,
"onPullDownRefresh": true,
"onReachBottom": true,
"onReady": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onResize": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onShareAppMessage": true,
"onShareTimeline": true,
"onShow": true,
"onTabItemTap": true,
"onThemeChange": true,
"onUnhandledRejection": true,
"onUnload": true,
"onUnmounted": true,
"onUpdated": true,
"onWatcherCleanup": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useId": true,
"useModel": true,
"useSlots": true,
"useTemplateRef": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
}
}

View File

@@ -1 +1 @@
npx --no-install commitlint --edit $1
#npx --no-install commitlint --edit $1

View File

@@ -1,2 +1,2 @@
echo "Running pre-commit hook..."
pnpm run lint:lint-staged
#pnpm run lint:lint-staged

View File

@@ -25,9 +25,12 @@
"keyframes-name-pattern": null,
"no-descending-specificity": null,
"no-empty-source": null,
"unit-no-unknown": [true, {
"ignoreUnits": ["rpx"]
}],
"unit-no-unknown": [
true,
{
"ignoreUnits": ["rpx"]
}
],
"selector-pseudo-class-no-unknown": [
true,
{

View File

@@ -4,6 +4,7 @@ import tseslint from "typescript-eslint"; // TypeScript 配置
import pluginVue from "eslint-plugin-vue"; // Vue 配置
export default [
{ extends: ["./.eslintrc-auto-import.json"] },
{ files: ["**/*.{js,mjs,cjs,ts,vue}"] }, // 校验的文件类型
{ languageOptions: { globals: globals.browser } }, // 设置浏览器环境的全局变量
pluginJs.configs.recommended, // JavaScript 推荐配置

View File

@@ -89,6 +89,7 @@
"@dcloudio/uni-mp-weixin": "3.0.0-4020420240722002",
"@dcloudio/uni-mp-xhs": "3.0.0-4020420240722002",
"@dcloudio/uni-quickapp-webview": "3.0.0-4020420240722002",
"pinia": "^2.2.2",
"vue": "^3.4.21",
"vue-i18n": "^9.1.9"
},
@@ -126,7 +127,9 @@
"typescript-eslint": "^8.6.0",
"unocss": "^0.62.4",
"unocss-preset-weapp": "^0.62.2",
"unplugin-auto-import": "^0.18.3",
"vite": "5.2.8",
"vue-tsc": "^1.0.24"
}
},
"packageManager": "pnpm@9.1.3+sha512.7c2ea089e1a6af306409c4fc8c4f0897bdac32b772016196c469d9428f1fe2d5a21daf8ad6512762654ac645b5d9136bb210ec9a00afa8dbc4677843ba362ecd"
}

11515
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

44
src/api/auth.ts Normal file
View 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
View 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[];
}

View File

@@ -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,
};

View File

@@ -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
View 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>

View File

@@ -1,5 +0,0 @@
<template>
<view class="flex-center flex-col">
<text class="text-blue font-bold text-lg">我的</text>
</view>
</template>

View 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
View 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
View 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
View 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
View 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
View 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,
});
},
});
});
}

View File

@@ -12,9 +12,8 @@ export default {
],
shortcuts: [
{
"border-base": "border border-gray-500_10",
"flex-center": "flex justify-center items-center",
"flex-x-center": "flex justify-center",
"flex-col": "flex flex-col",
},
],

View File

@@ -1,5 +1,6 @@
import { defineConfig } from "vite";
import uni from "@dcloudio/vite-plugin-uni";
import AutoImport from "unplugin-auto-import/vite";
export default defineConfig(async () => {
const UnoCss = await import("unocss/vite").then((i) => i.default);
@@ -10,6 +11,14 @@ export default defineConfig(async () => {
// https://github.com/unocss/unocss
UnoCss(),
AutoImport({
imports: ["vue", "uni-app"],
dts: "src/types/auto-imports.d.ts", // 自动生成的类型声明文件
eslintrc: {
enabled: true,
},
}),
],
};
});