import { reactive } from 'vue' import { getEntityOptions } from '../../services/api' import type { OptionSource } from '../../types/api' export const GENERATED_FIELD_WIDGETS = [ 'display', 'drop-down', 'text', 'hidden', 'date-time', 'submit', 'lookup', 'hyperlink', 'ignored', 'unknown', 'textarea', 'check', 'file', 'radio', 'password' ] as const export type SelectOption = { label: string value: unknown source?: string } export type SelectOptionState = { loading: boolean reason: string loaded: boolean options: SelectOption[] } const controlOnlyWidgets = new Set(['hidden', 'ignored', 'submit']) const readonlyWidgets = new Set(['display', 'display-entity', 'label', 'readonly', 'read-only']) const hyperlinkWidgets = new Set(['hyperlink', 'link']) const selectWidgets = new Set(['drop-down', 'select', 'combo-box', 'multi-select']) const radioWidgets = new Set(['radio']) const checkboxWidgets = new Set(['check', 'checkbox', 'boolean']) const lookupWidgets = new Set(['lookup']) const uploadWidgets = new Set(['file', 'upload', 'image', 'binary']) const textareaWidgets = new Set(['textarea']) const passwordWidgets = new Set(['password']) function cleanFieldText(value: unknown) { return String(value || '') .replace(/\$\{uiLabelMap\.([^}]+)\}/g, '$1') .replace(/\$\{([^}]+)\}/g, '$1') .replace(/([a-z0-9])([A-Z])/g, '$1 $2') .replace(/[-_]+/g, ' ') .trim() } export function fieldName(field: Record) { return String(field.name || field.fieldName || field.id || field.parameterName || field.attributeName || '') } export function labelFor(field: Record) { return cleanFieldText(field.title || field.label || field.description || field.name || field.fieldName || field.id) || '字段' } export function fieldWidget(field: Record) { return String(field.widget || field.type || field.fieldType || 'text').trim().toLowerCase() } function fieldHaystack(field: Record) { return [ fieldWidget(field), field.name, field.fieldName, field.id, field.title, field.label, field.description, field.tooltip, field.target ].map((item) => String(item || '').toLowerCase()).join(' ') } export function isControlField(field: Record) { return controlOnlyWidgets.has(fieldWidget(field)) } export function isReadonlyWidget(field: Record) { return readonlyWidgets.has(fieldWidget(field)) } export function isHyperlinkWidget(field: Record) { const widget = fieldWidget(field) if (hyperlinkWidgets.has(widget)) return true return widget === 'unknown' && Boolean(field.target) && /link|url|uri|website/.test(fieldHaystack(field)) } export function isSelectWidget(field: Record) { if (isRadioWidget(field) || isCheckboxWidget(field)) return false return selectWidgets.has(fieldWidget(field)) || Array.isArray(field.options) && field.options.length > 0 || Array.isArray(field.optionSources) && field.optionSources.length > 0 } export function isRadioWidget(field: Record) { return radioWidgets.has(fieldWidget(field)) } export function isMultiSelectWidget(field: Record) { return fieldWidget(field) === 'multi-select' } export function isCheckboxWidget(field: Record) { return checkboxWidgets.has(fieldWidget(field)) } export function isLookupWidget(field: Record) { return lookupWidgets.has(fieldWidget(field)) } export function isUploadWidget(field: Record) { const haystack = fieldHaystack(field) return uploadWidgets.has(fieldWidget(field)) || /upload|file|binary|image|logo|picture|photo|avatar|vcard/.test(haystack) } export function isDateWidget(field: Record) { const haystack = fieldHaystack(field) return fieldWidget(field).includes('date') || fieldWidget(field).includes('time') || /date|time|timestamp/.test(haystack) } export function datePickerTypeFor(field: Record) { const haystack = fieldHaystack(field) return haystack.includes('time') || haystack.includes('timestamp') ? 'datetime' : 'date' } export function isTextareaWidget(field: Record) { return textareaWidgets.has(fieldWidget(field)) || /description|comment|note|content|message|body|remark|memo/.test(fieldHaystack(field)) } export function isPasswordWidget(field: Record) { return passwordWidgets.has(fieldWidget(field)) } export function isNumericWidget(field: Record) { const haystack = fieldHaystack(field) return ['number', 'integer', 'decimal', 'currency'].includes(fieldWidget(field)) || /amount|balance|cost|price|total|quantity|count|limit|sequence|percent|rate|coeff|debit|credit|weight|height|width/.test(haystack) || /\bnum\b/.test(haystack) } export function inputTypeFor(field: Record) { const haystack = fieldHaystack(field) if (/email|fromaddress|toaddress/.test(haystack)) return 'email' if (/url|uri|website/.test(haystack)) return 'url' if (/phone|telecom|tel/.test(haystack)) return 'tel' return 'text' } export function shouldLoadOptions(field: Record) { return isSelectWidget(field) || isRadioWidget(field) } export function fieldValueText(value: unknown) { if (value === undefined || value === null || value === '') return '-' if (typeof value === 'boolean') return value ? '是' : '否' if (Array.isArray(value)) return value.length ? `${value.length} 项` : '-' if (typeof value === 'object') { const entries = Object.entries(value as Record) .filter(([, entryValue]) => entryValue !== undefined && entryValue !== null && entryValue !== '' && typeof entryValue !== 'object') .slice(0, 2) if (!entries.length) return '已提供' return entries.map(([key, entryValue]) => `${cleanFieldText(key)}: ${entryValue}`).join(',') } return String(value) } export function staticOptionsFor(field: Record): SelectOption[] { const options = Array.isArray(field.options) ? field.options : [] return options .map((option) => { if (typeof option === 'string' || typeof option === 'number' || typeof option === 'boolean') { return { label: cleanFieldText(option), value: option } } if (!option || typeof option !== 'object') return null const item = option as Record const value = Object.prototype.hasOwnProperty.call(item, 'value') ? item.value : item.key || item.id || item.name const label = cleanFieldText(item.label || item.description || item.text || item.name || value) return { label, value } }) .filter((option): option is SelectOption => Boolean(option)) } export function optionSourcesFor(field: Record): OptionSource[] { const sources: OptionSource[] = Array.isArray(field.optionSources) ? field.optionSources as OptionSource[] : [] if (field.optionSource && typeof field.optionSource === 'object') { sources.push(field.optionSource as OptionSource) } const entityName = field.optionEntityName || field.entityName if (entityName) { sources.push({ type: 'entity-options', entityName: String(entityName), keyFieldName: String(field.keyFieldName || field.valueField || field.keyField || fieldName(field)), description: String(field.descriptionFields || field.labelField || field.descriptionField || 'description') }) } return sources } export function firstEntityOptionSource(field: Record) { return optionSourcesFor(field).find((source) => source.type === 'entity-options' && source.entityName) } export function optionSourceReason(field: Record, state?: SelectOptionState) { if (state?.reason) return state.reason const listSource = optionSourcesFor(field).find((source) => source.type === 'list-options') if (listSource) { return `动态列表 ${listSource.listName || fieldName(field)} 需要进入原业务上下文后加载` } const entitySource = firstEntityOptionSource(field) if (entitySource) { return '动态选项来自业务资料' } return '' } export function createOptionState() { return reactive>({}) } export async function ensureFieldOptions( field: Record, states: Record, query = '' ) { const name = fieldName(field) const source = firstEntityOptionSource(field) if (!name || !source) return const state = states[name] || (states[name] = { loading: false, reason: '', loaded: false, options: [] }) if (state.loading) return state.loading = true state.reason = '' try { const result = await getEntityOptions(source, { query, pageSize: 60 }) state.options = Array.isArray(result.options) ? result.options.map((option) => ({ label: String(option.label || option.value || ''), value: option.value, source: 'entity-options' })) : [] state.loaded = !result.unavailable state.reason = result.unavailable ? String(result.reason || '动态选项暂不可用') : '' } catch (error) { state.loaded = false state.reason = error instanceof Error ? error.message : '动态选项加载失败' } finally { state.loading = false } } export function mergedOptionsFor(field: Record, states: Record) { const seen = new Set() const merged: SelectOption[] = [] for (const option of [...staticOptionsFor(field), ...(states[fieldName(field)]?.options || [])]) { const key = String(option.value) if (seen.has(key)) continue seen.add(key) merged.push(option) } return merged }