index.js 11 KB

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