diff --git a/reading-platform-frontend/src/components/course-edit/Step1BasicInfo.vue b/reading-platform-frontend/src/components/course-edit/Step1BasicInfo.vue index f346e1f..f32cd83 100644 --- a/reading-platform-frontend/src/components/course-edit/Step1BasicInfo.vue +++ b/reading-platform-frontend/src/components/course-edit/Step1BasicInfo.vue @@ -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(); // 校验课程封面 diff --git a/reading-platform-frontend/src/components/course/LessonConfigPanel.vue b/reading-platform-frontend/src/components/course/LessonConfigPanel.vue index 96543b3..c428704 100644 --- a/reading-platform-frontend/src/components/course/LessonConfigPanel.vue +++ b/reading-platform-frontend/src/components/course/LessonConfigPanel.vue @@ -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) { diff --git a/reading-platform-frontend/src/utils/trimFormModel.ts b/reading-platform-frontend/src/utils/trimFormModel.ts new file mode 100644 index 0000000..555381a --- /dev/null +++ b/reading-platform-frontend/src/utils/trimFormModel.ts @@ -0,0 +1,47 @@ +/** + * 深度去除对象/数组中所有字符串的前后空格,并尽量避免破坏非“普通对象”的结构(例如 Dayjs 实例)。 + * + * 该函数会“就地修改”传入的 model(适配 ant-design-vue 的表单 v-model/validate 习惯)。 + */ + +type AnyRecord = Record; + +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): 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(model: T): T { + // 普通对象/数组一般不会包含循环引用,但这里做个保护 + const seen = new WeakSet(); + return walk(model, seen) as T; +} + diff --git a/reading-platform-frontend/src/views/admin/tenants/TenantListView.vue b/reading-platform-frontend/src/views/admin/tenants/TenantListView.vue index 0a87894..2a69f6d 100644 --- a/reading-platform-frontend/src/views/admin/tenants/TenantListView.vue +++ b/reading-platform-frontend/src/views/admin/tenants/TenantListView.vue @@ -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; diff --git a/reading-platform-frontend/src/views/school/classes/ClassListView.vue b/reading-platform-frontend/src/views/school/classes/ClassListView.vue index 2f9d1dd..1fdb370 100644 --- a/reading-platform-frontend/src/views/school/classes/ClassListView.vue +++ b/reading-platform-frontend/src/views/school/classes/ClassListView.vue @@ -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; diff --git a/reading-platform-frontend/src/views/school/growth/GrowthRecordView.vue b/reading-platform-frontend/src/views/school/growth/GrowthRecordView.vue index 5e78c8f..2cbbf9e 100644 --- a/reading-platform-frontend/src/views/school/growth/GrowthRecordView.vue +++ b/reading-platform-frontend/src/views/school/growth/GrowthRecordView.vue @@ -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 ? '更新成功' : '添加成功'); diff --git a/reading-platform-frontend/src/views/school/parents/ParentListView.vue b/reading-platform-frontend/src/views/school/parents/ParentListView.vue index 242c5b2..48e2c41 100644 --- a/reading-platform-frontend/src/views/school/parents/ParentListView.vue +++ b/reading-platform-frontend/src/views/school/parents/ParentListView.vue @@ -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; diff --git a/reading-platform-frontend/src/views/school/schedule/ScheduleList.vue b/reading-platform-frontend/src/views/school/schedule/ScheduleList.vue index 6c0914a..03f809b 100644 --- a/reading-platform-frontend/src/views/school/schedule/ScheduleList.vue +++ b/reading-platform-frontend/src/views/school/schedule/ScheduleList.vue @@ -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; diff --git a/reading-platform-frontend/src/views/school/students/StudentListView.vue b/reading-platform-frontend/src/views/school/students/StudentListView.vue index 3ff003c..806f98f 100644 --- a/reading-platform-frontend/src/views/school/students/StudentListView.vue +++ b/reading-platform-frontend/src/views/school/students/StudentListView.vue @@ -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) { diff --git a/reading-platform-frontend/src/views/school/teachers/TeacherListView.vue b/reading-platform-frontend/src/views/school/teachers/TeacherListView.vue index 25b68da..006e7d1 100644 --- a/reading-platform-frontend/src/views/school/teachers/TeacherListView.vue +++ b/reading-platform-frontend/src/views/school/teachers/TeacherListView.vue @@ -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; diff --git a/reading-platform-frontend/src/views/teacher/growth/GrowthRecordView.vue b/reading-platform-frontend/src/views/teacher/growth/GrowthRecordView.vue index b987aa1..c6f3dee 100644 --- a/reading-platform-frontend/src/views/teacher/growth/GrowthRecordView.vue +++ b/reading-platform-frontend/src/views/teacher/growth/GrowthRecordView.vue @@ -245,6 +245,7 @@ import { type CreateGrowthRecordDto, type UpdateGrowthRecordDto, } from '@/api/growth'; +import { trimFormModel } from '@/utils/trimFormModel'; // 图片加载状态管理 const recordLoadingStates = ref>({}); @@ -421,6 +422,8 @@ const handleDelete = async (id: number) => { const handleModalOk = async () => { try { + // 提交前移除字符串字段首尾空格,避免空格导致的误校验 + trimFormModel(formState as any); await formRef.value?.validate(); submitting.value = true; diff --git a/reading-platform-frontend/tests/e2e/admin/06-tenants.spec.ts b/reading-platform-frontend/tests/e2e/admin/06-tenants.spec.ts index 5508d71..31e39c0 100644 --- a/reading-platform-frontend/tests/e2e/admin/06-tenants.spec.ts +++ b/reading-platform-frontend/tests/e2e/admin/06-tenants.spec.ts @@ -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*="套餐类型"]'); diff --git a/reading-platform-frontend/tests/e2e/school/05-teachers.spec.ts b/reading-platform-frontend/tests/e2e/school/05-teachers.spec.ts index 6996630..fe2543d 100644 --- a/reading-platform-frontend/tests/e2e/school/05-teachers.spec.ts +++ b/reading-platform-frontend/tests/e2e/school/05-teachers.spec.ts @@ -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"]')); diff --git a/reading-platform-frontend/tests/e2e/school/06-parents.spec.ts b/reading-platform-frontend/tests/e2e/school/06-parents.spec.ts index 2350dbd..19c6a72 100644 --- a/reading-platform-frontend/tests/e2e/school/06-parents.spec.ts +++ b/reading-platform-frontend/tests/e2e/school/06-parents.spec.ts @@ -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. 点击确定按钮