hookehuyr

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

- 新增老师表单录入页面,包含姓名、身份证、尺寸等必填字段
- 实现表单验证逻辑,包括身份证号码校验
- 添加本地存储mock服务用于表单数据保存
- 配置路由模块以支持老师表单页面访问
- 使用Vant UI组件构建响应式表单界面
1 +const teacherFormRoutes = [
2 + {
3 + path: '/teacher-form',
4 + name: '老师表单录入',
5 + component: () => import('@/views/teacherForm/index.vue'),
6 + meta: {
7 + title: '老师表单录入'
8 + },
9 + children: []
10 + }
11 +];
12 +
13 +export default teacherFormRoutes;
14 +
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 +
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 +
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 +