index.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. Page({
  2. data: {
  3. formData: {
  4. occupation: '',
  5. name: '',
  6. gender: '女',
  7. phone: '',
  8. code: ''
  9. },
  10. imgCodeInput: '',
  11. imgCodeImage: '',
  12. showImgCodePanel: false,
  13. isCodeSent: false,
  14. isSendingCode: false,
  15. isLoadingImgCode: false,
  16. isVerifyingImgCode: false,
  17. isSubmitting: false,
  18. showVideoModal: false,
  19. showSubmitTip: false,
  20. submitTipText: '',
  21. countdown: 0,
  22. 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',
  23. submitApi: 'https://kt.colexiu.com/edu-app/open/student/requestTrial',
  24. getImgCodeApi: 'https://kt.colexiu.com/edu-app/open/sendImgCode',
  25. sendSmsApi: 'https://kt.colexiu.com/edu-app/open/sendSmsVerify'
  26. },
  27. handleInputChange: function (e) {
  28. const field = e.currentTarget.dataset.field
  29. const value = e.detail.value
  30. const next = Object.assign({}, this.data.formData, { [field]: value })
  31. this.setData({ formData: next })
  32. },
  33. handleImgCodeInput: function (e) {
  34. const value = (e.detail.value || '').trim()
  35. this.setData({
  36. imgCodeInput: value
  37. })
  38. // 输入满足长度后自动校验,校验通过后自动发送短信
  39. if (value.length >= 4) {
  40. this.verifyImgCodeAndSendSms()
  41. }
  42. },
  43. handleSelectOption: function (e) {
  44. const field = e.currentTarget.dataset.field
  45. const value = e.currentTarget.dataset.value
  46. const next = Object.assign({}, this.data.formData, { [field]: value })
  47. this.setData({ formData: next })
  48. },
  49. handleSendCode: function () {
  50. const phone = (this.data.formData.phone || '').replace(/\s+/g, '')
  51. if (!/^1[3-9]\d{9}$/.test(phone)) {
  52. tt.showToast({
  53. title: '请先输入正确手机号',
  54. icon: 'none',
  55. duration: 3000
  56. })
  57. return
  58. }
  59. if (this.data.isSendingCode || this.data.isLoadingImgCode || this.data.countdown > 0) {
  60. return
  61. }
  62. // 先展示图形验证码区域,再拉取图片
  63. this.setData({
  64. showImgCodePanel: true,
  65. imgCodeInput: '',
  66. imgCodeImage: '',
  67. isLoadingImgCode: true
  68. })
  69. this.requestImgCode()
  70. },
  71. requestImgCode: function () {
  72. const phone = (this.data.formData.phone || '').replace(/\s+/g, '')
  73. tt.request({
  74. url: this.data.getImgCodeApi,
  75. method: 'GET',
  76. data: {
  77. phone: phone
  78. },
  79. success: (res) => {
  80. if (res.statusCode !== 200) {
  81. tt.showToast({
  82. title: this.getResponseMessage(res, '获取图形验证码失败'),
  83. icon: 'none',
  84. duration: 3000
  85. })
  86. return
  87. }
  88. const base64 = this.getImgCodeBase64(res)
  89. if (!base64) {
  90. tt.showToast({
  91. title: '图形验证码加载失败,请点击图片重试',
  92. icon: 'none',
  93. duration: 3000
  94. })
  95. return
  96. }
  97. this.setData({
  98. showImgCodePanel: true,
  99. imgCodeImage: base64.startsWith('data:image') ? base64 : `data:image/png;base64,${base64}`,
  100. imgCodeInput: ''
  101. })
  102. },
  103. fail: (_err) => {
  104. tt.showToast({
  105. title: '获取图形验证码失败',
  106. icon: 'none',
  107. duration: 3000
  108. })
  109. },
  110. complete: () => {
  111. this.setData({ isLoadingImgCode: false })
  112. }
  113. })
  114. },
  115. handleRefreshImgCode: function () {
  116. if (this.data.isLoadingImgCode || this.data.isVerifyingImgCode) {
  117. return
  118. }
  119. this.setData({
  120. imgCodeInput: '',
  121. isLoadingImgCode: true
  122. })
  123. this.requestImgCode()
  124. },
  125. handleCloseImgCodeModal: function () {
  126. this.setData({
  127. showImgCodePanel: false,
  128. imgCodeInput: '',
  129. imgCodeImage: ''
  130. })
  131. },
  132. verifyImgCodeAndSendSms: function () {
  133. const phone = (this.data.formData.phone || '').replace(/\s+/g, '')
  134. const imgCode = (this.data.imgCodeInput || '').trim()
  135. if (!/^1[3-9]\d{9}$/.test(phone)) {
  136. tt.showToast({
  137. title: '请先输入正确手机号',
  138. icon: 'none',
  139. duration: 3000
  140. })
  141. return
  142. }
  143. if (imgCode.length < 4) {
  144. return
  145. }
  146. if (this.data.isSendingCode || this.data.isVerifyingImgCode) {
  147. return
  148. }
  149. this.setData({ isVerifyingImgCode: true })
  150. tt.request({
  151. url: this.data.sendSmsApi,
  152. method: 'POST',
  153. header: {
  154. 'Content-Type': 'application/x-www-form-urlencoded'
  155. },
  156. data: {
  157. mobile: phone,
  158. type: 'REGISTER',
  159. clientId: 'BACKEND',
  160. code: imgCode
  161. },
  162. success: (res) => {
  163. const bizSuccess = !!(res && res.data && Number(res.data.code) === 200)
  164. if (!bizSuccess) {
  165. tt.showToast({
  166. title: this.getResponseMessage(res, '图形验证码错误'),
  167. icon: 'none',
  168. duration: 3000
  169. })
  170. this.refreshImgCodeAfterVerifyFail()
  171. return
  172. }
  173. this.setData({
  174. isCodeSent: true,
  175. showImgCodePanel: false,
  176. imgCodeInput: ''
  177. })
  178. this.startCountdown(60)
  179. tt.showToast({
  180. title: '验证码已发送',
  181. icon: 'none',
  182. duration: 3000
  183. })
  184. },
  185. fail: (_err) => {
  186. tt.showToast({
  187. title: '发送失败,请稍后重试',
  188. icon: 'none',
  189. duration: 3000
  190. })
  191. this.refreshImgCodeAfterVerifyFail()
  192. },
  193. complete: () => {
  194. this.setData({ isVerifyingImgCode: false })
  195. }
  196. })
  197. },
  198. startCountdown: function (seconds) {
  199. if (this._countdownTimer) {
  200. clearInterval(this._countdownTimer)
  201. }
  202. this.setData({ countdown: seconds })
  203. this._countdownTimer = setInterval(() => {
  204. const next = this.data.countdown - 1
  205. if (next <= 0) {
  206. clearInterval(this._countdownTimer)
  207. this._countdownTimer = null
  208. this.setData({ countdown: 0 })
  209. return
  210. }
  211. this.setData({ countdown: next })
  212. }, 1000)
  213. },
  214. handleSubmit: function () {
  215. if (this.data.isSubmitting) {
  216. return
  217. }
  218. const data = this.data.formData
  219. const occupation = (data.occupation || '').trim()
  220. const name = (data.name || '').trim()
  221. const gender = (data.gender || '').trim()
  222. const phone = (data.phone || '').replace(/\s+/g, '')
  223. const code = (data.code || '').trim()
  224. console.log('[TRIAL_FLOW] SUBMIT_CLICK', { occupation: occupation, gender: gender })
  225. if (!occupation) {
  226. this.showSubmitTip('请选择职业')
  227. return
  228. }
  229. if (!name) {
  230. this.showSubmitTip('请输入姓名')
  231. return
  232. }
  233. if (!gender) {
  234. this.showSubmitTip('请选择性别')
  235. return
  236. }
  237. if (!/^1[3-9]\d{9}$/.test(phone)) {
  238. this.showSubmitTip('请输入正确的11位手机号')
  239. return
  240. }
  241. if (!code) {
  242. this.showSubmitTip('请输入短信验证码')
  243. return
  244. }
  245. this.setData({ isSubmitting: true })
  246. this.submitTrial({
  247. occupation: occupation,
  248. name: name,
  249. gender: gender,
  250. phone: phone,
  251. code: code
  252. })
  253. },
  254. submitTrial: function (params) {
  255. tt.request({
  256. url: this.data.submitApi,
  257. method: 'POST',
  258. header: {
  259. 'Content-Type': 'application/x-www-form-urlencoded'
  260. },
  261. data: {
  262. occupation: params.occupation,
  263. name: params.name,
  264. gender: params.gender,
  265. phone: params.phone,
  266. code: params.code
  267. },
  268. success: (res) => {
  269. const bizSuccess = this.isApiSuccess(res)
  270. if (!bizSuccess) {
  271. this.showSubmitTip(this.getResponseMessage(res, '提交失败,请稍后重试'))
  272. return
  273. }
  274. this.showSubmitTip(this.getResponseMessage(res, '提交成功'))
  275. this.setData({
  276. formData: {
  277. occupation: '',
  278. name: '',
  279. gender: '女',
  280. phone: '',
  281. code: ''
  282. },
  283. imgCodeInput: '',
  284. imgCodeImage: '',
  285. showImgCodePanel: false,
  286. isCodeSent: false
  287. })
  288. },
  289. fail: (_err) => {
  290. this.showSubmitTip('提交失败,请稍后重试')
  291. },
  292. complete: () => {
  293. this.setData({ isSubmitting: false })
  294. }
  295. })
  296. },
  297. getResponseMessage: function (res, fallback) {
  298. if (!res || !res.data) {
  299. return fallback
  300. }
  301. if (typeof res.data === 'string') {
  302. return res.data || fallback
  303. }
  304. return res.data.message || res.data.msg || fallback
  305. },
  306. isApiSuccess: function (res) {
  307. if (!res || res.statusCode !== 200) {
  308. return false
  309. }
  310. const data = res.data
  311. if (data === undefined || data === null || data === '') {
  312. return true
  313. }
  314. const failPattern = /(错误|失败|无效|过期|不存在|required|invalid|unauthorized|forbidden|denied|error|fail)/i
  315. if (typeof data === 'string') {
  316. return !failPattern.test(data)
  317. }
  318. if (typeof data !== 'object') {
  319. return true
  320. }
  321. if (typeof data.success === 'boolean') {
  322. return data.success
  323. }
  324. if (data.code !== undefined && data.code !== null && data.code !== '') {
  325. const code = String(data.code)
  326. if (code !== '0' && code !== '200') {
  327. return false
  328. }
  329. }
  330. if (data.status !== undefined && data.status !== null && data.status !== '') {
  331. const status = String(data.status)
  332. if (status !== '0' && status !== '200' && status.toLowerCase() !== 'success') {
  333. return false
  334. }
  335. }
  336. const msg = (data.message || data.msg || data.error || '').toString()
  337. if (msg && failPattern.test(msg)) {
  338. return false
  339. }
  340. return true
  341. },
  342. getImgCodeBase64: function (res) {
  343. if (!res || !res.data) {
  344. return ''
  345. }
  346. // 按接口约定:图片 base64 在 res.data.data
  347. if (typeof res.data.data === 'string') {
  348. return res.data.data.trim()
  349. }
  350. // 仅保留一个最小兜底:res.data 本身是字符串
  351. if (typeof res.data === 'string') {
  352. return res.data.trim()
  353. }
  354. return ''
  355. },
  356. refreshImgCodeAfterVerifyFail: function () {
  357. // 图形码失败后只刷新图片并等待用户再次输入,不触发短信发送
  358. this.setData({
  359. imgCodeInput: '',
  360. isLoadingImgCode: true
  361. })
  362. this.requestImgCode()
  363. },
  364. showSubmitTip: function (text) {
  365. if (this._submitTipTimer) {
  366. clearTimeout(this._submitTipTimer)
  367. }
  368. this.setData({
  369. showSubmitTip: true,
  370. submitTipText: text || ''
  371. })
  372. this._submitTipTimer = setTimeout(() => {
  373. this.setData({
  374. showSubmitTip: false,
  375. submitTipText: ''
  376. })
  377. this._submitTipTimer = null
  378. }, 3000)
  379. },
  380. handleGoProduct: function () {
  381. tt.redirectTo({
  382. url: '/pages/product/index'
  383. })
  384. },
  385. handleGoTrial: function () {
  386. },
  387. handleOpenVideo: function () {
  388. this.setData({
  389. showVideoModal: true
  390. }, () => {
  391. if (this.videoContext) {
  392. this.videoContext.play()
  393. }
  394. })
  395. },
  396. onReady: function () {
  397. this.videoContext = tt.createVideoContext('trialVideo', this)
  398. },
  399. handleCloseVideo: function () {
  400. if (this.videoContext) {
  401. this.videoContext.pause()
  402. }
  403. this.setData({
  404. showVideoModal: false
  405. })
  406. },
  407. noop: function () {
  408. },
  409. onUnload: function () {
  410. if (this._submitTipTimer) {
  411. clearTimeout(this._submitTipTimer)
  412. this._submitTipTimer = null
  413. }
  414. if (this._countdownTimer) {
  415. clearInterval(this._countdownTimer)
  416. this._countdownTimer = null
  417. }
  418. if (this.videoContext) {
  419. this.videoContext.pause()
  420. }
  421. }
  422. })