页面覆盖与 Thymeleaf 公共片段

适用范围:当前适配 Halo 2.26.0 及以上版本的言答Issue。旧版插件的界面与构建方式可能不同。

整页覆盖

路由通过 TemplateNameResolver 优先查找主题同名页面,未找到时使用插件页面:

路由主题覆盖文件公共正文片段主要数据
/subject/{subjectName}templates/subject.htmlplugin:PluginIssues:fragments/pages/subject :: contentissueSubjectVO
/subject/{subjectName}/issues、/issues/page/{page}templates/issues.htmlplugin:PluginIssues:fragments/pages/issues :: contentissueSubjectInfo、issueSubjectStats、issueItems、issueState
/subject/{subjectName}/issues/{issueName}templates/issue.htmlplugin:PluginIssues:fragments/pages/issue :: contentissueVO、issueSubjectInfo、issueSubjectStats、issueComments、issueCosedComment
/subject/{subjectName}/issues/newtemplates/newIssue.htmlplugin:PluginIssues:fragments/pages/newIssue :: contentissueSubjectInfo、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 :: headersubjectType, title, name, totalIssue, activeTab;另需登录用户、头像上下文
fragments/macro/issue-item :: issueItemissueVO;另需头像上下文
fragments/macro/subject-stats-card :: subjectStatsCardintro, data
fragments/macro/issue-right :: issueRightissueVO;另需头像上下文
fragments/macro/ownerAvatar :: ownerAvataravatar, 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 构建。