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, "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"
} }
} }
}, },

View File

@ -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;

View File

@ -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));
} }

View File

@ -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')"

View File

@ -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}

View File

@ -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:

View File

@ -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:

View File

@ -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: