/** * shared 层内部工具,不对外导出。 * * HTTP / WebSocket 收到的一切都先当 unknown,再用这些收窄函数校验; * 直接断言会把服务端字段改名这类问题推迟到运行期某个深处才炸。 */ /** * FormData / Blob(含 File)/ URLSearchParams 不是普通对象: * 展开或 JSON 序列化都会丢掉内容,必须原样交给 fetch(报名表导入就是被展平才丢文件)。 */ export function isRawBody(value: unknown): value is FormData | Blob | URLSearchParams { if (typeof FormData !== 'undefined' && value instanceof FormData) return true if (typeof Blob !== 'undefined' && value instanceof Blob) return true if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) return true return false } export function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) && !isRawBody(value) } export function readString(source: unknown, key: string): string | null { if (!isRecord(source)) return null const value = source[key] return typeof value === 'string' ? value : null } export function asRecord(value: unknown): Record { return isRecord(value) ? value : {} } /** 依次取第一个可用值:容忍服务端把数字写成字符串(settings 表存的是文本)。 */ export function firstString(...values: unknown[]): string | null { for (const value of values) { if (typeof value === 'string') return value } return null } export function firstNumber(def: number, ...values: unknown[]): number { const found = firstNumberOrNull(...values) return found === null ? def : found } export function firstNumberOrNull(...values: unknown[]): number | null { for (const value of values) { if (typeof value === 'number' && Number.isFinite(value)) return value if (typeof value === 'string' && value.trim() !== '') { const parsed = Number(value) if (Number.isFinite(parsed)) return parsed } } return null } export function firstBool(def: boolean, ...values: unknown[]): boolean { for (const value of values) { if (typeof value === 'boolean') return value if (typeof value === 'number') return value !== 0 if (typeof value === 'string') { const normalized = value.trim().toLowerCase() if (normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on') { return true } if (normalized === 'false' || normalized === '0' || normalized === 'no' || normalized === 'off') { return false } } } return def } export function emptyToNull(value: string | null): string | null { return value === null || value.trim() === '' ? null : value }