|
|
|
|
|
/** 报名表 / 评审表 schema_json 可视化编辑(单字段) */
|
|
|
|
|
|
|
|
|
|
|
|
export type FormSchemaPurpose = 'signup' | 'review'
|
|
|
|
|
|
|
|
|
|
|
|
export interface FormSchemaEditorItem {
|
|
|
|
|
|
/** 前端拖拽用,不落库 */
|
|
|
|
|
|
__uid: string
|
|
|
|
|
|
key: string
|
|
|
|
|
|
type: string
|
|
|
|
|
|
label: string
|
|
|
|
|
|
required: boolean
|
|
|
|
|
|
placeholder?: string
|
|
|
|
|
|
help?: string
|
|
|
|
|
|
/** select 等:{ label, value } */
|
|
|
|
|
|
options?: { label: string; value: string }[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 入库时为 checkbox + key commitment_accepted,仅供报名表可视化 */
|
|
|
|
|
|
export const SIGNUP_COMMITMENT_TYPE = 'commitment_promise' as const
|
|
|
|
|
|
|
|
|
|
|
|
export const SIGNUP_FIELD_TYPES: { value: string; label: string }[] = [
|
|
|
|
|
|
{ value: 'text', label: '单行文本' },
|
|
|
|
|
|
{ value: 'email', label: '邮箱' },
|
|
|
|
|
|
{ value: 'tel', label: '手机/电话' },
|
|
|
|
|
|
{ value: 'textarea', label: '多行文本' },
|
|
|
|
|
|
{ value: 'number', label: '数字' },
|
|
|
|
|
|
{ value: 'select', label: '下拉选择' },
|
|
|
|
|
|
{ value: 'checkbox', label: '勾选确认(通用)' },
|
|
|
|
|
|
{ value: SIGNUP_COMMITMENT_TYPE, label: '签署承诺书' },
|
|
|
|
|
|
{ value: 'file', label: '文件上传' },
|
|
|
|
|
|
{ value: 'date', label: '日期' },
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
export const REVIEW_FIELD_TYPES: { value: string; label: string }[] = [
|
|
|
|
|
|
{ value: 'number', label: '数字(打分维度)' },
|
|
|
|
|
|
{ value: 'textarea', label: '多行文本(评语等)' },
|
|
|
|
|
|
{ value: 'text', label: '单行文本' },
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
function newUid(): string {
|
|
|
|
|
|
return globalThis.crypto?.randomUUID?.() ?? `f_${Date.now()}_${Math.random().toString(36).slice(2)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function createEmptySchemaItem(
|
|
|
|
|
|
purpose: FormSchemaPurpose,
|
|
|
|
|
|
index: number,
|
|
|
|
|
|
): FormSchemaEditorItem {
|
|
|
|
|
|
const type = purpose === 'review' ? 'number' : 'text'
|
|
|
|
|
|
return {
|
|
|
|
|
|
__uid: newUid(),
|
|
|
|
|
|
key: purpose === 'review' ? `dim_${index + 1}` : `field_${index + 1}`,
|
|
|
|
|
|
type,
|
|
|
|
|
|
label: purpose === 'review' ? `评分维度 ${index + 1}` : `字段 ${index + 1}`,
|
|
|
|
|
|
required: purpose !== 'review',
|
|
|
|
|
|
placeholder: '',
|
|
|
|
|
|
help: '',
|
|
|
|
|
|
options: [],
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeOptions(raw: unknown): { label: string; value: string }[] {
|
|
|
|
|
|
if (!Array.isArray(raw)) return []
|
|
|
|
|
|
return raw
|
|
|
|
|
|
.map((x) => {
|
|
|
|
|
|
if (x !== null && typeof x === 'object' && 'label' in x && 'value' in x) {
|
|
|
|
|
|
return { label: String((x as { label: unknown }).label), value: String((x as { value: unknown }).value) }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof x === 'string') return { label: x, value: x }
|
|
|
|
|
|
return null
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter((x): x is { label: string; value: string } => x != null && x.label !== '')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 从接口 schema_json 解析为编辑器模型 */
|
|
|
|
|
|
export function schemaJsonToEditorItems(json: unknown, purpose: FormSchemaPurpose): FormSchemaEditorItem[] {
|
|
|
|
|
|
if (!Array.isArray(json)) return [createEmptySchemaItem(purpose, 0)]
|
|
|
|
|
|
return json.map((item, index) => {
|
|
|
|
|
|
if (item === null || typeof item !== 'object') {
|
|
|
|
|
|
return createEmptySchemaItem(purpose, index)
|
|
|
|
|
|
}
|
|
|
|
|
|
const o = item as Record<string, unknown>
|
|
|
|
|
|
const rawKey = String(o.key ?? (purpose === 'review' ? `dim_${index + 1}` : `field_${index + 1}`))
|
|
|
|
|
|
const rawType = String(o.type ?? (purpose === 'review' ? 'number' : 'text'))
|
|
|
|
|
|
const type =
|
|
|
|
|
|
purpose === 'signup' && rawKey === 'commitment_accepted' && rawType === 'checkbox'
|
|
|
|
|
|
? SIGNUP_COMMITMENT_TYPE
|
|
|
|
|
|
: rawType
|
|
|
|
|
|
return {
|
|
|
|
|
|
__uid: newUid(),
|
|
|
|
|
|
key: rawKey,
|
|
|
|
|
|
type,
|
|
|
|
|
|
label: String(o.label ?? `字段 ${index + 1}`),
|
|
|
|
|
|
required: Boolean(o.required),
|
|
|
|
|
|
placeholder: o.placeholder != null ? String(o.placeholder) : '',
|
|
|
|
|
|
help: o.help != null ? String(o.help) : '',
|
|
|
|
|
|
options: type === 'select' ? normalizeOptions(o.options) : [],
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 写入接口的 schema_json(数组) */
|
|
|
|
|
|
export function editorItemsToSchemaJson(items: FormSchemaEditorItem[]): unknown[] {
|
|
|
|
|
|
return items.map((item) => {
|
|
|
|
|
|
const isCommitment = item.type === SIGNUP_COMMITMENT_TYPE
|
|
|
|
|
|
const row: Record<string, unknown> = {
|
|
|
|
|
|
key: isCommitment ? 'commitment_accepted' : item.key.trim(),
|
|
|
|
|
|
type: isCommitment ? 'checkbox' : item.type,
|
|
|
|
|
|
label: item.label.trim(),
|
|
|
|
|
|
required: item.required,
|
|
|
|
|
|
}
|
|
|
|
|
|
if (item.placeholder?.trim()) row.placeholder = item.placeholder.trim()
|
|
|
|
|
|
if (item.help?.trim()) row.help = item.help.trim()
|
|
|
|
|
|
if (item.type === 'select' && item.options?.length) {
|
|
|
|
|
|
row.options = item.options.filter((o) => o.value !== '' || o.label !== '')
|
|
|
|
|
|
}
|
|
|
|
|
|
return row
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function validateEditorItems(items: FormSchemaEditorItem[]): string | null {
|
|
|
|
|
|
const keys = new Set<string>()
|
|
|
|
|
|
for (const it of items) {
|
|
|
|
|
|
const k = it.key.trim()
|
|
|
|
|
|
if (!k) return '存在未填写「字段 key」的项(须为英文/数字/下划线,与存储键一致)'
|
|
|
|
|
|
if (keys.has(k)) return `字段 key「${k}」重复`
|
|
|
|
|
|
keys.add(k)
|
|
|
|
|
|
if (!it.label.trim()) return `字段「${k}」缺少显示标签`
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|