docs: update documentation for Halo 2.16 (#367)
	
		
	
				
					
				
			为 Halo 2.16.0 更新文档。 /kind documentation ```release-note None ```wan92hen-patch-1
							parent
							
								
									78416a1337
								
							
						
					
					
						commit
						7eec30763e
					
				| @ -0,0 +1,78 @@ | ||||
| { | ||||
|   "version.label": { | ||||
|     "message": "2.16", | ||||
|     "description": "The label for version 2.16" | ||||
|   }, | ||||
|   "sidebar.tutorial.category.入门": { | ||||
|     "message": "入门", | ||||
|     "description": "The label for category 入门 in sidebar tutorial" | ||||
|   }, | ||||
|   "sidebar.tutorial.category.安装指南": { | ||||
|     "message": "安装指南", | ||||
|     "description": "The label for category 安装指南 in sidebar tutorial" | ||||
|   }, | ||||
|   "sidebar.tutorial.category.云平台": { | ||||
|     "message": "云平台", | ||||
|     "description": "The label for category 云平台 in sidebar tutorial" | ||||
|   }, | ||||
|   "sidebar.tutorial.category.其他指南": { | ||||
|     "message": "其他指南", | ||||
|     "description": "The label for category 其他指南 in sidebar tutorial" | ||||
|   }, | ||||
|   "sidebar.tutorial.category.用户指南": { | ||||
|     "message": "用户指南", | ||||
|     "description": "The label for category 用户指南 in sidebar tutorial" | ||||
|   }, | ||||
|   "sidebar.tutorial.category.参与贡献": { | ||||
|     "message": "参与贡献", | ||||
|     "description": "The label for category 参与贡献 in sidebar tutorial" | ||||
|   }, | ||||
|   "sidebar.developer.category.系统开发": { | ||||
|     "message": "系统开发", | ||||
|     "description": "The label for category 系统开发 in sidebar developer" | ||||
|   }, | ||||
|   "sidebar.developer.category.插件开发": { | ||||
|     "message": "插件开发", | ||||
|     "description": "The label for category 插件开发 in sidebar developer" | ||||
|   }, | ||||
|   "sidebar.developer.category.基础": { | ||||
|     "message": "基础", | ||||
|     "description": "The label for category 基础 in sidebar developer" | ||||
|   }, | ||||
|   "sidebar.developer.category.服务端": { | ||||
|     "message": "服务端", | ||||
|     "description": "The label for category 服务端 in sidebar developer" | ||||
|   }, | ||||
|   "sidebar.developer.category.UI": { | ||||
|     "message": "UI", | ||||
|     "description": "The label for category UI in sidebar developer" | ||||
|   }, | ||||
|   "sidebar.developer.category.API 参考": { | ||||
|     "message": "API 参考", | ||||
|     "description": "The label for category API 参考 in sidebar developer" | ||||
|   }, | ||||
|   "sidebar.developer.category.扩展点": { | ||||
|     "message": "扩展点", | ||||
|     "description": "The label for category 扩展点 in sidebar developer" | ||||
|   }, | ||||
|   "sidebar.developer.category.组件": { | ||||
|     "message": "组件", | ||||
|     "description": "The label for category 组件 in sidebar developer" | ||||
|   }, | ||||
|   "sidebar.developer.category.案例和最佳实践": { | ||||
|     "message": "案例和最佳实践", | ||||
|     "description": "The label for category 案例和最佳实践 in sidebar developer" | ||||
|   }, | ||||
|   "sidebar.developer.category.主题开发": { | ||||
|     "message": "主题开发", | ||||
|     "description": "The label for category 主题开发 in sidebar developer" | ||||
|   }, | ||||
|   "sidebar.developer.category.模板变量": { | ||||
|     "message": "模板变量", | ||||
|     "description": "The label for category 模板变量 in sidebar developer" | ||||
|   }, | ||||
|   "sidebar.developer.category.Finder API": { | ||||
|     "message": "Finder API", | ||||
|     "description": "The label for category Finder API in sidebar developer" | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,16 @@ | ||||
| --- | ||||
| title: 关于文档 | ||||
| description: 关于本文档站点的一些说明 | ||||
| --- | ||||
| 
 | ||||
| :::note | ||||
| 此文档使用 [Docusaurus](https://docusaurus.io/) 搭建,感谢 [Docusaurus](https://github.com/facebook/docusaurus) 社区所做的贡献。 | ||||
| ::: | ||||
| 
 | ||||
| ## 参与贡献 | ||||
| 
 | ||||
| :::tip | ||||
| 如果你发现文档中有不正确或者需要添加的内容,非常欢迎参与到文档编辑当中。 | ||||
| ::: | ||||
| 
 | ||||
| 当前文档的仓库地址为 [halo-dev/docs](https://github.com/halo-dev/docs) ,所以你可以 fork 此仓库,修改之后提交 `Pull request` 等待我们合并即可。 | ||||
| @ -0,0 +1,28 @@ | ||||
| --- | ||||
| title: 问题反馈 | ||||
| description: 问题反馈渠道及指南 | ||||
| --- | ||||
| 
 | ||||
| :::info | ||||
| 如果您在使用过程中遇到了什么问题,您可以通过下面的方式反馈,但请尽量按照要求提出反馈。 | ||||
| ::: | ||||
| 
 | ||||
| ## GitHub Issues | ||||
| 
 | ||||
| 链接:<https://github.com/halo-dev/halo/issues> | ||||
| 
 | ||||
| 如果你在使用过程中,遇到了一些 bug 或者需要添加某些新特性,请尽量在 GitHub Issues 进行反馈,这非常有助于我们跟踪解决此问题,您也可以很方便的接收到处理状态。 | ||||
| 
 | ||||
| 建议步骤: | ||||
| 
 | ||||
| 1. 在 [Issues 列表](https://github.com/halo-dev/halo/issues) 搜索相关问题,看看是否有其他人已经提到了此问题。 | ||||
| 2. 如果当前还没有人遇到您类似的问题,那么请点击右上角的 `New issue` 按钮创建新的 issue。 | ||||
| 3. 选择正确的反馈类型。 | ||||
| 4. 请尽可能详细的按照模板填写内容。 | ||||
| 5. 点击 `Submit new issue` 提交 issue。 | ||||
| 
 | ||||
| ## Halo 官方社区 | ||||
| 
 | ||||
| 链接:<https://bbs.halo.run> | ||||
| 
 | ||||
| 此平台主要目的用于与其他 Halo 用户进行交流。但如果您对 GitHub 不是很熟悉或者没有账号,您也可以在此平台进行反馈。 | ||||
| @ -0,0 +1,36 @@ | ||||
| --- | ||||
| title: 系统结构 | ||||
| description: Halo 项目的构成 | ||||
| --- | ||||
| 
 | ||||
| [Halo](https://github.com/halo-dev/halo) 博客系统分为以下四个部分: | ||||
| 
 | ||||
| | 项目名称                                                 | 简介                                                                                                                   | | ||||
| | :------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------- | | ||||
| | [halo](https://github.com/halo-dev/halo)                 | 提供整个系统的服务,采用 [Spring Boot](https://spring.io/) 开发                                                        | | ||||
| | [halo-admin](https://github.com/halo-dev/halo-admin)     | 负责后台管理的渲染,采用 [Vue](https://vuejs.org/) 开发,已集成在 Halo 运行包内,无需独立部署。                                      | | ||||
| | [halo-comment](https://github.com/halo-dev/halo-comment) | 评论插件,采用 [Vue](https://vuejs.org/) 开发,在主题中运行方式引入构建好的 `JavaScript` 文件即可                      | | ||||
| | [halo-theme-\*](https://github.com/halo-dev)             | 主题项目集,采用 [FreeMarker](https://freemarker.apache.org/) 模板引擎编写,需要包含一些特殊的配置才能够被 halo 所使用 | | ||||
| 
 | ||||
| ## 自定义配置 | ||||
| 
 | ||||
| > 为什么要提前讲自定义配置呢?是因为在这里让大家了解到 `Halo` 的`配置方式`,以及`配置优先级`,不至于未来运行项目的时候不知道如何优雅地修改配置。 | ||||
| 
 | ||||
| `Halo` 配置目录优先级如下(从上到下优先级越来越小,上层的配置将会覆盖下层): | ||||
| 
 | ||||
| - `Halo` 自定义配置 | ||||
|   - file:~/.halo/ | ||||
|   - file:~/.halo-dev/ | ||||
| - `Spring Boot` 默认配置 | ||||
|   - file:./config/ | ||||
|   - file:./ | ||||
|   - classpath:/config/ | ||||
|   - classpath:/ | ||||
| 
 | ||||
| > 参考: [Application Property Files](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-application-property-files) | ||||
| 
 | ||||
| 在开发的时候,希望大家能够在 `~/halo-dev/application.yml` 中进行添加自定义配置。当然后面也会讲到如何用`运行参数` 和 `VM options` 进行控制配置,届时可根据具体情况进行选择。 | ||||
| 
 | ||||
| :::warning | ||||
| 开发的时候,我们不建议直接更改`项目源码`中的所包含的`配置文件`,包括 `application.yml`、`application-dev.yml`、`application-test.yml` 和 `application-user.yml`。 | ||||
| ::: | ||||
| @ -0,0 +1,55 @@ | ||||
| --- | ||||
| title: 附件存储 | ||||
| description: 为附件存储方式提供的扩展点,可用于自定义附件存储方式。 | ||||
| --- | ||||
| 附件存储策略扩展点支持扩展附件的上传和存储方式,如将附件存储到第三方云存储服务中。 | ||||
| 
 | ||||
| 扩展点接口如下: | ||||
| 
 | ||||
| ```java | ||||
| public interface AttachmentHandler extends ExtensionPoint { | ||||
| 
 | ||||
|     Mono<Attachment> upload(UploadContext context); | ||||
| 
 | ||||
|     Mono<Attachment> delete(DeleteContext context); | ||||
| 
 | ||||
|     default Mono<URI> getSharedURL(Attachment attachment, | ||||
|       Policy policy, | ||||
|       ConfigMap configMap, | ||||
|       Duration ttl) { | ||||
|       return Mono.empty(); | ||||
|     } | ||||
|      | ||||
|     default Mono<URI> getPermalink(Attachment attachment, | ||||
|         Policy policy, | ||||
|         ConfigMap configMap) { | ||||
|         return Mono.empty(); | ||||
|     } | ||||
| ``` | ||||
| 
 | ||||
| - `upload` 方法用于上传附件,返回值为 `Mono<Attachment>`,其中 `Attachment` 为上传成功后的附件对象。 | ||||
| - `delete` 方法用于删除附件,返回值为 `Mono<Attachment>`,其中 `Attachment` 为删除后的附件对象。 | ||||
| - `getSharedURL` 方法用于获取附件的共享链接,返回值为 `Mono<URI>`,其中 `URI` 为附件的共享链接。 | ||||
| - `getPermalink` 方法用于获取附件的永久链接,返回值为 `Mono<URI>`,其中 `URI` 为附件的永久链接。 | ||||
| 
 | ||||
| `AttachmentHandler` 对应的 `ExtensionPointDefinition` 如下: | ||||
| 
 | ||||
| ```yaml | ||||
| apiVersion: plugin.halo.run/v1alpha1 | ||||
| kind: ExtensionPointDefinition | ||||
| metadata: | ||||
|   name: attachment-handler | ||||
| spec: | ||||
|   className: run.halo.app.core.extension.attachment.endpoint.AttachmentHandler | ||||
|   displayName: AttachmentHandler | ||||
|   type: MULTI_INSTANCE | ||||
|   description: "Provide extension points for attachment storage strategies" | ||||
| ``` | ||||
| 
 | ||||
| 即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `attachment-handler`。 | ||||
| 
 | ||||
| 可以参考以下项目示例: | ||||
| 
 | ||||
| - [S3 对象存储协议的存储插件](https://github.com/halo-dev/plugin-s3) | ||||
| - [阿里云 OSS 的存储策略插件](https://github.com/halo-sigs/plugin-alioss) | ||||
| - [又拍云 OSS 的存储策略](https://github.com/AirboZH/plugin-uposs) | ||||
| @ -0,0 +1,83 @@ | ||||
| --- | ||||
| title: 评论主体展示 | ||||
| description: 用于在管理端评论列表中展示评论的主体内容。 | ||||
| --- | ||||
| 
 | ||||
| 评论主体扩展点用于在管理端评论列表中展示评论的主体内容,评论自定义模型是 Halo 主应用提供的,如果你需要使用评论自定义模型,那么评论会统一 | ||||
| 展示在管理后台的评论列表中,这时就需要通过评论主体扩展点来展示评论的主体内容便于跳转到对应的页面,如果没有实现该扩展点,那么评论列表中对应的评论的主体会显示为“未知”。 | ||||
| 
 | ||||
| ```java | ||||
| public interface CommentSubject<T extends Extension> extends ExtensionPoint { | ||||
| 
 | ||||
|     Mono<T> get(String name); | ||||
| 
 | ||||
|     default Mono<SubjectDisplay> getSubjectDisplay(String name) { | ||||
|         return Mono.empty(); | ||||
|     } | ||||
| 
 | ||||
|     boolean supports(Ref ref); | ||||
| 
 | ||||
|     record SubjectDisplay(String title, String url, String kindName) { | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| - `get` 方法用于获取评论主体,参数 `name` 是评论主体的自定义模型对象的名称,返回值为 `Mono<T>`,其中 `T` 为评论主体对象,它是使用评论的那个自定义模型。 | ||||
| - `getSubjectDisplay` 方法用于获取评论主体的展示信息,返回值为 `Mono<SubjectDisplay>`,其中 `SubjectDisplay` 为评论主体的展示信息,包含标题、链接和类型名称,用于在主题端展示评论主体的信息。 | ||||
| - `supports` 方法用于判断是否支持该评论主体,返回值为 `boolean`,如果支持则返回 `true`,否则返回 `false`。 | ||||
| 
 | ||||
| 实现该扩展点后,评论列表会通过 `get` 方法将主体的自定义模型对象带到评论列表中,可以配置前端的扩展点来决定如何展示评论主体的信息,参考:[UI 评论来源显示](../../ui/extension-points//comment-subject-ref-create.md) | ||||
| 
 | ||||
| 例如对于文章是支持评论的,所以文章的评论主体扩展点实现如下: | ||||
| 
 | ||||
| ```java | ||||
| public class PostCommentSubject implements CommentSubject<Post> { | ||||
| 
 | ||||
|     private final ReactiveExtensionClient client; | ||||
|     private final ExternalLinkProcessor externalLinkProcessor; | ||||
| 
 | ||||
|     @Override | ||||
|     public Mono<Post> get(String name) { | ||||
|         return client.fetch(Post.class, name); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Mono<SubjectDisplay> getSubjectDisplay(String name) { | ||||
|         return get(name) | ||||
|             .map(post -> { | ||||
|                 var url = externalLinkProcessor | ||||
|                     .processLink(post.getStatusOrDefault().getPermalink()); | ||||
|                 return new SubjectDisplay(post.getSpec().getTitle(), url, "文章"); | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean supports(Ref ref) { | ||||
|         Assert.notNull(ref, "Subject ref must not be null."); | ||||
|         GroupVersionKind groupVersionKind = | ||||
|             new GroupVersionKind(ref.getGroup(), ref.getVersion(), ref.getKind()); | ||||
|         return GroupVersionKind.fromExtension(Post.class).equals(groupVersionKind); | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| `CommentSubject` 对应的 `ExtensionPointDefinition` 如下: | ||||
| 
 | ||||
| ```yaml | ||||
| apiVersion: plugin.halo.run/v1alpha1 | ||||
| kind: ExtensionPointDefinition | ||||
| metadata: | ||||
|   name: comment-subject | ||||
| spec: | ||||
|   className: run.halo.app.content.comment.CommentSubject | ||||
|   displayName: CommentSubject | ||||
|   type: MULTI_INSTANCE | ||||
|   description: "Provide extension points for comment subject display" | ||||
| ``` | ||||
| 
 | ||||
| 即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `comment-subject`。 | ||||
| 
 | ||||
| 可以参考其他使用该扩展点的项目示例: | ||||
| 
 | ||||
| - [Halo 自定义页面评论主体](https://github.com/halo-dev/halo/blob/main/application/src/main/java/run/halo/app/content/comment/SinglePageCommentSubject.java) | ||||
| - [瞬间的评论主体](https://github.com/halo-sigs/plugin-moments/blob/096b1b3e4a2ca44b6f858ba1181b62eeff64a139/src/main/java/run/halo/moments/MomentCommentSubject.java#L25) | ||||
| @ -0,0 +1,65 @@ | ||||
| --- | ||||
| title: 扩展点 | ||||
| description: Halo 服务端为插件提供的扩展点接口 | ||||
| --- | ||||
| 
 | ||||
| 术语: | ||||
| 
 | ||||
| - 扩展点 | ||||
|   - 由 Halo 定义的用于添加特定功能的接口。 | ||||
|   - 扩展点应该在服务的核心功能和它所认为的集成之间的交叉点上。 | ||||
|   - 扩展点是对服务的扩充,但不是影响服务的核心功能:区别在于,如果没有其核心功能,服务就无法运行,而扩展点对于特定的配置可能至关重要该服务最终是可选的。 | ||||
|   - 扩展点应该小且可组合,并且在相互配合使用时,可为 Halo 提供比其各部分总和更大的价值。 | ||||
| - 扩展 | ||||
|   - 扩展点的一种具体实现。 | ||||
| 
 | ||||
| 使用 Halo 扩展点的必要步骤是: | ||||
| 
 | ||||
| 1. 实现扩展点接口,然后标记上 `@Component` 注解。 | ||||
| 2. 对该扩展点的实现类进行 `ExtensionDefinition` 自定义模型对象的声明。 | ||||
| 
 | ||||
| 例如: 实现一个通知器的扩展,首先 `implements` ReactiveNotifier 扩展点接口: | ||||
| 
 | ||||
| ```java | ||||
| @Component | ||||
| public class EmailNotifier implements ReactiveNotifier { | ||||
|    @Override | ||||
|     public Mono<Void> notify(NotificationContext context) { | ||||
|         // Send notification | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| 然后声明一个 `ExtensionDefinition` 自定义模型对象: | ||||
| 
 | ||||
| ```yaml | ||||
| apiVersion: plugin.halo.run/v1alpha1 | ||||
| kind: ExtensionDefinition | ||||
| metadata: | ||||
|   name: halo-email-notifier # 指定一个扩展定义的名称 | ||||
| spec: | ||||
|   # 扩展的全限定类名 | ||||
|   className: run.halo.app.notification.EmailNotifier | ||||
|   # 所实现的扩展点定义的自定义模型对象名称 | ||||
|   extensionPointName: reactive-notifier | ||||
|   # 扩展名称用于展示给用户 | ||||
|   displayName: "EmailNotifier" | ||||
|   # 扩展的简要描述,用于让用户了解该扩展的作用 | ||||
|   description: "Support sending notifications to users via email" | ||||
| ``` | ||||
| 
 | ||||
| :::tip Note | ||||
| 单实例或多实例的扩展点需要声明对应的 `ExtensionPointDefinition` 自定义模型对象被称之为扩展点定义,用于描述该扩展点的信息,例如:扩展点的名称、描述、扩展点的类型等。 | ||||
| 
 | ||||
| 单实例或多实例扩展点的实现也必须声明一个对应的 `ExtensionDefinition` 自定义模型对象被称之为扩展定义,用于描述该扩展的信息,例如:扩展的名称、描述、对应扩展点的对象名称等。 | ||||
| ::: | ||||
| 
 | ||||
| 关于如何在插件中声明自定义模型对象请参考:[自定义模型](../../server/extension.md#declare-extension-object) | ||||
| 
 | ||||
| 以下是目前已支持的扩展点列表: | ||||
| 
 | ||||
| ```mdx-code-block | ||||
| import DocCardList from '@theme/DocCardList'; | ||||
| 
 | ||||
| <DocCardList /> | ||||
| ``` | ||||
| @ -0,0 +1,43 @@ | ||||
| --- | ||||
| title: 主题端文章内容处理 | ||||
| description: 提供扩展主题端文章内容处理的方法,干预文章内容的渲染。 | ||||
| --- | ||||
| 
 | ||||
| 主题端文章内容处理扩展点用于干预文章内容的渲染,例如:在文章内容中添加广告、添加版权信息等。 | ||||
| 
 | ||||
| ```java | ||||
| public interface ReactivePostContentHandler extends ExtensionPoint { | ||||
| 
 | ||||
|     Mono<PostContentContext> handle(@NonNull PostContentContext postContent); | ||||
| 
 | ||||
|     @Data | ||||
|     @Builder | ||||
|     class PostContentContext { | ||||
|         private Post post; | ||||
|         private String content; | ||||
|         private String raw; | ||||
|         private String rawType; | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| `handle` 方法用于处理文章内容,参数 `postContent` 为文章内容上下文,包含文章自定义模型对象、文章 html 内容、原始内容、原始内容类型等信息。 | ||||
| 
 | ||||
| `ReactivePostContentHandler` 对应的 `ExtensionPointDefinition` 如下: | ||||
| 
 | ||||
| ```yaml | ||||
| apiVersion: plugin.halo.run/v1alpha1 | ||||
| kind: ExtensionPointDefinition | ||||
| metadata: | ||||
|   name: reactive-post-content-handler | ||||
| spec: | ||||
|   className: run.halo.app.theme.ReactivePostContentHandler | ||||
|   displayName: ReactivePostContentHandler | ||||
|   type: MULTI_INSTANCE | ||||
|   description: "Provides a way to extend the post content to be displayed on the theme-side." | ||||
| ``` | ||||
| 
 | ||||
| 即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `reactive-post-content-handler`。 | ||||
| 
 | ||||
| 使用案例可以参考:[WebP Cloud 插件](https://github.com/webp-sh/halo-plugin-webp-cloud/blob/a6069dfa78931de0d5b5dfe98fdd18a0da75b09f/src/main/java/se/webp/plugin/WebpCloudPostContentHandler.java#L17) | ||||
| 它的作用是处理主题端文章内容中的所有图片的地址,将其替换为一个 WebP Cloud 的代理地址,从而实现文章内容中的图片都使用 WebP 格式。 | ||||
| @ -0,0 +1,38 @@ | ||||
| --- | ||||
| title: 主题端自定义页面内容处理 | ||||
| description: 提供扩展主题端自定义页面内容处理的方法,干预自定义页面内容的渲染。 | ||||
| --- | ||||
| 
 | ||||
| 主题端自定义页面内容处理扩展点,作用同 [主题端文章内容处理](./post-content.md) 扩展点,只是作用于自定义页面。 | ||||
| 
 | ||||
| ```java | ||||
| public interface ReactiveSinglePageContentHandler extends ExtensionPoint { | ||||
|     | ||||
|     Mono<SinglePageContentContext> handle(@NonNull SinglePageContentContext singlePageContent); | ||||
| 
 | ||||
|     @Data | ||||
|     @Builder | ||||
|     class SinglePageContentContext { | ||||
|         private SinglePage singlePage; | ||||
|         private String content; | ||||
|         private String raw; | ||||
|         private String rawType; | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| `ReactiveSinglePageContentHandler` 对应的 `ExtensionPointDefinition` 如下: | ||||
| 
 | ||||
| ```yaml | ||||
| apiVersion: plugin.halo.run/v1alpha1 | ||||
| kind: ExtensionPointDefinition | ||||
| metadata: | ||||
|   name: reactive-singlepage-content-handler | ||||
| spec: | ||||
|   className: run.halo.app.theme.ReactiveSinglePageContentHandler | ||||
|   displayName: ReactiveSinglePageContentHandler | ||||
|   type: MULTI_INSTANCE | ||||
|   description: "Provides a way to extend the single page content to be displayed on the theme-side." | ||||
| ``` | ||||
| 
 | ||||
| 即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `reactive-singlepage-content-handler`。 | ||||
| @ -0,0 +1,30 @@ | ||||
| --- | ||||
| title: 用户名密码认证管理器 | ||||
| description: 提供扩展用户名密码的身份验证的方法 | ||||
| --- | ||||
| 
 | ||||
| 用户名密码认证管理器扩展点用于替换 Halo 默认的用户名密码认证管理器实现,例如:使用第三方的身份验证服务,一个例子是 LDAP。 | ||||
| 
 | ||||
| ```java | ||||
| public interface UsernamePasswordAuthenticationManager extends ExtensionPoint { | ||||
|   Mono<Authentication> authenticate(Authentication authentication); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| `UsernamePasswordAuthenticationManager` 对应的 `ExtensionPointDefinition` 如下: | ||||
| 
 | ||||
| ```yaml | ||||
| apiVersion: plugin.halo.run/v1alpha1 | ||||
| kind: ExtensionPointDefinition | ||||
| metadata: | ||||
|   name: username-password-authentication-manager | ||||
| spec: | ||||
|   className: run.halo.app.security.authentication.login.UsernamePasswordAuthenticationManager | ||||
|   displayName: Username password authentication manager | ||||
|   type: SINGLETON | ||||
|   description: "Provides a way to extend the username password authentication." | ||||
| ``` | ||||
| 
 | ||||
| 即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName` 为 `username-password-authentication-manager`。 | ||||
| 
 | ||||
| 可以参考的实现示例 [TOTP 认证](https://github.com/halo-dev/halo/blob/86e688a15d05c084021b6ba5e75d56a8813c3813/application/src/main/java/run/halo/app/security/authentication/twofactor/totp/TotpAuthenticationFilter.java#L84) | ||||
| @ -0,0 +1,60 @@ | ||||
| --- | ||||
| title: 为主题提供数据 | ||||
| description: 了解如何为主题提供更多获取和使用数据的方法。 | ||||
| --- | ||||
| 
 | ||||
| 当你在插件中创建了自己的自定义模型时,你可能需要在主题模板中使用这些数据。或者,你提供一些额外的数据,以便主题可以使用它们,你可以通过创建一个自定义的 `finder` 来实现这一点。 | ||||
| 
 | ||||
| ## 创建一个 Finder | ||||
| 
 | ||||
| 首先,你需要创建一个 `interface`,并在其中定义你需要提供给主题获取数据的方法,方法的返回值可以是 `Mono` 或 `Flux` 类型,例如: | ||||
| 
 | ||||
| ```java | ||||
| public interface LinkFinder { | ||||
|   Mono<LinkVo> get(String linkName); | ||||
| 
 | ||||
|   Flux<LinkVo> listAll(); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| 然后写一个实现类,实现这个 `interface`,并在类上添加 `@Finder` 注解,例如: | ||||
| 
 | ||||
| ```java | ||||
| import run.halo.app.theme.finders.Finder; | ||||
| 
 | ||||
| @Finder("myPluginLinkFinder") | ||||
| public class LinkFinderImpl implements LinkFinder { | ||||
|   @Override | ||||
|   public Mono<LinkVo> get(String linkName) { | ||||
|     // ... | ||||
|   } | ||||
| 
 | ||||
|   @Override | ||||
|   public Flux<LinkVo> listAll() { | ||||
|     // ... | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| `@Finder` 注解的值是你在主题中使用的名称,例如,你可以在主题中使用 `myPluginLinkFinder.get('a-link-name')` 来获取数据,`myPluginLinkFinder` 就是你在 `@Finder` 注解中定义的名称。 | ||||
| 
 | ||||
| ## Finder 命名 | ||||
| 
 | ||||
| 为了避免与其他插件的 `finder` 名称冲突,建议在 `@Finder` 注解中添加一个你插件名称的前缀作为 `finder` 名称且名称需要是驼峰式的,不能包含除了 `_` 之外的其他特殊字符。 | ||||
| 
 | ||||
| 例如,你的插件名称是 `my_plugin`,你需要为主题提供一个获取链接的 `finder`,那么你可以这样定义 `@Finder` 注解: | ||||
| 
 | ||||
| ```java | ||||
| @Finder("myPluginLinkFinder") | ||||
| ``` | ||||
| 
 | ||||
| ## 使用 Finder | ||||
| 
 | ||||
| 在主题中,你可以通过 `finder` 名称和方法名及对应的参数来获取数据,例如: | ||||
| 
 | ||||
| ```html | ||||
| <div th:text="${myPluginLinkFinder.listAll()}"> | ||||
| </div> | ||||
| ``` | ||||
| 
 | ||||
| 模板语法参考:[Thymeleaf](https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#standard-expression-syntax)。 | ||||
| @ -0,0 +1,36 @@ | ||||
| --- | ||||
| title: 静态资源代理 | ||||
| description: 了解如果使用静态资源代理来访问插件中的静态资源 | ||||
| --- | ||||
| 
 | ||||
| 插件中的静态资源如图片等如果想被外部访问到,需要放到 `src/main/resources` 目录下,并通过创建 `ReverseProxy` 自定义模型对象来进行静态资源代理访问。 | ||||
| 
 | ||||
| 例如 `src/main/resources` 下的 `static` 目录下有一张 `halo.jpg`: | ||||
| 
 | ||||
| 1. 首先需要在 `src/main/resources/extensions` 下创建一个 `yaml`,文件名可以任意。 | ||||
| 2. 声明 `ReverseProxy` 对象如下: | ||||
| 
 | ||||
|   ```yaml | ||||
|   apiVersion: plugin.halo.run/v1alpha1 | ||||
|   kind: ReverseProxy | ||||
|   metadata: | ||||
|     # 为了避免与其他插件冲突,推荐带上插件名称前缀 | ||||
|     name: my-plugin-fake-reverse-proxy | ||||
|   rules: | ||||
|     - path: /res/** | ||||
|       file: | ||||
|         directory: static | ||||
|         # 如果想代理 static 下所有静态资源则省略 filename 配置 | ||||
|         filename: halo.jpg | ||||
|   ``` | ||||
| 
 | ||||
| 插件启动后会根据 `/plugins/{plugin-name}/assets/**` 规则生成访问路径, | ||||
| 因此该 `ReverseProxy` 的访问路径为: `/plugins/my-plugin/assets/res/halo.jpg`。 | ||||
| 
 | ||||
| - `rules` 下可以添加多组规则。 | ||||
| - `path` 为路径前缀。 | ||||
| - `file` 表示访问文件系统,目前暂时仅支持这一种。 | ||||
| - `directory` 表示要代理的目标文件目录,它相对于 `src/main/resources/` 目录。 | ||||
| - `filename` 表示要代理的目标文件名。 | ||||
| 
 | ||||
| `directory` 和 `filename` 都是可选的,但必须至少有一个被配置。 | ||||
| @ -0,0 +1,55 @@ | ||||
| --- | ||||
| title: AnnotationsForm | ||||
| description: 元数据表单组件 | ||||
| --- | ||||
| 
 | ||||
| 此组件用于提供统一的 [Annotations 表单](../../../../annotations-form.md),可以根据 `group` 和 `kind` 属性自动渲染对应的表单项。 | ||||
| 
 | ||||
| ## 使用示例 | ||||
| 
 | ||||
| ```html | ||||
| <script lang="ts" setup> | ||||
| import { ref } from "vue" | ||||
| 
 | ||||
| const annotationsFormRef = ref() | ||||
| const currentAnnotations = ref() | ||||
| 
 | ||||
| function handleSubmit () { | ||||
|   annotationsFormRef.value?.handleSubmit(); | ||||
|   await nextTick(); | ||||
| 
 | ||||
|   const { customAnnotations, annotations, customFormInvalid, specFormInvalid } = | ||||
|     annotationsFormRef.value || {}; | ||||
| 
 | ||||
|   // 表单验证不通过 | ||||
|   if (customFormInvalid || specFormInvalid) { | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   // 合并自定义数据和表单提供的数据 | ||||
|   const newAnnotations = { | ||||
|     ...annotations, | ||||
|     ...customAnnotations, | ||||
|   }; | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <AnnotationsForm | ||||
|     ref="annotationsFormRef" | ||||
|     :value="currentAnnotations" | ||||
|     kind="Post" | ||||
|     group="content.halo.run" | ||||
|   /> | ||||
| 
 | ||||
|   <VButton @click="handleSubmit">提交</VButton> | ||||
| </template> | ||||
| ``` | ||||
| 
 | ||||
| ## Props | ||||
| 
 | ||||
| | 属性名  | 类型                               | 默认值  | 描述                                      | | ||||
| |---------|------------------------------------|---------|-----------------------------------------| | ||||
| | `group` | string                             | 无,必填 | 定义组件所属的分组。                       | | ||||
| | `kind`  | string                             | 无,必填 | 定义组件的种类。                           | | ||||
| | `value` | { [key: string]: string; } \| null | null    | 可选,包含键值对的对象或空值,用于存储数据。 | | ||||
| @ -0,0 +1,25 @@ | ||||
| --- | ||||
| title: AttachmentFileTypeIcon | ||||
| description: 附件文件类型图标组件 | ||||
| --- | ||||
| 
 | ||||
| 此组件用于根据文件名显示文件类型图标。 | ||||
| 
 | ||||
| ## 使用示例 | ||||
| 
 | ||||
| ```html | ||||
| <script lang="ts" setup></script> | ||||
| 
 | ||||
| <template> | ||||
|   <AttachmentFileTypeIcon fileName="example.png" /> | ||||
| </div> | ||||
| ``` | ||||
| 
 | ||||
| ## Props | ||||
| 
 | ||||
| | 属性名       | 类型                | 默认值    | 描述                                 | | ||||
| |--------------|---------------------|-----------|------------------------------------| | ||||
| | `fileName`   | string \| undefined | undefined | 文件名,可以是字符串或未定义。         | | ||||
| | `displayExt` | boolean             | true      | 可选,是否显示文件扩展名,默认为 true。 | | ||||
| | `width`      | number              | 10        | 可选,组件宽度,默认为 10。             | | ||||
| | `height`     | number              | 10        | 可选,组件高度,默认为 10。             | | ||||
| @ -0,0 +1,50 @@ | ||||
| --- | ||||
| title: AttachmentSelectorModal | ||||
| description: 附件选择组件 | ||||
| --- | ||||
| 
 | ||||
| 此组件用于调出附件选择器,以供用户选择附件。 | ||||
| 
 | ||||
| :::info 注意 | ||||
| 此组件当前仅在 Console 中可用。 | ||||
| ::: | ||||
| 
 | ||||
| ## 使用示例 | ||||
| 
 | ||||
| ```html | ||||
| <script lang="ts" setup> | ||||
| import { ref } from "vue" | ||||
| 
 | ||||
| const visible = ref(false) | ||||
| 
 | ||||
| function onAttachmentSelect (attachments: AttachmentLike[]) { | ||||
|   console.log(attachments) | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <VButton @click="visible = true">选择附件</VButton> | ||||
| 
 | ||||
|   <AttachmentSelectorModal | ||||
|     v-model:visible="visible" | ||||
|     @select="onAttachmentSelect" | ||||
|   /> | ||||
| </template> | ||||
| ``` | ||||
| 
 | ||||
| ## Props | ||||
| 
 | ||||
| | 属性名    | 类型     | 默认值        | 描述                       | | ||||
| |-----------|----------|---------------|--------------------------| | ||||
| | `visible` | boolean  | false         | 控制组件是否可见。          | | ||||
| | `accepts` | string[] | () => ["*/*"] | 可选,定义可接受的文件类型。 | | ||||
| | `min`     | number   | undefined     | 可选,定义最小选择数量。     | | ||||
| | `max`     | number   | undefined     | 可选,定义最大选择数量。     | | ||||
| 
 | ||||
| ## Emits | ||||
| 
 | ||||
| | 事件名称       | 参数                                               | 描述                  | | ||||
| |----------------|---------------------------------------------------|---------------------| | ||||
| | update:visible | `visible`: boolean 类型,表示可见状态。              | 当可见状态更新时触发。 | | ||||
| | close          | 无                                                 | 当弹框关闭时触发。     | | ||||
| | select         | `attachments`: AttachmentLike[] 类型,表示附件数组。 | 当选择确定按钮时触发。 | | ||||
| @ -0,0 +1,18 @@ | ||||
| --- | ||||
| title: FilterCleanButton | ||||
| description: 过滤器清除按钮组件 | ||||
| --- | ||||
| 
 | ||||
| ## 使用示例 | ||||
| 
 | ||||
| ```html | ||||
| <script lang="ts" setup> | ||||
| function onClear () { | ||||
|   console.log("clear") | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <FilterCleanButton @click="onClear" /> | ||||
| </template> | ||||
| ``` | ||||
| @ -0,0 +1,48 @@ | ||||
| --- | ||||
| title: FilterDropdown | ||||
| description: 过滤器下拉组件 | ||||
| --- | ||||
| 
 | ||||
| 此组件为通用的下拉筛选组件,可以接收一个对象数组作为选项,并使用 `v-model` 绑定选择的值。 | ||||
| 
 | ||||
| ## 使用示例 | ||||
| 
 | ||||
| ```html | ||||
| <script lang="ts" setup> | ||||
| import { ref } from "vue" | ||||
| 
 | ||||
| const value = ref("") | ||||
| const items = [ | ||||
|   { | ||||
|     label: "最近创建", | ||||
|     value: "creationTimestamp,desc" | ||||
|   }, | ||||
|   { | ||||
|     label: "较晚创建", | ||||
|     value: "creationTimestamp,asc" | ||||
|   } | ||||
| ] | ||||
| 
 | ||||
| </script> | ||||
| <template> | ||||
|   <FilterDropdown | ||||
|     v-model="value" | ||||
|     label="排序" | ||||
|     :items="items" | ||||
|   /> | ||||
| </template> | ||||
| ``` | ||||
| 
 | ||||
| ## Props | ||||
| 
 | ||||
| | 属性名       | 类型                                                      | 默认值    | 描述                                               | | ||||
| |--------------|-----------------------------------------------------------|-----------|--------------------------------------------------| | ||||
| | `items`      | { label: string; value?: string \| boolean \| number; }[] | 无,必填   | 包含 `label` 和可选 `value` 的对象数组。            | | ||||
| | `label`      | string                                                    | 无,必填   | 组件的标签文本。                                    | | ||||
| | `modelValue` | string \| boolean \| number                               | undefined | 可选,用于绑定到组件的值,可以是字符串、布尔值或数字。 | | ||||
| 
 | ||||
| ## Emits | ||||
| 
 | ||||
| | 事件名称          | 参数                                                   | 描述                | | ||||
| |-------------------|--------------------------------------------------------|-------------------| | ||||
| | update:modelValue | `modelValue`: string \| boolean \| number \| undefined | 当模型值更新时触发。 | | ||||
| @ -0,0 +1,26 @@ | ||||
| --- | ||||
| title: HasPermission | ||||
| description: 权限判断组件 | ||||
| --- | ||||
| 
 | ||||
| 此组件用于根据权限控制元素的显示与隐藏。 | ||||
| 
 | ||||
| ## 使用方式 | ||||
| 
 | ||||
| ```html | ||||
| <script lang="ts" setup> | ||||
| import { VButton } from "@halo-dev/components" | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <HasPermission :permissions="['system:posts:manage']"> | ||||
|     <VButton type="danger">删除</VButton> | ||||
|   </HasPermission> | ||||
| </template> | ||||
| ``` | ||||
| 
 | ||||
| ## Props | ||||
| 
 | ||||
| | 属性名        | 类型     | 默认值  | 描述                    | | ||||
| |---------------|----------|------|-----------------------| | ||||
| | `permissions` | string[] | 无,必填 | 定义组件所需的权限列表。 | | ||||
| @ -0,0 +1,42 @@ | ||||
| --- | ||||
| title: 组件 | ||||
| description: 在 Halo UI 中可使用的组件。 | ||||
| --- | ||||
| 
 | ||||
| 此文档将介绍所有在插件中可用的组件,以及它们的使用方法和区别。 | ||||
| 
 | ||||
| ## 基础组件库 | ||||
| 
 | ||||
| 我们为 Halo 的前端封装了一个基础组件的库,你可以在插件中使用这些组件。 | ||||
| 
 | ||||
| 安装方式: | ||||
| 
 | ||||
| ```bash | ||||
| pnpm install @halo-dev/components | ||||
| ``` | ||||
| 
 | ||||
| 在 Vue 组件中: | ||||
| 
 | ||||
| ```html | ||||
| <script lang="ts" setup> | ||||
| import { VButton } from "@halo-dev/components"; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <VButton>Hello</VButton> | ||||
| </template> | ||||
| ``` | ||||
| 
 | ||||
| 所有可用的基础组件以及文档可查阅:<https://halo-ui-components.pages.dev> | ||||
| 
 | ||||
| ## 业务组件和指令 | ||||
| 
 | ||||
| 除了基础组件库,我们还为 Halo 的前端封装了一些业务组件和指令,这些组件已经在全局注册,你可以直接在插件中使用这些组件和指令。 | ||||
| 
 | ||||
| 以下是所有可用的业务组件和指令: | ||||
| 
 | ||||
| ```mdx-code-block | ||||
| import DocCardList from '@theme/DocCardList'; | ||||
| 
 | ||||
| <DocCardList /> | ||||
| ``` | ||||
| @ -0,0 +1,33 @@ | ||||
| --- | ||||
| title: SearchInput | ||||
| description: 搜索输入框组件 | ||||
| --- | ||||
| 
 | ||||
| 此组件适用于关键词搜索场景,输入数据的过程中不会触发搜索,只有在输入完成后,点击回车才会触发搜索。 | ||||
| 
 | ||||
| ## 使用方式 | ||||
| 
 | ||||
| ```html | ||||
| <script lang="ts" setup> | ||||
| import { ref } from "vue" | ||||
| 
 | ||||
| const keyword = ref("") | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <SearchInput v-model="keyword" placeholder="请输入关键字" /> | ||||
| </template> | ||||
| ``` | ||||
| 
 | ||||
| ## Props | ||||
| 
 | ||||
| | 属性名        | 类型   | 默认值    | 描述                               | | ||||
| |---------------|--------|-----------|----------------------------------| | ||||
| | `placeholder` | string | undefined | 可选,用于指定输入字段的占位符文本。 | | ||||
| | `modelValue`  | string | 无,必填   | 用于绑定输入字段的值。              | | ||||
| 
 | ||||
| ## Emits | ||||
| 
 | ||||
| | 事件名称          | 参数                                  | 描述                | | ||||
| |-------------------|-------------------------------------|-------------------| | ||||
| | update:modelValue | `modelValue`: string 类型,表示模型值。 | 当模型值更新时触发。 | | ||||
| @ -0,0 +1,36 @@ | ||||
| --- | ||||
| title: VCodemirror | ||||
| description: 代码编辑器组件 | ||||
| --- | ||||
| 
 | ||||
| 此组件封装了 Codemirror 代码编辑器,适用于一些需要编辑代码的场景。 | ||||
| 
 | ||||
| ## 使用方式 | ||||
| 
 | ||||
| ```html | ||||
| <script lang="ts" setup> | ||||
| import { ref } from "vue" | ||||
| 
 | ||||
| const value = ref("") | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <VCodemirror v-model="value" height="300px" language="html" /> | ||||
| </template> | ||||
| ``` | ||||
| 
 | ||||
| ## Props | ||||
| 
 | ||||
| | 属性名       | 类型                                            | 默认值   | 描述                                        | | ||||
| |--------------|-------------------------------------------------|----------|-------------------------------------------| | ||||
| | `modelValue` | string                                          | ""       | 可选,绑定到组件的字符串值,默认为空字符串。   | | ||||
| | `height`     | string                                          | auto     | 可选,组件的高度,默认为 `"auto"`。            | | ||||
| | `language`   | keyof typeof presetLanguages \| LanguageSupport | yaml     | 代码编辑器的语言支持,默认为 `"yaml"`。       | | ||||
| | `extensions` | EditorStateConfig["extensions"]                 | () => [] | 可选,编辑器状态配置的扩展,默认为一个空数组。 | | ||||
| 
 | ||||
| ## Emits | ||||
| 
 | ||||
| | 事件名称          | 参数                               | 描述                | | ||||
| |-------------------|----------------------------------|-------------------| | ||||
| | update:modelValue | `value`: string 类型,表示模型值。   | 当模型值更新时触发。 | | ||||
| | change            | `value`: string 类型,表示变更的值。 | 当值发生变化时触发。 | | ||||
| @ -0,0 +1,18 @@ | ||||
| --- | ||||
| title: v-permission | ||||
| description: 权限指令 | ||||
| --- | ||||
| 
 | ||||
| 与 [HasPermission](./has-permission.md) 组件相同,此指令也是用于根据权限控制元素的显示与隐藏。 | ||||
| 
 | ||||
| ## 使用方式 | ||||
| 
 | ||||
| ```html | ||||
| <script lang="ts" setup> | ||||
| import { VButton } from "@halo-dev/components" | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <VButton type="danger" v-permission="['system:posts:manage']">删除</VButton> | ||||
| </template> | ||||
| ``` | ||||
| @ -0,0 +1,18 @@ | ||||
| --- | ||||
| title: v-tooltip | ||||
| description: Tooltip 指令 | ||||
| --- | ||||
| 
 | ||||
| 此指令用于在任何元素上添加一个提示框。 | ||||
| 
 | ||||
| ## 使用方式 | ||||
| 
 | ||||
| ```html | ||||
| <script lang="ts" setup> | ||||
| import { IconDeleteBin } from "@halo-dev/components" | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <IconDeleteBin v-tooltip="'删除此文档'" /> | ||||
| </template> | ||||
| ``` | ||||
| @ -0,0 +1,98 @@ | ||||
| --- | ||||
| title: 附件数据列表操作菜单 | ||||
| description: 扩展附件数据列表操作菜单 - attachment:list-item:operation:create | ||||
| --- | ||||
| 
 | ||||
| 此扩展点用于扩展附件数据列表的操作菜单项。 | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## 定义方式 | ||||
| 
 | ||||
| ```ts | ||||
| export default definePlugin({ | ||||
|   extensionPoints: { | ||||
|     "attachment:list-item:operation:create": ( | ||||
|       attachment: Ref<Attachment> | ||||
|     ): OperationItem<Attachment>[] | Promise<OperationItem<Attachment>[]> => { | ||||
|       return [ | ||||
|         { | ||||
|           priority: 10, | ||||
|           component: markRaw(VDropdownItem), | ||||
|           props: {}, | ||||
|           action: (item?: Attachment) => { | ||||
|             // do something | ||||
|           }, | ||||
|           label: "foo", | ||||
|           hidden: false, | ||||
|           permissions: [], | ||||
|           children: [], | ||||
|         }, | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ```mdx-code-block | ||||
| import OperationItem from "./interface/OperationItem.md"; | ||||
| 
 | ||||
| <OperationItem /> | ||||
| ``` | ||||
| 
 | ||||
| ## 示例 | ||||
| 
 | ||||
| 此示例将实现一个下载附件到本地的操作菜单项。 | ||||
| 
 | ||||
| ```ts | ||||
| import { definePlugin, type OperationItem } from "@halo-dev/console-shared"; | ||||
| import { Toast, VDropdownItem } from "@halo-dev/components"; | ||||
| import { markRaw, type Ref } from "vue"; | ||||
| import type { Attachment } from "@halo-dev/api-client"; | ||||
| 
 | ||||
| export default definePlugin({ | ||||
|   extensionPoints: { | ||||
|     "attachment:list-item:operation:create": ( | ||||
|       attachment: Ref<Attachment> | ||||
|     ): OperationItem<Attachment>[] | Promise<OperationItem<Attachment>[]> => { | ||||
|       return [ | ||||
|         { | ||||
|           priority: 10, | ||||
|           component: markRaw(VDropdownItem), | ||||
|           props: {}, | ||||
|           action: (item?: Attachment) => { | ||||
|             if (!item?.status?.permalink) { | ||||
|               Toast.error("该附件没有下载地址"); | ||||
|               return; | ||||
|             } | ||||
| 
 | ||||
|             const a = document.createElement("a"); | ||||
|             a.href = item.status.permalink; | ||||
|             a.download = item?.spec.displayName || item.metadata.name; | ||||
|             a.click(); | ||||
|           }, | ||||
|           label: "下载", | ||||
|           hidden: false, | ||||
|           permissions: [], | ||||
|           children: [], | ||||
|         }, | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| ``` | ||||
| 
 | ||||
| ## 实现案例 | ||||
| 
 | ||||
| - <https://github.com/halo-dev/plugin-s3> | ||||
| 
 | ||||
| ## 类型定义 | ||||
| 
 | ||||
| ### Attachment | ||||
| 
 | ||||
| ```mdx-code-block | ||||
| import Attachment from "./interface/Attachment.md"; | ||||
| 
 | ||||
| <Attachment /> | ||||
| ``` | ||||
| @ -0,0 +1,146 @@ | ||||
| --- | ||||
| title: 附件选择选项卡 | ||||
| description: 扩展附件选择组件的选项卡 - attachment:selector:create | ||||
| --- | ||||
| 
 | ||||
| 此扩展点用于扩展附件选择组件的选项卡,目前 Halo 仅包含内置的附件库,你可以通过此扩展点添加自定义的选项卡。 | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## 定义方式 | ||||
| 
 | ||||
| ```ts | ||||
| export default definePlugin({ | ||||
|   extensionPoints: { | ||||
|     "attachment:selector:create": (): AttachmentSelectProvider[]| Promise<AttachmentSelectProvider[]> => { | ||||
|       return [ | ||||
|         { | ||||
|           id: "foo", | ||||
|           label: "foo", | ||||
|           component: markRaw(FooComponent), | ||||
|         }, | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ```ts title="AttachmentSelectProvider" | ||||
| export interface AttachmentSelectProvider { | ||||
|   id: string;                                   // 选项卡 ID | ||||
|   label: string;                                // 选项卡名称 | ||||
|   component: Component | string;                // 选项卡组件 | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| 其中,`component` 可以是组件对象或组件名称,且此组件有以下实现要求: | ||||
| 
 | ||||
| 1. 组件必须包含名称为 `selected` 的 `prop`,用于接收当前选中的附件。 | ||||
| 
 | ||||
|     ```ts | ||||
|     const props = withDefaults( | ||||
|        defineProps<{ | ||||
|            selected: AttachmentLike[]; | ||||
|        }>(), | ||||
|        { | ||||
|            selected: () => [], | ||||
|        } | ||||
|     ); | ||||
|     ``` | ||||
| 
 | ||||
| 2. 组件必须包含名称为 `update:selected` 的 emit 事件,用于更新选中的附件。 | ||||
| 
 | ||||
|     ```ts | ||||
|     const emit = defineEmits<{ | ||||
|         (event: "update:selected", attachments: AttachmentLike[]): void; | ||||
|     }>(); | ||||
|     ``` | ||||
| 
 | ||||
| ```ts title="AttachmentLike" | ||||
| export type AttachmentLike = | ||||
|   | Attachment | ||||
|   | string | ||||
|   | { | ||||
|       url: string; | ||||
|       type: string; | ||||
|     }; | ||||
| ``` | ||||
| 
 | ||||
| ## 示例 | ||||
| 
 | ||||
| 为附件选择组件添加 Stickers 选项卡,用于从给定的贴纸列表选择附件。 | ||||
| 
 | ||||
| ```ts title="index.ts" | ||||
| import { | ||||
|   definePlugin, | ||||
|   type AttachmentSelectProvider, | ||||
| } from "@halo-dev/console-shared"; | ||||
| import { markRaw } from "vue"; | ||||
| import StickerSelectorProvider from "./components/StickerSelectorProvider.vue"; | ||||
| 
 | ||||
| export default definePlugin({ | ||||
|   components: {}, | ||||
|   routes: [], | ||||
|   extensionPoints: { | ||||
|     "attachment:selector:create": (): AttachmentSelectProvider[] => { | ||||
|       return [ | ||||
|         { | ||||
|           id: "stickers", | ||||
|           label: "Stickers", | ||||
|           component: markRaw(StickerSelectorProvider), | ||||
|         }, | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ```html title="StickerSelectorProvider.vue" | ||||
| <script lang="ts" setup> | ||||
| const props = withDefaults( | ||||
|   defineProps<{ | ||||
|     selected: AttachmentLike[]; | ||||
|   }>(), | ||||
|   { | ||||
|     selected: () => [], | ||||
|   } | ||||
| ); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
|   (event: "update:selected", attachments: AttachmentLike[]): void; | ||||
| }>(); | ||||
| 
 | ||||
| const stickers = [ | ||||
|   { | ||||
|     url: "https://picsum.photos/200?random=1", | ||||
|   }, | ||||
|   { | ||||
|     url: "https://picsum.photos/200?random=2", | ||||
|   }, | ||||
|   { | ||||
|     url: "https://picsum.photos/200?random=3", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const selectedStickers = ref<Set<String>>(new Set()); | ||||
| 
 | ||||
| const handleSelect = async (url: string) => { | ||||
|   if (selectedStickers.value.has(url)) { | ||||
|     selectedStickers.value.delete(url); | ||||
|     return; | ||||
|   } | ||||
|   selectedStickers.value.add(url); | ||||
|   emit('update:selected', Array.from(selectedStickers.value)); | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <div> | ||||
|     <img v-for="sticker in stickers" :src="sticker.url" @click="handleSelect(sticker.url)" /> | ||||
|   </div> | ||||
| </template> | ||||
| ``` | ||||
| 
 | ||||
| ## 实现案例 | ||||
| 
 | ||||
| - <https://github.com/halo-sigs/plugin-unsplash> | ||||
| @ -0,0 +1,41 @@ | ||||
| --- | ||||
| title: 备份数据列表操作菜单 | ||||
| description: 扩展备份数据列表操作菜单 - backup:list-item:operation:create | ||||
| --- | ||||
| 
 | ||||
| 此扩展点用于扩展备份数据列表的操作菜单项。 | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## 定义方式 | ||||
| 
 | ||||
| ```ts | ||||
| export default definePlugin({ | ||||
|   extensionPoints: { | ||||
|     "backup:list-item:operation:create": ( | ||||
|       backup: Ref<Backup> | ||||
|     ): OperationItem<Backup>[] | Promise<OperationItem<Backup>[]> => { | ||||
|       return [ | ||||
|         { | ||||
|           priority: 10, | ||||
|           component: markRaw(VDropdownItem), | ||||
|           props: {}, | ||||
|           action: (item?: Backup) => { | ||||
|             // do something | ||||
|           }, | ||||
|           label: "foo", | ||||
|           hidden: false, | ||||
|           permissions: [], | ||||
|           children: [], | ||||
|         }, | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ```mdx-code-block | ||||
| import OperationItem from "./interface/OperationItem.md"; | ||||
| 
 | ||||
| <OperationItem /> | ||||
| ``` | ||||
| @ -0,0 +1,36 @@ | ||||
| --- | ||||
| title: 备份页面选项卡 | ||||
| description: 扩展备份页面选项卡 - backup:tabs:create | ||||
| --- | ||||
| 
 | ||||
| 此扩展点可以针对备份页面扩展更多关于 UI 的功能,比如定时备份设置、备份到第三方云存储等。 | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## 定义方式 | ||||
| 
 | ||||
| ```ts | ||||
| export default definePlugin({ | ||||
|   extensionPoints: { | ||||
|     "backup:tabs:create": (): BackupTab[] | Promise<BackupTab[]> => { | ||||
|       return [ | ||||
|         { | ||||
|           id: "foo", | ||||
|           label: "foo", | ||||
|           component: markRaw(FooComponent), | ||||
|           permissions: [], | ||||
|         }, | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ```ts title="BackupTab" | ||||
| export interface BackupTab { | ||||
|   id: string;                 // 选项卡 ID | ||||
|   label: string;              // 选项卡标题 | ||||
|   component: Raw<Component>;  // 选项卡面板组件 | ||||
|   permissions?: string[];     // 选项卡权限 | ||||
| } | ||||
| ``` | ||||
| @ -0,0 +1,114 @@ | ||||
| --- | ||||
| title: 评论来源显示 | ||||
| description: 扩展评论来源显示 - comment:subject-ref:create | ||||
| --- | ||||
| 
 | ||||
| Console 的评论管理列表的评论来源默认仅支持显示来自文章和页面的评论,如果其他插件中的业务模块也使用了评论,那么就可以通过该拓展点来扩展评论来源的显示。 | ||||
| 
 | ||||
| :::info 提示 | ||||
| 此扩展点需要后端配合使用,请参考 [评论主体展示](../../server/extension-points/comment-subject.md)。 | ||||
| ::: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## 定义方式 | ||||
| 
 | ||||
| ```ts | ||||
| export default definePlugin({ | ||||
|   extensionPoints: { | ||||
|     "comment:subject-ref:create": (): CommentSubjectRefProvider[] => { | ||||
|       return [ | ||||
|         { | ||||
|           kind: "Example", | ||||
|           group: "example.halo.run", | ||||
|           resolve: (subject: Extension): CommentSubjectRefResult => { | ||||
|             return { | ||||
|               label: "foo", | ||||
|               title: subject.title, | ||||
|               externalUrl: `/example/${subject.metadata.name}`, | ||||
|               route: { | ||||
|                 name: "Example", | ||||
|               }, | ||||
|             }; | ||||
|           }, | ||||
|         }, | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ```ts title="CommentSubjectRefProvider" | ||||
| export type CommentSubjectRefProvider = { | ||||
|   kind: string; | ||||
|   group: string; | ||||
|   resolve: (subject: Extension) => CommentSubjectRefResult; | ||||
| }; | ||||
| ``` | ||||
| 
 | ||||
| ```ts title="CommentSubjectRefResult" | ||||
| export interface CommentSubjectRefResult { | ||||
|   label: string; | ||||
|   title: string; | ||||
|   route?: RouteLocationRaw; | ||||
|   externalUrl?: string; | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ## 示例 | ||||
| 
 | ||||
| 此示例以[瞬间插件](https://github.com/halo-sigs/plugin-moments)为例,目前瞬间插件中的评论是通过 Halo 的内置的评论功能实现的,所以需要扩展评论来源显示。 | ||||
| 
 | ||||
| ```ts | ||||
| import { | ||||
|   definePlugin, | ||||
|   type CommentSubjectRefResult, | ||||
| } from "@halo-dev/console-shared"; | ||||
| import { markRaw } from "vue"; | ||||
| import type { Moment } from "@/types"; | ||||
| import { formatDatetime } from "./utils/date"; | ||||
| import type { Extension } from "@halo-dev/api-client/index"; | ||||
| 
 | ||||
| export default definePlugin({ | ||||
|   extensionPoints: { | ||||
|     "comment:subject-ref:create": () => { | ||||
|       return [ | ||||
|         { | ||||
|           kind: "Moment", | ||||
|           group: "moment.halo.run", | ||||
|           resolve: (subject: Extension): CommentSubjectRefResult => { | ||||
|             const moment = subject as Moment; | ||||
|             return { | ||||
|               label: "瞬间", | ||||
|               title: determineMomentTitle(moment), | ||||
|               externalUrl: `/moments/${moment.metadata.name}`, | ||||
|               route: { | ||||
|                 name: "Moments", | ||||
|               }, | ||||
|             }; | ||||
|           }, | ||||
|         }, | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| const determineMomentTitle = (moment: Moment) => { | ||||
|   const pureContent = stripHtmlTags(moment.spec.content.raw); | ||||
|   const title = !pureContent?.trim() | ||||
|     ? formatDatetime(new Date(moment.spec.releaseTime || "")) | ||||
|     : pureContent; | ||||
|   return title?.substring(0, 100); | ||||
| }; | ||||
| 
 | ||||
| const stripHtmlTags = (str: string) => { | ||||
|   // strip html tags | ||||
|   const stripped = str?.replace(/<\/?[^>]+(>|$)/gi, "") || ""; | ||||
|   // strip newlines and collapse spaces | ||||
|   return stripped.replace(/\n/g, " ").replace(/\s+/g, " "); | ||||
| }; | ||||
| ``` | ||||
| 
 | ||||
| ## 实现案例 | ||||
| 
 | ||||
| - <https://github.com/halo-sigs/plugin-moments> | ||||
| @ -0,0 +1,14 @@ | ||||
| --- | ||||
| title: 扩展点 | ||||
| description: Halo UI 为插件提供的扩展点接口 | ||||
| --- | ||||
| 
 | ||||
| UI 扩展点是用于扩展 Console 和 UC 的界面的接口,通过实现扩展点接口,插件可以在 Console 和 UC 中扩展功能。 | ||||
| 
 | ||||
| 以下是目前已支持的扩展点列表: | ||||
| 
 | ||||
| ```mdx-code-block | ||||
| import DocCardList from '@theme/DocCardList'; | ||||
| 
 | ||||
| <DocCardList /> | ||||
| ``` | ||||
| @ -0,0 +1,25 @@ | ||||
| ```ts | ||||
| export interface Attachment { | ||||
|   apiVersion: "storage.halo.run/v1alpha1" | ||||
|   kind: "Attachment" | ||||
|   metadata: { | ||||
|     annotations: {} | ||||
|     creationTimestamp: string | ||||
|     labels: {} | ||||
|     name: string                                // 附件的唯一标识 | ||||
|     version: number | ||||
|   } | ||||
|   spec: { | ||||
|     displayName: string                         // 附件名称 | ||||
|     groupName: string                           // 附件分组 | ||||
|     mediaType: string                           // 附件类型 | ||||
|     ownerName: string                           // 附件上传者 | ||||
|     policyName: string                          // 附件存储策略 | ||||
|     size: number                                // 附件大小 | ||||
|     tags: Array<string> | ||||
|   } | ||||
|   status: { | ||||
|     permalink: string                           // 附件固定访问地址 | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| @ -0,0 +1,119 @@ | ||||
| ```ts | ||||
| export interface ListedPost { | ||||
|   categories: Array<{                             // 文章的分类集合 | ||||
|     apiVersion: "content.halo.run/v1alpha1"; | ||||
|     kind: "Category"; | ||||
|     metadata: { | ||||
|       annotations: {}; | ||||
|       creationTimestamp: string; | ||||
|       labels: {}; | ||||
|       name: string;                               // 分类的唯一标识 | ||||
|       version: number; | ||||
|     }; | ||||
|     spec: { | ||||
|       children: Array<string>;                    // 子分类 | ||||
|       cover: string;                              // 分类封面图 | ||||
|       description: string;                        // 分类描述 | ||||
|       displayName: string;                        // 分类名称 | ||||
|       priority: number;                           // 分类优先级 | ||||
|       slug: string;                               // 分类别名 | ||||
|       template: string;                           // 分类渲染模板 | ||||
|     }; | ||||
|     status: { | ||||
|       permalink: string;                          // 分类的永久链接 | ||||
|       postCount: number;                          // 分类下的文章总数 | ||||
|       visiblePostCount: number;                   // 分类下可见的文章总数 | ||||
|     }; | ||||
|   }>; | ||||
|   contributors: Array<{                           // 文章的贡献者集合 | ||||
|     avatar: string;                               // 贡献者头像 | ||||
|     displayName: string;                          // 贡献者名称 | ||||
|     name: string;                                 // 贡献者唯一标识 | ||||
|   }>; | ||||
|   owner: {                                        // 文章的作者信息 | ||||
|     avatar: string;                               // 作者头像 | ||||
|     displayName: string;                          // 作者名称 | ||||
|     name: string;                                 // 作者唯一标识 | ||||
|   }; | ||||
|   post: {                                         // 文章信息 | ||||
|     apiVersion: "content.halo.run/v1alpha1"; | ||||
|     kind: "Post"; | ||||
|     metadata: { | ||||
|       annotations: {}; | ||||
|       creationTimestamp: string; | ||||
|       labels: {}; | ||||
|       name: string;                               // 文章的唯一标识 | ||||
|       version: number; | ||||
|     }; | ||||
|     spec: { | ||||
|       allowComment: boolean;                      // 是否允许评论 | ||||
|       baseSnapshot: string;                       // 内容基础快照 | ||||
|       categories: Array<string>;                  // 文章所属分类 | ||||
|       cover: string;                              // 文章封面图 | ||||
|       deleted: boolean;                           // 是否已删除 | ||||
|       excerpt: {                                  // 文章摘要 | ||||
|         autoGenerate: boolean;                    // 是否自动生成 | ||||
|         raw: string;                              // 摘要内容 | ||||
|       }; | ||||
|       headSnapshot: string;                       // 内容最新快照 | ||||
|       htmlMetas: Array<{}>; | ||||
|       owner: string;                              // 文章作者的唯一标识 | ||||
|       pinned: boolean;                            // 是否置顶 | ||||
|       priority: number;                           // 文章优先级 | ||||
|       publish: boolean;                           // 是否发布 | ||||
|       publishTime: string;                        // 发布时间 | ||||
|       releaseSnapshot: string;                    // 已发布的内容快照 | ||||
|       slug: string;                               // 文章别名 | ||||
|       tags: Array<string>;                        // 文章所属标签 | ||||
|       template: string;                           // 文章渲染模板 | ||||
|       title: string;                              // 文章标题 | ||||
|       visible: string;                            // 文章可见性 | ||||
|     }; | ||||
|     status: { | ||||
|       commentsCount: number;                      // 文章评论总数 | ||||
|       conditions: Array<{ | ||||
|         lastTransitionTime: string; | ||||
|         message: string; | ||||
|         reason: string; | ||||
|         status: string; | ||||
|         type: string; | ||||
|       }>; | ||||
|       contributors: Array<string>; | ||||
|       excerpt: string;                            // 最终的文章摘要,根据是否自动生成计算 | ||||
|       inProgress: boolean;                        // 是否有未发布的内容 | ||||
|       lastModifyTime: string;                     // 文章最后修改时间 | ||||
|       permalink: string;                          // 文章的永久链接 | ||||
|       phase: string; | ||||
|     }; | ||||
|   }; | ||||
|   stats: { | ||||
|     approvedComment: number;                      // 已审核的评论数 | ||||
|     totalComment: number;                         // 评论总数 | ||||
|     upvote: number;                               // 点赞数 | ||||
|     visit: number;                                // 访问数 | ||||
|   }; | ||||
|   tags: Array<{                                   // 文章的标签集合 | ||||
|     apiVersion: "content.halo.run/v1alpha1"; | ||||
|     kind: "Tag"; | ||||
|     metadata: { | ||||
|       annotations: {}; | ||||
|       creationTimestamp: string; | ||||
|       labels: {}; | ||||
|       name: string;                               // 标签的唯一标识 | ||||
|       version: number; | ||||
|     }; | ||||
|     spec: { | ||||
|       color: string;                              // 标签颜色 | ||||
|       cover: string;                              // 标签封面图 | ||||
|       displayName: string;                        // 标签名称 | ||||
|       slug: string;                               // 标签别名 | ||||
|     }; | ||||
|     status: { | ||||
|       permalink: string;                          // 标签的永久链接 | ||||
|       postCount: number;                          // 标签下的文章总数 | ||||
|       visiblePostCount: number;                   // 标签下可见的文章总数 | ||||
|     }; | ||||
|   }>; | ||||
| } | ||||
| 
 | ||||
| ``` | ||||
| @ -0,0 +1,12 @@ | ||||
| ```ts title="OperationItem" | ||||
| export interface OperationItem<T> { | ||||
|   priority: number;                 // 排序优先级 | ||||
|   component: Raw<Component>;        // 菜单项组件 | ||||
|   props?: Record<string, unknown>;  // 菜单项组件属性 | ||||
|   action?: (item?: T) => void;      // 菜单项点击事件 | ||||
|   label?: string;                   // 菜单项标题 | ||||
|   hidden?: boolean;                 // 菜单项是否隐藏 | ||||
|   permissions?: string[];           // 菜单项 UI 权限 | ||||
|   children?: OperationItem<T>[];    // 子菜单项 | ||||
| } | ||||
| ``` | ||||
| @ -0,0 +1,50 @@ | ||||
| ```ts | ||||
| export interface Plugin { | ||||
|   apiVersion: "plugin.halo.run/v1alpha1" | ||||
|   kind: "Plugin" | ||||
|   metadata: { | ||||
|     annotations: {} | ||||
|     creationTimestamp: string         // 创建时间 | ||||
|     labels: {} | ||||
|     name: string                      // 唯一标识 | ||||
|     version: number | ||||
|   } | ||||
|   spec: { | ||||
|     author: {                         // 作者信息 | ||||
|       name: string | ||||
|       website: string | ||||
|     } | ||||
|     configMapName: string             // 关联的 ConfigMap 模型,用于存储配置 | ||||
|     description: string               // 插件描述 | ||||
|     displayName: string               // 插件名称 | ||||
|     enabled: boolean | ||||
|     homepage: string                  // 插件主页 | ||||
|     license: Array<{                  // 插件协议 | ||||
|       name: string | ||||
|       url: string | ||||
|     }> | ||||
|     logo: string | ||||
|     pluginDependencies: {} | ||||
|     repo: string                      // 插件仓库地址 | ||||
|     requires: string                  // 所依赖的 Halo 版本 | ||||
|     settingName: string               // 关联的 Setting 模型,用于渲染配置表单 | ||||
|     version: string                   // 插件版本 | ||||
|   } | ||||
|   status: { | ||||
|     conditions: Array<{ | ||||
|       lastTransitionTime: string | ||||
|       message: string | ||||
|       reason: string | ||||
|       status: string | ||||
|       type: string | ||||
|     }> | ||||
|     entry: string | ||||
|     lastProbeState: string | ||||
|     lastStartTime: string | ||||
|     loadLocation: string | ||||
|     logo: string                      // 插件 Logo 地址 | ||||
|     phase: string | ||||
|     stylesheet: string | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| @ -0,0 +1,63 @@ | ||||
| ```ts | ||||
| export interface Theme { | ||||
|   apiVersion: "theme.halo.run/v1alpha1" | ||||
|   kind: "Theme" | ||||
|   metadata: { | ||||
|     annotations: {} | ||||
|     creationTimestamp: string | ||||
|     labels: {} | ||||
|     name: string                                // 主题的唯一标识 | ||||
|     version: number | ||||
|   } | ||||
|   spec: { | ||||
|     author: {                                   // 主题作者信息 | ||||
|       name: string | ||||
|       website: string | ||||
|     } | ||||
|     configMapName: string                       // 关联的 ConfigMap 模型,用于存储配置 | ||||
|     customTemplates: {                          // 自定义模板信息 | ||||
|       category: Array<{ | ||||
|         description: string | ||||
|         file: string | ||||
|         name: string | ||||
|         screenshot: string | ||||
|       }> | ||||
|       page: Array<{ | ||||
|         description: string | ||||
|         file: string | ||||
|         name: string | ||||
|         screenshot: string | ||||
|       }> | ||||
|       post: Array<{ | ||||
|         description: string | ||||
|         file: string | ||||
|         name: string | ||||
|         screenshot: string | ||||
|       }> | ||||
|     } | ||||
|     description: string                         // 主题描述 | ||||
|     displayName: string                         // 主题名称 | ||||
|     homepage: string                            // 主题主页 | ||||
|     license: Array<{                            // 主题许可证信息 | ||||
|       name: string | ||||
|       url: string | ||||
|     }> | ||||
|     logo: string                                // 主题 Logo | ||||
|     repo: string                                // 主题仓库地址 | ||||
|     requires: string                            // 所依赖的 Halo 版本 | ||||
|     settingName: string                         // 关联的 Setting 模型,用于渲染配置表单 | ||||
|     version: string                             // 主题版本 | ||||
|   } | ||||
|   status: { | ||||
|     conditions: Array<{ | ||||
|       lastTransitionTime: string | ||||
|       message: string | ||||
|       reason: string | ||||
|       status: string | ||||
|       type: string | ||||
|     }> | ||||
|     location: string | ||||
|     phase: string | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| @ -0,0 +1,44 @@ | ||||
| --- | ||||
| title: 插件安装界面选项卡 | ||||
| description: 扩展插件安装界面选项卡 - plugin:installation:tabs:create | ||||
| --- | ||||
| 
 | ||||
| 目前 Halo 原生支持本地上传和远程下载的方式安装插件,此扩展点用于扩展插件安装界面的选项卡,以支持更多的安装方式。 | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## 定义方式 | ||||
| 
 | ||||
| ```ts | ||||
| export default definePlugin({ | ||||
|   extensionPoints: { | ||||
|     "plugin:installation:tabs:create": (): PluginInstallationTab[] | Promise<PluginInstallationTab[]> => { | ||||
|       return [ | ||||
|         { | ||||
|           id: "foo", | ||||
|           label: "foo", | ||||
|           component: markRaw(FooComponent), | ||||
|           props: {}, | ||||
|           permissions: [], | ||||
|           priority: 0, | ||||
|         } | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ```ts title="PluginInstallationTab" | ||||
| export interface PluginInstallationTab { | ||||
|   id: string;                       // 选项卡 ID | ||||
|   label: string;                    // 选项卡标题 | ||||
|   component: Raw<Component>;        // 选项卡面板组件 | ||||
|   props?: Record<string, unknown>;  // 选项卡面板组件属性 | ||||
|   permissions?: string[];           // 选项卡 UI 权限 | ||||
|   priority: number;                 // 选项卡排序优先级 | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ## 实现案例 | ||||
| 
 | ||||
| - <https://github.com/halo-dev/plugin-app-store> | ||||
| @ -0,0 +1,55 @@ | ||||
| --- | ||||
| title: 插件数据列表操作菜单 | ||||
| description: 扩展插件数据列表操作菜单 - plugin:list-item:operation:create | ||||
| --- | ||||
| 
 | ||||
| 此扩展点用于扩展插件数据列表的操作菜单项。 | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## 定义方式 | ||||
| 
 | ||||
| ```ts | ||||
| export default definePlugin({ | ||||
|   extensionPoints: { | ||||
|     "plugin:list-item:operation:create": ( | ||||
|       plugin: Ref<Plugin> | ||||
|     ): OperationItem<Plugin>[] | Promise<OperationItem<Plugin>[]> => { | ||||
|       return [ | ||||
|         { | ||||
|           priority: 10, | ||||
|           component: markRaw(VDropdownItem), | ||||
|           props: {}, | ||||
|           action: (item?: Plugin) => { | ||||
|             // do something | ||||
|           }, | ||||
|           label: "foo", | ||||
|           hidden: false, | ||||
|           permissions: [], | ||||
|           children: [], | ||||
|         }, | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ```mdx-code-block | ||||
| import OperationItem from "./interface/OperationItem.md"; | ||||
| 
 | ||||
| <OperationItem /> | ||||
| ``` | ||||
| 
 | ||||
| ## 实现案例 | ||||
| 
 | ||||
| - <https://github.com/halo-dev/plugin-app-store> | ||||
| 
 | ||||
| ## 类型定义 | ||||
| 
 | ||||
| ### Plugin | ||||
| 
 | ||||
| ```mdx-code-block | ||||
| import Plugin from "./interface/Plugin.md"; | ||||
| 
 | ||||
| <Plugin /> | ||||
| ``` | ||||
| @ -0,0 +1,92 @@ | ||||
| --- | ||||
| title: 文章数据列表操作菜单 | ||||
| description: 扩展文章数据列表操作菜单 - post:list-item:operation:create | ||||
| --- | ||||
| 
 | ||||
| 此扩展点用于扩展文章数据列表的操作菜单项。 | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## 定义方式 | ||||
| 
 | ||||
| ```ts | ||||
| export default definePlugin({ | ||||
|   extensionPoints: { | ||||
|     "post:list-item:operation:create": ( | ||||
|       post: Ref<ListedPost> | ||||
|     ): OperationItem<ListedPost>[] | Promise<OperationItem<ListedPost>[]> => { | ||||
|       return [ | ||||
|         { | ||||
|           priority: 10, | ||||
|           component: markRaw(VDropdownItem), | ||||
|           props: {}, | ||||
|           action: (item?: ListedPost) => { | ||||
|             // do something | ||||
|           }, | ||||
|           label: "foo", | ||||
|           hidden: false, | ||||
|           permissions: [], | ||||
|           children: [], | ||||
|         }, | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ```mdx-code-block | ||||
| import OperationItem from "./interface/OperationItem.md"; | ||||
| 
 | ||||
| <OperationItem /> | ||||
| ``` | ||||
| 
 | ||||
| ## 示例 | ||||
| 
 | ||||
| 此示例将实现一个操作菜单项,点击后会将文章内容作为文件下载到本地。 | ||||
| 
 | ||||
| ```ts | ||||
| import type { ListedPost } from "@halo-dev/api-client"; | ||||
| import { VDropdownItem } from "@halo-dev/components"; | ||||
| import { definePlugin } from "@halo-dev/console-shared"; | ||||
| import axios from "axios"; | ||||
| import { markRaw } from "vue"; | ||||
| 
 | ||||
| export default definePlugin({ | ||||
|   extensionPoints: { | ||||
|     "post:list-item:operation:create": () => { | ||||
|       return [ | ||||
|         { | ||||
|           priority: 21, | ||||
|           component: markRaw(VDropdownItem), | ||||
|           label: "下载到本地", | ||||
|           visible: true, | ||||
|           permissions: [], | ||||
|           action: async (post: ListedPost) => { | ||||
|             const { data } = await axios.get( | ||||
|               `/apis/api.console.halo.run/v1alpha1/posts/${post.post.metadata.name}/head-content` | ||||
|             ); | ||||
|             const blob = new Blob([data.raw], { | ||||
|               type: "text/plain;charset=utf-8", | ||||
|             }); | ||||
|             const url = window.URL.createObjectURL(blob); | ||||
|             const link = document.createElement("a"); | ||||
|             link.href = url; | ||||
|             link.download = `${post.post.spec.title}.${data.rawType}`; | ||||
|             link.click(); | ||||
|           }, | ||||
|         }, | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ## 类型定义 | ||||
| 
 | ||||
| ### ListedPost | ||||
| 
 | ||||
| ```mdx-code-block | ||||
| import ListedPost from "./interface/ListedPost.md"; | ||||
| 
 | ||||
| <ListedPost /> | ||||
| ``` | ||||
| @ -0,0 +1,91 @@ | ||||
| --- | ||||
| title: 主题数据列表操作菜单 | ||||
| description: 扩展主题数据列表操作菜单 - theme:list-item:operation:create | ||||
| --- | ||||
| 
 | ||||
| 此扩展点用于扩展主题数据列表的操作菜单项。 | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## 定义方式 | ||||
| 
 | ||||
| ```ts | ||||
| export default definePlugin({ | ||||
|   extensionPoints: { | ||||
|     "theme:list-item:operation:create": ( | ||||
|       theme: Ref<Theme> | ||||
|     ): OperationItem<Theme>[] | Promise<OperationItem<Theme>[]> => { | ||||
|       return [ | ||||
|         { | ||||
|           priority: 10, | ||||
|           component: markRaw(VDropdownItem), | ||||
|           props: {}, | ||||
|           action: (item?: Theme) => { | ||||
|             // do something | ||||
|           }, | ||||
|           label: "foo", | ||||
|           hidden: false, | ||||
|           permissions: [], | ||||
|           children: [], | ||||
|         }, | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ```mdx-code-block | ||||
| import OperationItem from "./interface/OperationItem.md"; | ||||
| 
 | ||||
| <OperationItem /> | ||||
| ``` | ||||
| 
 | ||||
| ## 示例 | ||||
| 
 | ||||
| 此示例将实现一个跳转到前台预览主题的操作菜单项。 | ||||
| 
 | ||||
| ```ts | ||||
| import { definePlugin, type OperationItem } from "@halo-dev/console-shared"; | ||||
| import { VButton } from "@halo-dev/components"; | ||||
| import { markRaw, type Ref } from "vue"; | ||||
| import type { Theme } from "@halo-dev/api-client"; | ||||
| 
 | ||||
| export default definePlugin({ | ||||
|   extensionPoints: { | ||||
|     "theme:list-item:operation:create": ( | ||||
|       theme: Ref<Theme> | ||||
|     ): OperationItem<Theme>[] | Promise<OperationItem<Theme>[]> => { | ||||
|       return [ | ||||
|         { | ||||
|           priority: 10, | ||||
|           component: markRaw(VButton), | ||||
|           props: { | ||||
|             size: "sm", | ||||
|           }, | ||||
|           action: (item?: Theme) => { | ||||
|             window.open(`/?preview-theme=${item?.metadata.name}`); | ||||
|           }, | ||||
|           label: "前台预览", | ||||
|           hidden: false, | ||||
|           permissions: [], | ||||
|           children: [], | ||||
|         }, | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ## 实现案例 | ||||
| 
 | ||||
| - <https://github.com/halo-dev/plugin-app-store> | ||||
| 
 | ||||
| ## 类型定义 | ||||
| 
 | ||||
| ### Theme | ||||
| 
 | ||||
| ```mdx-code-block | ||||
| import Theme from "./interface/Theme.md"; | ||||
| 
 | ||||
| <Theme /> | ||||
| ``` | ||||
| @ -0,0 +1,44 @@ | ||||
| --- | ||||
| title: 主题管理界面选项卡 | ||||
| description: 扩展主题管理界面选项卡 - theme:list:tabs:create | ||||
| --- | ||||
| 
 | ||||
| 目前在 Halo 的主题管理中原生支持本地上传和远程下载的方式安装主题,此扩展点用于扩展主题管理界面的选项卡,以支持更多的安装方式。 | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## 定义方式 | ||||
| 
 | ||||
| ```ts | ||||
| export default definePlugin({ | ||||
|   extensionPoints: { | ||||
|     "theme:list:tabs:create": (): ThemeListTab[] | Promise<ThemeListTab[]> => { | ||||
|       return [ | ||||
|         { | ||||
|           id: "foo", | ||||
|           label: "foo", | ||||
|           component: markRaw(FooComponent), | ||||
|           props: {}, | ||||
|           permissions: [], | ||||
|           priority: 0, | ||||
|         } | ||||
|       ]; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ```ts title="ThemeListTab" | ||||
| export interface ThemeListTab { | ||||
|   id: string;                       // 选项卡 ID | ||||
|   label: string;                    // 选项卡标题 | ||||
|   component: Raw<Component>;        // 选项卡面板组件 | ||||
|   props?: Record<string, unknown>;  // 选项卡面板组件属性 | ||||
|   permissions?: string[];           // 选项卡 UI 权限 | ||||
|   priority: number;                 // 选项卡排序优先级 | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ## 实现案例 | ||||
| 
 | ||||
| - <https://github.com/halo-dev/plugin-app-store> | ||||
| @ -0,0 +1,4 @@ | ||||
| --- | ||||
| title: 附录 | ||||
| description: 附录 | ||||
| --- | ||||
| @ -0,0 +1,12 @@ | ||||
| --- | ||||
| title: 介绍 | ||||
| description: 介绍插件 UI 部分的基础知识。 | ||||
| --- | ||||
| 
 | ||||
| Halo 插件体系的 UI 部分可以让开发者在 Console 控制台和 UC 个人中心添加新的页面或者扩展已有的功能。 | ||||
| 
 | ||||
| 在开始之前,建议先熟悉或安装以下库和工具: | ||||
| 
 | ||||
| 1. [Node.js 18+](https://nodejs.org) | ||||
| 2. [pnpm 8+](https://pnpm.io) | ||||
| 3. [Vue.js 3](https://vuejs.org) | ||||
| @ -0,0 +1,6 @@ | ||||
| --- | ||||
| title: 介绍 | ||||
| description: 插件开发的准备工作 | ||||
| --- | ||||
| 
 | ||||
| Halo 采用可插拔架构,功能模块之间耦合度低、灵活性提高,支持用户按需安装、卸载插件,操作便捷。同时提供插件开发接口以确保较高扩展性和可维护性,这个系列的文档将帮助你了解如何开发 Halo 插件。 | ||||
| @ -0,0 +1,64 @@ | ||||
| --- | ||||
| title: 模型元数据 | ||||
| --- | ||||
| 
 | ||||
| 在 [元数据表单定义](../annotations-form.md) 我们介绍了如何为模型添加元数据表单,此文档将介绍如何在主题模板中使用元数据。 | ||||
| 
 | ||||
| 我们在模板中专门为获取 annotations 数据提供了三个方法,可以更加方便的设置默认值和判断元数据字段是否存在。 | ||||
| 
 | ||||
| ## #annotations.get(extension,key) | ||||
| 
 | ||||
| ### 描述 | ||||
| 
 | ||||
| 根据对象和元数据的 key 获取元数据的值。 | ||||
| 
 | ||||
| ### 示例 | ||||
| 
 | ||||
| ```html {4} | ||||
| <div th:with="menu = ${menuFinder.getPrimary()}"> | ||||
|   <ul th:with="menuItems = ${menu.menuItems}"> | ||||
|     <li th:each="menuItem : ${menuItems}"> | ||||
|       <i th:class="${#annotations.get(menuItem, 'icon')}"></i> | ||||
|       <a th:href="@{${menuItem.status.href}}" th:text="${menuItem.status.displayName}"></a> | ||||
|     </li> | ||||
|    </ul> | ||||
| </div> | ||||
| ``` | ||||
| 
 | ||||
| ## #annotations.getOrDefault(extension,key,defaultValue) | ||||
| 
 | ||||
| ### 描述 | ||||
| 
 | ||||
| 根据对象和元数据的 key 获取元数据的值,同时支持设置默认值。 | ||||
| 
 | ||||
| ### 示例 | ||||
| 
 | ||||
| ```html {4} | ||||
| <div th:with="menu = ${menuFinder.getPrimary()}"> | ||||
|   <ul th:with="menuItems = ${menu.menuItems}"> | ||||
|     <li th:each="menuItem : ${menuItems}"> | ||||
|       <i th:class="${#annotations.getOrDefault(menuItem, 'icon', 'fa')}"></i> | ||||
|       <a th:href="@{${menuItem.status.href}}" th:text="${menuItem.status.displayName}"></a> | ||||
|     </li> | ||||
|    </ul> | ||||
| </div> | ||||
| ``` | ||||
| 
 | ||||
| ## #annotations.contains(extension,key) | ||||
| 
 | ||||
| ### 描述 | ||||
| 
 | ||||
| 根据对象和元数据的 key 判断元数据是否存在。 | ||||
| 
 | ||||
| ### 示例 | ||||
| 
 | ||||
| ```html {4} | ||||
| <div th:with="menu = ${menuFinder.getPrimary()}"> | ||||
|   <ul th:with="menuItems = ${menu.menuItems}"> | ||||
|     <li th:each="menuItem : ${menuItems}"> | ||||
|       <i th:if="${#annotations.contains(menuItem, 'icon')}" th:class="${#annotations.get(menuItem, 'icon')}"></i> | ||||
|       <a th:href="@{${menuItem.status.href}}" th:text="${menuItem.status.displayName}"></a> | ||||
|     </li> | ||||
|    </ul> | ||||
| </div> | ||||
| ``` | ||||
| @ -0,0 +1,47 @@ | ||||
| --- | ||||
| title: 常用代码片段 | ||||
| description: 本文档介绍了常用的代码片段,以便于开发者快速上手。 | ||||
| --- | ||||
| 
 | ||||
| ## 布局模板 | ||||
| 
 | ||||
| 通常情况下,我们需要一个公共模板来定义页面的布局。 | ||||
| 
 | ||||
| ```html title="templates/layout.html" | ||||
| <!DOCTYPE html> | ||||
| <html lang="en" xmlns:th="https://www.thymeleaf.org" th:fragment="html (head,content)"> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <meta http-equiv="X-UA-Compatible" content="IE=edge" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2" /> | ||||
|     <title th:text="${site.title}"></title> | ||||
|     <link rel="stylesheet" th:href="@{/assets/dist/style.css}" /> | ||||
|     <script th:src="@{/assets/dist/main.iife.js}"></script> | ||||
|     <th:block th:if="${head != null}"> | ||||
|       <th:block th:replace="${head}" /> | ||||
|     </th:block> | ||||
|   </head> | ||||
|   <body> | ||||
|     <section> | ||||
|       <th:block th:replace="${content}" /> | ||||
|     </section> | ||||
|   </body> | ||||
| </html> | ||||
| ``` | ||||
| 
 | ||||
| ```html title="templates/index.html" | ||||
| <!DOCTYPE html> | ||||
| <html | ||||
|   xmlns:th="https://www.thymeleaf.org" | ||||
|   th:replace="~{modules/layout :: html(head = null,content = ~{::content})}" | ||||
| > | ||||
|   <th:block th:fragment="content"> | ||||
|     <!-- 文章列表 --> | ||||
|     <ul> | ||||
|       <li th:each="post : ${posts.items}"> | ||||
|         <a th:href="@{${post.status.permalink}}" th:text="${post.spec.title}"></a> | ||||
|       </li> | ||||
|     </ul> | ||||
|   </th:block> | ||||
| </html> | ||||
| ``` | ||||
| @ -0,0 +1,95 @@ | ||||
| --- | ||||
| title: 配置文件 | ||||
| description: 关于主题配置文件的文档。 | ||||
| --- | ||||
| 
 | ||||
| 目前 Halo 2.0 的主题必须在根目录包含 `theme.yaml`,用于配置主题的基本信息,如主题名称、版本、作者等。 | ||||
| 
 | ||||
| ## 格式示例 | ||||
| 
 | ||||
| ```yaml title="theme.yaml" | ||||
| apiVersion: theme.halo.run/v1alpha1 | ||||
| kind: Theme | ||||
| metadata: | ||||
|   name: theme-foo | ||||
| spec: | ||||
|   displayName: 示例主题 | ||||
|   author: | ||||
|     name: Halo | ||||
|     website: https://www.halo.run | ||||
|   description: 一个示例主题 | ||||
|   logo: https://www.halo.run/logo | ||||
|   homepage: https://github.com/halo-sigs/theme-foo | ||||
|   repo: https://github.com/halo-sigs/theme-foo.git | ||||
|   issues: https://github.com/halo-sigs/theme-foo/issues | ||||
|   settingName: "theme-foo-setting" | ||||
|   configMapName: "theme-foo-configMap" | ||||
|   customTemplates: | ||||
|     post: | ||||
|       - name: 文档 | ||||
|         description: 文档类型的文章 | ||||
|         screenshot:  | ||||
|         file: post_documentation.html | ||||
|     category: | ||||
|       - name: 知识库 | ||||
|         description: 知识库类型的分类 | ||||
|         screenshot:  | ||||
|         file: category_knowledge.html | ||||
|     page: | ||||
|       - name: 关于 | ||||
|         description: 关于页面 | ||||
|         screenshot: | ||||
|         file: page_about.html | ||||
|   version: 1.0.0 | ||||
|   requires: 2.0.0 | ||||
|   license: | ||||
|     - name: "GPL-3.0" | ||||
|       url: "https://github.com/halo-sigs/theme-foo/blob/main/LICENSE" | ||||
| ``` | ||||
| 
 | ||||
| ## 字段详解 | ||||
| 
 | ||||
| | 字段                            | 描述                                                                                          | 是否必填 | | ||||
| |---------------------------------|---------------------------------------------------------------------------------------------|---------| | ||||
| | `metadata.name`                 | 主题的唯一标识,**需要注意:此字段的值需要和主题文件夹名称一致,否则可能导致部分资源无法正常加载。**                                                                                | 是       | | ||||
| | `spec.displayName`              | 显示名称                                                                                      | 是       | | ||||
| | `spec.author.name`              | 作者名称                                                                                      | 否       | | ||||
| | `spec.author.website`           | 作者网站                                                                                      | 否       | | ||||
| | `spec.description`              | 主题描述                                                                                      | 否       | | ||||
| | `spec.logo`                     | 主题 Logo                                                                                     | 否       | | ||||
| | `spec.homepage`                 | 主题网站                                                                                      | 否       | | ||||
| | `spec.repo`                     | 主题代码托管地址                                                                              | 否       | | ||||
| | `spec.issues`                   | 主题问题反馈地址,如果你的主题是开源在 GitHub 上,可以直接配置为 GitHub Issues 地址。            | 否       | | ||||
| | `spec.settingName`              | 设置表单定义的名称,需要同时创建对应的 `settings.yaml` 文件                                    | 否       | | ||||
| | `spec.configMapName`            | 设置持久化配置的 ConfigMap 名称                                                               | 否       | | ||||
| | `spec.customTemplates.post`     | 文章的自定义模板配置,详细文档可查阅 [模板路由](./template-route-mapping#custom-templates)     | 否       | | ||||
| | `spec.customTemplates.category` | 分类的自定义模板配置,详细文档可查阅 [模板路由](./template-route-mapping#custom-templates)     | 否       | | ||||
| | `spec.customTemplates.page`     | 独立页面的自定义模板配置,详细文档可查阅 [模板路由](./template-route-mapping#custom-templates) | 否       | | ||||
| | `spec.version`                  | 主题版本                                                                                      | 是       | | ||||
| | `spec.requires`                 | 所需 Halo 的运行版本                                                                          | 是       | | ||||
| | `spec.license`                  | 协议                                                                                          | 否       | | ||||
| 
 | ||||
| ## 更新配置 | ||||
| 
 | ||||
| 由于目前 `theme.yaml` 是持久化存储在数据库中的,不会在修改之后主动更新,所以我们在 Console 的主题页面添加了 `重载主题配置` 的选项。 | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## 从 1.x 迁移 | ||||
| 
 | ||||
| 为了方便主题开发者从 1.x 迁移,我们提供了工具用于迁移配置文件。 | ||||
| 
 | ||||
| 工具仓库地址:<https://github.com/halo-sigs/convert-theme-config-to-next> | ||||
| 
 | ||||
| ```bash | ||||
| # 1.x 版本主题 | ||||
| cd path/to/theme | ||||
| 
 | ||||
| npx @halo-dev/convert-theme-config-to-next theme | ||||
| ``` | ||||
| 
 | ||||
| 执行完成之后即可看到主题目录下生成了 `theme.2.0.yaml` 文件,重命名为 `theme.yaml` 即可。 | ||||
| 
 | ||||
| :::tip | ||||
| 转换完成之后需要修改 `metadata.name`、`spec.settingName` 和 `spec.configMapName`。 | ||||
| ::: | ||||
| @ -0,0 +1,10 @@ | ||||
| --- | ||||
| title: Finder API | ||||
| description: 本文档介绍 Finder API 的使用方法。 | ||||
| --- | ||||
| 
 | ||||
| import DocCardList from '@theme/DocCardList'; | ||||
| 
 | ||||
| 目前在主题模板中获取数据可以使用对应路由提供的 [模板变量](./template-variables),但为了满足在任意位置获取数据的需求,我们提供了 Finder API。 | ||||
| 
 | ||||
| <DocCardList /> | ||||
| @ -0,0 +1,187 @@ | ||||
| --- | ||||
| title: 文章分类 | ||||
| description: 文章分类 - CategoryFinder | ||||
| --- | ||||
| 
 | ||||
| import CategoryVo from "../vo/CategoryVo.md" | ||||
| import CategoryTreeVo from "../vo/CategoryTreeVo.md" | ||||
| 
 | ||||
| ## getByName(name) | ||||
| 
 | ||||
| ```js | ||||
| categoryFinder.getByName(name) | ||||
| ``` | ||||
| 
 | ||||
| ### 描述 | ||||
| 
 | ||||
| 根据 `metadata.name` 获取文章分类。 | ||||
| 
 | ||||
| ### 参数 | ||||
| 
 | ||||
| 1. `name:string` - 分类的唯一标识 `metadata.name`。 | ||||
| 
 | ||||
| ### 返回值 | ||||
| 
 | ||||
| [#CategoryVo](#categoryvo) | ||||
| 
 | ||||
| ### 示例 | ||||
| 
 | ||||
| ```html | ||||
| <div th:with="category = ${categoryFinder.getByName('category-foo')}"> | ||||
|   <a th:href="@{${category.status.permalink}}" th:text="${category.spec.displayName}"></a> | ||||
| </div> | ||||
| ``` | ||||
| 
 | ||||
| ## getByNames(names) | ||||
| 
 | ||||
| ```js | ||||
| categoryFinder.getByNames(names) | ||||
| ``` | ||||
| 
 | ||||
| ### 描述 | ||||
| 
 | ||||
| 根据一组 `metadata.name` 获取文章分类。 | ||||
| 
 | ||||
| ### 参数 | ||||
| 
 | ||||
| 1. `names:List<string>` - 分类的唯一标识 `metadata.name` 的集合。 | ||||
| 
 | ||||
| ### 返回值 | ||||
| 
 | ||||
| List<[#CategoryVo](#categoryvo)> | ||||
| 
 | ||||
| ### 示例 | ||||
| 
 | ||||
| ```html | ||||
| <div th:with="categories = ${categoryFinder.getByNames(['category-foo', 'category-bar'])}"> | ||||
|   <a th:each="category : ${categories}" th:href="@{${category.status.permalink}}" th:text="${category.spec.displayName}"></a> | ||||
| </div> | ||||
| ``` | ||||
| 
 | ||||
| ## list(page,size) | ||||
| 
 | ||||
| ```js | ||||
| categoryFinder.list(page,size) | ||||
| ``` | ||||
| 
 | ||||
| ### 描述 | ||||
| 
 | ||||
| 根据分页参数获取分类列表。 | ||||
| 
 | ||||
| ### 参数 | ||||
| 
 | ||||
| 1. `page:int` - 分页页码,从 1 开始 | ||||
| 2. `size:int` - 分页条数 | ||||
| 
 | ||||
| ### 返回值 | ||||
| 
 | ||||
| [#ListResult<CategoryVo\>](#listresultcategoryvo) | ||||
| 
 | ||||
| ### 示例 | ||||
| 
 | ||||
| ```html | ||||
| <ul th:with="categories = ${categoryFinder.list(1,10)}"> | ||||
|   <li th:each="category : ${categories.items}"> | ||||
|     <a th:href="@{${category.status.permalink}}" th:text="${category.spec.displayName}"></a> | ||||
|   </li> | ||||
| </ul> | ||||
| ``` | ||||
| 
 | ||||
| ## listAll() | ||||
| 
 | ||||
| ```js | ||||
| categoryFinder.listAll() | ||||
| ``` | ||||
| 
 | ||||
| ### 描述 | ||||
| 
 | ||||
| 获取所有文章分类。 | ||||
| 
 | ||||
| ### 参数 | ||||
| 
 | ||||
| 无 | ||||
| 
 | ||||
| ### 返回值 | ||||
| 
 | ||||
| List<[#CategoryVo](#categoryvo)> | ||||
| 
 | ||||
| ### 示例 | ||||
| 
 | ||||
| ```html | ||||
| <ul th:with="categories = ${categoryFinder.listAll()}"> | ||||
|   <li th:each="category : ${categories}"> | ||||
|     <a th:href="@{${category.status.permalink}}" th:text="${category.spec.displayName}"></a> | ||||
|   </li> | ||||
| </ul> | ||||
| ``` | ||||
| 
 | ||||
| ## listAsTree() | ||||
| 
 | ||||
| ```js | ||||
| categoryFinder.listAsTree() | ||||
| ``` | ||||
| 
 | ||||
| ### 描述 | ||||
| 
 | ||||
| 获取所有文章分类的多层级结构。 | ||||
| 
 | ||||
| ### 参数 | ||||
| 
 | ||||
| 无 | ||||
| 
 | ||||
| ### 返回值 | ||||
| 
 | ||||
| List<[#CategoryTreeVo](#categorytreevo)> | ||||
| 
 | ||||
| ### 示例 | ||||
| 
 | ||||
| ```html | ||||
| <div th:with="categories = ${categoryFinder.listAsTree()}"> | ||||
|   <ul> | ||||
|     <li th:replace="~{modules/category-tree :: single(categories=${categories})}" /> | ||||
|   </ul> | ||||
| </div> | ||||
| ``` | ||||
| 
 | ||||
| ```html title="/templates/category-tree.html" | ||||
| <ul th:fragment="next (categories)"> | ||||
|   <li th:fragment="single (categories)" th:each="category : ${categories}"> | ||||
|     <a th:href="@{${category.status.permalink}}"> | ||||
|       <span th:text="${category.spec.displayName}"> </span> | ||||
|     </a> | ||||
|     <th:block th:if="${not #lists.isEmpty(category.children)}"> | ||||
|       <th:block th:replace="~{modules/category-tree :: next (categories=${category.children})}"></th:block> | ||||
|     </th:block> | ||||
|   </li> | ||||
| </ul> | ||||
| ``` | ||||
| 
 | ||||
| ## 类型定义 | ||||
| 
 | ||||
| ### CategoryVo | ||||
| 
 | ||||
| <CategoryVo /> | ||||
| 
 | ||||
| ### ListResult<CategoryVo\> | ||||
| 
 | ||||
| ```json title="ListResult<CategoryVo>" | ||||
| { | ||||
|   "page": 0,                                   // 当前页码 | ||||
|   "size": 0,                                   // 每页条数 | ||||
|   "total": 0,                                  // 总条数 | ||||
|   "items": "List<#CategoryVo>",                // 分类列表数据 | ||||
|   "first": true,                               // 是否为第一页 | ||||
|   "last": true,                                // 是否为最后一页 | ||||
|   "hasNext": true,                             // 是否有下一页 | ||||
|   "hasPrevious": true,                         // 是否有上一页 | ||||
|   "totalPages": 0                              // 总页数 | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| - [#CategoryVo](#categoryvo) | ||||
| 
 | ||||
| ### CategoryTreeVo | ||||
| 
 | ||||
| <CategoryTreeVo /> | ||||
| 
 | ||||
| - [#CategoryTreeVo](#categorytreevo) | ||||
| @ -0,0 +1,155 @@ | ||||
| --- | ||||
| title: 评论 | ||||
| description: 评论 - CommentFinder | ||||
| --- | ||||
| 
 | ||||
| import CommentVo from "../vo/CommentVo.md" | ||||
| import ReplyVo from "../vo/ReplyVo.md" | ||||
| 
 | ||||
| ## getByName(name) | ||||
| 
 | ||||
| ```js | ||||
| commentFinder.getByName(name) | ||||
| ``` | ||||
| 
 | ||||
| ### 描述 | ||||
| 
 | ||||
| 根据 `metadata.name` 获取评论。 | ||||
| 
 | ||||
| ### 参数 | ||||
| 
 | ||||
| 1. `name:string` - 评论的唯一标识 `metadata.name`。 | ||||
| 
 | ||||
| ### 返回值 | ||||
| 
 | ||||
| [#CommentVo](#commentvo) | ||||
| 
 | ||||
| ### 示例 | ||||
| 
 | ||||
| ```html | ||||
| <div th:with="comment = ${commentFinder.getByName('comment-foo')}"> | ||||
|   <span th:text="${comment.spec.owner.displayName}"></span> | ||||
|   <div th:text="${comment.spec.content}"></div> | ||||
| </div> | ||||
| ``` | ||||
| 
 | ||||
| ## list(ref,page,size) | ||||
| 
 | ||||
| ```js | ||||
| commentFinder.list(ref,page,size) | ||||
| ``` | ||||
| 
 | ||||
| ### 描述 | ||||
| 
 | ||||
| 根据评论的 `metadata.name` 和分页参数获取回复列表。 | ||||
| 
 | ||||
| ### 参数 | ||||
| 
 | ||||
| 1. `ref:#Ref` - 评论的唯一标识 `metadata.name`。 | ||||
| 2. `page:int` - 分页页码,从 1 开始 | ||||
| 3. `size:int` - 分页条数 | ||||
| 
 | ||||
| - [#Ref](#ref) | ||||
| 
 | ||||
| ### 返回值 | ||||
| 
 | ||||
| [#ListResult<CommentVo\>](#listresultcommentvo) | ||||
| 
 | ||||
| ### 示例 | ||||
| 
 | ||||
| ```html | ||||
| <ul th:with="comments = ${commentFinder.list({ group: 'content.halo.run', version: 'v1alpha1', kind: 'Post', name: 'post-foo' },1,10)}"> | ||||
|   <li th:each="comment : ${comments.items}"> | ||||
|     <span th:text="${comment.spec.owner.displayName}"></span> | ||||
|     <div th:text="${comment.spec.content}"></div> | ||||
|   </li> | ||||
| </ul> | ||||
| ``` | ||||
| 
 | ||||
| ## listReply(commentName,page,size) | ||||
| 
 | ||||
| ```js | ||||
| commentFinder.listReply(commentName,page,size) | ||||
| ``` | ||||
| 
 | ||||
| ### 描述 | ||||
| 
 | ||||
| 根据评论的 `metadata.name` 和分页参数获取回复列表。 | ||||
| 
 | ||||
| ### 参数 | ||||
| 
 | ||||
| 1. `commentName:string` - 评论的唯一标识 `metadata.name`。 | ||||
| 1. `page:int` - 分页页码,从 1 开始 | ||||
| 2. `size:int` - 分页条数 | ||||
| 
 | ||||
| ### 返回值 | ||||
| 
 | ||||
| [#ListResult<ReplyVo\>](#listresultreplyvo) | ||||
| 
 | ||||
| ### 示例 | ||||
| 
 | ||||
| ```html | ||||
| <ul th:with="replies = ${commentFinder.listReply('comment-foo',1,10)}"> | ||||
|   <li th:each="reply : ${replies.items}"> | ||||
|     <span th:text="${reply.spec.owner.displayName}"></span> | ||||
|     <div th:text="${reply.spec.content}"></div> | ||||
|   </li> | ||||
| </ul> | ||||
| ``` | ||||
| 
 | ||||
| ## 类型定义 | ||||
| 
 | ||||
| ### CommentVo | ||||
| 
 | ||||
| <CommentVo /> | ||||
| 
 | ||||
| ### ListResult<CommentVo\> | ||||
| 
 | ||||
| ```json title="ListResult<CommentVo>" | ||||
| { | ||||
|   "page": 0,                                   // 当前页码 | ||||
|   "size": 0,                                   // 每页条数 | ||||
|   "total": 0,                                  // 总条数 | ||||
|   "items": "List<#CommentVo>",                 // 评论列表数据 | ||||
|   "first": true,                               // 是否为第一页 | ||||
|   "last": true,                                // 是否为最后一页 | ||||
|   "hasNext": true,                             // 是否有下一页 | ||||
|   "hasPrevious": true,                         // 是否有上一页 | ||||
|   "totalPages": 0                              // 总页数 | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| - [#CommentVo](#commentvo) | ||||
| 
 | ||||
| ### ReplyVo | ||||
| 
 | ||||
| <ReplyVo /> | ||||
| 
 | ||||
| ### ListResult<ReplyVo\> | ||||
| 
 | ||||
| ```json title="ListResult<ReplyVo>" | ||||
| { | ||||
|   "page": 0,                                   // 当前页码 | ||||
|   "size": 0,                                   // 每页条数 | ||||
|   "total": 0,                                  // 总条数 | ||||
|   "items": "List<#ReplyVo>",                   // 回复列表数据 | ||||
|   "first": true,                               // 是否为第一页 | ||||
|   "last": true,                                // 是否为最后一页 | ||||
|   "hasNext": true,                             // 是否有下一页 | ||||
|   "hasPrevious": true,                         // 是否有上一页 | ||||
|   "totalPages": 0                              // 总页数 | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| - [#ReplyVo](#replyvo) | ||||
| 
 | ||||
| ### Ref | ||||
| 
 | ||||
| ```json title="Ref" | ||||
| { | ||||
|   "group": "string", | ||||
|   "kind": "string", | ||||
|   "version": "string", | ||||
|   "name": "string" | ||||
| } | ||||
| ``` | ||||
| @ -0,0 +1,64 @@ | ||||
| --- | ||||
| title: 作者 | ||||
| description: 作者 - ContributorFinder | ||||
| --- | ||||
| 
 | ||||
| import ContributorVo from "../vo/ContributorVo.md" | ||||
| 
 | ||||
| ## getContributor(name) | ||||
| 
 | ||||
| ```js | ||||
| contributorFinder.getContributor(name) | ||||
| ``` | ||||
| 
 | ||||
| ### 描述 | ||||
| 
 | ||||
| 根据 `metadata.name` 获取作者。 | ||||
| 
 | ||||
| ### 参数 | ||||
| 
 | ||||
| 1. `name:string` - 作者的唯一标识 `metadata.name`。 | ||||
| 
 | ||||
| ### 返回值 | ||||
| 
 | ||||
| [#ContributorVo](#contributorvo) | ||||
| 
 | ||||
| ### 示例 | ||||
| 
 | ||||
| ```html | ||||
| <div th:with="contributor = ${contributorFinder.getContributor('contributor-foo')}"> | ||||
|   <h1 th:text="${contributor.displayName}"></h1> | ||||
| </div> | ||||
| ``` | ||||
| 
 | ||||
| ## getContributors(names) | ||||
| 
 | ||||
| ```js | ||||
| contributorFinder.getContributors(names) | ||||
| ``` | ||||
| 
 | ||||
| ### 描述 | ||||
| 
 | ||||
| 根据一组 `metadata.name` 获取作者。 | ||||
| 
 | ||||
| ### 参数 | ||||
| 
 | ||||
| 1. `names:List<string>` - 作者的唯一标识 `metadata.name` 的集合。 | ||||
| 
 | ||||
| ### 返回值 | ||||
| 
 | ||||
| List<[#ContributorVo](#contributorvo)> | ||||
| 
 | ||||
| ### 示例 | ||||
| 
 | ||||
| ```html | ||||
| <div th:with="contributors = ${contributorFinder.getContributors(['contributor-foo, 'contributor-bar'])}"> | ||||
|   <span th:each="contributor : ${contributors}" th:text="${contributor.displayName}"></span> | ||||
| </div> | ||||
| ``` | ||||
| 
 | ||||
| ## 类型定义 | ||||
| 
 | ||||
| ### ContributorVo | ||||
| 
 | ||||
| <ContributorVo /> | ||||
| @ -0,0 +1,87 @@ | ||||
| --- | ||||
| title: 导航菜单 | ||||
| description: 导航菜单 - MenuFinder | ||||
| --- | ||||
| 
 | ||||
| import MenuItemVo from "../vo/MenuItemVo.md" | ||||
| import MenuVo from "../vo/MenuVo.md" | ||||
| 
 | ||||
| ## getByName(name) | ||||
| 
 | ||||
| ```js | ||||
| menuFinder.getByName(name) | ||||
| ``` | ||||
| 
 | ||||
| ### 描述 | ||||
| 
 | ||||
| 根据 `metadata.name` 获取菜单。 | ||||
| 
 | ||||
| ### 参数 | ||||
| 
 | ||||
| 1. `name:string` - 菜单的唯一标识 `metadata.name`。 | ||||
| 
 | ||||
| ### 返回值 | ||||
| 
 | ||||
| [#MenuVo](#menuvo) | ||||
| 
 | ||||
| ### 示例 | ||||
| 
 | ||||
| ```html | ||||
| <div th:with="menu = ${menuFinder.getByName('menu-foo')}"> | ||||
|   <ul th:with="menuItems = ${menu.menuItems}"> | ||||
|     <li th:each="menuItem : ${menuItems}"> | ||||
|       <a | ||||
|         th:href="@{${menuItem.status.href}}" | ||||
|         th:text="${menuItem.status.displayName}" | ||||
|         th:target="${menuItem.spec.target?.value}" | ||||
|       > | ||||
|       </a> | ||||
|     </li> | ||||
|   </ul> | ||||
| </div> | ||||
| ``` | ||||
| 
 | ||||
| ## getPrimary() | ||||
| 
 | ||||
| ```js | ||||
| menuFinder.getPrimary() | ||||
| ``` | ||||
| 
 | ||||
| ### 描述 | ||||
| 
 | ||||
| 获取主菜单。 | ||||
| 
 | ||||
| ### 参数 | ||||
| 
 | ||||
| 无 | ||||
| 
 | ||||
| ### 返回值 | ||||
| 
 | ||||
| [#MenuVo](#menuvo) | ||||
| 
 | ||||
| ### 示例 | ||||
| 
 | ||||
| ```html | ||||
| <div th:with="menu = ${menuFinder.getPrimary()}"> | ||||
|   <ul th:with="menuItems = ${menu.menuItems}"> | ||||
|     <li th:each="menuItem : ${menuItems}"> | ||||
|       <a | ||||
|         th:href="@{${menuItem.status.href}}" | ||||
|         th:text="${menuItem.status.displayName}" | ||||
|         th:target="${menuItem.spec.target?.value}" | ||||
|       > | ||||
|       </a> | ||||
|     </li> | ||||
|   </ul> | ||||
| </div> | ||||
| ``` | ||||
| 
 | ||||
| ## 类型定义 | ||||
| 
 | ||||
| ### MenuVo | ||||
| 
 | ||||
| <MenuVo /> | ||||
| 
 | ||||
| ### MenuItemVo | ||||
| 
 | ||||
| <MenuItemVo /> | ||||
Some files were not shown because too many files have changed in this diff Show More
					Loading…
					
					
				
		Reference in new issue