index.vue 10.4 KB
<template>
  <div class="family-form-page">
    <van-config-provider :theme-vars="themeVars">
      <div class="form-card">
        <header class="page-header">
          <h1>[新]第六届“益起走”幼儿园公益践行赛报名</h1>
          <p>感谢参加本场活动!</p>
        </header>

        <van-form
          scroll-to-error
          scroll-to-error-position="center"
          validate-first
          @submit="saveForm"
          @failed="handleSubmitFailed"
        >
          <section class="form-section">
            <div class="field-block required">
              <div class="field-label">所在幼儿园班级</div>
              <div class="school-row">
                <van-field
                  v-model="form.kg_name"
                  readonly
                  is-link
                  name="kg_name"
                  placeholder="请选择幼儿园"
                  :rules="[{ required: true, message: '请选择幼儿园' }]"
                  @click="showSchoolPicker = true"
                />
                <van-field
                  v-model="form.class_name"
                  readonly
                  is-link
                  name="class_name"
                  placeholder="请选择班级"
                  :rules="[{ required: true, message: '请选择班级' }]"
                  @click="openClassPicker"
                />
              </div>
            </div>

            <div class="field-block required">
              <div class="field-label">活动当天参赛的孩童数量(输入大于等于1的数字)</div>
              <van-field
                v-model="form.child_count"
                name="child_count"
                type="digit"
                placeholder="请输入孩童数量"
                :rules="[{ validator: childCountValidator, message: '请输入大于等于1的数字' }]"
              />
            </div>

            <div class="field-block required">
              <div class="field-label">活动当天参赛的家长数量(请输入1-2之间的数字)</div>
              <van-field
                v-model="form.parent_count"
                name="parent_count"
                type="digit"
                placeholder="请输入家长数量"
                :rules="[{ validator: parentCountValidator, message: '请输入1-2之间的数字' }]"
              />
            </div>

            <div class="field-block required">
              <div class="field-label">参赛孩童姓名</div>
              <p class="field-desc">如果有2位小朋友参赛,可以用空格间隔孩童姓名,例:申小花 申小朵</p>
              <van-field
                v-model="form.child_names"
                name="child_names"
                placeholder="请输入参赛孩童姓名"
                :rules="[{ required: true, message: '请输入参赛孩童姓名' }]"
              />
            </div>

            <div class="field-block required">
              <div class="field-label">手机</div>
              <p class="field-desc">每组家庭填写一个手机号码,该号码将用于登录专属小程序支付报名费、募款、并在活动当天在线打卡等。</p>
              <van-field
                v-model="form.phone"
                name="phone"
                type="tel"
                maxlength="11"
                placeholder="请输入手机号"
                :rules="[
                  { required: true, message: '请输入手机号' },
                  { validator: phoneValidator, message: '请输入正确手机号' }
                ]"
              />
            </div>

            <div class="field-block required">
              <div class="field-label">证件登记</div>
              <p class="field-desc">请准确填写参赛人员姓名和证件信息,用于购买赛事保险;如证件为非身份证,请填写出生年月日。</p>
              <p class="field-desc">填写格式:<br>姓名1 证件号1<br>姓名2 证件号2</p>
              <van-field
                v-model="form.certificate_info"
                name="certificate_info"
                type="textarea"
                rows="4"
                autosize
                placeholder="请输入证件登记信息"
                :rules="[{ required: true, message: '请输入证件登记信息' }]"
              />
            </div>
          </section>

          <section class="form-section clothes-section">
            <div class="section-title">服装尺码登记</div>
            <p class="section-desc">在需要的尺码里,填写数量</p>

            <div v-for="item in clothesFields" :key="item.field" class="field-block">
              <div class="field-label">{{ item.label }}</div>
              <van-field
                v-model="form[item.field]"
                :name="item.field"
                type="digit"
                :placeholder="`请输入${item.label}数量`"
              />
            </div>
          </section>

          <div class="submit-bar">
            <van-button block round type="primary" native-type="submit" :loading="saveLoading">保存</van-button>
          </div>
        </van-form>
      </div>

      <van-popup v-model:show="showSchoolPicker" round position="bottom">
        <van-picker
          :columns="schoolOnlyColumns"
          @cancel="showSchoolPicker = false"
          @confirm="onSchoolConfirm"
        />
      </van-popup>

      <van-popup v-model:show="showClassPicker" round position="bottom">
        <van-picker
          :columns="classColumns"
          @cancel="showClassPicker = false"
          @confirm="onClassConfirm"
        />
      </van-popup>
    </van-config-provider>
  </div>
</template>

<script setup>
import { computed, onMounted, reactive, ref } from 'vue';
import { showDialog, showFailToast, showSuccessToast } from 'vant';
import { styleColor } from '@/constant.js';
import {
  fetchSchoolOptions,
  submitRegistration
} from './service';

const themeVars = {
  buttonPrimaryBackground: styleColor.baseColor,
  buttonPrimaryBorderColor: styleColor.baseColor,
  buttonPrimaryColor: styleColor.baseFontColor
};

const defaultForm = {
  kg_id: '',
  kg_name: '',
  class_id: '',
  class_name: '',
  child_count: '',
  parent_count: '',
  child_names: '',
  phone: '',
  certificate_info: '',
  clothes_child_110: '',
  clothes_child_120: '',
  clothes_child_130: '',
  clothes_adult_xs: '',
  clothes_adult_s: '',
  clothes_adult_m: '',
  clothes_adult_l: ''
};

const clothesFields = [
  { label: '孩童110码', field: 'clothes_child_110' },
  { label: '孩童120码', field: 'clothes_child_120' },
  { label: '孩童130码', field: 'clothes_child_130' },
  { label: '成年XS码', field: 'clothes_adult_xs' },
  { label: '成年S码', field: 'clothes_adult_s' },
  { label: '成年M码', field: 'clothes_adult_m' },
  { label: '成年L码', field: 'clothes_adult_l' }
];

const form = reactive({ ...defaultForm });
const schoolColumns = ref([]);
const showSchoolPicker = ref(false);
const showClassPicker = ref(false);
const saveLoading = ref(false);

const currentSchool = computed(() => schoolColumns.value.find((item) => item.value === form.kg_id));
const classColumns = computed(() => currentSchool.value?.children || []);
const schoolOnlyColumns = computed(() => schoolColumns.value.map((item) => ({
  text: item.text,
  value: item.value
})));

const childCountValidator = (value) => Number(value) >= 1;
const parentCountValidator = (value) => {
  const count = Number(value);
  return count >= 1 && count <= 2;
};
const phoneValidator = (value) => /^1\d{10}$/.test(value);

const openClassPicker = () => {
  if (!form.kg_id) {
    showFailToast('请先选择幼儿园');
    return;
  }
  showClassPicker.value = true;
};

const onSchoolConfirm = ({ selectedOptions }) => {
  const school = selectedOptions[0];
  showSchoolPicker.value = false;
  form.kg_id = school.value;
  form.kg_name = school.text;
  form.class_id = '';
  form.class_name = '';
};

const onClassConfirm = ({ selectedOptions }) => {
  const classItem = selectedOptions[0];
  showClassPicker.value = false;
  form.class_id = classItem.value;
  form.class_name = classItem.text;
};

const saveForm = async () => {
  saveLoading.value = true;
  try {
    await submitRegistration({ ...form });
    showSuccessToast('保存成功');
  } catch (error) {
    showFailToast(error.message);
  } finally {
    saveLoading.value = false;
  }
};

const handleSubmitFailed = ({ errors = [] }) => {
  const message = errors.find((item) => item.message)?.message || '请检查报名信息';

  showDialog({
    title: '请完善报名信息',
    message,
    confirmButtonColor: styleColor.baseColor
  });
};

onMounted(async () => {
  document.title = '家庭表单录入';

  try {
    schoolColumns.value = await fetchSchoolOptions();
  } catch (error) {
    showFailToast(error.message);
  }
});
</script>

<style lang="less" scoped>
.family-form-page {
  min-height: 100vh;
  padding: 16px;
  background: #f3f5f7;
  box-sizing: border-box;
}

.form-card {
  min-height: calc(100vh - 32px);
  padding: 22px 14px 28px;
  background: #ffffff;
  border-radius: 16px;
  box-sizing: border-box;
}

.page-header {
  margin-bottom: 42px;

  h1 {
    margin: 0;
    color: #101010;
    font-size: 22px;
    line-height: 1.35;
    font-weight: 700;
  }

  p {
    margin: 14px 0 0;
    color: #8a8f96;
    font-size: 14px;
    line-height: 1.6;
  }
}

.form-section {
  margin-bottom: 36px;
}

.field-block {
  margin-bottom: 30px;

  :deep(.van-cell) {
    min-height: 46px;
    padding: 0 14px;
    align-items: center;
    border: 1px solid #e8e8e8;
    border-radius: 6px;
    box-shadow: none;
  }

  :deep(.van-field__control) {
    color: #222222;
    font-size: 14px;
  }

  :deep(.van-field__control::placeholder) {
    color: #b9bec3;
  }

  :deep(.van-field__error-message) {
    padding-top: 4px;
  }
}

.field-label,
.section-title {
  position: relative;
  margin-bottom: 14px;
  color: #101010;
  font-size: 17px;
  line-height: 1.4;
  font-weight: 700;
}

.required .field-label {
  padding-left: 14px;

  &::before {
    position: absolute;
    left: 0;
    top: 1px;
    color: #ee3f3f;
    content: '*';
  }
}

.field-desc,
.section-desc {
  margin: -6px 0 14px;
  color: #9aa1a8;
  font-size: 14px;
  line-height: 1.65;
}

.school-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.clothes-section {
  padding-top: 4px;
}

.section-desc {
  margin-top: 0;
  margin-bottom: 58px;
}

.submit-bar {
  padding-top: 4px;
}

@media (max-width: 340px) {
  .school-row {
    grid-template-columns: 1fr;
  }
}
</style>