diff --git a/api-docs/openapi/v3_0/aggregated.json b/api-docs/openapi/v3_0/aggregated.json index 908167ed3..52399b4ce 100644 --- a/api-docs/openapi/v3_0/aggregated.json +++ b/api-docs/openapi/v3_0/aggregated.json @@ -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" + ] } } }, diff --git a/api/src/main/java/run/halo/app/core/extension/content/Category.java b/api/src/main/java/run/halo/app/core/extension/content/Category.java index bc5dfadf1..76df38a84 100644 --- a/api/src/main/java/run/halo/app/core/extension/content/Category.java +++ b/api/src/main/java/run/halo/app/core/extension/content/Category.java @@ -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; + /** + *

Used to specify the template for the posts associated with the category.

+ *

The priority is not as high as that of the post.

+ *

If the post also specifies a template, the post's template will prevail.

+ */ + @Schema(requiredMode = NOT_REQUIRED, maxLength = 255) + private String postTemplate; + @Schema(requiredMode = REQUIRED, defaultValue = "0") private Integer priority; diff --git a/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java b/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java index a13f58575..fd7b4c2b4 100644 --- a/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java +++ b/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java @@ -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 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 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 bestMatchPost(PostPatternVariable variable) { return postsByPredicates(variable) .filter(post -> { @@ -143,10 +157,10 @@ public class PostRouteFactory implements RouteFactory { } Flux 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 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)); } diff --git a/ui/console-src/modules/contents/posts/categories/components/CategoryEditingModal.vue b/ui/console-src/modules/contents/posts/categories/components/CategoryEditingModal.vue index f2e222b70..72d468d9b 100644 --- a/ui/console-src/modules/contents/posts/categories/components/CategoryEditingModal.vue +++ b/ui/console-src/modules/contents/posts/categories/components/CategoryEditingModal.vue @@ -51,6 +51,7 @@ const formState = ref({ 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" > + - + 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: diff --git a/ui/src/locales/zh-CN.yaml b/ui/src/locales/zh-CN.yaml index 3284a164e..ba7e54e84 100644 --- a/ui/src/locales/zh-CN.yaml +++ b/ui/src/locales/zh-CN.yaml @@ -366,12 +366,16 @@ core: refresh_message: 根据名称重新生成别名 template: label: 自定义模板 + help: 自定义分类归档页面的渲染模版,需要主题提供支持 cover: label: 封面图 help: 需要主题适配以支持 description: label: 描述 help: 需要主题适配以支持 + post_template: + label: 自定义文章模板 + help: 自定义当前分类下文章的渲染模版,需要主题提供支持 page: title: 页面 actions: diff --git a/ui/src/locales/zh-TW.yaml b/ui/src/locales/zh-TW.yaml index 643931bb5..c68a1f847 100644 --- a/ui/src/locales/zh-TW.yaml +++ b/ui/src/locales/zh-TW.yaml @@ -346,12 +346,16 @@ core: refresh_message: 根據名稱重新生成別名 template: label: 自定義模板 + help: 自定義分類歸檔頁面的渲染模板,需要主題提供支持 cover: label: 封面圖 help: 需要主題適配以支持 description: label: 描述 help: 需要主題適配以支持 + post_template: + label: 自定義文章模板 + help: 自定義當前分類下文章的渲染模板,需要主題提供支持 page: title: 頁面 actions: