master
lion 3 weeks ago
parent 34e1b67815
commit 6f3879e77c

@ -9,8 +9,9 @@
</view>
</view>
<u-form :model="form" :label-width="140" ref="uForm" :label-align="'left'" :error-type="['message']">
<u-form-item label="姓名" prop="username">
{{form.username}}
<u-form-item label="姓名" prop="username" :required="canFillUsername">
<u-input v-if="canFillUsername" v-model="form.username" border placeholder="请输入姓名" />
<text v-else>{{form.username}}</text>
</u-form-item>
<u-form-item label="性别" prop="sex" required>
<u-input @click="showSex = true" placeholder="请选择性别" v-model="form.sex" type="select" />
@ -132,6 +133,7 @@
data() {
return {
userAvatar: '',
canFillUsername: false,
showMobile: false,
showSex: false,
myMobile: '',
@ -179,6 +181,13 @@
value: '女'
}],
rules: {
username: [{
validator: (rule, value, callback) => {
return !this.canFillUsername || (typeof value === 'string' && value.trim().length > 0);
},
message: '请输入姓名',
trigger: ['blur', 'change'],
}],
sex: [{
required: true,
message: '请选择性别',
@ -366,6 +375,8 @@
console.log("res", res)
// this.form = this.base.requestToForm(res.user, this.form)
this.form = this.base.deepCopy(res.user)
// Use the loaded name so typing does not hide the input.
this.canFillUsername = !String(res.user.username == null ? '' : res.user.username).trim()
// 头像
this.userAvatar = res.user && res.user.headimgurl ? res.user.headimgurl : ''
if (res.user.plate) {

@ -30,7 +30,7 @@
</u-form>
</view>
<view class="form-btn">
<view @click="saveUser" :style="{ opacity: saving ? 0.6 : 1 }">{{ saving ? '处理中…' : registrationComplete && fromBooking ? '返回场地预约' : registrationComplete && fromSupply ? '返回供需对接' : registrationComplete && survey_id ? '返回问卷' : '注册' }}</view>
<view @click="saveUser" :style="{ opacity: saving ? 0.6 : 1 }">{{ saving ? '处理中…' : registrationComplete && fromBooking ? '返回场地预约' : registrationComplete && fromSupply ? '返回供需对接' : '注册' }}</view>
</view>
<!-- 修改手机号 -->
@ -53,7 +53,7 @@
</view>
</view>
<view class="form-btn">
<view @click="changeMobile" type="primary">确定</view>
<view @click="changeMobile" type="primary">{{ bindingMobile || saving ? '处理中…' : '确定' }}</view>
</view>
</u-popup>
</view>
@ -95,7 +95,9 @@
return { fromSupply: false, fromBooking: false,
survey_id: '',
saving: false,
bindingMobile: false,
registrationComplete: false,
lastRegisteredPayload: '',
showMobile: false,
showCompanySelector: false,
showCompanyInput: false,
@ -134,6 +136,12 @@
},
form: {
username: '',
name: '',
mobile: '',
email: '',
birthday: '',
company_name: '',
company_position: '',
sex: ''
},
sexList: [{
@ -218,6 +226,18 @@
this.fromSupply = options.from_supply === '1'
this.fromBooking = options.from_booking === '1'
this.survey_id = /^\d+$/.test(String(options.survey_id || '')) ? String(options.survey_id) : ''
if (this.survey_id && this.$u && this.$u.api && this.$u.api.user) {
this.$u.api.user().then(res => {
if (!res || !res.user) return
const user = res.user
const form = { ...this.form }
for (const key of ['username', 'sex', 'birthday', 'email', 'company_name', 'company_position', 'mobile']) {
const value = key === 'username' ? user.name : user[key]
if ((form[key] == null || !String(form[key]).trim()) && value != null) form[key] = value
}
this.form = form
}).catch(() => this.base.toast('读取个人资料失败,请重试'))
}
this.course_id = options.id ? options.id : null
this.isDonate = options.isDonate?options.isDonate:false
},
@ -301,6 +321,8 @@
})
},
changeMobile() {
if (this.bindingMobile || this.saving) return
if (this.isBasicInfoIncomplete()) { this.base.toast('请先完善基础信息后再绑定'); return }
if (this.base.isNull(this.myMobile)) {
this.base.toast('请输入手机号')
return
@ -310,16 +332,21 @@
return
}
const mobile = this.myMobile
this.bindingMobile = true
return this.$u.api.bindMobile({
mobile: this.myMobile,
code: this.myCode,
is_bind: 1
}).then(res => {
this.base.toast('绑定成功')
this.form.mobile = this.myMobile
}).then(async res => {
if (!res || !res.id || res.errcode) return false
this.$set(this.form, 'mobile', mobile)
// that.myMobile = ''
this.myCode = ''
this.showMobile = false
// Wait for the verified phone to reach u-form's model before validating.
await this.$nextTick()
this.bindingMobile = false
return await this.saveUser({ stayOnPage: Boolean(this.survey_id) })
}).catch(error => {
const data = error && error.data
if (!data || data.errmsg !== '手机号已存在') return
@ -348,7 +375,7 @@
})
}
})
})
}).finally(() => { this.bindingMobile = false })
},
selectSex(e) {
this.form.sex = this.sexList[e[0]]['value']
@ -400,61 +427,64 @@
this.manualCompanyName = '';
},
saveUser() {
if (this.saving) return
async saveUser(options = {}) {
if (this.saving || this.bindingMobile) return false
this.saving = true
if (this.registrationComplete && (this.fromSupply || this.fromBooking)) return returnToSupply(this).catch(() => this.base.toast('返回页面失败,请重试')).finally(() => { this.saving = false })
if (this.registrationComplete && this.survey_id) {
return returnToSurvey(this.survey_id).catch(() => this.base.toast('返回问卷失败,请重试')).finally(() => { this.saving = false })
const stayOnPage = options && options.stayOnPage === true
const payload = { ...this.form, name: this.form.username }
if (this.survey_id) payload.survey_id = this.survey_id
const signature = JSON.stringify(payload)
let currentSaved = this.registrationComplete && signature === this.lastRegisteredPayload
try {
if (!stayOnPage && currentSaved && (this.fromSupply || this.fromBooking)) {
await returnToSupply(this)
return true
}
if (!stayOnPage && currentSaved && this.survey_id) {
await returnToSurvey(this.survey_id)
return true
}
const valid = await new Promise(resolve => this.$refs.uForm.validate(resolve))
if (!valid) {
this.base.toast('请检查并完善注册资料后重新提交')
return false
}
const res = await this.$u.api.saveUser(payload)
if (res == null || res === false || (typeof res === 'object' && res.errcode)) throw new Error('注册未成功')
currentSaved = true
this.registrationComplete = true
this.lastRegisteredPayload = signature
if (stayOnPage) {
this.base.toast('绑定并注册成功,请确认资料')
return true
}
if (this.fromSupply || this.fromBooking) {
this.registrationComplete = true
await returnToSupply(this)
return true
}
if (this.survey_id) {
this.registrationComplete = true
await returnToSurvey(this.survey_id)
return true
}
const profile = await this.$u.api.user()
this.$u.vuex('vuex_user', profile.user)
this.base.toast('注册成功', 2000, () => {
if (this.course_id) uni.redirectTo({ url: '/packages/course/detail?id=' + this.course_id })
else if (this.isDonate) uni.redirectTo({ url: '/packages/donate/index' })
else uni.switchTab({ url: '/pages/me/index' })
})
return true
} catch (error) {
this.base.toast(currentSaved
? (this.fromBooking ? '注册已成功,返回场地预约失败,请重试' : this.fromSupply ? '注册已成功,返回供需对接失败,请重试' : '注册已成功,返回问卷失败,请点击返回问卷重试')
: '注册未完成,请检查提示后重试')
return false
} finally {
this.saving = false
}
let that = this
this.$refs.uForm.validate(valid => {
if (valid) {
this.form.name = this.form.username
this.$u.api.saveUser(this.form).then(async res => {
if (res == null || res === false) throw new Error('注册未成功')
if (this.fromSupply || this.fromBooking) { this.registrationComplete = true; await returnToSupply(this); return }
if (this.survey_id) {
this.registrationComplete = true
await returnToSurvey(this.survey_id)
return
}
return this.$u.api.user().then(res => {
this.$u.vuex('vuex_user', res.user)
this.base.toast("注册成功", 2000, function() {
console.log("that.course_id", that.course_id)
if (that.course_id) {
uni.redirectTo({
url: '/packages/course/detail?id=' +
that.course_id
})
// uni.redirectTo({
// url:'/pages/course/index'
// })
}else if(that.isDonate){
uni.redirectTo({
url: '/packages/donate/index'
})
} else {
uni.switchTab({
url: '/pages/me/index'
})
}
})
})
}).catch(() => {
this.base.toast(this.registrationComplete ? (this.fromBooking ? '注册已成功,返回场地预约失败,请重试' : this.fromSupply ? '注册已成功,返回供需对接失败,请重试' : '注册已成功,返回问卷失败,请点击返回问卷重试') : '注册未完成,请检查提示后重试')
}).finally(() => { this.saving = false })
} else {
this.saving = false
console.log('验证失败');
// this.base.toast("注册失败")
}
});
},
},
}
}
</script>

@ -1,282 +1,282 @@
<template>
<view class="container">
<image class="cbg" :src="base.imgHost('login-top.png')"></image>
<view class="login">
<image class="login-logo" :src="base.imgHost('login-logo.png')"></image>
<view class="login-form">
<view>
<u-input :custom-style="inputStyle" :clearable="false" v-model="form.mobile"
placeholder="手机号"></u-input>
</view>
<view class="sendmsg">
<u-input v-model="form.code" :custom-style="inputSendStyle" :clearable="false"
placeholder="验证码"></u-input>
<view @click="getSmsCode" class="send" :class="{'hasSend':hasSend}">{{hasSend?'已发送':'获取验证码'}}<text
v-if="hasSend">({{count}}s)</text></view>
</view>
</view>
<view class="login-btn">
<view @click="handleMsgLogin">{{ bindingComplete && fromBooking ? '返回场地预约' : bindingComplete && fromSupply ? '返回供需对接' : '登录' }}</view>
</view>
<view class="login-links">
<view class="login-msg" @click="toRegister">注册账号</view>
<view class="login-msg" @click="goHome">返回首页</view>
</view>
</view>
</view>
</template>
<script>
import { returnToSupply } from '@/common/supplyIdentity.js'
import { returnToSurvey } from '@/common/surveyNavigation.js'
export default {
data() {
return { fromSupply: false, fromBooking: false,
survey_id: '',
binding: false,
bindingComplete: false,
hasSend: false,
isDonate:false,
count: 60,
inputStyle: {
'padding': '0rpx 30rpx',
'height': '80rpx',
'border': '1rpx solid #dad8d8;',
'border-radius': '20rpx'
},
inputSendStyle: {
'padding': '0rpx 30rpx',
'height': '80rpx',
'border': '1rpx solid #dad8d8;',
'border-radius': '20rpx 0 0 20rpx'
},
form: {
mobile: '',
code: '',
},
sendTimer: null,
type: 'me'
}
},
onLoad(options) {
this.fromSupply = options.from_supply === '1'
this.fromBooking = options.from_booking === '1'
if (/^1[3-9]\d{9}$/.test(String(options.mobile || ''))) this.form.mobile = String(options.mobile)
this.survey_id = /^\d+$/.test(String(options.survey_id || '')) ? String(options.survey_id) : ''
this.type = options.id ? 'course' : 'me'
this.isDonate = options.isDonate?true:false
},
onUnload() {
if (this.sendTimer) {
clearInterval(this.sendTimer);
this.sendTimer = null
}
},
onHide() {
if (this.sendTimer) {
clearInterval(this.sendTimer);
this.sendTimer = null
}
},
methods: {
getSmsCode() {
if (this.hasSend) {
return
}
if (this.base.isNull(this.form.mobile)) {
this.base.toast('手机号码不能为空')
return
}
if (!this.base.isMobile(this.form.mobile)) {
this.base.toast('手机号码错误')
return
}
let that = this
this.$u.api.sendSms({
mobile: this.form.mobile
}).then(res => {
this.base.toast("发送成功")
this.hasSend = true
this.sendTimer = setInterval(function() {
if (that.count > 1) {
that.count--
} else {
clearInterval(that.sendTimer);
that.sendTimer = null
that.count = 60
that.hasSend = false;
}
}, 1000)
})
},
handleMsgLogin() {
if (this.binding) return
if (this.bindingComplete && (this.fromSupply || this.fromBooking)) return returnToSupply(this).catch(() => this.base.toast('返回页面失败,请重试'))
if (this.bindingComplete && this.survey_id) {
this.binding = true
return returnToSurvey(this.survey_id).catch(() => this.base.toast('返回问卷失败,请重试')).finally(() => { this.binding = false })
}
if (this.base.isNull(this.form.mobile)) {
this.base.toast('请输入手机号')
return
}
if (this.base.isNull(this.form.code)) {
this.base.toast('请输入验证码')
return
}
let that = this
this.binding = true
return this.$u.api.checkMobile({
mobile: this.form.mobile,
code: this.form.code
}).then(async res => {
if (!res || !res.token) throw new Error('绑定未成功')
this.bindingComplete = true
this.$u.vuex('vuex_token', res.token)
this.base.toast('绑定成功')
if (that.sendTimer) {
clearInterval(that.sendTimer);
that.hasSend = false;
}
uni.removeStorageSync("stbc1_lifeData")
uni.setStorageSync("stbc1_lifeData", {
'vuex_token': res.token
})
if (that.fromSupply || that.fromBooking) { await returnToSupply(that) } else if (that.survey_id) {
await returnToSurvey(that.survey_id)
} else if (that.type == 'course') {
uni.switchTab({
url: '/pages/course/index'
})
} else if (that.isDonate) {
uni.redirectTo({
url: '/packages/donate/index'
})
} else {
uni.switchTab({
url: '/pages/me/index'
})
}
// this.$u.api.user().then(res => {
// this.$u.vuex('vuex_user', res.user)
// })
}).catch(() => {
this.base.toast(this.bindingComplete ? (this.fromBooking ? '登录已成功,返回场地预约失败,请重试' : this.fromSupply ? '登录已成功,返回供需对接失败,请重试' : '绑定已成功,返回问卷失败,请重试') : '绑定未完成,请检查提示后重试')
}).finally(() => { this.binding = false })
},
toRegister() {
uni.redirectTo({
url: '/packages/register/index' + (this.fromBooking ? '?from_booking=1' : this.fromSupply ? '?from_supply=1' : this.survey_id ? '?survey_id=' + this.survey_id : '')
})
},
goHome() {
uni.switchTab({
url: '/pages/index/index'
})
}
}
}
</script>
<style scoped lang="scss">
.container {
width: 100%;
height: 100vh;
overflow: scroll;
background: #fefcf7;
position: relative;
.cbg {
position: absolute;
top: 0;
left: 0;
width: 509rpx;
height: 369rpx;
}
.login {
width: 590rpx;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
&-logo {
width: 345rpx;
height: 72rpx;
margin: 0 auto;
display: block;
}
&-form {
margin: 100rpx 0;
&>view {
border-radius: 20rpx;
&:first-child {
margin-bottom: 50rpx;
}
}
.sendmsg {
display: flex;
justify-content: space-between;
align-items: center;
u-input {
width: calc(100% - 200rpx);
}
.send {
background: #c69c6d;
color: #fff;
height: 80rpx;
line-height: 80rpx;
border-radius: 0 20rpx 20rpx 0;
width: 200rpx;
text-align: center;
border: 1px solid #c69c6d;
box-sizing: content-box;
}
.hasSend {
background: #dad8d8;
color: #333;
border: 1px solid #dad8d8;
}
}
}
&-btn {
width: 100%;
position: relative;
&>view {
width: 70%;
text-align: center;
margin: 0 auto;
color: #fff;
background: linear-gradient(to right, #e4cdb4, #c69c6d);
border-radius: 30rpx;
padding: 20rpx;
}
}
&-msg {
padding: 20rpx 20rpx;
color: #0d0398;
}
&-links {
display: flex;
justify-content: center;
align-items: center;
}
}
}
<template>
<view class="container">
<image class="cbg" :src="base.imgHost('login-top.png')"></image>
<view class="login">
<image class="login-logo" :src="base.imgHost('login-logo.png')"></image>
<view class="login-form">
<view>
<u-input :custom-style="inputStyle" :clearable="false" v-model="form.mobile"
placeholder="手机号"></u-input>
</view>
<view class="sendmsg">
<u-input v-model="form.code" :custom-style="inputSendStyle" :clearable="false"
placeholder="验证码"></u-input>
<view @click="getSmsCode" class="send" :class="{'hasSend':hasSend}">{{hasSend?'已发送':'获取验证码'}}<text
v-if="hasSend">({{count}}s)</text></view>
</view>
</view>
<view class="login-btn">
<view @click="handleMsgLogin">{{ bindingComplete && fromBooking ? '返回场地预约' : bindingComplete && fromSupply ? '返回供需对接' : '登录' }}</view>
</view>
<view class="login-links">
<view class="login-msg" @click="toRegister">注册账号</view>
<view class="login-msg" @click="goHome">返回首页</view>
</view>
</view>
</view>
</template>
<script>
import { returnToSupply } from '@/common/supplyIdentity.js'
import { returnToSurvey } from '@/common/surveyNavigation.js'
export default {
data() {
return { fromSupply: false, fromBooking: false,
survey_id: '',
binding: false,
bindingComplete: false,
hasSend: false,
isDonate:false,
count: 60,
inputStyle: {
'padding': '0rpx 30rpx',
'height': '80rpx',
'border': '1rpx solid #dad8d8;',
'border-radius': '20rpx'
},
inputSendStyle: {
'padding': '0rpx 30rpx',
'height': '80rpx',
'border': '1rpx solid #dad8d8;',
'border-radius': '20rpx 0 0 20rpx'
},
form: {
mobile: '',
code: '',
},
sendTimer: null,
type: 'me'
}
},
onLoad(options) {
this.fromSupply = options.from_supply === '1'
this.fromBooking = options.from_booking === '1'
if (/^1[3-9]\d{9}$/.test(String(options.mobile || ''))) this.form.mobile = String(options.mobile)
this.survey_id = /^\d+$/.test(String(options.survey_id || '')) ? String(options.survey_id) : ''
this.type = options.id ? 'course' : 'me'
this.isDonate = options.isDonate?true:false
},
onUnload() {
if (this.sendTimer) {
clearInterval(this.sendTimer);
this.sendTimer = null
}
},
onHide() {
if (this.sendTimer) {
clearInterval(this.sendTimer);
this.sendTimer = null
}
},
methods: {
getSmsCode() {
if (this.hasSend) {
return
}
if (this.base.isNull(this.form.mobile)) {
this.base.toast('手机号码不能为空')
return
}
if (!this.base.isMobile(this.form.mobile)) {
this.base.toast('手机号码错误')
return
}
let that = this
this.$u.api.sendSms({
mobile: this.form.mobile
}).then(res => {
this.base.toast("发送成功")
this.hasSend = true
this.sendTimer = setInterval(function() {
if (that.count > 1) {
that.count--
} else {
clearInterval(that.sendTimer);
that.sendTimer = null
that.count = 60
that.hasSend = false;
}
}, 1000)
})
},
handleMsgLogin() {
if (this.binding) return
if (this.bindingComplete && (this.fromSupply || this.fromBooking)) return returnToSupply(this).catch(() => this.base.toast('返回页面失败,请重试'))
if (this.bindingComplete && this.survey_id) {
this.binding = true
return returnToSurvey(this.survey_id).catch(() => this.base.toast('返回问卷失败,请重试')).finally(() => { this.binding = false })
}
if (this.base.isNull(this.form.mobile)) {
this.base.toast('请输入手机号')
return
}
if (this.base.isNull(this.form.code)) {
this.base.toast('请输入验证码')
return
}
let that = this
this.binding = true
return this.$u.api.checkMobile({
mobile: this.form.mobile,
code: this.form.code
}).then(async res => {
if (!res || !res.token) throw new Error('绑定未成功')
this.bindingComplete = true
this.$u.vuex('vuex_token', res.token)
this.base.toast('绑定成功')
if (that.sendTimer) {
clearInterval(that.sendTimer);
that.hasSend = false;
}
uni.removeStorageSync("stbc1_lifeData")
uni.setStorageSync("stbc1_lifeData", {
'vuex_token': res.token
})
if (that.fromSupply || that.fromBooking) { await returnToSupply(that) } else if (that.survey_id) {
await returnToSurvey(that.survey_id)
} else if (that.type == 'course') {
uni.switchTab({
url: '/pages/course/index'
})
} else if (that.isDonate) {
uni.redirectTo({
url: '/packages/donate/index'
})
} else {
uni.switchTab({
url: '/pages/me/index'
})
}
// this.$u.api.user().then(res => {
// this.$u.vuex('vuex_user', res.user)
// })
}).catch(() => {
this.base.toast(this.bindingComplete ? (this.fromBooking ? '登录已成功,返回场地预约失败,请重试' : this.fromSupply ? '登录已成功,返回供需对接失败,请重试' : '绑定已成功,返回问卷失败,请重试') : '绑定未完成,请检查提示后重试')
}).finally(() => { this.binding = false })
},
toRegister() {
uni.redirectTo({
url: '/packages/register/index' + (this.fromBooking ? '?from_booking=1' : this.fromSupply ? '?from_supply=1' : this.survey_id ? '?survey_id=' + this.survey_id : '')
})
},
goHome() {
uni.switchTab({
url: '/pages/index/index'
})
}
}
}
</script>
<style scoped lang="scss">
.container {
width: 100%;
height: 100vh;
overflow: scroll;
background: #fefcf7;
position: relative;
.cbg {
position: absolute;
top: 0;
left: 0;
width: 509rpx;
height: 369rpx;
}
.login {
width: 590rpx;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
&-logo {
width: 345rpx;
height: 72rpx;
margin: 0 auto;
display: block;
}
&-form {
margin: 100rpx 0;
&>view {
border-radius: 20rpx;
&:first-child {
margin-bottom: 50rpx;
}
}
.sendmsg {
display: flex;
justify-content: space-between;
align-items: center;
u-input {
width: calc(100% - 200rpx);
}
.send {
background: #c69c6d;
color: #fff;
height: 80rpx;
line-height: 80rpx;
border-radius: 0 20rpx 20rpx 0;
width: 200rpx;
text-align: center;
border: 1px solid #c69c6d;
box-sizing: content-box;
}
.hasSend {
background: #dad8d8;
color: #333;
border: 1px solid #dad8d8;
}
}
}
&-btn {
width: 100%;
position: relative;
&>view {
width: 70%;
text-align: center;
margin: 0 auto;
color: #fff;
background: linear-gradient(to right, #e4cdb4, #c69c6d);
border-radius: 30rpx;
padding: 20rpx;
}
}
&-msg {
padding: 20rpx 20rpx;
color: #0d0398;
}
&-links {
display: flex;
justify-content: center;
align-items: center;
}
}
}
</style>

@ -1,7 +1,7 @@
<template>
<view class="survey-fill-page">
<image class="cbg" :src="base.imgHost('common_bg.png')" />
<view class="progress-bar-container"><view class="progress-info">
<view v-if="!loading && !error && !unavailableMessage && !profileRequired" class="progress-bar-container"><view class="progress-info">
<view class="progress-info-progress"><u-line-progress :height="10" :percent="completion.percent" :show-percent="false" active-color="#2018a0" /></view>
<text class="progress-text">{{ completion.answered }}/{{ completion.total }}</text>
</view>
@ -11,7 +11,10 @@
</view>
<view class="survey-container">
<view v-if="loading">正在加载问卷…</view>
<view v-else-if="error"><text>{{ error }}</text><button @click="load">重新加载</button></view>
<view v-else-if="unavailableMessage || error" class="survey-status">
<text class="survey-status-message">{{ unavailableMessage || error }}</text>
<button class="survey-reload" hover-class="survey-reload--pressed" @click="load">重新加载</button>
</view>
<view v-else-if="profileRequired" class="profile-gate"><view>请先绑定或注册后填写问卷</view><button class="profile-gate-action" hover-class="profile-gate-action--pressed" @click="showRegister = true">绑定或注册</button></view>
<template v-else>
<view class="survey-header"><view class="survey-title">{{ survey.title }}</view><view class="survey-description"><u-parse :html="descriptionHtml" :autoset-title="false" /></view></view>
@ -85,6 +88,7 @@ export default {
questionList: [],
loading: false,
error: '',
unavailableMessage: '',
submitting: false,
uploadingFiles: {},
completed: false,
@ -145,20 +149,67 @@ export default {
hasOther(q) {
return showOther(q, q.value)
},
leaveUnavailable(message) {
this.error = message
availabilityError(res) {
if (!res) return null
if (['draft', 'not_started', 'ended'].includes(res.access_state)) return res
// MySQL times use the backend's Asia/Shanghai timezone. Avoid iOS string parsing
// and the development computer's timezone when checking the opening window.
const parseTime = value => {
if (!value) return null
const parts = String(value).match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/)
if (!parts) return null
return { parts, timestamp: Date.UTC(+parts[1], +parts[2] - 1, +parts[3], +parts[4], +parts[5], +parts[6]) - 8 * 60 * 60 * 1000 }
}
const now = Date.now()
const start = parseTime(res.start_time)
if (start && start.timestamp > now) {
const [, year, month, day, hour, minute, second] = start.parts
const time = hour + ':' + minute + ':' + second
const display = year + '年' + month + '月' + day + '日' + (time === '00:00:00' ? '' : ' ' + time)
return { access_state: 'not_started', access_error: '该问卷尚未开始,请于' + display + '后扫码打开该问卷。' }
}
if (res.status !== undefined && res.status !== null && Number(res.status) !== 1) {
return { access_state: 'draft', access_error: '该问卷还未开始。' }
}
const end = parseTime(res.end_time)
if ((end && end.timestamp <= now) || res.access_error === '当前问卷已截止') {
return { access_state: 'ended', access_error: '该问卷已结束。' }
}
return null
},
handleAccessError(res) {
res = this.availabilityError(res) || res
if (!res || !res.access_error) return false
this.unavailableMessage = res.access_error
this.error = ''
this.survey = {}
this.questionList = []
this.completed = false
this.showRegister = false
uni.showToast({ title: message, icon: 'none', duration: 2000 })
setTimeout(() => uni.switchTab({ url: '/pages/index/index' }), 2000)
this.profileRequired = false
this.keyboardOpen = false
this.activeChapter = null
this.errorId = null
return true
},
async load() {
if (this.loading) return
this.loading = true
this.error = ''
this.unavailableMessage = ''
this.survey = {}
this.questionList = []
this.completed = false
this.showRegister = false
this.profileRequired = false
this.errorId = null
this.activeChapter = null
this.keyboardOpen = false
try {
const res = await this.$u.api.courseEvaluationDetail({
course_content_evaluation_id: this.id
})
if (res && res.access_error) { this.leaveUnavailable(res.access_error); return }
if (this.handleAccessError(res)) return
if (!res || !res.id) throw new Error('问卷不存在')
const reply = res.my_response
this.completed = Boolean(reply)
@ -213,7 +264,7 @@ export default {
},
submit() {
if (Object.values(this.uploadingFiles).some(Boolean)) return uni.showToast({ title: '请等待文件上传完成', icon: 'none' })
if (this.submitting || this.profileRequired) return
if (this.submitting || this.profileRequired || this.loading || this.unavailableMessage || this.error) return
const error = firstError(this.questionList)
if (error) {
this.errorId = error.id
@ -238,7 +289,7 @@ export default {
time_total: Math.floor((Date.now() - this.startedAt) / 1000),
data: submission(this.questionList)
})
if (res && res.access_error) { this.leaveUnavailable(res.access_error); return }
if (this.handleAccessError(res)) return
if (!res || !res.id) throw new Error('提交失败,请检查提示后重试')
this.completed = true
uni.showToast({ title: '提交成功', icon: 'success' })
@ -286,6 +337,37 @@ export default {
}
.survey-status {
margin: 100rpx 0 0;
padding: 64rpx 40rpx;
background: #fff;
border-radius: 28rpx;
box-shadow: 0 8rpx 32rpx rgba(32, 24, 160, 0.06);
text-align: center;
}
.survey-status-message {
display: block;
font-size: 30rpx;
line-height: 1.8;
color: #42465a;
word-break: break-all;
}
.survey-reload {
width: 280rpx;
margin: 40rpx auto 0;
padding: 0;
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
font-weight: 500;
color: #fff;
background: linear-gradient(110deg, #6059bd, #2018a0);
border-radius: 40rpx;
box-shadow: 0 8rpx 20rpx rgba(32, 24, 160, 0.15);
}
.survey-reload::after { border: none; }
.survey-reload--pressed { opacity: 0.8; }
.survey-header {
background: white;
border-radius: 24rpx;
@ -577,4 +659,4 @@ export default {
background: linear-gradient(to right, #5e5fbc, #0d0398);
}
}
</style>
</style>

@ -14,6 +14,8 @@ let modal, redirect
const uni = { showModal: options => { modal = options }, redirectTo: options => { redirect = options } }
const page = loadPage('../packages/register/index.vue', uni)
const state = {
$set: (target, key, value) => { target[key] = value }, $nextTick: () => Promise.resolve(),
isBasicInfoIncomplete: () => false, saveUser() { this.registrations = (this.registrations || 0) + 1 },
myMobile: '13800138000', myCode: '123456', form: {}, showMobile: true,
sendTimer: null, hasSend: true, count: 35, survey_id: '12', course_id: '34', isDonate: true,
base: { isNull: value => !value, toast() {} },
@ -33,9 +35,11 @@ state.$u.api.bindMobile = async () => { throw { data: { errmsg: '验证码错误
state.myCode = '654321'
await page.methods.changeMobile.call(state)
assert.equal(modal, undefined)
assert.equal(state.registrations || 0, 0)
state.$u.api.bindMobile = async () => ({ id: 1 })
await page.methods.changeMobile.call(state)
assert.equal(state.form.mobile, '13800138000')
assert.equal(state.registrations, 1)
const login = loadPage('../packages/register/login.vue', uni)
const loginState = { form: { mobile: '' } }
login.onLoad.call(loginState, { mobile: '13800138000', survey_id: '12' })
@ -45,3 +49,22 @@ loginState.form.mobile = ''
login.onLoad.call(loginState, { mobile: 'invalid' })
assert.equal(loginState.form.mobile, '')
console.log('Registration duplicate-phone and login prefill checks passed')
for (const response of [false, null, undefined, { errcode: 1, errmsg: '验证码错误' }]) {
const before = state.registrations
state.myCode = '123456'
state.$u.api.bindMobile = async () => response
await page.methods.changeMobile.call(state)
assert.equal(state.registrations, before)
assert.equal(state.bindingMobile, false)
}
let completeBinding, requests = 0
state.myCode = '123456'
state.$u.api.bindMobile = () => { requests++; return new Promise(resolve => { completeBinding = resolve }) }
const pending = page.methods.changeMobile.call(state)
page.methods.changeMobile.call(state)
assert.equal(requests, 1)
completeBinding({ id: 1 })
await pending
assert.equal(state.registrations, 2)
console.log('Verified phone binding auto-registers once; errors and duplicate clicks never register')

@ -177,8 +177,9 @@ const savedQuestions = [
let toast
const previousUni = globalThis.uni
globalThis.uni = { showToast: args => { toast = args.title } }
const completeProfile = { mobile: '13800000000', name: '测试学员', sex: '男', birthday: '1990-01-01', email: 'test@example.com', company_name: '测试公司', company_position: '经理' }
const returned = new Vue(page)
returned.$u = { api: { user: async () => ({ user: { mobile: '13800000000' } }), courseEvaluationDetail: async () => ({ id: 8, title: '新标题', my_response: {
returned.$u = { api: { user: async () => ({ user: completeProfile }), courseEvaluationDetail: async () => ({ id: 8, title: '新标题', my_response: {
title: '提交时标题', desc: '<p>原说明</p>', questions: savedQuestions
} }) } }
await returned.load()
@ -269,7 +270,7 @@ const registration = {
form: { username: '张三' },
$refs: { uForm: { validate: callback => callback(true) } },
base: { toast: noop },
$u: { vuex: noop, api: { saveUser: async () => ({}), user: async () => ({ user: { mobile: '13800000000' } }) } }
$u: { vuex: noop, api: { saveUser: async () => ({}), user: async () => ({ user: completeProfile }) } }
}
registrationPage.onLoad.call(registration, { survey_id: '8' })
registrationPage.methods.saveUser.call(registration)
@ -316,8 +317,9 @@ const freshRegistration = {
}
globalThis.uni = { redirectTo: args => { identityRoute = args.url; args.success() } }
identityRoute = ''
const firstRegistration = registrationPage.methods.saveUser.call(freshRegistration)
registrationPage.methods.saveUser.call(freshRegistration)
registrationPage.methods.saveUser.call(freshRegistration)
await Promise.resolve()
assert.equal(saves, 1)
assert.equal(identityRoute, '')
completeSave('更新成功')
@ -356,24 +358,103 @@ assert.deepEqual(submission(chained).map(q => q.id), [91])
chained[0].value = 'yes'
assert.equal(visibleQuestions(chained).length, 3)
for (const message of ['当前问卷已截止', '您还未报名相关课程']) {
for (const [state, message] of [
[undefined, '您还未报名相关课程'],
['not_started', '该问卷尚未开始,请于2026年10月23日后扫码打开该问卷。'],
['ended', '该问卷已结束。'],
['draft', '该问卷还未开始。']
]) {
const denied = new Vue(page)
denied.id = 9
denied.$u = { api: { courseEvaluationDetail: async () => ({ id: 9, access_error: message }), user: () => { throw new Error('must not fetch profile') } } }
let timer
const originalTimeout = globalThis.setTimeout
globalThis.setTimeout = callback => { timer = callback }
let notice, destination
globalThis.uni = { showToast: options => { notice = options.title }, switchTab: options => { destination = options.url } }
denied.questionList = prepareQuestions([{ id: 1, name: '旧问题', edit_input: 'text' }])
denied.survey = { title: '旧问卷' }
denied.showRegister = true
let calls = 0
denied.$u = { api: { courseEvaluationDetail: async () => { calls++; return { id: 9, access_state: state, access_error: message } },
user: () => { throw new Error('must not fetch profile') } } }
globalThis.uni = { showToast: () => { throw new Error('must show message on page') },
switchTab: () => { throw new Error('must stay on survey page') },
showModal: () => { throw new Error('must not submit unavailable survey') } }
await denied.load()
assert.equal(denied.unavailableMessage, message)
assert.equal(denied.error, '')
assert.equal(denied.loading, false)
assert.equal(denied.showRegister, false)
assert.deepEqual(denied.questionList, [])
assert.deepEqual(denied.survey, {})
denied.submit()
await denied.load()
assert.equal(calls, 2)
denied.$u.api.courseEvaluationDetail = async () => ({ id: 9, title: '已开始问卷', course_content_evaluation_asks: [{ id: 1, edit_input: 'text', name: '姓名' }] })
denied.$u.api.user = async () => ({ user: { mobile: '13800000000' } })
await denied.load()
assert.equal(notice, message)
assert.equal(denied.error, message)
timer()
assert.equal(destination, '/pages/index/index')
globalThis.setTimeout = originalTimeout
assert.equal(denied.unavailableMessage, '')
assert.equal(denied.survey.title, '已开始问卷')
assert.equal(denied.questionList.length, 1)
denied.$destroy()
}
console.log('Deadline and audience denial toast then return home verified')
globalThis.uni = previousUni
console.log('Draft, future and ended surveys hide stale content, stay on page, block submit and can reload after opening')
// Access may be revoked after opening the form: submission must hide the old
// questions as well, without a toast, delayed navigation or registration popup.
const audienceRevoked = new Vue(page)
audienceRevoked.id = 9
audienceRevoked.survey = { title: '此前可填写', schema_hash: 'test' }
audienceRevoked.questionList = prepareQuestions([{ id: 1, name: '意见', edit_input: 'text' }])
audienceRevoked.questionList[0].value = '已填写内容'
audienceRevoked.$u = { api: { courseContentForm: async () => ({ access_error: '您还未报名相关课程' }) } }
let revokedSubmission
globalThis.uni = {
showModal: args => { revokedSubmission = args.success({ confirm: true }) },
showToast: () => { throw new Error('must display the audience message on page') },
switchTab: () => { throw new Error('must not return home') }
}
audienceRevoked.submit()
await revokedSubmission
assert.equal(audienceRevoked.unavailableMessage, '您还未报名相关课程')
assert.deepEqual(audienceRevoked.questionList, [])
assert.deepEqual(audienceRevoked.survey, {})
assert.equal(audienceRevoked.submitting, false)
assert.equal(audienceRevoked.showRegister, false)
audienceRevoked.$destroy()
globalThis.uni = previousUni
console.log('Audience denial at entry and submission hides questions, stays on page and supports reloading after approval')
const actualNow = Date.now
Date.now = () => Date.UTC(2026, 9, 22, 4, 0, 0) // 2026-10-22 12:00:00 Asia/Shanghai
try {
for (const [fields, message] of [
[{ status: '0', start_time: '2026-10-26 00:00:00' }, '该问卷尚未开始,请于2026年10月26日后扫码打开该问卷。'],
[{ status: 0 }, '该问卷还未开始。'],
[{ status: '0', start_time: '2026-10-26 09:30:15' }, '该问卷尚未开始,请于2026年10月26日 09:30:15后扫码打开该问卷。'],
[{ status: '0', start_time: '2026-10-21 00:00:00' }, '该问卷还未开始。'],
[{ status: '1', start_time: '2026-10-26 00:00:00' }, '该问卷尚未开始,请于2026年10月26日后扫码打开该问卷。'],
[{ status: 1, start_time: '2026-10-26 09:30:15' }, '该问卷尚未开始,请于2026年10月26日 09:30:15后扫码打开该问卷。'],
[{ status: 1, end_time: '2026-10-22 12:00:00' }, '该问卷已结束。'],
[{ access_error: '当前问卷已截止' }, '该问卷已结束。']
]) {
const denied = new Vue(page)
denied.id = 9
denied.$u = { api: { courseEvaluationDetail: async () => ({ id: 9, title: '不应显示', course_content_evaluation_asks: [{ id: 1, name: '不应显示的题目', edit_input: 'text' }], ...fields }),
user: () => { throw new Error('must not fetch profile before checking raw status and dates') } } }
globalThis.uni = { showToast: () => { throw new Error('must stay on message page') }, switchTab: () => { throw new Error('must not leave page') } }
await denied.load()
assert.equal(denied.unavailableMessage, message)
assert.equal(denied.error, '')
assert.deepEqual(denied.questionList, [])
assert.deepEqual(denied.survey, {})
denied.$destroy()
}
const available = new Vue(page)
assert.equal(available.availabilityError({ status: '1', start_time: '2026-10-22 12:00:00', end_time: '2026-10-23 00:00:00' }), null)
assert.equal(available.availabilityError({ status: '1', start_time: null, end_time: null }), null)
available.$destroy()
} finally {
Date.now = actualNow
globalThis.uni = previousUni
}
console.log('Legacy full responses: string draft status, future opening, deadline and Shanghai timezone boundaries are blocked before profile/questions load')
const fileQuestions = prepareQuestions([{ id: 301, edit_input: 'file', name: '文件', need_fill: true, config: { max_files: 1 } }])
assert.ok(firstError(fileQuestions))
@ -453,3 +534,131 @@ assert.equal(required(financing[1], financing), true)
financing[0].value = ['later']
assert.equal(required(financing[1], financing), false)
console.log('Multiple visibility triggers and required overrides verified')
const incomplete = new Vue(page)
incomplete.$u = { api: { user: async () => ({ user: { mobile: '13800000000' } }), courseEvaluationDetail: async () => ({ id: 9, requires_profile: true }) } }
await incomplete.load()
assert.equal(incomplete.profileRequired, false)
assert.equal(incomplete.showRegister, false)
incomplete.$destroy()
identityRoute = ''
globalThis.uni = { redirectTo: args => { identityRoute = args.url; args.success() }, removeStorageSync: noop, setStorageSync: noop }
const needsProfile = { ...binding, binding: false, bindingComplete: false, survey_id: '9', $u: { vuex: noop, api: { checkMobile: async () => ({ token: 'test', requires_profile: true }) } } }
await bindingPage.methods.handleMsgLogin.call(needsProfile)
assert.equal(identityRoute, '/packages/surveyFill/index?id=9')
const registrationPrefill = { form: {}, survey_id: '', base: { toast: noop }, $u: { api: { user: async () => ({ user: completeProfile }) } } }
registrationPage.onLoad.call(registrationPrefill, { survey_id: '9' })
await new Promise(resolve => setTimeout(resolve, 0))
assert.equal(registrationPrefill.form.username, completeProfile.name)
assert.equal(registrationPrefill.form.mobile, completeProfile.mobile)
assert.equal(registrationPrefill.form.company_name, completeProfile.company_name)
globalThis.uni = previousUni
console.log('Mobile-only survey gate, binding return and verified profile prefill passed')
let automaticSaves = 0
identityRoute = ''
globalThis.uni = { redirectTo: args => { identityRoute = args.url; args.success() } }
const automaticRegistration = {
survey_id: '9', form: { ...completeProfile, username: completeProfile.name }, myMobile: completeProfile.mobile, myCode: '123456',
saving: false, bindingMobile: false, registrationComplete: false, isBasicInfoIncomplete: () => false,
$refs: { uForm: { validate: callback => callback(true) } },
base: { toast: noop, isNull: value => !value },
$u: { api: { bindMobile: async () => ({ id: 200, mobile: completeProfile.mobile }), saveUser: async payload => { automaticSaves++; assert.equal(payload.survey_id, '9'); return '更新成功' } } },
$set: (target, key, value) => { target[key] = value }, $nextTick: () => Promise.resolve(),
saveUser(options) { return registrationPage.methods.saveUser.call(this, options) }
}
await registrationPage.methods.changeMobile.call(automaticRegistration)
await new Promise(resolve => setTimeout(resolve, 0))
assert.equal(automaticSaves, 1)
assert.equal(automaticRegistration.registrationComplete, true)
assert.equal(identityRoute, '')
automaticRegistration.form.company_position = '修改后职务'
automaticRegistration.$u.api.saveUser = async payload => {
automaticSaves++
assert.equal(payload.company_position, '修改后职务')
return '更新成功'
}
assert.equal(await automaticRegistration.saveUser(), true)
assert.equal(automaticSaves, 2)
assert.equal(identityRoute, '/packages/surveyFill/index?id=9')
globalThis.uni = previousUni
console.log('Auto-registration stays on the page; explicit registration saves edits before returning')
// Simulate the u-form child receiving its phone prop only on Vue's next update.
let boundRequests = 0, registeredRequests = 0, finishRegistration
const reactiveRegistration = new Vue(registrationPage)
reactiveRegistration.form = { ...reactiveRegistration.form, ...completeProfile, username: completeProfile.name, mobile: '' }
reactiveRegistration.survey_id = '9'
reactiveRegistration.myMobile = completeProfile.mobile
reactiveRegistration.myCode = '123456'
reactiveRegistration.base = { isNull: value => value == null || !String(value).trim(), toast: noop }
let renderedMobile = ''
reactiveRegistration.$watch(() => reactiveRegistration.form.mobile, value => { renderedMobile = value })
reactiveRegistration.$refs.uForm = { validate: callback => {
assert.equal(renderedMobile, completeProfile.mobile, 'the bound phone must reach the form before validation')
callback(true)
} }
reactiveRegistration.$u = { api: {
bindMobile: async () => { boundRequests++; return { id: 200, mobile: completeProfile.mobile } },
saveUser: payload => {
registeredRequests++
assert.equal(payload.mobile, completeProfile.mobile)
assert.equal(payload.survey_id, '9')
return new Promise(resolve => { finishRegistration = resolve })
}
} }
identityRoute = ''
globalThis.uni = { redirectTo: options => { identityRoute = options.url; options.success() } }
const registrationChain = reactiveRegistration.changeMobile()
reactiveRegistration.changeMobile()
await new Promise(resolve => setTimeout(resolve, 0))
assert.equal(boundRequests, 1)
assert.equal(registeredRequests, 1)
assert.equal(identityRoute, '', 'must wait for the registration API to succeed')
assert.equal(reactiveRegistration.saving, true)
reactiveRegistration.changeMobile()
assert.equal(boundRequests, 1)
finishRegistration('更新成功')
assert.equal(await registrationChain, true)
assert.equal(identityRoute, '')
reactiveRegistration.form.username = '修改后的姓名'
const explicitRegistration = reactiveRegistration.saveUser()
await new Promise(resolve => setTimeout(resolve, 0))
assert.equal(registeredRequests, 2)
assert.equal(identityRoute, '')
finishRegistration('更新成功')
assert.equal(await explicitRegistration, true)
assert.equal(identityRoute, '/packages/surveyFill/index?id=9')
assert.equal(reactiveRegistration.saving, false)
assert.equal(reactiveRegistration.bindingMobile, false)
reactiveRegistration.$destroy()
globalThis.uni = previousUni
console.log('Reactive phone sync and the full bind/register/return promise chain verified')
// Saving or validation failures must not navigate; retry saving uses the verified phone.
const failedRegistration = {
...automaticRegistration, registrationComplete: false, saving: false, bindingMobile: false, myCode: '123456',
form: { ...automaticRegistration.form },
$u: { api: { bindMobile: async () => ({ id: 200 }), saveUser: async () => false } }
}
identityRoute = ''
globalThis.uni = { redirectTo: options => { identityRoute = options.url; options.success() } }
assert.equal(await registrationPage.methods.changeMobile.call(failedRegistration), false)
assert.equal(failedRegistration.registrationComplete, false)
assert.equal(failedRegistration.saving, false)
assert.equal(identityRoute, '')
failedRegistration.$u.api.saveUser = async () => '更新成功'
assert.equal(await registrationPage.methods.saveUser.call(failedRegistration), true)
assert.equal(identityRoute, '/packages/surveyFill/index?id=9')
let invalidSaves = 0
const invalidRegistration = {
...failedRegistration, registrationComplete: false, saving: false, myCode: '123456',
$refs: { uForm: { validate: callback => callback(false) } },
$u: { api: { bindMobile: async () => ({ id: 200 }), saveUser: async () => { invalidSaves++; return '更新成功' } } }
}
identityRoute = ''
assert.equal(await registrationPage.methods.changeMobile.call(invalidRegistration), false)
assert.equal(invalidSaves, 0)
assert.equal(identityRoute, '')
globalThis.uni = previousUni
console.log('Registration failure/validation failure never navigate; registration can retry without rebinding')

Loading…
Cancel
Save