feat(admin): make all sortable/repeater cards collapsible, collapsed by default

- 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>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-10-28 14:12:13 +08:00
1 parent 8843ae4261
commit 116b6b2223
9 files changed
+114 -76

No files matched your search

@@ -143,12 +143,11 @@ test('等级 Tab 标题由显示名称动态驱动', () => {
assert.match(rendererVue, /:name="tabName\(tab\)"/);
assert.match(rendererVue, /:tab="tabName\(tab\)"/);
assert.match(rendererVue, /function tabName\(tab\) \{[\s\S]*?tab\.titleKey[\s\S]*?tab\.titleFallback/);
// 手风琴:默认收起,点击标题行展开。
assert.match(fieldVue, /const repeaterCollapsible = computed\(\(\) => !!props\.schema\.collapsible\)/);
assert.match(fieldVue, /const repeaterDefaultOpen = computed\(\(\) => props\.schema\.defaultCollapsed === false\)/);
assert.match(fieldVue, /function toggleRepeaterItem\(index\)/);
assert.match(fieldVue, /v-show="!repeaterCollapsible \|\| isRepeaterItemOpen\(index\)"/);
// 手风琴:所有 repeater/group 行默认收起,点击标题行展开(按行 key 记录开合)。
assert.match(fieldVue, /useCollapsibleItems\(\)/);
assert.match(fieldVue, /v-show="isRepeaterItemOpen\(repeaterDragKey\(element\)\)"/);
assert.match(fieldVue, /aksrt-field__repeater-summary/);
assert.doesNotMatch(fieldVue, /schema\.collapsible|schema\.defaultCollapsed/);
// accordion/fieldset 的 sections 分块手风琴。
assert.match(fieldVue, /const fieldsetSections = computed\(/);
assert.match(fieldVue, /function toggleFieldsetSection\(index\)/);
@@ -168,8 +167,6 @@ test('结构化分组默认收起,标题行带摘要;升级价格按三块
assert.deepStrictEqual(Object.keys(accordion.groups).sort(), [...expectedGroups].sort());
for (const key of expectedGroups) {
const cfg = accordion.groups[key];
assert.strictEqual(cfg.collapsible, true, `${key} 应开启手风琴`);
assert.strictEqual(cfg.defaultCollapsed, true, `${key} 应默认收起`);
assert.ok(cfg.listPreview && cfg.listPreview.titleKey, `${key} 应配置标题行摘要`);
assert.ok(cfg.listPreview.descKey, `${key} 应配置标题行时长摘要`);
assert.strictEqual(cfg.listPreview.descUnitKey, 'unit', `${key} 时长摘要应带单位`);
@@ -470,13 +467,11 @@ test('SPA 镜像 schema 与 PHP schema 同构', async () => {
const points = schema.groups.find((g) => g.key === 'points-exchange').fields[1];
assert.strictEqual(points.type, 'group');
assert.deepStrictEqual(points.fields.map((f) => f.key), ['points', 'level', 'time', 'unit']);
// 手风琴配置与 PHP 侧一致。
// 标题行摘要配置与 PHP 侧一致(折叠为渲染器统一行为,不再由 schema 开关)。
for (const key of ['vip_1_product', 'vip_2_product', 'vip_1_renew_product', 'vip_2_renew_product']) {
const group = findGroup(schema, key);
assert.strictEqual(group.collapsible, true, `${key} 应开启手风琴`);
assert.strictEqual(group.defaultCollapsed, true, `${key} 应默认收起`);
assert.strictEqual(findGroup(schema, key).listPreview.prefix, '价格:', `${key} 摘要前缀`);
}
assert.strictEqual(findGroup(schema, 'pay_vip_points_exchange_product').collapsible, true);
assert.strictEqual(findGroup(schema, 'pay_vip_points_exchange_product').listPreview.prefix, '积分:');
const upgrade = findGroup(schema, 'vip_upgrade_product');
assert.strictEqual(upgrade.sections.length, 3);
// SPA 默认值与 PHP 侧一致。
@@ -267,8 +267,6 @@ namespace {
foreach ($tab['fields'] as $field) {
if (($field['type'] ?? '') === 'group') {
$out['groups'][$tab['key'] . '/' . $field['key']] = [
'collapsible' => $field['collapsible'] ?? null,
'defaultCollapsed' => $field['defaultCollapsed'] ?? null,
'listPreview' => $field['listPreview'] ?? null,
];
}
@@ -294,8 +292,6 @@ namespace {
continue;
}
$out['groups']['points-exchange/' . $field['key']] = [
'collapsible' => $field['collapsible'] ?? null,
'defaultCollapsed' => $field['defaultCollapsed'] ?? null,
'listPreview' => $field['listPreview'] ?? null,
];
}