master
lion 3 months ago
parent 8e1e6dfa30
commit e42d521a42

@ -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<FormSchemaRow[]> {
export interface FormSchemaListResult {
data: FormSchemaRow[]
signupFieldRegistry?: SignupFieldRegistryManifest
}
export async function listFormSchemas(
competitionId: number,
purpose?: 'signup' | 'review',
): Promise<FormSchemaListResult> {
const { data } = await adminHttp.get<unknown>(`/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<FormSchemaRow> {

@ -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<string, string[]>
generic_scalar_types: string[]
all_supported_form_keys: string[]
}
export interface FormSchemaPayload {
purpose: 'signup' | 'review'
name: string

@ -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',

@ -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

@ -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<string, string[]>
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
}

@ -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

Loading…
Cancel
Save