2025 后端自学UNIAPP【项目实战:旅游项目】5、个人中心页面:微信登录,同意授权,获取用户信息

发布于:2025-05-16 ⋅ 阅读:(141) ⋅ 点赞:(0)

一、框架以及准备工作

1、前端项目文件结构展示           2、后端项目文件结构展示

  

3、登录微信公众平台,注册一个个人的程序,获取大appid(前端后端都需要)和密钥(后端需要) 

微信公众平台微信公众平台,给个人、企业和组织提供业务服务与用户管理能力的全新服务平台。https://mp.weixin.qq.com/

4、后端代码配置上自己的appid和密钥以及相关文件

我的是 application.properties,

后端代码逻辑可以从这里获取,自己编写到自己的java后端业务代码中2025 Java 微信小程序根据code获取openid,二次code获取手机号【工具类】拿来就用-CSDN博客文章浏览阅读4次。Spring Boot实现微信小程序登录,含session_key获取和手机号解密功能。 https://blog.csdn.net/m0_47484034/article/details/147993148?spm=1001.2014.3001.5501

#wechat
wx.open.applet.app_id=自己的
wx.open.applet.secret=自己的

5、前端配置上自己的appid

 

二、前端页面源码

1、页面源码personal_center.vue

微信登录重点是:弹出框的相关逻辑

<template>
	<view class="container">
		<!-- 个人信息 -->
		<view class="info-box">
			<!-- 签到、设置、聊天一行 -->
			<view class="info-header">
				<!-- 签到 -->
				<view class="sign-in">
					<up-icon name="calendar" color="#fff" size="30"></up-icon>
					<view class="sign-text">签到</view>
				</view>
				<!-- 设置和聊天 -->
				<view class="action-icons">
					<up-icon name="chat" color="#fff" size="30"></up-icon>
					<up-icon name="setting" color="#fff" size="30" style="margin-left: 20rpx;"></up-icon>
				</view>
			</view>

			<!-- 个人信息卡片 -->
			<view class="profile-card" @click="toGetUserInfo">
				<!-- 个人信息卡片 信息主体 -->
				<view class="profile-main">
					<!-- 如果未登录/未注册显示默认头像并提示注册/登录 -->
					<template v-if="!userInfo.nickName">
						<!-- 默认头像 -->
						<image class="avatar" src="../../static/logo.png" mode="aspectFill"></image>
						<!-- 去注册/登录 -->
						<view class="nikename">注册 / 登录</view>
					</template>
					<!-- 如果登录后,显示出头像和昵称 -->
					<template v-else>
						<!-- 获取头像 -->
						<image class="avatar" image :src="userInfo.avatar" mode="aspectFill"></image>
						<!-- 昵称 -->
						<view class="nikename">{{userInfo.nickName}}</view>
					</template>
				</view>
				<view class="profile-other">
					<view class="profile-other-item favourite">
						<view class="profile-other-item-num favourite—num">5</view>
						<view class="profile-other-item-text favourite-text">最爱</view>
					</view>
					<view class="profile-other-item browse">
						<view class="profile-other-item-num browse-num">100</view>
						<view class="profile-other-item-text browse-text">浏览</view>
					</view>
					
				</view>
			</view>
		</view>
		<!-- 功能列表 -->
		<view class="list-box">
			<view class="list">
				<uni-list>
					<uni-list-item title="个人信息" showArrow thumb="/static/user-menu/个人信息.png" thumb-size="sm"
						clickable />
					<uni-list-item title="我的购物车" showArrow thumb="/static/user-menu/我的购物车.png" thumb-size="sm"
						clickable />
					<uni-list-item title="用户反馈" showArrow thumb="/static/user-menu/用户反馈.png" thumb-size="sm"
						clickable />
					<uni-list-item title="我的邮件" showArrow thumb="/static/user-menu/我的邮件.png" thumb-size="sm"
						clickable />
					<uni-list-item title="分享有礼" showArrow thumb="/static/user-menu/分享有礼.png" thumb-size="sm"
						clickable />
				</uni-list>
			</view>
		</view>

		<!-- 弹出层 -->
		<up-popup :show="show" @close="close">
			<view class="popup">
				<view class="popup-title">获取您的昵称、头像</view>

				<!-- 头像选择 -->
				<view class="popup-child-box">
					<view class="label">获取用户头像</view>
					<button class="avatar-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
						<image class="avatar-img" :src="userInfo.avatar || '/static/default-avatar.png'"></image>
					</button>
				</view>

				<!-- 昵称输入 -->
				<view class="popup-child-box">
					<view class="label">获取用户昵称</view>
					<input class="nickname-input" type="nickname" @input="changeName" />
				</view>
				<button size="default" type="primary" @click="userSubmit">确认</button>
			</view>
		</up-popup>
	</view>
</template>

<script setup>
	import {
		ref,
		reactive
	} from 'vue'
	// 生命周期,进来就加载
	import {
		onLoad
	} from '@dcloudio/uni-app'
	// 生命周期,进来就加载
	import {
		login
	} from '../../api/my_api'
	import avatar from '../../uni_modules/uview-plus/components/u-avatar/avatar';
	onLoad(() => {
		// 尝试从本地存储获取用户信息
		const storedUserInfo = uni.getStorageSync('userInfo');
		if (storedUserInfo) {
			userInfo.nickName = storedUserInfo.nickName;
			userInfo.avatar = storedUserInfo.avatar;
		}
	})

	const userInfo = reactive({
		nickName: "",
		avatar: ""
	})
	// 控制弹出层的显示
	const show = ref(false)
	
	//关闭弹出层的显示
	const close = () => {
		show.value = false
	}
	const onChooseAvatar = (e) => {
		userInfo.avatar = e.detail.avatarUrl
	}
	const changeName = (e) => {
		userInfo.nickName = e.detail.value
	}
	// 用户提交信息
	const userSubmit = async () => {

		if (!userInfo.nickName || !userInfo.avatar) {
			uni.showToast({
				title: '请填写完整信息',
				icon: 'none'
			});
			return;
		}

		try {
			// 保存用户信息到本地存储
			uni.setStorageSync('userInfo', {
				nickName: userInfo.nickName,
				avatar: userInfo.avatar
			});

			uni.showToast({
				title: '信息保存成功',
				icon: 'success'
			});
			show.value = false
		} catch (e) {
			uni.showToast({
				title: '保存失败,请重试',
				icon: 'none'
			});
		}

	}
	//微信授权登录,获取微信用户相关信息
	const toGetUserInfo = () => {
		// 如果已经有用户信息,直接显示弹窗
		if (userInfo.nickName && userInfo.avatar) {
			show.value = true;
			return;
		}

		uni.showModal({
			title: "温馨提示",
			content: "授权登录才可以正常使用小程序",
			success(res) {
				if (res.confirm) {
					uni.login({
						success: async (data) => {
							uni.getUserInfo({
								provider: 'weixin',
								success: async function(infoRes) {
									const user = await login(data.code, infoRes
										.userInfo.avatarUrl)
									userInfo.nickName = infoRes.userInfo.nickName
									userInfo.avatar = infoRes.userInfo.avatarUrl
									// 保存用户信息到本地存储
									uni.setStorageSync('userInfo', {
										nickName: userInfo.nickName,
										avatar: userInfo.avatar
									});
									show.value = true
								}
							})
						}
					})
				}
			}
		})
	}
</script>


<style lang="scss" scoped>
	.container {
		height: 100vh;
		background-color: #f5f5f5;

		// 个人信息区域
		.info-box {
			width: 100%;
			position: relative;
			z-index: 1;
			overflow: hidden;
			padding: 40rpx 30rpx;
			box-sizing: border-box;

			&::after {
				content: "";
				width: 140%;
				height: 400rpx;
				z-index: -1;
				position: absolute;
				top: 0;
				left: -20%;
				background-color: #6799FF;
				border-radius: 0 0 50% 50%;
			}

			// 顶部图标行
			.info-header {
				display: flex;
				justify-content: space-between;
				align-items: center;
				margin-bottom: 40rpx;

				.sign-in {
					display: flex;
					align-items: center;

					.sign-text {
						color: white;
						font-size: 28rpx;
						margin-left: 10rpx;
					}
				}

				.action-icons {
					display: flex;
					align-items: center;
				}
			}

			// 个人信息卡片
			.profile-card {
				background-color: #fff;
				border-radius: 20rpx;
				padding: 30rpx;
				box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.08);

				// 个人信息卡片 信息主体
				.profile-main {
					display: flex;
					align-items: center;

					// 用户头像
					.avatar {
						width: 120rpx;
						height: 120rpx;
						border-radius: 50%;
						margin-right: 30rpx;
					}

					// 用户昵称
					.nikename {
						font-size: 36rpx;
						font-weight: bold;
						color: #333;
					}
				}

				.profile-other {
					display: flex;
					justify-content: space-around;
					align-items: center;

					.profile-other-item {
						text-align: center;
						margin-top: 40rpx;

						.profile-other-item-num {
							color: black;
							font-weight: 700;
							font-size: 25rpx;
						}

						.profile-other-item-text {
							color: #757575;
							margin-top: 10rpx;
							font-size: 20rpx;
						}
					}
				}
			}
		}

		// 功能列表
		.list-box {
			height: 200rpx;
			margin: -10rpx auto 0;
			padding: 20rpx;
			box-sizing: border-box;
			border-radius: 12rpx;

		}

		/* 弹出层样式 */
		.popup {
			padding: 40rpx;
			border-radius: 20rpx;
			background: #fff;

			/* 标题 */
			.popup-title {
				margin-bottom: 40rpx;
				font-size: 36rpx;
				font-weight: bold;
				text-align: center;
				color: #333;
			}
		}

		/* 每个选项容器 */
		.popup-child-box {
			display: flex;
			justify-content: space-between;
			align-items: center;
			padding: 30rpx 0;
			border-bottom: 1rpx solid #f0f0f0;

			/* 标签文字 */
			.label {
				font-size: 32rpx;
				color: #666;
				width: 200rpx;
			}

			/* 头像按钮 */
			.avatar-btn {
				margin: 0;
				padding: 0;
				border: none;
				background: transparent;
				line-height: 1;

				.avatar-img {
					width: 100rpx;
					height: 100rpx;
					border-radius: 50%;
					background: #f5f5f5;
				}
			}

			/* 昵称输入框 */
			.nickname-input {
				flex: 1;
				text-align: right;
				font-size: 32rpx;
				color: #333;
			}
		}
	}
</style>

2、调用的后端各个接口的js文件:my_api.js

// 引入公共的请求封装
import http from './my_http.js'

// 登录接口
export const login = async (code, avatar) => {
		const res = await http('/login/getWXSessionKey', {code,avatar});
};


// 获取bannner列表
export const getBannerList = () => {
	return http('/banner/list')
}

// 获取景点类型列表(支持传入typeId参数)
export const getTypeList = () => {
	return http('/type/list')
}

// 获取景点列表
export const getAttractionList = (typeId) => {
	// 如果有typeId就拼接到URL,没有就不加
	const url = typeId ? `/attraction/list?typeId=${typeId}` : '/attraction/list'
	return http(url)
}
// 获取景点详情
export const getAttractionInfo = (attractionId) => {
	return http(`/attraction/getInfo/${attractionId}`)
}



3、公共http请求封装的js文件:my_http.js

/**
 * 基础API请求地址(常量,全大写命名规范)
 * @type {string}
 * @constant
 */
let BASE_URL="http://localhost:9001/travel"
/**
 * 封装的HTTP请求核心函数
 * @param {string} url - 请求的接口路径(不需要包含基础接口URL)
 * @param {Object} [data={}] - 请求参数,默认为空对象
 * @param {string} [method='GET'] - HTTP方法,默认GET,支持GET/POST/DELETE/PUT等
 * @returns {Promise} - 返回Promise便于链式调用
 * 
 */
export default function http(url, data = {}, method = 'GET') {
	// 返回一个Promise对象,支持外部链式调用
	return new Promise((resolve, reject) => {
		// 调用uni-app的底层请求API
		uni.request({
			// 拼接完整请求地址(基础接口URL +  请求的接口路径)
			url: BASE_URL + url,

			// 请求参数(GET请求时会自动转为query string)
			data: data,

			// 请求方法(转换为大写保证兼容性)
			method: method.toUpperCase(),

			// 请求头配置
			header: {
				// 从本地存储获取token,没有就位空
				'token': uni.getStorageSync('token') || '',
				// 默认JSON格式
				'Content-Type': 'application/json'
			},

			// 请求成功回调(注意:只要收到服务器响应就会触发,无论HTTP状态码)
			success: (res) => {
				/* HTTP层状态码处理(4xx/5xx等也会进入success回调) */
				if (res.statusCode !== 200) {
					const errMsg = `[${res.statusCode}]${res.errMsg || '请求失败'}`
					showErrorToast(errMsg)
					// 使用Error对象传递更多错误信息
					reject(errMsg)
				}

				/* 业务层状态码处理(假设1表示成功) */
				if (res.data.code === "200") {
					// 提取业务数据(约定data字段为有效载荷)
					resolve(res.data.data)
				} else {
					// 业务错误处理
					const errMsg = res.data.msg || `业务错误[${res.data.code}]`
					showErrorToast(errMsg)
					reject(res.data.msg)
				}
			},

			// 请求失败回调(网络错误、超时等)
			fail: (err) => {
				const errMsg = `网络连接失败: ${err.errMsg || '未知错误'}`
				showErrorToast(errMsg)
				reject(new Error(errMsg))
			},


		})
	})
}

/**
 * 显示统一格式的错误提示(私有工具方法)
 * @param {string} message - 需要显示的错误信息
 * @private
 */
function showErrorToast(message) {
	uni.showToast({
		title: message, // 提示内容
		icon: 'none', // 不显示图标
		duration: 3000 // 3秒后自动关闭
	})
}

三、效果

      

     


网站公告

今日签到

点亮在社区的每一天
去签到