feat: add registration page for new miniprogram users

- New register page with chooseAvatar, nickname input (random default),
  and required phone number authorization via getPhoneNumber
- Login simplified: removed deprecated wx.getUserProfile, checks
  is_new_user flag to redirect new users to registration
- Server login response now includes is_new_user field
这个提交包含在:
Guoguo
2026-05-06 06:12:34 -07:00
父节点 5cc41f0f10
当前提交 563e515bce
修改 9 个文件,包含 301 行新增92 行删除
+1
查看文件
@@ -2,6 +2,7 @@
"pages": [
"pages/index/index",
"pages/login/login",
"pages/register/register",
"pages/scan/scan",
"pages/ble-connect/ble-connect",
"pages/bind-success/bind-success",
+6 -62
查看文件
@@ -1,15 +1,12 @@
var app = getApp()
var http = require('../../utils/request')
Page({
data: {
statusBarHeight: 44,
loading: false,
userProfile: null
loading: false
},
onLoad: function () {
var app = getApp()
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
},
@@ -17,73 +14,20 @@ Page({
var self = this
self.setData({ loading: true })
self.getWechatProfile().then(function (profile) {
self.setData({ userProfile: profile })
return app.doLogin()
}).then(function () {
return self.saveProfile()
}).then(function () {
app.doLogin().then(function (data) {
self.setData({ loading: false })
if (data && data.is_new_user) {
wx.redirectTo({ url: '/pages/register/register' })
} else {
wx.switchTab({ url: '/pages/index/index' })
}
}).catch(function (err) {
self.setData({ loading: false })
wx.showToast({ title: err.message || '登录失败', icon: 'none' })
})
},
getWechatProfile: function () {
return new Promise(function (resolve) {
if (!wx.getUserProfile) {
resolve(null)
return
}
wx.getUserProfile({
desc: '用于完善会员资料',
success: function (res) {
resolve(res.userInfo || null)
},
fail: function () {
resolve(null)
}
})
})
},
saveProfile: function () {
var profile = this.data.userProfile || {}
if (!profile.nickName && !profile.avatarUrl) return Promise.resolve()
return this.updateProfile({
nickname: profile.nickName || '',
avatar: profile.avatarUrl || '',
gender: profile.gender || 0
})
},
updateProfile: function (payload) {
return http.put('/api/v1/user/profile', payload).then(function () {
return app.loadProfile()
})
},
onAgreement: function () {
wx.showModal({ title: '提示', content: '用户协议和隐私政策内容建设中', showCancel: false })
},
onGetPhoneNumber: function (e) {
var self = this
if (!e.detail || !e.detail.code) {
wx.showToast({ title: '已跳过手机号授权', icon: 'none' })
return
}
self.setData({ loading: true })
app.doLogin().then(function () {
return http.post('/api/v1/user/phone', { code: e.detail.code })
}).then(function () {
self.setData({ loading: false })
wx.switchTab({ url: '/pages/index/index' })
}).catch(function (err) {
self.setData({ loading: false })
wx.showToast({ title: err.message || '手机号授权失败', icon: 'none' })
})
}
})
+3 -9
查看文件
@@ -1,7 +1,7 @@
<view class="page">
<view class="page-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;">
<view class="page-header-title">光子美容仪</view>
<view class="page-header-subtitle">微信授权登录</view>
<view class="page-header-subtitle">微信登录</view>
</view>
<view class="page-content">
@@ -9,17 +9,11 @@
<view class="flower-area">
<text class="flower">🌸</text>
<view class="desc">授权获取微信昵称和头像</view>
<view class="desc">用于完善会员资料</view>
<view class="desc">登录后即可使用全部功能</view>
</view>
<button class="login-btn" loading="{{loading}}" bindtap="onLogin" disabled="{{loading}}">
微信授权登录
</button>
<!-- 手机号授权已预留。需要时展示该按钮并隐藏上面的普通登录按钮。 -->
<button wx:if="{{false}}" class="phone-btn disabled-phone-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber" disabled="{{loading}}">
授权手机号登录(预留)
微信登录
</button>
<view class="agreement" bindtap="onAgreement">登录即表示同意《用户协议》和《隐私政策》</view>
-20
查看文件
@@ -46,26 +46,6 @@
border: none;
}
.phone-btn {
display: block;
width: 100%;
padding: 28rpx;
background: #ffffff;
color: #E6508C;
border: 2rpx solid #E6508C;
border-radius: 20rpx;
font-size: 30rpx;
text-align: center;
margin-top: 24rpx;
}
.phone-btn::after {
border: none;
}
.disabled-phone-btn {
display: none;
}
.agreement {
text-align: center;
+102
查看文件
@@ -0,0 +1,102 @@
var app = getApp()
var http = require('../../utils/request')
var DEFAULT_AVATAR = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI9t2NHfLvvMiaiaCJQn3KP2P0vAHp1QBP0piavhGfhKCARlGd6z1EbR58ibJXKCp5WVlNnojwA/640'
function randomNickname() {
var digits = ''
for (var i = 0; i < 4; i++) {
digits += Math.floor(Math.random() * 10)
}
return '用户' + digits
}
Page({
data: {
statusBarHeight: 44,
loading: false,
avatarUrl: DEFAULT_AVATAR,
nickname: '',
phone: ''
},
onLoad: function () {
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
nickname: randomNickname()
})
},
onChooseAvatar: function (e) {
if (e.detail && e.detail.avatarUrl) {
this.setData({ avatarUrl: e.detail.avatarUrl })
}
},
onNicknameInput: function (e) {
this.setData({ nickname: e.detail.value || '' })
},
onNicknameBlur: function (e) {
var val = (e.detail.value || '').trim()
if (!val) {
this.setData({ nickname: randomNickname() })
} else {
this.setData({ nickname: val })
}
},
onGetPhoneNumber: function (e) {
var self = this
if (!e.detail || !e.detail.code) {
wx.showToast({ title: '需要授权手机号才能完成注册', icon: 'none' })
return
}
self.setData({ loading: true })
http.post('/api/v1/user/phone', { code: e.detail.code }).then(function (data) {
self.setData({
loading: false,
phone: data.phone || data.pure_phone_number || '已授权'
})
wx.showToast({ title: '手机号授权成功', icon: 'success' })
}).catch(function (err) {
self.setData({ loading: false })
wx.showToast({ title: err.message || '手机号授权失败', icon: 'none' })
})
},
onSubmit: function () {
var self = this
var nickname = (self.data.nickname || '').trim()
var avatarUrl = self.data.avatarUrl
if (!self.data.phone) {
wx.showToast({ title: '请先授权手机号', icon: 'none' })
return
}
if (!nickname) {
wx.showToast({ title: '请输入昵称', icon: 'none' })
return
}
self.setData({ loading: true })
http.put('/api/v1/user/profile', {
nickname: nickname,
avatar: avatarUrl !== DEFAULT_AVATAR ? avatarUrl : ''
}).then(function () {
return app.loadProfile()
}).then(function () {
self.setData({ loading: false })
wx.switchTab({ url: '/pages/index/index' })
}).catch(function (err) {
self.setData({ loading: false })
wx.showToast({ title: err.message || '保存失败', icon: 'none' })
})
},
onAgreement: function () {
wx.showModal({ title: '提示', content: '用户协议和隐私政策内容建设中', showCancel: false })
}
})
+4
查看文件
@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "完善资料",
"navigationStyle": "custom"
}
+43
查看文件
@@ -0,0 +1,43 @@
<view class="page">
<view class="page-header page-header-pink" style="padding-top: {{statusBarHeight + 24}}px;">
<view class="page-header-title">光子美容仪</view>
<view class="page-header-subtitle">完善个人资料</view>
</view>
<view class="page-content">
<view class="section-title">设置头像</view>
<view class="avatar-area">
<button class="avatar-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
<image class="avatar-img" src="{{avatarUrl}}" mode="aspectFill"></image>
<view class="avatar-edit-hint">点击更换</view>
</button>
</view>
<view class="section-title">设置昵称</view>
<view class="input-wrap">
<input type="nickname" class="weui-input nickname-input" value="{{nickname}}" bindinput="onNicknameInput" bindblur="onNicknameBlur" placeholder="请输入昵称" />
</view>
<view class="section-title">授权手机号</view>
<view class="phone-area">
<block wx:if="{{phone}}">
<view class="phone-done">
<text class="phone-icon">&#x2713;</text>
<text class="phone-text">{{phone}}</text>
</view>
</block>
<block wx:else>
<button class="phone-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber" disabled="{{loading}}">
授权手机号
</button>
<view class="phone-hint">手机号用于接收服务通知,必须授权</view>
</block>
</view>
<button class="submit-btn" bindtap="onSubmit" loading="{{loading}}" disabled="{{loading}}">
完成注册
</button>
<view class="agreement" bindtap="onAgreement">注册即表示同意《用户协议》和《隐私政策》</view>
</view>
</view>
+138
查看文件
@@ -0,0 +1,138 @@
.page {
min-height: 100vh;
background: #ffffff;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #333333;
margin-bottom: 20rpx;
margin-top: 36rpx;
}
.section-title:first-child {
margin-top: 0;
}
.avatar-area {
display: flex;
justify-content: center;
margin-bottom: 12rpx;
}
.avatar-btn {
background: none;
border: none;
padding: 0;
margin: 0;
line-height: normal;
display: flex;
flex-direction: column;
align-items: center;
}
.avatar-btn::after {
border: none;
}
.avatar-img {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
border: 4rpx solid #f0f0f0;
background: #f5f5f5;
}
.avatar-edit-hint {
font-size: 24rpx;
color: #999999;
margin-top: 12rpx;
}
.input-wrap {
background: #f5f5f5;
border-radius: 16rpx;
padding: 24rpx 28rpx;
margin-bottom: 12rpx;
}
.nickname-input {
font-size: 30rpx;
color: #333333;
height: 48rpx;
line-height: 48rpx;
}
.phone-area {
margin-bottom: 12rpx;
}
.phone-btn {
display: block;
width: 100%;
padding: 28rpx;
background: #ffffff;
color: #E6508C;
border: 2rpx solid #E6508C;
border-radius: 20rpx;
font-size: 30rpx;
text-align: center;
}
.phone-btn::after {
border: none;
}
.phone-hint {
font-size: 24rpx;
color: #999999;
text-align: center;
margin-top: 12rpx;
}
.phone-done {
display: flex;
align-items: center;
justify-content: center;
background: #f0fff4;
border: 2rpx solid #52c41a;
border-radius: 20rpx;
padding: 28rpx;
}
.phone-icon {
color: #52c41a;
font-size: 36rpx;
margin-right: 16rpx;
}
.phone-text {
font-size: 30rpx;
color: #333333;
}
.submit-btn {
display: block;
width: 100%;
padding: 32rpx;
background: #E6508C;
color: #ffffff;
border: none;
border-radius: 20rpx;
font-size: 32rpx;
font-weight: 600;
text-align: center;
margin-top: 60rpx;
}
.submit-btn::after {
border: none;
}
.agreement {
text-align: center;
font-size: 24rpx;
color: #999999;
margin-top: 32rpx;
}
+3
查看文件
@@ -16,7 +16,9 @@ router.post('/auth/login', wrap(async (req, res) => {
const session = await code2Session(req.body.code || '')
let user = await userDao.findByOpenid(session.openid)
let isNewUser = false
if (!user) {
isNewUser = true
const result = await userDao.create(session.openid)
user = await userDao.findById(result.insertId)
await logDao.write({ user_id: user.user_id, action: 'user_register', detail: '新用户注册', ip: req.ip })
@@ -26,6 +28,7 @@ router.post('/auth/login', wrap(async (req, res) => {
res.json(ok({
token,
user_id: String(user.user_id),
is_new_user: isNewUser,
user_info: {
user_id: String(user.user_id),
nickname: user.nickname || '用户' + String(user.user_id),