From e42d521a4252e768b8df7589b261d74005f01f87 Mon Sep 17 00:00:00 2001 From: lion <120344285@qq.com> Date: Mon, 13 Jul 2026 12:03:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A8=E8=8D=90=E6=96=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/admin/formSchemas.ts | 22 +++- src/api/admin/types.ts | 11 ++ src/utils/defaultSignupFormSchema.ts | 7 ++ src/utils/formSchemaEditor.ts | 16 ++- src/utils/signupApplicationFields.ts | 112 ++++++++++++++++++ .../admin/competition/CompetitionFormView.vue | 13 +- 6 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 src/utils/signupApplicationFields.ts diff --git a/src/api/admin/formSchemas.ts b/src/api/admin/formSchemas.ts index 292bd8b..0ce1388 100644 --- a/src/api/admin/formSchemas.ts +++ b/src/api/admin/formSchemas.ts @@ -1,12 +1,26 @@ import { adminHttp } from './http' -import type { FormSchemaPayload, FormSchemaRow } from './types' +import type { FormSchemaPayload, FormSchemaRow, SignupFieldRegistryManifest } from './types' -export async function listFormSchemas(competitionId: number, purpose?: 'signup' | 'review'): Promise { +export interface FormSchemaListResult { + data: FormSchemaRow[] + signupFieldRegistry?: SignupFieldRegistryManifest +} + +export async function listFormSchemas( + competitionId: number, + purpose?: 'signup' | 'review', +): Promise { const { data } = await adminHttp.get(`/competitions/${competitionId}/form-schemas`, { params: purpose ? { purpose } : {}, }) - const body = data as { data?: FormSchemaRow[] } - return body.data ?? [] + const body = data as { + data?: FormSchemaRow[] + meta?: { signup_field_registry?: SignupFieldRegistryManifest } + } + return { + data: body.data ?? [], + signupFieldRegistry: body.meta?.signup_field_registry, + } } export async function getFormSchema(competitionId: number, schemaId: number): Promise { diff --git a/src/api/admin/types.ts b/src/api/admin/types.ts index ee4933b..1b3a238 100644 --- a/src/api/admin/types.ts +++ b/src/api/admin/types.ts @@ -64,10 +64,21 @@ export interface FormSchemaRow { /** 列表接口返回的字段标签预览(不含完整 schema_json) */ field_labels?: string[] schema_json?: unknown + signup_field_registry?: SignupFieldRegistryManifest created_at?: string updated_at?: string } +/** 与后端 SignupApplicationFieldRegistry::manifest() 一致 */ +export interface SignupFieldRegistryManifest { + scalar_field_keys: string[] + file_field_keys: string[] + commitment_field_key: string + scalar_type_hints: Record + generic_scalar_types: string[] + all_supported_form_keys: string[] +} + export interface FormSchemaPayload { purpose: 'signup' | 'review' name: string diff --git a/src/utils/defaultSignupFormSchema.ts b/src/utils/defaultSignupFormSchema.ts index c998d69..fa80af5 100644 --- a/src/utils/defaultSignupFormSchema.ts +++ b/src/utils/defaultSignupFormSchema.ts @@ -116,6 +116,13 @@ export const DEFAULT_SIGNUP_FORM_SCHEMA: SignupFormSchemaField[] = [ required: false, placeholder: '约200字左右', }, + { + key: 'recommend', + type: 'text', + label: '推荐方', + required: false, + placeholder: '如有推荐单位或推荐人可填写', + }, { key: 'commitment_accepted', type: 'checkbox', diff --git a/src/utils/formSchemaEditor.ts b/src/utils/formSchemaEditor.ts index 7d80753..3fe652e 100644 --- a/src/utils/formSchemaEditor.ts +++ b/src/utils/formSchemaEditor.ts @@ -27,8 +27,12 @@ export interface FormSchemaEditorItem { fileMaxCount?: string } -/** 入库时为 checkbox + key commitment_accepted,仅供报名表可视化 */ -export const SIGNUP_COMMITMENT_TYPE = 'commitment_promise' as const +import { + SIGNUP_COMMITMENT_TYPE, + validateSignupSchemaFieldKey, +} from './signupApplicationFields' + +export { SIGNUP_COMMITMENT_TYPE } from './signupApplicationFields' export const SIGNUP_FIELD_TYPES: { value: string; label: string }[] = [ { value: 'text', label: '单行文本' }, @@ -221,6 +225,14 @@ export function validateEditorItems(items: FormSchemaEditorItem[], purpose: Form if (keys.has(k)) return `字段 key「${k}」重复` keys.add(k) if (!it.label.trim()) return `字段「${k}」缺少显示标签` + if (purpose === 'signup') { + const typeForValidate = it.type === SIGNUP_COMMITMENT_TYPE ? 'checkbox' : it.type + const keyErr = validateSignupSchemaFieldKey( + it.type === SIGNUP_COMMITMENT_TYPE ? 'commitment_accepted' : k, + typeForValidate, + ) + if (keyErr) return keyErr + } if ( purpose === 'signup' && it.type !== SIGNUP_COMMITMENT_TYPE diff --git a/src/utils/signupApplicationFields.ts b/src/utils/signupApplicationFields.ts new file mode 100644 index 0000000..b297a70 --- /dev/null +++ b/src/utils/signupApplicationFields.ts @@ -0,0 +1,112 @@ +/** + * 报名表 schema 字段注册表(运行时由管理端 API 同步,与 Application::$fillable 对齐)。 + */ + +export const SIGNUP_COMMITMENT_FIELD_KEY = 'commitment_accepted' as const +export const SIGNUP_COMMITMENT_TYPE = 'commitment_promise' as const + +export const SIGNUP_FILE_FIELD_KEYS = ['plan', 'supporting'] as const + +/** 接口 meta.signup_field_registry / signup_field_registry 结构 */ +export interface SignupFieldRegistryManifest { + scalar_field_keys: string[] + file_field_keys: string[] + commitment_field_key: string + scalar_type_hints: Record + generic_scalar_types: string[] + all_supported_form_keys: string[] +} + +/** 未拉取接口前的兜底(与当前默认 fillable 一致) */ +const FALLBACK_MANIFEST: SignupFieldRegistryManifest = { + scalar_field_keys: [ + 'player_name', + 'school', + 'degree', + 'contact_email', + 'contact_mobile', + 'entry_group', + 'company_name', + 'project_name', + 'track', + 'location_country', + 'location_province', + 'location_city', + 'oversea_country', + 'intro', + 'recommend', + ], + file_field_keys: [...SIGNUP_FILE_FIELD_KEYS], + commitment_field_key: SIGNUP_COMMITMENT_FIELD_KEY, + scalar_type_hints: { + player_name: ['text'], + school: ['text'], + degree: ['select'], + contact_email: ['email', 'text'], + contact_mobile: ['tel', 'text'], + entry_group: ['select'], + company_name: ['text'], + project_name: ['text'], + track: ['select'], + location_country: ['select'], + location_province: ['select', 'text'], + location_city: ['select', 'text'], + oversea_country: ['text'], + intro: ['textarea', 'text'], + recommend: ['text', 'textarea'], + }, + generic_scalar_types: ['text', 'textarea', 'email', 'tel', 'select', 'number', 'date'], + all_supported_form_keys: [], +} + +FALLBACK_MANIFEST.all_supported_form_keys = [ + ...FALLBACK_MANIFEST.scalar_field_keys, + ...FALLBACK_MANIFEST.file_field_keys, + FALLBACK_MANIFEST.commitment_field_key, +] + +let activeManifest: SignupFieldRegistryManifest = FALLBACK_MANIFEST + +export function setSignupFieldRegistry(manifest: SignupFieldRegistryManifest | null | undefined): void { + if (manifest && Array.isArray(manifest.scalar_field_keys)) { + activeManifest = manifest + } +} + +export function getSignupFieldRegistry(): SignupFieldRegistryManifest { + return activeManifest +} + +function allowedTypesForKey(key: string): string[] { + const m = activeManifest + return m.scalar_type_hints[key] ?? m.generic_scalar_types +} + +export function validateSignupSchemaFieldKey(key: string, type: string): string | null { + const k = key.trim() + const t = type.trim() + if (!k) return '存在未填写「字段 key」的项(须与 applications 表列名一致)' + + const m = activeManifest + + if (k === m.commitment_field_key) { + return t === 'checkbox' || t === SIGNUP_COMMITMENT_TYPE + ? null + : '参赛承诺书字段须为 checkbox 类型' + } + + if (m.file_field_keys.includes(k)) { + return t === 'file' ? null : `附件字段「${k}」须为 file 类型` + } + + if (!m.scalar_field_keys.includes(k)) { + return `字段 key「${k}」不是 applications 表可填列,请使用:${m.all_supported_form_keys.join('、')}(新增列请先做数据库迁移并加入 Application.$fillable)` + } + + const allowed = allowedTypesForKey(k) + if (!t || !allowed.includes(t)) { + return `字段「${k}」类型须为:${allowed.join(' / ')}` + } + + return null +} diff --git a/src/views/admin/competition/CompetitionFormView.vue b/src/views/admin/competition/CompetitionFormView.vue index 7cb5f86..661acc2 100644 --- a/src/views/admin/competition/CompetitionFormView.vue +++ b/src/views/admin/competition/CompetitionFormView.vue @@ -14,6 +14,7 @@ import { updateTrack, } from '../../../api/admin/competitions' import * as formSchemaApi from '../../../api/admin/formSchemas' +import { setSignupFieldRegistry } from '../../../utils/signupApplicationFields' import { createSignupChannel, getSignupChannel, @@ -341,7 +342,11 @@ async function refreshSignupSchemas() { if (!cid) return schemaBusy.value = true try { - signupSchemas.value = await formSchemaApi.listFormSchemas(cid, 'signup') + const signupRes = await formSchemaApi.listFormSchemas(cid, 'signup') + signupSchemas.value = signupRes.data + if (signupRes.signupFieldRegistry) { + setSignupFieldRegistry(signupRes.signupFieldRegistry) + } } catch (e) { ElMessage.error(e instanceof Error ? e.message : '加载报名表列表失败') } finally { @@ -354,7 +359,8 @@ async function refreshReviewSchemas() { if (!cid) return schemaBusy.value = true try { - reviewSchemas.value = await formSchemaApi.listFormSchemas(cid, 'review') + const reviewRes = await formSchemaApi.listFormSchemas(cid, 'review') + reviewSchemas.value = reviewRes.data } catch (e) { ElMessage.error(e instanceof Error ? e.message : '加载评审表列表失败') } finally { @@ -1092,6 +1098,9 @@ async function openEditSchema(row: FormSchemaRow) { editSchemaError.value = '' try { const full = await formSchemaApi.getFormSchema(cid, row.id) + if (full.signup_field_registry) { + setSignupFieldRegistry(full.signup_field_registry) + } editSchemaId.value = full.id editSchemaName.value = full.name editSchemaPurpose.value = full.purpose