Skip to main content

主页自定义

¥Homepage customization

5.13.0This feature requires Strapi version 5.13.0 or later.

主页是 Strapi 管理面板的登录页面。默认情况下,它使用 2 个默认小部件提供内容概览:

¥The Homepage is the landing page of the Strapi admin panel. By default, it provides an overview of your content with 2 default widgets:

  • 上次编辑的条目:显示最近修改的内容条目,包括其内容类型、状态和更新时间。

    ¥Last edited entries: Displays recently modified content entries, including their content type, status, and when they were updated.

  • 上次发布的条目:显示最近发布的内容条目,让你快速访问和管理已发布的内容。

    ¥Last published entries: Shows recently published content entries, allowing you to quickly access and manage your published content.

Homepage with default widgetsHomepage with default widgets

这些默认小部件目前无法删除,但你可以通过创建自己的小部件来自定义主页。

¥These default widgets cannot currently be removed, but you can customize the Homepage by creating your own widgets.

注意

如果你最近创建了一个 Strapi 项目,主页可能还会在小部件上方显示快速导览(如果你尚未跳过)。

¥If you recently created a Strapi project, the Homepage may also display a quick tour above widgets if you haven't skipped it yet.

添加自定义小部件

¥Adding custom widgets

要添加自定义小部件,你可以:

¥To add a custom widget, you can:

  • 市场 安装插件

    ¥install a plugin from the Marketplace

  • 或者,创建并注册你自己的小部件

    ¥or create and register your own widgets

本页面将介绍如何创建和注册小部件。

¥The present page will describe how to create and register your widgets.

注册自定义小部件

¥Registering custom widgets

要注册小部件,请使用 app.widgets.register()

¥To register a widget, use app.widgets.register():

信息

本页上的示例将介绍如何通过插件注册窗口小部件。如果你在应用的全局 register() 生命周期方法中注册了小部件,则大部分代码应该可重用,但不应传递 pluginId 属性。

¥The examples on the present page will cover registering a widget through a plugin. Most of the code should be reusable if you register the widget in the application's global register() lifecycle method, except you should not pass the pluginId property.

src/plugins/my-plugin/admin/src/index.js
import pluginId from './pluginId';
import MyWidgetIcon from './components/MyWidgetIcon';

export default {
register(app) {
// Register the plugin itself
app.registerPlugin({
id: pluginId,
name: 'My Plugin',
});

// Register a widget for the Homepage
app.widgets.register({
icon: MyWidgetIcon,
title: {
id: `${pluginId}.widget.title`,
defaultMessage: 'My Widget',
},
component: async () => {
const component = await import('./components/MyWidget');
return component.default;
},
/**

* Use this instead if you used a named export for your component
*/
// component: async () => {
// const { Component } = await import('./components/MyWidget');
// return Component;
// },
id: 'my-custom-widget',
pluginId: pluginId,
});
},

bootstrap() {},
// ...
};
此 API 需要 Strapi 5.13+ 版本。

app.widgets.register API 仅适用于 Strapi 5.13 及更高版本。尝试使用旧版本的 Strapi 调用 API 将导致管理面板崩溃。想要注册小部件的插件开发者应该:

¥The app.widgets.register API only works with Strapi 5.13 and above. Trying to call the API with older versions of Strapi will crash the admin panel. Plugin developers who want to register widgets should either:

  • 在其插件 package.json 中将 ^5.13.0 设置为其 @strapi/strapi peerDependency。此对等依赖支持 Marketplace 的兼容性检查。

    ¥set ^5.13.0 as their @strapi/strapi peerDependency in their plugin package.json. This peer dependency powers the Marketplace's compatibility check.

  • 或者,在调用 API 之前检查它是否存在:

    ¥or check if the API exists before calling it:

    if ('widgets' in app) {
    // proceed with the registration
    }

如果插件的主要目的是注册小部件,则建议使用 peerDependency 方法。如果插件想要添加小部件,但其大部分功能位于其他位置,则第二种方法更有意义。

¥The peerDependency approach is recommended if the whole purpose of the plugin is to register widgets. The second approach makes more sense if a plugin wants to add a widget but most of its functionality is elsewhere.

小部件 API 参考

¥Widget API reference

app.widgets.register() 方法可以接受单个窗口小部件配置对象或配置对象数组。每个小部件配置对象都可以接受以下属性:

¥The app.widgets.register() method can take either a single widget configuration object or an array of configuration objects. Each widget configuration object can accept the following properties:

属性类型描述必需的
iconReact.ComponentType显示在小部件标题旁边的图标组件是的
titleMessageDescriptor支持翻译的小部件标题是的
component() => Promise<React.ComponentType>返回小部件组件的异步函数是的
idstring小部件的唯一标识符是的
linkObject可选链接,可添加到小部件(参见链接对象属性)
pluginIdstring注册小部件的插件 ID
permissionsPermission[]查看小部件所需的权限

链接对象属性:

¥Link object properties:

如果你想在小部件上添加链接(例如,导航到详细视图),你可以提供一个具有以下属性的 link 对象:

¥If you want to add a link to your widget (e.g., to navigate to a detailed view), you can provide a link object with the following properties:

属性类型描述必需的
labelMessageDescriptor链接显示的文本是的
hrefstring链接应导航至的 URL是的

创建小部件组件

¥Creating a widget component

小部件组件的设计应以紧凑且信息丰富的方式显示内容。

¥Widget components should be designed to display content in a compact and informative way.

以下是如何实现一个基本的小部件组件:

¥Here's how to implement a basic widget component:

src/plugins/my-plugin/admin/src/components/MyWidget/index.js
import React, { useState, useEffect } from 'react';
import { Widget } from '@strapi/admin/strapi-admin';



const MyWidget = () => {


const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
const [error, setError] = useState(null);

useEffect(() => {
// Fetch your data here
const fetchData = async () => {
try {
// Replace with your actual API call
const response = await fetch('/my-plugin/data');
const result = await response.json();

setData(result);
setLoading(false);
} catch (err) {
setError(err);
setLoading(false);
}
};

fetchData();
}, []);

if (loading) {
return <Widget.Loading />;
}

if (error) {
return <Widget.Error />;
}

if (!data || data.length === 0) {
return <Widget.NoData />;
}

return (
<div>
{/* Your widget content here */}
<ul>
{data.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
};

export default MyWidget;
提示

为简单起见,以下示例直接在 useEffect 钩子内部使用数据提取。虽然这仅用于演示目的,但它可能无法反映生产环境中的最佳实践。

¥For simplicity, the example below uses data fetching directly inside a useEffect hook. While this works for demonstration purposes, it may not reflect best practices in production.

为了获得更强大的解决方案,请考虑 React 文档 中推荐的替代方法。如果你希望集成数据获取库,我们建议你使用 TanStackQuery

¥For more robust solutions, consider alternative approaches recommended in the React documentation. If you're looking to integrate a data fetching library, we recommend using TanStackQuery.

数据管理:

¥Data management:

Rendering and Data management

上方的绿色框表示用户的 React 组件(来自 API 中的 widget.component)的渲染区域。你可以在此框中渲染任何你喜欢的内容。但是,该框之外的所有内容均由 Strapi 渲染。这确保了管理面板内整体设计的一致性。API 中提供的 icontitlelink(可选)属性用于显示窗口小部件。

¥The green box above represents the area where the user’s React component (from widget.component in the API) is rendered. You can render whatever you like inside of this box. Everything outside that box is, however, rendered by Strapi. This ensures overall design consistency within the admin panel. The icon, title, and link (optional) properties provided in the API are used to display the widget.

小部件辅助组件参考

¥Widget helper components reference

Strapi 提供了几个辅助组件,以便在各个小部件之间保持一致的用户体验:

¥Strapi provides several helper components to maintain a consistent user experience across widgets:

组件描述用法
Widget.Loading显示加载旋转图标和消息正在获取数据时
Widget.Error显示错误状态发生错误时
Widget.NoData当没有可用数据时显示小部件没有数据可显示时
Widget.NoPermissions当用户缺少所需权限时显示用户无法访问小部件时

这些组件有助于在不同的小部件之间保持一致的外观和体验。你可以渲染这些组件而不使用子组件以获得默认的措辞:<Widget.Error /> 或者你可以传递子函数来覆盖默认副本并指定你自己的措辞:<Widget.Error>Your custom error message</Widget.Error>

¥These components help maintain a consistent look and feel across different widgets. You could render these components without children to get the default wording: <Widget.Error /> or you could pass children to override the default copy and specify your own wording: <Widget.Error>Your custom error message</Widget.Error>.

示例:添加内容指标小部件

¥Example: Adding a content metrics widget

以下是如何创建内容指标小部件的完整示例,该小部件显示 Strapi 应用中每种内容类型的条目数量。

¥The following is a complete example of how to create a content metrics widget that displays the number of entries for each content type in your Strapi application.

最终结果将在你的管理面板的 主页中显示如下:

¥The end result will look like the following in your admin panel's Homepage:

Billing tab of Profile pageBilling tab of Profile page

小部件显示计数,例如,当你在安装时提供 --example 标志时,Strapi 自动生成的 content-types(详情请参阅 CLI 安装选项)。

¥The widget shows counts for example content-types automatically generated by Strapi when you provide the --example flag on installation (see CLI installation options for details).

此小部件可以通过以下方式添加到 Strapi:

¥This widget can be added to Strapi by:

  1. 创建 "content-metrics" 插件(详情请参阅 插件创建 文档)

    ¥creating a "content-metrics" plugin (see plugin creation documentation for details)

  2. 重复使用下面提供的代码示例。

    ¥re-using the code examples provided below.

提示

如果你更喜欢亲自动手,可以重复使用以下 CodeSandbox 链接

¥If you prefer a hands-on approach, you can reuse the following CodeSandbox link.

以下文件注册插件和小部件:

¥The following file registers the plugin and the widget:

src/plugins/content-metrics/admin/src/index.js
import { PLUGIN_ID } from './pluginId';
import { Initializer } from './components/Initializer';
import { PluginIcon } from './components/PluginIcon';
import { Stethoscope } from '@strapi/icons'

export default {
register(app) {
app.addMenuLink({
to: `plugins/${PLUGIN_ID}`,
icon: PluginIcon,
intlLabel: {
id: `${PLUGIN_ID}.plugin.name`,
defaultMessage: PLUGIN_ID,
},
Component: async () => {
const { App } = await import('./pages/App');
return App;
},
});

app.registerPlugin({
id: PLUGIN_ID,
initializer: Initializer,
isReady: false,
name: PLUGIN_ID,
});

// Registers the widget
app.widgets.register({
icon: Stethoscope,
title: {
id: `${PLUGIN_ID}.widget.metrics.title`,
defaultMessage: 'Content Metrics',
},
component: async () => {
const component = await import('./components/MetricsWidget');
return component.default;
},
id: 'content-metrics',
pluginId: PLUGIN_ID,
});
},

async registerTrads({ locales }) {
return Promise.all(
locales.map(async (locale) => {
try {
const { default: data } = await import(`./translations/${locale}.json`);
return { data, locale };
} catch {
return { data: {}, locale };
}
})
);
},

bootstrap() {},
};

以下文件定义了小部件的组件及其逻辑。它将利用我们为插件创建的特定控制器和路由:

¥The following file defines the widget's component and its logic. It's tapping into a specific controller and route that we'll create for the plugin:

src/plugins/content-metrics/admin/src/components/MetricsWidget/index.js
import React, { useState, useEffect } from 'react';
import { Table, Tbody, Tr, Td, Typography, Box } from '@strapi/design-system';
import { Widget } from '@strapi/admin/strapi-admin'



const MetricsWidget = () => {


const [loading, setLoading] = useState(true);
const [metrics, setMetrics] = useState(null);
const [error, setError] = useState(null);

useEffect(() => {
const fetchMetrics = async () => {
try {
const response = await fetch('/api/content-metrics/count');
const data = await response.json();

console.log("data:", data);

const formattedData = {};

if (data && typeof data === 'object') {
Object.keys(data).forEach(key => {
const value = data[key];
formattedData[key] = typeof value === 'number' ? value : String(value);
});
}

setMetrics(formattedData);
setLoading(false);
} catch (err) {
console.error(err);
setError(err.message || 'An error occurred');
setLoading(false);
}
};

fetchMetrics();
}, []);

if (loading) {
return (
<Widget.Loading />
);
}

if (error) {
return (
<Widget.Error />
);
}

if (!metrics || Object.keys(metrics).length === 0) {
return <Widget.NoData>No content types found</Widget.NoData>;
}

return (
<Table>
<Tbody>
{Object.entries(metrics).map(([contentType, count], index) => (
<Tr key={index}>
<Td>
<Typography variant="omega">{String(contentType)}</Typography>
</Td>
<Td>
<Typography variant="omega" fontWeight="bold">{String(count)}</Typography>
</Td>
</Tr>
))}
</Tbody>
</Table>
);
};

export default MetricsWidget;

以下文件定义了一个自定义控制器,用于统计所有内容类型:

¥The following file defines a custom controller that counts all content-types:

src/plugins/content-metrics/server/src/controllers/metrics.js
'use strict';
module.exports = ({ strapi }) => ({
async getContentCounts(ctx) {
try {
// Get all content types
const contentTypes = Object.keys(strapi.contentTypes)
.filter(uid => uid.startsWith('api::'))
.reduce((acc, uid) => {
const contentType = strapi.contentTypes[uid];
acc[contentType.info.displayName || uid] = 0;
return acc;
}, {});

// Count entities for each content type
for (const [name, _] of Object.entries(contentTypes)) {
const uid = Object.keys(strapi.contentTypes)
.find(key =>
strapi.contentTypes[key].info.displayName === name || key === name
);

if (uid) {
// Using the count() method from the Document Service API
const count = await strapi.documents(uid).count();
contentTypes[name] = count;
}
}

ctx.body = contentTypes;
} catch (err) {
ctx.throw(500, err);
}
}
});

以下文件确保指标控制器可通过自定义 /count 路由访问:

¥The following file ensures that the metrics controller is reachable at a custom /count route:

src/plugins/content-metrics/server/src/routes/index.js
export default {
'content-api': {
type: 'content-api',
routes: [
{
method: 'GET',
path: '/count',
handler: 'metrics.getContentCounts',
config: {
policies: [],
},
},
],
},
};