修复:根据图片字段选择前台列表布局

This commit is contained in:
王鹏
2026-07-15 14:44:02 +08:00
parent 79dea897bc
commit 8247c86862
8 changed files with 92 additions and 53 deletions

View File

@@ -903,38 +903,18 @@ function solutionPreviewClasses(preset) {
const solution = visualSolutionDefinition(preset) const solution = visualSolutionDefinition(preset)
const navigation = solutionNavigation(solution.shell, 'frontend') const navigation = solutionNavigation(solution.shell, 'frontend')
const header = String(solution.shell.header || '') const header = String(solution.shell.header || '')
const listLayout = String(solution.layoutPolicy['frontend.list'] || '')
const layoutClass = listLayout.includes('card')
? 'preview-layout-card'
: listLayout.includes('split')
? 'preview-layout-split'
: listLayout.includes('dense')
? 'preview-layout-dense'
: listLayout
? 'preview-layout-table'
: 'preview-layout-unconfigured'
return [ return [
navigation ? `preview-navigation-${navigation}` : 'preview-navigation-unconfigured', navigation ? `preview-navigation-${navigation}` : 'preview-navigation-unconfigured',
header ? `preview-header-${header}` : 'preview-header-unconfigured', header ? `preview-header-${header}` : 'preview-header-unconfigured',
layoutClass 'preview-layout-adaptive'
] ]
} }
function solutionMetaLabels(preset) { function solutionMetaLabels(preset) {
const solution = visualSolutionDefinition(preset) const solution = visualSolutionDefinition(preset)
const listLayout = String(solution.layoutPolicy['frontend.list'] || '')
const frontendNavigation = navigationLabel(solutionNavigation(solution.shell, 'frontend'), '前台') const frontendNavigation = navigationLabel(solutionNavigation(solution.shell, 'frontend'), '前台')
const adminNavigation = navigationLabel(solutionNavigation(solution.shell, 'admin'), '后台') const adminNavigation = navigationLabel(solutionNavigation(solution.shell, 'admin'), '后台')
const layout = listLayout.includes('card') return [`${frontendNavigation} / ${adminNavigation}`, '列表按图片自适应']
? '卡片列表'
: listLayout.includes('split')
? '分栏列表'
: listLayout.includes('dense')
? '高密度列表'
: listLayout
? '标准表格'
: '布局未配置'
return [`${frontendNavigation} / ${adminNavigation}`, layout]
} }
function solutionNavigation(shell, scope) { function solutionNavigation(shell, scope) {

View File

@@ -411,6 +411,8 @@ test('generate page presents style presets as visual themes with previews', () =
assert.match(source, /navigationLabel\(solutionNavigation\(solution\.shell, 'frontend'\), '前台'\)/) assert.match(source, /navigationLabel\(solutionNavigation\(solution\.shell, 'frontend'\), '前台'\)/)
assert.match(source, /navigationLabel\(solutionNavigation\(solution\.shell, 'admin'\), '后台'\)/) assert.match(source, /navigationLabel\(solutionNavigation\(solution\.shell, 'admin'\), '后台'\)/)
assert.match(source, /scope === 'admin' \? 'adminNavigation' : 'frontendNavigation'/) assert.match(source, /scope === 'admin' \? 'adminNavigation' : 'frontendNavigation'/)
assert.match(source, /列表按图片自适应/)
assert.match(source, /'preview-layout-adaptive'/)
}) })
test('generate page shows one managed-theme error and never falls back to local presets', () => { test('generate page shows one managed-theme error and never falls back to local presets', () => {

View File

@@ -208,6 +208,8 @@ public class PageThemeDefinitionServiceImpl implements IPageThemeDefinitionServi
Map<String, Object> layoutPolicy = parseObject( Map<String, Object> layoutPolicy = parseObject(
StringUtils.defaultIfEmpty(theme.getLayoutPolicyJson(), "{}"), "布局策略"); StringUtils.defaultIfEmpty(theme.getLayoutPolicyJson(), "{}"), "布局策略");
layoutPolicy.remove("frontend.list");
layoutPolicy.remove("frontend.current_user_list");
for (Map.Entry<String, Object> entry : layoutPolicy.entrySet()) for (Map.Entry<String, Object> entry : layoutPolicy.entrySet())
{ {
String[] parts = entry.getKey().split("\\.", 2); String[] parts = entry.getKey().split("\\.", 2);

View File

@@ -275,29 +275,23 @@ public class PageCompositionService
} }
if ("current_user_list".equals(pageType)) if ("current_user_list".equals(pageType))
{ {
boolean useCard = imageCount > 0 || browsingScene || listCount <= 6; boolean useCard = imageCount > 0;
decision.canvas = useCard ? "frontend-current-user-card-v1" : "frontend-current-user-list-v1"; decision.canvas = useCard ? "frontend-current-user-card-v1" : "frontend-current-user-list-v1";
decision.addReason(useCard ? "个人记录适合卡片式浏览" : "个人记录字段较多,保留表格效率"); decision.addReason(useCard ? "检测到图片字段,个人记录采用卡片列表" : "未检测到图片字段,个人记录采用标准列表");
decision.confidence = 0.84D; decision.confidence = 0.98D;
return decision; return decision;
} }
if (categoryField && queryCount >= 2) if (imageCount > 0)
{
decision.canvas = "frontend-list-split-v1";
decision.addReason("分类字段和筛选条件明显,采用分栏列表");
decision.confidence = 0.88D;
}
else if (imageCount > 0 && (browsingScene || listCount <= 8))
{ {
decision.canvas = "frontend-list-card-v1"; decision.canvas = "frontend-list-card-v1";
decision.addReason("记录包含图片且偏浏览场景,采用卡片列表"); decision.addReason("检测到图片字段,前台采用卡片列表");
decision.confidence = 0.91D; decision.confidence = 0.98D;
} }
else else
{ {
decision.canvas = "frontend-list-v1"; decision.canvas = "frontend-list-v1";
decision.addReason("数据维护和检索特征更强,采用标准表"); decision.addReason("未检测到图片字段,前台采用标准");
decision.confidence = 0.72D; decision.confidence = 0.98D;
} }
return decision; return decision;
} }

View File

@@ -504,8 +504,15 @@ public class PageStylePresetRegistry
public String preferredLayout(String scope, String pageType) public String preferredLayout(String scope, String pageType)
{ {
String normalizedScope = StringUtils.defaultIfEmpty(scope, "frontend");
String normalizedPageType = StringUtils.defaultIfEmpty(pageType, "list");
if ("frontend".equalsIgnoreCase(normalizedScope)
&& ("list".equals(normalizedPageType) || "current_user_list".equals(normalizedPageType)))
{
return "";
}
return StringUtils.defaultString(layoutPolicy.get( return StringUtils.defaultString(layoutPolicy.get(
StringUtils.defaultIfEmpty(scope, "frontend") + "." + StringUtils.defaultIfEmpty(pageType, "list"))); normalizedScope + "." + normalizedPageType));
} }
public String shellValue(String name, String fallback) public String shellValue(String name, String fallback)

View File

@@ -60,6 +60,20 @@ public class PageThemeDefinitionServiceImplTest
assertFalse(theme.getShellJson().contains("\"navigation\"")); assertFalse(theme.getShellJson().contains("\"navigation\""));
} }
@Test
public void insertRemovesFrontendListLayoutOverrides()
{
PageThemeDefinition theme = customTheme();
theme.setLayoutPolicyJson("{\"frontend.list\":\"frontend-list-card-v1\","
+ "\"frontend.current_user_list\":\"frontend-current-user-card-v1\"}");
when(mapper.selectPageThemeByCode("ocean-blue")).thenReturn(null);
service.insertPageTheme(theme);
assertFalse(theme.getLayoutPolicyJson().contains("frontend.list"));
assertFalse(theme.getLayoutPolicyJson().contains("frontend.current_user_list"));
}
@Test @Test
public void insertRejectsInvalidTokenJson() public void insertRejectsInvalidTokenJson()
{ {

View File

@@ -24,7 +24,7 @@ public class PageCompositionServiceStylePresetTest
private final PageCompositionService service = new PageCompositionService(); private final PageCompositionService service = new PageCompositionService();
@Test @Test
public void cardPortalPresetKeepsAutomaticallySelectedFrontendCanvas() public void visualPresetDoesNotForceCardLayoutWithoutImage()
{ {
FrontProjectPageDesign design = design("frontend", "list"); FrontProjectPageDesign design = design("frontend", "list");
@@ -33,7 +33,7 @@ public class PageCompositionServiceStylePresetTest
column("status", "status", "1", "1", "1")), null, null, true, "card_portal"); column("status", "status", "1", "1", "1")), null, null, true, "card_portal");
Map<String, Object> layout = readLayout(design); Map<String, Object> layout = readLayout(design);
assertEquals("frontend-list-card-v1", layout.get("canvas")); assertEquals("frontend-list-v1", layout.get("canvas"));
assertEquals("card_portal", layout.get("stylePreset")); assertEquals("card_portal", layout.get("stylePreset"));
assertTrue(((Map<?, ?>) layout.get("styleTokens")).containsKey("primary")); assertTrue(((Map<?, ?>) layout.get("styleTokens")).containsKey("primary"));
Map<?, ?> composition = (Map<?, ?>) layout.get("composition"); Map<?, ?> composition = (Map<?, ?>) layout.get("composition");
@@ -41,6 +41,22 @@ public class PageCompositionServiceStylePresetTest
assertTrue(String.valueOf(composition.get("reasons")).contains("card_portal")); assertTrue(String.valueOf(composition.get("reasons")).contains("card_portal"));
} }
@Test
public void visualPresetDoesNotForceStandardLayoutWithImage()
{
FrontProjectPageDesign design = design("frontend", "list");
FrontProjectColumn cover = column("cover_url", "coverUrl", "0", "1", "1");
cover.setHtmlType("imageUpload");
service.compose(design, table(), Arrays.asList(
column("title", "title", "1", "1", "1"), cover),
null, null, true, "minimal-white");
Map<String, Object> layout = readLayout(design);
assertEquals("frontend-list-card-v1", layout.get("canvas"));
assertEquals("minimal-white", layout.get("stylePreset"));
}
@Test @Test
public void dataDensePresetKeepsAutomaticallySelectedAdminCanvas() public void dataDensePresetKeepsAutomaticallySelectedAdminCanvas()
{ {
@@ -109,7 +125,25 @@ public class PageCompositionServiceStylePresetTest
Map<String, Object> layout = readLayout(design); Map<String, Object> layout = readLayout(design);
assertEquals("current_user_list", design.getPageType()); assertEquals("current_user_list", design.getPageType());
assertEquals("frontend-current-user-card-v1", layout.get("canvas")); assertEquals("frontend-current-user-list-v1", layout.get("canvas"));
}
@Test
public void myReaderListUsesCardsWhenItHasAnImage()
{
FrontProjectPageDesign design = design("frontend", "list");
design.setPageCode("my_borrows_page");
design.setPageName("My Borrows");
design.setRoutePath("/my/borrows");
FrontProjectColumn cover = column("book_cover", "bookCover", "0", "1", "0");
cover.setHtmlType("imageUpload");
service.compose(design, table(), Arrays.asList(
column("reader_id", "readerId", "0", "1", "0"), cover),
null, null, true, "classic-admin");
assertEquals("current_user_list", design.getPageType());
assertEquals("frontend-current-user-card-v1", readLayout(design).get("canvas"));
} }
@Test @Test
@@ -122,10 +156,10 @@ public class PageCompositionServiceStylePresetTest
"modern-blue", PageLayoutTemplateRegistry.STABLE_RANDOM, Long.valueOf(42L)); "modern-blue", PageLayoutTemplateRegistry.STABLE_RANDOM, Long.valueOf(42L));
Map<String, Object> layout = readLayout(design); Map<String, Object> layout = readLayout(design);
assertTrue(!"frontend-list-card-v1".equals(layout.get("canvas"))); assertTrue(!"frontend-list-v1".equals(layout.get("canvas")));
Map<?, ?> composition = (Map<?, ?>) layout.get("composition"); Map<?, ?> composition = (Map<?, ?>) layout.get("composition");
assertEquals(PageLayoutTemplateRegistry.STABLE_RANDOM, composition.get("variantMode")); assertEquals(PageLayoutTemplateRegistry.STABLE_RANDOM, composition.get("variantMode"));
assertEquals("frontend-list-card-v1", composition.get("recommendedCanvas")); assertEquals("frontend-list-v1", composition.get("recommendedCanvas"));
assertEquals(layout.get("canvas"), composition.get("selectedCanvas")); assertEquals(layout.get("canvas"), composition.get("selectedCanvas"));
assertEquals(42L, ((Number) composition.get("variantSeed")).longValue()); assertEquals(42L, ((Number) composition.get("variantSeed")).longValue());

View File

@@ -189,6 +189,13 @@
</el-row> </el-row>
<el-divider content-position="left">页面布局策略</el-divider> <el-divider content-position="left">页面布局策略</el-divider>
<el-alert
title="前台列表按字段自动布局:存在图片字段时使用卡片列表,否则使用标准列表。"
type="info"
:closable="false"
show-icon
style="margin-bottom: 18px"
/>
<el-row :gutter="18"> <el-row :gutter="18">
<el-col v-for="item in layoutPolicyFields" :key="item.key" :span="8"> <el-col v-for="item in layoutPolicyFields" :key="item.key" :span="8">
<el-form-item :label="item.label"> <el-form-item :label="item.label">
@@ -258,12 +265,6 @@ function defaultAssets() {
return { iconStyle: 'outline', illustrationStyle: 'clean', chartPalette: ['#2563eb', '#06b6d4', '#7c3aed', '#f59e0b', '#10b981'] } return { iconStyle: 'outline', illustrationStyle: 'clean', chartPalette: ['#2563eb', '#06b6d4', '#7c3aed', '#f59e0b', '#10b981'] }
} }
const FRONT_LIST_LAYOUTS = [
{ label: '标准表格', value: 'frontend-list-v1' }, { label: '卡片列表', value: 'frontend-list-card-v1' }, { label: '分栏列表', value: 'frontend-list-split-v1' }
]
const FRONT_MINE_LAYOUTS = [
{ label: '我的列表', value: 'frontend-current-user-list-v1' }, { label: '我的卡片', value: 'frontend-current-user-card-v1' }
]
const FRONT_DETAIL_LAYOUTS = [ const FRONT_DETAIL_LAYOUTS = [
{ label: '基础详情', value: 'frontend-detail-v1' }, { label: '图文详情', value: 'frontend-detail-media-v1' } { label: '基础详情', value: 'frontend-detail-v1' }, { label: '图文详情', value: 'frontend-detail-media-v1' }
] ]
@@ -297,8 +298,6 @@ export default {
assetForm: defaultAssets(), assetForm: defaultAssets(),
chartPaletteText: defaultAssets().chartPalette.join(', '), chartPaletteText: defaultAssets().chartPalette.join(', '),
layoutPolicyFields: [ layoutPolicyFields: [
{ key: 'frontend.list', label: '前台列表', options: FRONT_LIST_LAYOUTS },
{ key: 'frontend.current_user_list', label: '个人列表', options: FRONT_MINE_LAYOUTS },
{ key: 'frontend.detail', label: '前台详情', options: FRONT_DETAIL_LAYOUTS }, { key: 'frontend.detail', label: '前台详情', options: FRONT_DETAIL_LAYOUTS },
{ key: 'frontend.form', label: '前台表单', options: FRONT_FORM_LAYOUTS }, { key: 'frontend.form', label: '前台表单', options: FRONT_FORM_LAYOUTS },
{ key: 'admin.list', label: '后台列表', options: ADMIN_LIST_LAYOUTS }, { key: 'admin.list', label: '后台列表', options: ADMIN_LIST_LAYOUTS },
@@ -383,6 +382,12 @@ export default {
delete result.navigation delete result.navigation
return result return result
}, },
normalizeLayoutPolicy(policy) {
const result = Object.assign({}, policy || {})
delete result['frontend.list']
delete result['frontend.current_user_list']
return result
},
rowPreviewStyle(row) { rowPreviewStyle(row) {
const tokens = this.parseObject(row.tokensJson, defaultTokens()) const tokens = this.parseObject(row.tokensJson, defaultTokens())
return { '--mini-primary': tokens.primary, '--mini-accent': tokens.accent, '--mini-bg': tokens.background, '--mini-surface': tokens.surface, '--mini-border': tokens.border } return { '--mini-primary': tokens.primary, '--mini-accent': tokens.accent, '--mini-bg': tokens.background, '--mini-surface': tokens.surface, '--mini-border': tokens.border }
@@ -401,7 +406,7 @@ export default {
this.tokenForm = this.parseObject(this.form.tokensJson, defaultTokens()) this.tokenForm = this.parseObject(this.form.tokensJson, defaultTokens())
this.componentForm = this.parseObject(this.form.componentDefaultsJson, defaultComponents()) this.componentForm = this.parseObject(this.form.componentDefaultsJson, defaultComponents())
this.shellForm = this.parseShell(this.form.shellJson) this.shellForm = this.parseShell(this.form.shellJson)
this.layoutPolicyForm = this.parseObject(this.form.layoutPolicyJson, defaultLayoutPolicy()) this.layoutPolicyForm = this.normalizeLayoutPolicy(this.parseObject(this.form.layoutPolicyJson, defaultLayoutPolicy()))
this.assetForm = this.parseObject(this.form.assetPolicyJson, defaultAssets()) this.assetForm = this.parseObject(this.form.assetPolicyJson, defaultAssets())
this.chartPaletteText = Array.isArray(this.assetForm.chartPalette) ? this.assetForm.chartPalette.join(', ') : '' this.chartPaletteText = Array.isArray(this.assetForm.chartPalette) ? this.assetForm.chartPalette.join(', ') : ''
this.title = '修改视觉方案' this.title = '修改视觉方案'
@@ -416,8 +421,9 @@ export default {
const shell = Object.assign({}, this.shellForm) const shell = Object.assign({}, this.shellForm)
delete shell.navigation delete shell.navigation
this.form.shellJson = JSON.stringify(shell) this.form.shellJson = JSON.stringify(shell)
this.form.layoutPolicyJson = JSON.stringify(Object.keys(this.layoutPolicyForm).reduce((result, key) => { const layoutPolicy = this.normalizeLayoutPolicy(this.layoutPolicyForm)
if (this.layoutPolicyForm[key]) result[key] = this.layoutPolicyForm[key] this.form.layoutPolicyJson = JSON.stringify(Object.keys(layoutPolicy).reduce((result, key) => {
if (layoutPolicy[key]) result[key] = layoutPolicy[key]
return result return result
}, {})) }, {}))
this.form.assetPolicyJson = JSON.stringify(Object.assign({}, this.assetForm, { this.form.assetPolicyJson = JSON.stringify(Object.assign({}, this.assetForm, {