《校园生活平台从 0 到 1 的搭建》第四篇:微信授权登录前端

✅ 一、功能目标(FUNCTION GOALS)

该模块旨在实现一个安全、稳定、可拓展的微信登录 + 用户信息系统,作为校园平台的用户体系基础,目标如下:

  1. 微信授权登录:
    调用 wx.login() 获取 code,配合后端实现 openid 获取与注册/登录流程。
  2. 本地持久化登录状态:
    登录成功后,将后端返回的 tokenuserInfo 存入本地 Storage 与 Vuex,全局可用。
  3. 请求统一注入 token:
    所有请求通过封装函数发送,自动带上 Authorization,确保后端识别用户身份。
  4. token 自动校验与跳转:
    若后端返回 401 状态,前端统一处理:清除登录信息、跳转登录页。
  5. 用户中心展示个人信息:
    展示用户头像、昵称等,支持后续扩展:等级、积分、订单、签到、签名等。
  6. 退出登录功能:
    用户点击退出后,立即清除缓存,返回登录页。

✅ 二、实现步骤概览

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. 启动流程

  1. 打开 HBuilderX 或 VSCode;
  2. 启动微信开发者工具``;
  3. 修改 utils/request.jsbaseUrl 为后端实际地址;
  4. 点击用户登录。

🧪 2. 联调测试建议流程

测试项 说明
微信登录 执行 wx.login(),返回 code,后端返回 token + user
token 存储 Storage和 Vuex 均应保存登录信息
接口请求 请求自动携带 Authorizationtoken
token 过期 自动跳转登录页,清除缓存
用户资料展示 头像、昵称正确展示
退出登录 清空状态,返回登录页

相关推荐
风清云淡_A15 分钟前
【Flutter3.8x】flutter从入门到实战基础教程(四):自定义实现一个自增的StatefulWidget组件
前端·flutter
curdcv_po17 分钟前
🔥 Three.js 一个项目用到:轨道控制器、动画与GUI交互
前端
琹箐17 分钟前
CSS font-weight:500不生效
前端·css
程序视点18 分钟前
Java语言核心特性全解析:从面向对象到跨平台原理
java·后端·java ee
代码的余温38 分钟前
React核心:组件化与虚拟DOM揭秘
前端·react.js·前端框架
小螺号dididi吹38 分钟前
【React】状态管理
前端·javascript·react.js
代码的余温42 分钟前
React Refs:直接操作DOM的终极指南
前端·javascript·react.js
懋学的前端攻城狮44 分钟前
深入浅出Linux-01:系统化掌握基础操作
linux·后端
考虑考虑1 小时前
Redis8中的布隆过滤器
redis·后端·程序员
一只小风华~1 小时前
JavaScript 定时器
开发语言·前端·javascript·vue.js·web