diff --git a/src/components/admin/FormSchemaVisualEditor.vue b/src/components/admin/FormSchemaVisualEditor.vue index a81eac0..b47470c 100644 --- a/src/components/admin/FormSchemaVisualEditor.vue +++ b/src/components/admin/FormSchemaVisualEditor.vue @@ -45,6 +45,8 @@ function onTypeChange(element: FormSchemaEditorItem) { if (element.type === SIGNUP_COMMITMENT_TYPE) { element.key = 'commitment_accepted' element.options = [] + element.requiredWhenField = '' + element.requiredWhenValuesLines = '' if (!element.label.trim()) { element.label = '本人已阅读并同意《参赛承诺书》的全部内容' } @@ -134,6 +136,23 @@ function applyOptionsText(element: FormSchemaEditorItem, raw: string) { @input="(v: string) => applyOptionsText(element, v)" /> + +
条件必填(可选)
+

+ 当下方「依赖字段」取值为列表中任一值时,本字段视为必填(示例:企业名称依赖「参赛组别」为「创业组」)。 +

+ + +
复制 @@ -169,6 +188,13 @@ function applyOptionsText(element: FormSchemaEditorItem, raw: string) { color: var(--el-text-color-secondary); } +.cond-hint { + margin: 0 0 8px; + font-size: 12px; + color: var(--el-text-color-secondary); + line-height: 1.5; +} + .schema-draggable-list { display: flex; flex-direction: column; diff --git a/src/utils/defaultSignupFormSchema.ts b/src/utils/defaultSignupFormSchema.ts index d7d1d75..2db9c7b 100644 --- a/src/utils/defaultSignupFormSchema.ts +++ b/src/utils/defaultSignupFormSchema.ts @@ -10,6 +10,8 @@ export interface SignupFormSchemaField { placeholder?: string help?: string options?: { label: string; value: string }[] + /** 当依赖字段取值命中列表时,本字段视为必填(与企业名称依赖参赛组别一致) */ + required_when?: { field: string; values: string[] } } /** @@ -39,12 +41,24 @@ export const DEFAULT_SIGNUP_FORM_SCHEMA: SignupFormSchemaField[] = [ required: true, placeholder: '11 位中国大陆手机号', }, + { + key: 'entry_group', + type: 'select', + label: '参赛组别', + required: true, + options: [ + { label: '创新组', value: '创新组' }, + { label: '创业组', value: '创业组' }, + ], + help: '选择「创业组」时须填写企业名称', + }, { key: 'company_name', type: 'text', label: '企业名称', required: false, - placeholder: '如已注册企业,请填写', + required_when: { field: 'entry_group', values: ['创业组'] }, + placeholder: '创业组为必填;创新组选填', }, { key: 'project_name', type: 'text', label: '项目名称', required: true }, { @@ -142,6 +156,19 @@ export function normalizeSignupSchema(raw: unknown): SignupFormSchemaField[] { const helpRaw = o.help const help = helpRaw != null && String(helpRaw).trim() !== '' ? String(helpRaw).trim() : undefined + let required_when: { field: string; values: string[] } | undefined + const rw = o.required_when + if (rw != null && typeof rw === 'object' && !Array.isArray(rw)) { + const rwo = rw as Record + const wf = String(rwo.field ?? '').trim() + const valsRaw = rwo.values + const values = Array.isArray(valsRaw) + ? valsRaw.map((x) => String(x).trim()).filter((s) => s !== '') + : [] + if (wf && values.length) { + required_when = { field: wf, values } + } + } let options: { label: string; value: string }[] | undefined if (type === 'select' && Array.isArray(o.options)) { options = parseSchemaOptionsArray(o.options) @@ -161,7 +188,7 @@ export function normalizeSignupSchema(raw: unknown): SignupFormSchemaField[] { } } } - out.push({ key, type, label, required, placeholder, help, options }) + out.push({ key, type, label, required, placeholder, help, options, required_when }) } return out.length ? out : DEFAULT_SIGNUP_FORM_SCHEMA.map((f) => ({ ...f })) } diff --git a/src/utils/formSchemaEditor.ts b/src/utils/formSchemaEditor.ts index da73f05..ea164e8 100644 --- a/src/utils/formSchemaEditor.ts +++ b/src/utils/formSchemaEditor.ts @@ -13,6 +13,10 @@ export interface FormSchemaEditorItem { help?: string /** select 等:{ label, value } */ options?: { label: string; value: string }[] + /** 报名表:条件必填依赖字段 key(入库为 required_when.field) */ + requiredWhenField?: string + /** 报名表:依赖取值,每行一个(入库为 required_when.values) */ + requiredWhenValuesLines?: string } /** 入库时为 checkbox + key commitment_accepted,仅供报名表可视化 */ @@ -55,6 +59,8 @@ export function createEmptySchemaItem( placeholder: '', help: '', options: [], + requiredWhenField: '', + requiredWhenValuesLines: '', } } @@ -85,6 +91,19 @@ export function schemaJsonToEditorItems(json: unknown, purpose: FormSchemaPurpos purpose === 'signup' && rawKey === 'commitment_accepted' && rawType === 'checkbox' ? SIGNUP_COMMITMENT_TYPE : rawType + let requiredWhenField = '' + let requiredWhenValuesLines = '' + if (purpose === 'signup') { + const rw = o.required_when + if (rw != null && typeof rw === 'object' && !Array.isArray(rw)) { + const rwo = rw as Record + requiredWhenField = String(rwo.field ?? '').trim() + const vals = rwo.values + if (Array.isArray(vals)) { + requiredWhenValuesLines = vals.map((x) => String(x).trim()).filter(Boolean).join('\n') + } + } + } return { __uid: newUid(), key: rawKey, @@ -94,12 +113,14 @@ export function schemaJsonToEditorItems(json: unknown, purpose: FormSchemaPurpos placeholder: o.placeholder != null ? String(o.placeholder) : '', help: o.help != null ? String(o.help) : '', options: type === 'select' ? normalizeOptions(o.options) : [], + requiredWhenField, + requiredWhenValuesLines, } }) } /** 写入接口的 schema_json(数组) */ -export function editorItemsToSchemaJson(items: FormSchemaEditorItem[]): unknown[] { +export function editorItemsToSchemaJson(items: FormSchemaEditorItem[], purpose: FormSchemaPurpose): unknown[] { return items.map((item) => { const isCommitment = item.type === SIGNUP_COMMITMENT_TYPE const row: Record = { @@ -113,11 +134,25 @@ export function editorItemsToSchemaJson(items: FormSchemaEditorItem[]): unknown[ if (item.type === 'select' && item.options?.length) { row.options = item.options.filter((o) => o.value !== '' || o.label !== '') } + if ( + purpose === 'signup' + && !isCommitment + && item.requiredWhenField?.trim() + && item.requiredWhenValuesLines?.trim() + ) { + const values = item.requiredWhenValuesLines + .split('\n') + .map((s) => s.trim()) + .filter(Boolean) + if (values.length) { + row.required_when = { field: item.requiredWhenField.trim(), values } + } + } return row }) } -export function validateEditorItems(items: FormSchemaEditorItem[]): string | null { +export function validateEditorItems(items: FormSchemaEditorItem[], purpose: FormSchemaPurpose): string | null { const keys = new Set() for (const it of items) { const k = it.key.trim() @@ -125,6 +160,14 @@ export function validateEditorItems(items: FormSchemaEditorItem[]): string | nul if (keys.has(k)) return `字段 key「${k}」重复` keys.add(k) if (!it.label.trim()) return `字段「${k}」缺少显示标签` + if ( + purpose === 'signup' + && it.type !== SIGNUP_COMMITMENT_TYPE + && it.requiredWhenField?.trim() + && !it.requiredWhenValuesLines?.trim() + ) { + return `字段「${k}」填写了条件必填依赖,但未填写「当取值」列表` + } } return null } diff --git a/src/views/ApplyFormView.vue b/src/views/ApplyFormView.vue index 6ed7979..33f7133 100644 --- a/src/views/ApplyFormView.vue +++ b/src/views/ApplyFormView.vue @@ -271,6 +271,11 @@ function effectiveRequired(field: SignupFormSchemaField): boolean { if (field.key === 'oversea_country') { return isOversea.value } + const rw = field.required_when + if (rw?.field && rw.values?.length) { + const dep = String(formModel[rw.field] ?? '').trim() + if (rw.values.includes(dep)) return true + } return field.required } @@ -842,6 +847,7 @@ function applyServerPayload(d: { degree?: string contact_email?: string contact_mobile?: string + entry_group?: string company_name?: string project_name?: string track?: string @@ -861,6 +867,7 @@ function applyServerPayload(d: { formModel.degree = d.degree || '' formModel.contact_email = d.contact_email || '' formModel.contact_mobile = d.contact_mobile || '' + formModel.entry_group = d.entry_group || '' formModel.company_name = d.company_name || '' formModel.project_name = d.project_name || '' formModel.track = d.track || '' diff --git a/src/views/admin/competition/CompetitionFormView.vue b/src/views/admin/competition/CompetitionFormView.vue index 2cc2f03..c6398e0 100644 --- a/src/views/admin/competition/CompetitionFormView.vue +++ b/src/views/admin/competition/CompetitionFormView.vue @@ -168,7 +168,11 @@ watch(editSchemaTab, (t) => { }) function syncEditVisualToJson() { - editSchemaJsonText.value = JSON.stringify(editorItemsToSchemaJson(editSchemaVisualItems.value), null, 2) + editSchemaJsonText.value = JSON.stringify( + editorItemsToSchemaJson(editSchemaVisualItems.value, editSchemaPurpose.value), + null, + 2, + ) } function syncEditJsonToVisual() { @@ -646,12 +650,12 @@ async function saveEditedSchema() { editSchemaError.value = '' let parsed: unknown[] = [] if (editSchemaTab.value === 'visual') { - const err = validateEditorItems(editSchemaVisualItems.value) + const err = validateEditorItems(editSchemaVisualItems.value, editSchemaPurpose.value) if (err) { editSchemaError.value = err return } - parsed = editorItemsToSchemaJson(editSchemaVisualItems.value) as unknown[] + parsed = editorItemsToSchemaJson(editSchemaVisualItems.value, editSchemaPurpose.value) as unknown[] } else { try { const raw = JSON.parse(editSchemaJsonText.value) diff --git a/src/views/reviewer/ReviewerApplicationDetailView.vue b/src/views/reviewer/ReviewerApplicationDetailView.vue index 6235420..1b480a3 100644 --- a/src/views/reviewer/ReviewerApplicationDetailView.vue +++ b/src/views/reviewer/ReviewerApplicationDetailView.vue @@ -40,6 +40,7 @@ interface DetailPayload { degree: string contact_email: string contact_mobile: string + entry_group: string company_name: string track_title: string track_code: string @@ -375,6 +376,10 @@ async function submitReviewScore() { +
+ + +