页面覆盖与 Thymeleaf 公共片段
适用范围:当前适配 Halo 2.26.0 及以上版本的言答Issue。旧版插件的界面与构建方式可能不同。
整页覆盖
路由通过 TemplateNameResolver 优先查找主题同名页面,未找到时使用插件页面:
| 路由 | 主题覆盖文件 | 公共正文片段 | 主要数据 |
|---|---|---|---|
/subject/{subjectName} | templates/subject.html | plugin:PluginIssues:fragments/pages/subject :: content | issueSubjectVO |
/subject/{subjectName}/issues、/issues/page/{page} | templates/issues.html | plugin:PluginIssues:fragments/pages/issues :: content | issueSubjectInfo、issueSubjectStats、issueItems、issueState |
/subject/{subjectName}/issues/{issueName} | templates/issue.html | plugin:PluginIssues:fragments/pages/issue :: content | issueVO、issueSubjectInfo、issueSubjectStats、issueComments、issueCosedComment |
/subject/{subjectName}/issues/new | templates/newIssue.html | plugin:PluginIssues:fragments/pages/newIssue :: content | issueSubjectInfo、issueSubjectStats |
创建页仍要求登录。issueCosedComment 是既有模型字段的拼写,含义为默认关闭理由,适配时保留此名称。
所有插件路由还提供 pluginVersion、issueAvatarMode、contentStyle、diceBarAvatarStyle、diceBarAvatarSize。issueItems 包含 items/page/size/total/totalPages/prevUrl/nextUrl/hasPrevious()/hasNext() 等分页信息。
例如主题只想给列表页面加一个说明区,可添加 templates/issues.html:
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org"
th:replace="~{layout :: html(head=~{::head}, content=~{::content})}">
<th:block th:fragment="head">
<title th:text="${issueSubjectInfo.title + ' · 问题反馈'}"></title>
<th:block th:replace="~{plugin:PluginIssues:fragments/assets :: head(version=${pluginVersion})}" />
</th:block>
<th:block th:fragment="content">
<p>欢迎反馈问题,提交前请先搜索已有讨论。</p>
<th:block th:replace="~{plugin:PluginIssues:fragments/pages/issues :: content}" />
</th:block>
</html>
这个例子要求主题已提供上一节的 layout.html。Halo 的特殊 fallback 解析只针对插件模板;主题自己调用 layout 时,需要确实存在该模板。
公共正文片段已经包含 .piw-app 容器及头像上下文,不要再包一层 data-piw-root。资源片段 fragments/assets :: head(version) 每页只引入一次。所有插件片段使用完整的 plugin:PluginIssues: 命名空间,避免与主题的 fragments/header 等重名。
组件复用与运行时
以下片段可在 .piw-app 容器内使用,路径均以 plugin:PluginIssues: 开头:
| 片段 | 参数及上下文 |
|---|---|
fragments/header :: header | subjectType, title, name, totalIssue, activeTab;另需登录用户、头像上下文 |
fragments/macro/issue-item :: issueItem | issueVO;另需头像上下文 |
fragments/macro/subject-stats-card :: subjectStatsCard | intro, data |
fragments/macro/issue-right :: issueRight | issueVO;另需头像上下文 |
fragments/macro/ownerAvatar :: ownerAvatar | avatar, name;另需 issueAvatarMode 及 Alpine avatarStyle/size |
这些是依赖插件模型的 Thymeleaf 片段,不是独立 Web Components。首次适配建议复用整个 fragments/pages/* :: content。自定义组件组合需要保留 x-data、x-ref、登录条件和 API 所需的数据。
自定义页面的运行容器示例(在插件路由模型中):
<section class="piw-app" data-piw-root x-ignore x-data="piwApp"
th:attr="data-avatar-size=${diceBarAvatarSize},data-avatar-style=${diceBarAvatarStyle}">
<div x-data="piwDiceBearAvatar(avatarStyle, size)">
<th:block th:each="item : ${issueItems.items}">
<th:block th:replace="~{plugin:PluginIssues:fragments/macro/issue-item :: issueItem(issueVO=${item})}" />
</th:block>
</div>
</section>
x-ignore 防止主题先于插件注册组件时初始化这些节点,插件会在挂载时移除它。脚本使用 defer,在 DOM 就绪后挂载;如果 window.Alpine 已存在,会复用该实例且不会再次调用 start()。否则插件启动自带的 Alpine 3。主题自行使用 Alpine 时,应通过常规 defer/module 脚本加载,并暴露 window.Alpine;不要在页面就绪后再异步加载另一个 Alpine 覆盖它。
插件组件名统一使用 piw 前缀,JS 入口为 window.PluginIssues,新模板不再依赖泛用的 window.main。动态插入带 data-piw-root、x-ignore 的完整片段后,可以调用 PluginIssues.mount();已经挂载的根节点会被跳过。加载静态 JS 需要允许 Alpine 默认表达式执行方式的 CSP;此版本并非 Alpine CSP 构建。