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 { uploadFile, getFileUrl } from '@/api/file';
|
||||
import type { Theme } from '@/api/theme';
|
||||
import { trimFormModel } from '@/utils/trimFormModel';
|
||||
|
||||
interface BasicInfoData {
|
||||
name: string;
|
||||
@ -316,6 +317,10 @@ const handleChange = () => {
|
||||
// 验证
|
||||
const validate = async () => {
|
||||
try {
|
||||
// 提交前统一去除首尾空格(仅处理 plain object/array 内字符串)
|
||||
trimFormModel(formData as any);
|
||||
// 同步到父组件 v-model,避免只改了子组件内部状态导致提交仍带空格
|
||||
handleChange();
|
||||
await formRef.value?.validate();
|
||||
|
||||
// 校验课程封面
|
||||
|
||||
@ -204,6 +204,7 @@ import type { FormInstance } from 'ant-design-vue';
|
||||
import FileUploader from './FileUploader.vue';
|
||||
import LessonStepsEditor from './LessonStepsEditor.vue';
|
||||
import type { StepData } from './LessonStepsEditor.vue';
|
||||
import { trimFormModel } from '@/utils/trimFormModel';
|
||||
|
||||
export interface LessonData {
|
||||
id?: number | string;
|
||||
@ -360,6 +361,10 @@ const handleChange = () => {
|
||||
// 表单校验(供 Step4/5/6 调用)
|
||||
const validate = async () => {
|
||||
try {
|
||||
// 提交前统一去除首尾空格(仅处理 plain object/array 内字符串)
|
||||
trimFormModel(lessonData as any);
|
||||
// 同步到父组件 v-model,避免提交仍带空格
|
||||
handleChange();
|
||||
await formRef.value?.validate();
|
||||
return { valid: true, errors: [] as string[] };
|
||||
} 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_PLACEHOLDER,
|
||||
} from '@/constants/loginAccount';
|
||||
import { trimFormModel } from '@/utils/trimFormModel';
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
@ -570,6 +571,8 @@ const handleEdit = (record: Tenant) => {
|
||||
// 弹窗确认
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
// 提交前先移除表单字符串字段首尾空格,避免仅有空格导致的误校验
|
||||
trimFormModel(formData as any);
|
||||
await formRef.value?.validate();
|
||||
} catch {
|
||||
return;
|
||||
|
||||
@ -324,6 +324,7 @@ import {
|
||||
removeClassTeacher,
|
||||
} from '@/api/school';
|
||||
import type { ClassInfo, CreateClassDto, Teacher, Student, ClassTeacher, AddClassTeacherDto } from '@/api/school';
|
||||
import { trimFormModel } from '@/utils/trimFormModel';
|
||||
|
||||
// Custom gender icons as components
|
||||
const BoyOutlined = {
|
||||
@ -511,6 +512,8 @@ const getPrimaryTeacherId = (teachers?: ClassTeacher[]): number | undefined => {
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
// 提交前移除字符串字段首尾空格,避免空格导致的误校验
|
||||
trimFormModel(formState as any);
|
||||
await formRef.value?.validate();
|
||||
submitting.value = true;
|
||||
|
||||
|
||||
@ -274,6 +274,7 @@ import {
|
||||
import { message } from 'ant-design-vue';
|
||||
import type { FormInstance } from 'ant-design-vue';
|
||||
import { fileApi, validateFileType } from '@/api/file';
|
||||
import { trimFormModel } from '@/utils/trimFormModel';
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
@ -395,6 +396,8 @@ const handleDelete = async (id: number) => {
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
// 提交前移除字符串字段首尾空格,避免空格导致的误校验
|
||||
trimFormModel(formState as any);
|
||||
await formRef.value?.validate();
|
||||
submitting.value = true;
|
||||
message.success(isEdit.value ? '更新成功' : '添加成功');
|
||||
|
||||
@ -347,6 +347,7 @@ import {
|
||||
LOGIN_ACCOUNT_PATTERN_MESSAGE,
|
||||
LOGIN_ACCOUNT_PLACEHOLDER,
|
||||
} from '@/constants/loginAccount';
|
||||
import { trimFormModel } from '@/utils/trimFormModel';
|
||||
|
||||
// 状态辅助函数
|
||||
const getParentStatusText = (status: string): string => {
|
||||
@ -507,6 +508,8 @@ const handleEdit = (record: Parent) => {
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
// 提交前先移除表单字符串字段首尾空格,避免空格导致的校验失败
|
||||
trimFormModel(formState as any);
|
||||
await formRef.value?.validate();
|
||||
submitting.value = true;
|
||||
|
||||
|
||||
@ -183,6 +183,7 @@ import {
|
||||
type Teacher,
|
||||
} from '@/api/school';
|
||||
import { getLessonTypeName, getLessonTagStyle, translateGenericStatus, getGenericStatusStyle } from '@/utils/tagMaps';
|
||||
import { trimFormModel } from '@/utils/trimFormModel';
|
||||
|
||||
// 状态辅助函数
|
||||
const getScheduleStatusText = (status: string): string => {
|
||||
@ -369,6 +370,8 @@ const handleModalCancel = () => {
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
// 提交前移除字符串字段首尾空格,避免空格导致的误校验
|
||||
trimFormModel(formState as any);
|
||||
await formRef.value?.validate();
|
||||
modalLoading.value = true;
|
||||
|
||||
|
||||
@ -345,6 +345,7 @@ import {
|
||||
} from '@/api/school';
|
||||
import type { Student, CreateStudentDto, ClassInfo, ImportResult, StudentClassHistory, TransferStudentDto } from '@/api/school';
|
||||
import dayjs from 'dayjs';
|
||||
import { trimFormModel } from '@/utils/trimFormModel';
|
||||
|
||||
const loading = ref(false);
|
||||
const classesLoading = ref(false);
|
||||
@ -513,6 +514,8 @@ const handleEdit = (record: Student) => {
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
// 提交前移除字符串字段首尾空格,避免空格导致的校验失败
|
||||
trimFormModel(formState as any);
|
||||
await formRef.value?.validate();
|
||||
const classId = formState.classId;
|
||||
if (Number(classId || '-1') < 0) {
|
||||
|
||||
@ -239,6 +239,7 @@ import {
|
||||
LOGIN_ACCOUNT_PATTERN_MESSAGE,
|
||||
LOGIN_ACCOUNT_PLACEHOLDER,
|
||||
} from '@/constants/loginAccount';
|
||||
import { trimFormModel } from '@/utils/trimFormModel';
|
||||
|
||||
// 状态辅助函数
|
||||
const getTeacherStatusText = (status: string): string => {
|
||||
@ -390,6 +391,8 @@ const handleEdit = (record: Teacher) => {
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
// 提交前先移除表单字符串字段首尾空格,避免空格导致的校验失败
|
||||
trimFormModel(formState as any);
|
||||
await formRef.value?.validate();
|
||||
submitting.value = true;
|
||||
|
||||
|
||||
@ -245,6 +245,7 @@ import {
|
||||
type CreateGrowthRecordDto,
|
||||
type UpdateGrowthRecordDto,
|
||||
} from '@/api/growth';
|
||||
import { trimFormModel } from '@/utils/trimFormModel';
|
||||
|
||||
// 图片加载状态管理
|
||||
const recordLoadingStates = ref<Record<number, boolean>>({});
|
||||
@ -421,6 +422,8 @@ const handleDelete = async (id: number) => {
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
// 提交前移除字符串字段首尾空格,避免空格导致的误校验
|
||||
trimFormModel(formState as any);
|
||||
await formRef.value?.validate();
|
||||
submitting.value = true;
|
||||
|
||||
|
||||
@ -194,19 +194,19 @@ test.describe('租户管理', () => {
|
||||
|
||||
// 填写学校名称
|
||||
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*="登录账号"]');
|
||||
await accountInput.fill(TEST_DATA.tenant.account);
|
||||
await accountInput.fill(` ${TEST_DATA.tenant.account} `);
|
||||
|
||||
// 填写联系人
|
||||
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*="联系电话"]');
|
||||
await phoneInput.fill(TEST_DATA.tenant.contactPhone);
|
||||
await phoneInput.fill(` ${TEST_DATA.tenant.contactPhone} `);
|
||||
|
||||
// 选择套餐类型
|
||||
const packageSelect = page.locator('[placeholder*="套餐类型"]');
|
||||
|
||||
@ -54,12 +54,12 @@ test.describe('学校端教师管理功能', () => {
|
||||
|
||||
const nameInput = page.locator('input[placeholder*="教师姓名"]').or(page.locator('input[formitemlabel*="姓名"]'));
|
||||
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*="账号"]'));
|
||||
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"]'));
|
||||
|
||||
@ -51,15 +51,21 @@ test.describe('学校端家长管理功能', () => {
|
||||
// 4. 填写家长信息
|
||||
const parentName = `测试家长_${Date.now()}`;
|
||||
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*="姓名"]'));
|
||||
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*="手机"]'));
|
||||
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. 点击确定按钮
|
||||
|
||||
Loading…
Reference in New Issue
Block a user