mirror of https://github.com/halo-dev/halo
feat: support setting rendering templates for related posts on category (#6106)
#### What type of PR is this? /kind feature /area core /milestone 2.17.x #### What this PR does / why we need it: 支持在分类上为关联的文章统一设置渲染模板 现在文章的模板生效顺序为: 1. 文章关联的分类上设置的文章模板,如果有多个则选择第一个 2. 文章上设置的自定义模板 3. 文章的默认模板 #### Which issue(s) this PR fixes: Fixes #6101 #### Does this PR introduce a user-facing change? ```release-note 支持在分类上为关联的文章统一设置渲染模板 ```pull/6031/head^2
parent
6d3a157d35
commit
b5f9010e60
|
@ -13499,6 +13499,10 @@
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"postTemplate": {
|
||||||
|
"maxLength": 255,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"priority": {
|
"priority": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
|
@ -13509,6 +13513,7 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"template": {
|
"template": {
|
||||||
|
"maxLength": 255,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17446,12 +17451,12 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"default": "PUBLIC",
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
],
|
]
|
||||||
"default": "PUBLIC"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -19101,12 +19106,12 @@
|
||||||
},
|
},
|
||||||
"visible": {
|
"visible": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"default": "PUBLIC",
|
||||||
"enum": [
|
"enum": [
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"INTERNAL",
|
"INTERNAL",
|
||||||
"PRIVATE"
|
"PRIVATE"
|
||||||
],
|
]
|
||||||
"default": "PUBLIC"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package run.halo.app.core.extension.content;
|
package run.halo.app.core.extension.content;
|
||||||
|
|
||||||
|
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
|
||||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||||
import static run.halo.app.core.extension.content.Category.KIND;
|
import static run.halo.app.core.extension.content.Category.KIND;
|
||||||
|
|
||||||
|
@ -53,8 +54,17 @@ public class Category extends AbstractExtension {
|
||||||
|
|
||||||
private String cover;
|
private String cover;
|
||||||
|
|
||||||
|
@Schema(requiredMode = NOT_REQUIRED, maxLength = 255)
|
||||||
private String template;
|
private String template;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Used to specify the template for the posts associated with the category.</p>
|
||||||
|
* <p>The priority is not as high as that of the post.</p>
|
||||||
|
* <p>If the post also specifies a template, the post's template will prevail.</p>
|
||||||
|
*/
|
||||||
|
@Schema(requiredMode = NOT_REQUIRED, maxLength = 255)
|
||||||
|
private String postTemplate;
|
||||||
|
|
||||||
@Schema(requiredMode = REQUIRED, defaultValue = "0")
|
@Schema(requiredMode = REQUIRED, defaultValue = "0")
|
||||||
private Integer priority;
|
private Integer priority;
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
package run.halo.app.theme.router.factories;
|
package run.halo.app.theme.router.factories;
|
||||||
|
|
||||||
|
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||||
|
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||||
|
|
||||||
import com.google.common.cache.Cache;
|
import com.google.common.cache.Cache;
|
||||||
import com.google.common.cache.CacheBuilder;
|
import com.google.common.cache.CacheBuilder;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
@ -120,13 +123,24 @@ public class PostRouteFactory implements RouteFactory {
|
||||||
})
|
})
|
||||||
.flatMap(postVo -> {
|
.flatMap(postVo -> {
|
||||||
Map<String, Object> model = ModelMapUtils.postModel(postVo);
|
Map<String, Object> model = ModelMapUtils.postModel(postVo);
|
||||||
String template = postVo.getSpec().getTemplate();
|
return determineTemplate(request, postVo)
|
||||||
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
|
||||||
DefaultTemplateEnum.POST.getValue())
|
|
||||||
.flatMap(templateName -> ServerResponse.ok().render(templateName, model));
|
.flatMap(templateName -> ServerResponse.ok().render(templateName, model));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Mono<String> determineTemplate(ServerRequest request, PostVo postVo) {
|
||||||
|
return Flux.fromIterable(defaultIfNull(postVo.getCategories(), List.of()))
|
||||||
|
.filter(category -> isNotBlank(category.getSpec().getPostTemplate()))
|
||||||
|
.concatMap(category -> viewNameResolver.resolveViewNameOrDefault(request,
|
||||||
|
category.getSpec().getPostTemplate(), null)
|
||||||
|
)
|
||||||
|
.next()
|
||||||
|
.switchIfEmpty(Mono.defer(() -> viewNameResolver.resolveViewNameOrDefault(request,
|
||||||
|
postVo.getSpec().getTemplate(),
|
||||||
|
DefaultTemplateEnum.POST.getValue())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
Mono<PostVo> bestMatchPost(PostPatternVariable variable) {
|
Mono<PostVo> bestMatchPost(PostPatternVariable variable) {
|
||||||
return postsByPredicates(variable)
|
return postsByPredicates(variable)
|
||||||
.filter(post -> {
|
.filter(post -> {
|
||||||
|
@ -143,10 +157,10 @@ public class PostRouteFactory implements RouteFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
Flux<Post> postsByPredicates(PostPatternVariable patternVariable) {
|
Flux<Post> postsByPredicates(PostPatternVariable patternVariable) {
|
||||||
if (StringUtils.isNotBlank(patternVariable.getName())) {
|
if (isNotBlank(patternVariable.getName())) {
|
||||||
return fetchPostsByName(patternVariable.getName());
|
return fetchPostsByName(patternVariable.getName());
|
||||||
}
|
}
|
||||||
if (StringUtils.isNotBlank(patternVariable.getSlug())) {
|
if (isNotBlank(patternVariable.getSlug())) {
|
||||||
return fetchPostsBySlug(patternVariable.getSlug());
|
return fetchPostsBySlug(patternVariable.getSlug());
|
||||||
}
|
}
|
||||||
return Flux.empty();
|
return Flux.empty();
|
||||||
|
@ -163,7 +177,7 @@ public class PostRouteFactory implements RouteFactory {
|
||||||
private Flux<Post> fetchPostsBySlug(String slug) {
|
private Flux<Post> fetchPostsBySlug(String slug) {
|
||||||
return queryPostPredicateResolver.getListOptions()
|
return queryPostPredicateResolver.getListOptions()
|
||||||
.flatMapMany(listOptions -> {
|
.flatMapMany(listOptions -> {
|
||||||
if (StringUtils.isNotBlank(slug)) {
|
if (isNotBlank(slug)) {
|
||||||
var other = QueryFactory.equal("spec.slug", slug);
|
var other = QueryFactory.equal("spec.slug", slug);
|
||||||
listOptions.setFieldSelector(listOptions.getFieldSelector().andQuery(other));
|
listOptions.setFieldSelector(listOptions.getFieldSelector().andQuery(other));
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,7 @@ const formState = ref<Category>({
|
||||||
description: "",
|
description: "",
|
||||||
cover: "",
|
cover: "",
|
||||||
template: "",
|
template: "",
|
||||||
|
postTemplate: "",
|
||||||
priority: 0,
|
priority: 0,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
|
@ -160,6 +161,7 @@ onMounted(() => {
|
||||||
|
|
||||||
// custom templates
|
// custom templates
|
||||||
const { templates } = useThemeCustomTemplates("category");
|
const { templates } = useThemeCustomTemplates("category");
|
||||||
|
const { templates: postTemplates } = useThemeCustomTemplates("post");
|
||||||
|
|
||||||
// slug
|
// slug
|
||||||
const { handleGenerateSlug } = useSlugify(
|
const { handleGenerateSlug } = useSlugify(
|
||||||
|
@ -243,9 +245,26 @@ const { handleGenerateSlug } = useSlugify(
|
||||||
:label="
|
:label="
|
||||||
$t('core.post_category.editing_modal.fields.template.label')
|
$t('core.post_category.editing_modal.fields.template.label')
|
||||||
"
|
"
|
||||||
|
:help="
|
||||||
|
$t('core.post_category.editing_modal.fields.template.help')
|
||||||
|
"
|
||||||
type="select"
|
type="select"
|
||||||
name="template"
|
name="template"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
|
<FormKit
|
||||||
|
v-model="formState.spec.postTemplate"
|
||||||
|
:options="postTemplates"
|
||||||
|
:label="
|
||||||
|
$t(
|
||||||
|
'core.post_category.editing_modal.fields.post_template.label'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:help="
|
||||||
|
$t('core.post_category.editing_modal.fields.post_template.help')
|
||||||
|
"
|
||||||
|
type="select"
|
||||||
|
name="postTemplate"
|
||||||
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.spec.cover"
|
v-model="formState.spec.cover"
|
||||||
:help="$t('core.post_category.editing_modal.fields.cover.help')"
|
:help="$t('core.post_category.editing_modal.fields.cover.help')"
|
||||||
|
|
|
@ -44,6 +44,12 @@ export interface CategorySpec {
|
||||||
* @memberof CategorySpec
|
* @memberof CategorySpec
|
||||||
*/
|
*/
|
||||||
'displayName': string;
|
'displayName': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof CategorySpec
|
||||||
|
*/
|
||||||
|
'postTemplate'?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {number}
|
* @type {number}
|
||||||
|
|
|
@ -366,12 +366,20 @@ core:
|
||||||
refresh_message: Regenerate slug based on display name.
|
refresh_message: Regenerate slug based on display name.
|
||||||
template:
|
template:
|
||||||
label: Custom template
|
label: Custom template
|
||||||
|
help: >-
|
||||||
|
Customize the rendering template of the category archive page, which
|
||||||
|
requires support from the theme
|
||||||
cover:
|
cover:
|
||||||
label: Cover
|
label: Cover
|
||||||
help: Theme adaptation is required to support
|
help: Theme adaptation is required to support
|
||||||
description:
|
description:
|
||||||
label: Description
|
label: Description
|
||||||
help: Theme adaptation is required to support
|
help: Theme adaptation is required to support
|
||||||
|
post_template:
|
||||||
|
label: Custom post template
|
||||||
|
help: >-
|
||||||
|
Customize the rendering template of posts in the current category,
|
||||||
|
which requires support from the theme
|
||||||
page:
|
page:
|
||||||
title: Pages
|
title: Pages
|
||||||
actions:
|
actions:
|
||||||
|
@ -728,9 +736,9 @@ core:
|
||||||
version_mismatch:
|
version_mismatch:
|
||||||
title: Version mismatch
|
title: Version mismatch
|
||||||
description: >-
|
description: >-
|
||||||
The imported configuration file version does not match the
|
The imported configuration file version does not match the current
|
||||||
current theme version, which may lead to compatibility issues.
|
theme version, which may lead to compatibility issues. Do you want
|
||||||
Do you want to continue importing?
|
to continue importing?
|
||||||
invalid_format: Invalid theme configuration file
|
invalid_format: Invalid theme configuration file
|
||||||
mismatched_theme: Configuration file does not match the selected theme
|
mismatched_theme: Configuration file does not match the selected theme
|
||||||
list_modal:
|
list_modal:
|
||||||
|
|
|
@ -366,12 +366,16 @@ core:
|
||||||
refresh_message: 根据名称重新生成别名
|
refresh_message: 根据名称重新生成别名
|
||||||
template:
|
template:
|
||||||
label: 自定义模板
|
label: 自定义模板
|
||||||
|
help: 自定义分类归档页面的渲染模版,需要主题提供支持
|
||||||
cover:
|
cover:
|
||||||
label: 封面图
|
label: 封面图
|
||||||
help: 需要主题适配以支持
|
help: 需要主题适配以支持
|
||||||
description:
|
description:
|
||||||
label: 描述
|
label: 描述
|
||||||
help: 需要主题适配以支持
|
help: 需要主题适配以支持
|
||||||
|
post_template:
|
||||||
|
label: 自定义文章模板
|
||||||
|
help: 自定义当前分类下文章的渲染模版,需要主题提供支持
|
||||||
page:
|
page:
|
||||||
title: 页面
|
title: 页面
|
||||||
actions:
|
actions:
|
||||||
|
|
|
@ -346,12 +346,16 @@ core:
|
||||||
refresh_message: 根據名稱重新生成別名
|
refresh_message: 根據名稱重新生成別名
|
||||||
template:
|
template:
|
||||||
label: 自定義模板
|
label: 自定義模板
|
||||||
|
help: 自定義分類歸檔頁面的渲染模板,需要主題提供支持
|
||||||
cover:
|
cover:
|
||||||
label: 封面圖
|
label: 封面圖
|
||||||
help: 需要主題適配以支持
|
help: 需要主題適配以支持
|
||||||
description:
|
description:
|
||||||
label: 描述
|
label: 描述
|
||||||
help: 需要主題適配以支持
|
help: 需要主題適配以支持
|
||||||
|
post_template:
|
||||||
|
label: 自定義文章模板
|
||||||
|
help: 自定義當前分類下文章的渲染模板,需要主題提供支持
|
||||||
page:
|
page:
|
||||||
title: 頁面
|
title: 頁面
|
||||||
actions:
|
actions:
|
||||||
|
|
Loading…
Reference in New Issue