修复:根据图片字段选择前台列表布局
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
Reference in New Issue
Block a user