- Add useCollapsibleItems: key-based open state, clickable/keyboard head, aria-expanded - sortable items with sub-fields and every inline repeater/group row now collapse - Repeater head summary falls back to the first filled field when no listPreview - Drop per-schema collapsible/defaultCollapsed for repeater groups (JS + PHP) - Align FloatBtnItemPanel head with the shared collapse attrs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
77 lines
2.8 KiB
JavaScript
77 lines
2.8 KiB
JavaScript
/**
|
|
* 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 || '暂无描述',
|
|
}
|
|
}
|