diff --git a/docs/zh-Hans/SignalR-Integration.md b/docs/zh-Hans/SignalR-Integration.md index 8045bbd394..3dea373f5a 100644 --- a/docs/zh-Hans/SignalR-Integration.md +++ b/docs/zh-Hans/SignalR-Integration.md @@ -224,4 +224,14 @@ ABP实现 `SignalR` 的 `IUserIdProvider` 接口,从ABP框架的 `ICurrentUser` 参阅 [SignalR集成Demo](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo),它有一个简单的聊天页面,可以在(经过身份验证的)用户之间发送消息. -![signalr-demo-chat](images/signalr-demo-chat.png) \ No newline at end of file +![signalr-demo-chat](images/signalr-demo-chat.png) + +## 备注 + +ABP框架不会更改SignalR. 就像在其他ASP.NET Core应用程序中一样,它也可以在基于ABP框架的应用程序中工作. + +参考[微软文档](https://docs.microsoft.com/zh-cn/aspnet/core/signalr/scale)托管和扩展您的应用程序,集成[Azure](https://docs.microsoft.com/zh-cn/aspnet/core/signalr/publish-to-azure-web-app)或[Redis底版](https://docs.microsoft.com/zh-cn/aspnet/core/signalr/redis-backplane)...等. + +## 另请参阅 + +* [微软SignalR文档](https://docs.microsoft.com/zh-cn/aspnet/core/signalr/introduction) \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/Part-1.md b/docs/zh-Hans/Tutorials/Part-1.md index 9589c430b6..5ee6b31e08 100644 --- a/docs/zh-Hans/Tutorials/Part-1.md +++ b/docs/zh-Hans/Tutorials/Part-1.md @@ -30,7 +30,7 @@ ASP.NET Core {{UI_Value}} 系列教程包括三个3个部分: - [Part-2: 创建,编辑,删除书籍](Part-2.md) - [Part-3: 集成测试](Part-3.md) -> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-booktore-application). +> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). ### 创建新项目 @@ -44,19 +44,19 @@ ASP.NET Core {{UI_Value}} 系列教程包括三个3个部分: abp new Acme.BookStore --template app --database-provider {{DB}} --ui {{UI_Text}} --mobile none ``` -![Creating project](./images/booktore-create-project-{{UI_Text}}.png) +![Creating project](./images/bookstore-create-project-{{UI_Text}}.png) ### 应用迁移 项目创建后,需要应用初始化迁移创建数据库. 运行 `Acme.BookStore.DbMigrator` 应用程序. 它会应用所有迁移,完成流程后你会看到以下结果,数据库已经准备好了! -![Migrations applied](./images/booktore-migrations-applied-{{UI_Text}}.png) +![Migrations applied](./images/bookstore-migrations-applied-{{UI_Text}}.png) > 另外你也可以在 Visual Studio 包管理控制台运行 `Update-Database` 命令应用迁移. #### 初始化数据库表 -![Initial database tables](./images/booktore-database-tables-{{DB}}.png) +![Initial database tables](./images/bookstore-database-tables-{{DB}}.png) ### 运行应用程序 @@ -64,7 +64,7 @@ abp new Acme.BookStore --template app --database-provider {{DB}} --ui {{UI_Text} 更多信息,参阅[入门教程](../../Getting-Started?UI={{UI}})的运行应用程序部分. -![Set as startup project](./images/booktore-start-project-{{UI_Text}}.png) +![Set as startup project](./images/bookstore-start-project-{{UI_Text}}.png) {{if UI == "NG"}} @@ -105,7 +105,7 @@ http://localhost:4200/ 下面的图片展示了从启动模板创建的项目是如何分层的. -![booktore-visual-studio-solution](./images/booktore-solution-structure-{{UI_Text}}.png) +![bookstore-visual-studio-solution](./images/bookstore-solution-structure-{{UI_Text}}.png) > 你可以查看[应用程序模板文档](../startup-templates/application#solution-structure)以详细了解解决方案结构. @@ -295,7 +295,7 @@ namespace Acme.BookStore 这个启动模板使用了[EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/)来创建并维护数据库结构.打开 **程序包管理器控制台(Package Manager Console) (PMC)** (工具/Nuget包管理器菜单) -![Open Package Manager Console](./images/booktore-open-package-manager-console.png) +![Open Package Manager Console](./images/bookstore-open-package-manager-console.png) 选择 `Acme.BookStore.EntityFrameworkCore.DbMigrations`作为默认的项目然后执行下面的命令: @@ -303,7 +303,7 @@ namespace Acme.BookStore Add-Migration "Created_Book_Entity" ``` -![booktore-pmc-add-book-migration](./images/booktore-pmc-add-book-migration-v2.png) +![bookstore-pmc-add-book-migration](./images/bookstore-pmc-add-book-migration-v2.png) 这样就会在 `Migrations` 文件夹中创建一个新的migration类.然后执行 `Update-Database` 命令更新数据库结构: @@ -311,7 +311,7 @@ Add-Migration "Created_Book_Entity" Update-Database ```` -![booktore-update-database-after-book-entity](./images/booktore-update-database-after-book-entity.png) +![bookstore-update-database-after-book-entity](./images/bookstore-update-database-after-book-entity.png) #### 添加示例数据 @@ -328,7 +328,7 @@ INSERT INTO AppBook (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES ('4fa024a1-95ac-49c6-a709-6af9e4d54b54', '2018-07-02', 'Pet Sematary',5,'1983-11-14','23.7') ``` -![booktore-book-table](./images/booktore-book-table.png) +![bookstore-book-table](./images/bookstore-book-table.png) {{end}} @@ -501,7 +501,7 @@ namespace Acme.BookStore 你会看到一些内置的接口和`Book`的接口,它们都是REST风格的: -![booktore-swagger](images/booktore-swagger.png) +![bookstore-swagger](images/bookstore-swagger.png) Swagger有一个很好的UI来测试API. 你可以尝试执行`[GET] /api/app/book` API来获取书籍列表. @@ -529,11 +529,11 @@ acme.bookStore.book.getList({}).done(function (result) { console.log(result); }) 运行这段代码会产生下面的输出: -![booktore-test-js-proxy-getlist](./images/booktore-test-js-proxy-getlist.png) +![bookstore-test-js-proxy-getlist](./images/bookstore-test-js-proxy-getlist.png) 你可以看到服务器返回的 **book list**.你还可以切换到开发者工具的 **network** 查看客户端到服务器端的通讯信息: -![booktore-test-js-proxy-getlist-network](./images/booktore-test-js-proxy-getlist-network.png) +![bookstore-test-js-proxy-getlist-network](./images/bookstore-test-js-proxy-getlist-network.png) 我们使用`create`方法 **创建一本新书**: @@ -555,7 +555,7 @@ successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246 在 `Acme.BookStore.Web`项目的`Pages`文件夹下创建一个新的文件夹叫`Book`并添加一个名为`Index.cshtml`的Razor Page. -![booktore-add-index-page](./images/booktore-add-index-page-v2.png) +![bookstore-add-index-page](./images/bookstore-add-index-page-v2.png) 打开`Index.cshtml`并把内容修改成下面这样: @@ -621,7 +621,7 @@ namespace Acme.BookStore.Web.Menus 本地化文本位于`Acme.BookStore.Domain.Shared`项目的`Localization/BookStore`文件夹下: -![booktore-localization-files](./images/booktore-localization-files-v2.png) +![bookstore-localization-files](./images/bookstore-localization-files-v2.png) 打开`en.json`文件,将`Menu:BookStore`和`Menu:Book`键的本地化文本添加到文件末尾: @@ -653,7 +653,7 @@ namespace Acme.BookStore.Web.Menus 运行该应用程序,看到新菜单项已添加到顶部栏: -![booktore-menu-items](./images/booktore-new-menu-item.png) +![bookstore-menu-items](./images/bookstore-new-menu-item.png) 点击BookStore下Book子菜单项就会跳转到新增的书籍页面. @@ -701,7 +701,7 @@ namespace Acme.BookStore.Web.Menus 在`Pages/Book/`文件夹中创建 `index.js`文件 -![booktore-index-js-file](./images/booktore-index-js-file-v2.png) +![bookstore-index-js-file](./images/bookstore-index-js-file-v2.png) `index.js`的内容如下: @@ -727,7 +727,7 @@ $(function () { 最终的页面如下: -![Book list](./images/booktore-book-list-2.png) +![Book list](./images/bookstore-book-list-2.png) {{end}} @@ -760,7 +760,7 @@ yarn yarn ng generate module book --routing true ``` -![Generating book module](./images/booktore-creating-book-module-terminal.png) +![Generating book module](./images/bookstore-creating-book-module-terminal.png) #### 路由 @@ -793,7 +793,7 @@ import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added th yarn ng generate component book/book-list ``` -![Creating book list](./images/booktore-creating-book-list-terminal.png) +![Creating book list](./images/bookstore-creating-book-list-terminal.png) 打开 `app\book` 目录下的 `book.module.ts` 文件,使用以下内容替换它: @@ -849,7 +849,7 @@ yarn start 我们将看到book页面的 **book-list works!**: -![Initial book list page](./images/booktore-initial-book-list-page.png) +![Initial book list page](./images/bookstore-initial-book-list-page.png) #### 创建 BookState @@ -1048,11 +1048,11 @@ export class BookListComponent implements OnInit { 现在你可以在浏览器看到最终结果: -![Book list final result](./images/booktore-book-list.png) +![Book list final result](./images/bookstore-book-list.png) 项目的文件系统结构: -![Book list final result](./images/booktore-angular-file-tree.png) +![Book list final result](./images/bookstore-angular-file-tree.png) 在本教程中我们遵循了官方的[Angular风格指南](https://angular.io/guide/styleguide#file-tree). diff --git a/docs/zh-Hans/Tutorials/Part-2.md b/docs/zh-Hans/Tutorials/Part-2.md index 5e73c753e6..8dd64a49d0 100644 --- a/docs/zh-Hans/Tutorials/Part-2.md +++ b/docs/zh-Hans/Tutorials/Part-2.md @@ -29,7 +29,7 @@ end - **Part 2: 创建,编辑,删除书籍(本章)** - [Part-3: 集成测试](Part-3.md) -> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-booktore-application). +> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). {{if UI == "MVC"}} @@ -37,13 +37,13 @@ end 通过本节, 你将会了解如何创建一个 modal form 来实现新增书籍的功能. 最终成果如下图所示: -![booktore-create-dialog](./images/booktore-create-dialog-2.png) +![bookstore-create-dialog](./images/bookstore-create-dialog-2.png) #### 新建 modal form 在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个 `CreateModal.cshtml` Razor页面: -![booktore-add-create-dialog](./images/booktore-add-create-dialog-v2.png) +![bookstore-add-create-dialog](./images/bookstore-add-create-dialog-v2.png) ##### CreateModal.cshtml.cs @@ -130,7 +130,7 @@ namespace Acme.BookStore.Web.Pages.Books 如下图所示,只是在表格 **右上方** 添加了 **New book** 按钮: -![booktore-new-book-button](./images/booktore-new-book-button.png) +![bookstore-new-book-button](./images/bookstore-new-book-button.png) 打开 `Pages/book/index.js` 在 `datatable` 配置代码后面添加如下代码: @@ -155,7 +155,7 @@ $('#NewBookButton').click(function (e) { 在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个名叫 `EditModal.cshtml` 的Razor页面: -![booktore-add-edit-dialog](./images/booktore-add-edit-dialog.png) +![bookstore-add-edit-dialog](./images/bookstore-add-edit-dialog.png) #### EditModal.cshtml.cs @@ -258,7 +258,7 @@ namespace Acme.BookStore.Web 我们将为表格每行添加下拉按钮 ("Actions") . 最终效果如下: -![booktore-book-table-actions](images/booktore-book-table-actions.png) +![bookstore-book-table-actions](images/bookstore-book-table-actions.png) 打开 `Pages/Books/Index.cshtml` 页面,并按下方所示修改表格部分的代码: @@ -450,6 +450,8 @@ $(function () { {{end}} +{{if UI == "NG"}} + ### 新增 Book 实体 下面的章节中,你将学习到如何创建一个新的模态对话框来新增Book实体. @@ -654,7 +656,7 @@ export class BookListComponent implements OnInit { 你可以打开浏览器,点击**New book**按钮看到模态框. -![Empty modal for new book](./images/booktore-empty-new-book-modal.png) +![Empty modal for new book](./images/bookstore-empty-new-book-modal.png) #### 添加响应式表单 @@ -878,7 +880,7 @@ export class BookListComponent implements OnInit { 现在你可以打开浏览器看到以下变化: -![New book modal](./images/booktore-new-book-form.png) +![New book modal](./images/bookstore-new-book-form.png) #### 保存图书 @@ -990,7 +992,7 @@ export class BookListComponent implements OnInit { 模态框最终看起来像这样: -![Save button to the modal](./images/booktore-new-book-form-v2.png) +![Save button to the modal](./images/bookstore-new-book-form-v2.png) ### 更新图书 @@ -1192,7 +1194,7 @@ export class BookListComponent implements OnInit { UI最终看起来像这样: -![Action buttons](./images/booktore-actions-buttons.png) +![Action buttons](./images/bookstore-actions-buttons.png) 打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `` 标签: @@ -1322,7 +1324,7 @@ delete(id: string) { `delete` 方法会显示一个确认弹层并订阅用户响应. 只在用户点击 `Yes` 按钮时分派动作. 确认弹层看起来如下: -![booktore-confirmation-popup](./images/booktore-confirmation-popup.png) +![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png) #### 添加删除按钮 @@ -1339,7 +1341,7 @@ delete(id: string) { 最终操作下拉框UI看起来如下: -![booktore-final-actions-dropdown](./images/booktore-final-actions-dropdown.png) +![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown.png) {{end}} diff --git a/docs/zh-Hans/UI/Angular/Component-Replacement.md b/docs/zh-Hans/UI/Angular/Component-Replacement.md index dc7c1a33c4..4465f34893 100644 --- a/docs/zh-Hans/UI/Angular/Component-Replacement.md +++ b/docs/zh-Hans/UI/Angular/Component-Replacement.md @@ -15,16 +15,20 @@ import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddRepl import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum import { Store } from '@ngxs/store'; // imported Store //... -export class AppComponent { + +@Component(/* component metadata */) +export class AppComponent implements OnInit { constructor(..., private store: Store) {} // injected Store ngOnInit() { + // added dispatch this.store.dispatch( new AddReplaceableComponent({ component: YourNewRoleComponent, key: eIdentityComponents.Roles, }), ); + //... } } @@ -54,7 +58,7 @@ yarn ng generate component shared/my-application-layout --export --entryComponen ``` -打开 `app.component.ts` 添加以下内容: +打开 `src/app` 文件夹下的 `app.component.ts` 文件添加以下内容: ```js import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent @@ -62,11 +66,13 @@ import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeB import { MyApplicationLayoutComponent } from './shared/my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent import { Store } from '@ngxs/store'; // imported Store //... -export class AppComponent { + +@Component(/* component metadata */) +export class AppComponent implements OnInit { constructor(..., private store: Store) {} // injected Store ngOnInit() { - // added below content + // added dispatch this.store.dispatch( new AddReplaceableComponent({ component: MyApplicationLayoutComponent, @@ -79,6 +85,464 @@ export class AppComponent { } ``` +### 布局组件 + +![Layout Components](./images/layout-components.png) + +#### 如何替换LogoComponent + +![LogoComponent](./images/logo-component.png) + +在 `angular` 目录下运行以下命令创建新的组件 `LogoComponent`: + +```bash +yarn ng generate component logo --inlineTemplate --inlineStyle --entryComponent + +# You don't need the --entryComponent option in Angular 9 +``` + +打开 `src/app/logo` 目录下生成的 `logo.component.ts` 并使用以下内容替换它: + +```js +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-logo', + template: ` + + + logo + + `, +}) +export class LogoComponent {} +``` + +打开 `src/app` 目录下的 `app.component.ts` 做以下修改: + +```js +import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent +import { Store } from '@ngxs/store'; // imported Store +import { LogoComponent } from './logo/logo.component'; // imported NavItemsComponent +import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents +//... + +@Component(/* component metadata */) +export class AppComponent implements OnInit { + constructor(..., private store: Store) {} // injected Store + + ngOnInit() { + //... + + // added dispatch + this.store.dispatch( + new AddReplaceableComponent({ + component: LogoComponent, + key: eThemeBasicComponents.Logo, + }), + ); + } +} +``` + +最终UI如下: + +![New logo](./images/replaced-logo-component.png) + +#### 如何替换RoutesComponent + +![RoutesComponent](./images/routes-component.png) + +在 `angular` 目录下运行以下命令创建新的组件 `RoutesComponent`: + + +```bash +yarn ng generate component routes --entryComponent + +# You don't need the --entryComponent option in Angular 9 +``` + +打开 `src/app/routes` 目录下生成的 `routes.component.ts` 并使用以下内容替换它: + +```js +import { ABP, ReplaceableComponents } from '@abp/ng.core'; +import { + Component, + HostBinding, + Inject, + Renderer2, + TrackByFunction, + AfterViewInit, +} from '@angular/core'; +import { fromEvent } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; + +@Component({ + selector: 'app-routes', + templateUrl: 'routes.component.html', +}) +export class RoutesComponent implements AfterViewInit { + @HostBinding('class.mx-auto') + marginAuto = true; + + smallScreen = window.innerWidth < 992; + + constructor(private renderer: Renderer2) {} + + ngAfterViewInit() { + fromEvent(window, 'resize') + .pipe(debounceTime(150)) + .subscribe(() => { + this.smallScreen = window.innerWidth < 992; + }); + } +} +``` + +打开 `src/app/routes` 目录下生成的 `routes.component.html` 并使用以下内容替换它: + +```html + +``` + +打开 `src/app` 目录下的 `app.component.ts` 做以下修改: + +```js +import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent +import { Store } from '@ngxs/store'; // imported Store +import { RoutesComponent } from './routes/routes.component'; // imported NavItemsComponent +import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents +//... + +@Component(/* component metadata */) +export class AppComponent implements OnInit { + constructor(..., private store: Store) {} // injected Store + + ngOnInit() { + //... + + // added dispatch + this.store.dispatch( + new AddReplaceableComponent({ + component: RoutesComponent, + key: eThemeBasicComponents.Routes, + }), + ); + } +} +``` + +最终UI如下: + +![New routes](./images/replaced-routes-component.png) + +#### 如何替换NavItemsComponent + +![NavItemsComponent](./images/nav-items-component.png) + +在 `angular` 目录下运行以下命令创建新的组件 `NavItemsComponent`: + +```bash +yarn ng generate component nav-items --entryComponent + +# You don't need the --entryComponent option in Angular 9 +``` + +打开 `src/app/nav-items` 目录下生成的 `nav-items.component.ts` 并使用以下内容替换它: + +```js +import { + ApplicationConfiguration, + AuthService, + ConfigState, + SessionState, + SetLanguage, +} from '@abp/ng.core'; +import { Component, AfterViewInit } from '@angular/core'; +import { Navigate, RouterState } from '@ngxs/router-plugin'; +import { Select, Store } from '@ngxs/store'; +import { Observable, fromEvent } from 'rxjs'; +import { map, debounceTime } from 'rxjs/operators'; +import snq from 'snq'; + +@Component({ + selector: 'app-nav-items', + templateUrl: 'nav-items.component.html', +}) +export class NavItemsComponent implements AfterViewInit { + @Select(ConfigState.getOne('currentUser')) + currentUser$: Observable; + + @Select(ConfigState.getDeep('localization.languages')) + languages$: Observable; + + smallScreen = window.innerWidth < 992; + + get defaultLanguage$(): Observable { + return this.languages$.pipe( + map( + languages => + snq( + () => languages.find(lang => lang.cultureName === this.selectedLangCulture).displayName, + ), + '', + ), + ); + } + + get dropdownLanguages$(): Observable { + return this.languages$.pipe( + map( + languages => + snq(() => languages.filter(lang => lang.cultureName !== this.selectedLangCulture)), + [], + ), + ); + } + + get selectedLangCulture(): string { + return this.store.selectSnapshot(SessionState.getLanguage); + } + + constructor(private store: Store, private authService: AuthService) {} + + ngAfterViewInit() { + fromEvent(window, 'resize') + .pipe(debounceTime(150)) + .subscribe(() => { + this.smallScreen = window.innerWidth < 992; + }); + } + + onChangeLang(cultureName: string) { + this.store.dispatch(new SetLanguage(cultureName)); + } + + logout() { + this.authService.logout().subscribe(() => { + this.store.dispatch( + new Navigate(['/'], null, { + state: { redirectUrl: this.store.selectSnapshot(RouterState).state.url }, + }), + ); + }); + } +} +``` + +打开 `src/app/nav-items` 目录下生成的 `nav-items.component.html` 并使用以下内容替换它: + +```html + +``` + +打开 `src/app` 目录下的 `app.component.ts` 做以下修改: + +```js +import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent +import { Store } from '@ngxs/store'; // imported Store +import { NavItemsComponent } from './nav-items/nav-items.component'; // imported NavItemsComponent +import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents +//... + +@Component(/* component metadata */) +export class AppComponent implements OnInit { + constructor(..., private store: Store) {} // injected Store + + ngOnInit() { + //... + + // added dispatch + this.store.dispatch( + new AddReplaceableComponent({ + component: NavItemsComponent, + key: eThemeBasicComponents.NavItems, + }), + ); + } +} +``` + +最终UI如下: + +![New nav-items](./images/replaced-nav-items-component.png) + ## 下一步是什么? -- [自定义设置页面](./Custom-Setting-Page.md) +- [自定义设置页面](./Custom-Setting-Page.md) \ No newline at end of file diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json index d798580a25..23b17c1231 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/docs/zh-Hans/docs-nav.json @@ -454,6 +454,15 @@ } ] }, + { + "text": "实时", + "items": [ + { + "text": "SignalR集成", + "path": "SignalR-Integration.md" + } + ] + }, { "text": "后台服务", "items": [ diff --git a/docs/zh-Hans/images/hello-template.png b/docs/zh-Hans/images/hello-template.png new file mode 100644 index 0000000000..0a3ce2816a Binary files /dev/null and b/docs/zh-Hans/images/hello-template.png differ diff --git a/docs/zh-Hans/images/multiple-file-template.png b/docs/zh-Hans/images/multiple-file-template.png new file mode 100644 index 0000000000..a2e7327301 Binary files /dev/null and b/docs/zh-Hans/images/multiple-file-template.png differ