生成仪表板选项卡应用

仪表板是一种跟踪、分析和显示数据以深入了解组织或特定流程的工具。 Teams 中的仪表板允许监视和查看重要指标。

Teams 工具包中的“仪表板”选项卡模板允许你开始将画布与多个卡片集成,这些卡片提供 Teams 中内容的概述。 可以执行下列操作:

  • 使用小组件在“仪表板”选项卡中显示应用和服务中的内容。
  • 将应用与图形 API集成,以可视化有关所选数据实现的详细信息。
  • 创建可自定义的仪表板,使你的企业能够设置特定目标,帮助你跟踪你需要在多个领域和跨部门查看的信息。

屏幕截图显示了仪表板的示例。

你的团队可以使用 Teams 仪表板 选项卡应用从 Teams 中的不同源获取最新更新。 使用仪表板选项卡应用来连接许多指标、数据源、API 和服务。 仪表板选项卡应用可帮助企业从源中提取相关信息,并将其呈现给用户。 有关创建仪表板选项卡应用的详细信息,请参阅分步指南

添加新仪表板

创建仪表板选项卡应用后,可以添加新仪表板。

若要添加新仪表板,请执行以下步骤:

  1. 创建仪表板类
  2. 重写方法以自定义仪表板选项卡应用
  3. 为新的仪表板选项卡应用添加路由
  4. 修改清单以添加新仪表板选项卡应用

创建仪表板类

在 目录中创建扩展名.tsx为 仪表板 src/dashboards 的文件,例如 YourDashboard.tsx。 然后,创建一个扩展 的类 BaseDashboard class from
@microsoft/teamsfx-react

//YourDashboard.tsx
import { BaseDashboard } from "@microsoft/teamsfx-react";

export default class YourDashboard extends BaseDashboard<any, any> {}

注意

所有方法都是可选的。 如果不重写任何方法,则使用默认仪表板布局。

重写方法以自定义仪表板选项卡应用

BaseDashboard提供了一些可替代方法来自定义仪表板布局。 下表列出了可以替代的方法:

方法 函数
styling() 自定义仪表板的样式。
layout() 定义小组件布局。

以下代码是自定义仪表板布局的示例:

.your-dashboard-layout {
  grid-template-columns: 6fr 4fr;
}
import { BaseDashboard } from "@microsoft/teamsfx-react";
import ListWidget from "../widgets/ListWidget";
import ChartWidget from "../widgets/ChartWidget";

export default class YourDashboard extends BaseDashboard<any, any> {
  override styling(): string {
    return "your-dashboard-layout";
  }

  override layout(): JSX.Element | undefined {
    return (
      <>
        <ListWidget />
        <ChartWidget />
      </>
    );
  }
}

为新的仪表板选项卡应用添加路由

必须将小组件链接到数据源文件。 小组件从源文件中选取仪表板中显示的数据。

打开 文件并src/App.tsx添加新仪表板的路由。 下面是一个示例:

import YourDashboard from "./dashboards/YourDashboard";

export default function App() {
  ...
  <Route path="/yourdashboard" element={<YourDashboard />} />
  ...
}

修改清单以添加新仪表板选项卡应用

打开 文件,appPackage/manifest.json并在 下staticTabs添加新的“仪表板”选项卡。 有关详细信息,请参阅 应用清单。 下面是一个示例:

{
  "entityId": "index1",
  "name": "Your Dashboard",
  "contentUrl": "${{TAB_ENDPOINT}}/index.html#/yourdashboard",
  "websiteUrl": "${{TAB_ENDPOINT}}/index.html#/yourdashboard",
  "scopes": ["personal"]
}

自定义仪表板布局

TeamsFx 提供了用于定义和修改仪表板布局的便捷方法。 以下是方法:

  • 一行中的三个小组件,高度为 350 像素,分别占宽度的 20%、60% 和 20%。

    .customize-class-name {
      grid-template-rows: 350px;
      grid-template-columns: 2fr 6fr 2fr;
    }
    
    export default class SampleDashboard extends BaseDashboard<any, any> {
    
      override styling(): string {
        return "customize-class-name";
      }
    
      override layout(): JSX.Element | undefined {
        return (
          <>
           <ListWidget />
           <ChartWidget />
           <NewsWidget />
          </>
        );
      }
    }
    

    屏幕截图显示了自定义仪表板布局。

  • 一行中的两个小组件,宽度为 600 像素和 1100 像素。 第一行的高度是其内容的最大高度,第二行的高度为 400 像素。

    .customize-class-name {
      grid-template-rows: max-content 400px;
      grid-template-columns: 600px 1100px;
    }
    
        export default class SampleDashboard extends Dashboard {
      override styling(): string {
        return "customize-class-name";
      }
    
      override layout(): JSX.Element | undefined {
        return (
          <>
        <ListWidget />
        <ChartWidget />
        <NewsWidget />
          </>
        );
      }
        }
    

    屏幕截图显示自定义仪表板布局的高度和宽度。

  • 在一列中排列两个小组件。

    .one-column {
        display: grid;
        gap: 20px;
        grid-template-rows: 1fr 1fr;
      }
    
      export default class SampleDashboard extends BaseDashboard<any, any> {
        override layout(): JSX.Element | undefined {
          return (
            <>
              <NewsWidget />
              <div className="one-column">
                <ListWidget />
                <ChartWidget />          
              </div>
            </>
          );
        }
      }
    

    屏幕截图显示了双小组件自定义。

仪表板选项卡应用抽象

为了调整仪表板的布局,TeamsFx 提供了一个BaseDashboard类,供开发人员实现仪表板。

以下代码是 类的示例 BaseDashboard

function dashboardStyle(isMobile?: boolean) {
  return mergeStyles({
    display: "grid",
    gap: "20px",
    padding: "20px",
    gridTemplateRows: "1fr",
    gridTemplateColumns: "4fr 6fr",
    ...(isMobile === true ? { gridTemplateColumns: "1fr", gridTemplateRows: "1fr" } : {}),
  });
}

interface BaseDashboardState {
  isMobile?: boolean;
  showLogin?: boolean;
  observer?: ResizeObserver;
}

export class BaseDashboard<P, S> extends Component<P, S & BaseDashboardState> {
  private ref: React.RefObject<HTMLDivElement>;

  public constructor(props: Readonly<P>) {
    super(props);
    this.state = {
      isMobile: undefined,
      showLogin: undefined,
      observer: undefined,
    } as S & BaseDashboardState;
    this.ref = React.createRef<HTMLDivElement>();
  }

  public async componentDidMount() {
    const observer = new ResizeObserver((entries) => {
      for (const entry of entries) {
        if (entry.target === this.ref.current) {
          const { width } = entry.contentRect;
          this.setState({ isMobile: width < 600 } as S & BaseDashboardState);
        }
      }
    });
    observer.observe(this.ref.current!);
  }

  public componentWillUnmount(): void {
    if (this.state.observer && this.ref.current) {
      this.state.observer.unobserve(this.ref.current);
    }
  }

  public render() {
    return (
      <div
        ref={this.ref}
        className={mergeStyles(dashboardStyle(this.state.isMobile), this.styling())}
      >
        {this.layout()}
      </div>
    );
  }

  protected layout(): JSX.Element | undefined {
    return undefined;
  }

  protected styling(): string {
    return null;
  }
}

在 类中 BaseDashboard ,TeamsFx 提供具有可自定义方法的基本布局。 仪表板仍然是一个响应组件,TeamsFx 根据响应组件的生命周期提供函数的基本实现,例如:

  • 基于网格布局实现基本呈现逻辑。
  • 添加观察程序以自动适应移动设备。

下面是要替代的可自定义方法:

方法 函数 建议重写
constructor() 初始化仪表板状态和变量。
componentDidMount() 在装载组件后调用。
componentWillUnmount() 卸载组件时调用 。
render() 发生更新时调用 。 此方法中定义了仪表板默认布局。
layout 定义仪表板中小组件的布局。 可以重写此方法。
styling() 自定义仪表板的样式。 可以重写此方法。

在仪表板中使用小组件

小组件在仪表板上显示可配置的信息和图表。 它们显示在小组件板上,你可以在其中固定、取消固定、排列、调整小组件大小和自定义小组件以反映你的兴趣。 小组件板经过优化,可根据使用情况显示相关小组件和个性化内容。

自定义小组件

可以通过重写 类中的 BaseWidget 以下方法来自定义小组件:

  • 重写 header()body()footer() 以自定义小组件。

    export class NewsWidget extends BaseWidget<any, any> {
    override header(): JSX.Element | undefined {
    return (
      <div>
        <News28Regular />
        <Text>Your News</Text>
        <Button icon={<MoreHorizontal32Regular />} appearance="transparent" />
      </div>
    );
    }
    
    override body(): JSX.Element | undefined {
    return (
      <div>
        <Image src="image.svg" />
        <Text>Lorem Ipsum Dolor</Text>
        <Text>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Enim,
          elementum sed
        </Text>
      </div>
    );
    }
    
    override footer(): JSX.Element | undefined {
    return (
      <Button
        appearance="transparent"
        icon={<ArrowRight16Filled />}
        iconPosition="after"
        size="small"
      >
        View details
      </Button>
    );
    }
    }
    

    屏幕截图显示了小组件中页眉、正文和页脚内容的示例。

  • 重写 body()footer() 以自定义小组件。

    export class NewsWidget extends BaseWidget<any, any> {
    override body(): JSX.Element | undefined {
    return (
      <div>
        <Image src="image.svg" />
        <Text>Lorem Ipsum Dolor</Text>
        <Text>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Enim,
          elementum sed
        </Text>
      </div>
    );
    }
    
    override footer(): JSX.Element | undefined {
    return (
      <Button
        appearance="transparent"
        icon={<ArrowRight16Filled />}
        iconPosition="after"
        size="small"
      >
        View details
      </Button>
    );
    }
    }
    

    屏幕截图显示了小组件中的正文和页脚内容。

  • 重写 body() 以自定义小组件。

    export class NewsWidget extends BaseWidget<any, any> {
    override body(): JSX.Element | undefined {
    return (
      <div>
        <Image src="image.svg" />
        <Text>Lorem Ipsum Dolor</Text>
        <Text>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Enim,
          elementum sed
        </Text>
      </div>
    );
    }
    }
    

    屏幕截图显示小组件中的正文内容。

包括数据加载程序

如果要在加载小组件之前将数据加载程序包含在小组件中,可以将属性添加到小组件的状态,以指示数据加载程序为 loading()。 可以使用此属性向用户显示加载指示器。

示例:

    override loading(): JSX.Element | undefined {
     return (
      <div className="loading">
       <Spinner label="Loading..." labelPosition="below" />
      </div>
     );
    }

现在,加载数据时会显示加载微调器。 加载数据时,加载微调器处于隐藏状态,并显示列表数据和页脚按钮。

图形表示形式显示加载数据时的加载微调器。

处理空状态

当数据为空时,可以在小组件中显示特定内容。 为此,需要修改 body 小组件文件中的 方法,以采用数据的不同状态。

以下示例演示如何在 ListWidget 的数据为空时显示空图像。

override body(): JSX.Element | undefined {
  let hasData = this.state.data && this.state.data.length > 0;
  return (
    <div>
      {hasData ? (
        <>
          {this.state.data?.map((t: ListModel) => {
            ...
          })}
        </>
      ) : (
        <div>
          <Image src="empty-default.svg" height="150px" />
          <Text align="center">No data</Text>
        </div>
      )}
    </div>
  );
}

当数据为空时,可以使用类似的方法来删除小组件的页脚内容。

  override footer(): JSX.Element | undefined {
    let hasData = this.state.data && this.state.data.length > 0;
    if (hasData) {
      return <Button>...</Button>;
    }
  }

当数据为空时,列表小组件如下所示:

显示列表中没有数据的屏幕截图。

按计划刷新数据

以下示例演示如何在小组件中显示实时数据。 小组件显示当前时间和更新。

interface IRefreshWidgetState {
  data: string;
}

export class RefreshWidget extends BaseWidget<any, IRefreshWidgetState> {
  override body(): JSX.Element | undefined {
    return <>{this.state.data}</>;
  }

  async componentDidMount() {
    setInterval(() => {
      this.setState({ data: new Date().toLocaleTimeString() });
    }, 1000);
  }
}

可以修改 setInterval 方法以调用自己的函数来刷新数据,如下所示 setInterval(() => yourGetDataFunction(), 1000)

小组件抽象

为了简化小组件的开发,TeamsFx SDK 提供了一个 BaseWidget 类,供开发人员继承以实现满足其需求的小组件,而无需过多关注实现小组件布局。

以下代码是 BaseWidget 类的示例:

export interface IWidgetClassNames {
  root?: string;
  header?: string;
  body?: string;
  footer?: string;
}

const classNames: IWidgetClassNames = mergeStyleSets({
  root: {
    display: "grid",
    padding: "1.25rem 2rem 1.25rem 2rem",
    backgroundColor: tokens.colorNeutralBackground1,
    border: "1px solid var(--colorTransparentStroke)",
    boxShadow: tokens.shadow4,
    borderRadius: tokens.borderRadiusMedium,
    gap: tokens.spacingHorizontalL,
    gridTemplateRows: "max-content 1fr max-content",
  },
  header: {
    display: "grid",
    height: "max-content",
    "& div": {
      display: "grid",
      gap: tokens.spacingHorizontalS,
      alignItems: "center",
      gridTemplateColumns: "min-content 1fr min-content",
    },
    "& svg": {
      height: "1.5rem",
      width: "1.5rem",
    },
    "& span": {
      fontWeight: tokens.fontWeightSemibold,
      lineHeight: tokens.lineHeightBase200,
      fontSize: tokens.fontSizeBase200,
    },
  },
  footer: {
    "& button": {
      width: "fit-content",
    },
  },
});

interface BaseWidgetState {
  loading?: boolean;
}

export class BaseWidget<P, S> extends Component<P, S & BaseWidgetState> {
  public constructor(props: Readonly<P>) {
    super(props);
    this.state = { loading: undefined } as S & BaseWidgetState;
  }

  public async componentDidMount() {
    this.setState({ ...(await this.getData()), loading: false });
  }

  public render() {
    const { root, header, body, footer } = this.styling();
    const showLoading = this.state.loading !== false && this.loading() !== undefined;
    return (
      <div className={mergeStyles(classNames.root, root)}>
        {this.header() && (
          <div className={mergeStyles(classNames.header, header)}>{this.header()}</div>
        )}
        {showLoading ? (
          this.loading()
        ) : (
          <>
            {this.body() !== undefined && <div className={body}>{this.body()}</div>}
            {this.footer() !== undefined && (
              <div className={mergeStyles(classNames.footer, footer)}>{this.footer()}</div>
            )}
          </>
        )}
      </div>
    );
  }

  protected async getData(): Promise<S> {
    return undefined;
  }

  protected header(): JSX.Element | undefined {
    return undefined;
  }

  protected body(): JSX.Element | undefined {
    return undefined;
  }

  protected footer(): JSX.Element | undefined {
    return undefined;
  }

  protected loading(): JSX.Element | undefined {
    return undefined;
  }

  protected styling(): IWidgetClassNames {
    return {};
  }
}

下面是替代的建议方法:

方法 函数 建议重写
constructor() 调用初始 this.state 并调用超级类 React.Component的构造函数。
componentDidMount() 在装载组件后调用 ,并通过调用 getData() 方法将值分配给data状态的 属性。
render() 每当有更新时调用 。 此方法中定义了仪表板默认布局。
getData() 调用小组件所需的数据。 此方法返回的值设置为 this.state.data
header() 调用小组件标头的外观。 可以选择重写此方法以自定义小组件,否则小组件将不具有标头。
body() 调用小组件正文的外观。 可以选择重写此方法以自定义小组件,否则小组件将不具有正文。
footer() 调用小组件页脚的外观。 可以选择重写此方法以自定义小组件,否则小组件将没有页脚。
loading() 当小组件正在提取数据时调用 。 如果需要加载指示器,方法可以返回一个 JSX.Element ,其中包含呈现加载指示器所需的组件。
style() 调用一个 对象,该对象定义小组件的不同部分的类名称。

Microsoft Graph 工具包作为小组件内容

Microsoft Graph 工具包是一组可更新的、与框架无关的 Web 组件,可帮助访问和使用 Microsoft Graph。 可以将 Microsoft Graph 工具包与任何 Web 框架一起使用,也可以不使用框架。

若要使用 Microsoft Graph 工具包作为小组件内容,请执行以下步骤:

  1. 向 Teams 应用添加 SSO 功能:Microsoft Teams 为应用提供单一登录 (SSO) 功能,以获取登录的 Teams 用户令牌以访问 Microsoft Graph。 有关详细信息,请参阅 Teams 应用的 SSO 功能

  2. 安装所需的 npm 包。

    在项目 tabs 文件夹中运行以下命令以安装所需的 npm 包:

    npm install @microsoft/mgt-react @microsoft/mgt-teamsfx-provider
    
  3. 添加新的 Graph 工具包小组件:在项目 src/views/widgets 文件夹中创建新的小组件文件, GraphWidget.tsx例如 。 在此小组件中,我们将引导用户同意应用访问 Microsoft Graph,然后使用 Microsoft Graph 工具包显示用户的待办事项列表。

    以下代码是在小组件中使用 Microsoft Graph 工具包中的 Todo 组件的示例:

    import { Providers, ProviderState, Todo } from "@microsoft/mgt-react";
    import { TeamsFxProvider } from "@microsoft/mgt-teamsfx-provider";
    
    import { loginAction } from "../../internal/login";
    import { TeamsUserCredentialContext } from "../../internal/singletonContext";
    import { BaseWidget } from "@microsoft/teamsfx-react";
    
    interface IGraphWidgetState {
      needLogin: boolean;
    }
    
    export class GraphWidget extends Widget<any, IGraphWidgetState> {
      override body(): JSX.Element | undefined {
        return <div>{this.state.needLogin === false && <Todo />}</div>;
      }
    
      async componentDidMount() {
        super.componentDidMount();
    
        // Initialize TeamsFx provider
        const provider = new TeamsFxProvider(TeamsUserCredentialContext.getInstance().getCredential(), [
         "Tasks.ReadWrite",
    ]);
        Providers.globalProvider = provider;
    
        // Check if user is signed in
        if (await this.checkIsConsentNeeded()) {
          await loginAction(["Tasks.ReadWrite"]);
    }
    
    // Update signed in state
    Providers.globalProvider.setState(ProviderState.SignedIn);
    this.setState({ needLogin: false });
    
      }
    
      /**
    
      * Check if user needs to consent
      * @returns true if user needs to consent
      */
    
      async checkIsConsentNeeded() {
        let needConsent = false;
        try {
          await TeamsUserCredentialContext.getInstance().getCredential().getToken(["Tasks.ReadWrite"]);
        } catch (error) {
          needConsent = true;
        }
        return needConsent;
      }
    }
    

    可以在小组件中使用替代的 Microsoft Graph 工具包组件。 有关详细信息,请参阅 Microsoft Graph 工具包

  4. 将小组件添加到仪表板布局。 在仪表板文件中包含新小组件。

    ...
    export default class YourDashboard extends BaseDashboard<any, any> {
      ...
      override layout(): undefined | JSX.Element {
        return (
          <>
            <GraphWiget />
          </>
        );
      }
      ...
    }
    

现在,启动或刷新 Teams 应用,你将看到使用 Microsoft Graph 工具包的新小组件。

图形 API调用

Microsoft 图形 API是一个 Web API,可用于与 Microsoft 云和其他服务进行通信。 自定义应用程序可以使用 Microsoft Graph API 连接到数据,并在自定义应用程序中使用它来增强组织工作效率。

在实现图形 API调用逻辑之前,需要为仪表板项目启用 SSO。 有关详细信息,请参阅 向 Teams 应用添加单一登录

添加图形 API调用:

从前端调用图形 API (使用委托的权限)

如果要从前端选项卡调用图形 API,请执行以下步骤:

  1. 若要获取与要调用的图形 API关联的权限范围的名称,请参阅 图形 API

  2. 通过添加与要调用图形 API相关的范围来创建 Graph 客户端。

    let credential: TeamsUserCredential;  
    credential = TeamsUserCredentialContext.getInstance().getCredential();
    const graphClient: Client = createMicrosoftGraphClientWithCredential(credential, scope);
    
  3. 调用图形 API并将响应分析为特定模型。

    try {
      const graphApiResult = await graphClient.api("<GRAPH_API_PATH>").get();
      // Parse the graphApiResult into a Model you defined, used by the front-end.
    } catch (e) {}
    

从后端调用图形 API (使用应用程序权限)

如果要从后端调用图形 API,请执行以下步骤:

  1. 同意应用程序权限
  2. 添加 Azure 函数
  3. 在 Azure 函数中添加逻辑
  4. 从前端调用 Azure 函数

若要同意应用程序权限,请执行以下步骤:

  1. 转到Azure 门户
  2. 选择“Microsoft Entra ID”。
  3. 在左窗格中选择“应用注册”。
  4. 选择仪表板应用。
  5. 在左窗格中选择“ API 权限 ”。
  6. 选择“ 添加权限”。
  7. 选择 Microsoft Graph
  8. 选择“应用程序权限”。
  9. 查找所需的权限。
  10. 选择底部的 “添加权限 ”按钮。
  11. 选择“ ✔授予管理员同意”。
  12. 选择“ ”按钮以完成管理员同意。

添加 Azure 函数

在Visual Studio Code的左窗格中,转到 Teams 工具包>添加功能>Azure Functions并输入函数名称。

屏幕截图显示了选择Azure Functions。

有关如何将 Azure 函数添加到项目的详细信息,请参阅将 Azure Functions 与 Teams 应用集成

在 Azure 函数中添加逻辑

index.ts/index.ts在名为 Azure Function 的文件夹下,可以添加包含后端图形 API调用应用程序权限的逻辑。 请参阅以下代码片段:

/**
 * This function handles requests from teamsfx client.
 * The HTTP request should contain an SSO token queried from Teams in the header.
 * Before triggering this function, teamsfx binding would process the SSO token and generate teamsfx configuration.
 *
 * You should initializes the teamsfx SDK with the configuration and calls these APIs.
 *
 * The response contains multiple message blocks constructed into a JSON object, including:
 * - An echo of the request body.
 * - The display name encoded in the SSO token.
 * - Current user's Microsoft 365 profile if the user has consented.
 *
 * @param {Context} context - The Azure Functions context object.
 * @param {HttpRequest} req - The HTTP request.
 * @param {teamsfxContext} TeamsfxContext - The context generated by teamsfx binding.
 */
export default async function run(
  context: Context,
  req: HttpRequest,
  teamsfxContext: TeamsfxContext
): Promise<Response> {
  context.log("HTTP trigger function processed a request.");

  // Initialize response.
  const res: Response = {
    status: 200,
    body: {},
  };

  // Your logic here.

  return res;
}

从前端调用 Azure 函数

按函数名称调用 Azure 函数。 请参阅以下代码片段来调用 Azure 函数:

const functionName = process.env.REACT_APP_FUNC_NAME || "myFunc";
export let taskName: string;

export async function callFunction(params?: string) {
  taskName = params || "";
  const credential = TeamsUserCredentialContext.getInstance().getCredential();
  if (!credential) {
    throw new Error("TeamsFx SDK is not initialized.");
  }
  try {
    const apiBaseUrl = process.env.REACT_APP_FUNC_ENDPOINT + "/api/";    
    const apiClient = createApiClient(
      apiBaseUrl,
      new BearerTokenAuthProvider(async () => (await credential.getToken(""))!.token)
    );
    const response = await apiClient.get(functionName);
    return response.data;
  } catch (err: unknown) {
    ...
  }
}

有关更多信息,请参阅:

嵌入 Power BI 以仪表板

若要将 Power BI 嵌入到仪表板,请参阅 Power BI 客户端响应

分步指南

按照分指南生成仪表板,并了解如何向仪表板添加小组件和图形 API调用。

另请参阅