Parcourir la source

完成国开大功能

yuanmingze il y a 1 mois
Parent
commit
99a3a266ee

+ 36 - 0
src/pages.json

@@ -14,6 +14,42 @@
         "navigationStyle": "custom"
       }
     },
+    {
+      "path": "pages/compliance-training/index",
+      "style": {
+        "navigationBarTitleText": "医药代表合规培训",
+        "navigationBarTextStyle": "black",
+        "navigationBarBackgroundColor": "#ffffff",
+        "backgroundColor": "#f4f5f7",
+        "h5": {
+          "titleNView": false
+        }
+      }
+    },
+    {
+      "path": "pages/compliance-training/plan",
+      "style": {
+        "navigationBarTitleText": "合规上岗 · 专属方案",
+        "navigationBarTextStyle": "black",
+        "navigationBarBackgroundColor": "#ffffff",
+        "backgroundColor": "#f4f5f7",
+        "h5": {
+          "titleNView": false
+        }
+      }
+    },
+    {
+      "path": "pages/compliance-training/registration",
+      "style": {
+        "navigationBarTitleText": "医药代表合规培训报名表",
+        "navigationBarTextStyle": "black",
+        "navigationBarBackgroundColor": "#ffffff",
+        "backgroundColor": "#f4f5f7",
+        "h5": {
+          "titleNView": false
+        }
+      }
+    },
 
     {
       "path": "pages/information/index",

+ 339 - 0
src/pages/compliance-training/index.vue

@@ -0,0 +1,339 @@
+<template>
+  <view class="entry-page">
+    <!-- #ifdef H5 -->
+    <view class="h5-page-header">
+      <view class="header-back" @click="goBack">
+        <wd-icon name="left" size="36rpx" color="#26313f" />
+      </view>
+      <text>医药代表合规培训</text>
+    </view>
+    <!-- #endif -->
+
+    <view class="hero">
+      <view class="hero-tag-wrap">
+        <view class="hero-tag">国家级高校培训中心 × 药企 联合开展</view>
+      </view>
+      <view class="hero-title">你想怎么开始?</view>
+      <view class="hero-subtitle">两种方式,按需选择,都不影响后续更改</view>
+    </view>
+
+    <view class="entry-content">
+      <view class="entry-card">
+        <view class="card-header">
+          <view class="card-icon card-icon-plan">💬</view>
+          <view class="card-heading">
+            <view class="card-title">先留信息,领取我的专属方案</view>
+            <view class="card-subtitle">还不确定?留个联系方式,顾问为你解答</view>
+          </view>
+        </view>
+
+        <view class="feature-list">
+          <view class="feature-item" v-for="item in planFeatures" :key="item">
+            <text class="feature-check">✓</text>
+            <text>{{ item }}</text>
+          </view>
+        </view>
+
+        <button
+          class="action-button action-button-outline"
+          hover-class="action-button-outline-hover"
+          @click="goTo('/pages/compliance-training/plan')"
+        >
+          领取我的专属方案 →
+        </button>
+      </view>
+
+      <view class="entry-card registration-card">
+        <view class="card-header">
+          <view class="card-icon card-icon-registration">📝</view>
+          <view class="card-heading">
+            <view class="card-title">我已决定,直接报名</view>
+            <view class="card-subtitle">目标明确?现在登记,尽快开班</view>
+          </view>
+        </view>
+
+        <view class="feature-list">
+          <view class="feature-item" v-for="item in registrationFeatures" :key="item">
+            <text class="feature-check">✓</text>
+            <text>{{ item }}</text>
+          </view>
+        </view>
+
+        <button
+          class="action-button action-button-primary"
+          hover-class="action-button-primary-hover"
+          @click="goTo('/pages/compliance-training/registration')"
+        >
+          去报名 →
+        </button>
+      </view>
+
+      <view class="contact-line">
+        <text>企业集体报名?请致电</text>
+        <text class="contact-phone" @click="callService">400-639-1811</text>
+      </view>
+      <view class="record-count">全国已有 280,000+ 医药代表完成备案</view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+
+import { onLoad } from '@dcloudio/uni-app'
+
+const planFeatures = [
+  '只需填写基本信息,30 秒完成',
+  '顾问 1 对 1 回电,为你匹配专属方案',
+  '不着急报名,了解清楚再说',
+]
+const registrationFeatures = [
+  '填写报名信息,上传两寸证件照',
+  '教务老师 1 个工作日内审核并联系你',
+  '开通课程,培训合格双证上岗',
+]
+const pageOptions = ref<Record<string, string>>({})
+
+const goBack = () => {
+  uni.navigateBack()
+}
+
+const buildTargetUrl = (path: string): string => {
+  const query = Object.entries(pageOptions.value)
+    .filter(([, value]) => value !== undefined && value !== null)
+    .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
+    .join('&')
+
+  return query ? `${path}?${query}` : path
+}
+
+const goTo = (path: string) => {
+  uni.navigateTo({
+    url: buildTargetUrl(path),
+  })
+}
+
+const callService = () => {
+  uni.makePhoneCall({
+    phoneNumber: '400-639-1811',
+  })
+}
+
+onLoad((options?: Record<string, string>) => {
+  pageOptions.value = { ...(options || {}) }
+})
+</script>
+
+<style lang="scss" scoped>
+.entry-page {
+  min-height: 100vh;
+  background: #f4f5f7;
+  color: #252f3d;
+}
+
+.h5-page-header {
+  position: sticky;
+  top: 0;
+  z-index: 100;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 98rpx;
+  border-bottom: 1rpx solid #eeeeee;
+  background: #ffffff;
+  color: #252f3d;
+  font-size: 34rpx;
+  font-weight: 700;
+  box-sizing: border-box;
+}
+
+.header-back {
+  position: absolute;
+  left: 24rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 52rpx;
+  height: 70rpx;
+}
+
+.hero {
+  min-height: 282rpx;
+  padding: 48rpx 40rpx 42rpx;
+  background: linear-gradient(135deg, #1754ad 0%, #1766d3 54%, #2380f8 100%);
+  color: #ffffff;
+  box-sizing: border-box;
+}
+
+.hero-tag-wrap {
+  display: flex;
+}
+
+.hero-tag {
+  padding: 7rpx 18rpx;
+  border: 1rpx solid rgba(255, 211, 0, 0.72);
+  border-radius: 30rpx;
+  color: #ffd500;
+  font-size: 24rpx;
+  font-weight: 700;
+  line-height: 30rpx;
+}
+
+.hero-title {
+  margin-top: 27rpx;
+  font-size: 48rpx;
+  font-weight: 700;
+  line-height: 66rpx;
+}
+
+.hero-subtitle {
+  margin-top: 6rpx;
+  color: rgba(255, 255, 255, 0.72);
+  font-size: 27rpx;
+  line-height: 40rpx;
+}
+
+.entry-content {
+  padding: 32rpx 24rpx 48rpx;
+}
+
+.entry-card {
+  padding: 34rpx 32rpx 35rpx;
+  border-radius: 22rpx;
+  background: #ffffff;
+  box-shadow: 0 7rpx 18rpx rgba(32, 46, 66, 0.09);
+}
+
+.registration-card {
+  margin-top: 26rpx;
+}
+
+.card-header {
+  display: flex;
+  align-items: center;
+}
+
+.card-icon {
+  display: flex;
+  flex: 0 0 88rpx;
+  align-items: center;
+  justify-content: center;
+  width: 88rpx;
+  height: 88rpx;
+  margin-right: 20rpx;
+  border-radius: 22rpx;
+  font-size: 45rpx;
+  line-height: 1;
+}
+
+.card-icon-plan {
+  background: #e8f2ff;
+}
+
+.card-icon-registration {
+  background: #fff3d7;
+}
+
+.card-heading {
+  flex: 1;
+  min-width: 0;
+}
+
+.card-title {
+  color: #202a38;
+  font-size: 33rpx;
+  font-weight: 700;
+  line-height: 46rpx;
+}
+
+.card-subtitle {
+  margin-top: 3rpx;
+  color: #a7afba;
+  font-size: 24rpx;
+  line-height: 35rpx;
+}
+
+.feature-list {
+  margin-top: 27rpx;
+}
+
+.feature-item {
+  display: flex;
+  align-items: flex-start;
+  color: #586475;
+  font-size: 26rpx;
+  line-height: 46rpx;
+}
+
+.feature-check {
+  flex: 0 0 31rpx;
+  margin-right: 1rpx;
+  color: #39bd20;
+  font-size: 32rpx;
+  font-weight: 700;
+  line-height: 44rpx;
+}
+
+.action-button {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 92rpx;
+  margin: 29rpx 0 0;
+  padding: 0;
+  border-radius: 48rpx;
+  font-size: 31rpx;
+  font-weight: 700;
+  line-height: 92rpx;
+  box-sizing: border-box;
+
+  &::after {
+    border: none;
+  }
+}
+
+.action-button-outline {
+  border: 2rpx solid #1678f4;
+  background: #ffffff;
+  color: #1678f4;
+}
+
+.action-button-outline-hover {
+  background: #f0f6ff;
+}
+
+.action-button-primary {
+  background: #247af2;
+  box-shadow: 0 11rpx 22rpx rgba(28, 116, 235, 0.25);
+  color: #ffffff;
+}
+
+.action-button-primary-hover {
+  background: #176de4;
+}
+
+.contact-line {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-top: 29rpx;
+  color: #626d7c;
+  font-size: 25rpx;
+  line-height: 38rpx;
+}
+
+.contact-phone {
+  margin-left: 6rpx;
+  color: #1678f4;
+  font-weight: 700;
+}
+
+.record-count {
+  margin-top: 24rpx;
+  color: #abb3bf;
+  font-size: 22rpx;
+  line-height: 34rpx;
+  text-align: center;
+}
+</style>

+ 628 - 0
src/pages/compliance-training/plan.vue

@@ -0,0 +1,628 @@
+<template>
+  <view class="plan-page">
+    <!-- #ifdef H5 -->
+    <view class="h5-page-header">合规上岗 · 专属方案</view>
+    <!-- #endif -->
+
+    <view class="hero">
+      <view class="hero-glow hero-glow-top"></view>
+      <view class="hero-glow hero-glow-bottom"></view>
+      <view class="hero-content">
+        <view class="hero-tag">国家级高校培训中心 × 药企 联合开展</view>
+        <view class="hero-kicker">医药代表</view>
+        <view class="hero-title">
+          <text>合规上岗 × 学历提升</text>
+          <text class="hero-title-tail">一站搞定</text>
+        </view>
+        <view class="hero-description">考培分离 · 双证上岗 · 成果存入学分银行可换学历学分</view>
+        <view class="hero-description hero-description-last">线上学习不脱产 · 学信网可查</view>
+      </view>
+      <view class="hero-provider-note">培训服务及学历证书由国家级高校培训中心提供及颁发</view>
+    </view>
+
+    <view class="page-content">
+      <view class="section-title">
+        <view class="section-title-line"></view>
+        <text>你能带走什么</text>
+      </view>
+
+      <view class="benefit-list">
+        <view class="benefit-card" v-for="item in benefits" :key="item.title">
+          <view class="benefit-icon">{{ item.icon }}</view>
+          <view class="benefit-content">
+            <view class="benefit-title">{{ item.title }}</view>
+            <view class="benefit-description">{{ item.description }}</view>
+          </view>
+        </view>
+      </view>
+
+      <view class="section-title form-section-title">
+        <view class="section-title-line"></view>
+        <text>免费获取我的专属方案</text>
+      </view>
+
+      <view class="form-card">
+        <view class="field">
+          <view class="field-label">您的称呼 <text class="required">*</text></view>
+          <input
+            v-model="form.name"
+            class="field-input"
+            type="text"
+            :maxlength="50"
+            placeholder="怎么称呼您"
+            placeholder-class="field-placeholder"
+          />
+        </view>
+
+        <view class="field">
+          <view class="field-label">手机号 <text class="required">*</text></view>
+          <input
+            :value="form.phone"
+            class="field-input"
+            type="number"
+            :maxlength="11"
+            placeholder="用于接收方案与开课通知"
+            placeholder-class="field-placeholder"
+            @input="onPhoneInput"
+          />
+        </view>
+
+        <view class="field">
+          <view class="field-label">
+            所在企业
+            <text class="field-hint">选填 · 方便匹配企业集体报名通道</text>
+          </view>
+          <input
+            v-model="form.company"
+            class="field-input"
+            type="text"
+            :maxlength="50"
+            placeholder="请输入企业名称"
+            placeholder-class="field-placeholder"
+          />
+        </view>
+
+        <view class="field">
+          <view class="field-label">
+            您目前的学历 <text class="required">*</text>
+            <text class="field-hint">单选</text>
+          </view>
+          <view class="option-list">
+            <view
+              v-for="item in educationOptions"
+              :key="item"
+              class="option-pill"
+              :class="{ 'option-pill-active': form.education === item }"
+              hover-class="option-pill-hover"
+              @click="selectEducation(item)"
+            >
+              {{ item }}
+            </view>
+          </view>
+        </view>
+
+        <view class="field interest-field">
+          <view class="field-label">
+            我最想了解 <text class="required">*</text>
+            <text class="field-hint">可多选</text>
+          </view>
+          <view class="option-list">
+            <view
+              v-for="item in interestOptions"
+              :key="item"
+              class="option-pill"
+              :class="{ 'option-pill-active': selectedInterests.indexOf(item) !== -1 }"
+              hover-class="option-pill-hover"
+              @click="toggleInterest(item)"
+            >
+              {{ item }}
+            </view>
+          </view>
+        </view>
+
+        <button
+          class="submit-button"
+          :class="{ 'submit-button-disabled': submitting || submitted }"
+          :disabled="submitting || submitted"
+          hover-class="submit-button-hover"
+          @click="submit"
+        >
+          {{ submitButtonText }}
+        </button>
+        <view class="privacy-note">
+          <view>提交即视为同意我们就本培训项目与您联系</view>
+          <view>信息仅用于方案匹配,不会用于其他用途</view>
+        </view>
+      </view>
+
+      <view class="service-phone">服务热线:400-639-1811</view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { computed, reactive, ref } from 'vue'
+
+import { onLoad } from '@dcloudio/uni-app'
+
+import {
+  type CompliancePlanPayload,
+  submitInfoCollectionApi,
+} from '@/services/modules/infoCollection'
+
+const benefits = [
+  {
+    icon: '📜',
+    title: '双证上岗',
+    description:
+      '国家级高校培训中心《医药代表合规培训结业证书》+企业《考核合格证明》,官网可查、档案留痕',
+  },
+  {
+    icon: '🎓',
+    title: '学历直达',
+    description:
+      '免试注册入学国家级高校药学专业(专科/专升本),学信网终身可查,正好满足新规学历门槛',
+  },
+  {
+    icon: '💳',
+    title: '学分银行',
+    description: '培训合格即开通终身学习账户,成果可申请学历课程免修免考——读学历少修课、早毕业',
+  },
+  {
+    icon: '🩺',
+    title: '陪诊顾问职业证书(增值可选)',
+    description: '付费增值项:按需加购《医疗陪诊顾问职业技能培训证书》,多一条职业曲线',
+  },
+]
+
+const educationOptions = ['医药相关 大专及以上', '非相关专业 大专', '大专以下 / 在读']
+const interestOptions = [
+  '合规培训双证',
+  '学历提升(高校药学)',
+  '陪诊顾问职业证书',
+  '公司集体报名咨询',
+]
+
+const selectedInterests = ref<string[]>([])
+const form = reactive({
+  name: '',
+  phone: '',
+  company: '',
+  education: '',
+})
+const submitting = ref(false)
+const submitted = ref(false)
+const channel = ref('miniapp')
+
+const submitButtonText = computed(() => {
+  if (submitted.value) return '提交成功'
+  return submitting.value ? '正在提交...' : '领取我的提升方案'
+})
+
+const onPhoneInput = (event: { detail: { value?: string | number } }) => {
+  const phone = String(event.detail.value || '')
+    .replace(/\D/g, '')
+    .slice(0, 11)
+  form.phone = phone
+  return phone
+}
+
+const selectEducation = (item: string) => {
+  form.education = item
+}
+
+const toggleInterest = (item: string) => {
+  const index = selectedInterests.value.indexOf(item)
+
+  if (index === -1) {
+    selectedInterests.value.push(item)
+  } else {
+    selectedInterests.value.splice(index, 1)
+  }
+}
+
+const showToast = (title: string) => {
+  uni.showToast({
+    title,
+    icon: 'none',
+  })
+}
+
+const validate = (): boolean => {
+  const name = form.name.trim()
+  const phone = form.phone.trim()
+  const company = form.company.trim()
+
+  if (!name) {
+    showToast('请填写您的称呼')
+    return false
+  }
+  if (!/^1[3-9]\d{9}$/.test(phone)) {
+    showToast('请填写正确的手机号')
+    return false
+  }
+  if (company.length > 50) {
+    showToast('企业名称不能超过50个字符')
+    return false
+  }
+  if (!form.education) {
+    showToast('请选择您目前的学历')
+    return false
+  }
+  if (!selectedInterests.value.length) {
+    showToast('请选择您最想了解的内容')
+    return false
+  }
+  return true
+}
+
+const submit = async () => {
+  if (submitting.value || submitted.value || !validate()) return
+
+  const data: CompliancePlanPayload = {
+    source: 'A001',
+    channel: channel.value,
+    name: form.name.trim(),
+    phone: form.phone.trim(),
+    company: form.company.trim(),
+    education: form.education,
+    interest: selectedInterests.value.join('、'),
+  }
+
+  try {
+    submitting.value = true
+    const result = await submitInfoCollectionApi(data)
+
+    if (result.code === 0 || result.code === 200) {
+      submitted.value = true
+      uni.showToast({
+        title: '提交成功',
+        icon: 'success',
+      })
+
+      // #ifndef H5
+      uni.navigateBack()
+      // #endif
+      return
+    }
+
+    showToast(result.msg || '提交失败,请稍后重试')
+  } catch (error) {
+    showToast(getErrorMessage(error, '提交失败,请检查网络后重试'))
+  } finally {
+    submitting.value = false
+  }
+}
+
+const getErrorMessage = (error: unknown, fallback: string): string => {
+  if (error instanceof Error && error.message) return error.message
+
+  if (typeof error === 'object' && error !== null) {
+    const target = error as { msg?: string; message?: string }
+    return target.msg || target.message || fallback
+  }
+
+  return fallback
+}
+
+onLoad((options?: Record<string, string>) => {
+  // #ifdef H5
+  channel.value = options?.channel || 'h5_sms'
+  // #endif
+})
+</script>
+
+<style lang="scss" scoped>
+.plan-page {
+  min-height: 100vh;
+  background: #f4f5f7;
+  color: #20242b;
+}
+
+.h5-page-header {
+  position: sticky;
+  top: 0;
+  z-index: 100;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 88rpx;
+  border-bottom: 1rpx solid #eeeeee;
+  background: #ffffff;
+  color: #20242b;
+  font-size: 32rpx;
+  font-weight: 600;
+  box-sizing: border-box;
+}
+
+.hero {
+  position: relative;
+  height: 380rpx;
+  overflow: hidden;
+  background: linear-gradient(135deg, #0b55bd 0%, #126de0 54%, #1284ff 100%);
+}
+
+.hero-glow {
+  position: absolute;
+  border-radius: 50%;
+  background: rgba(255, 213, 72, 0.2);
+  filter: blur(22rpx);
+}
+
+.hero-glow-top {
+  top: -78rpx;
+  right: -58rpx;
+  width: 230rpx;
+  height: 230rpx;
+}
+
+.hero-glow-bottom {
+  right: 35rpx;
+  bottom: -150rpx;
+  width: 310rpx;
+  height: 250rpx;
+  background: rgba(27, 168, 255, 0.34);
+}
+
+.hero-content {
+  position: relative;
+  z-index: 1;
+  padding: 47rpx 40rpx 36rpx;
+  color: #ffffff;
+}
+
+.hero-tag {
+  display: inline-flex;
+  align-items: center;
+  height: 48rpx;
+  padding: 0 18rpx;
+  border: 1rpx solid rgba(255, 220, 63, 0.56);
+  border-radius: 24rpx;
+  background: rgba(255, 211, 45, 0.13);
+  color: #ffe247;
+  font-size: 25rpx;
+  font-weight: 600;
+}
+
+.hero-kicker {
+  margin-top: 24rpx;
+  font-size: 42rpx;
+  font-weight: 700;
+  line-height: 1;
+}
+
+.hero-title {
+  display: flex;
+  align-items: baseline;
+  margin-top: 15rpx;
+  color: #ffdd2f;
+  font-size: 40rpx;
+  font-weight: 700;
+  line-height: 1.15;
+}
+
+.hero-title-tail {
+  margin-left: 14rpx;
+  color: #ffffff;
+  font-size: 32rpx;
+}
+
+.hero-description {
+  margin-top: 26rpx;
+  color: rgba(255, 255, 255, 0.76);
+  font-size: 24rpx;
+  line-height: 1.4;
+}
+
+.hero-description-last {
+  margin-top: 8rpx;
+}
+
+.hero-provider-note {
+  position: absolute;
+  right: 24rpx;
+  bottom: 14rpx;
+  z-index: 1;
+  color: rgba(255, 255, 255, 0.62);
+  font-size: 18rpx;
+  line-height: 26rpx;
+  text-align: right;
+  white-space: nowrap;
+}
+
+.page-content {
+  padding: 34rpx 24rpx 44rpx;
+}
+
+.section-title {
+  display: flex;
+  align-items: center;
+  font-size: 30rpx;
+  font-weight: 700;
+  line-height: 42rpx;
+}
+
+.section-title-line {
+  width: 6rpx;
+  height: 28rpx;
+  margin-right: 10rpx;
+  border-radius: 3rpx;
+  background: #1988ff;
+}
+
+.benefit-list {
+  margin-top: 20rpx;
+}
+
+.benefit-card {
+  display: flex;
+  align-items: flex-start;
+  min-height: 122rpx;
+  margin-bottom: 18rpx;
+  padding: 25rpx 24rpx;
+  border-radius: 18rpx;
+  background: #ffffff;
+  box-shadow: 0 5rpx 17rpx rgba(25, 43, 69, 0.07);
+  box-sizing: border-box;
+}
+
+.benefit-icon {
+  display: flex;
+  flex: 0 0 62rpx;
+  align-items: center;
+  justify-content: center;
+  width: 62rpx;
+  height: 62rpx;
+  margin-right: 22rpx;
+  border-radius: 14rpx;
+  background: #edf6ff;
+  font-size: 31rpx;
+  line-height: 1;
+}
+
+.benefit-content {
+  flex: 1;
+  min-width: 0;
+}
+
+.benefit-title {
+  font-size: 29rpx;
+  font-weight: 700;
+  line-height: 38rpx;
+}
+
+.benefit-description {
+  margin-top: 5rpx;
+  color: #666d77;
+  font-size: 23rpx;
+  line-height: 35rpx;
+}
+
+.form-section-title {
+  margin-top: 34rpx;
+}
+
+.form-card {
+  margin-top: 20rpx;
+  padding: 34rpx 28rpx 37rpx;
+  border-radius: 18rpx;
+  background: #ffffff;
+  box-shadow: 0 5rpx 17rpx rgba(25, 43, 69, 0.07);
+}
+
+.field {
+  margin-bottom: 35rpx;
+}
+
+.field-label {
+  margin-bottom: 17rpx;
+  font-size: 27rpx;
+  font-weight: 700;
+  line-height: 38rpx;
+}
+
+.required {
+  color: #f0565b;
+}
+
+.field-hint {
+  margin-left: 8rpx;
+  color: #a1a5ad;
+  font-size: 21rpx;
+  font-weight: 400;
+}
+
+.field-input {
+  width: 100%;
+  height: 88rpx;
+  padding: 0 24rpx;
+  border: 1rpx solid #e5e7eb;
+  border-radius: 8rpx;
+  background: #f7f8fa;
+  color: #30343a;
+  font-size: 27rpx;
+  box-sizing: border-box;
+}
+
+.field-placeholder {
+  color: #b6bac2;
+}
+
+.option-list {
+  display: flex;
+  flex-wrap: wrap;
+  margin: -8rpx -6rpx 0;
+}
+
+.option-pill {
+  display: flex;
+  align-items: center;
+  min-height: 62rpx;
+  margin: 8rpx 6rpx 0;
+  padding: 0 25rpx;
+  border: 1rpx solid #eceef1;
+  border-radius: 32rpx;
+  background: #f8f8fa;
+  color: #626975;
+  font-size: 24rpx;
+  line-height: 1.2;
+  box-sizing: border-box;
+}
+
+.option-pill-active {
+  border-color: #1d80ef;
+  background: #eaf4ff;
+  color: #1675df;
+  font-weight: 600;
+}
+
+.option-pill-hover {
+  opacity: 0.78;
+}
+
+.interest-field {
+  margin-bottom: 42rpx;
+}
+
+.submit-button {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 94rpx;
+  margin: 0;
+  padding: 0;
+  border-radius: 47rpx;
+  background: #187df2;
+  box-shadow: 0 12rpx 25rpx rgba(22, 121, 242, 0.28);
+  color: #ffffff;
+  font-size: 32rpx;
+  font-weight: 700;
+  line-height: 94rpx;
+}
+
+.submit-button-hover {
+  background: #0f6edc;
+}
+
+.submit-button-disabled {
+  background: #8ebff8;
+  box-shadow: none;
+}
+
+.privacy-note {
+  margin-top: 22rpx;
+  color: #b0b3ba;
+  font-size: 20rpx;
+  line-height: 32rpx;
+  text-align: center;
+}
+
+.service-phone {
+  margin-top: 34rpx;
+  color: #8f9298;
+  font-size: 28rpx;
+  font-weight: 700;
+  text-align: center;
+}
+</style>

+ 1073 - 0
src/pages/compliance-training/registration.vue

@@ -0,0 +1,1073 @@
+<template>
+  <view class="registration-page">
+    <!-- #ifdef H5 -->
+    <view class="h5-page-header">医药代表合规培训报名表</view>
+    <!-- #endif -->
+
+    <view class="hero">
+      <view class="hero-title">正式报名 · 一次填完</view>
+      <view class="hero-subtitle">信息仅用于学籍注册与证书制作</view>
+
+      <view class="progress-list">
+        <view
+          v-for="(step, index) in steps"
+          :key="step"
+          class="progress-step"
+          :class="{ active: index === 0 }"
+        >
+          <view class="step-number">{{ index + 1 }}</view>
+          <text class="step-name">{{ step }}</text>
+          <view v-if="index < steps.length - 1" class="step-line"></view>
+        </view>
+      </view>
+    </view>
+
+    <view class="page-content">
+      <view class="prepare-tip">
+        <view class="prepare-title">📋 填写前请备齐</view>
+        <view class="prepare-text">两寸免冠证件照一张(白底或蓝底,用于证书制作)。</view>
+      </view>
+
+      <view class="section-title"><view class="section-mark"></view><text>一、基本信息</text></view>
+      <view class="form-card">
+        <view class="field">
+          <view class="field-label"
+            >姓名 <text class="required">*</text><text class="label-note">用于证书印制</text></view
+          >
+          <input
+            v-model="form.name"
+            class="field-input"
+            :maxlength="50"
+            placeholder="请输入真实姓名"
+            placeholder-class="field-placeholder"
+          />
+        </view>
+
+        <view class="field">
+          <view class="field-label">性别 <text class="required">*</text></view>
+          <view class="gender-list">
+            <view
+              v-for="item in genderOptions"
+              :key="item"
+              class="gender-option"
+              :class="{ selected: form.gender === item }"
+              @click="form.gender = item"
+            >
+              <view class="radio-icon"
+                ><view v-if="form.gender === item" class="radio-dot"></view
+              ></view>
+              <text>{{ item }}</text>
+            </view>
+          </view>
+        </view>
+
+        <view class="field">
+          <view class="field-label">手机号 <text class="required">*</text></view>
+          <input
+            :value="form.phone"
+            class="field-input"
+            type="number"
+            :maxlength="11"
+            placeholder="请输入手机号"
+            placeholder-class="field-placeholder"
+            @input="onPhoneInput"
+          />
+        </view>
+
+        <view class="field">
+          <view class="field-label"
+            >身份证号 <text class="required">*</text
+            ><text class="label-note">用于学籍与证书注册</text></view
+          >
+          <input
+            :value="form.idCard"
+            class="field-input"
+            :maxlength="18"
+            placeholder="请输入身份证号"
+            placeholder-class="field-placeholder"
+            @input="onIdCardInput"
+          />
+        </view>
+
+        <view class="field">
+          <view class="field-label">所在地区(省/市) <text class="required">*</text></view>
+          <view class="field-input selector" @click="showRegionPicker = true">
+            <text :class="{ 'field-placeholder': !regionText }">{{
+              regionText || '请选择省 / 市'
+            }}</text>
+            <wd-icon name="down" size="24rpx" color="#aeb6c2" />
+          </view>
+        </view>
+
+        <view class="field">
+          <view class="field-label">所属公司 / 单位 / 机构 <text class="required">*</text></view>
+          <input
+            v-model="form.company"
+            class="field-input"
+            :maxlength="100"
+            placeholder="请输入单位全称"
+            placeholder-class="field-placeholder"
+          />
+        </view>
+
+        <view class="field field-last">
+          <view class="field-label">职务 <text class="required">*</text></view>
+          <input
+            v-model="form.jobTitle"
+            class="field-input"
+            :maxlength="50"
+            placeholder="请输入职务"
+            placeholder-class="field-placeholder"
+          />
+        </view>
+      </view>
+
+      <view class="section-title"><view class="section-mark"></view><text>二、学历报名</text></view>
+      <view class="form-card education-card">
+        <view class="field-label">是否需要同步报名学历 <text class="required">*</text></view>
+        <view
+          v-for="item in educationOptions"
+          :key="item.value"
+          class="education-option"
+          :class="{ selected: selectedEducation.indexOf(item.value) !== -1 }"
+          @click="toggleEducation(item.value)"
+        >
+          <view class="checkbox-icon">
+            <wd-icon
+              v-if="selectedEducation.indexOf(item.value) !== -1"
+              name="check"
+              size="26rpx"
+              color="#ffffff"
+            />
+          </view>
+          <text class="education-label">{{ item.label }}</text>
+        </view>
+      </view>
+
+      <view class="section-title"
+        ><view class="section-mark"></view><text>三、照片(证书使用)</text></view
+      >
+      <view class="form-card photo-card">
+        <view class="field-label photo-label"
+          >照片(证书使用) <text class="required">*</text></view
+        >
+        <view class="photo-hint">白底或蓝底两寸免冠照片</view>
+
+        <view class="photo-grid">
+          <view v-for="(image, index) in images" :key="image.remoteUrl" class="photo-item">
+            <image
+              class="photo-preview"
+              :src="image.localUrl"
+              mode="aspectFill"
+              @click="previewImage(index)"
+            />
+            <view class="photo-delete" @click.stop="removeImage(index)">×</view>
+          </view>
+
+          <view
+            v-if="images.length < 2"
+            class="upload-box"
+            :class="{ disabled: uploading, 'upload-box-empty': images.length === 0 }"
+            @click="chooseImages"
+          >
+            <view class="upload-plus">+</view>
+            <view class="upload-title">{{ uploading ? '上传中...' : '添加图片' }}</view>
+            <view class="upload-tip">图片要求:最少1张</view>
+          </view>
+        </view>
+      </view>
+
+      <view class="section-title"><view class="section-mark"></view><text>四、证书寄送</text></view>
+      <view class="form-card address-card">
+        <view class="field-label address-label">
+          联系地址 <text class="required">*</text><text class="label-note">用于寄送证书</text>
+        </view>
+        <view class="field-input selector" @click="showAddressPicker = true">
+          <text :class="{ 'field-placeholder': !addressRegionText }">{{
+            addressRegionText || '省 / 市 / 区'
+          }}</text>
+          <wd-icon name="down" size="24rpx" color="#aeb6c2" />
+        </view>
+        <textarea
+          v-model="form.addressDetail"
+          class="address-detail"
+          :maxlength="100"
+          placeholder="请填写详细地址(小区/楼栋/门牌号)"
+          placeholder-class="field-placeholder"
+          :show-confirm-bar="false"
+        />
+      </view>
+
+      <button
+        class="submit-button"
+        :disabled="submitting || uploading || submitted"
+        :class="{ disabled: submitting || uploading || submitted }"
+        @click="submit"
+      >
+        {{ submitButtonText }}
+      </button>
+
+      <view class="footer-note">
+        <view>教务老师将于1个工作日内与您确认报名信息并开通课程</view>
+        <view>信息仅用于学籍注册与证书制作,不会用于其他用途</view>
+      </view>
+      <view class="service-phone">服务热线:400-639-1811</view>
+    </view>
+
+    <wd-cascader
+      v-model="regionPickerValue"
+      v-model:visible="showRegionPicker"
+      title="选择所在地区"
+      :options="regionOptions"
+      root-portal
+      @confirm="onRegionChange"
+    />
+    <wd-cascader
+      v-model="addressPickerValue"
+      v-model:visible="showAddressPicker"
+      title="选择证书寄送地区"
+      :options="addressOptions"
+      root-portal
+      @confirm="onAddressRegionChange"
+    />
+  </view>
+</template>
+
+<script setup lang="ts">
+import { computed, reactive, ref } from 'vue'
+
+import { onLoad } from '@dcloudio/uni-app'
+import { useCascaderAreaData } from '@vant/area-data'
+
+import {
+  getInfoCollectionUploadUrl,
+  submitInfoCollectionApi,
+  type TrainingRegistrationPayload,
+} from '@/services/modules/infoCollection'
+
+interface AreaOption {
+  value: string
+  text: string
+  children?: AreaOption[]
+  isLeaf?: boolean
+}
+
+interface SelectedRegion {
+  province: string
+  city: string
+  area?: string
+}
+
+interface SelectedImageFile {
+  path: string
+  size?: number
+  name?: string
+  type?: string
+}
+
+interface UploadedImage {
+  localUrl: string
+  remoteUrl: string
+}
+
+interface UploadResponse {
+  code: number | string
+  msg?: string
+  data?: {
+    url?: string
+  }
+}
+
+const IMAGE_TYPES = ['jpg', 'jpeg', 'png', 'bmp', 'gif', 'webp']
+const MAX_IMAGE_SIZE = 5 * 1024 * 1024
+
+const steps = ['填写提交', '教务审核', '开通课程', '双证上岗']
+const genderOptions = ['男', '女']
+const educationOptions = [
+  {
+    label: '【专科】药学专业、护理学专业',
+    value: '【专科】药学专业、护理学专业',
+  },
+  {
+    label: '【专科起点本科】药学专业、护理学专业',
+    value: '【专科起点本科】药学专业、护理学专业',
+  },
+]
+
+const form = reactive({
+  name: '',
+  gender: '',
+  phone: '',
+  idCard: '',
+  company: '',
+  jobTitle: '',
+  addressDetail: '',
+})
+
+const region = ref<SelectedRegion>()
+const addressRegion = ref<SelectedRegion>()
+const selectedEducation = ref<string[]>([])
+const images = ref<UploadedImage[]>([])
+const showRegionPicker = ref(false)
+const showAddressPicker = ref(false)
+const regionPickerValue = ref<string | number>()
+const addressPickerValue = ref<string | number>()
+const uploading = ref(false)
+const submitting = ref(false)
+const submitted = ref(false)
+const channel = ref('miniapp')
+
+const rawAreaOptions = useCascaderAreaData() as AreaOption[]
+const regionOptions = convertCityLeaf(rawAreaOptions)
+const addressOptions = rawAreaOptions
+
+const regionText = computed(() => {
+  if (!region.value) return ''
+  return `${region.value.province} / ${region.value.city}`
+})
+
+const addressRegionText = computed(() => {
+  if (!addressRegion.value) return ''
+  return `${addressRegion.value.province} / ${addressRegion.value.city} / ${addressRegion.value.area}`
+})
+
+const submitButtonText = computed(() => {
+  if (submitted.value) return '报名已提交'
+  if (submitting.value) return '提交中...'
+  return '提交报名'
+})
+
+const showToast = (title: string) => {
+  uni.showToast({
+    title,
+    icon: 'none',
+  })
+}
+
+const onPhoneInput = (event: { detail: { value?: string | number } }) => {
+  const phone = String(event.detail.value || '')
+    .replace(/\D/g, '')
+    .slice(0, 11)
+  form.phone = phone
+  return phone
+}
+
+const onIdCardInput = (event: { detail: { value?: string | number } }) => {
+  const idCard = String(event.detail.value || '')
+    .replace(/[^0-9Xx]/g, '')
+    .slice(0, 18)
+  form.idCard = idCard
+  return idCard
+}
+
+const onRegionChange = (event: { selectedOptions?: AreaOption[] }) => {
+  const items = event.selectedOptions ?? []
+  if (items.length < 2) return
+
+  region.value = {
+    province: items[0].text,
+    city: items[1].text,
+  }
+  regionPickerValue.value = items[items.length - 1].value
+}
+
+const onAddressRegionChange = (event: { selectedOptions?: AreaOption[] }) => {
+  const items = event.selectedOptions ?? []
+  if (items.length < 3) return
+
+  addressRegion.value = {
+    province: items[0].text,
+    city: items[1].text,
+    area: items[2].text,
+  }
+  addressPickerValue.value = items[items.length - 1].value
+}
+
+const toggleEducation = (value: string) => {
+  const index = selectedEducation.value.indexOf(value)
+
+  if (index === -1) {
+    selectedEducation.value.push(value)
+  } else {
+    selectedEducation.value.splice(index, 1)
+  }
+}
+
+const chooseImages = () => {
+  if (uploading.value || images.value.length >= 2) return
+
+  uni.chooseImage({
+    count: 2 - images.value.length,
+    sizeType: ['compressed'],
+    sourceType: ['album'],
+    success: async (result) => {
+      const tempFiles = (result.tempFiles || []) as Array<{
+        path?: string
+        size?: number
+        name?: string
+        type?: string
+      }>
+      const tempFilePaths = Array.isArray(result.tempFilePaths) ? result.tempFilePaths : []
+      const files: SelectedImageFile[] = tempFilePaths.map((path, index) => ({
+        path,
+        size: tempFiles[index]?.size,
+        name: tempFiles[index]?.name,
+        type: tempFiles[index]?.type,
+      }))
+
+      for (const file of files) {
+        const errorMessage = validateImage(file)
+
+        if (errorMessage) {
+          showToast(errorMessage)
+          continue
+        }
+
+        try {
+          uploading.value = true
+          const remoteUrl = await uploadImage(file.path)
+          images.value.push({
+            localUrl: file.path,
+            remoteUrl,
+          })
+        } catch (error) {
+          showToast(getErrorMessage(error, '图片上传失败,请重试'))
+        } finally {
+          uploading.value = false
+        }
+      }
+    },
+  })
+}
+
+const validateImage = (file: SelectedImageFile): string => {
+  if (file.size && file.size > MAX_IMAGE_SIZE) {
+    return '图片大小不能超过5MB'
+  }
+
+  const extension =
+    String(file.name || file.path)
+      .split('?')[0]
+      .split('.')
+      .pop()
+      ?.toLowerCase() || ''
+  const mimeType = String(file.type || '').toLowerCase()
+  const hasKnownExtension = IMAGE_TYPES.includes(extension)
+  const hasImageMimeType = mimeType.startsWith('image/')
+
+  if (!hasKnownExtension && !hasImageMimeType) {
+    return '仅支持jpg、jpeg、png、bmp、gif、webp格式'
+  }
+
+  return ''
+}
+
+const uploadImage = (filePath: string): Promise<string> => {
+  return new Promise((resolve, reject) => {
+    uni.uploadFile({
+      url: getInfoCollectionUploadUrl(),
+      filePath,
+      name: 'file',
+      success(response) {
+        if (response.statusCode !== 200) {
+          reject(new Error(`图片上传失败(HTTP ${response.statusCode})`))
+          return
+        }
+
+        try {
+          const result = (
+            typeof response.data === 'string' ? JSON.parse(response.data) : response.data
+          ) as UploadResponse
+
+          if (!result || Number(result.code) !== 0) {
+            reject(new Error(result?.msg || '图片上传失败,请重试'))
+            return
+          }
+
+          const remoteUrl = result.data?.url
+
+          if (!remoteUrl) {
+            reject(new Error('图片上传接口未返回图片地址'))
+            return
+          }
+
+          resolve(remoteUrl)
+        } catch {
+          reject(new Error('图片上传响应解析失败'))
+        }
+      },
+      fail() {
+        reject(new Error('图片上传失败,请检查网络后重试'))
+      },
+    })
+  })
+}
+
+const previewImage = (index: number) => {
+  uni.previewImage({
+    current: index,
+    urls: images.value.map((item) => item.localUrl),
+  })
+}
+
+const removeImage = (index: number) => {
+  if (uploading.value) return
+  images.value.splice(index, 1)
+}
+
+const validate = (): boolean => {
+  const name = form.name.trim()
+  const phone = form.phone.trim()
+  const idCard = form.idCard.trim()
+  const company = form.company.trim()
+  const jobTitle = form.jobTitle.trim()
+  const addressDetail = form.addressDetail.trim()
+
+  if (!name) return showValidationError('请填写姓名')
+  if (name.length > 50) return showValidationError('姓名最多可填写50个字')
+  if (!form.gender) return showValidationError('请选择性别')
+  if (!phone) return showValidationError('请填写手机号')
+  if (!/^1[3-9]\d{9}$/.test(phone)) return showValidationError('手机号格式错误')
+  if (!idCard) return showValidationError('请填写身份证号')
+  if (!isIdCard(idCard)) return showValidationError('身份证号格式错误')
+  if (!region.value) return showValidationError('请选择所在地区')
+  if (!company) return showValidationError('请填写所属公司/单位/机构')
+  if (company.length > 100) {
+    return showValidationError('所属公司/单位/机构最多可填写100个字')
+  }
+  if (!jobTitle) return showValidationError('请填写职务')
+  if (jobTitle.length > 50) return showValidationError('职务最多可填写50个字')
+  if (!selectedEducation.value.length) return showValidationError('请选择需要同步报名的学历')
+  if (!images.value.length) return showValidationError('请至少上传1张照片')
+  if (!addressRegion.value) return showValidationError('请选择证书寄送地区')
+  if (!addressDetail) return showValidationError('请填写详细地址')
+  if (addressDetail.length > 100) return showValidationError('详细地址最多可填写100个字')
+
+  return true
+}
+
+const showValidationError = (message: string): false => {
+  showToast(message)
+  return false
+}
+
+const submit = async () => {
+  if (submitting.value || uploading.value || submitted.value || !validate()) return
+  if (!region.value || !addressRegion.value) return
+
+  const data: TrainingRegistrationPayload = {
+    source: 'A002',
+    channel: channel.value,
+    name: form.name.trim(),
+    gender: form.gender,
+    phone: form.phone.trim(),
+    idCard: form.idCard.trim(),
+    region: `${region.value.province}/${region.value.city}`,
+    company: form.company.trim(),
+    jobTitle: form.jobTitle.trim(),
+    applyEducation: selectedEducation.value.join('、'),
+    certificateImages: images.value.map((item) => item.remoteUrl).join(','),
+    address: `${addressRegion.value.province}/${addressRegion.value.city}/${addressRegion.value.area}/${form.addressDetail.trim()}`,
+  }
+
+  try {
+    submitting.value = true
+    const result = await submitInfoCollectionApi(data)
+
+    if (result.code === 0 || result.code === 200) {
+      submitted.value = true
+      uni.showToast({
+        title: '报名提交成功',
+        icon: 'success',
+      })
+      return
+    }
+
+    showToast(result.msg || '提交失败,请稍后重试')
+  } catch (error) {
+    showToast(getErrorMessage(error, '提交失败,请检查网络后重试'))
+  } finally {
+    submitting.value = false
+  }
+}
+
+function convertCityLeaf(list: AreaOption[]): AreaOption[] {
+  return list.map((item) => ({
+    ...item,
+    children: item.children?.map((city) => ({
+      value: city.value,
+      text: city.text,
+      isLeaf: true,
+    })),
+  }))
+}
+
+const isIdCard = (value: string): boolean => {
+  const idCard = value.trim()
+  const pattern =
+    /^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$/
+
+  if (!pattern.test(idCard)) return false
+  if (idCard.length !== 18) return true
+
+  const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
+  const checksumMap = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
+  const sum = weights.reduce((total, weight, index) => {
+    return total + Number(idCard[index]) * weight
+  }, 0)
+
+  return idCard[17].toUpperCase() === checksumMap[sum % 11]
+}
+
+const getErrorMessage = (error: unknown, fallback: string): string => {
+  if (error instanceof Error && error.message) return error.message
+
+  if (typeof error === 'object' && error !== null) {
+    const target = error as { msg?: string; message?: string }
+    return target.msg || target.message || fallback
+  }
+
+  return fallback
+}
+
+onLoad((options?: Record<string, string>) => {
+  // #ifdef H5
+  channel.value = options?.channel || 'h5_sms'
+  // #endif
+})
+</script>
+
+<style lang="scss" scoped>
+.registration-page {
+  min-height: 100vh;
+  background: #f3f5f8;
+  color: #252b34;
+}
+
+.h5-page-header {
+  position: sticky;
+  top: 0;
+  z-index: 100;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 88rpx;
+  border-bottom: 1rpx solid #eeeeee;
+  background: #ffffff;
+  color: #252b34;
+  font-size: 32rpx;
+  font-weight: 600;
+  box-sizing: border-box;
+}
+
+.hero {
+  padding: 43rpx 40rpx 39rpx;
+  background: linear-gradient(135deg, #1460c7 0%, #1979eb 72%, #3986dd 100%);
+  color: #ffffff;
+}
+
+.hero-title {
+  font-size: 39rpx;
+  font-weight: 700;
+  line-height: 55rpx;
+}
+
+.hero-subtitle {
+  margin-top: 8rpx;
+  color: rgba(255, 255, 255, 0.66);
+  font-size: 23rpx;
+  line-height: 34rpx;
+}
+
+.progress-list {
+  display: flex;
+  align-items: center;
+  margin-top: 27rpx;
+}
+
+.progress-step {
+  display: flex;
+  flex: 1;
+  align-items: center;
+  min-width: 0;
+  color: rgba(255, 255, 255, 0.68);
+  font-size: 22rpx;
+
+  &.active {
+    color: #ffffff;
+    font-weight: 600;
+
+    .step-number {
+      background: #ffdf1f;
+      color: #785d00;
+    }
+  }
+}
+
+.step-number {
+  display: flex;
+  flex: 0 0 35rpx;
+  align-items: center;
+  justify-content: center;
+  width: 35rpx;
+  height: 35rpx;
+  border: 1rpx solid rgba(255, 255, 255, 0.2);
+  border-radius: 50%;
+  background: rgba(255, 255, 255, 0.14);
+  font-size: 20rpx;
+}
+
+.step-name {
+  flex: 0 0 auto;
+  margin-left: 9rpx;
+  white-space: nowrap;
+}
+
+.step-line {
+  flex: 1;
+  min-width: 12rpx;
+  height: 1rpx;
+  margin: 0 9rpx;
+  background: rgba(255, 255, 255, 0.28);
+}
+
+.page-content {
+  padding: 24rpx 24rpx 54rpx;
+}
+
+.prepare-tip {
+  padding: 23rpx 28rpx;
+  border: 1rpx solid #f2b943;
+  border-radius: 13rpx;
+  background: #fffaf0;
+}
+
+.prepare-title {
+  color: #7e5415;
+  font-size: 27rpx;
+  font-weight: 700;
+  line-height: 38rpx;
+}
+
+.prepare-text {
+  margin-top: 8rpx;
+  color: #9b722b;
+  font-size: 23rpx;
+  line-height: 34rpx;
+}
+
+.section-title {
+  display: flex;
+  align-items: center;
+  margin: 29rpx 0 17rpx;
+  font-size: 30rpx;
+  font-weight: 700;
+  line-height: 42rpx;
+}
+
+.section-mark {
+  width: 6rpx;
+  height: 27rpx;
+  margin-right: 12rpx;
+  border-radius: 4rpx;
+  background: #2182f3;
+}
+
+.form-card {
+  padding: 33rpx 27rpx;
+  border-radius: 15rpx;
+  background: #ffffff;
+  box-shadow: 0 4rpx 14rpx rgba(26, 40, 60, 0.07);
+}
+
+.field {
+  margin-bottom: 31rpx;
+}
+
+.field-last {
+  margin-bottom: 0;
+}
+
+.field-label {
+  margin-bottom: 15rpx;
+  font-size: 27rpx;
+  font-weight: 700;
+  line-height: 39rpx;
+}
+
+.required {
+  color: #f0444c;
+}
+
+.field-input {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  height: 88rpx;
+  padding: 0 25rpx;
+  border: 1rpx solid #e7e9ed;
+  border-radius: 8rpx;
+  background: #f6f7f9;
+  color: #333a45;
+  font-size: 26rpx;
+  box-sizing: border-box;
+}
+
+.field-placeholder {
+  color: #b3bac5;
+}
+
+.selector {
+  justify-content: space-between;
+}
+
+.gender-list {
+  display: flex;
+}
+
+.gender-option {
+  display: flex;
+  align-items: center;
+  height: 68rpx;
+  margin-right: 17rpx;
+  padding: 0 28rpx;
+  border: 1rpx solid #edf0f3;
+  border-radius: 36rpx;
+  background: #f7f8fa;
+  color: #58616e;
+  font-size: 25rpx;
+
+  &.selected {
+    border-color: #85bafb;
+    background: #eef6ff;
+    color: #1879e8;
+  }
+}
+
+.radio-icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 30rpx;
+  height: 30rpx;
+  margin-right: 10rpx;
+  border: 2rpx solid #cbd4df;
+  border-radius: 50%;
+  box-sizing: border-box;
+}
+
+.radio-dot {
+  width: 16rpx;
+  height: 16rpx;
+  border-radius: 50%;
+  background: #2182f3;
+}
+
+.education-card {
+  padding-bottom: 31rpx;
+}
+
+.education-option {
+  display: flex;
+  align-items: center;
+  min-height: 86rpx;
+  margin-top: 22rpx;
+  padding: 18rpx 24rpx;
+  border: 1rpx solid #eef0f3;
+  border-radius: 9rpx;
+  background: #f6f7f9;
+  color: #555f6d;
+  box-sizing: border-box;
+
+  &.selected {
+    border-color: #75b1f9;
+    background: #eef6ff;
+    color: #1678e5;
+  }
+}
+
+.checkbox-icon {
+  display: flex;
+  flex: 0 0 31rpx;
+  align-items: center;
+  justify-content: center;
+  width: 31rpx;
+  height: 31rpx;
+  margin-right: 14rpx;
+  border: 2rpx solid #cbd4df;
+  border-radius: 6rpx;
+  box-sizing: border-box;
+}
+
+.education-option.selected .checkbox-icon {
+  border-color: #2182f3;
+  background: #2182f3;
+}
+
+.education-label {
+  font-size: 26rpx;
+  line-height: 38rpx;
+}
+
+.photo-card {
+  padding-bottom: 31rpx;
+}
+
+.photo-label {
+  margin-bottom: 3rpx;
+}
+
+.photo-hint {
+  color: #a2aab5;
+  font-size: 21rpx;
+  line-height: 31rpx;
+}
+
+.photo-grid {
+  display: flex;
+  flex-wrap: wrap;
+  margin-top: 10rpx;
+  gap: 14rpx;
+}
+
+.photo-item,
+.upload-box {
+  position: relative;
+  width: calc((100% - 14rpx) / 2);
+  height: 185rpx;
+  box-sizing: border-box;
+}
+
+.photo-preview {
+  width: 100%;
+  height: 100%;
+  border-radius: 10rpx;
+}
+
+.photo-delete {
+  position: absolute;
+  top: -12rpx;
+  right: -12rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 38rpx;
+  height: 38rpx;
+  border-radius: 50%;
+  background: rgba(34, 39, 47, 0.78);
+  color: #ffffff;
+  font-size: 31rpx;
+  line-height: 1;
+}
+
+.upload-box {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  border: 2rpx dashed #c3d3e8;
+  border-radius: 10rpx;
+  background: #fbfcfe;
+
+  &.disabled {
+    opacity: 0.62;
+  }
+}
+
+.upload-box-empty {
+  width: 100%;
+}
+
+.upload-plus {
+  color: #1980ef;
+  font-size: 52rpx;
+  font-weight: 300;
+  line-height: 50rpx;
+}
+
+.upload-title {
+  margin-top: 5rpx;
+  color: #596575;
+  font-size: 24rpx;
+  line-height: 34rpx;
+}
+
+.upload-tip {
+  margin-top: 4rpx;
+  color: #b7bdc7;
+  font-size: 20rpx;
+  line-height: 29rpx;
+}
+
+.address-card {
+  padding-bottom: 31rpx;
+}
+
+.address-label {
+  margin-bottom: 16rpx;
+}
+
+.label-note {
+  margin-left: 8rpx;
+  color: #a5acb6;
+  font-size: 21rpx;
+  font-weight: 400;
+}
+
+.address-detail {
+  width: 100%;
+  height: 88rpx;
+  min-height: 88rpx;
+  margin-top: 14rpx;
+  padding: 24rpx 25rpx;
+  border: 1rpx solid #e7e9ed;
+  border-radius: 8rpx;
+  background: #f6f7f9;
+  color: #333a45;
+  font-size: 26rpx;
+  line-height: 38rpx;
+  box-sizing: border-box;
+}
+
+.submit-button {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 94rpx;
+  margin: 39rpx 0 0;
+  padding: 0;
+  border-radius: 48rpx;
+  background: #2180f3;
+  box-shadow: 0 11rpx 23rpx rgba(26, 119, 232, 0.28);
+  color: #ffffff;
+  font-size: 31rpx;
+  font-weight: 700;
+  line-height: 94rpx;
+
+  &::after {
+    border: none;
+  }
+
+  &.disabled {
+    background: #91bdf2;
+    box-shadow: none;
+  }
+}
+
+.footer-note {
+  margin-top: 25rpx;
+  color: #a9afb8;
+  font-size: 20rpx;
+  line-height: 33rpx;
+  text-align: center;
+}
+
+.service-phone {
+  margin-top: 34rpx;
+  color: #8f9298;
+  font-size: 28rpx;
+  font-weight: 700;
+  text-align: center;
+}
+</style>

+ 13 - 1
src/pages/index/components/GdComplianceBannerDialog.vue

@@ -10,7 +10,12 @@
     :close-on-click-modal="false"
     custom-style="width: 580rpx; overflow: hidden; background: transparent; --wot-popup-close-color: #ffffff; --wot-popup-close-size: 34rpx; --wot-popup-close-margin: 20rpx;"
   >
-    <image class="banner-image" :src="BANNER_IMAGE_URL" mode="widthFix" />
+    <image
+      class="banner-image"
+      :src="BANNER_IMAGE_URL"
+      mode="widthFix"
+      @click="openCompliancePlan"
+    />
   </wd-popup>
 </template>
 
@@ -65,6 +70,13 @@ const scheduleShow = () => {
   }, 300)
 }
 
+const openCompliancePlan = () => {
+  visible.value = false
+  uni.navigateTo({
+    url: '/pages/compliance-training/plan',
+  })
+}
+
 watch(
   () =>
     [

+ 11 - 0
src/pages/information/components/academic/InformationBannerSwiper.vue

@@ -21,6 +21,7 @@ import { ref } from 'vue'
 interface SwiperItem {
   id: string
   image: string
+  target?: string
 }
 
 interface SwiperClickEvent {
@@ -31,6 +32,11 @@ interface SwiperClickEvent {
 const current = ref(0)
 
 const swiperList = ref<SwiperItem[]>([
+  {
+    id: 'gd-compliance-training',
+    image: 'https://yy-cloud-oss-public.oss-cn-beijing.aliyuncs.com/image/bannerc.jpg',
+    target: '/pages/compliance-training/plan',
+  },
   {
     id: 'academic-agent',
     image: 'https://yy-cloud-oss.oss-cn-beijing.aliyuncs.com/img/banner_20250317.png',
@@ -52,6 +58,11 @@ const swiperList = ref<SwiperItem[]>([
 ])
 
 const handleSwiperClick = ({ item }: SwiperClickEvent): void => {
+  if (item.target) {
+    uni.navigateTo({ url: item.target })
+    return
+  }
+
   if (!item.id) return
 
   uni.navigateTo({

+ 85 - 0
src/services/modules/infoCollection/index.ts

@@ -0,0 +1,85 @@
+interface InfoCollectionResponse<T = unknown> {
+  code: number
+  msg?: string
+  data?: T
+}
+
+interface InfoCollectionRequestError {
+  statusCode?: number
+  msg: string
+  raw?: unknown
+}
+
+export interface CompliancePlanPayload {
+  source: 'A001'
+  channel: string
+  name: string
+  phone: string
+  company: string
+  education: string
+  interest: string
+}
+
+export interface TrainingRegistrationPayload {
+  source: 'A002'
+  channel: string
+  name: string
+  gender: string
+  phone: string
+  idCard: string
+  region: string
+  company: string
+  jobTitle: string
+  applyEducation: string
+  certificateImages: string
+  address: string
+}
+
+const getInfoCollectionUrl = (path: string): string => {
+  let baseUrl = String(import.meta.env.VITE_BASE_API || '').replace(/\/$/, '')
+
+  // #ifdef H5
+  if (import.meta.env.MODE === 'development') {
+    baseUrl = ''
+  }
+  // #endif
+
+  return `${baseUrl}${path}`
+}
+
+export const submitInfoCollectionApi = (
+  data: CompliancePlanPayload | TrainingRegistrationPayload
+): Promise<InfoCollectionResponse> => {
+  return new Promise((resolve, reject: (reason: InfoCollectionRequestError) => void) => {
+    uni.request({
+      url: getInfoCollectionUrl('/admin/info/collection'),
+      method: 'POST',
+      data,
+      header: {
+        'Content-Type': 'application/json',
+      },
+      success(response) {
+        if (response.statusCode === 200) {
+          resolve(response.data as InfoCollectionResponse)
+          return
+        }
+
+        const responseData = response.data as { msg?: string } | undefined
+        reject({
+          statusCode: response.statusCode,
+          msg: responseData?.msg || `提交失败(HTTP ${response.statusCode})`,
+        })
+      },
+      fail(error) {
+        reject({
+          msg: '提交失败,请检查网络后重试',
+          raw: error,
+        })
+      },
+    })
+  })
+}
+
+export const getInfoCollectionUploadUrl = (): string => {
+  return getInfoCollectionUrl('/admin/info/upload')
+}