fix(前端): 表单提交前去除首尾空格并统一校验
Made-with: Cursor
This commit is contained in:
parent
9d76e178de
commit
5f55aadb75
@ -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();
|
||||||
|
|
||||||
// 校验课程封面
|
// 校验课程封面
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
47
reading-platform-frontend/src/utils/trimFormModel.ts
Normal file
47
reading-platform-frontend/src/utils/trimFormModel.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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 ? '更新成功' : '添加成功');
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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*="套餐类型"]');
|
||||||
|
|||||||
@ -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"]'));
|
||||||
|
|||||||
@ -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. 点击确定按钮
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user