Page({ data: { formData: { occupation: '', name: '', gender: '女', phone: '', code: '' }, imgCodeInput: '', imgCodeImage: '', showImgCodePanel: false, isCodeSent: false, isSendingCode: false, isLoadingImgCode: false, isVerifyingImgCode: false, isSubmitting: false, showVideoModal: false, showSubmitTip: false, submitTipText: '', countdown: 0, trialVideoUrl: 'https://daya-online-1303457149.cos.ap-nanjing.myqcloud.com/product-video/%E8%80%81%E5%B8%88%E7%AB%AF%E5%A6%82%E4%BD%95%E7%99%BB%E5%BD%95%E4%BD%BF%E7%94%A8.mp4', submitApi: 'https://kt.colexiu.com/edu-app/open/student/requestTrial', getImgCodeApi: 'https://kt.colexiu.com/edu-app/open/sendImgCode', sendSmsApi: 'https://kt.colexiu.com/edu-app/open/sendSmsVerify' }, handleInputChange: function (e) { const field = e.currentTarget.dataset.field const value = e.detail.value const next = Object.assign({}, this.data.formData, { [field]: value }) this.setData({ formData: next }) }, handleImgCodeInput: function (e) { const value = (e.detail.value || '').trim() this.setData({ imgCodeInput: value }) // 输入满足长度后自动校验,校验通过后自动发送短信 if (value.length >= 4) { this.verifyImgCodeAndSendSms() } }, handleSelectOption: function (e) { const field = e.currentTarget.dataset.field const value = e.currentTarget.dataset.value const next = Object.assign({}, this.data.formData, { [field]: value }) this.setData({ formData: next }) }, handleSendCode: function () { const phone = (this.data.formData.phone || '').replace(/\s+/g, '') if (!/^1[3-9]\d{9}$/.test(phone)) { tt.showToast({ title: '请先输入正确手机号', icon: 'none', duration: 3000 }) return } if (this.data.isSendingCode || this.data.isLoadingImgCode || this.data.countdown > 0) { return } // 先展示图形验证码区域,再拉取图片 this.setData({ showImgCodePanel: true, imgCodeInput: '', imgCodeImage: '', isLoadingImgCode: true }) this.requestImgCode() }, requestImgCode: function () { const phone = (this.data.formData.phone || '').replace(/\s+/g, '') tt.request({ url: this.data.getImgCodeApi, method: 'GET', data: { phone: phone }, success: (res) => { if (res.statusCode !== 200) { tt.showToast({ title: this.getResponseMessage(res, '获取图形验证码失败'), icon: 'none', duration: 3000 }) return } const base64 = this.getImgCodeBase64(res) if (!base64) { tt.showToast({ title: '图形验证码加载失败,请点击图片重试', icon: 'none', duration: 3000 }) return } this.setData({ showImgCodePanel: true, imgCodeImage: base64.startsWith('data:image') ? base64 : `data:image/png;base64,${base64}`, imgCodeInput: '' }) }, fail: (_err) => { tt.showToast({ title: '获取图形验证码失败', icon: 'none', duration: 3000 }) }, complete: () => { this.setData({ isLoadingImgCode: false }) } }) }, handleRefreshImgCode: function () { if (this.data.isLoadingImgCode || this.data.isVerifyingImgCode) { return } this.setData({ imgCodeInput: '', isLoadingImgCode: true }) this.requestImgCode() }, handleCloseImgCodeModal: function () { this.setData({ showImgCodePanel: false, imgCodeInput: '', imgCodeImage: '' }) }, verifyImgCodeAndSendSms: function () { const phone = (this.data.formData.phone || '').replace(/\s+/g, '') const imgCode = (this.data.imgCodeInput || '').trim() if (!/^1[3-9]\d{9}$/.test(phone)) { tt.showToast({ title: '请先输入正确手机号', icon: 'none', duration: 3000 }) return } if (imgCode.length < 4) { return } if (this.data.isSendingCode || this.data.isVerifyingImgCode) { return } this.setData({ isVerifyingImgCode: true }) tt.request({ url: this.data.sendSmsApi, method: 'POST', header: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: { mobile: phone, type: 'REGISTER', clientId: 'BACKEND', code: imgCode }, success: (res) => { const bizSuccess = !!(res && res.data && Number(res.data.code) === 200) if (!bizSuccess) { tt.showToast({ title: this.getResponseMessage(res, '图形验证码错误'), icon: 'none', duration: 3000 }) this.refreshImgCodeAfterVerifyFail() return } this.setData({ isCodeSent: true, showImgCodePanel: false, imgCodeInput: '' }) this.startCountdown(60) tt.showToast({ title: '验证码已发送', icon: 'none', duration: 3000 }) }, fail: (_err) => { tt.showToast({ title: '发送失败,请稍后重试', icon: 'none', duration: 3000 }) this.refreshImgCodeAfterVerifyFail() }, complete: () => { this.setData({ isVerifyingImgCode: false }) } }) }, startCountdown: function (seconds) { if (this._countdownTimer) { clearInterval(this._countdownTimer) } this.setData({ countdown: seconds }) this._countdownTimer = setInterval(() => { const next = this.data.countdown - 1 if (next <= 0) { clearInterval(this._countdownTimer) this._countdownTimer = null this.setData({ countdown: 0 }) return } this.setData({ countdown: next }) }, 1000) }, handleSubmit: function () { if (this.data.isSubmitting) { return } const data = this.data.formData const occupation = (data.occupation || '').trim() const name = (data.name || '').trim() const gender = (data.gender || '').trim() const phone = (data.phone || '').replace(/\s+/g, '') const code = (data.code || '').trim() console.log('[TRIAL_FLOW] SUBMIT_CLICK', { occupation: occupation, gender: gender }) if (!occupation) { this.showSubmitTip('请选择职业') return } if (!name) { this.showSubmitTip('请输入姓名') return } if (!gender) { this.showSubmitTip('请选择性别') return } if (!/^1[3-9]\d{9}$/.test(phone)) { this.showSubmitTip('请输入正确的11位手机号') return } if (!code) { this.showSubmitTip('请输入短信验证码') return } this.setData({ isSubmitting: true }) this.submitTrial({ occupation: occupation, name: name, gender: gender, phone: phone, code: code }) }, submitTrial: function (params) { tt.request({ url: this.data.submitApi, method: 'POST', header: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: { occupation: params.occupation, name: params.name, gender: params.gender, phone: params.phone, code: params.code }, success: (res) => { const bizSuccess = this.isApiSuccess(res) if (!bizSuccess) { this.showSubmitTip(this.getResponseMessage(res, '提交失败,请稍后重试')) return } this.showSubmitTip(this.getResponseMessage(res, '提交成功')) this.setData({ formData: { occupation: '', name: '', gender: '女', phone: '', code: '' }, imgCodeInput: '', imgCodeImage: '', showImgCodePanel: false, isCodeSent: false }) }, fail: (_err) => { this.showSubmitTip('提交失败,请稍后重试') }, complete: () => { this.setData({ isSubmitting: false }) } }) }, getResponseMessage: function (res, fallback) { if (!res || !res.data) { return fallback } if (typeof res.data === 'string') { return res.data || fallback } return res.data.message || res.data.msg || fallback }, isApiSuccess: function (res) { if (!res || res.statusCode !== 200) { return false } const data = res.data if (data === undefined || data === null || data === '') { return true } const failPattern = /(错误|失败|无效|过期|不存在|required|invalid|unauthorized|forbidden|denied|error|fail)/i if (typeof data === 'string') { return !failPattern.test(data) } if (typeof data !== 'object') { return true } if (typeof data.success === 'boolean') { return data.success } if (data.code !== undefined && data.code !== null && data.code !== '') { const code = String(data.code) if (code !== '0' && code !== '200') { return false } } if (data.status !== undefined && data.status !== null && data.status !== '') { const status = String(data.status) if (status !== '0' && status !== '200' && status.toLowerCase() !== 'success') { return false } } const msg = (data.message || data.msg || data.error || '').toString() if (msg && failPattern.test(msg)) { return false } return true }, getImgCodeBase64: function (res) { if (!res || !res.data) { return '' } // 按接口约定:图片 base64 在 res.data.data if (typeof res.data.data === 'string') { return res.data.data.trim() } // 仅保留一个最小兜底:res.data 本身是字符串 if (typeof res.data === 'string') { return res.data.trim() } return '' }, refreshImgCodeAfterVerifyFail: function () { // 图形码失败后只刷新图片并等待用户再次输入,不触发短信发送 this.setData({ imgCodeInput: '', isLoadingImgCode: true }) this.requestImgCode() }, showSubmitTip: function (text) { if (this._submitTipTimer) { clearTimeout(this._submitTipTimer) } this.setData({ showSubmitTip: true, submitTipText: text || '' }) this._submitTipTimer = setTimeout(() => { this.setData({ showSubmitTip: false, submitTipText: '' }) this._submitTipTimer = null }, 3000) }, handleGoProduct: function () { tt.redirectTo({ url: '/pages/product/index' }) }, handleGoTrial: function () { }, handleOpenVideo: function () { this.setData({ showVideoModal: true }, () => { if (this.videoContext) { this.videoContext.play() } }) }, onReady: function () { this.videoContext = tt.createVideoContext('trialVideo', this) }, handleCloseVideo: function () { if (this.videoContext) { this.videoContext.pause() } this.setData({ showVideoModal: false }) }, noop: function () { }, onUnload: function () { if (this._submitTipTimer) { clearTimeout(this._submitTipTimer) this._submitTipTimer = null } if (this._countdownTimer) { clearInterval(this._countdownTimer) this._countdownTimer = null } if (this.videoContext) { this.videoContext.pause() } } })