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
guqing 2024-06-20 16:12:07 +08:00 committed by GitHub
parent 6d3a157d35
commit b5f9010e60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 83 additions and 13 deletions

View File

@ -13499,6 +13499,10 @@
"minLength": 1,
"type": "string"
},
"postTemplate": {
"maxLength": 255,
"type": "string"
},
"priority": {
"type": "integer",
"format": "int32",
@ -13509,6 +13513,7 @@
"type": "string"
},
"template": {
"maxLength": 255,
"type": "string"
}
}
@ -17446,12 +17451,12 @@
},
"visible": {
"type": "string",
"default": "PUBLIC",
"enum": [
"PUBLIC",
"INTERNAL",
"PRIVATE"
],
"default": "PUBLIC"
]
}
}
},
@ -19101,12 +19106,12 @@
},
"visible": {
"type": "string",
"default": "PUBLIC",
"enum": [
"PUBLIC",
"INTERNAL",
"PRIVATE"
],
"default": "PUBLIC"
]
}
}
},

View File

@ -1,5 +1,6 @@
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 run.halo.app.core.extension.content.Category.KIND;
@ -53,8 +54,17 @@ public class Category extends AbstractExtension {
private String cover;
@Schema(requiredMode = NOT_REQUIRED, maxLength = 255)
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")
private Integer priority;

View File

@ -1,11 +1,14 @@
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.accept;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
@ -120,13 +123,24 @@ public class PostRouteFactory implements RouteFactory {
})
.flatMap(postVo -> {
Map<String, Object> model = ModelMapUtils.postModel(postVo);
String template = postVo.getSpec().getTemplate();
return viewNameResolver.resolveViewNameOrDefault(request, template,
DefaultTemplateEnum.POST.getValue())
return determineTemplate(request, postVo)
.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) {
return postsByPredicates(variable)
.filter(post -> {
@ -143,10 +157,10 @@ public class PostRouteFactory implements RouteFactory {
}
Flux<Post> postsByPredicates(PostPatternVariable patternVariable) {
if (StringUtils.isNotBlank(patternVariable.getName())) {
if (isNotBlank(patternVariable.getName())) {
return fetchPostsByName(patternVariable.getName());
}
if (StringUtils.isNotBlank(patternVariable.getSlug())) {
if (isNotBlank(patternVariable.getSlug())) {
return fetchPostsBySlug(patternVariable.getSlug());
}
return Flux.empty();
@ -163,7 +177,7 @@ public class PostRouteFactory implements RouteFactory {
private Flux<Post> fetchPostsBySlug(String slug) {
return queryPostPredicateResolver.getListOptions()
.flatMapMany(listOptions -> {
if (StringUtils.isNotBlank(slug)) {
if (isNotBlank(slug)) {
var other = QueryFactory.equal("spec.slug", slug);
listOptions.setFieldSelector(listOptions.getFieldSelector().andQuery(other));
}

View File

@ -51,6 +51,7 @@ const formState = ref<Category>({
description: "",
cover: "",
template: "",
postTemplate: "",
priority: 0,
children: [],
},
@ -160,6 +161,7 @@ onMounted(() => {
// custom templates
const { templates } = useThemeCustomTemplates("category");
const { templates: postTemplates } = useThemeCustomTemplates("post");
// slug
const { handleGenerateSlug } = useSlugify(
@ -243,9 +245,26 @@ const { handleGenerateSlug } = useSlugify(
:label="
$t('core.post_category.editing_modal.fields.template.label')
"
:help="
$t('core.post_category.editing_modal.fields.template.help')
"
type="select"
name="template"
></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
v-model="formState.spec.cover"
:help="$t('core.post_category.editing_modal.fields.cover.help')"

View File

@ -44,6 +44,12 @@ export interface CategorySpec {
* @memberof CategorySpec
*/
'displayName': string;
/**
*
* @type {string}
* @memberof CategorySpec
*/
'postTemplate'?: string;
/**
*
* @type {number}

View File

@ -366,12 +366,20 @@ core:
refresh_message: Regenerate slug based on display name.
template:
label: Custom template
help: >-
Customize the rendering template of the category archive page, which
requires support from the theme
cover:
label: Cover
help: Theme adaptation is required to support
description:
label: Description
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:
title: Pages
actions:
@ -728,9 +736,9 @@ core:
version_mismatch:
title: Version mismatch
description: >-
The imported configuration file version does not match the
current theme version, which may lead to compatibility issues.
Do you want to continue importing?
The imported configuration file version does not match the current
theme version, which may lead to compatibility issues. Do you want
to continue importing?
invalid_format: Invalid theme configuration file
mismatched_theme: Configuration file does not match the selected theme
list_modal:

View File

@ -366,12 +366,16 @@ core:
refresh_message: 根据名称重新生成别名
template:
label: 自定义模板
help: 自定义分类归档页面的渲染模版,需要主题提供支持
cover:
label: 封面图
help: 需要主题适配以支持
description:
label: 描述
help: 需要主题适配以支持
post_template:
label: 自定义文章模板
help: 自定义当前分类下文章的渲染模版,需要主题提供支持
page:
title: 页面
actions:

View File

@ -346,12 +346,16 @@ core:
refresh_message: 根據名稱重新生成別名
template:
label: 自定義模板
help: 自定義分類歸檔頁面的渲染模板,需要主題提供支持
cover:
label: 封面圖
help: 需要主題適配以支持
description:
label: 描述
help: 需要主題適配以支持
post_template:
label: 自定義文章模板
help: 自定義當前分類下文章的渲染模板,需要主題提供支持
page:
title: 頁面
actions: