/** * repeater / group 行的纯函数:新行默认值、行 ID、listPreview 摘要与预览。 * inline 与 list-modal 两种渲染共用。 */ let rowIdCounter = 0 export function createRepeaterRowId() { return `rpt_${Date.now()}_${++rowIdCounter}` } export function createRepeaterRow(fields) { const row = { _id: createRepeaterRowId() } for (const field of (Array.isArray(fields) ? fields : [])) { // UI-only chrome rows (notice / alert) are presentation only. if (field.ui_only) continue if (field.default !== undefined) { row[field.key] = field.default } else if (field.type === 'switch') { row[field.key] = false } else if (field.type === 'checkbox' || field.type === 'repeater') { row[field.key] = [] } else { row[field.key] = '' } } return row } /** time/unit 形态的时长摘要:0 视为永久,其余按 day/month 补单位。 */ function rowDuration(row, cfg) { if (!cfg.descKey) return '' const raw = row[cfg.descKey] if (raw === '' || raw == null) return '' const amount = Number(raw) if (!Number.isFinite(amount)) return String(raw) if (amount <= 0) return '永久' if (!cfg.descUnitKey) return String(raw) const unit = String(row[cfg.descUnitKey] || '') if (unit === 'month') return `${amount} 个月` if (unit === 'day') return `${amount} 天` return String(raw) } /** 未配置 titleKey 时,取首个值为非空字符串/数字的字段作为标题。 */ function fallbackTitleKey(row, fields) { if (row.title != null && String(row.title).trim() !== '') return 'title' for (const field of (Array.isArray(fields) ? fields : [])) { if (field.ui_only) continue const value = row[field.key] if ((typeof value === 'string' && value.trim() !== '') || typeof value === 'number') return field.key } return 'title' } /** 收起态标题行上的一行摘要,取自 listPreview 配置。 */ export function repeaterRowSummary(row, cfg = {}, fields = []) { if (!row) return '' const title = String(row[cfg.titleKey || fallbackTitleKey(row, fields)] ?? '').trim() const prefix = String(cfg.prefix || '') if (title === '') return prefix const duration = rowDuration(row, cfg) return duration ? `${prefix}${title} · ${duration}` : `${prefix}${title}` } /** list-modal 卡片预览:图标 / 自定义图标 HTML / 图标类 / 标题 / 描述。 */ export function repeaterRowPreview(row, cfg = {}) { const read = (key) => String(row?.[key] || '').trim() const desc = read(cfg.descKey || 'desc') || read(cfg.urlKey || 'url') return { icon: read(cfg.iconKey || 'icon'), customIcon: read(cfg.customIconKey || 'custom_icon'), iconClass: read(cfg.iconClassKey || 'icon_class'), title: read(cfg.titleKey || 'title') || '未命名卡片', desc: desc || '暂无描述', } }