hookehuyr

feat(teacher-form): 新增老师表单录入页面及功能

- 新增老师表单录入页面,包含姓名、身份证、尺寸等必填字段
- 实现表单验证逻辑,包括身份证号码校验
- 添加本地存储mock服务用于表单数据保存
- 配置路由模块以支持老师表单页面访问
- 使用Vant UI组件构建响应式表单界面
const teacherFormRoutes = [
{
path: '/teacher-form',
name: '老师表单录入',
component: () => import('@/views/teacherForm/index.vue'),
meta: {
title: '老师表单录入'
},
children: []
}
];
export default teacherFormRoutes;
<template>
<div class="teacher-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>
<van-field
v-model="form.name"
name="name"
placeholder="请输入姓名"
:rules="[{ required: true, message: '请输入姓名' }]"
/>
</div>
<div class="field-block required">
<div class="field-label">身份证</div>
<van-field
v-model="form.id_card"
name="id_card"
maxlength="18"
placeholder="请输入身份证号码"
:rules="[
{ required: true, message: '请输入身份证号码' },
{ validator: idCardValidator, message: '请输入正确身份证号码' }
]"
/>
</div>
<div class="field-block required">
<div class="field-label">尺寸</div>
<van-field
v-model="form.size_name"
readonly
is-link
name="size_name"
placeholder="请选择尺寸"
:rules="[{ required: true, message: '请选择尺寸' }]"
@click="showSizePicker = true"
/>
</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="showSizePicker" round position="bottom">
<van-picker
:columns="sizeColumns"
@cancel="showSizePicker = false"
@confirm="onSizeConfirm"
/>
</van-popup>
</van-config-provider>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { showDialog, showFailToast, showSuccessToast } from 'vant';
import { styleColor } from '@/constant.js';
import { submitTeacherRegistration } from './service';
const themeVars = {
buttonPrimaryBackground: styleColor.baseColor,
buttonPrimaryBorderColor: styleColor.baseColor,
buttonPrimaryColor: styleColor.baseFontColor
};
const sizeColumns = [
{ text: '成年XS码', value: 'adult_xs' },
{ text: '成年S码', value: 'adult_s' },
{ text: '成年M码', value: 'adult_m' },
{ text: '成年L码', value: 'adult_l' }
];
const form = reactive({
name: '',
id_card: '',
size: '',
size_name: ''
});
const showSizePicker = ref(false);
const saveLoading = ref(false);
const idCardValidator = (value) => {
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)) {
return false;
}
const factors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
const checks = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
const sum = value
.slice(0, 17)
.split('')
.reduce((total, item, index) => total + Number(item) * factors[index], 0);
return checks[sum % 11] === value[17].toUpperCase();
};
const onSizeConfirm = ({ selectedOptions }) => {
const size = selectedOptions[0];
showSizePicker.value = false;
form.size = size.value;
form.size_name = size.text;
};
const saveForm = async () => {
saveLoading.value = true;
try {
await submitTeacherRegistration({ ...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(() => {
document.title = '老师表单录入';
});
</script>
<style lang="less" scoped>
.teacher-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 {
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: '*';
}
}
.submit-bar {
padding-top: 4px;
}
</style>
const STORAGE_KEY = 'teacher-form-registrations';
const delay = (data, timeout = 200) => new Promise((resolve) => {
setTimeout(() => resolve(data), timeout);
});
const readRegistrations = () => {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
} catch (error) {
console.warn('读取老师报名 mock 缓存失败', error);
return [];
}
};
const writeRegistrations = (list) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
};
export const saveMockTeacherRegistration = (params) => {
const list = readRegistrations();
const now = Date.now();
const nextItem = {
...params,
id: `teacher_${now}`,
updated_at: now
};
list.unshift(nextItem);
writeRegistrations(list);
return delay({
code: 1,
msg: '保存成功',
data: nextItem
}, 300);
};
import { saveMockTeacherRegistration } from './mock';
export const submitTeacherRegistration = async (formData) => {
const res = await saveMockTeacherRegistration(formData);
if (res.code !== 1) {
throw new Error(res.msg || '保存老师表单失败');
}
return res.data;
};