refactor: update permission control docs for plugin (#443)

### What this PR does?
重构权限控制部分文档并调整相应结构

```release-note
None
```
This commit is contained in:
guqing
2024-10-25 16:01:56 +08:00
committed by GitHub
parent e3125c4547
commit 717ee402f4
189 changed files with 1014 additions and 454 deletions

View File

@@ -0,0 +1,76 @@
---
title: Web 过滤器
description: 为 Web 请求提供过滤器扩展点,可用于对请求进行拦截、修改等操作。
---
在现代的 Web 应用开发中过滤器Filter是一个非常重要的概念。你可以使用 `run.halo.app.security.AdditionalWebFilter` 在服务器处理请求之前或之后执行特定的任务。
通过实现这个接口,开发者可以自定义过滤逻辑,用于处理进入和离开应用程序的 HTTP 请求和响应。
AdditionalWebFilter 能做什么?
1. 认证与授权: AdditionalWebFilter 可以用来检查用户是否登录,或者是否有权限访问某个资源。
2. 日志记录与审计: 在请求处理之前或之后记录日志,帮助了解应用程序的使用情况。
3. 请求重构: 修改请求数据,例如添加、删除或修改请求头或请求参数。
4. 响应处理: 修改响应,例如设置通用的响应头。
5. 性能监控: 记录处理请求所需的时间,用于性能分析。
6. 异常处理: 统一处理请求过程中抛出的异常。
7. ......
## 使用示例
以下是一个使用 `AdditionalWebFilter` 来拦截 `/login` 请求实现用户名密码登录的示例:
```java
@Component
public class UsernamePasswordAuthenticator implements AdditionalWebFilter {
final ServerWebExchangeMatcher requiresMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/login");
@Override
@NonNull
public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {
return this.requiresAuthenticationMatcher.matches(exchange)
.filter((matchResult) -> {
return matchResult.isMatch();
}).flatMap((matchResult) -> {
return this.authenticationConverter.convert(exchange);
}).switchIfEmpty(chain.filter(exchange)
.then(Mono.empty()))
.flatMap((token) -> {
return this.authenticate(exchange, chain, token);
}).onErrorResume(AuthenticationException.class, (ex) -> {
return this.authenticationFailureHandler.onAuthenticationFailure(new WebFilterExchange(exchange, chain), ex);
});
}
@Override
public int getOrder() {
return SecurityWebFiltersOrder.FORM_LOGIN.getOrder();
}
}
```
1. `filter` 方法中的 `chain.filter(exchange)` 表示继续执行后续的过滤器,如果不调用这个方法,请求将不会继续执行后续的过滤器或目标处理程序。
2. `getOrder` 方法用于指定过滤器的执行顺序,比如 `SecurityWebFiltersOrder.FORM_LOGIN.getOrder()` 表示在 Spring Security 的表单登录过滤器之前执行,参考:[SecurityWebFiltersOrder](https://github.com/spring-projects/spring-security/blob/main/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java)。
`AdditionalWebFilter` 对应的 `ExtensionPointDefinition` 如下:
```yaml
apiVersion: plugin.halo.run/v1alpha1
kind: ExtensionPointDefinition
metadata:
name: additional-webfilter
spec:
className: run.halo.app.security.AdditionalWebFilter
displayName: AdditionalWebFilter
type: MULTI_INSTANCE
description: "Contract for interception-style, chained processing of Web requests that may be used to
implement cross-cutting, application-agnostic requirements such as security, timeouts, and others."
```
即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName``additional-webfilter`
以下是一些可以参考的项目示例:
- [OAuth2 第三方登录插件](https://github.com/halo-sigs/plugin-oauth2)
- [Halo 用户名密码登录](https://github.com/halo-dev/halo/blob/main/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordAuthenticator.java)

View File

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

View File

@@ -0,0 +1,81 @@
---
title: 认证安全过滤器
description: 提供 Security WebFilter 扩展点插件可实现自定义认证逻辑例如用户名密码认证JWT 认证,匿名认证。
---
此前Halo 提供了 AdditionalWebFilter 作为扩展点供插件扩展认证相关的功能。但是近期我们明确了 AdditionalWebFilter
的使用用途,故不再作为认证的扩展点。
目前Halo 提供了三种认证扩展点:表单登录认证、普通认证和匿名认证。
## 表单登录FormLogin
示例如下:
```java
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import run.halo.app.security.FormLoginSecurityWebFilter;
@Component
public class MyFormLoginSecurityWebFilter implements FormLoginSecurityWebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// Do your logic here
return chain.filter(exchange);
}
}
```
## 普通认证Authentication
示例如下:
```java
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import run.halo.app.security.AuthenticationSecurityWebFilter;
@Component
public class MyAuthenticationSecurityWebFilter implements AuthenticationSecurityWebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// Do your logic here
return chain.filter(exchange);
}
}
```
## 匿名认证Anonymous Authentication
示例如下:
```java
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import run.halo.app.security.AnonymousAuthenticationSecurityWebFilter;
@Component
public class MyAnonymousAuthenticationSecurityWebFilter
implements AnonymousAuthenticationSecurityWebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// Do your logic here
return chain.filter(exchange);
}
}
```
我们在实现扩展点的时候需要注意:如果当前请求不满足认证条件,请一定要调用 `chain.filter(exchange)`,给其他 filter 留下机会。
后续会根据需求实现其他认证相关的扩展点。

View File

@@ -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/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)

View File

@@ -0,0 +1,41 @@
---
title: 评论组件
description: 用于自定义评论组件,可在主题端使用其他评论组件。
---
评论组件扩展点用于自定义主题端使用的评论组件Halo 通过插件提供了一个默认的评论组件,如果你需要使用其他的评论组件,那么可以通过实现该扩展点来自定义评论组件。
```java
public interface CommentWidget extends ExtensionPoint {
String ENABLE_COMMENT_ATTRIBUTE = CommentWidget.class.getName() + ".ENABLE";
void render(ITemplateContext context, IProcessableElementTag tag,
IElementTagStructureHandler structureHandler);
}
```
其中 `render` 方法用于在主题端模板中渲染评论组件,参数:
- `context` 为模板上下文,包含执行模板的上下文:变量、模板数据等。
- 参数 `tag``<halo:comment />` 标签它包含元素的名称及其属性
- `structureHandler` 是一个特殊的对象,它允许 `CommentWidget` 向引擎发出指令,指示模板引擎应根据处理器的执行而执行哪些操作。
`CommentWidget` 对应的 `ExtensionPointDefinition` 如下:
```yaml
apiVersion: plugin.halo.run/v1alpha1
kind: ExtensionPointDefinition
metadata:
name: comment-widget
spec:
className: run.halo.app.theme.dialect.CommentWidget
displayName: CommentWidget
type: SINGLETON
description: "Provides an extension point for the comment widget on the theme-side."
```
即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName``comment-widget`
参考:[Thymeleaf IElementTagProcessor 文档](https://www.thymeleaf.org/doc/tutorials/3.1/extendingthymeleaf.html#element-tag-processors-ielementtagprocessor)。
参考:[Halo 默认评论组件的实现](https://github.com/halo-dev/plugin-comment-widget/blob/main/src/main/java/run/halo/comment/widget/DefaultCommentWidget.java)。

View File

@@ -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` 自定义模型对象被称之为扩展定义,用于描述该扩展的信息,例如:扩展的名称、描述、对应扩展点的对象名称等。
:::
关于如何在插件中声明自定义模型对象请参考:[自定义模型](../../api-reference/server/extension.md#declare-extension-object)
以下是目前已支持的扩展点列表:
```mdx-code-block
import DocCardList from '@theme/DocCardList';
<DocCardList />
```

View File

@@ -0,0 +1,60 @@
---
title: 通知器
description: 为以何种方式向用户发送通知提供的扩展点。
---
通知器扩展点是用于扩展为 Halo 通知系统提供更多通知方式的扩展点例如邮件、短信、WebHook 等。
```java
public interface ReactiveNotifier extends ExtensionPoint {
Mono<Void> notify(NotificationContext context);
}
```
`notify` 方法用于发送通知参数context 为通知上下文,包含通知的内容、接收者、通知配置等信息。
除了实现该扩展点并声明 `ExtensionDefinition` 自定义模型对象外,还需要声明一个 `NotifierDescriptor` 自定义模型对象用于描述通知器,例如:
```yaml
apiVersion: notification.halo.run/v1alpha1
kind: NotifierDescriptor
metadata:
name: default-email-notifier
spec:
displayName: '邮件通知'
description: '通过邮件将通知发送给用户'
notifierExtName: 'halo-email-notifier'
senderSettingRef:
name: 'notifier-setting-for-email'
group: 'sender'
#receiverSettingRef:
# name: ''
# group: ''
```
- `notifierExtName` 为通知器扩展的自定义模型对象名称
- `senderSettingRef` 用于声明通知器的发送者配置例如邮件通知器的发送者配置为SMTP 服务器地址、端口、用户名、密码等,如果没有可以不配置,参考:[表单定义](../../../form-schema.md)
- `name` 为发送者配置的名称,它是一个 `Setting` 自定义模型对象的名称。
- `group` 用于引用到一个具体的配置 Schema 组,它是一个 `Setting` 自定义模型对象中描述的 `formSchema``group`,由于 `Setting` 可以声明多个配置分组但通知器的发送者配置只能有在一个组,因此需要指定一个组。
- `receiverSettingRef` 用于声明通知器的接收者配置,例如:邮件通知器的接收者配置为:接收者邮箱地址,如果没有可以不配置,`name``group` 配置同 `senderSettingRef`
当配置了 `senderSettingRef` 后,触发通知时 `notify` 方法的 `context` 参数中会包含 `senderConfig` 即为发送者配置的值,`receiverConfig` 同理。
`ReactiveNotifier` 对应的 `ExtensionPointDefinition` 如下:
```yaml
apiVersion: plugin.halo.run/v1alpha1
kind: ExtensionPointDefinition
metadata:
name: reactive-notifier
spec:
className: run.halo.app.notification.ReactiveNotifier
displayName: Notifier
type: MULTI_INSTANCE
description: "Provides a way to extend the notifier to send notifications to users."
```
即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName``reactive-notifier`
使用案例可以参考:[Halo 邮件通知器](https://github.com/halo-dev/halo/blob/main/application/src/main/java/run/halo/app/notification/EmailNotifier.java)

View File

@@ -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 格式。

View File

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

View File

@@ -0,0 +1,77 @@
---
title: 主题端 Halo Footer 标签处理
description: 提供扩展主题端 HTML 页面中的 <halo:footer/> 标签内容处理的方法。
---
Halo 为主题端模板提供了自定义标签 `<halo:footer/>` 的处理扩展点,以便可以添加额外的页脚内容如版权信息、备案号等。
## 使用场景
- 添加备案号
- 添加版权信息
- 添加统计代码
- 添加自定义脚本
- 添加自定义链接
## 扩展点定义
扩展 `<halo:footer/>` 标签的扩展点定义为 `TemplateFooterProcessor`,对应的 `ExtensionPoint` 类型为 `MULTI_INSTANCE`,即可以有多个实现类。
```java
public interface TemplateFooterProcessor extends ExtensionPoint {
Mono<Void> process(ITemplateContext context, IProcessableElementTag tag,
IElementTagStructureHandler structureHandler, IModel model);
}
```
`TemplateFooterProcessor` 对应的 `ExtensionPointDefinition` 资源描述如下:
```yaml
apiVersion: plugin.halo.run/v1alpha1
kind: ExtensionPointDefinition
metadata:
name: template-footer-processor
spec:
className: run.halo.app.theme.dialect.TemplateFooterProcessor
displayName: 页脚标签内容处理器
type: MULTI_INSTANCE
description: "提供用于扩展 <halo:footer/> 标签内容的扩展方式。"
```
即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName``template-footer-processor`
## 示例实现
以下是一个简单的 TemplateFooterProcessor 插件实现示例:
```java
@Component
public class FakeFooterCodeInjection implements TemplateFooterProcessor {
@Override
public Mono<Void> process(ITemplateContext context, IProcessableElementTag tag,
IElementTagStructureHandler structureHandler, IModel model) {
var factory = context.getModelFactory();
// regular footer text
var copyRight = factory.createText("<div>© 2024 Halo</div>");
model.add(copyRight);
return Mono.empty();
}
}
```
声明 ExtensionDefinition 自定义模型对象时对应的 extensionPointName 为 template-footer-processor。
```yaml
apiVersion: plugin.halo.run/v1alpha1
kind: ExtensionDefinition
metadata:
name: custom-footer-extension
spec:
extensionPointName: template-footer-processor
className: com.example.FakeFooterCodeInjection
displayName: "Custom Footer Extension"
description: "Adds custom footer content."
icon: 'some-icon'
```

View File

@@ -0,0 +1,87 @@
---
title: 主题端 HTML Head 标签处理
description: 提供扩展主题端 HTML 页面中的 Head 标签内容处理的方法,干预 HTML 页面的 Head 标签内容。
---
主题端 HTML Head 标签处理扩展点的作用是干预 HTML 页面中的 Head 标签内容,可以添加自定义的 CSS、JS 及 meta 标签等,以满足特定的定制化需求。
## 使用场景
- **添加自定义样式或脚本**:在 HTML Head 中插入额外的 CSS 文件或 JavaScript 脚本文件,以增强页面的交互性或样式。
- **定制 Meta 标签**:为特定页面添加或修改 meta 标签,如描述、作者、关键词等,以提高 SEO 和页面信息的完整性。
- **引入第三方库**:引入第三方库(如 Google Fonts、Font Awesome 等),以满足页面的特殊功能或风格需求。
- **定制 Open Graph 等社交媒体标签**:为社交媒体分享优化页面标签内容。
## 扩展点定义
主题端 HTML Head 标签处理的扩展点定义为 `TemplateHeadProcessor`,对应的 `ExtensionPoint` 类型为 `MULTI_INSTANCE`,即可以有多个实现类。
```java
@FunctionalInterface
public interface TemplateHeadProcessor extends ExtensionPoint {
Mono<Void> process(ITemplateContext context, IModel model,
IElementModelStructureHandler structureHandler);
}
```
`TemplateHeadProcessor` 对应的 `ExtensionPointDefinition` 资源描述如下:
```yaml
apiVersion: plugin.halo.run/v1alpha1
kind: ExtensionPointDefinition
metadata:
name: template-head-processor
spec:
className: run.halo.app.theme.dialect.TemplateHeadProcessor
displayName: TemplateHeadProcessor
type: MULTI_INSTANCE
description: "Provides a way to extend the head tag content in the theme-side HTML page."
```
即声明 `ExtensionDefinition` 自定义模型对象时对应的 `extensionPointName``template-head-processor`
## 示例实现
以下是一个简单的 TemplateHeadProcessor 插件实现示例:
```java
@Component
public class CustomHeadProcessor implements TemplateHeadProcessor {
@Override
public Mono<Void> process(ITemplateContext context, IModel model,
IElementModelStructureHandler structureHandler) {
// 添加自定义 CSS 文件
model.add(context.createStandaloneElementTag("link",
"rel", "stylesheet",
"href", "/custom/styles.css"));
// 添加自定义 Meta 标签
model.add(context.createStandaloneElementTag("meta",
"name", "author",
"content", "Your Name"));
return Mono.empty();
}
}
```
声明 ExtensionDefinition 自定义模型对象时对应的 extensionPointName 为 template-head-processor。
```yaml
apiVersion: plugin.halo.run/v1alpha1
kind: ExtensionDefinition
metadata:
name: custom-head-extension
spec:
extensionPointName: template-head-processor
className: com.example.CustomHeadProcessor
displayName: "Custom Head Extension"
description: "Adds custom CSS and meta tags to the head section."
```
## 使用此扩展点的插件
- [集成 highlight.js 为文章提供代码块高亮渲染](https://github.com/halo-sigs/plugin-highlightjs)
- [集成 lightgallery.js支持在内容页面放大显示图片](https://github.com/halo-sigs/plugin-lightgallery)
- [Halo 2.0 对 Umami 的集成](https://github.com/halo-sigs/plugin-umami)

View File

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

View File

@@ -0,0 +1,98 @@
---
title: 附件数据列表操作菜单
description: 扩展附件数据列表操作菜单 - attachment:list-item:operation:create
---
此扩展点用于扩展附件数据列表的操作菜单项。
![附件数据列表操作菜单](/img/developer-guide/plugin/extension-points/ui/attachment-list-item-operation-create.png)
## 定义方式
```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](https://github.com/halo-dev/plugin-s3)
## 类型定义
### Attachment
```mdx-code-block
import Attachment from "./interface/Attachment.md";
<Attachment />
```

View File

@@ -0,0 +1,146 @@
---
title: 附件选择选项卡
description: 扩展附件选择组件的选项卡 - attachment:selector:create
---
此扩展点用于扩展附件选择组件的选项卡,目前 Halo 仅包含内置的附件库,你可以通过此扩展点添加自定义的选项卡。
![附件选择选项卡](/img/developer-guide/plugin/extension-points/ui/attachment-selector-create.png)
## 定义方式
```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](https://github.com/halo-sigs/plugin-unsplash)

View File

@@ -0,0 +1,41 @@
---
title: 备份数据列表操作菜单
description: 扩展备份数据列表操作菜单 - backup:list-item:operation:create
---
此扩展点用于扩展备份数据列表的操作菜单项。
![备份数据列表操作菜单](/img/developer-guide/plugin/extension-points/ui/backup-list-item-operation-create.png)
## 定义方式
```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 />
```

View File

@@ -0,0 +1,36 @@
---
title: 备份页面选项卡
description: 扩展备份页面选项卡 - backup:tabs:create
---
此扩展点可以针对备份页面扩展更多关于 UI 的功能,比如定时备份设置、备份到第三方云存储等。
![备份页面选项卡](/img/developer-guide/plugin/extension-points/ui/backup-tabs-create.png)
## 定义方式
```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[]; // 选项卡权限
}
```

View File

@@ -0,0 +1,81 @@
---
title: 评论数据列表操作菜单
description: 扩展评论数据列表操作菜单 - comment:list-item:operation:create
---
此扩展点用于扩展评论数据列表的操作菜单项。
![评论数据列表操作菜单](/img/developer-guide/plugin/extension-points/ui/comment-list-item-operation-create.png)
## 定义方式
```ts
export default definePlugin({
extensionPoints: {
"comment:list-item:operation:create": (
comment: Ref<ListedComment>
): OperationItem<ListedComment>[] | Promise<OperationItem<ListedComment>[]> => {
return [
{
priority: 10,
component: markRaw(VDropdownItem),
props: {},
action: (item?: ListedComment) => {
// do something
},
label: "foo",
hidden: false,
permissions: [],
children: [],
},
];
},
},
});
```
```mdx-code-block
import OperationItem from "./interface/OperationItem.md";
<OperationItem />
```
## 示例
此示例将实现一个操作菜单项。
```ts
import type { ListedComment } from "@halo-dev/api-client";
import { VDropdownItem } from "@halo-dev/components";
import { definePlugin } from "@halo-dev/console-shared";
import { markRaw } from "vue";
export default definePlugin({
extensionPoints: {
"comment:list-item:operation:create": () => {
return [
{
priority: 21,
component: markRaw(VDropdownItem),
label: "测试评论菜单",
visible: true,
permissions: [],
action: async (comment: ListedComment) => {
console.log(comment)
},
},
];
},
},
});
```
## 类型定义
### ListedComment
```mdx-code-block
import ListedComment from "./interface/ListedComment.md";
<ListedComment />
```

View File

@@ -0,0 +1,114 @@
---
title: 评论来源显示
description: 扩展评论来源显示 - comment:subject-ref:create
---
Console 的评论管理列表的评论来源默认仅支持显示来自文章和页面的评论,如果其他插件中的业务模块也使用了评论,那么就可以通过该拓展点来扩展评论来源的显示。
:::info 提示
此扩展点需要后端配合使用,请参考 [评论主体展示](../server/comment-subject.md)。
:::
![评论来源显示](/img/developer-guide/plugin/extension-points/ui/comment-subject-ref-create.png)
## 定义方式
```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](https://github.com/halo-sigs/plugin-moments)

View File

@@ -0,0 +1,479 @@
---
title: 默认编辑器
description: 扩展默认编辑器 - default:editor:extension:create
---
此扩展点用于扩展默认编辑器的功能。
## 定义方式
```ts
export default definePlugin({
extensionPoints: {
"default:editor:extension:create": (): AnyExtension[] | Promise<AnyExtension[]> => {
return [FooExtension];
},
},
});
```
:::info 提示
AnyExtension 类型来自 [Tiptap](https://github.com/ueberdosis/tiptap),这意味着 Halo 默认编辑器的扩展点返回类型与 Tiptap 的扩展完全一致Tiptap 的扩展文档可参考:[https://tiptap.dev/docs/editor/api/extensions](https://tiptap.dev/docs/editor/api/extensions)。此外Halo 也为默认编辑器的扩展提供了一些独有的参数,用于实现工具栏、指令等扩展。
:::
### Halo 独有扩展
阅读此文当前请确保已经熟悉了 Tiptap 的扩展文档这里将介绍如何对编辑器的功能进行扩展包括但不限于扩展工具栏、悬浮工具栏、Slash Command、拖拽功能等。
目前支持的所有扩展类型如下所示:
```ts
export interface ExtensionOptions {
// 顶部工具栏扩展
getToolbarItems?: ({
editor,
}: {
editor: Editor;
}) => ToolbarItem | ToolbarItem[];
// Slash Command 扩展
getCommandMenuItems?: () => CommandMenuItem | CommandMenuItem[];
// 悬浮菜单扩展
getBubbleMenu?: ({ editor }: { editor: Editor }) => NodeBubbleMenu;
// 工具箱扩展
getToolboxItems?: ({
editor,
}: {
editor: Editor;
}) => ToolboxItem | ToolboxItem[];
// 拖拽扩展
getDraggable?: ({ editor }: { editor: Editor }) => DraggableItem | boolean;
}
```
#### 1. 顶部工具栏扩展
编辑器顶部功能区域内容的扩展,通常用于增加用户常用操作,例如文本加粗、变更颜色等。
![顶部工具栏扩展](/img/developer-guide/plugin/extension-points/ui/default-editor-extension-toolbar.png)
在 [https://github.com/halo-sigs/richtext-editor/pull/16](https://github.com/halo-sigs/richtext-editor/pull/16) 中,我们实现了对顶部工具栏的扩展,如果需要添加额外的功能,只需要在具体的 Tiptap Extension 中的 `addOptions` 中定义 `getToolbarItems` 函数即可,如:
```ts
{
addOptions() {
return {
...this.parent?.(),
getToolbarItems({ editor }: { editor: Editor }) {
return []
},
};
},
}
```
其中 `getToolbarItems` 即为对顶部工具栏的扩展。其返回类型为:
```ts
// 顶部工具栏扩展
getToolbarItems?: ({
editor,
}: {
editor: Editor;
}) => ToolbarItem | ToolbarItem[];
// 工具栏
export interface ToolbarItem {
priority: number;
component: Component;
props: {
editor: Editor;
isActive: boolean;
disabled?: boolean;
icon?: Component;
title?: string;
action?: () => void;
};
children?: ToolbarItem[];
}
```
如下为 [`Bold`](https://github.com/halo-dev/halo/blob/main/console/packages/editor/src/extensions/bold/index.ts) 扩展中对于 `getToolbarItems` 的扩展示例:
```ts
addOptions() {
return {
...this.parent?.(),
getToolbarItems({ editor }: { editor: Editor }) {
return {
priority: 40,
component: markRaw(ToolbarItem),
props: {
editor,
isActive: editor.isActive("bold"),
icon: markRaw(MdiFormatBold),
title: i18n.global.t("editor.common.bold"),
action: () => editor.chain().focus().toggleBold().run(),
},
};
},
};
},
```
#### 2. 工具箱扩展
编辑器工具箱区域的扩展,可用于增加编辑器附属操作,例如插入表格,插入第三方组件等功能。
![工具箱扩展](/img/developer-guide/plugin/extension-points/ui/default-editor-extension-toolbox.png)
在 [https://github.com/halo-sigs/richtext-editor/pull/27](https://github.com/halo-sigs/richtext-editor/pull/27) 中,我们实现了对编辑器工具箱区域的扩展,如果需要添加额外的功能,只需要在具体的 Tiptap Extension 中的 `addOptions` 中定义 `getToolboxItems` 函数即可,如:
```ts
{
addOptions() {
return {
...this.parent?.(),
getToolboxItems({ editor }: { editor: Editor }) {
return []
},
};
},
}
```
其中 `getToolboxItems` 即为对工具箱的扩展。其返回类型为:
```ts
// 工具箱扩展
getToolboxItems?: ({
editor,
}: {
editor: Editor;
}) => ToolboxItem | ToolboxItem[];
export interface ToolboxItem {
priority: number;
component: Component;
props: {
editor: Editor;
icon?: Component;
title?: string;
description?: string;
action?: () => void;
};
}
```
如下为 [`Table`](https://github.com/halo-dev/halo/blob/main/console/packages/editor/src/extensions/table/index.ts) 扩展中对于 `getToolboxItems` 工具箱的扩展示例:
```ts
addOptions() {
return {
...this.parent?.(),
getToolboxItems({ editor }: { editor: Editor }) {
return {
priority: 15,
component: markRaw(ToolboxItem),
props: {
editor,
icon: markRaw(MdiTablePlus),
title: i18n.global.t("editor.menus.table.add"),
action: () =>
editor
.chain()
.focus()
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
.run(),
},
};
},
}
}
```
#### 3. Slash Command 扩展
Slash Command (斜杠命令)的扩展,可用于在当前行快捷执行功能操作,例如转换当前行为标题、在当前行添加代码块等功能。
![Slash Command 扩展](/img/developer-guide/plugin/extension-points/ui/default-editor-extension-slash-command.png)
在 [https://github.com/halo-sigs/richtext-editor/pull/16](https://github.com/halo-sigs/richtext-editor/pull/16) 中,我们实现了对 Slash Command 指令的扩展,如果需要添加额外的功能,只需要在具体的 Tiptap Extension 中的 `addOptions` 中定义 `getCommandMenuItems` 函数即可,如:
```ts
{
addOptions() {
return {
...this.parent?.(),
getCommandMenuItems() {
return []
},
};
},
}
```
其中 `getCommandMenuItems` 即为对工具箱的扩展。其返回类型为:
```ts
// Slash Command 扩展
getCommandMenuItems?: () => CommandMenuItem | CommandMenuItem[];
export interface CommandMenuItem {
priority: number;
icon: Component;
title: string;
keywords: string[];
command: ({ editor, range }: { editor: Editor; range: Range }) => void;
}
```
如下为 [`Table`](https://github.com/halo-dev/halo/blob/main/console/packages/editor/src/extensions/table/index.ts) 扩展中对于 `getCommandMenuItems` 的扩展示例:
```ts
addOptions() {
return {
...this.parent?.(),
getCommandMenuItems() {
return {
priority: 120,
icon: markRaw(MdiTable),
title: "editor.extensions.commands_menu.table",
keywords: ["table", "biaoge"],
command: ({ editor, range }: { editor: Editor; range: Range }) => {
editor
.chain()
.focus()
.deleteRange(range)
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
.run();
},
};
},
}
}
```
#### 4. 悬浮菜单扩展
编辑器悬浮菜单的扩展。可用于支持目标元素组件的功能扩展及操作简化。例如 `Table` 扩展中的添加下一列、添加上一列等操作。
![悬浮菜单扩展](/img/developer-guide/plugin/extension-points/ui/default-editor-extension-bubble-menu.png)
在 [https://github.com/halo-sigs/richtext-editor/pull/38](https://github.com/halo-sigs/richtext-editor/pull/38) 中,我们重构了对编辑器悬浮区域的扩展,如果需要对某个块进行支持,只需要在具体的 Tiptap Extension 中的 `addOptions` 中定义 `getBubbleMenu` 函数即可,如:
```ts
{
addOptions() {
return {
...this.parent?.(),
getBubbleMenu({ editor }: { editor: Editor }) {
return []
},
};
},
}
```
其中 `getBubbleMenu` 即为对悬浮菜单的扩展。其返回类型为:
```ts
// 悬浮菜单扩展
getBubbleMenu?: ({ editor }: { editor: Editor }) => NodeBubbleMenu;
interface BubbleMenuProps {
pluginKey?: string; // 悬浮菜单插件 Key建议命名方式 xxxBubbleMenu
editor?: Editor;
shouldShow: (props: { // 悬浮菜单显示的条件
editor: Editor;
node?: HTMLElement;
view?: EditorView;
state?: EditorState;
oldState?: EditorState;
from?: number;
to?: number;
}) => boolean;
tippyOptions?: Record\<string, unknown\>; // 可自由定制悬浮菜单所用的 tippy 组件的选项
getRenderContainer?: (node: HTMLElement) => HTMLElement; // 悬浮菜单所基准的 DOM
defaultAnimation?: boolean; // 是否启用默认动画。默认为 true
}
// 悬浮菜单
export interface NodeBubbleMenu extends BubbleMenuProps {
component?: Component; // 不使用默认的样式,与 items 二选一
items?: BubbleItem[]; // 悬浮菜单子项,使用默认的形式进行,与 items 二选一
}
// 悬浮菜单子项
export interface BubbleItem {
priority: number; // 优先级,数字越小优先级越大,越靠前
component?: Component; // 完全自定义子项样式
props: {
isActive: ({ editor }: { editor: Editor }) => boolean; // 当前功能是否已经处于活动状态
visible?: ({ editor }: { editor: Editor }) => boolean; // 是否显示当前子项
icon?: Component; // 图标
iconStyle?: string; // 图标自定义样式
title?: string; // 标题
action?: ({ editor }: { editor: Editor }) => Component | void; // 点击子项后的操作,如果返回 Component则会将其包含在下拉框中。
};
}
```
如下为 [`Table`](https://github.com/halo-dev/halo/blob/main/console/packages/editor/src/extensions/table/index.ts) 扩展中对于 `getBubbleMenu` 悬浮菜单的部分扩展示例:
```ts
addOptions() {
return {
...this.parent?.(),
getBubbleMenu({ editor }) {
return {
pluginKey: "tableBubbleMenu",
shouldShow: ({ state }: { state: EditorState }): boolean => {
return isActive(state, Table.name);
},
getRenderContainer(node) {
let container = node;
if (container.nodeName === "#text") {
container = node.parentElement as HTMLElement;
}
while (
container &&
container.classList &&
!container.classList.contains("tableWrapper")
) {
container = container.parentElement as HTMLElement;
}
return container;
},
tippyOptions: {
offset: [26, 0],
},
items: [
{
priority: 10,
props: {
icon: markRaw(MdiTableColumnPlusBefore),
title: i18n.global.t("editor.menus.table.add_column_before"),
action: () => editor.chain().focus().addColumnBefore().run(),
},
},
]
}
}
}
}
```
#### 5. 拖拽功能扩展
拖拽功能的扩展,可用于支持当前块元素的拖拽功能。
![拖拽功能扩展](/img/developer-guide/plugin/extension-points/ui/default-editor-extension-drag.png)
在 [https://github.com/halo-sigs/richtext-editor/pull/48](https://github.com/halo-sigs/richtext-editor/pull/48) 中,我们实现了对所有元素的拖拽功能,如果需要让当前扩展支持拖拽,只需要在具体的 Tiptap Extension 中的 `addOptions` 中定义 `getDraggable` 函数,并让其返回 true 即可。如:
```ts
{
addOptions() {
return {
...this.parent?.(),
getDraggable() {
return true;
},
};
},
}
```
其中 `getDraggable` 即为为当前扩展增加可拖拽的功能。其返回类型为:
```ts
// 拖拽扩展
getDraggable?: ({ editor }: { editor: Editor }) => DraggableItem | boolean;
export interface DraggableItem {
getRenderContainer?: ({ // 拖拽按钮计算偏移位置的基准 DOM
dom,
view,
}: {
dom: HTMLElement;
view: EditorView;
}) => DragSelectionNode;
handleDrop?: ({ // 完成拖拽功能之后的处理。返回 true 则会阻止拖拽的发生
view,
event,
slice,
insertPos,
node,
selection,
}: {
view: EditorView;
event: DragEvent;
slice: Slice;
insertPos: number;
node: Node;
selection: Selection;
}) => boolean | void;
allowPropagationDownward?: boolean; // 是否允许拖拽事件向内部传播,
}
export interface DragSelectionNode {
$pos?: ResolvedPos;
node?: Node;
el: HTMLElement;
nodeOffset?: number;
dragDomOffset?: {
x: number;
y: number;
};
}
```
> 拖拽会从父 Node 节点开始触发,直到找到一个实现 `getDraggable` 的扩展,如果没有找到,则不会触发拖拽事件。父 Node 可以通过 `allowPropagationDownward` 来控制是否允许拖拽事件向内部传播。如果 `allowPropagationDownward` 设置为 true则会继续向内部寻找实现 `getDraggable` 的扩展,如果没有找到,则触发父 Node 的 `getDraggable` 实现,否则继续进行传播。
如下为 [`Iframe`](https://github.com/halo-dev/halo/blob/main/console/packages/editor/src/extensions/iframe/index.ts) 扩展中对于 `getDraggable` 拖拽功能的扩展示例:
```ts
addOptions() {
return {
...this.parent?.(),
getDraggable() {
return {
getRenderContainer({ dom, view }) {
let container = dom;
while (
container.parentElement &&
container.parentElement.tagName !== "P"
) {
container = container.parentElement;
}
if (container) {
container = container.firstElementChild
?.firstElementChild as HTMLElement;
}
let node;
if (container.firstElementChild) {
const pos = view.posAtDOM(container.firstElementChild, 0);
const $pos = view.state.doc.resolve(pos);
node = $pos.node();
}
return {
node: node,
el: container as HTMLElement,
};
},
};
},
}
}
```
## 实现案例
- [https://github.com/halo-sigs/plugin-hybrid-edit-block](https://github.com/halo-sigs/plugin-hybrid-edit-block)
- [https://github.com/halo-sigs/plugin-katex](https://github.com/halo-sigs/plugin-katex)
- [https://github.com/halo-sigs/plugin-text-diagram](https://github.com/halo-sigs/plugin-text-diagram)

View File

@@ -0,0 +1,188 @@
---
title: 编辑器集成
description: 通过实现扩展点为文章提供新的编辑器 - editor:create
---
此扩展点可以为文章提供新的独立编辑器。
![编辑器集成](/img/developer-guide/plugin/extension-points/ui/editor-create.png)
## 定义方式
```ts
export default definePlugin({
extensionPoints: {
"editor:create": (): EditorProvider[] | Promise<EditorProvider[]> => {
return [
{
name: "foo",
displayName: "foo",
logo: "/plugins/plugin-foo/assets/logo.png",
component: FooComponent,
rawType: "markdown",
},
];
},
},
});
```
```ts title="EditorProvider"
export interface EditorProvider {
name: string;
displayName: string;
logo?: string;
component: Component;
rawType: string;
}
```
其中,`component` 可以是组件对象或组件名称,且此组件有以下实现要求:
1. 组件包含以下 props
1. `title:string`:用于接收标题。
2. `raw:string`:用于接收原始内容。
3. `content:string`:用于接收渲染后的内容。
4. `uploadImage?: (file: File) => Promise<Attachment>`:用于上传图片,在编辑器内部获取到 File 之后直接调用此方法即可得到上传后的附件信息。
2. 组件包含以下 emit 事件:
1. `update:title`:用于更新标题。
2. `update:raw`:用于更新原始内容。
3. `update:content`:用于更新渲染后的内容。
4. `update`:发送更新事件。
## 示例
此示例将实现一个简单的 Markdown 编辑器。
```ts title="index.ts"
import { definePlugin } from "@halo-dev/console-shared";
import { markRaw } from "vue";
import MarkdownEditor from "./components/markdown-editor.vue";
export default definePlugin({
extensionPoints: {
"editor:create": () => {
return [
{
name: "markdown",
displayName: "Markdown 编辑器",
component: markRaw(MarkdownEditor),
rawType: "markdown",
logo: "/plugins/markdown-editor/assets/logo.png",
},
];
},
},
});
```
```html title="./components/markdown-editor.vue"
<script setup lang="ts">
import { marked } from "marked";
import { debounce } from "lodash-es";
import { ref, computed, onMounted } from "vue";
import type { Attachment } from "@halo-dev/api-client";
const props = withDefaults(
defineProps<{
raw: string;
content: string;
uploadImage?: (file: File) => Promise<Attachment>;
}>(),
{
raw: "",
content: "",
uploadImage: undefined,
}
);
const emit = defineEmits<{
(event: "update:raw", value: string): void;
(event: "update:content", value: string): void;
(event: "update", value: string): void;
}>();
const output = computed(() => marked(props.raw));
const update = debounce((e) => {
emit("update:raw", e.target.value);
emit("update:content", marked(e.target.value));
if (e.target.value !== props.raw) {
emit("update", e.target.value);
}
}, 100);
const textareaRef = ref();
onMounted(() => {
textareaRef.value.addEventListener("paste", (e) => {
if (e.clipboardData && e.clipboardData.items) {
const items = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") !== -1) {
const file = items[i].getAsFile();
props.uploadImage?.(file).then((attachment: Attachment) => {
emit(
"update:raw",
props.raw +
`![${attachment.spec.displayName}](${attachment.status?.permalink})`
);
});
}
}
}
});
});
</script>
<template>
<div class="editor">
<textarea ref="textareaRef" class="input" :value="raw" @input="update"></textarea>
<div class="output" v-html="output"></div>
</div>
</template>
<style>
body {
margin: 0;
}
.editor {
height: 100vh;
display: flex;
}
.input,
.output {
overflow: auto;
width: 50%;
height: 100%;
box-sizing: border-box;
padding: 0 20px;
}
.input {
border: none;
border-right: 1px solid #ccc;
resize: none;
outline: none;
background-color: #f6f6f6;
font-size: 14px;
font-family: 'Monaco', courier, monospace;
padding: 20px;
}
code {
color: #f66;
}
</style>
```
> 来源:[https://vuejs.org/examples/#markdown](https://vuejs.org/examples/#markdown)
## 实现案例
- [https://github.com/halo-sigs/plugin-stackedit](https://github.com/halo-sigs/plugin-stackedit)
- [https://github.com/halo-sigs/plugin-bytemd](https://github.com/halo-sigs/plugin-bytemd)
- [https://github.com/justice2001/halo-plugin-vditor](https://github.com/justice2001/halo-plugin-vditor)

View File

@@ -0,0 +1,14 @@
---
title: 扩展点
description: Halo UI 为插件提供的扩展点接口
---
UI 扩展点是用于扩展 Console 和 UC 的界面的接口,通过实现扩展点接口,插件可以在 Console 和 UC 中扩展功能。
以下是目前已支持的扩展点列表:
```mdx-code-block
import DocCardList from '@theme/DocCardList';
<DocCardList />
```

View File

@@ -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 // 附件固定访问地址
}
}
```

View File

@@ -0,0 +1,64 @@
```ts
export interface ListedComment {
comment:{
apiVersion: "content.halo.run/v1alpha1"
kind: "Comment"
metadata: {
annotations: {}
creationTimestamp: string
labels: {}
name: string // 评论的唯一标识
version: number
}
spec: {
allowNotification: boolean; // 是否允许通知
approved: boolean;
approvedTime: string;
content: string; // 最终渲染的文本
creationTime: string; // 创建时间
hidden: boolean;
ipAddress: string; // 评论者 IP 地址
lastReadTime: string;
owner: { // 创建者信息
annotations: {};
displayName: string;
kind: string;
name: string;
};
priority: number; // 排序字段
raw: string; // 原始文本,一般用于给编辑器使用
subjectRef: { // 引用关联,比如文章、自定义页面
group: string;
kind: string;
name: string;
version: string;
};
top: boolean; // 是否置顶
userAgent: string; // 评论者 UserAgent 信息
}
status: {
hasNewReply: boolean; // 是否有新回复
lastReplyTime: string;
observedVersion: number;
replyCount: number; // 回复数量
unreadReplyCount: number;
visibleReplyCount: number;
}
}
owner: { // 创建者信息
avatar: string; // 头像
displayName: string; // 描述
email: string; // 邮箱
kind: string;
name: string; // 创建者的唯一标识
}
stats: {
upvote: number;
}
subject: {
apiVersion: string;
kind: string;
metadata: Metadata;
}
}
```

View File

@@ -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; // 标签下可见的文章总数
};
}>;
}
```

View File

@@ -0,0 +1,49 @@
```ts
export interface ListedReply {
owner: { // 创建者信息
avatar: string; // 头像
displayName: string; // 描述
email: string; // 邮箱
kind: string;
name: string; // 创建者的唯一标识
}
reply:{
apiVersion: "content.halo.run/v1alpha1"
kind: "Reply"
metadata: {
annotations: {}
creationTimestamp: string
labels: {}
name: string // 评论的唯一标识
version: number
}
spec: {
allowNotification: boolean; // 是否允许通知
approved: boolean;
approvedTime: string;
commentName: string; // 被回复的评论名称,即 Comment 的 metadata.name
content: string; // 最终渲染的文本
creationTime: string; // 创建时间
hidden: boolean;
ipAddress: string; // 评论者 IP 地址
owner: { // 创建者信息
annotations: {};
displayName: string;
kind: string;
name: string;
};
priority: number; // 排序字段
quoteReply: string; // 被回复的回复名称,即 Reply 的 metadata.name
raw: string; // 原始文本,一般用于给编辑器使用
top: boolean; // 是否置顶
userAgent: string; // 评论者 UserAgent 信息
}
status: {
observedVersion: number;
}
}
stats: {
upvote: number;
}
}
```

View File

@@ -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>[]; // 子菜单项
}
```

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
---
title: 插件安装界面选项卡
description: 扩展插件安装界面选项卡 - plugin:installation:tabs:create
---
目前 Halo 原生支持本地上传和远程下载的方式安装插件,此扩展点用于扩展插件安装界面的选项卡,以支持更多的安装方式。
![插件安装界面选项卡](/img/developer-guide/plugin/extension-points/ui/plugin-installation-tabs-create.png)
## 定义方式
```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](https://github.com/halo-dev/plugin-app-store)

View File

@@ -0,0 +1,84 @@
---
title: 插件数据列表显示字段
description: 扩展插件数据列表显示字段 - plugin:list-item:field:create
---
此扩展点用于扩展插件数据列表的显示字段。
![插件数据列表显示字段](/img/developer-guide/plugin/extension-points/ui/plugin-list-item-field-create.png)
## 定义方式
```ts
export default definePlugin({
extensionPoints: {
"plugin:list-item:field:create": (plugin: Ref<Plugin>): EntityFieldItem[] | Promise<EntityFieldItem[]> => {
return [
{
priority: 0,
position: "start",
component: markRaw(FooComponent),
props: {},
permissions: [],
hidden: false,
}
];
},
},
});
```
```ts title="EntityFieldItem"
export interface EntityFieldItem {
priority: number;
position: "start" | "end";
component: Raw<Component>;
props?: Record\<string, unknown\>;
permissions?: string[];
hidden?: boolean;
}
```
## 示例
此示例将添加一个显示插件 requires版本要求的字段。
```ts
import { definePlugin } from "@halo-dev/console-shared";
import { markRaw, type Ref } from "vue";
import type { Plugin } from "@halo-dev/api-client";
import { VEntityField } from "@halo-dev/components";
export default definePlugin({
extensionPoints: {
"plugin:list-item:field:create": (plugin: Ref<Plugin>) => {
return [
{
priority: 0,
position: "end",
component: markRaw(VEntityField),
props: {
description: plugin.value.spec.requires,
},
permissions: [],
hidden: false,
},
];
},
},
});
```
## 实现案例
- [https://github.com/halo-dev/plugin-app-store](https://github.com/halo-dev/plugin-app-store)
## 类型定义
### Plugin
```mdx-code-block
import Plugin from "./interface/Plugin.md";
<Plugin />
```

View File

@@ -0,0 +1,55 @@
---
title: 插件数据列表操作菜单
description: 扩展插件数据列表操作菜单 - plugin:list-item:operation:create
---
此扩展点用于扩展插件数据列表的操作菜单项。
![插件数据列表操作菜单](/img/developer-guide/plugin/extension-points/ui/plugin-list-item-operation-create.png)
## 定义方式
```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](https://github.com/halo-dev/plugin-app-store)
## 类型定义
### Plugin
```mdx-code-block
import Plugin from "./interface/Plugin.md";
<Plugin />
```

View File

@@ -0,0 +1,90 @@
---
title: 插件详情选项卡
description: 扩展当前插件的详情选项卡 - plugin:self:tabs:create
---
此扩展点用于在 Console 的插件详情页面中添加自定义选项卡,可以用于自定义插件的配置页面。
![插件详情选项卡](/img/developer-guide/plugin/extension-points/ui/plugin-self-tabs-create.png)
## 定义方式
```ts
export default definePlugin({
extensionPoints: {
"plugin:self:tabs:create": (): PluginTab[] | Promise<PluginTab[]> => {
return [
{
id: "foo",
label: "foo",
component: markRaw(FooComponent),
permissions: [],
},
];
},
},
});
```
```ts title="PluginTab"
export interface PluginTab {
id: string; // 选项卡 ID不能与设置表单的 group 重复
label: string; // 选项卡标题
component: Raw<Component>; // 选项卡面板组件
permissions?: string[]; // 选项卡权限
}
```
其中,`component` 组件可以注入inject以下属性
- `plugin`:当前插件对象,类型为 Ref\<[Plugin](#plugin)\>。
## 示例
此示例实现了一个自定义选项卡,用于获取插件的数据并显示名称。
```ts
import { definePlugin, PluginTab } from "@halo-dev/console-shared";
import MyComponent from "./views/my-component.vue";
import { markRaw } from "vue";
export default definePlugin({
components: {},
routes: [],
extensionPoints: {
"plugin:self:tabs:create": () : PluginTab[] => {
return [
{
id: "my-tab-panel",
label: "My Tab Panel",
component: markRaw(MyComponent),
permissions: []
},
];
},
},
});
```
```html title="./views/my-component.vue"
<script lang="ts" setup>
const plugin = inject<Ref<Plugin | undefined>>("plugin");
</script>
<template>
<h1>{{ plugin?.spec.displayName }}</h1>
</template>
```
## 实现案例
- [https://github.com/halo-dev/plugin-app-store](https://github.com/halo-dev/plugin-app-store)
## 类型定义
### Plugin
```mdx-code-block
import Plugin from "./interface/Plugin.md";
<Plugin />
```

View File

@@ -0,0 +1,80 @@
---
title: 文章数据列表显示字段
description: 扩展文章数据列表显示字段 - plugin:list-item:field:create
---
此扩展点用于扩展文章数据列表的显示字段。
![文章数据列表显示字段](/img/developer-guide/plugin/extension-points/ui/post-list-item-field-create.png)
## 定义方式
```ts
export default definePlugin({
extensionPoints: {
"post:list-item:field:create": (post: Ref<ListedPost>): EntityFieldItem[] | Promise<EntityFieldItem[]> => {
return [
{
priority: 0,
position: "start",
component: markRaw(FooComponent),
props: {},
permissions: [],
hidden: false,
}
];
},
},
});
```
```ts title="EntityFieldItem"
export interface EntityFieldItem {
priority: number;
position: "start" | "end";
component: Raw<Component>;
props?: Record\<string, unknown\>;
permissions?: string[];
hidden?: boolean;
}
```
## 示例
此示例将添加一个显示文章 slug别名的字段。
```ts
import { definePlugin } from "@halo-dev/console-shared";
import { markRaw, type Ref } from "vue";
import type { ListedPost } from "@halo-dev/api-client";
import { VEntityField } from "@halo-dev/components";
export default definePlugin({
extensionPoints: {
"post:list-item:field:create": (post: Ref<ListedPost>) => {
return [
{
priority: 0,
position: "end",
component: markRaw(VEntityField),
props: {
description: post.value.post.spec.slug,
},
permissions: [],
hidden: false,
},
];
},
},
});
```
## 类型定义
### ListedPost
```mdx-code-block
import ListedPost from "./interface/ListedPost.md";
<ListedPost />
```

View File

@@ -0,0 +1,92 @@
---
title: 文章数据列表操作菜单
description: 扩展文章数据列表操作菜单 - post:list-item:operation:create
---
此扩展点用于扩展文章数据列表的操作菜单项。
![文章数据列表操作菜单](/img/developer-guide/plugin/extension-points/ui/post-list-item-operation-create.png)
## 定义方式
```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 />
```

View File

@@ -0,0 +1,81 @@
---
title: 回复数据列表操作菜单
description: 扩展回复数据列表操作菜单 - reply:list-item:operation:create
---
此扩展点用于扩展回复数据列表的操作菜单项。
![回复数据列表操作菜单](/img/developer-guide/plugin/extension-points/ui/reply-list-item-operation-create.png)
## 定义方式
```ts
export default definePlugin({
extensionPoints: {
"reply:list-item:operation:create": (
reply: Ref<ListedReply>
): OperationItem<ListedReply>[] | Promise<OperationItem<ListedReply>[]> => {
return [
{
priority: 10,
component: markRaw(VDropdownItem),
props: {},
action: (item?: ListedReply) => {
// do something
},
label: "foo",
hidden: false,
permissions: [],
children: [],
},
];
},
},
});
```
```mdx-code-block
import OperationItem from "./interface/OperationItem.md";
<OperationItem />
```
## 示例
此示例将实现一个操作菜单项。
```ts
import type { ListedReply } from "@halo-dev/api-client";
import { VDropdownItem } from "@halo-dev/components";
import { definePlugin } from "@halo-dev/console-shared";
import { markRaw } from "vue";
export default definePlugin({
extensionPoints: {
"reply:list-item:operation:create": () => {
return [
{
priority: 21,
component: markRaw(VDropdownItem),
label: "测试回复菜单",
visible: true,
permissions: [],
action: async (reply: ListedReply) => {
console.log(reply)
},
},
];
},
},
});
```
## 类型定义
### ListedReply
```mdx-code-block
import ListedReply from "./interface/ListedReply.md";
<ListedReply />
```

View File

@@ -0,0 +1,91 @@
---
title: 主题数据列表操作菜单
description: 扩展主题数据列表操作菜单 - theme:list-item:operation:create
---
此扩展点用于扩展主题数据列表的操作菜单项。
![主题数据列表操作菜单](/img/developer-guide/plugin/extension-points/ui/theme-list-item-operation-create.png)
## 定义方式
```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](https://github.com/halo-dev/plugin-app-store)
## 类型定义
### Theme
```mdx-code-block
import Theme from "./interface/Theme.md";
<Theme />
```

View File

@@ -0,0 +1,44 @@
---
title: 主题管理界面选项卡
description: 扩展主题管理界面选项卡 - theme:list:tabs:create
---
目前在 Halo 的主题管理中原生支持本地上传和远程下载的方式安装主题,此扩展点用于扩展主题管理界面的选项卡,以支持更多的安装方式。
![主题管理界面选项卡](/img/developer-guide/plugin/extension-points/ui/theme-list-tabs-create.png)
## 定义方式
```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](https://github.com/halo-dev/plugin-app-store)

View File

@@ -0,0 +1,41 @@
---
title: 个人资料选项卡
description: 扩展个人中心的个人资料选项卡 - uc:user:profile:tabs:create
---
此扩展点用于扩展个人中心的个人资料选项卡。
![个人资料选项卡](/img/developer-guide/plugin/extension-points/ui/uc-user-profile-tabs-create.png)
## 定义方式
```ts
export default definePlugin({
extensionPoints: {
"uc:user:profile:tabs:create": (): UserProfileTab[] | Promise<UserProfileTab[]> => {
return [
{
id: "foo",
label: "foo",
component: markRaw(FooComponent),
priority: 20,
},
];
},
},
});
```
```ts title="UserProfileTab"
export interface UserProfileTab {
id: string; // 选项卡 ID
label: string; // 选项卡标题
component: Raw<Component>; // 选项卡面板组件
priority: number; // 排序优先级
}
```
其中,`component` 组件有以下实现要求:
1. 组件包含以下 props
1. `user:DetailedUser`:当前用户信息。

View File

@@ -0,0 +1,41 @@
---
title: 用户详情选项卡
description: 扩展用户详情选项卡 - user:detail:tabs:create
---
此扩展点用于扩展用户详情页面的选项卡。
![用户详情选项卡](/img/developer-guide/plugin/extension-points/ui/user-detail-tabs-create.png)
## 定义方式
```ts
export default definePlugin({
extensionPoints: {
"user:detail:tabs:create": (): UserTab[] | Promise<UserTab[]> => {
return [
{
id: "foo",
label: "foo",
component: markRaw(FooComponent),
priority: 20,
},
];
},
},
});
```
```ts title="UserTab"
export interface UserTab {
id: string; // 选项卡 ID
label: string; // 选项卡标题
component: Raw<Component>; // 选项卡面板组件
priority: number; // 排序优先级
}
```
其中,`component` 组件有以下实现要求:
1. 组件包含以下 props
1. `user:DetailedUser`:当前用户信息。