feat(teacher-form): 新增老师表单录入页面及功能
- 新增老师表单录入页面,包含姓名、身份证、尺寸等必填字段 - 实现表单验证逻辑,包括身份证号码校验 - 添加本地存储mock服务用于表单数据保存 - 配置路由模块以支持老师表单页面访问 - 使用Vant UI组件构建响应式表单界面
Showing
4 changed files
with
304 additions
and
0 deletions
src/views/teacherForm/index.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="teacher-form-page"> | ||
| 3 | + <van-config-provider :theme-vars="themeVars"> | ||
| 4 | + <div class="form-card"> | ||
| 5 | + <header class="page-header"> | ||
| 6 | + <h1>老师表单录入</h1> | ||
| 7 | + <p>请填写老师报名信息。</p> | ||
| 8 | + </header> | ||
| 9 | + | ||
| 10 | + <van-form | ||
| 11 | + scroll-to-error | ||
| 12 | + scroll-to-error-position="center" | ||
| 13 | + validate-first | ||
| 14 | + @submit="saveForm" | ||
| 15 | + @failed="handleSubmitFailed" | ||
| 16 | + > | ||
| 17 | + <section class="form-section"> | ||
| 18 | + <div class="field-block required"> | ||
| 19 | + <div class="field-label">姓名</div> | ||
| 20 | + <van-field | ||
| 21 | + v-model="form.name" | ||
| 22 | + name="name" | ||
| 23 | + placeholder="请输入姓名" | ||
| 24 | + :rules="[{ required: true, message: '请输入姓名' }]" | ||
| 25 | + /> | ||
| 26 | + </div> | ||
| 27 | + | ||
| 28 | + <div class="field-block required"> | ||
| 29 | + <div class="field-label">身份证</div> | ||
| 30 | + <van-field | ||
| 31 | + v-model="form.id_card" | ||
| 32 | + name="id_card" | ||
| 33 | + maxlength="18" | ||
| 34 | + placeholder="请输入身份证号码" | ||
| 35 | + :rules="[ | ||
| 36 | + { required: true, message: '请输入身份证号码' }, | ||
| 37 | + { validator: idCardValidator, message: '请输入正确身份证号码' } | ||
| 38 | + ]" | ||
| 39 | + /> | ||
| 40 | + </div> | ||
| 41 | + | ||
| 42 | + <div class="field-block required"> | ||
| 43 | + <div class="field-label">尺寸</div> | ||
| 44 | + <van-field | ||
| 45 | + v-model="form.size_name" | ||
| 46 | + readonly | ||
| 47 | + is-link | ||
| 48 | + name="size_name" | ||
| 49 | + placeholder="请选择尺寸" | ||
| 50 | + :rules="[{ required: true, message: '请选择尺寸' }]" | ||
| 51 | + @click="showSizePicker = true" | ||
| 52 | + /> | ||
| 53 | + </div> | ||
| 54 | + </section> | ||
| 55 | + | ||
| 56 | + <div class="submit-bar"> | ||
| 57 | + <van-button block round type="primary" native-type="submit" :loading="saveLoading">保存</van-button> | ||
| 58 | + </div> | ||
| 59 | + </van-form> | ||
| 60 | + </div> | ||
| 61 | + | ||
| 62 | + <van-popup v-model:show="showSizePicker" round position="bottom"> | ||
| 63 | + <van-picker | ||
| 64 | + :columns="sizeColumns" | ||
| 65 | + @cancel="showSizePicker = false" | ||
| 66 | + @confirm="onSizeConfirm" | ||
| 67 | + /> | ||
| 68 | + </van-popup> | ||
| 69 | + </van-config-provider> | ||
| 70 | + </div> | ||
| 71 | +</template> | ||
| 72 | + | ||
| 73 | +<script setup> | ||
| 74 | +import { onMounted, reactive, ref } from 'vue'; | ||
| 75 | +import { showDialog, showFailToast, showSuccessToast } from 'vant'; | ||
| 76 | +import { styleColor } from '@/constant.js'; | ||
| 77 | +import { submitTeacherRegistration } from './service'; | ||
| 78 | + | ||
| 79 | +const themeVars = { | ||
| 80 | + buttonPrimaryBackground: styleColor.baseColor, | ||
| 81 | + buttonPrimaryBorderColor: styleColor.baseColor, | ||
| 82 | + buttonPrimaryColor: styleColor.baseFontColor | ||
| 83 | +}; | ||
| 84 | + | ||
| 85 | +const sizeColumns = [ | ||
| 86 | + { text: '成年XS码', value: 'adult_xs' }, | ||
| 87 | + { text: '成年S码', value: 'adult_s' }, | ||
| 88 | + { text: '成年M码', value: 'adult_m' }, | ||
| 89 | + { text: '成年L码', value: 'adult_l' } | ||
| 90 | +]; | ||
| 91 | + | ||
| 92 | +const form = reactive({ | ||
| 93 | + name: '', | ||
| 94 | + id_card: '', | ||
| 95 | + size: '', | ||
| 96 | + size_name: '' | ||
| 97 | +}); | ||
| 98 | + | ||
| 99 | +const showSizePicker = ref(false); | ||
| 100 | +const saveLoading = ref(false); | ||
| 101 | + | ||
| 102 | +const idCardValidator = (value) => { | ||
| 103 | + if (!/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(value)) { | ||
| 104 | + return false; | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + const factors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; | ||
| 108 | + const checks = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']; | ||
| 109 | + const sum = value | ||
| 110 | + .slice(0, 17) | ||
| 111 | + .split('') | ||
| 112 | + .reduce((total, item, index) => total + Number(item) * factors[index], 0); | ||
| 113 | + | ||
| 114 | + return checks[sum % 11] === value[17].toUpperCase(); | ||
| 115 | +}; | ||
| 116 | + | ||
| 117 | +const onSizeConfirm = ({ selectedOptions }) => { | ||
| 118 | + const size = selectedOptions[0]; | ||
| 119 | + showSizePicker.value = false; | ||
| 120 | + form.size = size.value; | ||
| 121 | + form.size_name = size.text; | ||
| 122 | +}; | ||
| 123 | + | ||
| 124 | +const saveForm = async () => { | ||
| 125 | + saveLoading.value = true; | ||
| 126 | + try { | ||
| 127 | + await submitTeacherRegistration({ ...form }); | ||
| 128 | + showSuccessToast('保存成功'); | ||
| 129 | + } catch (error) { | ||
| 130 | + showFailToast(error.message); | ||
| 131 | + } finally { | ||
| 132 | + saveLoading.value = false; | ||
| 133 | + } | ||
| 134 | +}; | ||
| 135 | + | ||
| 136 | +const handleSubmitFailed = ({ errors = [] }) => { | ||
| 137 | + const message = errors.find((item) => item.message)?.message || '请检查老师表单信息'; | ||
| 138 | + | ||
| 139 | + showDialog({ | ||
| 140 | + title: '请完善老师信息', | ||
| 141 | + message, | ||
| 142 | + confirmButtonColor: styleColor.baseColor | ||
| 143 | + }); | ||
| 144 | +}; | ||
| 145 | + | ||
| 146 | +onMounted(() => { | ||
| 147 | + document.title = '老师表单录入'; | ||
| 148 | +}); | ||
| 149 | +</script> | ||
| 150 | + | ||
| 151 | +<style lang="less" scoped> | ||
| 152 | +.teacher-form-page { | ||
| 153 | + min-height: 100vh; | ||
| 154 | + padding: 16px; | ||
| 155 | + background: #f3f5f7; | ||
| 156 | + box-sizing: border-box; | ||
| 157 | +} | ||
| 158 | + | ||
| 159 | +.form-card { | ||
| 160 | + min-height: calc(100vh - 32px); | ||
| 161 | + padding: 22px 14px 28px; | ||
| 162 | + background: #ffffff; | ||
| 163 | + border-radius: 16px; | ||
| 164 | + box-sizing: border-box; | ||
| 165 | +} | ||
| 166 | + | ||
| 167 | +.page-header { | ||
| 168 | + margin-bottom: 42px; | ||
| 169 | + | ||
| 170 | + h1 { | ||
| 171 | + margin: 0; | ||
| 172 | + color: #101010; | ||
| 173 | + font-size: 22px; | ||
| 174 | + line-height: 1.35; | ||
| 175 | + font-weight: 700; | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + p { | ||
| 179 | + margin: 14px 0 0; | ||
| 180 | + color: #8a8f96; | ||
| 181 | + font-size: 14px; | ||
| 182 | + line-height: 1.6; | ||
| 183 | + } | ||
| 184 | +} | ||
| 185 | + | ||
| 186 | +.form-section { | ||
| 187 | + margin-bottom: 36px; | ||
| 188 | +} | ||
| 189 | + | ||
| 190 | +.field-block { | ||
| 191 | + margin-bottom: 30px; | ||
| 192 | + | ||
| 193 | + :deep(.van-cell) { | ||
| 194 | + min-height: 46px; | ||
| 195 | + padding: 0 14px; | ||
| 196 | + align-items: center; | ||
| 197 | + border: 1px solid #e8e8e8; | ||
| 198 | + border-radius: 6px; | ||
| 199 | + box-shadow: none; | ||
| 200 | + } | ||
| 201 | + | ||
| 202 | + :deep(.van-field__control) { | ||
| 203 | + color: #222222; | ||
| 204 | + font-size: 14px; | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + :deep(.van-field__control::placeholder) { | ||
| 208 | + color: #b9bec3; | ||
| 209 | + } | ||
| 210 | + | ||
| 211 | + :deep(.van-field__error-message) { | ||
| 212 | + padding-top: 4px; | ||
| 213 | + } | ||
| 214 | +} | ||
| 215 | + | ||
| 216 | +.field-label { | ||
| 217 | + position: relative; | ||
| 218 | + margin-bottom: 14px; | ||
| 219 | + color: #101010; | ||
| 220 | + font-size: 17px; | ||
| 221 | + line-height: 1.4; | ||
| 222 | + font-weight: 700; | ||
| 223 | +} | ||
| 224 | + | ||
| 225 | +.required .field-label { | ||
| 226 | + padding-left: 14px; | ||
| 227 | + | ||
| 228 | + &::before { | ||
| 229 | + position: absolute; | ||
| 230 | + left: 0; | ||
| 231 | + top: 1px; | ||
| 232 | + color: #ee3f3f; | ||
| 233 | + content: '*'; | ||
| 234 | + } | ||
| 235 | +} | ||
| 236 | + | ||
| 237 | +.submit-bar { | ||
| 238 | + padding-top: 4px; | ||
| 239 | +} | ||
| 240 | +</style> | ||
| 241 | + |
src/views/teacherForm/mock.js
0 → 100644
| 1 | +const STORAGE_KEY = 'teacher-form-registrations'; | ||
| 2 | + | ||
| 3 | +const delay = (data, timeout = 200) => new Promise((resolve) => { | ||
| 4 | + setTimeout(() => resolve(data), timeout); | ||
| 5 | +}); | ||
| 6 | + | ||
| 7 | +const readRegistrations = () => { | ||
| 8 | + try { | ||
| 9 | + return JSON.parse(localStorage.getItem(STORAGE_KEY)) || []; | ||
| 10 | + } catch (error) { | ||
| 11 | + console.warn('读取老师报名 mock 缓存失败', error); | ||
| 12 | + return []; | ||
| 13 | + } | ||
| 14 | +}; | ||
| 15 | + | ||
| 16 | +const writeRegistrations = (list) => { | ||
| 17 | + localStorage.setItem(STORAGE_KEY, JSON.stringify(list)); | ||
| 18 | +}; | ||
| 19 | + | ||
| 20 | +export const saveMockTeacherRegistration = (params) => { | ||
| 21 | + const list = readRegistrations(); | ||
| 22 | + const now = Date.now(); | ||
| 23 | + const nextItem = { | ||
| 24 | + ...params, | ||
| 25 | + id: `teacher_${now}`, | ||
| 26 | + updated_at: now | ||
| 27 | + }; | ||
| 28 | + | ||
| 29 | + list.unshift(nextItem); | ||
| 30 | + writeRegistrations(list); | ||
| 31 | + | ||
| 32 | + return delay({ | ||
| 33 | + code: 1, | ||
| 34 | + msg: '保存成功', | ||
| 35 | + data: nextItem | ||
| 36 | + }, 300); | ||
| 37 | +}; | ||
| 38 | + |
src/views/teacherForm/service.js
0 → 100644
| 1 | +import { saveMockTeacherRegistration } from './mock'; | ||
| 2 | + | ||
| 3 | +export const submitTeacherRegistration = async (formData) => { | ||
| 4 | + const res = await saveMockTeacherRegistration(formData); | ||
| 5 | + if (res.code !== 1) { | ||
| 6 | + throw new Error(res.msg || '保存老师表单失败'); | ||
| 7 | + } | ||
| 8 | + | ||
| 9 | + return res.data; | ||
| 10 | +}; | ||
| 11 | + |
-
Please register or login to post a comment