# 一、注册登录页面

关于注册登录的说明:

  1. 注册登录我们从最简单的开始讲起,即:用户名和密码开始讲起,后续我们会逐步讲解:手机号验证码登录本机一键登录第三方登录如:微信、QQ、微博等App扫码登录
  2. 在最初开发注册登录的时候,就要加上用户服务协议用户隐私政策(监管要求)。

# 1. 创建页面并打开页面

  1. 新建页面 /pages/loginCenter/loginCenter.vue;
  2. pages.json 中配置:
{
    "path" : "pages/loginCenter/loginCenter",
    "style" : 
    {
        "navigationBarTitleText" : "登录注册",
        "navigationStyle": "custom"
    }
}
  1. /pages/wode/wode.nvue页面点击头像打开注册登录页面
    methods: {
        clickAvatarNickname(){
            uni.navigateTo({
                url: '/pages/loginCenter/loginCenter',
            });
        }
    }

# 2. 登录注册页面代码

由于我们已经引入了uViewUI框架,所以可以直接使用它的组件,如: https://uviewui.com/components/input.html (opens new window)

在页面 /pages/loginCenter/loginCenter.vue

# ① 登录注册页面初步样式

<template>
	<view class="page">
		<!-- 导航栏 -->
		<chat-navbar  :fixed="true"
		:showPlus="false" :showUser="false"
		:showBack="true" navbarClass="navbar-bgColor"
		:h5WeiXinNeedNavbar="false">
		</chat-navbar>
		<!-- 表单部分 -->
		<view class="p-3">
			<!-- 用户名 -->
			<view class="mb-3">
				<u-input type="text" v-model="form.username"
				showWordLimit clearable
				:placeholder="form.username_placeHolder" :maxlength="20"
				:adjustPosition="false"
				customStyle="background-color:#ffffff;height:60rpx;">
				</u-input>
			</view>
			<!-- 密码 -->
			<view class="mb-5">
				<u-input type="password" v-model="form.password"
				showWordLimit clearable
				:placeholder="form.password_placeHolder" :maxlength="20"
				:adjustPosition="false"
				customStyle="background-color:#ffffff;height:60rpx;">
				</u-input>
			</view>
			<!-- 协议 -->
			<view class="flex align-center mb-5">
				<view style="width: 60rpx;">
					<checkbox-group>
						<checkbox value="tongyi" :checked="false" />
					</checkbox-group>
				</view> 
				<view class="flex align-center font-sm">
					<text class="text-light-muted">我已阅读并同意</text>
					<text class="text-primary ml-1">《用户服务协议》</text>
					<text class="text-primary ml-1">《用户隐私政策》</text>
				</view>
			</view>
			<!-- 提交按钮 -->
			<u-button type="primary" shape="circle">登 录</u-button>
		</view>
	</view>
</template>

<script>
	export default {
		data() {
			return {
				form:{
					username:'',
					password:'',
					username_placeHolder:'用户名',
					password_placeHolder:'密码',
				},
				page:{
					// 页面类型登录还是注册:默认登录
					type:"login", 
					// 默认登录方式:username 账号密码 phoneCode 验证码 等
					typeMode:'username', 
				},
			}
		},
		methods: {
			
		}
	}
</script>

<style>
/* 导航栏颜色 */
.navbar-bgColor{
	background-color: #ededed;
}
</style>

# ② 控制提交按钮样式及勾选协议条款动画

关于css动画库非常多,大家可以去网上找,这里提供一个 Animate.css 动画库 https://animate-css.nodejs.cn/ (opens new window)

# 1. 先将动画库引入到 App.vue

把下载的动画库css名称为:animate.min.css 放入到项目的 /common/css/animate.min.css,然后引入

<style>
	/*每个页面公共css */
	...
	/* #ifndef APP-PLUS-NVUE */
	...
	@import '/common/css/animate.min.css';
	/* #endif */
</style>

# 2. 控制提交按钮样式及勾选协议条款动画

在页面 /pages/loginCenter/loginCenter.vue

<template>
	<view class="page">
		<!-- 导航栏 -->
		...
		<!-- 表单部分 -->
		<view class="p-3">
			<!-- 用户名 -->
			...
			<!-- 密码 -->
			...
			<!-- 协议 -->
			<view class="flex align-center mb-5" :class="tiaokuanAnimateClass">
				<view style="width: 60rpx;">
					<checkbox-group @change="checkboxChange">
						<checkbox value="tongyi" :checked="false" />
					</checkbox-group>
				</view> 
				<view class="flex align-center font-sm">
					...
				</view>
			</view>
			<!-- 提交按钮 -->
			<u-button type="primary" shape="circle" :disabled="!submitAbled"
			@click="submit">登 录</u-button>
		</view>
	</view>
</template>

<script>
	export default {
		data() {
			return {
				form:{
					username:'',
					password:'',
					username_placeHolder:'用户名',
					password_placeHolder:'密码',
				},
				page:{
					// 页面类型登录还是注册:默认登录
					type:"login", 
					// 默认登录方式:username 账号密码 phoneCode 验证码 等
					typeMode:'username', 
				},
				other:{
					agreeTiaoKuan:false, // 是否同意条款协议
					tiaokuanAnimateCount:0,//提示条款动画计数器
				},
			}
		},
		computed:{
			//提交按钮是否可用
			submitAbled(){
				if(this.page.type == 'login'){
					//账号密码登录
					if(this.page.typeMode == 'username'){
						this.form.username_placeHolder = '用户名';
						this.form.password_placeHolder = '密码';
						// 账号小于4位 密码小于6位 提交按钮不可用
						if(this.form.username.length < 4 || this.form.password.length < 6){
							return false;
						}
						return true;
					}
					// 验证码登录
					// 其它登录方式
				}
			},
			// 没有勾选条款的动画处理
			tiaokuanAnimateClass(){
				if(this.other.tiaokuanAnimateCount == 0){
					return ``;
				}else if(this.other.tiaokuanAnimateCount > 0 && (this.other.tiaokuanAnimateCount % 2 == 1)){
					return `animate__animated animate__heartBeat`;
				}else if(this.other.tiaokuanAnimateCount > 0 && (this.other.tiaokuanAnimateCount % 2 == 0)){
					return `animate__animated animate__shakeX`;
				}
			},
		},
		methods: {
			// 提交数据
			submit(){
				if(!this.other.agreeTiaoKuan){
					this.other.tiaokuanAnimateCount += 1;
					console.log('提示用户要勾选协议,然后给点动画',this.other.tiaokuanAnimateCount);
					return;
				}
				console.log('提交数据',this.form);
			},
			//是否同意隐私政策
			checkboxChange(e){
				console.log('是否同意隐私政策',e);
				if(e.detail.value[0] == 'tongyi'){
					this.other.agreeTiaoKuan = true;
					this.other.tiaokuanAnimateCount = 0;
				}else{
					this.other.agreeTiaoKuan = false;
				}
			},
		}
	}
</script>

# ③ 登录注册切换

在页面 /pages/loginCenter/loginCenter.vue

<template>
	<view class="page">
		<!-- 导航栏 -->
		...
		<!-- 表单部分 -->
		<view class="p-3">
			<!-- 用户名 -->
			...
			<!-- 密码 -->
			...
			<!-- 条款协议 -->
			<view class="flex align-center mb-5" >
				...
			</view>
			<!-- 提交框 -->
			<u-button type="primary" shape="circle"
			@click="submit" :disabled="!submitAbled">{{page.pageTitle}}</u-button>
			<!-- 其它登录方式 注册登录切换 -->
			<view class="flex align-center justify-between py-5 mt-2">
				<view class="text-light-muted mx-2 font">手机号登录</view>
				<view class="text-light-muted mx-2 font" @click="changeType">
					{{page.type === 'login' ? '注册账号':'登录账号' }}
				</view>
			</view>
		</view>
	</view>
</template>

<script>
	export default {
		data() {
			return {
				form:{
					...
				},
				page:{
					// 页面类型登录还是注册:默认登录
					type:"login",					
					// 默认登录方式:username 用户名密码 phoneCode 验证码 等
                    typeMode:"username",
					// 页面标题
					pageTitle:'',
				},
				other:{
					...
				},
			}
		},
		computed:{
			// 提交按钮是否可用
			submitAbled(){
				if(this.page.type == 'login'){
					// 账号密码登录
					if(this.page.typeMode == 'username'){
						this.form.username_placeHolder = '用户名';
						this.form.password_placeHolder = '密码';
						// 账号输入小于4位  密码输入小于6位 提交按钮不可用
						if(this.form.username.length < 4 || this.form.password.length < 6){
							return false;
						}
						return true;
					}
					//验证码登录
					//其它登录方式
				}else if(this.page.type == 'reg'){
					this.form.username_placeHolder = '设置用户名4-20位(字母,数字,下划线)';
					this.form.password_placeHolder = '设置6-20位密码';
					if(this.form.username.length < 4 || this.form.password.length < 6){
						return false;
					}
					return true;
				}
			},
			...,
		},
		onLoad() {
			this.pageInit();
		},
		methods: {
			// type切换
			changeType(){
				this.page.type = this.page.type === 'login' ? 'reg' : 'login';
				this.pageInit();
			},
			// 页面标题及初始化
			pageInit(){
				// 重置输入内容
				this.form.username = '';
				this.form.password = '';
				// 页面导航栏标题
				if(this.page.type === 'login'){
					this.page.pageTitle = this.page.typeMode == 'username' ? 
					this.page.typeMode == 'phoneCode' ? 
					'手机验证码登录' : '账 号 登 录' : this.page.pageTitle;
				}else if(this.page.type === 'reg'){
					this.page.pageTitle = '注 册';
				}
				uni.setNavigationBarTitle({
					title:this.page.pageTitle
				});
			},
			// 提交数据
			submit(){
				if(!this.other.agreeTiaoKuan){
					this.other.tiaokuanAnimateCount += 1;
					console.log('提示用户要勾选条款,给他动画提示',this.other.tiaokuanAnimateCount);
					// #ifdef MP
					uni.showToast({
						title: '请勾选已阅读并同意服务协议和隐私政策',
						icon:'none'
					});
					// #endif
					return;
				}
				if(this.page.type == 'login'){
					console.log('提交登录数据',this.form);
				}else if(this.page.type === 'reg'){
					console.log('提交注册数据',this.form);
				}
				
			},
			...
		}
	}
</script>

说明:
本季课程,我们以账号注册登录为准,后续课程给大家讲解:手机号验证码登录本机一键登录、第三方登录如:微信QQ微博等、App扫码登录

# 二、关于注册登录功能实现的重要说明

从注册开始,我们需要进行前后端的交互操作了,如果对后端感兴趣的同学,可以有以下学习方式:

  1. 如果对后端完全不会的同学,如果想从零开始学习后端知识,可以从我们的 第二学期第三季开始学起 ;

  2. 如果学习过我们 第二学期第三季课程的同学,可以去学习我们的全栈课程:第四学期第一季,我们主讲后端知识,前6个章节主要是讲商城板块的后端功能,并且为了方便不同技术栈的同学学习,我们通过 (thinkphp + mysql)(egg.js + mysql)两套技术栈给大家讲解后端知识和接口开发;

  3. 我们本季度课程后端部分,也是在我们第四学期第一季课程中,之所以这样设定,主要是为了让同学们对后端开发有一整套的知识体系和代码的完整性开发,对后端感兴趣的同学,可以按照我们课程顺序将第四学期第一季课程学习过来,我们本季课程后端在第四学期第一季课程的 第三学期第二季即时通讯后端内容讲解板块;

  4. 关于即时通讯的 登录、注册、退出登录 接口,打开可以看 eggjs即时通讯接口

  5. 如果不想学习后端的同学,我给大家提供了一套线上接口,供大家使用,大家跟着老师的视频学习即可;

# 三、注册登录功能实现

  1. 本地接口:大家可以去群里面下载本季课程全部后端代码压缩包,下载后解压一下,然后在本地运行后端代码,点击查看:如何在本地运行后端代码(记得把数据导入数据库) ,然后在本地进行接口调试;
  2. 线上接口:如果没有学习过我们前面的课程,没有后端基础的同学,可以使用线上接口,网址是 https://lesson07.51yrc.com,大家跟着老师的视频进行调试接口;

# 1. 注册功能接口文档

  1. 注册接口文档

  2. 注册接口文档 (再次说明)
  1. 请求方式:post [用postman测试]或者[用Apipost测试]
  2. 接口示例:http://127.0.0.1:7001/api/regChat(小程序真机测试用自己的ip地址,如http://192.168.2.7:7001/api/regChat)

① 本地接口地址(去群里面下载本季课程全部后端代码,然后在本地运行):http://127.0.0.1:7001/api/regChat (opens new window) (小程序真机测试用自己的ip地址,如http://192.168.2.7:7001/api/regChat)
② 线上接口地址:https://lesson07.51yrc.com/api/regChat (opens new window)

  1. 请求参数 [body] -> [x-www-form-urlencoded]
参数 是否必填 类型 长度 说明
username string 4-20位,字母数字下划线 用户名(账号),如:my01
password string 6-20位 密码,如:123456
  1. 返回示例
{
    "msg": "ok",
    "data": {
        "create_time": "2025-07-13 11:46:05",
        "id": 6,
        "uuid": "be047323-c8e3-4305-b014-3d8c7d24f300",
        "username": "my01",
        "nickname": "",
        "avatar": "https://thinkphp-all.oss-cn-hangzhou.aliyuncs.com/public/67b3001b2aedd.png",
        "mobile": null,
        "email": null,
        "status": 1,
        "last_login": null,
        "last_login_ip": null,
        "register_time": "2025-07-13T03:46:05.000Z",
        "register_ip": null,
        "is_deleted": 0,
        "wechat_openid": null,
        "qq_openid": null,
        "weibo_uid": null,
        "role": "user",
        "is_email_verified": 0,
        "is_mobile_verified": 0,
        "order": 50,
        "update_time": "2025-07-13T03:46:05.000Z",
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVfdGltZSI6IjIwMjUtMDctMTMgMTE6NDY6MDUiLCJpZCI6NiwidXVpZCI6ImJlMDQ3MzIzLWM4ZTMtNDMwNS1iMDE0LTNkOGM3ZDI0ZjMwMCIsInVzZXJuYW1lIjoibXkwNiIsInBhc3N3b3JkIjoiOGQ5NjllZWY2ZWNhZDNjMjlhM2E2MjkyODBlNjg2Y2YwYzNmNWQ1YTg2YWZmM2NhMTIwMjBjOTIzYWRjNmM5MiIsIm5pY2tuYW1lIjoiIiwiYXZhdGFyIjoiaHR0cHM6Ly90aGlua3BocC1hbGwub3NzLWNuLWhhbmd6aG91LmFsaXl1bmNzLmNvbS9wdWJsaWMvNjdiMzAwMWIyYWVkZC5wbmciLCJtb2JpbGUiOm51bGwsImVtYWlsIjpudWxsLCJzdGF0dXMiOjEsImxhc3RfbG9naW4iOm51bGwsImxhc3RfbG9naW5faXAiOm51bGwsInJlZ2lzdGVyX3RpbWUiOiIyMDI1LTA3LTEzVDAzOjQ2OjA1LjAwMFoiLCJyZWdpc3Rlcl9pcCI6bnVsbCwiaXNfZGVsZXRlZCI6MCwid2VjaGF0X29wZW5pZCI6bnVsbCwicXFfb3BlbmlkIjpudWxsLCJ3ZWlib191aWQiOm51bGwsInJvbGUiOiJ1c2VyIiwiaXNfZW1haWxfdmVyaWZpZWQiOjAsImlzX21vYmlsZV92ZXJpZmllZCI6MCwib3JkZXIiOjUwLCJ1cGRhdGVfdGltZSI6IjIwMjUtMDctMTNUMDM6NDY6MDUuMDAwWiIsImlhdCI6MTc1MjM3ODM2NX0.M7fae1gBTqQO4ewAELqz51blbv_0kh27UVgDOVImwkU"
    }
}

# 2. 登录功能接口文档

  1. 用户登录接口文档

  2. 登录接口文档 (再次说明)
  1. 请求方式:post [用postman测试]或者[用Apipost测试]
  2. 接口示例:http://127.0.0.1:7001/api/loginChat(小程序真机测试用自己的ip地址,如http://192.168.2.7:7001/api/loginChat)

① 本地接口地址(去群里面下载本季课程全部后端代码,然后在本地运行):http://127.0.0.1:7001/api/loginChat (opens new window) (小程序真机测试用自己的ip地址,如http://192.168.2.7:7001/api/loginChat)
② 线上接口地址:https://lesson07.51yrc.com/api/loginChat (opens new window)

  1. 请求参数 [body] -> [x-www-form-urlencoded]
参数 是否必填 类型 长度 说明
username string 4-20位,字母数字下划线 用户名(账号),如:my01
password string 6-20位 密码,如:123456
  1. 返回示例
{
    "msg": "ok",
    "data": {
        "create_time": "2025-07-13 11:46:05",
        "id": 6,
        "uuid": "be047323-c8e3-4305-b014-3d8c7d24f300",
        "username": "my01",
        "nickname": "",
        "avatar": "https://thinkphp-all.oss-cn-hangzhou.aliyuncs.com/public/67b3001b2aedd.png",
        "mobile": null,
        "email": null,
        "status": 1,
        "last_login": null,
        "last_login_ip": null,
        "register_time": "2025-07-13T03:46:05.000Z",
        "register_ip": null,
        "is_deleted": 0,
        "wechat_openid": null,
        "qq_openid": null,
        "weibo_uid": null,
        "role": "user",
        "is_email_verified": 0,
        "is_mobile_verified": 0,
        "order": 50,
        "update_time": "2025-07-13T03:46:05.000Z",
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVfdGltZSI6IjIwMjUtMDctMTMgMTE6NDY6MDUiLCJpZCI6NiwidXVpZCI6ImJlMDQ3MzIzLWM4ZTMtNDMwNS1iMDE0LTNkOGM3ZDI0ZjMwMCIsInVzZXJuYW1lIjoibXkwNiIsInBhc3N3b3JkIjoiOGQ5NjllZWY2ZWNhZDNjMjlhM2E2MjkyODBlNjg2Y2YwYzNmNWQ1YTg2YWZmM2NhMTIwMjBjOTIzYWRjNmM5MiIsIm5pY2tuYW1lIjoiIiwiYXZhdGFyIjoiaHR0cHM6Ly90aGlua3BocC1hbGwub3NzLWNuLWhhbmd6aG91LmFsaXl1bmNzLmNvbS9wdWJsaWMvNjdiMzAwMWIyYWVkZC5wbmciLCJtb2JpbGUiOm51bGwsImVtYWlsIjpudWxsLCJzdGF0dXMiOjEsImxhc3RfbG9naW4iOm51bGwsImxhc3RfbG9naW5faXAiOm51bGwsInJlZ2lzdGVyX3RpbWUiOiIyMDI1LTA3LTEzVDAzOjQ2OjA1LjAwMFoiLCJyZWdpc3Rlcl9pcCI6bnVsbCwiaXNfZGVsZXRlZCI6MCwid2VjaGF0X29wZW5pZCI6bnVsbCwicXFfb3BlbmlkIjpudWxsLCJ3ZWlib191aWQiOm51bGwsInJvbGUiOiJ1c2VyIiwiaXNfZW1haWxfdmVyaWZpZWQiOjAsImlzX21vYmlsZV92ZXJpZmllZCI6MCwib3JkZXIiOjUwLCJ1cGRhdGVfdGltZSI6IjIwMjUtMDctMTNUMDM6NDY6MDUuMDAwWiIsImlhdCI6MTc1MjM3ODM2NX0.M7fae1gBTqQO4ewAELqz51blbv_0kh27UVgDOVImwkU"
    }
}

# 3. 注册登录前后端交互

# ① 新建一个数据配置文件

新建 /common/mixins/configData.js

export default{
	data() {
		return {
			// 请求数据地址
			requestUrl:{
				//本地http://127.0.0.1:7001 
				//小程序真机调试用本地ip地址
				http:'http://192.168.2.7:7001',
				//线上
				// http:'https://lesson07.51yrc.com',
			},
		}
	},
}

# ② 在页面引入数据配置文件

在页面 /pages/loginCenter/loginCenter.vue

<script>
	import configDataJs from '@/common/mixins/configData.js';
	import regloginJs from '@/pages/loginCenter/reglogin.js';
	export default {
		mixins:[configDataJs,regloginJs],
		methods: {
            // 提交数据
			submit(){
				...
				if(this.page.type == 'login'){
					console.log('提交登录数据',this.form);
					this.loginSubmit();
				}else if(this.page.type == 'reg'){
					console.log('提交注册数据',this.form);
					this.regSubmit();
				}
				
			},
		},
	}
</script>

# ③ 注册登录提交方法

新建一个js文件:/pages/loginCenter/reglogin.js 写方法

export default {
	methods: {
		// 提交注册数据
		regSubmit() {
			console.log('提交注册数据方法', this.form);
			uni.$u.http.post(this.requestUrl.http + '/api/regChat', this.form).then(res => {
				console.log('服务器返回注册结果', res);
			}).catch(err => {
				console.log('出错', err);
				if (err.data && err.data.data) {
					uni.showToast({title: err.data.data,icon: 'none',duration:5000});
				}
			});
		},
		// 提交登录数据
		loginSubmit() {
			console.log('提交登录数据方法', this.form);
			uni.$u.http.post(this.requestUrl.http + '/api/loginChat', this.form).then(res => {
				console.log('服务器返回登录结果', res);
			}).catch(err => {
				console.log('出错', err);
				if (err.data && err.data.data) {
					uni.showToast({title: err.data.data,icon: 'none',duration:5000});
				}
			});
		},
	},
}

# 四、注册登录成功后续处理

后续处理会用到我们前面讲过的vuex

# ① 新建即时通讯用户模块

新建 /store/modules/chatuser.js

export default{
	// 对应的mapState,在computed中引用导入
	// 类似于data,把全局或者公共部分放在这里
	state:{
		regloginUser:false, //登录注册返回结果存储
	},
	// 异步的方法,在methods引入
	actions:{
		// 注册登录后续操作
		regloginAction({ state }, regloginRes){
			console.log('后续操作传递的数据',regloginRes);
			//登录注册返回结果存储
			state.regloginUser = regloginRes;
			//存储到本地,单独存一下返回结果中的token
			uni.setStorageSync('chatuser',JSON.stringify(regloginRes));
			uni.setStorageSync('chatuser_token',regloginRes.token);
			uni.setStorageSync('chatuser_id',JSON.stringify(regloginRes.id));
		},
	},
}

# ② 将模块引入vuex中

/store/index.js

...
// 导入引入音频模块
...
// 引入chatuser模块
import Chatuser from '@/store/modules/chatuser.js';

export default new Vuex.Store({
	// 对应的mapState,在computed中引用导入
	//state:{
	//	testVuex:'即时通讯',
	//}
	// 模块
	modules:{
		..., // 引入音频模块
		Chatuser, // 引入chatuser模块
	}
});

# ③ 执行登录注册后续操作

/pages/loginCenter/reglogin.js

export default {
	methods: {
		//提交注册数据
		regSubmit() {
			console.log('提交注册数据方法', this.form);
			uni.$u.http.post(this.requestUrl.http + '/api/regChat', this.form).then(res => {
				console.log('服务器返回的注册结果', res);
				//调用vuex的actions中的方法,执行后续操作可用.then
				this.$store.dispatch('regloginAction', res.data.data);
				uni.showToast({title: '提交成功',icon: 'none',duration: 3000});
				// 跳转
				uni.switchTab({
					url: '/pages/wode/wode',
				});
			}).catch(err => {
				console.log('注册出错', err);
				if (err.data && err.data.data) {
					uni.showToast({title: err.data.data,icon: 'none',duration: 5000});
				}
			});

		},
		//提交登录数据
		loginSubmit() {
			console.log('提交登录数据方法', this.form);
			uni.$u.http.post(this.requestUrl.http + '/api/loginChat', this.form).then(res => {
				console.log('服务器返回的登录结果', res);
				//调用vuex的actions中的方法,执行后续操作可用.then
				this.$store.dispatch('regloginAction', res.data.data);
				uni.showToast({title: '登录成功',icon: 'none',duration: 3000});
				// 跳转
				uni.switchTab({
					url: '/pages/wode/wode',
				});
			}).catch(err => {
				console.log('登录出错', err);
				if (err.data && err.data.data) {
					uni.showToast({title: err.data.data,icon: 'none',duration: 5000});
				}
			});
		},
	},
}

# ④ 合并成一个方法

  1. /pages/loginCenter/loginCenter.vue
    // 提交数据
	submit(){
		...
		// 提交数据
		// if(this.page.type == 'login'){
		// 	console.log('提交登录数据',this.form);
		// 	this.loginSubmit();
		// }else if(this.page.type == 'reg'){
		// 	console.log('提交注册数据',this.form);
		// 	this.regSubmit();
		// }
		this.regloginSubmit(this.page.type);
	},
  1. /pages/loginCenter/reglogin.js
	//提交登录注册数据
	regloginSubmit(pagetype){
		console.log('提交数据方法', this.form);
		uni.$u.http.post(this.requestUrl.http + `/api/${pagetype}Chat`, this.form).then(res => {
			console.log('服务器返回的结果', res);
			//调用vuex的actions中的方法,执行后续操作可用.then
			this.$store.dispatch('regloginAction', res.data.data);
			uni.showToast({title: '提交成功',icon: 'none',duration: 3000});
			// 跳转
			uni.switchTab({
				url: '/pages/wode/wode',
			});
		}).catch(err => {
			console.log('出错', err);
			if (err.data && err.data.data) {
				uni.showToast({title: err.data.data,icon: 'none',duration: 5000});
			}
		});
	},

# 五、 即时通讯用户退出登录

# 1. 退出登录接口文档

  1. 接口文档:三、用户退出

  2. 退出登录接口文档 (再次说明)
  1. 请求方式:post [用postman测试]或者[用Apipost测试]
  2. 接口示例:http://127.0.0.1:7001/api/chat/logout(小程序真机测试用自己的ip地址,如http://192.168.2.7:7001/api/chat/logout)

① 本地接口地址(去群里面下载本季课程全部后端代码,然后在本地运行):http://127.0.0.1:7001/api/chat/logout (opens new window) (小程序真机测试用自己的ip地址,如http://192.168.2.7:7001/api/chat/logout)
② 线上接口地址:https://lesson07.51yrc.com/api/chat/logout (opens new window)

  1. 请求参数 [body] -> [none] 无需传参
  2. header头传token

请求参数 [Headers] -> [Key: token, Value: token值]

参数 是否必填 类型 长度 说明
token string 由服务器生成 token令牌,如:eyJhbGciO.....
  1. 返回示例
{
    "msg": "ok",
    "data": true
}

# 2. 登录和退出登录逻辑

在页面 /pages/wode/wode.nvue

methods: {
	clickAvatarNickname(){
		let chatuser = uni.getStorageSync('chatuser');
		if(!chatuser){
			return uni.navigateTo({
				url: '/pages/loginCenter/loginCenter',
			});
		}
		// 打开设置
		uni.navigateTo({
			url:'/pages/setpage/setpage'
		});
	}
}

# 3. 新建设置页并设置

新建页面 /pages/setpage/setpage.vuepages.json进行设置

{
	"path" : "pages/setpage/setpage",
	"style" :
	{
		"navigationBarTitleText" : "设置",
		"navigationStyle": "custom"
	}
}

# 4. 用户退出功能

  1. 在vuex中,在/store/modules/chatuser.js中添加一个用户退出方法
export default{
	// 对应的mapState,在computed中引用导入
	// 类似于data,把全局或者公共部分放在这里
	state:{
	   regloginUser:null, //注册登录后续的操作res结果存储
	},
	// 异步的方法,在methods引入
	actions:{
		//注册登录后续的操作
		...,
		//退出登录
		logoutAction({commit,state}){
			state.regloginUser = null;
			// 删除本地存储
			uni.removeStorageSync('chatuser');
			uni.removeStorageSync('chatuser_id');
			uni.removeStorageSync('chatuser_token');
		},
	},
}
  1. /pages/setpage/setpage.vue
<template>
	<view>
		<!-- 导航栏 -->
		<chat-navbar title="设置" :fixed="true"
		:showPlus="false" :showUser="false"
		:showBack="true" navbarClass="navbar-bgColor"
		:h5WeiXinNeedNavbar="false">
		</chat-navbar>
		<!-- 内容 -->
		<view class="p-3">
			<!-- 退出 -->
			<u-button type="primary" text="退 出 登 录" @click="logout"></u-button>
		</view>
	</view>
</template>

<script>
	import configDataJs from '@/common/mixins/configData.js';
	export default {
		mixins:[configDataJs],
		data() {
			return {
				chatuser_token:'',
			}
		},
		onLoad() {
			this.chatuser_token = uni.getStorageSync('chatuser_token');
			if(!this.chatuser_token){
				uni.switchTab({
					url: '/pages/wode/wode',
				});
			}
		},
		methods: {
			// 退出登录
			logout(){
				uni.$u.http.post(this.requestUrl.http + `/api/chat/logout`, {}, {
					header: {
						token:this.chatuser_token,
					}, 
				}).then(res => {
				    console.log('服务器返回的结果',res);
					this.$store.dispatch('logoutAction');
				    uni.showToast({title:'退出登录成功',icon:'none',duration:3000});
				    uni.switchTab({
				   	   url:'/pages/wode/wode',
				    });
				}).catch(err => {
				    console.log('出错',err);
				    if(err.data && err.data.data){
					   uni.showToast({title:err.data.data,icon:'none',duration:5000});
				    }
				});
			}
		}
	}
</script>

<style>
/* 导航栏背景色 */
.navbar-bgColor{
	background-color: #ededed;
}
</style>

# 六、[重要亮点]关于游客(未登录用户)使用即时通讯的说明和处理

# I. 需求场景说明

相信有同学会好奇,游客(不登录的用户) 也能使用即时通讯吗?难道不是应该登录之后才能使用吗?

说明:正常情况,确实应该是登录之后才能使用,但是有一种以下的场景(当然很多系统将以下的这种场景也是要求登录使用)

场景1:
有公司或者企业,希望他的系统H5(网页)、小程序或者App, 在用户打开或者浏览的时候,用户没有登录,但是用户有问题想咨询他的客服进行聊天,咨询他公司的业务,用户也是只想简单的咨询一下业务,不想为了咨询一下业务,还要去注册登录他们公司的系统,这样的需求场景,我们需要考虑:在用户不登录(游客)的情况下,如何发起聊天?

场景2:
若有的公司系统想开放了客服群聊功能,那么不登录的用户可以进入这个客服群聊,进行咨询业务,也有这样的使用场景

当然以上场景,对于我们的系统开发,是比较苛刻的,会增加我们的开发难度和工作量,但既然是我们自己开发系统,那么我们都考虑一下这些功能,提高我们项目的身价。

# II. 给游客(未登录用户)一个身份标识

当然,我们知道,游客(未登录用户)主要给他开放的是和客服进行业务咨询聊天,因此游客的功能我们应该加以限制和简单化。
我们可以考虑在用户第一次刚打开我们的网页H5端、或者小程序、或者app,此时刚打开肯定没有登录,我们给游客一个标识身份,然后按这种身份进行功能处理。
当然,标识我们可以通过 uni.getSystemInfoSync() 获取设备信息等内容,但是考虑到不同端:网页H5端、小程序、app 不统一的问题,我们可以自己写一个用户标识。

# ① 引入uuid库

关于uni-app如何引入uuid库,打开可以看这个链接: 一、uni-app引入uuid库
考虑到一部分同学没有node基础知识,所以我们采用第二种方式,引入js文件

  1. 打开一个uuid的在线链接,我们这里以v5版本为例,打开链接:https://cdn.jsdelivr.net/npm/uuid@latest/dist/umd/uuidv5.min.js (opens new window)
  2. 将这个js文件保存到项目 /common/js/uuidv5.min.js 位置;

# ② 生成稳定的设备指纹

新建一个js文件,/common/js/deviceId.js,内容如下:

// 引入UUID库 V5
import uuidv5 from '@/common/js/uuidv5.min.js';

// 生成稳定的设备指纹
const generateDeviceFingerprint = () => {
	const systemInfo = uni.getSystemInfoSync();

	// 组合不可变设备特征(跨平台兼容)
	const fingerprintSource = {
		model: systemInfo.model || systemInfo.deviceModel || 'unknown', // 设备型号
		os: systemInfo.osName || systemInfo.platform || 'unknown', // 操作系统
		screen: `${systemInfo.screenWidth || 0}x${systemInfo.screenHeight || 0}`, // 屏幕尺寸
		language: systemInfo.language || 'zh-CN', // 系统语言
		brand: systemInfo.brand || systemInfo.deviceBrand || 'unknown',
		pixelRatio: systemInfo.pixelRatio || systemInfo.devicePixelRatio || 0, // 屏幕
		// 添加更多稳定属性...
	};

	return JSON.stringify(fingerprintSource);
};

// 生成持久化设备ID
export const getDeviceId = () => {
	// 1. 尝试从本地存储获取
	let deviceId = uni.getStorageSync('device_id');

	// 2. 若不存在则生成
	if (!deviceId) {
		const namespace = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; // DNS命名空间
		const fingerprint = generateDeviceFingerprint();
		deviceId = uuidv5(fingerprint, namespace);

		// 持久化存储(仅限支持持久化的平台)
		try {
			uni.setStorageSync('device_id', deviceId);

			// iOS专用:钥匙链存储(跨应用卸载)
			// #ifdef APP-PLUS
			if (plus.os.name === 'iOS') {
				const keychain = plus.ios.import('KeychainItemWrapper');
				const wrapper = keychain.alloc().initWithIdentifierAccessGroup('device_id', null);
				wrapper.setObjectForKey(deviceId, kSecValueData);
				wrapper.release();
			}
			// #endif
		} catch (e) {
			console.error('Storage failed', e);
		}
	}

	// 3. iOS钥匙链恢复逻辑
	// #ifdef APP-PLUS
	if (plus.os.name === 'iOS' && !deviceId) {
		try {
			const keychain = plus.ios.import('KeychainItemWrapper');
			const wrapper = keychain.alloc().initWithIdentifierAccessGroup('device_id', null);
			deviceId = wrapper.objectForKey(kSecValueData);
			wrapper.release();

			if (deviceId) uni.setStorageSync('device_id', deviceId);
		} catch (e) {
			console.error('Keychain read failed', e);
		}
	}
	// #endif

	return deviceId || 'fallback-id'; // 最终回退机制
};

# ③ 【选用】关于②生成的设备指纹确实太稳定了,主要是因为只使用了硬件信息,增加一些动态和可变的因素,提高设备的区分度

js文件,/common/js/deviceId.js,内容如下:
主要优化点:

  • 增强硬件特征:
    • 添加设备唯一ID、IMEI、OAID等系统级标识
    • 增加CPU、内存、存储等硬件信息
    • 平台特有信息(Android ID、iOS identifierForVendor)
  • 保持重装稳定性:
    • 优先使用存储的device_id
    • iOS钥匙链跨应用卸载保持
    • 使用稳定的硬件特征作为基础
  • 提高设备区分度:
    • H5平台:增强Canvas和WebGL指纹
    • APP平台:获取更多系统级标识
    • 小程序平台:环境特定信息
  • 分层指纹策略:
    • 硬件核心特征(高稳定、高区分)
    • 平台增强特征(中等稳定、高区分)
  • 环境补充特征(低稳定、补充区分)
    • 这样既保持了同一设备重装应用时的稳定性,又大大增强了不同设备之间的区分度。
// 引入UUID库 V5
import uuidv5 from '@/common/js/uuidv5.min.js';

// 生成稳定的设备指纹(重点区分不同设备)
const generateDeviceFingerprint = () => {
	const systemInfo = uni.getSystemInfoSync();

	// 核心硬件特征(稳定且区分度高)
	const hardwareFingerprint = {
		// 1. 设备标识相关(高区分度)
		model: systemInfo.model || systemInfo.deviceModel || 'unknown',
		brand: systemInfo.brand || systemInfo.deviceBrand || 'unknown',
		deviceId: systemInfo.deviceId || systemInfo.uuid || 'unknown', // 设备唯一ID
		
		// 2. 系统特征(中等区分度)
		os: systemInfo.osName || systemInfo.platform || 'unknown',
		version: systemInfo.version || systemInfo.system || 'unknown',
		sdkVersion: systemInfo.SDKVersion || 'unknown',
		
		// 3. 屏幕和显示特征(高区分度)
		screen: `${systemInfo.screenWidth || 0}x${systemInfo.screenHeight || 0}`,
		pixelRatio: systemInfo.pixelRatio || systemInfo.devicePixelRatio || 0,
		windowWidth: systemInfo.windowWidth || 0,
		windowHeight: systemInfo.windowHeight || 0,
		statusBarHeight: systemInfo.statusBarHeight || 0,
		
		// 4. 硬件能力特征(中等区分度)
		cpu: systemInfo.cpuName || systemInfo.processor || 'unknown',
		abi: systemInfo.abi || systemInfo.cpuArchitecture || 'unknown',
		memory: systemInfo.memorySize || systemInfo.ram || 'unknown',
		storage: systemInfo.storageSize || 'unknown',
		
		// 5. 区域和语言(低区分度,但增加组合复杂度)
		language: systemInfo.language || 'zh-CN',
		region: systemInfo.region || systemInfo.country || 'CN',
		timezone: (new Date()).getTimezoneOffset().toString(),
		
		// 6. 应用环境特征
		appVersion: systemInfo.appVersion || '1.0.0',
		appName: systemInfo.appName || 'unknown'
	};

	// 平台增强特征
	const platformEnhanced = getPlatformEnhancedFeatures(systemInfo);
	
	return JSON.stringify({
		...hardwareFingerprint,
		...platformEnhanced
	});
};

// 获取平台增强特征
const getPlatformEnhancedFeatures = (systemInfo) => {
	const enhanced = {};
	
	// APP平台增强
	// #ifdef APP-PLUS
	enhanced.deviceType = systemInfo.deviceType || 'unknown';
	enhanced.rom = systemInfo.rom || 'unknown';
	enhanced.host = systemInfo.host || 'unknown';
	enhanced.imei = systemInfo.imei || 'unknown'; // 设备IMEI
	enhanced.oaid = systemInfo.oaid || 'unknown'; // 移动安全联盟OAID
	enhanced.serial = systemInfo.serial || 'unknown'; // 设备序列号
	
	// 获取更多Android特有信息
	if (plus.os.name === 'Android') {
		enhanced.androidId = systemInfo.androidId || 'unknown';
		enhanced.board = systemInfo.board || 'unknown';
		enhanced.bootloader = systemInfo.bootloader || 'unknown';
		enhanced.device = systemInfo.device || 'unknown';
		enhanced.fingerprint = systemInfo.fingerprint || 'unknown';
		enhanced.hardware = systemInfo.hardware || 'unknown';
		enhanced.manufacturer = systemInfo.manufacturer || 'unknown';
		enhanced.product = systemInfo.product || 'unknown';
	}
	
	// iOS特有信息
	if (plus.os.name === 'iOS') {
		enhanced.identifierForVendor = systemInfo.identifierForVendor || 'unknown';
	}
	// #endif
	
	// H5平台增强
	// #ifdef H5
	enhanced.userAgent = navigator.userAgent || 'unknown';
	enhanced.hardwareConcurrency = navigator.hardwareConcurrency || 0;
	enhanced.deviceMemory = navigator.deviceMemory || 0;
	enhanced.maxTouchPoints = navigator.maxTouchPoints || 0;
	
	// 获取更多H5特征
	try {
		// 屏幕色彩深度
		enhanced.colorDepth = screen.colorDepth || 0;
		enhanced.pixelDepth = screen.pixelDepth || 0;
		
		// 时区信息
		enhanced.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'unknown';
		
		// 语言信息
		enhanced.languages = navigator.languages ? navigator.languages.join(',') : (navigator.language || 'unknown');
	} catch (e) {
		console.log('H5 feature detection failed', e);
	}
	// #endif
	
	// 微信小程序增强
	// #ifdef MP-WEIXIN
	enhanced.environment = systemInfo.environment || 'unknown';
	enhanced.host = systemInfo.host || 'unknown';
	enhanced.appKey = systemInfo.appKey || 'unknown';
	enhanced.appBaseLibVersion = systemInfo.appBaseLibVersion || 'unknown';
	// #endif
	
	return enhanced;
};

// 生成Canvas指纹(H5平台,稳定且区分度高)
const generateStableCanvasFingerprint = () => {
	// #ifdef H5
	try {
		const canvas = document.createElement('canvas');
		const ctx = canvas.getContext('2d');
		
		canvas.width = 240;
		canvas.height = 60;
		
		// 绘制复杂图形,增加指纹区分度
		ctx.textBaseline = 'alphabetic';
		ctx.fillStyle = '#f60';
		ctx.fillRect(0, 0, canvas.width, canvas.height);
		ctx.fillStyle = '#069';
		
		// 使用不同字体和样式
		ctx.font = 'bold 14px "Arial", "Microsoft YaHei", sans-serif';
		ctx.fillText('Device Fingerprint Stability Test 设备指纹稳定性测试', 4, 20);
		
		ctx.font = 'italic 12px "Times New Roman", serif';
		ctx.fillText('📱💻🖥️ Stability & Uniqueness 稳定性和唯一性', 4, 40);
		
		// 添加图形元素
		ctx.strokeStyle = 'rgba(102, 204, 0, 0.7)';
		ctx.beginPath();
		ctx.arc(200, 30, 15, 0, Math.PI * 2);
		ctx.stroke();
		
		return canvas.toDataURL();
	} catch (e) {
		return 'canvas_unsupported';
	}
	// #endif
	
	return 'non_h5_platform';
};

// 生成WebGL指纹(H5平台)
const generateWebGLFingerprint = () => {
	// #ifdef H5
	try {
		const canvas = document.createElement('canvas');
		const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
		
		if (!gl) return 'webgl_unsupported';
		
		const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
		if (debugInfo) {
			const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || '';
			const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || '';
			return `${vendor}-${renderer}`;
		}
		
		// 获取WebGL基本参数
		const glVersion = gl.getParameter(gl.VERSION) || '';
		const glShadingLanguageVersion = gl.getParameter(gl.SHADING_LANGUAGE_VERSION) || '';
		const glVendor = gl.getParameter(gl.VENDOR) || '';
		const glRenderer = gl.getParameter(gl.RENDERER) || '';
		
		return `${glVersion}-${glShadingLanguageVersion}-${glVendor}-${glRenderer}`;
	} catch (e) {
		return 'webgl_error';
	}
	// #endif
	
	return 'non_h5_platform';
};

// 生成增强的设备指纹(保持重装稳定性,增强设备区分度)
const generateEnhancedFingerprint = () => {
	const systemInfo = uni.getSystemInfoSync();
	
	// 基础硬件指纹(稳定核心)
	const baseHardwareFingerprint = generateDeviceFingerprint();
	
	// 平台特定增强指纹
	const platformFingerprints = {
		// H5平台使用Canvas和WebGL
		// #ifdef H5
		canvasFP: generateStableCanvasFingerprint(),
		webglFP: generateWebGLFingerprint(),
		// #endif
		
		// 所有平台都有的增强特征
		performanceMark: (performance && performance.now) ? 
			Math.floor(performance.now() / 1000).toString() : // 降低精度保持稳定
			Math.floor(Date.now() / 60000).toString() // 按分钟计算
	};
	
	// 组合指纹,硬件特征权重高,平台特征作为补充
	const combinedFingerprint = {
		hardware: baseHardwareFingerprint,
		platform: platformFingerprints,
		version: 'v2.0' // 指纹版本,便于后续升级
	};
	
	return JSON.stringify(combinedFingerprint);
};

// 生成持久化设备ID
export const getDeviceId = () => {
	// 1. 优先从本地存储获取(保持重装稳定性)
	let deviceId = uni.getStorageSync('device_id');

	// 2. 若不存在则生成新ID
	if (!deviceId) {
		const namespace = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
		
		// 使用增强的设备指纹
		const fingerprint = generateEnhancedFingerprint();
		deviceId = uuidv5(fingerprint, namespace);

		// 持久化存储
		try {
			uni.setStorageSync('device_id', deviceId);

			// iOS专用:钥匙链存储(跨应用卸载保持稳定)
			// #ifdef APP-PLUS
			if (plus.os.name === 'iOS') {
				const keychain = plus.ios.import('KeychainItemWrapper');
				const wrapper = keychain.alloc().initWithIdentifierAccessGroup('device_id', null);
				wrapper.setObjectForKey(deviceId, kSecValueData);
				wrapper.release();
			}
			
			// Android专用:使用系统标识增强稳定性
			if (plus.os.name === 'Android') {
				// 尝试获取Android ID等系统级标识
				const systemInfo = uni.getSystemInfoSync();
				if (systemInfo.androidId) {
					uni.setStorageSync('android_backup_id', systemInfo.androidId);
				}
			}
			// #endif
		} catch (e) {
			console.error('Storage failed', e);
		}
	}

	// 3. iOS钥匙链恢复逻辑(重装应用时恢复)
	// #ifdef APP-PLUS
	if (plus.os.name === 'iOS' && !deviceId) {
		try {
			const keychain = plus.ios.import('KeychainItemWrapper');
			const wrapper = keychain.alloc().initWithIdentifierAccessGroup('device_id', null);
			deviceId = wrapper.objectForKey(kSecValueData);
			wrapper.release();

			if (deviceId) {
				uni.setStorageSync('device_id', deviceId);
			}
		} catch (e) {
			console.error('Keychain read failed', e);
		}
	}
	// #endif

	// 4. 最终回退机制
	if (!deviceId) {
		// 使用简化但仍有区分度的回退方案
		const systemInfo = uni.getSystemInfoSync();
		const fallbackSource = {
			model: systemInfo.model || 'unknown',
			brand: systemInfo.brand || 'unknown', 
			screen: `${systemInfo.screenWidth || 0}x${systemInfo.screenHeight || 0}`,
			os: systemInfo.osName || 'unknown'
		};
		deviceId = 'fallback-' + uuidv5(JSON.stringify(fallbackSource), namespace);
		
		// 存储回退ID
		try {
			uni.setStorageSync('device_id', deviceId);
		} catch (e) {
			console.error('Fallback storage failed', e);
		}
	}

	return deviceId;
};

// 获取设备指纹详情(用于调试和分析)
export const getDeviceFingerprintDetail = () => {
	return {
		hardware: generateDeviceFingerprint(),
		enhanced: generateEnhancedFingerprint(),
		platform: getPlatformEnhancedFeatures(uni.getSystemInfoSync())
	};
};

# ④ 执行生成游客标识(可以是应用打开就执行或者某个页面去执行)

我们一般设定应用打开就执行,在 App.vue 中添加以下代码:

...

<script>
	import { getDeviceId } from '@/common/js/deviceId.js';
	export default {
		onLaunch: function() {
			...
			// 生成持久化设备ID
			getDeviceId();
		},
		onShow: function() {
			...
		},
		onHide: function() {
			...
		}
	}
</script>

# III. 系统给游客用户注册身份便于游客和客服进行即时通讯

# 1. 系统给游客用户注册身份接口说明

  1. 关于给游客用户注册身份的接口说明:十三、给游客用户注册身份

# 2. 新建js文件完成系统给游客用户注册身份

新建一个js文件,/pages/loginCenter/visitor.js,内容如下:

import { getDeviceId } from '@/common/js/deviceId.js';
import { requestUrl, visitorSalt } from '@/common/mixins/configData.js';
import sha256 from '@/common/js/sha256.min.js';


// 导出独立的方法,而不是vue组件混入
//给游客一个身份
export const registerGuest = async function(store){
	const deviceId = getDeviceId();
	console.log('设备id', deviceId);
	
	// 给游客注册一个身份
	const timestamp = Date.now();
	
	// 生成安全签名(防篡改)
	// SHA256签名
	const generateSign = async (params)=> {
		const sortedStr = Object.keys(params).sort().map(k => `${k}=${params[k]}`).join('&');
		// 使用sha256加密
		return sha256(sortedStr + 'APP_SECRET'); // 真实项目中替换为动态密钥
	};
	
	const sign = await generateSign({
		deviceId,
		timestamp,
		salt: visitorSalt, // 固定盐值(混淆用)
	});
	
	/*
	const sign = sha256(
		`deviceId=${deviceId}&salt=${this.visitorSalt}&timestamp=${timestamp}APP_SECRET`
	);
	*/
   
    const systemInfo = uni.getSystemInfoSync();
	
	try {
		const res = await uni.request({
			url: requestUrl.http + '/api/visitorRegister',
			method: 'POST',
			header: {
				'X-Security-Sign': sign,
			},
			data: {
				deviceId,
				timestamp,
				//以下参数选填
				uniplatform: systemInfo.uniPlatform || 'unknown',
				devicemodel: systemInfo.model || systemInfo.deviceModel || 'unknown',
				deviceos: systemInfo.osName || systemInfo.platform || 'unknown',
				devicebrand: systemInfo.brand || systemInfo.deviceBrand || 'unknown',
			}
		});
	
		console.log('服务器响应:', res);
		if (res.data.msg == 'ok') {
		  console.log('成功', res.data);
		  //调用vuex里面的actions中的方法,执行后续操作,连缀用.then
		  // this.$store.dispatch('regloginAction',res.data.data);
		  store.dispatch('regloginAction',res.data.data);
		  return res.data.data;
		} else {
		  // console.warn('失败',res.data);
		  uni.showToast({title: '请求过于频繁,请1个小时后再试', icon:'none',duration:1000*60*60});
		  return null;
		}
		
	} catch (e) {
		console.error('请求失败:', e);
		return null;
	}
	
}


//默认导出(保持原有结构)
export default {
	methods: {
		//给游客一个身份
		registerGuest: function(){
			return registerGuest(this.$store);
		},
		// 其他方法...
	}
}

# 3. configData.js文件又混入改成导出方便在App.vue中直接使用

说明:在App.vue中使用混入mixins,会出现兼容问题,所以我们采用导出方式,直接在App.vue中使用
在文件 /common/mixins/configData.js 中,内容如下:

/*
export default{
	data(){
		return {
			//请求地址
			requestUrl:{
				// 本地服务器测试地址http://127.0.0.1:7001
				// 小程序如果你用真机调试,那么你用ip地址
				http:'http://192.168.2.7:7001',
				// 线上服务器测试地址https://lesson07.51yrc.com
				// http:'https://lesson07.51yrc.com',
			},
			// 游客注册的盐值
			visitorSalt:'45ncashdaksh2!#@3nxjdas*_259',
		}
	},
}
*/

// 导出常量请求地址
export const requestUrl = {
   // 本地服务器测试地址http://127.0.0.1:7001
   // 小程序如果你用真机调试,那么你用ip地址
   http:'http://192.168.2.7:7001',
   // 线上服务器测试地址https://lesson07.51yrc.com
   // http:'https://lesson07.51yrc.com',
};
// 游客注册的盐值
export const visitorSalt = '45ncashdaksh2!#@3nxjdas*_259';

# 4. sha256加密文件

  1. 关于sha256加密,我们文档有专门的介绍,大家可以看这个:二、uni-app引入加密库(如CryptoJS)来实现SHA256加密
  2. 我们这里采用直接引入的方式,新建 /common/js/sha256.min.js文件,然后把代码贴进来。(大家也可以去下载本节课的课件,课件里面也有sha256加密文件)

# 5. 涉及vuex后续操作,将游客身份存储到本地(类似完成了一次用户登录,只不过用户是游客)

在文件 /store/modules/chatuser.js

export default{
	// 对应的mapState,在computed中引用导入
	// 类似于data,把全局或者公共部分放在这里
	state:{
	   regloginUser:null, //注册登录后续的操作res结果存储
	},
	// 异步的方法,在methods引入
	actions:{
		//注册登录后续的操作
		regloginAction({commit,state}, regloginRes){
			console.log('注册登录后续的操作传递的数据',regloginRes);
			//注册登录后续的操作res结果存储
			state.regloginUser = regloginRes;
			// 存储到本地,单独存一下token和id
			uni.setStorageSync('chatuser', JSON.stringify(regloginRes));
			uni.setStorageSync('chatuser_id', JSON.stringify(regloginRes.id));
			uni.setStorageSync('chatuser_token', regloginRes.token);
		},
		// 用户退出登录
		logoutAction({commit,state}){
			state.regloginUser = null;
			// 删除本地存储
			uni.removeStorageSync('chatuser');
			uni.removeStorageSync('chatuser_id');
			uni.removeStorageSync('chatuser_token');
		},
		// 初始化登录注册状态(避免刷新页面state没有数据)
		initChatuserAction({commit,state}){
			console.log('初始化登录注册状态',state);
			// 给state赋值
			let user = uni.getStorageSync('chatuser') ?
			JSON.parse(uni.getStorageSync('chatuser')) : '';
			if(user){
				state.regloginUser = user;
				console.log('初始化登录注册状态',state);
			}
		},
	},
}

# 6. 项目启动给游客用户注册身份

App.vue 中添加以下代码:

...

<script>
	// import { getDeviceId } from '@/common/js/deviceId.js';
	import visitorJs from '@/pages/loginCenter/visitor.js';
	export default {
		mixins:[visitorJs],
		onLaunch: function() {
			...
			// 生成持久化设备ID
			// getDeviceId();
			//给游客一个身份
			let user = uni.getStorageSync('chatuser') ? 
			JSON.parse(uni.getStorageSync('chatuser')) : '';
			if(!user) this.registerGuest();
			//初始化登录注册状态(避免刷新页面state没有数据)
			this.$store.dispatch('initChatuserAction');
			
		},
		onShow: function() {
			...
		},
		onHide: function() {
			...
		}
	}
</script>

# Ⅳ、游客用户正式注册和登录身份

# 1. 游客用户正式注册和登录身份接口说明

具体查看接口文档:十四、游客用户自己正式注册身份

# 2. 在页面 /pages/wode/wode.nvue

<template>
	<view>
		<!-- 头像昵称 -->
		<view ...>
			<view ...>
				<!-- 头像 -->
				<view class="ml-1 mr-3">
					<u--image :src="avatarShow"
					mode="widthFix" radius="20rpx"                  
					width="120rpx" height="120rpx"></u--image>
				</view>
				<!-- 昵称 -->
				<view class="flex flex-1 flex-column justify-center">
					<text class="font-lg font-weight-bold">{{nicknameShow}}</text>
					<view class="flex justify-between mt-2">
						<text class="font text-light-muted">{{nicknameNextShow}}</text>
						<text class="font-lg text-light-muted iconfont">&#xe657;</text>
					</view>
				</view>
			</view>
		</view>
	    ...
	</view>
</template>

<script>
	import {mapState,mapGetters,mapMutations,mapActions} from 'vuex';
	export default {
		...,
		onLoad() {
			this.statusBarHeight = uni.getSystemInfoSync().statusBarHeight;
			this.fixedHeight = this.statusBarHeight + uni.upx2px(90);
			
			console.log(this.user);
		},
		computed:{
			...mapState({
				user:state => state.Chatuser.regloginUser,
			}),
			...,
			//头像
			avatarShow(){
				return this.user && this.user.avatar;
			},
			//账号
			nicknameShow(){
				let str = ``;
				if(this.user){
					if(this.user.role == 'user'){
						str = this.user.nickname || this.user.username;
					}else if(this.user.role == 'visitor'){
						str = `登 录`;
					}
				}
				return str;
			},
			//账号下面小字
			nicknameNextShow(){
				let str = ``;
				if(this.user){
					if(this.user.role == 'user'){
						str = `账号:${this.user.username}`;
					}else if(this.user.role == 'visitor'){
						str = `登录获取更多服务`;
					}
				}
				return str;
			},
		},
		methods: {
			clickAvatarNickname(){
				if(!this.user || this.user.role == 'visitor'){
					return uni.navigateTo({
						url: '/pages/loginCenter/loginCenter',
					});
				}
				// 打开设置页
				uni.navigateTo({
					url: '/pages/setpage/setpage',
				});
			}
		}
	}
</script>

# 3. 在页面 /pages/loginCenter/loginCenter.vue

注意 import configDataJs from '@/common/mixins/configData.js'; 引入方式

<script>
	// import configDataJs from '@/common/mixins/configData.js';
	import regloginJs from '@/pages/loginCenter/reglogin.js';
	export default {
		mixins:[regloginJs],
		...
	}
</script>

# 4. 在文件 /pages/loginCenter/reglogin.js

import { requestUrl, visitorSalt } from '@/common/mixins/configData.js';
import sha256 from '@/common/js/sha256.min.js';
export default {
	methods: {
		//提交登录注册数据
		regloginSubmit(pagetype) {
			this.form.deviceId = uni.getStorageSync('device_id');
			this.form.timestamp = Date.now();
			console.log('提交数据方法', this.form);
			// 生成安全签名(防篡改)
			const sign = this.generateSign({
				deviceId: this.form.deviceId,
				timestamp: this.form.timestamp,
				salt: visitorSalt, // 固定盐值(混淆用)
			});
			//提交
			uni.$u.http.post(requestUrl.http + `/api/visitor${pagetype}Chat`, this.form, {
				header: {
					'X-Security-Sign': sign,
				},
			}).then(res => {
				console.log('服务器返回的结果', res);
				//调用vuex里面的actions中的方法,执行后续操作,连缀用.then
				this.$store.dispatch('regloginAction', res.data.data);
				uni.showToast({title: '提交成功',icon: 'none',duration: 3000});
				setTimeout(() => {
					uni.switchTab({
						url: '/pages/wode/wode',
					});
				}, 1000);

			}).catch(err => {
				console.log('出错', err);
				if (err.data && err.data.data) {
					uni.showToast({title: err.data.data,icon: 'none',duration: 5000});
				}
			});
		},
		// SHA256签名
		generateSign(params) {
			const sortedStr = Object.keys(params).sort().map(k => `${k}=${params[k]}`).join('&');
			// 使用sha256加密
			return sha256(sortedStr + 'APP_SECRET'); // 真实项目中替换为动态密钥
		},
	},
}

# 5. 在设置页 /pages/setpage/setpage.vue

<template>
	<view>
		<!-- 导航栏 -->
		<chat-navbar title="设置" :fixed="true" :showPlus="false" :showUser="false" :showBack="true"
			navbarClass="navbar-bgColor" :h5WeiXinNeedNavbar="false">
		</chat-navbar>
		<!-- 内容 -->
		<view class="p-3">
			<!-- 退出 -->
			<u-button type="primary" text="退 出 登 录" @click="logout"></u-button>
		</view>
	</view>
</template>

<script>
	import { requestUrl } from '@/common/mixins/configData.js';
	import visitorJs from '@/pages/loginCenter/visitor.js';
	export default {
		mixins: [visitorJs],
		data() {
			return {
				chatuser_token: '',
			}
		},
		onLoad() {
			this.chatuser_token = uni.getStorageSync('chatuser_token');
			if (!this.chatuser_token) {
				return uni.switchTab({
					url: '/pages/wode/wode'
				});
			}
		},
		methods: {
			// 退出登录
			logout() {
				uni.$u.http.post(requestUrl.http + `/api/chat/logout`, {}, {
					header: {
						token: this.chatuser_token,
					},
				}).then(res => {
					console.log('服务器返回的结果', res);
					//调用vuex里面的actions中的方法,执行用户退出登录
					this.$store.dispatch('logoutAction').then(() => {
						// 退出登录后重新以游客身份浏览
						this.registerGuest().then(() => {
							//初始化登录注册状态(避免刷新页面state没有数据)
							this.$store.dispatch('initChatuserAction');
						});
					});
					uni.showToast({ title: '退出登录成功', icon: 'none',duration: 3000});
					setTimeout(() => {
						uni.switchTab({
							url: '/pages/wode/wode',
						});
					}, 2000);
				}).catch(err => {
					console.log('出错', err);
					if (err.data && err.data.data) {
						uni.showToast({ title: err.data.data,icon: 'none',duration: 5000});
					}
				});
			}
		}
	}
</script>

<style>
	/* 导航栏背景色 */
	.navbar-bgColor {
		background-color: #ededed;
	}
</style>

# 6. 关于统计字段的说明

  1. 我们数据库中有几个统计字段,大家可以根据需求添加这些字段,具体的:
  1. 表字段:一、 user表字段设计
  2. 可查看接口文档:十三、给游客用户注册身份
  1. 这几个统计字段是在给未登录注册用户 注册游客身份的时候,我们可以进行处理,具体: 在 /pages/loginCenter/visitor.js文件:
import { getDeviceId } from '@/common/js/deviceId.js';
import { requestUrl, visitorSalt } from '@/common/mixins/configData.js';
import sha256 from '@/common/js/sha256.min.js';


export default {
	methods: {
		//给游客一个身份
		async registerGuest() {
			const deviceId = getDeviceId();
			console.log('设备id', deviceId);

			// 给游客注册一个身份
			const timestamp = Date.now();
			// 生成安全签名(防篡改)
			
			const sign = await this.generateSign({
				deviceId,
				timestamp,
				salt: visitorSalt, // 固定盐值(混淆用)
			});
		    /*
			const sign = sha256(
				`deviceId=${deviceId}&salt=${this.visitorSalt}&timestamp=${timestamp}APP_SECRET`
			);
			*/
            const systemInfo = uni.getSystemInfoSync();
			try {
				const res = await uni.request({
					url: requestUrl.http + '/api/visitorRegister',
					method: 'POST',
					header: {
						'X-Security-Sign': sign,
					},
					data: {
						deviceId,
						timestamp,
						//以下参数选填
						uniplatform: systemInfo.uniPlatform || 'unknown',
						devicemodel: systemInfo.model || systemInfo.deviceModel || 'unknown',
						deviceos: systemInfo.osName || systemInfo.platform || 'unknown',
						devicebrand: systemInfo.brand || systemInfo.deviceBrand || 'unknown',
					}
				});

				console.log('服务器响应:', res);
				if (res.data.msg == 'ok') {
				  console.log('成功', res.data);
				  //调用vuex里面的actions中的方法,执行后续操作,连缀用.then
				  this.$store.dispatch('regloginAction',res.data.data);
				} else {
				  console.warn('失败',res.data);
				}
				return true;
			} catch (e) {
				console.error('请求失败:', e);
				return null;
			}
		},
		// SHA256签名
		async generateSign(params) {
			const sortedStr = Object.keys(params).sort().map(k => `${k}=${params[k]}`).join('&');
			// 使用sha256加密
			return sha256(sortedStr + 'APP_SECRET'); // 真实项目中替换为动态密钥
		},
	}
}

# 七、用户(好友)相关功能处理

具体查看:用户(好友)相关功能处理

更新时间: 2025年10月30日星期四下午5点18分