> ## Documentation Index
> Fetch the complete documentation index at: https://docs.twenty.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 4. 构建界面

> 查看、侧边栏导航、命令和前部件。

现在，对象只能通过设置访问。 让应用在 UI 中真正“现身”：列表视图、侧边栏条目、一键式**生成文档**命令、用于**预览**文档的记录页面前端组件，以及用于模板的原生富文本**编辑器**选项卡。

## 视图和导航

[view](/l/zh/developers/extend/apps/layout/views) 是一个已保存的对象列表。
[导航菜单项](/l/zh/developers/extend/apps/layout/navigation-menu-items)
将此视图放置在侧边栏中。

```ts filename="src/views/documents.view.ts" theme={null}
import { defineView, ViewKey } from 'twenty-sdk/define';

export default defineView({
  universalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
  name: 'All documents',
  objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
  icon: 'IconFile',
  key: ViewKey.INDEX,
  position: 0,
  fields: [
    { universalIdentifier: DOCUMENTS_VIEW_NAME_FIELD_UNIVERSAL_IDENTIFIER,
      fieldMetadataUniversalIdentifier: DOCUMENT_NAME_FIELD_UNIVERSAL_IDENTIFIER,
      position: 0, isVisible: true, size: 280 },
    { universalIdentifier: DOCUMENTS_VIEW_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
      fieldMetadataUniversalIdentifier: DOCUMENT_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
      position: 1, isVisible: true, size: 120 },
    { universalIdentifier: DOCUMENTS_VIEW_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
      fieldMetadataUniversalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
      position: 2, isVisible: true, size: 200 },
  ],
});
```

```ts filename="src/navigation-menu-items/documents.navigation-menu-item.ts" theme={null}
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';

export default defineNavigationMenuItem({
  universalIdentifier: DOCUMENTS_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
  name: 'Documents',
  icon: 'IconFile',
  color: 'green',
  position: 1,
  type: NavigationMenuItemType.VIEW,
  viewUniversalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
});
```

为模板添加相同的配对。 两者现在都显示在侧边栏：

<Frame caption="侧边栏中的文档和模板，并列出生成的文档。">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty/images/docs/developers/extends/apps/documents/document-generator/04-documents-view.png" alt="使用生成文档的文档视图" />
</Frame>

## 前台组件

[前端组件](/l/zh/developers/extend/apps/layout/front-components)是一个被沙箱隔离在 Twenty 内部的 React 组件。 我们读取了选中的记录，通过 `CoreApiClient` 和 POSTs 将
人模板加载到最后一章的
路线。

```tsx filename="src/front-components/generate-document-form.front-component.tsx" theme={null}
import { useEffect, useState } from 'react';
import { CoreApiClient } from 'twenty-client-sdk/core';
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar, useSelectedRecordIds } from 'twenty-sdk/front-component';

const GenerateDocumentForm = () => {
  const selectedRecordIds = useSelectedRecordIds();
  const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
  const [templates, setTemplates] = useState<{ id: string; name: string }[]>([]);
  const [templateId, setTemplateId] = useState('');

  useEffect(() => {
    new CoreApiClient()
      .query({ documentTemplates: {
        __args: { filter: { target: { eq: 'PERSON' } }, first: 100 },
        edges: { node: { id: true, name: true } } } })
      .then(({ documentTemplates }) => {
        const list = documentTemplates?.edges?.map((e) => e.node) ?? [];
        setTemplates(list);
        if (list[0]) setTemplateId(list[0].id);
      });
  }, []);

  const generate = async () => {
    const apiBaseUrl = process.env.TWENTY_API_URL;
    const token = process.env.TWENTY_APP_ACCESS_TOKEN ?? process.env.TWENTY_API_KEY;
    const res = await fetch(`${apiBaseUrl}/s/documents/generate`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
      body: JSON.stringify({ templateId, recordId }),
    }).then((r) => r.json());
    await enqueueSnackbar({
      message: res.success ? 'Document generated.' : 'Generation failed.',
      variant: res.success ? 'success' : 'error',
    });
  };

  // ...render a <select> of templates and a Generate button
};

export default defineFrontComponent({
  universalIdentifier: GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
  name: 'generate-document-form',
  component: GenerateDocumentForm,
});
```

<Warning>
  内联 CSS 变量的样式(`var(--t-color-blu)`)，不是从
  `twai`导入的值。 构建过程中的 SDK 模型，所以模块级导入的
  主题常量将是“未定义的”。 请参阅
  [完整组件](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/front-components/generate-document-form.front-component.tsx)。
</Warning>

## 打开它的命令

当选中某个 Person 时，带有 `availabilityType: 'RECORD_SELECTION'` 的[命令菜单项](/l/zh/developers/extend/apps/layout/command-menu-items)会出现，并在侧边面板中打开该组件。

```ts filename="src/command-menu-items/generate-document.command-menu-item.ts" theme={null}
import { defineCommandMenuItem, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';

export default defineCommandMenuItem({
  universalIdentifier: GENERATE_DOCUMENT_COMMAND_UNIVERSAL_IDENTIFIER,
  label: 'Generate document',
  availabilityType: 'RECORD_SELECTION',
  availabilityObjectUniversalIdentifier:
    STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
  frontComponentUniversalIdentifier:
    GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
});
```

## 尝试整个流程

打开 **People**，勾选一位人员，然后按下 <kbd>⌘K</kbd> / <kbd>Ctrl K</kbd>。
“生成文档”出现，标记为您的应用：

<Frame caption="命令显示何时选中某人。">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty/images/docs/developers/extends/apps/documents/document-generator/06-command-menu.png" alt="带生成文档的命令菜单" />
</Frame>

运行它 — — 您的组件在侧面板中打开. 选择一个模板，点击
**生成**，以及在 **Documents** 中选择一个新的记录土地。

<Frame caption="前端组件，加载模板并生成点击。">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty/images/docs/developers/extends/apps/documents/document-generator/06b-front-compon.png" alt="生成文档侧面板" />
</Frame>

每个生成的文档都将您的应用记录为其作者：

<Frame caption="由文档生成器创建，状态已生成。">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty/images/docs/developers/extends/apps/documents/document-generator/05-document-record.png" alt="生成的文档记录" />
</Frame>

## 预览其记录页面上的文档

前面的组件不仅仅是命令菜单 — — 你可以在
录制页面上挂载一个 \*\*选项卡。 让我们在文档记录中添加一个 *Preview* 选项卡，使得
Markdown 物体作为一个可打印的打印页面。

组件从执行上下文读取当前记录ID，加载
文档并将其渲染。 前端组件运行在一个只允许白名单 HTML 标签的**沙箱**中——原始 HTML 注入（`dangerouslySetInnerHTML`）和 `\<style>` 会被阻止——因此我们通过一个小型的 [`Markdown`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/markdown-to-react.tsx) 助手，将 Markdown 渲染为带内联样式的 React 元素。

```tsx filename="src/front-components/document-viewer.front-component.tsx" theme={null}
import { CoreApiClient } from 'twenty-client-sdk/core';
import { defineFrontComponent } from 'twenty-sdk/define';
import { useFrontComponentExecutionContext } from 'twenty-sdk/front-component';
import { Markdown } from 'src/utils/markdown-to-react';

const DocumentViewer = () => {
  const recordId = useFrontComponentExecutionContext((c) => c.recordId ?? null);
  // ...load { content, file } for recordId, then derive the links:
  const pdfUrl = document.file?.[0]?.url;
  const webUrl = `${process.env.TWENTY_API_URL ?? ''}/s/documents/view?id=${recordId}`;

  // Render the template body, plus quick links to the web page and the PDF.
  // Links open in a new tab so they don't navigate the embedded component.
  return (
    <div style={styles.scroll}>
      <div style={styles.actions}>
        <a style={styles.actionLink} href={webUrl} target="_blank" rel="noopener noreferrer">
          Open web page
        </a>
        {pdfUrl ? (
          <a style={styles.actionLink} href={pdfUrl} target="_blank" rel="noopener noreferrer">
            Download PDF
          </a>
        ) : null}
      </div>
      <div style={styles.paper}>
        <div style={styles.body}>
          <Markdown content={document.content} />
        </div>
      </div>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
  name: 'document-viewer',
  component: DocumentViewer,
});
```

用[页面布局](/l/zh/developers/extend/apps/layout/page-layouts)挂载它。 `RECORD_PAGE` 布局会在对象的记录视图中添加选项卡；位于 `CANVAS` 选项卡中的 `FRONT_COMPONENT` 小部件则承载该组件：

```ts filename="src/page-layouts/document-record.page-layout.ts" theme={null}
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';

export default definePageLayout({
  universalIdentifier: DOCUMENT_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
  name: 'Document record page',
  type: 'RECORD_PAGE',
  objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
  tabs: [{
    universalIdentifier: DOCUMENT_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
    title: 'Preview',
    icon: 'IconEye',
    position: 50,
    layoutMode: PageLayoutTabLayoutMode.CANVAS,
    widgets: [{
      universalIdentifier: DOCUMENT_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
      title: 'Document preview',
      type: 'FRONT_COMPONENT',
      configuration: {
        configurationType: 'FRONT_COMPONENT',
        frontComponentUniversalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
      },
    }],
  }],
});
```

打开任何文档 — **预览** 选项卡使它美丽，并链接到
可分享的网页和 PDF ：

<Frame caption="预览选项卡使用内联样式以及快速链接打开文档。">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty/images/docs/developers/extends/apps/documents/document-generator/09-document-viewer.png" alt="记录页面选项卡中的文档查看器前置组件" />
</Frame>

## 使用富文本编辑器编辑模板

模板根本不需要自定义组件。 因为`body` 是一个
`RICH_TEXT` 字段 20个已经为此提供了一个完整的文本编辑器——
与标准注释和任务对象相同。 我们只是在
模板记录页面上显示。

在 `EDITOR` 显示模式中添加一个 `FIELD` 小部件的标签，通过 `field MetadataId` 指向`body`
字段：

```ts filename="src/page-layouts/template-record.page-layout.ts" theme={null}
{
  universalIdentifier: TEMPLATE_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
  title: 'Template',
  position: 1,
  layoutMode: PageLayoutTabLayoutMode.GRID,
  widgets: [{
    universalIdentifier: TEMPLATE_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
    title: 'Template',
    type: 'FIELD',
    gridPosition: { row: 0, column: 0, rowSpan: 6, columnSpan: 12 },
    configuration: {
      configurationType: 'FIELD',
      fieldMetadataId: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
      fieldDisplayMode: 'EDITOR',
    },
  }],
}
```

一个`RICH_TEXT`字段既存储编辑器块JSON也存储Markdown
投影。 生成管道读取Markdown 投影, 所以
占位符, PDF, 和可共享的网页都保持正常工作状态 —
查看完整的
[“模板-记录”。 年龄布局。s\`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/page-layouts/template-record.page-layout.ts)。
现在编辑器在一个合适的富文本编辑器中写入模板：

<Frame caption="模板选项卡：20个本地的富文本编辑器绑定到实体字段。">
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/twenty/images/docs/developers/extends/apps/documents/document-generator/10-template-editor.png" alt="本地富文本编辑器选项卡的模板记录" />
</Frame>

**在这一步之后：** 文档预览，模板是可编辑的
在应用中。 接下来，让AI 代理人从聊天室中生成它们。

<Card title="下一步：一个 AI 代理 →" icon="robot" href="/l/zh/developers/extend/apps/tutorials/document-generator/ai-agent">
  添加代理和技能来呼叫您的工具。
</Card>
