不农 發表於 2019-5-15 10:51:00

[开源]OSharpNS 步步为营系列 - 5. 添加前端Angular模块[完结]

<h1 id="什么是osharp">什么是OSharp</h1>
<p>OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于<code>.NetStandard2.0</code>开发的一个<code>.NetCore</code>快速开发框架。这个框架使用最新稳定版的<code>.NetCore SDK</code>(当前是.NET Core 2.2),对 AspNetCore 的配置、依赖注入、日志、缓存、实体框架、Mvc(WebApi)、身份认证、权限授权等模块进行更高一级的自动化封装,并规范了一套业务实现的代码结构与操作流程,使 .Net Core 框架更易于应用到实际项目开发中。</p>
<ul>
<li>开源地址:https://github.com/i66soft/osharp</li>
<li>官方示例:https://www.osharp.org</li>
<li>文档中心:https://docs.osharp.org</li>
<li>VS 插件:https://marketplace.visualstudio.com/items?itemName=LiuliuSoft.osharp</li>
<li>系列示例:https://github.com/i66soft/osharp-docs-samples</li>
</ul>
<h1 id="感谢大家关注">感谢大家关注</h1>
<p>首先特别感谢大家对OSharp快速开发框架的关注,这个系列每一篇都收到了比较多园友的关注,也在博客园首页开启了 <strong>霸屏模式</strong><br>
<img src="https://img2018.cnblogs.com/blog/87201/201905/87201-20190515104012609-1052163931.png" alt="" loading="lazy"></p>
<p>同时演示网站的用户注册数量也在持续上涨<br>
<img src="https://img2018.cnblogs.com/blog/87201/201905/87201-20190515104910541-1354596763.png" alt="" loading="lazy"></p>
<p>项目 Star 数也增长了几百,欢迎没点 Star 的也来关注下 OSharp快速开发框架</p>
<p><img src="https://img.shields.io/github/stars/i66soft/osharp.svg" alt="" loading="lazy"> <img src="https://img.shields.io/github/forks/i66soft/osharp.svg" alt="" loading="lazy"></p>
<p>再次感谢</p>
<h1 id="概述">概述</h1>
<p>前后端分离的系统中,前端和后端只有必要的数据通信交互,前端相当于一个完整的客户端应用程序,需要包含如下几个方面:</p>
<ul>
<li>各个模块的布局组合</li>
<li>各个页面的路由连接</li>
<li>业务功能的数据展现和操作流程体现</li>
<li>操作界面的菜单/按钮权限控制</li>
</ul>
<p>OSharp的Angular前端是基于 <strong>NG-ALAIN</strong> 框架的,这个框架基于阿里的 <strong>NG-ZORRO</strong> 封装了很多方便实用的组件,让我们很方便的实现自己需要的前端界面布局。</p>
<h2 id="前端业务模块代码布局">前端业务模块代码布局</h2>
<p>在Angular应用程序中,存在着模块<code>module</code>的组织形式,一个后端的模块正好可以对应着前端的一个<code>module</code>。</p>
<p>博客模块涉及的代码文件布局如下:</p>
<pre><code>src                                          源代码文件夹
└─app                                        APP文件夹
   └─routes                                  路由文件夹
       └─blogs                               博客模块文件夹
         ├─blogs.module.ts               博客模块文件
         ├─blogs.routing.ts                博客模块路由文件
         ├─blog                            博客组件文件夹
         │   ├─blog.component.html         博客组件模板文件
         │   └─blog.component.ts         博客组件文件
         └─post                            文章组件文件夹
               ├─post.component.html         文章组件模板文件
               └─post.component.ts         文章组件文件
</code></pre>
<h1 id="业务组件">业务组件</h1>
<p>组件<code>Component</code>是Angular应用程序的最小组织单元,是完成数据展现和业务操作的基本场所。</p>
<p>一个组件通常包含 <code>组件类</code> 和 <code>组件模板</code> 两个部分,如需要,还可包含 <code>组件样式</code>。</p>
<h2 id="stcomponentbase">STComponentBase</h2>
<p>为方便实现各个数据实体的通用管理列表,OSharp定义了一个通用列表组件基类 <code>STComponentBase</code>,基于这个基类,只需要传入几个关键的配置信息,即可很方便的实现一个后台管理的实体列表信息。<code>STComponentBase</code>主要特点如下:</p>
<ul>
<li>使用了 NG-ALAIN 的 <strong>STComponent</strong> 实现数据表格</li>
<li>使用 <strong>SFComponent + NzModalComponent</strong> 实现数据的 <code>添加/编辑</code> 操作</li>
<li>封装了一个通用的高级查询组件<code>AdSearchComponent</code>,可以很方便实现数据的多条件/条件组无级嵌套数据查询功能</li>
<li>对列表组件进行统一的界面布局,使各列表风格一致</li>
<li>提供了对列表数据的 <code>读取/添加/编辑/删除</code> 的默认实现</li>
<li>极易扩展其他表格功能</li>
</ul>
<p>STComponentBase 代码实现如下:</p>
<pre><code class="language-ts">export abstract class STComponentBase {
moduleName: string;

// URL
readUrl: string;
createUrl: string;
updateUrl: string;
deleteUrl: string;

// 表格属性
columns: STColumn[];
request: PageRequest;
req: STReq;
res: STRes;
page: STPage;
@ViewChild('st') st: STComponent;

// 编辑属性

schema: SFSchema;
ui: SFUISchema;
editRow: STData;
editTitle = '编辑';
@ViewChild('modal') editModal: NzModalComponent;

osharp: OsharpService;
alain: AlainService;
selecteds: STData[] = [];

public get http(): _HttpClient {
    return this.osharp.http;
}

constructor(injector: Injector) {
    this.osharp = injector.get(OsharpService);
    this.alain = injector.get(AlainService);
}

protected InitBase() {
    this.readUrl = `api/admin/${this.moduleName}/read`;
    this.createUrl = `api/admin/${this.moduleName}/create`;
    this.updateUrl = `api/admin/${this.moduleName}/update`;
    this.deleteUrl = `api/admin/${this.moduleName}/delete`;

    this.request = new PageRequest();
    this.columns = this.GetSTColumns();
    this.req = this.GetSTReq(this.request);
    this.res = this.GetSTRes();
    this.page = this.GetSTPage();

    this.schema = this.GetSFSchema();
    this.ui = this.GetSFUISchema();
}

// #region 表格

/**
   * 重写以获取表格的列设置Columns
   */
protected abstract GetSTColumns(): OsharpSTColumn[];

protected GetSTReq(request: PageRequest): STReq {
    let req: STReq = {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: request,
      allInBody: true,
      process: opt =&gt; this.RequestProcess(opt),
    };
    return req;
}

protected GetSTRes(): STRes {
    let res: STRes = {
      reName: { list: 'Rows', total: 'Total' },
      process: data =&gt; this.ResponseDataProcess(data),
    };
    return res;
}

protected GetSTPage(): STPage {
    let page: STPage = {
      showSize: true,
      showQuickJumper: true,
      toTop: true,
      toTopOffset: 0,
    };
    return page;
}

protected RequestProcess(opt: STRequestOptions): STRequestOptions {
    if (opt.body.PageCondition) {
      let page: PageCondition = opt.body.PageCondition;
      page.PageIndex = opt.body.pi;
      page.PageSize = opt.body.ps;
      if (opt.body.sort) {
      page.SortConditions = [];
      let sorts = opt.body.sort.split('-');
      for (const item of sorts) {
          let sort = new SortCondition();
          let num = item.lastIndexOf('.');
          let field = item.substr(0, num);
          field = this.ReplaceFieldName(field);
          sort.SortField = field;
          sort.ListSortDirection =
            item.substr(num + 1) === 'ascend'
            ? ListSortDirection.Ascending
            : ListSortDirection.Descending;
          page.SortConditions.push(sort);
      }
      } else {
      page.SortConditions = [];
      }
    }
    return opt;
}

protected ResponseDataProcess(data: STData[]): STData[] {
    return data;
}

protected ReplaceFieldName(field: string): string {
    return field;
}

search(request: PageRequest) {
    if (!request) {
      return;
    }
    this.req.body = request;
    this.st.reload();
}

change(value: STChange) {
    if (value.type === 'checkbox') {
      this.selecteds = value.checkbox;
    } else if (value.type === 'radio') {
      this.selecteds = ;
    }
}

error(value: STError) {
    console.log(value);
}

// #endregion

// #region 编辑

/**
   * 默认由列配置 `STColumn[]` 来生成SFSchema,不需要可以重写定义自己的SFSchema
   */
protected GetSFSchema(): SFSchema {
    let schema: SFSchema = { properties: this.ColumnsToSchemas(this.columns) };
    return schema;
}

protected ColumnsToSchemas(
    columns: OsharpSTColumn[],
): { : SFSchema } {
    let properties: { : SFSchema } = {};
    for (const column of columns) {
      if (!column.index || !column.editable || column.buttons) {
      continue;
      }
      let schema: SFSchema = this.alain.ToSFSchema(column);
      properties = schema;
    }
    return properties;
}

protected GetSFUISchema(): SFUISchema {
    let ui: SFUISchema = {};
    return ui;
}

protected toEnum(items: { id: number; text: string }[]): SFSchemaEnumType[] {
    return items.map(item =&gt; {
      let e: SFSchemaEnumType = { value: item.id, label: item.text };
      return e;
    });
}

create() {
    if (!this.editModal) return;
    this.schema = this.GetSFSchema();
    this.ui = this.GetSFUISchema();
    this.editRow = {};
    this.editTitle = '新增';
    this.editModal.open();
}

edit(row: STData) {
    if (!row || !this.editModal) {
      return;
    }
    this.schema = this.GetSFSchema();
    this.ui = this.GetSFUISchema();
    this.editRow = row;
    this.editTitle = '编辑';
    this.editModal.open();
}

close() {
    if (!this.editModal) return;
    console.log(this.editModal);
    this.editModal.destroy();
}

save(value: STData) {
    let url = value.Id ? this.updateUrl : this.createUrl;
    this.http.post&lt;AjaxResult&gt;(url, ).subscribe(result =&gt; {
      this.osharp.ajaxResult(result, () =&gt; {
      this.st.reload();
      this.editModal.destroy();
      });
    });
}

delete(value: STData) {
    if (!value) {
      return;
    }
    this.http.post&lt;AjaxResult&gt;(this.deleteUrl, ).subscribe(result =&gt; {
      this.osharp.ajaxResult(result, () =&gt; {
      this.st.reload();
      });
    });
}

// #endregion
}
</code></pre>
<p><code>STComponentBase</code> 基类的使用很简单,只需重写关键的 <code>GetSTColumns</code> 方法传入实体的列选项,即可完成一个管理列表的数据读取,查询,更新,删除等操作。</p>
<h2 id="博客模块的组件实现">博客模块的组件实现</h2>
<h3 id="博客-blog">博客-Blog</h3>
<ul>
<li>博客组件<code>blog.component.ts</code></li>
</ul>
<pre><code class="language-ts">import { Component, OnInit, Injector } from '@angular/core';
import { SFUISchema } from '@delon/form';
import { OsharpSTColumn } from '@shared/osharp/services/alain.types';
import { STComponentBase, } from '@shared/osharp/components/st-component-base';
import { STData } from '@delon/abc';
import { AjaxResult } from '@shared/osharp/osharp.model';

@Component({
selector: 'app-blog',
templateUrl: './blog.component.html',
styles: []
})
export class BlogComponent extends STComponentBase implements OnInit {

constructor(injector: Injector) {
    super(injector);
    this.moduleName = 'blog';
}

ngOnInit() {
    super.InitBase();
    this.createUrl = `api/admin/${this.moduleName}/apply`;
}

protected GetSTColumns(): OsharpSTColumn[] {
    let columns: OsharpSTColumn[] = [
      {
      title: '操作', fixed: 'left', width: 65, buttons: [{
          text: '操作', children: [
            { text: '审核', icon: 'flag', acl: 'Root.Admin.Blogs.Blog.Verify', iif: row =&gt; !row.IsEnabled, click: row =&gt; this.verify(row) },
            { text: '编辑', icon: 'edit', acl: 'Root.Admin.Blogs.Blog.Update', iif: row =&gt; row.Updatable, click: row =&gt; this.edit(row) },
          ]
      }]
      },
      { title: '编号', index: 'Id', sort: true, readOnly: true, editable: true, filterable: true, ftype: 'number' },
      { title: '博客地址', index: 'Url', sort: true, editable: true, filterable: true, ftype: 'string' },
      { title: '显示名称', index: 'Display', sort: true, editable: true, filterable: true, ftype: 'string' },
      { title: '已开通', index: 'IsEnabled', sort: true, filterable: true, type: 'yn' },
      { title: '作者编号', index: 'UserId', type: 'number' },
      { title: '创建时间', index: 'CreatedTime', sort: true, filterable: true, type: 'date' },
    ];
    return columns;
}

protected GetSFUISchema(): SFUISchema {
    let ui: SFUISchema = {
      '*': { spanLabelFixed: 100, grid: { span: 12 } },
      $Url: { grid: { span: 24 } },
      $Display: { grid: { span: 24 } },
    };
    return ui;
}

create() {
    if (!this.editModal) {
      return;
    }
    this.schema = this.GetSFSchema();
    this.ui = this.GetSFUISchema();
    this.editRow = {};
    this.editTitle = "申请博客";
    this.editModal.open();
}

save(value: STData) {
    // 申请博客
    if (!value.Id) {
      this.http.post&lt;AjaxResult&gt;(this.createUrl, value).subscribe(result =&gt; {
      this.osharp.ajaxResult(result, () =&gt; {
          this.st.reload();
          this.editModal.destroy();
      });
      });
      return;
    }
    // 审核博客
    if (value.Reason) {
      let url = 'api/admin/blog/verify';
      this.http.post&lt;AjaxResult&gt;(url, value).subscribe(result =&gt; {
      this.osharp.ajaxResult(result, () =&gt; {
          this.st.reload();
          this.editModal.destroy();
      });
      });
      return;
    }
    super.save(value);
}

verify(value: STData) {
    if (!value || !this.editModal) return;
    this.schema = {
      properties: {
      Id: { title: '编号', type: 'number', readOnly: true, default: value.Id },
      Name: { title: '博客名', type: 'string', readOnly: true, default: value.Display },
      IsEnabled: { title: '是否开通', type: 'boolean' },
      Reason: { title: '审核理由', type: 'string' }
      },
      required: ['Reason']
    };
    this.ui = {
      '*': { spanLabelFixed: 100, grid: { span: 12 } },
      $Id: { widget: 'text' },
      $Name: { widget: 'text', grid: { span: 24 } },
      $Reason: { widget: 'textarea', grid: { span: 24 } }
    };
    this.editRow = value;
    this.editTitle = "审核博客";
    this.editModal.open();
}
}
</code></pre>
<ul>
<li>博客组件模板<code>blog.component.html</code></li>
</ul>
<pre><code class="language-ts">&lt;nz-card&gt;
&lt;div&gt;
    &lt;button nz-button (click)="st.reload()"&gt;&lt;i nz-icon nzType="reload" nzTheme="outline"&gt;&lt;/i&gt;刷新&lt;/button&gt;
    &lt;button nz-button (click)="create()" acl="Root.Admin.Blogs.Blog.Apply" *ngIf="data.length == 0"&gt;&lt;i nz-icon type="plus-circle" theme="outline"&gt;&lt;/i&gt;申请&lt;/button&gt;
    &lt;osharp-ad-search ="request" ="columns" (submited)="search($event)"&gt;&lt;/osharp-ad-search&gt;
&lt;/div&gt;
&lt;st #st ="readUrl" ="columns" ="req" ="res" [(pi)]="request.PageCondition.PageIndex" [(ps)]="request.PageCondition.PageSize"="page" size="small" ="{x:'800px'}" multiSort
    (change)="change($event)" (error)="error($event)"&gt;&lt;/st&gt;
&lt;/nz-card&gt;

&lt;nz-modal #modal ="false" ="editTitle" ="false" ="null"&gt;
&lt;sf #sf mode="edit" ="schema" ="ui" ="editRow" button="none"&gt;
    &lt;div class="modal-footer"&gt;
      &lt;button nz-button type="button" (click)="close()"&gt;关闭&lt;/button&gt;
      &lt;button nz-button type="submit" ="'primary'" (click)="save(sf.value)" ="!sf.valid" ="http.loading" ="'Root.Admin.Blogs.Blog.Update'"&gt;保存&lt;/button&gt;
    &lt;/div&gt;
&lt;/sf&gt;
&lt;/nz-modal&gt;
</code></pre>
<h3 id="文章-post">文章-Post</h3>
<ul>
<li>文章组件<code>post.component.ts</code></li>
</ul>
<pre><code class="language-ts">import { Component, OnInit, Injector } from '@angular/core';
import { SFUISchema } from '@delon/form';
import { OsharpSTColumn } from '@shared/osharp/services/alain.types';
import { STComponentBase, } from '@shared/osharp/components/st-component-base';

@Component({
selector: 'app-post',
templateUrl: './post.component.html',
styles: []
})
export class PostComponent extends STComponentBase implements OnInit {

constructor(injector: Injector) {
    super(injector);
    this.moduleName = 'post';
}

ngOnInit() {
    super.InitBase();
}

protected GetSTColumns(): OsharpSTColumn[] {
    let columns: OsharpSTColumn[] = [
      {
      title: '操作', fixed: 'left', width: 65, buttons: [{
          text: '操作', children: [
            { text: '编辑', icon: 'edit', acl: 'Root.Admin.Blogs.Post.Update', iif: row =&gt; row.Updatable, click: row =&gt; this.edit(row) },
            { text: '删除', icon: 'delete', type: 'del', acl: 'Root.Admin.Blogs.Post.Delete', iif: row =&gt; row.Deletable, click: row =&gt; this.delete(row) },
          ]
      }]
      },
      { title: '编号', index: 'Id', sort: true, readOnly: true, editable: true, filterable: true, ftype: 'number' },
      { title: '文章标题', index: 'Title', sort: true, editable: true, filterable: true, ftype: 'string' },
      { title: '文章内容', index: 'Content', sort: true, editable: true, filterable: true, ftype: 'string' },
      { title: '博客编号', index: 'BlogId', readOnly: true, sort: true, filterable: true, type: 'number' },
      { title: '作者编号', index: 'UserId', readOnly: true, sort: true, filterable: true, type: 'number' },
      { title: '创建时间', index: 'CreatedTime', sort: true, filterable: true, type: 'date' },
    ];
    return columns;
}

protected GetSFUISchema(): SFUISchema {
    let ui: SFUISchema = {
      '*': { spanLabelFixed: 100, grid: { span: 12 } },
      $Title: { grid: { span: 24 } },
      $Content: { widget: 'textarea', grid: { span: 24 } }
    };
    return ui;
}
}
</code></pre>
<ul>
<li>文章组件模板<code>post.component.html</code></li>
</ul>
<pre><code class="language-ts">&lt;nz-card&gt;
&lt;div&gt;
    &lt;button nz-button (click)="st.reload()"&gt;&lt;i nz-icon nzType="reload" nzTheme="outline"&gt;&lt;/i&gt;刷新&lt;/button&gt;
    &lt;button nz-button (click)="create()" acl="Root.Admin.Blogs.Post.Create"&gt;&lt;i nz-icon type="plus-circle" theme="outline"&gt;&lt;/i&gt;新增&lt;/button&gt;
    &lt;osharp-ad-search ="request" ="columns" (submited)="search($event)"&gt;&lt;/osharp-ad-search&gt;
&lt;/div&gt;
&lt;st #st ="readUrl" ="columns" ="req" ="res" [(pi)]="request.PageCondition.PageIndex" [(ps)]="request.PageCondition.PageSize"="page" size="small"
    ="{x:'900px'}" multiSort (change)="change($event)" (error)="error($event)"&gt;&lt;/st&gt;
&lt;/nz-card&gt;

&lt;nz-modal #modal ="false" ="editTitle" ="false" ="null"&gt;
&lt;sf #sf mode="edit" ="schema" ="ui" ="editRow" button="none"&gt;
    &lt;div class="modal-footer"&gt;
      &lt;button nz-button type="button" (click)="close()"&gt;关闭&lt;/button&gt;
      &lt;button nz-button type="submit" ="'primary'" (click)="save(sf.value)" ="!sf.valid" ="http.loading" ="'Root.Admin.Blogs.Post.Update'"&gt;保存&lt;/button&gt;
    &lt;/div&gt;
&lt;/sf&gt;
&lt;/nz-modal&gt;
</code></pre>
<h1 id="模块路由-blogsroutingts">模块路由 blogs.routing.ts</h1>
<p>前端路由负责前端页面的连接导航,一个模块中的路由很简单,只要将组件导航起来即可。</p>
<pre><code class="language-ts">import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ACLGuard } from '@delon/acl';
import { BlogComponent } from './blog/blog.component';
import { PostComponent } from './post/post.component';

const routes: Routes = [
{ path: 'blog', component: BlogComponent, canActivate: , data: { title: '博客管理', reuse: true, guard: 'Root.Admin.Blogs.Blog.Read' } },
{ path: 'post', component: PostComponent, canActivate: , data: { title: '文章管理', reuse: true, guard: 'Root.Admin.Blogs.Post.Read' } },
];

@NgModule({
imports: ,
exports:
})
export class BlogsRoutingModule { }

</code></pre>
<p>此外,还需要在根路由配置 <strong>routes.routing.ts</strong> 上注册当前模块的路由,并使用延迟加载特性</p>
<pre><code class="language-ts">{ path: 'blogs', loadChildren: './blogs/blogs.module#BlogsModule', canActivateChild: , data: { guard: 'Root.Admin.Blogs' } },
</code></pre>
<h1 id="模块入口-blogsmodulets">模块入口 blogs.module.ts</h1>
<p>模块入口声明一个Angular模块,负责引入其他的公开模块,并声明自己的组件/服务</p>
<pre><code class="language-ts">import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared';
import { BlogsRoutingModule } from './blogs.routing';
import { BlogComponent } from './blog/blog.component';
import { PostComponent } from './post/post.component';

@NgModule({
imports: [
    CommonModule,
    SharedModule,
    BlogsRoutingModule
],
declarations: [
    BlogComponent,
    PostComponent,
]
})
export class BlogsModule { }
</code></pre>
<h1 id="菜单数据">菜单数据</h1>
<p>菜单数据指的是后台管理界面左侧导航菜单,在 <strong>assets/osharp/app-data.json</strong> 文件中进行配置。</p>
<pre><code class="language-json">{
"app": {
    "name": "OSharp Framework",
    "description": "一个开源的基于 .NETCORE 的快速开发框架"
},
"menu": [{
    "text": "导航菜单",
    "i18n": "menu.nav",
    "group": true,
    "hideInBreadcrumb": true,
    "children": [{
      "text": "主页",
      "i18n": "menu.nav.home",
      "icon": "anticon-dashboard",
      "link": "/dashboard",
      "acl": "Root.Admin.Dashboard"
    }]
}, {
    "text": "业务模块",
    "i18n": "menu.nav.business",
    "group": true,
    "hideInBreadcrumb": true,
    "children": [{
      "text": "博客模块",
      "group": "true",
      "icon": "anticon-border",
      "acl": "Root.Admin.Blogs",
      "children": [{
      "text": "博客管理",
      "link": "/blogs/blog",
      "acl": "Root.Admin.Blogs.Blog"
      }, {
      "text": "文章管理",
      "link": "/blogs/post",
      "acl": "Root.Admin.Blogs.Post"
      }]
    }]
}, {
    "text": "权限模块",
    // ...
}]
}   
</code></pre>
<h1 id="前端权限控制">前端权限控制</h1>
<p>OSharp的Angular前端项目的权限控制,是基于 NG-ALAIN 的 ACL 功能来实现的。ACL 全称叫访问控制列表(Access Control List),是一种非常简单的基于角色权限控制方式。</p>
<h2 id="前端权限控制流程">前端权限控制流程</h2>
<ul>
<li>代码实现时,基于ACL功能,给需要权限控制的节点配置需要的功能点字符串。配置原则为:执行当前功能主要需要涉及后端的哪个功能点,就在ACL设置哪个功能点的字符串</li>
<li>用户登录时,缓存用户的所有可用功能点集合</li>
<li>前端页面初始化或刷新时(前端路由跳转是无刷新的,只有主动F5或浏览器刷新时,才会刷新),从后端获取当前用户的可用功能点集合</li>
<li>将功能点集合缓存到 ACLService 中,作为ACL权限判断的数据源,然后一切权限判断的事就交给ACL了</li>
<li>ACL 根据 <strong>数据源中是否包含设置的ACL功能点</strong> 来决定是否显示/隐藏菜单项或按钮,从而达到前端权限控制的目的</li>
</ul>
<p>NG-ALAIN 的 ACL 模块的权限控制判断依赖可为 <strong>角色</strong> 或 <strong>功能点</strong>,默认的设置中,角色数据类型是字符串,功能点数据类型是数值。OSharp的功能点是形如 <code>Root.Admin.Blogs.Post</code> 的字符串形式,要应用上 ACL,需要进行如下配置:</p>
<p><strong>src/app/delon.module.ts</strong> 文件的 <strong>fnDelonACLConfig()</strong> 函数中进行配置</p>
<pre><code class="language-ts">export function fnDelonACLConfig(): DelonACLConfig {
return {
    guard_url: '/exception/403',
    preCan: (roleOrAbility: ACLCanType) =&gt; {
      function isAbility(val: string) {
      return val &amp;&amp; val.startsWith('Root.');
      }

      // 单个字符串,可能是角色也可能是功能点
      if (typeof roleOrAbility === 'string') {
      return isAbility(roleOrAbility) ? { ability: } : { role: };
      }
      // 字符串集合,每项可能是角色或是功能点,逐个处理每项
      if (Array.isArray(roleOrAbility) &amp;&amp; roleOrAbility.length &gt; 0 &amp;&amp; typeof roleOrAbility === 'string') {
      let abilities: string[] = [], roles: string[] = [];
      let type: ACLType = {};
      (roleOrAbility as string[]).forEach((val: string) =&gt; {
          if (isAbility(val)) abilities.push(val);
          else roles.push(val);
      });
      type.role = roles.length &gt; 0 ? roles : null;
      type.ability = abilities.length &gt; 0 ? abilities : null;
      return type;
      }
      return roleOrAbility;
    }
} as DelonACLConfig;
}
</code></pre>
<h2 id="组件权限控制">组件权限控制</h2>
<h3 id="组件中的权限控制">组件中的权限控制</h3>
<p>组件中的权限通常是按钮权限,例如:</p>
<ul>
<li>列表行操作按钮:<br>
通过 <code>acl</code> 控制功能权限,<code>iif</code> 控制数据权限,共同决定一个按钮是否可用。</li>
</ul>
<pre><code>{ text: '编辑', icon: 'edit', {==acl: 'Root.Admin.Blogs.Post.Update'==}, {==iif: row =&gt; row.Updatable==}, click: row =&gt; this.edit(row) },
</code></pre>
<h3 id="组件模板的权限控制">组件模板的权限控制</h3>
<p>组件模板中各个 html 元素,都可以进行权限控制:</p>
<ul>
<li>按钮权限:</li>
</ul>
<pre><code>&lt;button nz-button (click)="create()" {==acl="Root.Admin.Blogs.Post.Create"==}&gt;&lt;i nz-icon type="plus-circle" theme="outline"&gt;&lt;/i&gt;新增&lt;/button&gt;
</code></pre>
<h2 id="路由权限控制">路由权限控制</h2>
<p>路由的权限控制,通过 <strong>守卫路由</strong> 来实现,如果当前用户没有权限访问指定的路由链接,将会被拦截,未登录的用户将跳转到登录页,已登录的用户将跳转到 403 页面。</p>
<p>配置路由权限控制很简单,需要使用守卫路由 <code></code> ,然后在路由的 <code>data</code> 中配置 <code>guard</code> 为需要的功能点字符串:</p>
<pre><code>{ path: 'blog', component: BlogComponent, {==canActivate: ==}, data: { title: '博客管理', reuse: true, {==guard: 'Root.Admin.Blogs.Blog.Read'==} } },
</code></pre>
<h2 id="菜单权限控制">菜单权限控制</h2>
<p>菜单数据上也可以配置ACL权限控制,没权限的菜单不会显示</p>
<pre><code class="language-json">{
"text": "博客模块",
"group": "true",
"icon": "anticon-border",
"acl": "Root.Admin.Blogs",
"children": [{
    "text": "博客管理",
    "link": "/blogs/blog",
    "acl": "Root.Admin.Blogs.Blog"
}, {
    "text": "文章管理",
    "link": "/blogs/post",
    "acl": "Root.Admin.Blogs.Post"
}]
}
</code></pre>
<h1 id="权限控制效果演示">权限控制效果演示</h1>
<h2 id="博客信息">博客信息</h2>
<p>根据博客模块需求分析的设定,<strong>博客管理员</strong> 和 <strong>博主</strong> 两个角色对 <strong>博客</strong> 的权限分别如下:</p>
<table>
<thead>
<tr>
<th>--</th>
<th>博客管理员</th>
<th>博主</th>
</tr>
</thead>
<tbody>
<tr>
<td>查看</td>
<td>是</td>
<td>是</td>
</tr>
<tr>
<td>申请</td>
<td>否</td>
<td>是</td>
</tr>
<tr>
<td>审核</td>
<td>是</td>
<td>否</td>
</tr>
<tr>
<td>修改</td>
<td>是</td>
<td>是</td>
</tr>
</tbody>
</table>
<h3 id="博主-博客">博主-博客</h3>
<p>博主只能查看自己的博客数据,能申请博客,不能审核博客,申请成功之后,申请按钮隐藏。<br>
<img src="https://img2018.cnblogs.com/blog/87201/201905/87201-20190513203802916-297454218.gif" alt="" loading="lazy"></p>
<h3 id="博客管理员-博客">博客管理员-博客</h3>
<p>博客管理员不能申请博客,可以审核新增的博客,博客审核通过之后不能再次审核。</p>
<p><img src="https://img2018.cnblogs.com/blog/87201/201905/87201-20190513203827082-948442684.gif" alt="" loading="lazy"></p>
<h2 id="文章信息">文章信息</h2>
<p>根据博客模块需求分析的设定,<strong>博客管理员</strong> 和 <strong>博主</strong> 两个角色对 <strong>文章</strong> 的权限分别如下:</p>
<table>
<thead>
<tr>
<th>--</th>
<th>博客管理员</th>
<th>博主</th>
</tr>
</thead>
<tbody>
<tr>
<td>查看</td>
<td>是</td>
<td>是</td>
</tr>
<tr>
<td>新增</td>
<td>否</td>
<td>是</td>
</tr>
<tr>
<td>修改</td>
<td>是</td>
<td>是</td>
</tr>
<tr>
<td>删除</td>
<td>是</td>
<td>是</td>
</tr>
</tbody>
</table>
<h3 id="博主-文章">博主-文章</h3>
<p>博主能新增文章,只能查看、更新、删除自己的文章</p>
<p><img src="https://img2018.cnblogs.com/blog/87201/201905/87201-20190513203846217-1623321658.gif" alt="" loading="lazy"></p>
<h3 id="博客管理员-文章">博客管理员-文章</h3>
<p>博客管理员不能新增文章,能查看、更新、删除所有文章<br>
<img src="https://img2018.cnblogs.com/blog/87201/201905/87201-20190513203858890-672470871.gif" alt="" loading="lazy"></p>
<h1 id="步步为营教程总结">步步为营教程总结</h1>
<p>本系列教程为OSharp入门初级教程,通过一个 <strong>博客模块</strong> 实例来演示了使用OSharp框架进行业务开发所涉及到的项目分层,代码布局组织,业务代码实现规范,以及业务实现过程中常用的框架基础设施。让开发人员对使用OSharp框架进行项目开发的过程、使用难度等方面有一个初步的认识。</p>
<p>这只是一个简单的业务演示,限于篇幅,不可能对框架的技术细节进行很详细的讲解,后边,我们将会分Pack模块来对每个模块的设计思路,技术细节进行详细的解说。</p>


</div>
<div id="MySignature" role="contentinfo">
    <div id="MySignature_title">如果您看完本篇文章感觉不错,请点击一下右下角的<strong><span style="color: #ff0000; font-size: 18pt">【推荐】</span></strong>来支持一下博主,谢谢!</div>
<div id="MySignature_con">
<hr>
<p><strong>作者</strong>:郭明锋</p>
<p><strong>Q群</strong>:MVC EF技术交流(5008599)<img border="0" src="https://pub.idqqimg.com/wpa/images/group.png" alt="MVC EF 技术交流" title="MVC EF 技术交流"> OSharp开发框架交流(85895249)<img border="0" src="https://pub.idqqimg.com/wpa/images/group.png" alt="OSharp开发框架交流" title="OSharp开发框架交流"></p>
<p><strong>出处</strong>:https://www.cnblogs.com/guomingfeng</p>
<p><strong>声明</strong>:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。</p>
<hr>
</div><br><br>
来源:https://www.cnblogs.com/guomingfeng/p/osharpns-steps-angular.html
頁: [1]
查看完整版本: [开源]OSharpNS 步步为营系列 - 5. 添加前端Angular模块[完结]