目标: 为使用 Wagtail 时向用户显示上下文指南创建一种简单的方式。

为什么: Wagtail 的 UI 非常直观,但是,在第一次使用任何东西时,如果能得到一点帮助,那就太好了。

**如何:**我们希望为管理员用户维护这些指南提供一种方式(避免硬编码内容),它们应该易于创建并在可用时显示在特定页面上。

实施概述

  • 每个guide都可以映射到管理员内的一个页面。

  • 每个guide都可以有一个或多个带有基本文本内容的步骤,以及将步骤与 UI 元素对齐的选项。

  • 如果当前页面有可用的指南,它将在菜单中突出显示。如果当前页面没有可用的指南,菜单将简单地加载所有指南的列表。

  • Shepherd.js将用于以交互方式呈现 UI 步骤,这是一个很棒的 JS 库,它允许声明一系列“步骤”,将用户作为一系列弹出窗口进行浏览,一些步骤可以与 UI 中的元素对齐,并且该元素将突出显示。

  • WagtailmodelAdminhooks将用于添加定制。

  • 我们可以利用编辑指南到 Wagtail的内容来获取一些初始指南。

[Shepherd JS 演示页面](https://res.cloudinary.com/practicaldev/image/fetch/s--QV5itjlJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev- to-uploads.s3.amazonaws.com/uploads/articles/m0nkqqth14ckoz86lr86.png)

版本

  • 姜戈 3.2

  • 鹡鸰 2.14

  • Shepherd.js 8.3.1

教程

0\。在你开始之前

  • 假设您将运行 Wagtail 应用程序,如果没有,您可以使用Wagtail Bakery Demo作为起点。

  • 假设您将具备 Django 和 Wagtail 的基本知识,并且熟悉创建 Django 模型和 Python 类。

  • 假设您具有 Javascript 和 CSS 的基本知识,您可以复制和粘贴代码,但最好了解正在发生的事情。

1\。创建指南应用程序

  • 使用 Djangostartapp命令创建一个新应用程序'guide',其中将包含此功能的所有新模型和代码。

  • 运行django-admin startapp guide

  • 使用创建的新guide应用程序更新设置INSTALLED_APPS

  • 运行初始迁移./manage.py makemigrations guide

INSTALLED_APPS = [
  # ...
  'guide',
  # ... wagtail & django items
]

进入全屏模式 退出全屏模式

交叉检查(在您继续之前)

  • 您应该有一个新的应用程序文件夹guide,其中包含模型、视图等。

  • 您应该能够无错误地运行该应用程序。

2\。创建模型

  • 我们将创建两个新模型;GuideGuideStep

  • 其中Guide包含标题(用于搜索)、URL 路径(用于确定应在哪个管理 UI 页面上显示)以及指向一个或多个步骤的链接。我们希望为用户提供一种订购步骤的方式,甚至在以后重新订购它们。

  • Guide中,我们使用edit_handler来构建一个选项卡式 UI,以便将某些字段分开。

  • 其中GuideStep包含标题、文本和可选的元素选择器。所需数据基于可传递给Shepherd.jssteps的选项。

  • 此代码基于 Wagtail 文档中的内联面板和模型集群说明。

  • 如果在定义模型时遇到问题,您可能需要将'modelcluster'添加到INSTALLED_APPS

  • 创建模型后,请记住运行迁移和迁移/manage.py makemigrations/manage.py migrate

# guide/models.py
from django.db import models

from modelcluster.fields import ParentalKey
from modelcluster.models import ClusterableModel

from wagtail.admin.edit_handlers import (
    FieldPanel,
    InlinePanel,
    ObjectList,
    TabbedInterface,
)
from wagtail.core.models import Orderable


class GuideStep(models.Model):
    """
    Each step is a model to represent the step used by
    https://shepherdjs.dev/docs/Step.html
    This is an abstract model as `GuideRelatedStep` will be used for the actual model with a relation
    """

    title = models.CharField(max_length=255)
    text = models.CharField(max_length=255)
    element = models.CharField(max_length=255, blank=True)

    panels = [
        FieldPanel("title"),
        FieldPanel("text"),
        FieldPanel("element"),
    ]

    class Meta:
        abstract = True


class GuideRelatedStep(Orderable, GuideStep):
    """
    Creates an orderable (user can re-order in the admin) and related 'step'
    Will be a many to one relation against `Guide`
    """

    guide = ParentalKey("guide.Guide", on_delete=models.CASCADE, related_name="steps")


class Guide(ClusterableModel):
    """
    `ClusterableModel` used to ensure that this model can have orderable relations
    using the modelcluster library (similar to ForeignKey).
    edit_handler
    """

    title = models.CharField(max_length=255)
    # steps - see GuideRelatedStep
    url_path = models.CharField(max_length=255, blank=True)

    content_panels = [
        FieldPanel("title"),
        InlinePanel("steps", label="Steps", min_num=1),
    ]

    settings_panels = [
        FieldPanel("url_path"),
    ]

    edit_handler = TabbedInterface(
        [
            ObjectList(content_panels, heading="Content"),
            ObjectList(settings_panels, heading="Settings"),
        ]
    )

进入全屏模式 退出全屏模式

交叉检查(在您继续之前)

  • 您的迁移应该有一个新文件guide/migrations/001_initial.py

  • 您应该能够无错误地运行应用程序。

3\。添加modelAdmin的挂钩

  • 使用modelAdmin系统,我们将为我们的Guide模型创建一个基本管理模块,此代码基于文档](https://docs.wagtail.io/en/stable/reference/contrib/modeladmin/index.html#a-simple-example)中的[modelAdmin 示例。

  • 记得将'wagtail.contrib.modeladmin'添加到您的INSTALLED_APPS

  • 使用modelAdmin将通过将以下代码添加到新文件wagtail_hooks.py中,在侧边栏中设置一个新菜单项。

  • 请注意,我们已打开inspect_view_enabled,这是为了使每个指南的只读视图可用,并且还确保该模型的非编辑者可以访问此数据,检查这些权限以显示菜单项也。

  • 请记住授予所有用户“检查”指南的权限(否则菜单不会显示)。

  • 现在最好添加至少一个具有以下值的指南。

- Title: Dashboard
- URL Path: /admin/ **(on the settings tab*)*
- Step 1:
  - Title: Dashboard
  - Text: Clicking the logo returns you to your Dashboard
  - Element: a.logo
- Step 2:
  - Title: Search
  - Text: Search through to find any Pages, Documents, or Images
  - Element: .nav-search > div
- Step 3:
  - Title: Explorer Menu (Pages)
  - Text: Click the Pages button in the sidebar to open the explorer. This allows you to navigate through the sections of the site.
  - Element: .menu-item[data-explorer-menu-item]
- Step 4:
  - Title: Done
  - Text: That's it for now, keep an eye out for the Help menu item on other pages.
  - Element: (leave blank)

进入全屏模式 退出全屏模式

# guide/wagtail_hooks.py
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register

from .models import Guide


class GuideAdmin(ModelAdmin):
    menu_label = "Guide"
    model = Guide
    menu_icon = "help"
    menu_order = 8000
    list_display = ("title", "url_path")
    search_fields = ("title", "url_path")
    inspect_view_enabled = True


modeladmin_register(GuideAdmin)

进入全屏模式 退出全屏模式

交叉检查(在您继续之前)

  • 您现在应该在 Wagtail 管理员的左侧边栏中看到一个菜单项“指南”。

  • 您应该能够以非管理员用户身份登录,并且仍然可以看到此侧边栏菜单项。

[编辑指南示例](https://res.cloudinary.com/practicaldev/image/fetch/s--qgXTq-Bs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev -to-uploads.s3.amazonaws.com/uploads/articles/2bv7p4i1kcr7xw45uzoz.png)

4\。自定义Guide菜单项

  • 我们现在的目标是创建一个自定义的MenuItem,这是一个 Wagtail 类,用于为每个侧边栏菜单项生成内容。

  • 我们将使用from wagtail.contrib.modeladmin.menus import ModelAdminMenuItem类而不是扩展类from wagtail.admin.menu import MenuItem。这是因为ModelAdminMenuItem包含一些我们想要保留的特定ModelAdmin逻辑。

  • 每个MenuItem都有一个方法get_context,它为menu_item.html模板提供模板上下文。

  • 此模板接受attr_stringclassnames可用于注入内容。

4a。为Guide模型添加方法

  • 这个方法get_data_for_request将允许我们找到请求的 URL 路径与指南中的url_path对齐的第一个Guide实例。

  • 例如 - 如果使用 URL 路径“/admin/images/”创建指南,那么当我们在管理页面上时,我们希望返回有关该指南的数据。如果使用路径“/admin/images/#/”创建指南,那么我们希望在编辑任何图像时都能找到指南(注意哈希的使用)。

  • path_to_match = re.sub('[\d]+', '#', request.path)将采用当前请求路径(例如/admin/images/53/)并将其转换为任何数字都替换为哈希的路径(例如/admin/images/#/),这是一种允许模糊 URL 匹配的简单方法。

  • 返回的数据结构有意创建一个 JSON 字符串,因此更容易作为数据属性传递到我们的模型中。

# guide/models.py

class Guide(ClusterableModel):
    #...

    @classmethod
    def get_data_for_request(cls, request):
        """
        Returns a dict with data to be sent to the client (for the shepherd.js library)
        """

        path_to_match = re.sub("[\d]+", "#", request.path)

        guide = cls.objects.filter(url_path=path_to_match).first()

        if guide:
            steps = [
                {
                    "title": step.title,
                    "text": step.text,
                    "element": step.element,
                }
                for step in guide.steps.all()
            ]

            data = {"steps": steps, "title": guide.title}

            value_json = json.dumps(
                data,
                separators=(",", ":"),
            )

            data["value_json"] = value_json

            return data

        return None

进入全屏模式 退出全屏模式

4b。创建menu.py文件

  • 这将包含我们的新菜单类,我们可以将此代码放在wagtail_hooks.py文件中,但如果可能的话,最好隔离此逻辑。

  • 这里我们为MenuItem重写了get_context方法,先调用super的get_context方法,然后添加两项。

  • 首先,我们添加attr_string并构建一个data-help属性,该属性将包含我们指南的 JSON 输出(如果找到)。注意:有很多方法可以将数据传递给客户端,这是最简单的但并不完美。

  • 其次,如果我们知道我们找到了当前管理页面的匹配指南,我们将classnames项目扩展为help-available类。

  • 记住return context,否则你只会得到一个空白菜单项。

# guide/menu.py

from django.utils.html import format_html

from wagtail.contrib.modeladmin.menus import ModelAdminMenuItem

from .models import Guide


class GuideAdminMenuItem(ModelAdminMenuItem):
    def get_context(self, request):
        context = super().get_context(request)

        data = Guide.get_data_for_request(request)

        if data:

            context["attr_string"] = format_html('data-help="{}"', data["value_json"])
            context["classnames"] = context["classnames"] + " help-available"

        return context

进入全屏模式 退出全屏模式

4c。更新指南管理员以使用自定义菜单项

  • 通过覆盖get_menu_item,我们可以利用我们的自定义GuideAdminMenuItem而不是默认值。
# guide/wagtail_hooks.py
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register

from .menu import GuideAdminMenuItem # added
from .models import Guide

class GuideAdmin(ModelAdmin):
    # ...
    def get_menu_item(self, order=None):
        """
        Utilised by Wagtail's 'register_menu_item' hook to create a menu item
        to access the listing view, or can be called by ModelAdminGroup
        to create a SubMenu
        """
        return GuideAdminMenuItem(self, order or self.get_menu_order())

进入全屏模式 退出全屏模式

交叉检查(在您继续之前)

  • 当您在 Wagtail 管理员中加载仪表板页面时,您应该能够检查(浏览器开发人员工具)“指南”菜单项并查看类和自定义数据帮助属性。

[在开发工具中查看菜单项](https://res.cloudinary.com/practicaldev/image/fetch/s--zle3EVnV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https:// /dev-to-uploads.s3.amazonaws.com/uploads/articles/kk52zdrnnryf0wv0i1wn.png)

5.添加 JS 和 CSS

  • 在此步骤中需要解压,但目标是为 Shepherd.js 库提供正确的options,并且当用户单击菜单项按钮时,它应该触发游览,而不是转到指南列表。

5a。导入shepherd.js

  • 在我们的wagtail_hooks.py文件中,我们将利用insert_global_admin_js挂钩添加两个文件,第一个是 npm 包的 CDN 版本。

  • 通过https://www.jsdelivr.com/package/npm/shepherd.js 使用 NPM 包的托管 CDN 版本可以节省时间,但它可能不适合您的项目。

  • 在下面的代码片段中,我们还将使用 Wagtail 的静态系统添加一个 js 文件,但是该文件的代码在步骤 5c 中。

  • 交叉检查(在您继续之前) 记得重新启动您的开发服务器,一旦完成,您应该能够打开浏览器控制台并输入Shepherd以查看值。这意味着 CDN 已经工作,您还可以查看网络选项卡以检查它是否已加载。

#guide/wagtail_hooks.py

from django.templatetags.static import static # added
from django.utils.html import format_html # added

from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from wagtail.core import hooks # added

# .. other imports & GuideAdmin

@hooks.register("insert_global_admin_js")
def global_admin_js():
    """
    Sourced from https://www.jsdelivr.com/package/npm/shepherd.js
    """
    return format_html(
        '<script src="{}"></script><script src="{}"></script>',
        "https://cdn.jsdelivr.net/npm/shepherd.js@8/dist/js/shepherd.min.js",
        static("js/shepherd.js"),
    )

进入全屏模式 退出全屏模式

5b。添加自定义静态 CSS 文件

  • 下面的 CSS 代码包含 Shepherd.js 库提供的所有基本样式,并进行了一些调整,看起来更像“Wagtail”,您可以通过https://cdn.jsdelivr.net/npm/shepherd.js@8/dist/css/shepherd.css使用 CDN 版本来节省时间。

  • 重要的是要注意样式.menu-item .help-available::after- 这是在已知帮助项目可用时添加一个小的视觉指示器*(星号)。

  • 记得把'django.contrib.staticfiles'添加到你的INSTALLED_APPS中,这样才能使用静态文件。

  • 交叉检查(在您继续之前) 记住在更改静态文件时重新启动您的开发服务器,一旦完成,您应该能够看到此 CSS 文件已加载到网络选项卡中。

#guide/wagtail_hooks.py

# .. other imports & GuideAdmin + insert_global_admin_js

@hooks.register("insert_global_admin_css")
def global_admin_css():
    """
    Pulled from https://github.com/shipshapecode/shepherd/releases (assets)
    .button styles removed (so we can use Wagtail styles instead)
    """
    return format_html('<link rel="stylesheet" href="{}">', static("css/shepherd.css"))

进入全屏模式 退出全屏模式

/* guide/static/css/shepherd.css */
.shepherd-footer {
  border-bottom-left-radius: 5px;
  border-bottom-right-radius: 5px;
  display: flex;
  justify-content: flex-end;
  padding: 0 0.75rem 0.75rem;
}

.shepherd-footer .shepherd-button:last-child {
  margin-right: 0;
}

.shepherd-cancel-icon {
  background: transparent;
  border-radius: 0.25rem;
  border: none;
  color: inherit;
  font-size: 2em;
  cursor: pointer;
  font-weight: 400;
  margin: 0;
  padding: 0;
  transition: background-color 0.5s ease;
  width: 2.2rem;
  height: 2.2rem;
}

.shepherd-cancel-icon:hover {
  background-color: var(--color-primary-darker);
}

.shepherd-title {
  display: flex;
  font-size: 1.5rem;
  font-weight: 400;
  flex: 1 0 auto;
  margin: 0;
  padding: 0;
}

.shepherd-header {
  align-items: center;
  border-top-left-radius: 5px;
  border-top-right-radius: 5px;
  display: flex;
  justify-content: flex-end;
  line-height: 2em;
  padding: 0.75rem 0.75rem 0;
  margin-bottom: 0.25rem;
}

.shepherd-has-title .shepherd-content .shepherd-header {
  padding: 1em;
}

.shepherd-text {
  color: rgba(0, 0, 0, 0.75);
  font-size: 1rem;
  line-height: 1.3em;
  min-height: 4em;
  padding: 0.75em 1em;
}

.shepherd-text p {
  margin-top: 0;
}

.shepherd-text p:last-child {
  margin-bottom: 0;
}

.shepherd-content {
  border-radius: 5px;
  outline: none;
  padding: 0;
}

.shepherd-element {
  background: #fff;
  border-radius: 5px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
  max-width: 50em;
  opacity: 0;
  outline: none;
  transition: opacity 0.3s, visibility 0.3s;
  visibility: hidden;
  width: 100%;
  z-index: 9999;
}

.shepherd-enabled.shepherd-element {
  opacity: 1;
  visibility: visible;
}

.shepherd-element[data-popper-reference-hidden]:not(.shepherd-centered) {
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
}

.shepherd-element,
.shepherd-element *,
.shepherd-element :after,
.shepherd-element :before {
  box-sizing: border-box;
}

.shepherd-arrow,
.shepherd-arrow:before {
  position: absolute;
  width: 16px;
  height: 16px;
  z-index: -1;
}

.shepherd-arrow:before {
  content: "";
  transform: rotate(45deg);
  background: #fff;
}

.shepherd-element[data-popper-placement^="top"] > .shepherd-arrow {
  bottom: -8px;
}

.shepherd-element[data-popper-placement^="bottom"] > .shepherd-arrow {
  top: -8px;
}

.shepherd-element[data-popper-placement^="left"] > .shepherd-arrow {
  right: -8px;
}

.shepherd-element[data-popper-placement^="right"] > .shepherd-arrow {
  left: -8px;
}

.shepherd-element.shepherd-centered > .shepherd-arrow {
  opacity: 0;
}

.shepherd-element.shepherd-has-title[data-popper-placement^="bottom"]
  > .shepherd-arrow:before {
  background-color: #e6e6e6;
}

.shepherd-target-click-disabled.shepherd-enabled.shepherd-target,
.shepherd-target-click-disabled.shepherd-enabled.shepherd-target * {
  pointer-events: none;
}

.shepherd-target {
  outline: 4px dotted var(--color-input-focus);
  outline-offset: -2px;
}

.shepherd-modal-overlay-container {
  height: 0;
  left: 0;
  opacity: 0;
  overflow: hidden;
  pointer-events: none;
  position: fixed;
  top: 0;
  transition: all 0.3s ease-out, height 0ms 0.3s, opacity 0.3s 0ms;
  width: 100vw;
  z-index: 9997;
}

.shepherd-modal-overlay-container.shepherd-modal-is-visible {
  height: 100vh;
  opacity: 0.75;
  transition: all 0.3s ease-out, height 0s 0s, opacity 0.3s 0s;
}

.shepherd-modal-overlay-container.shepherd-modal-is-visible path {
  pointer-events: all;
}

.menu-item .help-available::after {
  content: "*";
}

进入全屏模式 退出全屏模式

5c。添加自定义静态 JS 文件

  • 完整的 JS 在下面,这个 JS 的目标是为找到的每个具有data-help属性的元素设置一个 Shepherd.js 游览。

  • 此数据属性将被解析为 JSON,如果找到steps,将设置游览,并且该元素将附加一个点击侦听器以触发游览。

  • 我们还设置了一些逻辑,以确保为步骤的每个可能状态显示正确的按钮(例如,第一步应该只有一个“下一步”按钮)。

  • Shepherd.js 文档包含有关传入的每个选项的信息,并且可以根据要求自定义这些选项。

  • 交叉检查(在继续之前) 记得在添加静态文件时重新启动您的开发服务器,完成后您应该能够看到该 JS 文件已加载到网络选项卡中。

// guide/static/js/shepherd.js
(() => {
  /* 1. set up buttons for each possible state (first, last, only) of a step */

  const nextButton = {
    action() {
      return this.next();
    },
    classes: "button",
    text: "Next",
  };

  const backButton = {
    action() {
      return this.back();
    },
    classes: "button button-secondary",
    secondary: true,
    text: "Back",
  };

  const doneButton = {
    action() {
      return this.next();
    },
    classes: "button",
    text: "Done",
  };

  /* 2. create a function that will maybe return an object with the buttons */

  const getButtons = ({ index, length }) => {
    if (length <= 1) return { buttons: [doneButton] }; // only a single step, no back needed
    if (index === 0) return { buttons: [nextButton] }; // first
    if (index === length - 1) return { buttons: [backButton, doneButton] }; // last
    return {};
  };

  /* 3. prepare the default step options */

  const defaultButtons = [backButton, nextButton];

  const defaultStepOptions = {
    arrow: false,
    buttons: defaultButtons,
    cancelIcon: { enabled: true },
    canClickTarget: false,
    scrollTo: { behavior: "smooth", block: "center" },
  };

  /* 4. once the DOM is loaded, find all the elements with the data-help attribute
     - for each of these elements attempt to parse the JSON into steps and title
     - if we find steps then initiate a `Shepherd` tour with those steps
     - finally, attach a click listener to the link so that the link will trigger the tour
   */

  window.addEventListener("DOMContentLoaded", () => {
    const links = document.querySelectorAll(".help-available[data-help]");

    // if no links found with data-help - return
    if (!links || links.length === 0) return;

    links.forEach((link) => {
      const data = link.dataset.help;

      // if data on data-help attribute is empty or missing, do not attempt to parse
      if (!data) return;

      const { steps = [], title } = JSON.parse(data);

      const tour = new Shepherd.Tour({
        defaultStepOptions,
        steps: steps.map(({ element, ...step }, index) => ({
          ...step,
          ...(element ? { attachTo: { element } } : {}),
          ...getButtons({ index, length: steps.length }),
        })),
        tourName: title,
        useModalOverlay: true,
      });

      link &&
        link.addEventListener("click", (event) => {
          event.preventDefault();
          tour.start();
        });
    });
  });
})();

进入全屏模式 退出全屏模式

最终实施

  • 现在,管理员主页(仪表板)页面上应该有一个功能齐全的游览触发器,“指南”菜单项应该有一个“*”以指示可用的帮助。

  • 点击时,应该会根据上面第3步添加的数据触发游览。

  • 你可以在 github 上看到所有最终代码https://github.com/lb-/bakerydemo/tree/tutorial/guide-app/guide

[编辑图像示例](https://res.cloudinary.com/practicaldev/image/fetch/s--ZdjDaNkw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to -uploads.s3.amazonaws.com/uploads/articles/iinau6pooddu8m0qq1ss.png)

[主页示例](https://res.cloudinary.com/practicaldev/image/fetch/s--LenXYhjf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to -uploads.s3.amazonaws.com/uploads/articles/44mpay8xhs0y4u3f7ruc.png)

  • 更新:20/08/2021 - 添加了关于INSTALLED_APPS的提醒。

未来增强思路

  • 使用相同的菜单项触发指南并显示指南列表并不理想,因为这可能会让用户感到困惑,而且当管理员真正想要编辑并且无法轻松访问指南列表时(如果有添加了很多指南)。

  • 如果该页面有可用的匹配指南,则为新用户提供仪表板面板,这已作为下面的奖励步骤 6 实施。

  • 使指南项目的检查视图在漂亮的 UI 中显示完整的步骤,因为这将是一个有用的资源,即使没有交互式游览方面。

  • 有办法跟踪用户点击了哪些指南,对新用户特别有帮助,甚至可能提供反馈。

6\。添加带有指南触发器的仪表板面板奖金

  • 这是一个粗略的实现,但它利用自定义MenuItem中的相同逻辑来潜在地呈现主页面板。

  • 此代码基于construct_homepage_panelsWagtail 文档。

  • 使用Guide.get_data_for_request(self.request),我们可以拉入一个潜在的数据对象,如果找到,将其传递给生成的 HTML。

  • 注意:我们需要重写__init__方法以确保可以使用request初始化此 Panel 类。

# wagtail_hooks.py

# imports and other hooks...

class GuidePanel:
    order = 500

    def __init__(self, request):
        self.request = request

    def render(self):
        data = Guide.get_data_for_request(self.request)

        if data:
            return format_html(
                """
            <section class="panel summary nice-padding">
                <h2>Guide</h2>
                <div>
                    <button class="button button-secondary help-available" data-help="{}">Show {} Guide</button>
                </div>
            </section>
            """,
                data["value_json"],
                data["title"],
            )

        return ""


@hooks.register("construct_homepage_panels")
def add_guide_panel(request, panels):
    panels.append(GuidePanel(request))

进入全屏模式 退出全屏模式

Logo

更多推荐