Bladeren bron

update:申请试用

yonge 3 dagen geleden
bovenliggende
commit
249103f6b9
4 gewijzigde bestanden met toevoegingen van 697 en 85 verwijderingen
  1. 402 23
      pages/trial/index.js
  2. 108 38
      pages/trial/index.ttml
  3. 186 9
      pages/trial/index.ttss
  4. 1 15
      project.config.json

+ 402 - 23
pages/trial/index.js

@@ -2,11 +2,25 @@ Page({
   data: {
     formData: {
       occupation: '',
-      contactName: '',
+      name: '',
+      gender: '女',
       phone: '',
-      organization: '',
-      notes: ''
-    }
+      code: ''
+    },
+    imgCodeInput: '',
+    imgCodeImage: '',
+    showImgCodePanel: false,
+    isCodeSent: false,
+    isSendingCode: false,
+    isLoadingImgCode: false,
+    isVerifyingImgCode: false,
+    isSubmitting: false,
+    showSubmitTip: false,
+    submitTipText: '',
+    countdown: 0,
+    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) {
@@ -16,44 +30,398 @@ Page({
     this.setData({ formData: next })
   },
 
-  handleSubmit: function () {
-    const data = this.data.formData
+  handleImgCodeInput: function (e) {
+    const value = (e.detail.value || '').trim()
+    this.setData({
+      imgCodeInput: value
+    })
+
+    // 输入满足长度后自动校验,校验通过后自动发送短信
+    if (value.length >= 4) {
+      this.verifyImgCodeAndSendSms()
+    }
+  },
 
-    if (!data.occupation.trim()) {
+  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: '请填写职业',
+        title: '请先输入正确手机号',
         icon: 'none',
-        duration: 2000
+        duration: 3000
       })
       return
     }
 
-    if (!data.contactName.trim() || !data.phone.trim()) {
+    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: '请完善联系人与手机号',
+        title: '请先输入正确手机号',
         icon: 'none',
-        duration: 2000
+        duration: 3000
       })
       return
     }
 
-    tt.showToast({
-      title: '提交成功,稍后联系您',
-      icon: 'success',
-      duration: 2000
+    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 })
+      }
     })
+  },
 
-    this.setData({
-      formData: {
-        occupation: '',
-        contactName: '',
-        phone: '',
-        organization: '',
-        notes: ''
+  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'
@@ -61,5 +429,16 @@ Page({
   },
 
   handleGoTrial: function () {
+  },
+
+  onUnload: function () {
+    if (this._submitTipTimer) {
+      clearTimeout(this._submitTipTimer)
+      this._submitTipTimer = null
+    }
+    if (this._countdownTimer) {
+      clearInterval(this._countdownTimer)
+      this._countdownTimer = null
+    }
   }
 })

+ 108 - 38
pages/trial/index.ttml

@@ -1,69 +1,108 @@
 <view class="trial-page">
   <view class="hero">
     <text class="title">申请试用</text>
-    <text class="subtitle">填写以下信息,我们会尽快与您联系并安排产品试用。</text>
+    <text class="subtitle">完整真实填写以下信息,即可免费申请软件试用。</text>
   </view>
 
   <view class="form-card">
     <view class="form-item">
       <text class="label">职业</text>
-      <input
-        class="input"
-        placeholder="请输入职业"
-        value="{{formData.occupation}}"
-        data-field="occupation"
-        bindinput="handleInputChange"
-      />
+      <view class="option-group">
+        <view
+          class="option-item {{formData.occupation === '学生' ? 'option-item-active' : ''}}"
+          data-field="occupation"
+          data-value="学生"
+          bindtap="handleSelectOption"
+        >
+          学生
+        </view>
+        <view
+          class="option-item {{formData.occupation === '教师' ? 'option-item-active' : ''}}"
+          data-field="occupation"
+          data-value="教师"
+          bindtap="handleSelectOption"
+        >
+          教师
+        </view>
+      </view>
     </view>
 
     <view class="form-item">
-      <text class="label">联系人</text>
+      <text class="label">姓名</text>
       <input
         class="input"
-        placeholder="请输入联系人姓名"
-        value="{{formData.contactName}}"
-        data-field="contactName"
+        placeholder="请输入姓名"
+        value="{{formData.name}}"
+        data-field="name"
         bindinput="handleInputChange"
       />
     </view>
 
     <view class="form-item">
-      <text class="label">手机号</text>
-      <input
-        class="input"
-        type="number"
-        maxlength="11"
-        placeholder="请输入手机号"
-        value="{{formData.phone}}"
-        data-field="phone"
-        bindinput="handleInputChange"
-      />
+      <text class="label">性别</text>
+      <view class="option-group">
+        <view
+          class="option-item {{formData.gender === '男' ? 'option-item-active' : ''}}"
+          data-field="gender"
+          data-value="男"
+          bindtap="handleSelectOption"
+        >
+          男
+        </view>
+        <view
+          class="option-item {{formData.gender === '女' ? 'option-item-active' : ''}}"
+          data-field="gender"
+          data-value="女"
+          bindtap="handleSelectOption"
+        >
+          女
+        </view>
+      </view>
     </view>
 
     <view class="form-item">
-      <text class="label">单位/学校</text>
+      <text class="label">手机号</text>
+      <view class="input-with-btn">
+        <input
+          class="input input-flex"
+          type="number"
+          maxlength="11"
+          placeholder="请输入手机号"
+          value="{{formData.phone}}"
+          data-field="phone"
+          bindinput="handleInputChange"
+        />
+        <button
+          class="code-btn"
+          bindtap="handleSendCode"
+          disabled="{{isSendingCode || isLoadingImgCode || countdown > 0}}"
+        >
+          {{countdown > 0 ? (countdown + 's后重发') : '获取验证码'}}
+        </button>
+      </view>
+    </view>
+
+    <view class="form-item" tt:if="{{isCodeSent}}">
+      <text class="label">验证码</text>
       <input
         class="input"
-        placeholder="请输入单位或学校名称"
-        value="{{formData.organization}}"
-        data-field="organization"
+        type="number"
+        maxlength="6"
+        placeholder="请输入验证码"
+        value="{{formData.code}}"
+        data-field="code"
         bindinput="handleInputChange"
       />
     </view>
 
-    <view class="form-item">
-      <text class="label">试用需求</text>
-      <textarea
-        class="textarea"
-        maxlength="300"
-        placeholder="请简要描述您的使用场景和试用诉求"
-        value="{{formData.notes}}"
-        data-field="notes"
-        bindinput="handleInputChange"
-      ></textarea>
-    </view>
+    <button class="submit-btn" bindtap="handleSubmit" disabled="{{isSubmitting}}" loading="{{isSubmitting}}">提交申请</button>
 
-    <button class="submit-btn" bindtap="handleSubmit">提交申请</button>
+    <view class="tips-box">
+      <text class="tips-title">提示说明</text>
+      <text class="tips-line">1. 职业选择“学生”:请前往 APP 应用商店搜索名称为“音乐数字课堂”进行下载并体验。</text>
+      <text class="tips-line">2. 职业选择“老师”:请下载安装 Chrome 浏览器,并在地址栏输入 kt.colexiu.com 访问体验。</text>
+      <text class="tips-line">3. 同一手机号仅可申请试用一次,不可重复申请。</text>
+    </view>
   </view>
 
   <view class="page-tabbar">
@@ -74,4 +113,35 @@
       <text class="page-tab-text page-tab-text-active">申请试用</text>
     </view>
   </view>
+
+  <view class="img-code-mask" tt:if="{{showImgCodePanel}}">
+    <view class="img-code-dialog">
+      <view class="img-code-close" bindtap="handleCloseImgCodeModal">×</view>
+      <text class="img-code-title">输入图形验证码</text>
+      <view class="img-code-content">
+        <input
+          class="img-code-input"
+          maxlength="8"
+          placeholder="请输入验证码"
+          value="{{imgCodeInput}}"
+          bindinput="handleImgCodeInput"
+        />
+        <view class="img-code-right">
+          <image
+            class="img-code-image"
+            src="{{imgCodeImage}}"
+            mode="aspectFit"
+            bindtap="handleRefreshImgCode"
+          />
+          <text class="img-code-refresh" bindtap="handleRefreshImgCode">
+            {{isLoadingImgCode ? '加载中...' : '看不清?换一换'}}
+          </text>
+        </view>
+      </view>
+    </view>
+  </view>
+
+  <view class="submit-tip" tt:if="{{showSubmitTip}}">
+    <text class="submit-tip-text">{{submitTipText}}</text>
+  </view>
 </view>

+ 186 - 9
pages/trial/index.ttss

@@ -30,11 +30,38 @@
 .form-card {
   background: #ffffff;
   border-radius: 16rpx;
-  padding: 20rpx;
+  padding: 24rpx;
+  box-shadow: 0 10rpx 30rpx rgba(40, 68, 120, 0.06);
 }
 
 .form-item {
-  margin-bottom: 18rpx;
+  margin-bottom: 22rpx;
+}
+
+.option-group {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+
+.option-item {
+  min-width: 180rpx;
+  height: 76rpx;
+  line-height: 76rpx;
+  padding: 0 28rpx;
+  text-align: center;
+  background: #f7f9fc;
+  border: 1rpx solid #dfe6f2;
+  border-radius: 12rpx;
+  color: #2c3e50;
+  font-size: 24rpx;
+}
+
+.option-item-active {
+  background: #e8f1ff;
+  border-color: #4facfe;
+  color: #2363d1;
+  font-weight: 600;
 }
 
 .label {
@@ -56,20 +83,149 @@
   color: #2c3e50;
 }
 
-.textarea {
-  width: 100%;
-  height: 180rpx;
-  background: #f7f9fc;
-  border: 1rpx solid #dfe6f2;
+.input-with-btn {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+}
+
+.input-flex {
+  flex: 1;
+}
+
+.code-btn {
+  width: 190rpx;
+  height: 76rpx;
+  line-height: 76rpx;
+  padding: 0;
   border-radius: 12rpx;
+  border: 1rpx solid #4facfe;
+  background: #eef6ff;
+  color: #2363d1;
+  font-size: 22rpx;
+  font-weight: 600;
+}
+
+.code-btn[disabled] {
+  color: #a0aec0;
+  border-color: #dfe6f2;
+  background: #f5f7fb;
+}
+
+.code-btn::after {
+  border: none;
+}
+
+.img-code-mask {
+  position: fixed;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  z-index: 99;
+  background: rgba(0, 0, 0, 0.45);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0 30rpx;
+  box-sizing: border-box;
+}
+
+.img-code-dialog {
+  width: 100%;
+  max-width: 660rpx;
+  background: #ffffff;
+  border-radius: 18rpx;
+  padding: 30rpx 26rpx 26rpx;
+  position: relative;
+}
+
+.img-code-close {
+  position: absolute;
+  right: 16rpx;
+  top: 8rpx;
+  width: 52rpx;
+  height: 52rpx;
+  line-height: 52rpx;
+  text-align: center;
+  font-size: 44rpx;
+  color: #8d98a8;
+}
+
+.img-code-title {
+  display: block;
+  text-align: center;
+  color: #2c3e50;
+  font-size: 36rpx;
+  font-weight: 600;
+  margin-bottom: 24rpx;
+}
+
+.img-code-content {
+  display: flex;
+  align-items: center;
+  gap: 14rpx;
+}
+
+.img-code-input {
+  flex: 1;
+  height: 88rpx;
+  background: #f4f6fa;
+  border: 1rpx solid #e4e9f2;
+  border-radius: 10rpx;
   box-sizing: border-box;
+  padding: 0 20rpx;
+  color: #2c3e50;
+  font-size: 30rpx;
+}
+
+.img-code-right {
+  width: 250rpx;
+}
+
+.img-code-image {
+  width: 250rpx;
+  height: 88rpx;
+  border-radius: 10rpx;
+  border: 1rpx solid #dfe6f2;
+  background: #ffffff;
+}
+
+.img-code-refresh {
+  display: block;
+  text-align: center;
+  color: #8b96a7;
+  font-size: 28rpx;
+  margin-top: 14rpx;
+}
+
+.tips-box {
+  margin-top: 20rpx;
   padding: 14rpx 16rpx;
+  background: #f7faff;
+  border: 1rpx solid #cfe3ff;
+  border-radius: 10rpx;
+}
+
+.tips-title {
+  display: block;
+  color: #1f4f95;
   font-size: 24rpx;
-  color: #2c3e50;
+  font-weight: 600;
+  margin-bottom: 6rpx;
+}
+
+.tips-line {
+  display: block;
+  color: #365c97;
+  font-size: 23rpx;
+  line-height: 1.6;
+  margin-top: 4rpx;
 }
 
 .submit-btn {
-  margin-top: 14rpx;
+  margin: 34rpx auto 0;
+  width: 360rpx;
   height: 84rpx;
   border-radius: 42rpx;
   background: #4facfe;
@@ -82,3 +238,24 @@
 .submit-btn::after {
   border: none;
 }
+
+.submit-tip {
+  position: fixed;
+  left: 50%;
+  top: 50%;
+  transform: translate(-50%, -50%);
+  max-width: 620rpx;
+  background: rgba(0, 0, 0, 0.78);
+  border-radius: 12rpx;
+  padding: 14rpx 20rpx;
+  z-index: 120;
+  box-sizing: border-box;
+}
+
+.submit-tip-text {
+  display: block;
+  color: #ffffff;
+  font-size: 24rpx;
+  line-height: 1.5;
+  text-align: center;
+}

+ 1 - 15
project.config.json

@@ -1,15 +1 @@
-{
-    "setting": {
-        "urlCheck": true,
-        "es6": true,
-        "postcss": true,
-        "minified": true,
-        "newFeature": true,
-        "autoCompile": true,
-        "compileHotReLoad": true,
-        "nativeCompile": true
-    },
-    "appid": "tt8a26fc9b1447aa6a01",
-    "projectname": "yyszkt_help",
-    "douyinProjectType": "native"
-}
+{"setting":{"urlCheck":true,"es6":true,"postcss":true,"minified":true,"newFeature":true,"autoCompile":true,"compileHotReLoad":true,"nativeCompile":true},"appid":"tt8a26fc9b1447aa6a01","projectname":"yyszkt_help","douyinProjectType":"native"}