fix(vip): restore the upgrade-price accordion and space out structured groups
升级价格 declared three reference-theme blocks through the accordion sections option, but the section builder handed the computed the whole field object instead of its sections array, so the guard always saw an empty length and the renderer silently fell back to a flat list. The three blocks ran into each other with no separation at all. - Move the section expansion into a pure buildFieldsetSections() helper so the mistake is now covered by a real contract test that runs the actual schema through it, instead of a regex over the component source. - Apply dependsOn to fields inside each block, so 天单价/促销标签 hide when 月费会员升级月费会员 is switched off, matching the flat path. - Give structured groups (会员商品/续费商品/积分兑换明细/升级价格) their own bottom margin and widen the gap between accordion blocks, so two adjacent groups no longer sit flush against each other. Verified: 657 Node contract tests passing, the admin SPA builds.
This commit is contained in:
1 parent
05f16d5fd7
commit
4b5098a753
3 files changed
+74
-14
No files matched your search
@@ -493,7 +493,7 @@
|
||||
</div>
|
||||
<div v-show="isFieldsetSectionOpen(sIndex)" class="aksrt-field__accordion-section-body">
|
||||
<AksField
|
||||
v-for="field in section.fields"
|
||||
v-for="field in sectionFields(section)"
|
||||
:key="field.key"
|
||||
:schema="field"
|
||||
:model-value="fieldsetValue(field)"
|
||||
@@ -741,6 +741,7 @@ import FaIconPicker from './FaIconPicker.vue'
|
||||
import AksMediaField from './AksMediaField.vue'
|
||||
import AksGalleryField from './AksGalleryField.vue'
|
||||
import AksCodeEditor from './AksCodeEditor.vue'
|
||||
import { buildFieldsetSections } from './fieldset-sections.js'
|
||||
import draggable from 'vuedraggable'
|
||||
import api from '@/api'
|
||||
|
||||
@@ -865,18 +866,7 @@ function fieldsetValue(field) {
|
||||
* key,默认全部收起,点击块标题展开。未声明 sections 时保持平铺渲染。
|
||||
* ------------------------------------------------------------------ */
|
||||
const fieldsetSections = computed(() => {
|
||||
const sections = Array.isArray(props.schema.sections) ? props.schema : []
|
||||
if (!sections.length) return []
|
||||
const byKey = {}
|
||||
for (const field of (props.schema.fields || [])) {
|
||||
byKey[field.key] = field
|
||||
}
|
||||
return sections.map((section) => ({
|
||||
title: String(section.title || ''),
|
||||
fields: (Array.isArray(section.keys) ? section.keys : [])
|
||||
.map((key) => byKey[key])
|
||||
.filter(Boolean),
|
||||
}))
|
||||
return buildFieldsetSections(props.schema)
|
||||
})
|
||||
|
||||
const openFieldsetSections = ref(new Set())
|
||||
@@ -885,6 +875,11 @@ function isFieldsetSectionOpen(index) {
|
||||
return openFieldsetSections.value.has(index)
|
||||
}
|
||||
|
||||
/** 分块内字段同样遵循 dependsOn,数据源是本 fieldset 的值对象。 */
|
||||
function sectionFields(section) {
|
||||
return (section.fields || []).filter((field) => matchDependency(field.dependsOn, props.modelValue))
|
||||
}
|
||||
|
||||
function toggleFieldsetSection(index) {
|
||||
const next = new Set(openFieldsetSections.value)
|
||||
if (next.has(index)) {
|
||||
@@ -1319,6 +1314,15 @@ function confirmRepeaterModal() {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 结构化分组(会员商品/续费商品/积分兑换/升级价格)与相邻字段之间留出更明确的间隔,
|
||||
* 避免两个分组上下贴在一起,和参考主题的留白一致。 */
|
||||
.aksrt-field--group,
|
||||
.aksrt-field--repeater,
|
||||
.aksrt-field--accordion,
|
||||
.aksrt-field--fieldset {
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.aksrt-field__input-wrap {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -1585,7 +1589,7 @@ function confirmRepeaterModal() {
|
||||
}
|
||||
|
||||
.aksrt-field__accordion-section + .aksrt-field__accordion-section {
|
||||
margin-top: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.aksrt-field__accordion-section-head {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 把 accordion / fieldset 的 `schema.sections` 声明展开成「块标题 + 字段定义」。
|
||||
*
|
||||
* 参考主题把「升级价格」这类复合配置拆成若干可折叠块,每块用 keys 声明自己
|
||||
* 包含哪些字段。这里是纯函数,方便在 Node 契约测试里直接用真实 schema 校验,
|
||||
* 避免渲染层的一个小疏忽让整块手风琴静默退化成平铺。
|
||||
*/
|
||||
export function buildFieldsetSections(schema) {
|
||||
const declared = schema && Array.isArray(schema.sections) ? schema.sections : []
|
||||
if (!declared.length) return []
|
||||
|
||||
const byKey = {}
|
||||
const nested = Array.isArray(schema.fields) ? schema.fields : []
|
||||
for (const field of nested) {
|
||||
if (field && field.key) byKey[field.key] = field
|
||||
}
|
||||
|
||||
return declared.map((section) => ({
|
||||
title: String((section && section.title) || ''),
|
||||
fields: (Array.isArray(section && section.keys) ? section.keys : [])
|
||||
.map((key) => byKey[key])
|
||||
.filter(Boolean),
|
||||
}))
|
||||
}
|
||||
@@ -448,3 +448,35 @@ test('SPA 镜像 schema 与 PHP schema 同构', async () => {
|
||||
assert.ok(!vipSchemaJs.includes(key), `vip.js 不应再含 ${key}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('升级价格 sections 能展开成三块手风琴,且块内字段遵循 dependsOn', async () => {
|
||||
const { buildFieldsetSections } = await import(
|
||||
pathToFileURL(path.join(root, 'assets/admin/src/components/form/fieldset-sections.js')).href
|
||||
);
|
||||
const mod = await import(pathToFileURL(path.join(root, 'assets/admin/src/schema/sections/payment/vip.js')).href);
|
||||
const upgrade = findGroup(mod.vipSchema, 'vip_upgrade_product');
|
||||
|
||||
const sections = buildFieldsetSections(upgrade);
|
||||
assert.strictEqual(sections.length, 3, '升级价格应展开成三块');
|
||||
assert.deepStrictEqual(sections.map((s) => s.title), [
|
||||
'月费会员升级月费会员',
|
||||
'月费会员升级永久会员',
|
||||
'永久会员升级永久会员',
|
||||
]);
|
||||
// 每块的字段并集恰好等于声明的全部字段,且顺序与 schema.fields 一致。
|
||||
const expanded = sections.flatMap((s) => s.fields.map((f) => f.key));
|
||||
assert.deepStrictEqual(expanded, upgrade.fields.map((f) => f.key));
|
||||
// 第一块带「已设置为永久会员」之外的提示与开关,字段定义必须是真实对象而非占位。
|
||||
assert.ok(sections[0].fields.every((f) => f && typeof f === 'object' && f.key));
|
||||
|
||||
// 没有 sections 声明的 fieldset 保持平铺,不产生假块。
|
||||
assert.deepStrictEqual(buildFieldsetSections({ type: 'fieldset', fields: [{ key: 'a' }] }), []);
|
||||
assert.deepStrictEqual(buildFieldsetSections(undefined), []);
|
||||
|
||||
// 渲染层把 sections 交给纯函数,并对块内字段套用 dependsOn。
|
||||
assert.match(fieldVue, /const fieldsetSections = computed\(\(\) => \{[\s\S]*?buildFieldsetSections\(props\.schema\)/);
|
||||
assert.match(fieldVue, /function sectionFields\(section\) \{[\s\S]*?matchDependency\(field\.dependsOn, props\.modelValue\)/);
|
||||
// 结构化分组之间留出间隔,两个分组不会上下贴在一起。
|
||||
assert.match(fieldVue, /\.aksrt-field--group,\s*\.aksrt-field--repeater,\s*\.aksrt-field--accordion,\s*\.aksrt-field--fieldset \{[\s\S]*?margin-bottom: 26px;/);
|
||||
assert.match(fieldVue, /\.aksrt-field__accordion-section \+ \.aksrt-field__accordion-section \{[\s\S]*?margin-top: 14px;/);
|
||||
});
|
||||
Reference in new issue
Block a user