You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cxxfds-web/src/utils/formSchemaEditor.ts

220 lines
7.9 KiB

1 month ago
/** 报名表 / 评审表 schema_json 可视化编辑(单字段) */
export type FormSchemaPurpose = 'signup' | 'review'
export interface FormSchemaEditorItem {
/** 前端拖拽用,不落库 */
__uid: string
key: string
type: string
label: string
required: boolean
3 weeks ago
/** 选手端展示在标题后;必填时落在 * 之后(入库 title_supplement */
titleSupplement?: string
1 month ago
placeholder?: string
help?: string
/** select 等:{ label, value } */
options?: { label: string; value: string }[]
1 month ago
/** 报名表:条件必填依赖字段 key入库为 required_when.field */
requiredWhenField?: string
/** 报名表:依赖取值,每行一个(入库为 required_when.values */
requiredWhenValuesLines?: string
3 weeks ago
/** 文件字段:每行一个扩展名不含点 */
fileExtensionsLines?: string
/** 文件字段:最多文件数,空为不限制 */
fileMaxCount?: string
1 month ago
}
/** 入库时为 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',
3 weeks ago
titleSupplement: '',
1 month ago
placeholder: '',
help: '',
options: [],
1 month ago
requiredWhenField: '',
requiredWhenValuesLines: '',
3 weeks ago
fileExtensionsLines: '',
fileMaxCount: '',
1 month ago
}
}
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
1 month ago
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<string, unknown>
requiredWhenField = String(rwo.field ?? '').trim()
const vals = rwo.values
if (Array.isArray(vals)) {
requiredWhenValuesLines = vals.map((x) => String(x).trim()).filter(Boolean).join('\n')
}
}
}
1 month ago
return {
__uid: newUid(),
key: rawKey,
type,
label: String(o.label ?? `字段 ${index + 1}`),
required: Boolean(o.required),
3 weeks ago
titleSupplement: o.title_supplement != null ? String(o.title_supplement) : '',
1 month ago
placeholder: o.placeholder != null ? String(o.placeholder) : '',
help: o.help != null ? String(o.help) : '',
options: type === 'select' ? normalizeOptions(o.options) : [],
1 month ago
requiredWhenField,
requiredWhenValuesLines,
3 weeks ago
fileExtensionsLines: (() => {
if (type !== 'file') return ''
const fe = o.file_extensions
if (!Array.isArray(fe)) return ''
const lines = fe
.map((x) =>
String(x)
.trim()
.toLowerCase()
.replace(/^\./, ''),
)
.filter(Boolean)
return lines.join('\n')
})(),
fileMaxCount: (() => {
if (type !== 'file') return ''
const mc = o.file_max_count
if (mc == null || mc === '') return ''
const n = typeof mc === 'number' ? mc : parseInt(String(mc), 10)
return Number.isFinite(n) && n >= 1 ? String(Math.floor(n)) : ''
})(),
1 month ago
}
})
}
/** 写入接口的 schema_json数组 */
1 month ago
export function editorItemsToSchemaJson(items: FormSchemaEditorItem[], purpose: FormSchemaPurpose): unknown[] {
1 month ago
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()
3 weeks ago
if (item.titleSupplement?.trim()) row.title_supplement = item.titleSupplement.trim()
3 weeks ago
if (item.type === 'file') {
const extLines = String(item.fileExtensionsLines ?? '')
.split('\n')
.map((s) =>
s
.trim()
.toLowerCase()
.replace(/^\./, ''),
)
.filter(Boolean)
if (extLines.length) row.file_extensions = [...new Set(extLines)]
const mc = parseInt(String(item.fileMaxCount ?? '').trim(), 10)
if (Number.isFinite(mc) && mc >= 1) row.file_max_count = mc
}
1 month ago
if (item.type === 'select' && item.options?.length) {
row.options = item.options.filter((o) => o.value !== '' || o.label !== '')
}
1 month ago
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 }
}
}
1 month ago
return row
})
}
1 month ago
export function validateEditorItems(items: FormSchemaEditorItem[], purpose: FormSchemaPurpose): string | null {
1 month ago
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}」缺少显示标签`
1 month ago
if (
purpose === 'signup'
&& it.type !== SIGNUP_COMMITMENT_TYPE
&& it.requiredWhenField?.trim()
&& !it.requiredWhenValuesLines?.trim()
) {
return `字段「${k}」填写了条件必填依赖,但未填写「当取值」列表`
}
1 month ago
}
return null
}