fix(前端): 表单提交前去除首尾空格并统一校验

Made-with: Cursor
This commit is contained in:
zhonghua 2026-03-26 10:21:34 +08:00
parent 9d76e178de
commit 5f55aadb75
14 changed files with 95 additions and 8 deletions

View File

@ -130,6 +130,7 @@ import { PlusOutlined } from '@ant-design/icons-vue';
import { getThemeList } from '@/api/theme'; import { getThemeList } from '@/api/theme';
import { uploadFile, getFileUrl } from '@/api/file'; import { uploadFile, getFileUrl } from '@/api/file';
import type { Theme } from '@/api/theme'; import type { Theme } from '@/api/theme';
import { trimFormModel } from '@/utils/trimFormModel';
interface BasicInfoData { interface BasicInfoData {
name: string; name: string;
@ -316,6 +317,10 @@ const handleChange = () => {
// //
const validate = async () => { const validate = async () => {
try { try {
// plain object/array
trimFormModel(formData as any);
// v-model
handleChange();
await formRef.value?.validate(); await formRef.value?.validate();
// //

View File

@ -204,6 +204,7 @@ import type { FormInstance } from 'ant-design-vue';
import FileUploader from './FileUploader.vue'; import FileUploader from './FileUploader.vue';
import LessonStepsEditor from './LessonStepsEditor.vue'; import LessonStepsEditor from './LessonStepsEditor.vue';
import type { StepData } from './LessonStepsEditor.vue'; import type { StepData } from './LessonStepsEditor.vue';
import { trimFormModel } from '@/utils/trimFormModel';
export interface LessonData { export interface LessonData {
id?: number | string; id?: number | string;
@ -360,6 +361,10 @@ const handleChange = () => {
// Step4/5/6 // Step4/5/6
const validate = async () => { const validate = async () => {
try { try {
// plain object/array
trimFormModel(lessonData as any);
// v-model
handleChange();
await formRef.value?.validate(); await formRef.value?.validate();
return { valid: true, errors: [] as string[] }; return { valid: true, errors: [] as string[] };
} catch (err: any) { } catch (err: any) {

View File

@ -0,0 +1,47 @@
/**
* / Dayjs
*
* model ant-design-vue v-model/validate
*/
type AnyRecord = Record<string, unknown>;
const isPlainObject = (val: unknown): val is AnyRecord => {
if (val === null || typeof val !== 'object') return false;
const proto = Object.getPrototypeOf(val);
return proto === Object.prototype || proto === null;
};
const walk = (val: unknown, seen: WeakSet<object>): unknown => {
if (typeof val === 'string') {
return val.trim();
}
if (Array.isArray(val)) {
for (let i = 0; i < val.length; i++) {
val[i] = walk(val[i], seen) as any;
}
return val;
}
if (val && typeof val === 'object') {
// 仅处理普通对象;其余对象(如 Date/Dayjs/自定义 class保持原样
if (!isPlainObject(val)) return val;
if (seen.has(val)) return val;
seen.add(val);
for (const key of Object.keys(val)) {
(val as AnyRecord)[key] = walk((val as AnyRecord)[key], seen);
}
return val;
}
return val;
};
export function trimFormModel<T>(model: T): T {
// 普通对象/数组一般不会包含循环引用,但这里做个保护
const seen = new WeakSet<object>();
return walk(model, seen) as T;
}

View File

@ -341,6 +341,7 @@ import {
LOGIN_ACCOUNT_PATTERN_MESSAGE, LOGIN_ACCOUNT_PATTERN_MESSAGE,
LOGIN_ACCOUNT_PLACEHOLDER, LOGIN_ACCOUNT_PLACEHOLDER,
} from '@/constants/loginAccount'; } from '@/constants/loginAccount';
import { trimFormModel } from '@/utils/trimFormModel';
// //
const searchForm = reactive({ const searchForm = reactive({
@ -570,6 +571,8 @@ const handleEdit = (record: Tenant) => {
// //
const handleModalOk = async () => { const handleModalOk = async () => {
try { try {
//
trimFormModel(formData as any);
await formRef.value?.validate(); await formRef.value?.validate();
} catch { } catch {
return; return;

View File

@ -324,6 +324,7 @@ import {
removeClassTeacher, removeClassTeacher,
} from '@/api/school'; } from '@/api/school';
import type { ClassInfo, CreateClassDto, Teacher, Student, ClassTeacher, AddClassTeacherDto } from '@/api/school'; import type { ClassInfo, CreateClassDto, Teacher, Student, ClassTeacher, AddClassTeacherDto } from '@/api/school';
import { trimFormModel } from '@/utils/trimFormModel';
// Custom gender icons as components // Custom gender icons as components
const BoyOutlined = { const BoyOutlined = {
@ -511,6 +512,8 @@ const getPrimaryTeacherId = (teachers?: ClassTeacher[]): number | undefined => {
const handleModalOk = async () => { const handleModalOk = async () => {
try { try {
//
trimFormModel(formState as any);
await formRef.value?.validate(); await formRef.value?.validate();
submitting.value = true; submitting.value = true;

View File

@ -274,6 +274,7 @@ import {
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue'; import type { FormInstance } from 'ant-design-vue';
import { fileApi, validateFileType } from '@/api/file'; import { fileApi, validateFileType } from '@/api/file';
import { trimFormModel } from '@/utils/trimFormModel';
const loading = ref(false); const loading = ref(false);
const submitting = ref(false); const submitting = ref(false);
@ -395,6 +396,8 @@ const handleDelete = async (id: number) => {
const handleModalOk = async () => { const handleModalOk = async () => {
try { try {
//
trimFormModel(formState as any);
await formRef.value?.validate(); await formRef.value?.validate();
submitting.value = true; submitting.value = true;
message.success(isEdit.value ? '更新成功' : '添加成功'); message.success(isEdit.value ? '更新成功' : '添加成功');

View File

@ -347,6 +347,7 @@ import {
LOGIN_ACCOUNT_PATTERN_MESSAGE, LOGIN_ACCOUNT_PATTERN_MESSAGE,
LOGIN_ACCOUNT_PLACEHOLDER, LOGIN_ACCOUNT_PLACEHOLDER,
} from '@/constants/loginAccount'; } from '@/constants/loginAccount';
import { trimFormModel } from '@/utils/trimFormModel';
// //
const getParentStatusText = (status: string): string => { const getParentStatusText = (status: string): string => {
@ -507,6 +508,8 @@ const handleEdit = (record: Parent) => {
const handleModalOk = async () => { const handleModalOk = async () => {
try { try {
//
trimFormModel(formState as any);
await formRef.value?.validate(); await formRef.value?.validate();
submitting.value = true; submitting.value = true;

View File

@ -183,6 +183,7 @@ import {
type Teacher, type Teacher,
} from '@/api/school'; } from '@/api/school';
import { getLessonTypeName, getLessonTagStyle, translateGenericStatus, getGenericStatusStyle } from '@/utils/tagMaps'; import { getLessonTypeName, getLessonTagStyle, translateGenericStatus, getGenericStatusStyle } from '@/utils/tagMaps';
import { trimFormModel } from '@/utils/trimFormModel';
// //
const getScheduleStatusText = (status: string): string => { const getScheduleStatusText = (status: string): string => {
@ -369,6 +370,8 @@ const handleModalCancel = () => {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
//
trimFormModel(formState as any);
await formRef.value?.validate(); await formRef.value?.validate();
modalLoading.value = true; modalLoading.value = true;

View File

@ -345,6 +345,7 @@ import {
} from '@/api/school'; } from '@/api/school';
import type { Student, CreateStudentDto, ClassInfo, ImportResult, StudentClassHistory, TransferStudentDto } from '@/api/school'; import type { Student, CreateStudentDto, ClassInfo, ImportResult, StudentClassHistory, TransferStudentDto } from '@/api/school';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { trimFormModel } from '@/utils/trimFormModel';
const loading = ref(false); const loading = ref(false);
const classesLoading = ref(false); const classesLoading = ref(false);
@ -513,6 +514,8 @@ const handleEdit = (record: Student) => {
const handleModalOk = async () => { const handleModalOk = async () => {
try { try {
//
trimFormModel(formState as any);
await formRef.value?.validate(); await formRef.value?.validate();
const classId = formState.classId; const classId = formState.classId;
if (Number(classId || '-1') < 0) { if (Number(classId || '-1') < 0) {

View File

@ -239,6 +239,7 @@ import {
LOGIN_ACCOUNT_PATTERN_MESSAGE, LOGIN_ACCOUNT_PATTERN_MESSAGE,
LOGIN_ACCOUNT_PLACEHOLDER, LOGIN_ACCOUNT_PLACEHOLDER,
} from '@/constants/loginAccount'; } from '@/constants/loginAccount';
import { trimFormModel } from '@/utils/trimFormModel';
// //
const getTeacherStatusText = (status: string): string => { const getTeacherStatusText = (status: string): string => {
@ -390,6 +391,8 @@ const handleEdit = (record: Teacher) => {
const handleModalOk = async () => { const handleModalOk = async () => {
try { try {
//
trimFormModel(formState as any);
await formRef.value?.validate(); await formRef.value?.validate();
submitting.value = true; submitting.value = true;

View File

@ -245,6 +245,7 @@ import {
type CreateGrowthRecordDto, type CreateGrowthRecordDto,
type UpdateGrowthRecordDto, type UpdateGrowthRecordDto,
} from '@/api/growth'; } from '@/api/growth';
import { trimFormModel } from '@/utils/trimFormModel';
// //
const recordLoadingStates = ref<Record<number, boolean>>({}); const recordLoadingStates = ref<Record<number, boolean>>({});
@ -421,6 +422,8 @@ const handleDelete = async (id: number) => {
const handleModalOk = async () => { const handleModalOk = async () => {
try { try {
//
trimFormModel(formState as any);
await formRef.value?.validate(); await formRef.value?.validate();
submitting.value = true; submitting.value = true;

View File

@ -194,19 +194,19 @@ test.describe('租户管理', () => {
// 填写学校名称 // 填写学校名称
const nameInput = page.locator('input[placeholder*="学校名称"]'); const nameInput = page.locator('input[placeholder*="学校名称"]');
await nameInput.fill(TEST_DATA.tenant.name); await nameInput.fill(` ${TEST_DATA.tenant.name} `);
// 填写登录账号 // 填写登录账号
const accountInput = page.locator('input[placeholder*="登录账号"]'); const accountInput = page.locator('input[placeholder*="登录账号"]');
await accountInput.fill(TEST_DATA.tenant.account); await accountInput.fill(` ${TEST_DATA.tenant.account} `);
// 填写联系人 // 填写联系人
const contactInput = page.locator('input[placeholder*="联系人"]'); const contactInput = page.locator('input[placeholder*="联系人"]');
await contactInput.fill(TEST_DATA.tenant.contactPerson); await contactInput.fill(` ${TEST_DATA.tenant.contactPerson} `);
// 填写联系电话 // 填写联系电话
const phoneInput = page.locator('input[placeholder*="联系电话"]'); const phoneInput = page.locator('input[placeholder*="联系电话"]');
await phoneInput.fill(TEST_DATA.tenant.contactPhone); await phoneInput.fill(` ${TEST_DATA.tenant.contactPhone} `);
// 选择套餐类型 // 选择套餐类型
const packageSelect = page.locator('[placeholder*="套餐类型"]'); const packageSelect = page.locator('[placeholder*="套餐类型"]');

View File

@ -54,12 +54,12 @@ test.describe('学校端教师管理功能', () => {
const nameInput = page.locator('input[placeholder*="教师姓名"]').or(page.locator('input[formitemlabel*="姓名"]')); const nameInput = page.locator('input[placeholder*="教师姓名"]').or(page.locator('input[formitemlabel*="姓名"]'));
if (await nameInput.count() > 0) { if (await nameInput.count() > 0) {
await nameInput.first().fill(teacherName); await nameInput.first().fill(` ${teacherName} `);
} }
const accountInput = page.locator('input[placeholder*="账号"]').or(page.locator('input[formitemlabel*="账号"]')); const accountInput = page.locator('input[placeholder*="账号"]').or(page.locator('input[formitemlabel*="账号"]'));
if (await accountInput.count() > 0) { if (await accountInput.count() > 0) {
await accountInput.first().fill(teacherAccount); await accountInput.first().fill(` ${teacherAccount} `);
} }
const passwordInput = page.locator('input[placeholder*="密码"]').or(page.locator('input[type="password"]')); const passwordInput = page.locator('input[placeholder*="密码"]').or(page.locator('input[type="password"]'));

View File

@ -51,15 +51,21 @@ test.describe('学校端家长管理功能', () => {
// 4. 填写家长信息 // 4. 填写家长信息
const parentName = `测试家长_${Date.now()}`; const parentName = `测试家长_${Date.now()}`;
const parentPhone = `138${Date.now().toString().substring(5)}`; const parentPhone = `138${Date.now().toString().substring(5)}`;
const parentAccount = `parent_test_${Date.now()}`;
const nameInput = page.locator('input[placeholder*="家长姓名"]').or(page.locator('input[formitemlabel*="姓名"]')); const nameInput = page.locator('input[placeholder*="家长姓名"]').or(page.locator('input[formitemlabel*="姓名"]'));
if (await nameInput.count() > 0) { if (await nameInput.count() > 0) {
await nameInput.first().fill(parentName); await nameInput.first().fill(` ${parentName} `);
} }
const phoneInput = page.locator('input[placeholder*="手机号"]').or(page.locator('input[formitemlabel*="手机"]')); const phoneInput = page.locator('input[placeholder*="手机号"]').or(page.locator('input[formitemlabel*="手机"]'));
if (await phoneInput.count() > 0) { if (await phoneInput.count() > 0) {
await phoneInput.first().fill(parentPhone); await phoneInput.first().fill(` ${parentPhone} `);
}
const accountInput = page.locator('input[placeholder*="账号"]').or(page.locator('input[formitemlabel*="账号"]'));
if (await accountInput.count() > 0) {
await accountInput.first().fill(` ${parentAccount} `);
} }
// 5. 点击确定按钮 // 5. 点击确定按钮