✅ 一、功能目标(FUNCTION GOALS)
该模块旨在实现一个安全、稳定、可拓展的微信登录 + 用户信息系统,作为校园平台的用户体系基础,目标如下:
- 微信授权登录:
调用wx.login()
获取code
,配合后端实现 openid 获取与注册/登录流程。 - 本地持久化登录状态:
登录成功后,将后端返回的token
与userInfo
存入本地Storage
与 Vuex,全局可用。 - 请求统一注入 token:
所有请求通过封装函数发送,自动带上Authorization
,确保后端识别用户身份。 - token 自动校验与跳转:
若后端返回 401 状态,前端统一处理:清除登录信息、跳转登录页。 - 用户中心展示个人信息:
展示用户头像、昵称等,支持后续扩展:等级、积分、订单、签到、签名等。 - 退出登录功能:
用户点击退出后,立即清除缓存,返回登录页。
✅ 二、实现步骤概览
1. 目录结构(简要)
bash
/uni-app-wxschool/
├── /pages/
│ ├── /login/ # 登录页(微信授权)
│ └── /profile/ # 用户中心页
├── /store/ # Vuex 状态(token + user)
├── /utils/
│ ├── request.js # 请求封装(带 token)
2. 请求封装(utils/request.js)
javascript
const baseUrl = 'http://localhost:3000/api' // 本地或远程地址
export function request(options) {
const token = uni.getStorageSync('token')
return new Promise((resolve, reject) => {
uni.request({
url: baseUrl + options.url,
method: options.method || 'GET',
header: {
...options.header,
'Authorization': token || ''
},
data: options.data || {},
success(res) {
if (res.statusCode === 401) {
uni.removeStorageSync('token')
uni.removeStorageSync('user')
uni.redirectTo({ url: '/pages/login/index' })
} else {
resolve(res.data)
}
},
fail: reject
})
})
}
3. Vuex 状态(store/index.js)
javascript
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
token: uni.getStorageSync('token') || '',
user: uni.getStorageSync('user') || null
},
mutations: {
setToken(state, token) {
state.token = token;
uni.setStorageSync('token', token);
},
setUser(state, user) {
state.user = user;
uni.setStorageSync('user', user);
},
logout(state) {
state.token = '';
// state.user = null;
uni.removeStorageSync('token');
// uni.removeStorageSync('user');
}
},
getters: {
isLoggedIn: (state) => !!state.token && !!state.user,
userNickname: (state) => state.user?.nickname || '',
userAvatar: (state) => state.user?.avatar_url || '/static/avatar-default.png'
}
});
export default store;
4. 全局挂载一下请求和 vuex,修改 main.js
javascript
import App from './App'
import store from './store'
import request from './utils/request' // 这里改成你实际路径
// #ifndef VUE3
import Vue from 'vue'
import './uni.promisify.adaptor'
Vue.config.productionTip = false
// 全局挂载 Vuex store
Vue.prototype.$store = store
// 全局挂载请求函数
Vue.prototype.$myRequest = request
App.mpType = 'app'
const app = new Vue({
store, // 注入 Vuex
...App
})
app.$mount()
// #endif
// #ifdef VUE3
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
return {
app
}
}
// #endif
5. 用户中心页面(profile/index.vue)
xml
<template>
<view class="profile-page">
<!-- 用户信息区域 -->
<view class="user-info" @tap="handleLoginTap">
<image class="avatar" :src="isLoggedIn ? user.avatar_url : '/static/avatar-default.png'" mode="aspectFill" />
<view class="user-text">
<text class="nickname">{{ isLoggedIn ? user.nickname : '点击登录' }}</text>
</view>
</view>
<!-- 登录/退出操作 -->
<view v-if="isLoggedIn" class="menu-item" @tap="logout">
<text>🚪 退出登录</text>
</view>
</view>
</template>
<script>
import { mapState } from 'vuex';
export default {
computed: {
...mapState(['user', 'token']),
isLoggedIn() {
return !!this.token && !!this.user;
}
},
methods: {
handleLoginTap() {
if (!this.isLoggedIn) {
uni.navigateTo({ url: '/pages/login/index' });
}
},
logout() {
uni.showModal({
title: '确认退出登录?',
success: (res) => {
if (res.confirm) {
// 清除 Vuex 状态和本地缓存 token、user
this.$store.commit('logout');
uni.showToast({ title: '已退出', icon: 'success' });
}
}
});
}
}
};
</script>
<style scoped>
.profile-page {
background-color: #f5f5f7;
min-height: 100vh;
padding: 30rpx;
}
.user-info {
background: #fff;
padding: 30rpx 40rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
margin-bottom: 40rpx;
}
.avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50rpx;
margin-right: 30rpx;
}
.user-text {
flex: 1;
}
.nickname {
font-size: 34rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.menu-list {
margin-top: 30rpx;
}
.menu-item {
background-color: #fff;
padding: 30rpx 20rpx;
border-radius: 16rpx;
margin-bottom: 20rpx;
font-size: 28rpx;
}
</style>
6.个人登录页面(login/index.vue)
- 前端调用
wx.login()
获取临时code
; - 后端用
appid + secret + code
调用微信 API 获取用户openid
; - 通过
openid
查询或注册用户; - 登录成功后,后端返回
token + 用户资料
; - 前端持久化存储 token 与 user 状态。
xml
<template>
<view class="login-page">
<view class="avatar-area">
<button class="avatar-wrapper" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<image class="avatar" :src="avatarUrl" mode="aspectFill"></image>
</button>
</view>
<view class="nickname-area">
<input type="nickname" class="nickNameInput" placeholder="请输入昵称" :value="nickname" @input="getNickname" maxlength="20" />
</view>
<button class="btn-login" @click="wxlogin">登录</button>
</view>
</template>
<script>
export default {
data() {
return {
avatarUrl: '',
nickname: '',
code: ''
};
},
onLoad() {
const cachedUser = uni.getStorageSync('user');
if (cachedUser) {
this.avatarUrl = cachedUser.avatar_url || '';
this.nickname = cachedUser.nickname || '';
}
},
methods: {
// 输入昵称
getNickname(e) {
this.nickname = e.detail.value.trim();
console.log('昵称:', this.nickname);
},
// 选头像
onChooseAvatar(e) {
this.avatarUrl = e.detail.avatarUrl;
console.log('头像:', this.avatarUrl);
},
// 微信登录获取 code
wxlogin() {
if (!this.avatarUrl || !this.nickname) {
uni.showToast({
title: '请上传头像和填写昵称',
icon: 'none',
duration: 2000
});
return;
}
uni.login({
provider: 'weixin',
success: (loginRes) => {
if (loginRes.code) {
this.code = loginRes.code;
this.getCode();
} else {
console.log('登录失败!' + loginRes.errMsg);
uni.showToast({ title: '登录失败,请重试', icon: 'none' });
}
},
fail: (err) => {
console.error('登录接口调用失败', err);
uni.showToast({ title: '登录接口调用失败', icon: 'none' });
}
});
},
// 调用后端登录接口
async getCode() {
try {
const res = await this.$myRequest({
method: 'post',
url: '/auth/wxlogin',
data: {
avatarUrl: this.avatarUrl,
nickname: this.nickname,
code: this.code
}
});
console.log(res);
if (res.code == 0) {
const user = res.data.user;
const token = res.data.token;
// 本地存储
uni.setStorageSync('user', user);
uni.setStorageSync('token', token);
// Vuex 提交
this.$store.commit('setUser', user);
this.$store.commit('setToken', token);
uni.showToast({
title: res.message,
icon: 'success',
duration: 2000
});
uni.navigateBack();
} else {
uni.showToast({
title: res.message || '登录失败',
icon: 'none',
duration: 1000
});
setTimeout(() => {
uni.navigateBack();
}, 1000);
}
} catch (err) {
console.error('请求错误', err);
uni.showToast({
title: '服务异常,请稍后重试',
icon: 'none',
duration: 2000
});
}
}
}
};
</script>
<style scoped>
.login-page {
padding: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
background: #f5f5f7;
height: 100vh;
justify-content: center;
}
.avatar-area {
margin-bottom: 40rpx;
}
.avatar-wrapper {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
overflow: hidden;
border: 2rpx solid #1aad19;
padding: 0;
}
.avatar {
width: 100%;
height: 100%;
}
.nickname-area {
width: 80%;
margin-bottom: 50rpx;
}
.nickname-input {
width: 100%;
height: 64rpx;
line-height: 64rpx;
border-radius: 10rpx;
padding: 0 20rpx;
font-size: 28rpx;
border: 1rpx solid #ccc;
box-sizing: border-box;
}
.btn-login {
width: 280rpx;
height: 64rpx;
background-color: #1aad19;
color: white;
border-radius: 32rpx;
font-size: 30rpx;
line-height: 64rpx;
text-align: center;
}
</style>
✅ 三、启动与测试说明
🚀 1. 启动流程
- 打开 HBuilderX 或 VSCode;
- 启动微信开发者工具``;
- 修改
utils/request.js
的baseUrl
为后端实际地址; - 点击用户登录。
🧪 2. 联调测试建议流程
测试项 | 说明 |
---|---|
微信登录 | 执行 wx.login() ,返回 code,后端返回 token + user |
token 存储 | Storage 和 Vuex 均应保存登录信息 |
接口请求 | 请求自动携带 Authorization token |
token 过期 | 自动跳转登录页,清除缓存 |
用户资料展示 | 头像、昵称正确展示 |
退出登录 | 清空状态,返回登录页 |