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
这个提交包含在:
@@ -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",
|
||||
|
||||
@@ -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' })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "完善资料",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
@@ -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">✓</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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
在新工单中引用
屏蔽一个用户