小程序按钮实现先表单校验再走手机号获取功能

文章目录

需求

在点击按钮进行提交表单时,需要先实现表单校验再走手机号获取功能

分析

我们知道open-type="getPhoneNumber"该属性只能在button上加

原先想法

javascript 复制代码
<view class="submit-btn">
  <button
    id="phoneBtn"
    class="hidden-btn"
    open-type="getPhoneNumber"
    bindgetphonenumber="onGetPhoneNumber"
    bindtap="checkFormThenGetPhone"
    catchtap="true"
  >
    <text class="submit-btn-text">立即云寄</text>
  </button>
</view>
javascript 复制代码
// 1. 点击按钮 → 先校验表单
  checkFormThenGetPhone() {
    // 防重复提交
    if (this.data.submitting) return;

    // 必填项校验
    if (!this.data.isAnonymous && !this.data.name.trim()) {
      wx.showToast({ title: '请输入姓名', icon: 'none' });
      return;
    }
    if (!this.data.region.length) {
      wx.showToast({ title: '请选择家乡/所在地', icon: 'none' });
      return;
    }
    if (!this.data.birthday) {
      wx.showToast({ title: '请选择出生日期', icon: 'none' });
      return;
    }
    if (!this.data.gender) {
      wx.showToast({ title: '请选择性别', icon: 'none' });
      return;
    }
    if (!this.data.wish.trim()) {
      wx.showToast({ title: '请填写云寄心愿', icon: 'none' });
      return;
    }

    // ======================
    // 表单校验通过!手动触发获取手机号
    // ======================
    wx.createSelectorQuery()
      .select('#phoneBtn')
      .node()
      .exec((res) => {
        const btn = res[0].node;
        btn.click();
      });
  },

  // 2. 获取手机号成功 → 自动提交表单
  async onGetPhoneNumber(e) {
    try {
      // 标记提交中
      this.setData({ submitting: true });

      // 校验用户是否授权
      if (!e?.detail || e.detail.errMsg !== 'getPhoneNumber:ok') {
        wx.showToast({ title: '取消授权', icon: 'none' });
        this.setData({ submitting: false });
        return;
      }

      // 获取凭证
      const phoneCode = e.detail.code;
      const { code: loginCode } = await wx.login();

      if (!phoneCode || !loginCode) {
        wx.showToast({ title: '获取凭证失败', icon: 'none' });
        this.setData({ submitting: false });
        return;
      }

      // 登录后端
      const resp = await weixinMiniAppLogin(phoneCode, loginCode, 'default');
      if (!resp || resp.code !== 0) {
        wx.showToast({ title: resp?.msg || '登录失败', icon: 'none' });
        this.setData({ submitting: false });
        return;
      }

      // 登录成功 → 保存用户信息
      const userInfo = await getUserInfo();
      const info = await getSocialUser(socialType);
      wx.setStorageSync('user', userInfo.data);
      wx.setStorageSync('openId', info.data.openid);

      // ======================
      // ✅ 登录成功 → 提交表单
      // ======================
      await this.submitRealForm();

    } catch (err) {
      console.log('登录异常:', err);
      wx.showToast({ title: '登录失败,请重试', icon: 'none' });
      this.setData({ submitting: false });
    }
  },

  // 3. 真正提交表单的方法
  async submitRealForm() {
    try {
      // 组装表单
      const formData = {
        name: this.data.isAnonymous ? '匿名' : this.data.name,
        isAnonymous: this.data.isAnonymous,
        region: this.data.region,
        birthday: this.data.birthday,
        gender: this.data.gender,
        wish: this.data.wish,
        plan: this.data.selectedPlan,
        audioFilePath: this.data.audioFilePath,
        attachmentPath: this.data.attachmentPath,
      };

      // 提交接口
      const res = await createPrayOrder(formData);
      console.log('提交成功:', res);

      wx.showToast({ title: '提交成功', icon: 'success' });

      // 提交成功后返回
      setTimeout(() => {
        wx.navigateBack({ delta: 1 });
      }, 1500);

    } catch (err) {
      console.error('提交失败:', err);
      wx.showToast({ title: '提交失败,请重试', icon: 'none' });
    } finally {
      this.setData({ submitting: false });
    }
  },

修改后的想法

  1. 校验必填项
  2. 系统登录校验
  3. 弹出小程序获取手机号获取的功能
  4. 套餐购买
  5. 支付
javascript 复制代码
  onPurchase() {
    this.closeModal()
    // ***必填项校验***
    if (!this.data.isAnonymous && !this.data.name.trim()) {
      wx.showToast({ title: '请输入姓名', icon: 'none' });
      return;
    }
    if (!this.data.region.length) {
      wx.showToast({ title: '请选择家乡/所在地', icon: 'none' });
      return;
    }
    if (!this.data.birthday) {
      wx.showToast({ title: '请选择出生日期', icon: 'none' });
      return;
    }
    if (!this.data.gender) {
      wx.showToast({ title: '请选择性别', icon: 'none' });
      return;
    }
    if (!this.data.wish.trim()) {
      wx.showToast({ title: '请填写内容', icon: 'none' });
      return;
    }
    // ***校验套餐***
    const { packageInfo, isPurchaseLocked } = this.data
    // 支付流程锁:仅在弹窗确认后加锁,但若已锁定则直接拦截后续触发
    if (isPurchaseLocked) {
      return
    }
    // ***登录校验***
    const user = wx.getStorageSync('user') || {}
    if (!user || !user.mobile || user.mobile === '未绑定手机号') {
      this.setData({ showModalLogin: true })
      return
    }
    
    // ***确认购买***
    wx.showModal({
      title: '确认购买',
      content: `确认购买 ${packageInfo.name || '无人机自动拍摄'}?\n价格:¥${packageInfo.price || 0.01}`,
      success: async res => {
        if (!res.confirm) {
          return
        }
        if (this.data.isPurchaseLocked) {
          return
        }

        this.setData({ isPurchaseLocked: true })
        
        try {
          const payRes = await createAndPayOrder(this)
          wx.showToast({ title: '支付成功', icon: 'success' })

          const data = (payRes && payRes.data) || {}
          const now = Date.now()
          const firstItem = (data.items && data.items[0]) || {}
          const amount =
            typeof data.payPrice === 'number' ? data.payPrice / 100 : 0
          const orderTime = Number(data.createTime) || now
          const orderTimeText = formatTime(orderTime)
          const validityPeriod = formatTime(
            orderTime + 30 * 24 * 60 * 60 * 1000
          )
          const id = String(data.id)
          const title = firstItem.spuName || ''
          const pickUpStoreId = data.pickUpStoreId
          console.log(data,id,title,pickUpStoreId);
          // ===ToDo:支付完成后的跳转===
          // 1. 清空页面表单信息
          this.setData({
            name: '',
            isAnonymous: false,
            region: [],
            regionText: '',
            birthday: '',
            gender: '',
            wish: '',
            wishLength: 0,
            recordState: 'idle',
            recordDuration: 0,
            audioFilePath: '',
            isPlaying: false,
            attachmentPath: '',
            photoUrl: '',
            resultText: '',
            isRecording: false,
            // 重置购买锁,允许再次购买
            isPurchaseLocked: false
          });
          // 支付成功后,直接发起跳转,避免跳转前窗口期重复触发
          await new Promise(resolve => {
            wx.switchTab({
              url: '/pages/live/live'
            });
          })
        } catch (err) {
          console.error('下单/支付失败', err)
          wx.showToast({ title: err.message || '支付失败', icon: 'none' })
          this.setData({ isPurchaseLocked: false })
        }
      },
    })

  },
javascript 复制代码
async onGetPhoneNumber(e) {
    try {
      // 校验用户是否授权
      if (!e?.detail || e.detail.errMsg !== 'getPhoneNumber:ok') {
        wx.showToast({ title: '取消授权', icon: 'none' });
        return;
      }

      // 获取凭证
      const phoneCode = e.detail.code;
      const { code: loginCode } = await wx.login();

      if (!phoneCode || !loginCode) {
        wx.showToast({ title: '获取凭证失败', icon: 'none' });
        return;
      }

      // 登录后端
      const resp = await weixinMiniAppLogin(phoneCode, loginCode, 'default', socialType);
      if (!resp || resp.code !== 0) {
        wx.showToast({ title: resp?.msg || '登录失败', icon: 'none' });
        return;
      }

      // 登录成功 → 保存用户信息
      const userInfo = await getUserInfo();
      const info = await getSocialUser(socialType);
      wx.setStorageSync('user', userInfo.data);
      wx.setStorageSync('openId', info.data.openid);

      // ======================
      // ✅ 登录成功 → 提交表单
      // ======================
      await this.onPurchase();

    } catch (err) {
      console.log('登录异常:', err);
      wx.showToast({ title: '登录失败,请重试', icon: 'none' });
    }
  },

整体代码

1. wxml
javascript 复制代码
<!-- pages/pray/pray.wxml -->
<navigation-bar title="飞达" back="{{false}}" color="#FFF5D0" background="transparent"></navigation-bar>
<scroll-view class="page-scroll" scroll-y="true" enhanced="true" show-scrollbar="false">
  <view class="pray-img"></view>
  <view class="page-container">
    <!-- ===== 表单卡片区 ===== -->
    <view class="form-card">
      <!-- 1. 姓名 -->
      <view class="field-section">
        <view class="field-header">
          <text class="field-label">您的姓名</text>
          <view class="anonymous-toggle" bindtap="onAnonymousToggle">
            <view class="checkbox-box {{isAnonymous ? 'checked' : ''}}">
              <text wx:if="{{isAnonymous}}" class="checkbox-tick">✓</text>
            </view>
            <text class="anonymous-text">匿名</text>
          </view>
        </view>
        <input class="field-input" placeholder="请输入姓名" placeholder-class="input-placeholder" value="{{name}}" bindinput="onNameInput" disabled="{{isAnonymous}}" />
      </view>
      <!-- 2. 家乡/所在地 -->
      <view class="field-section">
        <view class="field-header">
          <text class="field-label"><text class="required">*</text>家乡/所在地</text>
        </view>
        <picker mode="region" value="{{region}}" bindchange="onRegionChange">
          <view class="picker-display">
            <text class="picker-text {{regionText ? '' : 'placeholder'}}">
              {{regionText || '请选择地区'}}
            </text>
            <text class="picker-arrow">▾</text>
          </view>
        </picker>
      </view>
      <!-- 3. 出生日期 -->
      <view class="field-section">
        <view class="field-header">
          <text class="field-label"><text class="required">*</text>出生日期</text>
        </view>
        <picker mode="date" value="{{birthday}}" start="1920-01-01" end="2026-12-31" bindchange="onBirthdayChange">
          <view class="picker-display">
            <text class="picker-text {{birthday ? '' : 'placeholder'}}">
              {{birthday || '请选择日期'}}
            </text>
            <text class="picker-icon">📅</text>
          </view>
        </picker>
      </view>
      <!-- 4. 性别 -->
      <view class="field-section">
        <view class="field-header">
          <text class="field-label"><text class="required">*</text>性别</text>
          <view class="gender-options">
            <view class="gender-item" data-value="male" bindtap="onGenderChange">
              <view class="radio-circle {{gender === 'male' ? 'selected' : ''}}">
                <view wx:if="{{gender === 'male'}}" class="radio-dot"></view>
              </view>
              <text class="gender-text">男</text>
            </view>
            <view class="gender-item" data-value="female" bindtap="onGenderChange">
              <view class="radio-circle {{gender === 'female' ? 'selected' : ''}}">
                <view wx:if="{{gender === 'female'}}" class="radio-dot"></view>
              </view>
              <text class="gender-text">女</text>
            </view>
          </view>
        </view>
      </view>
      <!-- 5. 语音转文字 -->
      <view class="field-section" wx:if="{{1==0}}">
        <view class="field-header">
          <text class="field-label">
            <text class="required">*</text>语音转文字
          </text>
          <!-- 未录音状态 -->
          <view wx:if="{{recordState === 'idle'}}" class="audio-record-area" bindtap="startRecording">
            <text class="mic-icon">🎙</text>
          </view>
          <!-- 录音中状态 -->
          <view wx:elif="{{recordState === 'recording'}}" class="audio-record-area recording" bindtap="stopRecording">
            <view class="recording-pulse"></view>
            <text class="recording-duration">{{recordDuration}}s</text>
          </view>
          <!-- 已录音状态 -->
          <view wx:else class="audio-recorded-area">
            <view class="audio-play-btn" bindtap="toggleAudio">
              <text class="play-icon-text">{{isPlaying ? '⏸' : '▶'}}</text>
            </view>
            <text class="recorded-duration">{{recordDuration}}s</text>
            <view class="re-record-btn" bindtap="reRecord">
              <text class="re-record-text">重录</text>
            </view>
          </view>
        </view>
      </view>
      <!-- 6. 飞达/内容 -->
      <view class="field-section">
        <view class="field-header">
          <text class="field-label"><text class="required">*</text>内容<text class="field-note"> 内容将通过AI审核,请保持真诚善良</text></text>
        </view>
        <textarea class="field-textarea" placeholder="在这里写下您的内容,字数在300字内..." placeholder-class="input-placeholder" maxlength="300" value="{{wish}}" bindinput="onWishInput" />
        <!-- <view class="audio-record-area"  bindtouchstart="startRecord" bindtouchend="stopRecord">
          <text class="mic-icon">{{isRecording ? '⏹️' : '🎤'}}  {{isRecording ? '松开停止' : '长按语音转文字'}} <text class="record-time" wx:if="{{isRecording}}">{{recordDuration}}s</text></text>
        </view> -->
      </view>
      <!-- 7. 上传附件 -->
      <view class="field-section last">
        <view class="field-header">
          <text class="field-label">上传附件(可选)</text>
        </view>
        <view wx:if="{{!attachmentPath}}" class="upload-area" bindtap="onUploadImage">
          <text class="upload-text">+ 上传手写内容,支持小于2M</text>
        </view>
        <view wx:else class="preview-area">
          <image class="preview-image" src="{{attachmentPath}}" mode="aspectFill" />
          <view class="remove-btn" bindtap="onRemoveImage">
            <text class="remove-icon">×</text>
          </view>
        </view>
      </view>
    </view>
    <!-- ===== 服务套餐卡片 ===== -->
    <view class="section-divider">
      <text class="divider-text">服务选择</text>
      <block wx:for="{{plans}}" wx:key="key">
        <view class="plan-card {{selectedPlanId === item.key ? 'selected' : ''}}" data-plan="{{item.key}}" bindtap="onPlanSelect">
          <view class="plan-header">
            <text class="plan-name {{selectedPlanId === item.key ? 'active' : ''}}">{{item.name}}</text>
            <text class="{{selectedPlanId === item.key ? 'plan-price' : ''}}">¥{{item.price}}</text>
          </view>
          <view class="plan-features">
            <view class="feature-item" wx:for="{{item.content}}" wx:for-item="feat" wx:key="*this">
              <icon class="service-icon-check" type="success" size="14" color="#C89A5A"/>
              <text class="feature-text {{selectedPlanId === item.key ? 'feature-text-active' : ''}}">{{feat}}</text>
            </view>
          </view>
        </view>
      </block>
    <!-- ===== 提示信息 ===== -->
    <view class="notice-bar">
      <text class="notice-icon">ⓘ</text>
      <text class="notice-text">投放统一安排于每日晚间(00:00-01:00)订单打印后无法退单</text>
    </view>
    </view>
    <!-- 底部占位,防止被固定按钮遮挡 -->
    <view class="bottom-placeholder"></view>
  </view>
</scroll-view>
<!-- ===== 底部固定按钮栏 ===== -->
<view class="bottom-bar">
  <view class="price-text">¥<text class="price-text-active">{{currentPlan.price || 0}}</text></view>
  <view class="submit-btn {{isPurchaseLocked ? 'purchase-btn-disabled' : ''}}" bindtap="onPurchase">
    <text class="submit-btn-text">{{isPurchaseLocked ? '支付处理中...' : '立即飞达'}}</text>
  </view>
</view>
<!-- 弹窗 -->
<view class="custom-modal" wx:if="{{showModalLogin}}">
  <!-- 遮罩层:点击遮罩可以关闭弹窗 -->
  <view class="modal-mask" bindtap="closeModal"></view>
  
  <!-- 弹窗内容区域 -->
  <view class="modal-content">
    <view class="modal-header">
      <text class="modal-title">确认订单</text>
      <text class="modal-close" bindtap="closeModal">×</text>
    </view>
    
    <view class="modal-body">
      <text class="modal-desc">为了更好地为您服务,请授权手机号</text>
        <button
          id="phoneBtn"
          open-type="getPhoneNumber"
          bindgetphonenumber="onGetPhoneNumber"
          class="hidden-btn"
        ><text class="submit-btn-text">确定</text></button>
    </view>
  </view>
</view>
2. js
javascript 复制代码
// 引入微信同声传译插件
const plugin = requirePlugin('WechatSI')
// 获取全局唯一的语音识别管理器
const manager = plugin.getRecordRecognitionManager()
const { DictApi } = require('../../api/dict')
const { socialType } = require('../../utils/auth')
const { formatTime, parseDictListToKV } = require('../../utils/util')

const {
  fetchProductList,
  fetchProductDetail,
  createTradeOrder,
  submitPayOrder,
  getPayOrder,
  getPayOrderDetail,
} = require('../../api/prayApi');
const {
  weixinMiniAppLogin,
  getUserInfo,
  getSocialUser,
} = require('../../api/loginApi')
const { uploadAvatar } = require('../../api/mineApi')


async function loadDetail(ctx, id) {
  try {
    const res = await fetchProductDetail(id)
    const detail = res?.data || {}
    const name = detail.name
    const price = detail.price
    const description = detail.description || ''
    const introduction = detail.introduction || ''
    const img = detail.picUrl
    const sildeImgs = (detail.sliderPicUrls || []).map(url => ({
      url,
      isVideo: typeof url === 'string' && url.toLowerCase().endsWith('.mp4'),
    }))
    const content = detail.content
    const availableTimeInfo = detail.availableTimeInfo
    const tourMethod = detail.tourMethod
    const verificationMethod = detail.verificationMethod
    const validTime = detail.validTime

    const dictKV = ctx.data.dictKV
    const tourMethodKV = dictKV['product_tour_method'] || {}
    const verificationMethodKV = dictKV['product_verification_method'] || {}

    const tourMethodText = tourMethodKV[tourMethod] || ''
    const verificationMethodText =
      verificationMethodKV[verificationMethod] || ''

    const validTimeText = formatTime(validTime)

    ctx.setData({
      _spuDetail: detail,
      'packageInfo.name': name,
      'packageInfo.price': price / 100,
      'packageInfo.description': description,
      'packageInfo.content': content,
      'packageInfo.img': img,
      'packageInfo.sildeImgs': sildeImgs,
      'packageInfo.introduction': introduction,
      'packageInfo.availableTimeInfo': availableTimeInfo,
      'packageInfo.tourMethod': tourMethod,
      'packageInfo.verificationMethod': verificationMethod,
      'packageInfo.validTime': validTime,
      'packageInfo.tourMethodText': tourMethodText,
      'packageInfo.verificationMethodText': verificationMethodText,
      'packageInfo.validTimeText': validTimeText,
      'packageInfo.pickUpStoreName': detail.pickUpStoreName,
    })
  } catch (err) {
    console.error('获取商品详情失败', err)
  }
}

async function createAndPayOrder(ctx) {
  const detail = ctx.data._spuDetail || {}
  const skuList = detail.skus || []
  const sku = skuList[0] || { id: detail.skuId, socialType: socialType }

  const user = wx.getStorageSync('user') || {}
  const receiverMobile = user.mobile || '13800000000'
  const receiverName = user.nickname || '游客'
  const openid = wx.getStorageSync('openId')
  let createdOrderId = ''
  const payload = {
    order:{
      deliveryType: 2,
      items: [
        {
          skuId: sku?.id,
          count: 1,
          socialType: socialType,
        },
      ],
      pickUpStoreId: detail.pickUpStoreId,
      pointStatus: false,
      receiverMobile,
      receiverName,
    },
    wishInfo:{
      name: ctx.data.isAnonymous ? '匿名' : ctx.data.name,
      isAnonymous: ctx.data.isAnonymous?1:0,
      areaName: ctx.data.region[ctx.data.region.length - 1],
      birthday: ctx.data.birthday,
      sex: ctx.data.gender === 'male' ? 1 : 2,
      wishContent: ctx.data.wish,
      photoUrl: ctx.data.photoUrl,
    }
  }

  const cRes = await createTradeOrder(payload)
  const orderId = cRes?.data?.payOrderId
  if (!orderId) {
    throw new Error('创建订单失败')
  }
  createdOrderId = String(orderId)
  // 创建成功后,先查询一次订单信息/状态
  await getPayOrder(orderId, true)

  const payPayload = {
    id: orderId,
    channelCode: 'wx_wish',
    channelExtras: { openid },
    openid,
  }
  const pRes = await submitPayOrder(payPayload)
  const { code } = pRes
  if (code !== 0) {
    throw new Error('购买失败')
  }
  const payData = pRes?.data || {}

  const params = JSON.parse(payData.displayContent)
  const payParams = {
    timeStamp: params.timeStamp,
    nonceStr: params.nonceStr,
    package: params.packageValue,
    signType: params.signType,
    paySign: params.paySign,
  }

  let orderDetail = null
  await new Promise((resolve, reject) => {
    wx.requestPayment({
      ...payParams,
      success: async () => {
        if (createdOrderId) {
          try {
            const res = await getPayOrder(createdOrderId, true)
            orderDetail = await getPayOrderDetail(
              res?.data?.merchantOrderId,
              true
            )
          } catch (err) {
            console.error('获取订单详情失败', err)
          }
        }
        resolve()
      },
      fail: reject,
    })
  })

  return orderDetail
}

// pages/pray/pray.js
Page({
  data: {
    packageInfo: {
      id: '',
      name: '',
      price: 0,
      description: '',
      videoUrl: '',
      videoStates: {
        img1: false,
        img2: false,
      },
    },
    _spuDetail: null,
    dictKV: {},
    isFullScreen: false,
    isPurchaseLocked: false, // 防止重复点击购买按钮
    // 表单字段
    name: '',
    isAnonymous: false,
    region: [],
    regionText: '',
    birthday: '',
    gender: '',
    wish: '',
    wishLength: 0,

    // 录音
    recordState: 'idle', // 'idle' | 'recording' | 'recorded'
    recordDuration: 0,
    audioFilePath: '',
    isPlaying: false,

    // 附件
    attachmentPath: '',
    photoUrl: '',

    // 服务选择
    selectedPlanId: '',
    currentPlan: {},
    plans: [],

    showModalLogin: false, // 控制弹窗显示
    // ====语音转文字====
    // 录音状态:false-未录音,true-正在录音
    isRecording: false,
    // 识别结果文本
    resultText: '',
    // 录音时长(秒)
    recordDuration: 0,
    // 定时器ID
    timer: 0
  },

  // 录音管理器和音频播放器实例
  recorderManager: null,
  innerAudioContext: null,
  recordTimer: null,

  onLoad() {
    this._initRecorder();
    this._initAudioPlayer();
    this.initRecordManager()
    this.getPackageData() // 商品列表
  },

  onUnload() {
    if (this.recordTimer) {
      clearInterval(this.recordTimer);
    }
    if (this.innerAudioContext) {
      this.innerAudioContext.destroy();
    }
    // 页面卸载时停止录音并清除定时器
    if (this.data.isRecording) {
      manager.stop()
    }
    if (this.data.timer) {
      clearInterval(this.data.timer)
    }
  },

  // ===== 初始化 =====

   /**
    * 初始化录音管理器
  */
  initRecordManager() {
    // 识别中事件
    manager.onRecognize = (res) => {
      console.log('实时识别结果:', res.result)
      this.setData({
        resultText: res.result
      })
    }
    // 识别结束事件
    manager.onStop = (res) => {
      console.log('识别结束:', res)
      let text = res.result
      
      // 处理可能的空结果
      if (!text) {
        wx.showToast({
          title: '未识别到内容',
          icon: 'none',
          duration: 2000
        })
        text = ''
      } else {
        // 将识别结果添加到内容中
        this.setData({
          wish: this.data.wish + text,
          wishLength: (this.data.wish + text).length
        })
      }
      
      this.setData({
        resultText: text,
        isRecording: false,
        recordDuration: 0
      })
      
      // 清除定时器
      if (this.data.timer) {
        clearInterval(this.data.timer)
      }
    }
    // 识别错误事件
    manager.onError = (err) => {
      console.error('识别错误:', err)
      wx.showToast({
        title: '识别失败: ' + err.errMsg,
        icon: 'none',
        duration: 3000
      })
      this.setData({
        isRecording: false,
        recordDuration: 0
      })
      
      if (this.data.timer) {
        clearInterval(this.data.timer)
      }
    }
  },
  _initRecorder() {
    this.recorderManager = wx.getRecorderManager();

    this.recorderManager.onStart(() => {
      this.setData({ recordState: 'recording', recordDuration: 0 });
      this.recordTimer = setInterval(() => {
        this.setData({ recordDuration: this.data.recordDuration + 1 });
      }, 1000);
    });

    this.recorderManager.onStop((res) => {
      if (this.recordTimer) {
        clearInterval(this.recordTimer);
        this.recordTimer = null;
      }
      if (res.tempFilePath) {
        this.setData({
          recordState: 'recorded',
          audioFilePath: res.tempFilePath,
        });
      }
    });

    this.recorderManager.onError((err) => {
      if (this.recordTimer) {
        clearInterval(this.recordTimer);
        this.recordTimer = null;
      }
      this.setData({ recordState: 'idle', recordDuration: 0 });
      wx.showToast({ title: '录音失败', icon: 'none' });
      console.error('录音错误:', err);
    });
  },

  _initAudioPlayer() {
    this.innerAudioContext = wx.createInnerAudioContext();

    this.innerAudioContext.onEnded(() => {
      this.setData({ isPlaying: false });
    });

    this.innerAudioContext.onError((err) => {
      this.setData({ isPlaying: false });
      wx.showToast({ title: '播放失败', icon: 'none' });
      console.error('播放错误:', err);
    });
  },

  // ===== 表单事件 =====

  onNameInput(e) {
    this.setData({ name: e.detail.value });
  },

  onAnonymousToggle() {
    const isAnonymous = !this.data.isAnonymous;
    this.setData({
      isAnonymous,
      name: isAnonymous ? '' : this.data.name,
    });
  },

  onRegionChange(e) {
    const region = e.detail.value;
    this.setData({
      region,
      regionText: region.join(' / '),
    });
  },

  onBirthdayChange(e) {
    this.setData({ birthday: e.detail.value });
  },

  onGenderChange(e) {
    const value = e.currentTarget.dataset.value;
    this.setData({ gender: value });
  },

  // ===== 录音事件 =====

  startRecording() {
    wx.getSetting({
      success: (res) => {
        if (res.authSetting['scope.record'] === false) {
          wx.showModal({
            title: '需要录音权限',
            content: '请在设置中允许录音权限',
            success: (modalRes) => {
              if (modalRes.confirm) {
                wx.openSetting();
              }
            },
          });
          return;
        }

        wx.authorize({
          scope: 'scope.record',
          success: () => {
            this.recorderManager.start({
              duration: 60000,
              sampleRate: 16000,
              numberOfChannels: 1,
              encodeBitRate: 96000,
              format: 'mp3',
            });
          },
          fail: () => {
            wx.showToast({ title: '请授权录音权限', icon: 'none' });
          },
        });
      },
    });
  },

  stopRecording() {
    this.recorderManager.stop();
    console.log(this.data.audioFilePath,this.innerAudioContext)
  },
  
  toggleAudio() {
    if (this.data.isPlaying) {
      this.innerAudioContext.stop();
      this.setData({ isPlaying: false });
    } else {
      if (!this.data.audioFilePath) return;
      this.innerAudioContext.src = this.data.audioFilePath;
      console.log(this.data.audioFilePath,this.innerAudioContext.src)
      this.innerAudioContext.play();
      this.setData({ isPlaying: true });
    }
  },

  reRecord() {
    if (this.data.isPlaying) {
      this.innerAudioContext.stop();
    }
    this.setData({
      recordState: 'idle',
      recordDuration: 0,
      audioFilePath: '',
      isPlaying: false,
    });
  },

  // ===== 内容输入 =====

  onWishInput(e) {
    const wish = e.detail.value;
    this.setData({ wish, wishLength: wish.length });
  },

  // ===== 图片上传 =====

  onUploadImage() {
    wx.chooseMedia({
      count: 1,
      mediaType: ['image'],
      sourceType: ['album', 'camera'],
      success: (res) => {
        const tempFilePath = res.tempFiles[0].tempFilePath;
        this.setData({ attachmentPath: tempFilePath });
        wx.showLoading({ title: '上传中...' })
        uploadAvatar(tempFilePath)
          .then(fileUrl => {
            this.setData({ photoUrl: fileUrl })
          })
          .catch(err => {
            console.error(err)
            wx.hideLoading()
            wx.showToast({ title: '上传失败', icon: 'none' })
          })
          .finally(() => {
            wx.hideLoading()
            wx.showToast({ title: '上传成功', icon: 'success' })
          })
      },
    });
  },

  onRemoveImage() {
    this.setData({ attachmentPath: '', photoUrl: '' });
  },

  // ===== 套餐选择 =====
  async loadDicts() {
      const types = [
        'product_verification_method', // 核销方式
        'product_tour_method', // 观览方式
        'product_shoot_method', // 拍摄方式
      ]
      return Promise.all(types.map(type => DictApi(type)))
        .then(list => {
          const dictKV = {}
          list.forEach((res, idx) => {
            const type = types[idx]
            const dataList = (res && res.data) || res || []
            dictKV[type] = parseDictListToKV(dataList)
          })
          this.setData({ dictKV })
        })
        .catch(() => {})
    },
  async onPlanSelect(e) {
    const packageId = e.currentTarget.dataset.plan;
    this.setData({ selectedPlanId: packageId, currentPlan: this.data.plans.find(p => p.key === packageId) });
    // 拉取商品详情
    await this.loadDicts()
    loadDetail(this, packageId)
  },

  // ===== 提交验证 =====

  onPurchase() {
    this.closeModal()
    // ***必填项校验***
    if (!this.data.isAnonymous && !this.data.name.trim()) {
      wx.showToast({ title: '请输入姓名', icon: 'none' });
      return;
    }
    if (!this.data.region.length) {
      wx.showToast({ title: '请选择家乡/所在地', icon: 'none' });
      return;
    }
    if (!this.data.birthday) {
      wx.showToast({ title: '请选择出生日期', icon: 'none' });
      return;
    }
    if (!this.data.gender) {
      wx.showToast({ title: '请选择性别', icon: 'none' });
      return;
    }
    if (!this.data.wish.trim()) {
      wx.showToast({ title: '请填写内容', icon: 'none' });
      return;
    }
    // ***校验套餐***
    const { packageInfo, isPurchaseLocked } = this.data
    // 支付流程锁:仅在弹窗确认后加锁,但若已锁定则直接拦截后续触发
    if (isPurchaseLocked) {
      return
    }
    // ***登录校验***
    const user = wx.getStorageSync('user') || {}
    if (!user || !user.mobile || user.mobile === '未绑定手机号') {
      this.setData({ showModalLogin: true })
      return
    }
    
    // ***确认购买***
    wx.showModal({
      title: '确认购买',
      content: `确认购买 ${packageInfo.name || '无人机自动拍摄'}?\n价格:¥${packageInfo.price || 0.01}`,
      success: async res => {
        if (!res.confirm) {
          return
        }
        if (this.data.isPurchaseLocked) {
          return
        }

        this.setData({ isPurchaseLocked: true })
        
        try {
          const payRes = await createAndPayOrder(this)
          wx.showToast({ title: '支付成功', icon: 'success' })

          const data = (payRes && payRes.data) || {}
          const now = Date.now()
          const firstItem = (data.items && data.items[0]) || {}
          const amount =
            typeof data.payPrice === 'number' ? data.payPrice / 100 : 0
          const orderTime = Number(data.createTime) || now
          const orderTimeText = formatTime(orderTime)
          const validityPeriod = formatTime(
            orderTime + 30 * 24 * 60 * 60 * 1000
          )
          const id = String(data.id)
          const title = firstItem.spuName || ''
          const pickUpStoreId = data.pickUpStoreId
          console.log(data,id,title,pickUpStoreId);
          // ===ToDo:支付完成后的跳转===
          // 1. 清空页面表单信息
          this.setData({
            name: '',
            isAnonymous: false,
            region: [],
            regionText: '',
            birthday: '',
            gender: '',
            wish: '',
            wishLength: 0,
            recordState: 'idle',
            recordDuration: 0,
            audioFilePath: '',
            isPlaying: false,
            attachmentPath: '',
            photoUrl: '',
            resultText: '',
            isRecording: false,
            // 重置购买锁,允许再次购买
            isPurchaseLocked: false
          });
          // 支付成功后,直接发起跳转,避免跳转前窗口期重复触发
          await new Promise(resolve => {
            wx.switchTab({
              url: '/pages/live/live'
            });
          })
        } catch (err) {
          console.error('下单/支付失败', err)
          wx.showToast({ title: err.message || '支付失败', icon: 'none' })
          this.setData({ isPurchaseLocked: false })
        }
      },
    })

  },
  closeModal() {
    this.setData({ showModalLogin: false })
  },
  async onGetPhoneNumber(e) {
    try {
      // 校验用户是否授权
      if (!e?.detail || e.detail.errMsg !== 'getPhoneNumber:ok') {
        wx.showToast({ title: '取消授权', icon: 'none' });
        return;
      }

      // 获取凭证
      const phoneCode = e.detail.code;
      const { code: loginCode } = await wx.login();

      if (!phoneCode || !loginCode) {
        wx.showToast({ title: '获取凭证失败', icon: 'none' });
        return;
      }

      // 登录后端
      const resp = await weixinMiniAppLogin(phoneCode, loginCode, 'default', socialType);
      if (!resp || resp.code !== 0) {
        wx.showToast({ title: resp?.msg || '登录失败', icon: 'none' });
        return;
      }

      // 登录成功 → 保存用户信息
      const userInfo = await getUserInfo();
      const info = await getSocialUser(socialType);
      wx.setStorageSync('user', userInfo.data);
      wx.setStorageSync('openId', info.data.openid);

      // ======================
      // ✅ 登录成功 → 提交表单
      // ======================
      await this.onPurchase();

    } catch (err) {
      console.log('登录异常:', err);
      wx.showToast({ title: '登录失败,请重试', icon: 'none' });
    }
  },
 
  // ===== 语音转文字 =====
  startRecord() {
      // 检查录音权限
      wx.getSetting({
        success: (res) => {
          if (!res.authSetting['scope.record']) {
            // 申请录音权限
            wx.authorize({
              scope: 'scope.record',
              success: () => {
                this.startRecording()
              },
              fail: () => {
                wx.showModal({
                  title: '权限不足',
                  content: '需要录音权限才能使用语音识别功能,请在设置中开启',
                  confirmText: '去设置',
                  success: (modalRes) => {
                    if (modalRes.confirm) {
                      wx.openSetting()
                    }
                  }
                })
              }
            })
          } else {
            // this.startRecording()
          }
        }
      })
  },

  /**
  * 实际开始录音的逻辑
  */
  startRecording() {
    // 重置结果
    this.setData({
      resultText: '',
      isRecording: true,
      recordDuration: 0
    })

    // 开始录音
    manager.start({
      lang: 'zh_CN', // 使用简体中文作为默认语言
      duration: 60000 // 最长录音时间,单位ms
    })

    // 开始计时
    const timer = setInterval(() => {
      this.setData({
        recordDuration: this.data.recordDuration + 1
      })
    }, 1000)

    this.setData({ timer })
  },

  /**
  * 停止录音
  */
  stopRecord() {
    if (this.data.isRecording) {
      manager.stop()
    }
  },

  /**
  * 页面显示时更新 tabBar 选中状态
  */
  onShow() {      
    if (typeof this.getTabBar === 'function' && this.getTabBar()) {
      this.getTabBar().setData({
        selected: 1
      });
    }
  },
  // 获取套餐接口
  async getPackageData() {
    try {
      // 你的接口请求
      const res = await fetchProductList({socialType:36});
      const list = res.data?.list || [];
      let plans = [];
      if (list.length) {
        plans = list.map(item => ({
          ...item,
          key: item.id,
          name: item.name,
          content: item.content.split(','),
          price: item.price / 100
        })) || []
        this.setData({
          plans: plans || [],
        });
        this.onPlanSelect({currentTarget:{dataset:{plan:plans[0].key}}});
      }
    } catch (err) {
      console.log('获取套餐失败', err)
    } finally {
    }
  },
});
3. wxss
javascript 复制代码
/* pages/pray/pray.wxss */

/* ===== 页面基础 ===== */
page {
  height: calc(100% - 180rpx);
  display: flex;
  flex-direction: column;
  background-color: #F8F2E9;
  background-image: url('https://www.dkifly.com/ifly-api/admin-api/infra/file/29/get/wxMiniProDefault/feiyuan/bg-pray.jpg');
  background-repeat: no-repeat;
  background-size: cover;
  /* background: url('https://www.dkifly.com/ifly-api/admin-api/infra/file/29/get/wxMiniProDefault/feiyuan/bg-pray.jpg') no-repeat center center / cover; */
}

.page-scroll {
  flex: 1;
  overflow-y: hidden;
}

.pray-img{
  width: 532rpx;
  height: 150rpx;
  background: url("https://www.dkifly.com/ifly-api/admin-api/infra/file/29/get/wxMiniProDefault/feiyuan/pray.png") no-repeat center center;
  background-size: cover;
  flex-shrink: 0;
  margin: 0 auto;
  margin-bottom: 10rpx;
  margin-top: 20rpx;
}
.page-container {
  padding: 24rpx 49rpx 0;
  box-sizing: border-box;
}

/* ===== 表单卡片 ===== */
.form-card {
  background: #FFFCF8;
  border-radius: 20rpx 20rpx 20rpx 20rpx;
  padding: 32rpx 24rpx;
  box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
  margin-bottom: 8rpx;
}

/* ===== 字段通用 ===== */
.field-section {
  margin-bottom: 16rpx;
}

.field-section.last {
  margin-bottom: 0;
}

.field-header {
  display: flex;
  flex-direction: row;
  align-items: center;
  margin-bottom: 16rpx;
}

.field-label {
  font-weight: bold;
  font-size: 28rpx;
  color: #3A2020;
}

.required {
  color: #C82920;
  margin-right: 4rpx;
}

.field-note {
  font-weight: 400;
  font-size: 24rpx;
  color: #767676;
  display: block;
}

/* ===== 输入框 ===== */
.field-input {
  height: 80rpx;
  background: #F4EDE3;
  border-radius: 6rpx 6rpx 6rpx 6rpx;
  border: 2rpx solid #E8DECA;
  padding: 0 14rpx;
  font-size: 28rpx;
  color: #767676;
  font-weight: 400;
  box-sizing: border-box;
}

.input-placeholder {
  color: #CCCCCC;
  font-size: 28rpx;
}

/* ===== Picker 选择器 ===== */
.picker-display {
  height: 80rpx;
  line-height: 7rpx;
  background: #F4EDE3;
  border-radius: 6rpx 6rpx 6rpx 6rpx;
  border: 2rpx solid #E8DECA;
  padding: 0 14rpx;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  box-sizing: border-box;
}

.picker-text {
  font-size: 28rpx;
  color: #333333;
}

.picker-text.placeholder {
  color: #CCCCCC;
}

.picker-arrow {
  font-size: 28rpx;
  color: #CCCCCC;
}

.picker-icon {
  font-size: 32rpx;
}

/* Picker 暗色模式适配 */
/* @media (prefers-color-scheme: dark) {
  .picker-display {
    background-color: #1E1E1E;
    border-color: #3A3A3A;
  }

  .picker-text {
    color: #E0E0E0;
  }

  .picker-text.placeholder {
    color: #666666;
  }

  .picker-arrow {
    color: #666666;
  }
} */

/* ===== 匿名 Checkbox ===== */
.anonymous-toggle {
  display: flex;
  flex-direction: row;
  align-items: center;
  margin-left: auto;
}

.checkbox-box {
  width: 32rpx;
  height: 32rpx;
  border-radius: 4rpx 4rpx 4rpx 4rpx;
  border: 2rpx solid #3A2020;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 8rpx;
}

.checkbox-box.checked {
  background-color: #C82920;
  border-color: #C82920;
}

.checkbox-tick {
  font-size: 20rpx;
  color: #FFFFFF;
  line-height: 1;
}

.anonymous-text {
  font-size: 26rpx;
  color: #666666;
}

/* ===== 性别 Radio ===== */
.gender-options {
  display: flex;
  flex-direction: row;
  align-items: center;
  margin-left: auto;
}

.gender-item {
  display: flex;
  flex-direction: row;
  align-items: center;
  margin-left: 32rpx;
}

.radio-circle {
  width: 32rpx;
  height: 32rpx;
  border-radius: 50%;
  border: 2rpx solid #D0C8BC;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 8rpx;
}

.radio-circle.selected {
  border-color: #C82920;
}

.radio-dot {
  width: 18rpx;
  height: 18rpx;
  border-radius: 50%;
  background-color: #C82920;
}

.gender-text {
  font-size: 28rpx;
  color: #333333;
}

/* ===== 录音区域 ===== */
.audio-record-area {
  flex: 1;
  height: 62rpx;
  border-radius: 36rpx;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  background-color: #FAFAFA;
  background-image: url('https://www.dkifly.com/ifly-api/admin-api/infra/file/29/get/wxMiniProDefault/feiyuan/voiceToText.jpg');
  background-repeat: no-repeat;
  /* background-size: contain; */
  background-position: center center;
  margin-top: 16rpx;
  font-weight: bold;
  font-size: 28rpx;
  color: #3A2020;
  width: 100%;
}

.audio-record-area.recording {
  border-color: #C82920;
  background-color: #FFF5F5;
}

.mic-icon {
  font-size: 30rpx;
}

.recording-pulse {
  width: 20rpx;
  height: 20rpx;
  border-radius: 50%;
  background-color: #C82920;
  margin-right: 12rpx;
  animation: pulse 1s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.5; transform: scale(1.3); }
}

.recording-duration {
  font-size: 26rpx;
  color: #C82920;
  font-weight: 500;
}

.audio-recorded-area {
  flex: 1;
  height: 72rpx;
  margin-left: 16rpx;
  border-radius: 36rpx;
  border: 1rpx solid #E8DDD0;
  display: flex;
  flex-direction: row;
  align-items: center;
  padding: 0 16rpx;
  background-color: #FAFAFA;
}

.audio-play-btn {
  width: 48rpx;
  height: 48rpx;
  border-radius: 50%;
  background-color: #C82920;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 12rpx;
}

.play-icon-text {
  font-size: 20rpx;
  color: #FFFFFF;
}

.recorded-duration {
  font-size: 26rpx;
  color: #666666;
  flex: 1;
}

.re-record-btn {
  padding: 8rpx 20rpx;
  border-radius: 24rpx;
  border: 1rpx solid #C82920;
}

.re-record-text {
  font-size: 22rpx;
  color: #C82920;
}

/* ===== Textarea 内容 ===== */
.field-textarea {
  width: 100%;
  min-height: 160rpx;
  background: #F4EDE3;
  border-radius: 6rpx 6rpx 6rpx 6rpx;
  border: 2rpx solid #E8DECA;
  padding: 0 14rpx;
  font-size: 28rpx;
  color: #333333;
  box-sizing: border-box;
  line-height: 1.6;
}

/* ===== 上传区域 ===== */
.upload-area {
  min-height: 120rpx;
  border-radius: 6rpx 6rpx 6rpx 6rpx;
  border: 2rpx dashed #946F32;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #FAFAFA;
}

.upload-text {
  font-weight: 400;
  font-size: 28rpx;
  color: #946F32;
}

.preview-area {
  position: relative;
  width: 100%;
  border-radius: 12rpx;
  overflow: hidden;
}

.preview-image {
  width: 100%;
  height: 320rpx;
  border-radius: 12rpx;
  display: block;
}

.remove-btn {
  position: absolute;
  top: 12rpx;
  right: 12rpx;
  width: 48rpx;
  height: 48rpx;
  border-radius: 50%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}

.remove-icon {
  font-size: 32rpx;
  color: #FFFFFF;
  line-height: 1;
}

/* ===== 分割线 ===== */
.section-divider {
  align-items: center;
  padding: 19rpx;
  margin-top: 20rpx;
  background: #FFFCF8;
  border-radius: 20rpx 20rpx 20rpx 20rpx;
}

.divider-text {
  font-weight: bold;
  font-size: 36rpx;
  color: #3A2020;
  white-space: nowrap;
  margin-bottom: 16rpx;
}

/* ===== 服务套餐卡片 ===== */
.plan-card {
  background: #FFFCF8;
  border-radius: 20rpx 20rpx 20rpx 20rpx;
  padding: 28rpx;
  margin-bottom: 20rpx;
  border: 2rpx solid #E8DDD0;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}

.plan-card.selected {
  border-color: #D4AF37;
  box-shadow: 0 4rpx 16rpx rgba(212, 175, 55, 0.2);
}

.plan-header {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16rpx;
  color: #3A2020;
}

.plan-name {
 font-weight: bold;
  font-size: 36rpx;
}

.plan-name.active {
  font-weight: bold;
  font-size: 36rpx;
  color: #946F32;
}

.plan-price {
  font-weight: 400;
  font-size: 36rpx;
  color: #946F32;
}

.plan-features {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  padding: 0 40rpx;
}

.feature-item {
  display: flex;
  flex-direction: row;
  align-items: center;
  margin-right: 24rpx;
  margin-bottom: 12rpx;
  width: 40%;
}


.feature-text {
  font-weight: 400;
  color: #3A2020;
  font-size: 24rpx;
  margin-left: 15rpx;
}
.feature-text-active {
  color: #946F32;
}

/* ===== 提示信息 ===== */
.notice-bar {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  padding: 0rpx 10rpx;
}

.notice-icon {
  font-weight: 600;
  font-size: 46rpx;
  color: #F2675B;
  margin-right: 12rpx;
  flex-shrink: 0;
}

.notice-text {
  font-weight: 600;
  font-size: 26rpx;
  color: #F2675B;
  line-height: 1.6;
}

/* ===== 底部占位 ===== */
.bottom-placeholder {
  height: 44rpx;
}

/* ===== 底部按钮栏 ===== */
.bottom-bar {
  padding: 16rpx 49rpx;
  /* padding-bottom: calc(16rpx + env(safe-area-inset-bottom)); */
  background: #FEFBF3;
  box-shadow: 0rpx 6rpx 30rpx 1rpx rgba(0,0,0,0.05);
      display: flex;
    justify-content: space-between;
}

.submit-btn {
  width: 428rpx;
  height: 114rpx;
  background: url("https://www.dkifly.com/ifly-api/admin-api/infra/file/29/get/wxMiniProDefault/feiyuan/start-pray.jpg") no-repeat center center;
  background-size: cover;
  display: flex;
  align-items: center;
  justify-content: center;
}
.price-text {
  display: flex;
  text-align: center;
  align-items: center;
  font-weight: bold;
  color: #896A33;
}
.price-text-active{
  font-size: 48rpx;

}
.submit-btn-text {
  font-size: 32rpx;
  font-weight: bold;
  color: #FFFFFF;
  letter-spacing: 4rpx;
}


/* 弹窗容器 */
.custom-modal {
  position: fixed;
  z-index: 9999;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* 遮罩层 */
.modal-mask {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5); /* 半透明黑色 */
}

/* 弹窗主体 */
.modal-content {
  position: relative;
  width: 80%;
  max-width: 600rpx;
  background-color: #fff;
  border-radius: 16rpx;
  padding: 30rpx;
  box-sizing: border-box;
  text-align: center;
  z-index: 10000;
  animation: fadeIn 0.3s ease-out;
}

/* 弹窗头部 */
.modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20rpx;
  border-bottom: 1px solid #eee;
  padding-bottom: 10rpx;
}

.modal-title {
  font-size: 32rpx;
  font-weight: bold;
  color: #333;
}

.modal-close {
  font-size: 48rpx;
  color: #999;
  line-height: 30rpx;
  padding: 0 10rpx;
}

/* 弹窗内容 */
.modal-body {
  padding: 20rpx 0;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.modal-desc {
  display: block;
  font-size: 28rpx;
  color: #666;
  margin-bottom: 40rpx;
}

.hidden-btn {
  background: linear-gradient( 0deg, #EC6346 0%, #BF1920 100%);
  border-radius: 12rpx 12rpx 12rpx 12rpx;
  border: 2rpx solid #FFE6AC;
  color: #fff;
  width: 100% !important;
  height: 80rpx;
  display: flex;
  justify-content: center;
  align-items: center;
}
相关推荐
超级无敌谢大脚1 小时前
【无标题】
开发语言·前端·javascript
码途漫谈1 小时前
Easy-Vibe高级开发篇阅读笔记(十三)——多平台开发之Android App 原生开发
android·人工智能·笔记·ai·开源·ai编程
街灯L1 小时前
【ADB】使用ADB工具箱卸载安卓系统软件
android·adb
赏金术士2 小时前
Kotlin 从入门到进阶 之泛型 模块(七)
android·开发语言·kotlin
万象资讯2 小时前
2026实测|订货小程序哪个平台支持快速部署?
小程序
码云社区2 小时前
JAVA同城上门做饭系统家政上门同城服务系统源码小程序+APP+公众号+h5
java·开发语言·小程序
果壳~2 小时前
【Uniapp】【rich-text】富文本展示以及图片预览功能解决方案
前端·javascript·uni-app
z19408920662 小时前
在线生成背景:字号层级怎么做才像「正式物料」
前端·javascript·html
李白的天不白2 小时前
vue优化建议
前端·javascript·vue.js