什么是OSharp

OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架。这个框架使用最新稳定版的.NetCore SDK(当前是.NET Core 2.2),对 AspNetCore 的配置、依赖注入、日志、缓存、实体框架、Mvc(WebApi)、身份认证、权限授权等模块进行更高一级的自动化封装,并规范了一套业务实现的代码结构与操作流程,使 .Net Core 框架更易于应用到实际项目开发中。

感谢大家关注

首先特别感谢大家对OSharp快速开发框架的关注,这个系列每一篇都收到了比较多园友的关注,也在博客园首页开启了 霸屏模式

同时演示网站的用户注册数量也在持续上涨

项目 Star 数也增长了几百,欢迎没点 Star 的也来关注下 OSharp快速开发框架

再次感谢

概述

前后端分离的系统中,前端和后端只有必要的数据通信交互,前端相当于一个完整的客户端应用程序,需要包含如下几个方面:

  • 各个模块的布局组合
  • 各个页面的路由连接
  • 业务功能的数据展现和操作流程体现
  • 操作界面的菜单/按钮权限控制

OSharp的Angular前端是基于 NG-ALAIN 框架的,这个框架基于阿里的 NG-ZORRO 封装了很多方便实用的组件,让我们很方便的实现自己需要的前端界面布局。

前端业务模块代码布局

在Angular应用程序中,存在着模块module的组织形式,一个后端的模块正好可以对应着前端的一个module

博客模块涉及的代码文件布局如下:

  1. src 源代码文件夹
  2. └─app APP文件夹
  3. └─routes 路由文件夹
  4. └─blogs 博客模块文件夹
  5. ├─blogs.module.ts 博客模块文件
  6. ├─blogs.routing.ts 博客模块路由文件
  7. ├─blog 博客组件文件夹
  8. ├─blog.component.html 博客组件模板文件
  9. └─blog.component.ts 博客组件文件
  10. └─post 文章组件文件夹
  11. ├─post.component.html 文章组件模板文件
  12. └─post.component.ts 文章组件文件

业务组件

组件Component是Angular应用程序的最小组织单元,是完成数据展现和业务操作的基本场所。

一个组件通常包含 组件类组件模板 两个部分,如需要,还可包含 组件样式

STComponentBase

为方便实现各个数据实体的通用管理列表,OSharp定义了一个通用列表组件基类 STComponentBase,基于这个基类,只需要传入几个关键的配置信息,即可很方便的实现一个后台管理的实体列表信息。STComponentBase主要特点如下:

  • 使用了 NG-ALAIN 的 STComponent 实现数据表格
  • 使用 SFComponent + NzModalComponent 实现数据的 添加/编辑 操作
  • 封装了一个通用的高级查询组件AdSearchComponent,可以很方便实现数据的多条件/条件组无级嵌套数据查询功能
  • 对列表组件进行统一的界面布局,使各列表风格一致
  • 提供了对列表数据的 读取/添加/编辑/删除 的默认实现
  • 极易扩展其他表格功能

STComponentBase 代码实现如下:

  1. export abstract class STComponentBase {
  2. moduleName: string;
  3. // URL
  4. readUrl: string;
  5. createUrl: string;
  6. updateUrl: string;
  7. deleteUrl: string;
  8. // 表格属性
  9. columns: STColumn[];
  10. request: PageRequest;
  11. req: STReq;
  12. res: STRes;
  13. page: STPage;
  14. @ViewChild('st') st: STComponent;
  15. // 编辑属性
  16. schema: SFSchema;
  17. ui: SFUISchema;
  18. editRow: STData;
  19. editTitle = '编辑';
  20. @ViewChild('modal') editModal: NzModalComponent;
  21. osharp: OsharpService;
  22. alain: AlainService;
  23. selecteds: STData[] = [];
  24. public get http(): _HttpClient {
  25. return this.osharp.http;
  26. }
  27. constructor(injector: Injector) {
  28. this.osharp = injector.get(OsharpService);
  29. this.alain = injector.get(AlainService);
  30. }
  31. protected InitBase() {
  32. this.readUrl = `api/admin/${this.moduleName}/read`;
  33. this.createUrl = `api/admin/${this.moduleName}/create`;
  34. this.updateUrl = `api/admin/${this.moduleName}/update`;
  35. this.deleteUrl = `api/admin/${this.moduleName}/delete`;
  36. this.request = new PageRequest();
  37. this.columns = this.GetSTColumns();
  38. this.req = this.GetSTReq(this.request);
  39. this.res = this.GetSTRes();
  40. this.page = this.GetSTPage();
  41. this.schema = this.GetSFSchema();
  42. this.ui = this.GetSFUISchema();
  43. }
  44. // #region 表格
  45. /**
  46. * 重写以获取表格的列设置Columns
  47. */
  48. protected abstract GetSTColumns(): OsharpSTColumn[];
  49. protected GetSTReq(request: PageRequest): STReq {
  50. let req: STReq = {
  51. method: 'POST',
  52. headers: { 'Content-Type': 'application/json' },
  53. body: request,
  54. allInBody: true,
  55. process: opt => this.RequestProcess(opt),
  56. };
  57. return req;
  58. }
  59. protected GetSTRes(): STRes {
  60. let res: STRes = {
  61. reName: { list: 'Rows', total: 'Total' },
  62. process: data => this.ResponseDataProcess(data),
  63. };
  64. return res;
  65. }
  66. protected GetSTPage(): STPage {
  67. let page: STPage = {
  68. showSize: true,
  69. showQuickJumper: true,
  70. toTop: true,
  71. toTopOffset: 0,
  72. };
  73. return page;
  74. }
  75. protected RequestProcess(opt: STRequestOptions): STRequestOptions {
  76. if (opt.body.PageCondition) {
  77. let page: PageCondition = opt.body.PageCondition;
  78. page.PageIndex = opt.body.pi;
  79. page.PageSize = opt.body.ps;
  80. if (opt.body.sort) {
  81. page.SortConditions = [];
  82. let sorts = opt.body.sort.split('-');
  83. for (const item of sorts) {
  84. let sort = new SortCondition();
  85. let num = item.lastIndexOf('.');
  86. let field = item.substr(0, num);
  87. field = this.ReplaceFieldName(field);
  88. sort.SortField = field;
  89. sort.ListSortDirection =
  90. item.substr(num + 1) === 'ascend'
  91. ? ListSortDirection.Ascending
  92. : ListSortDirection.Descending;
  93. page.SortConditions.push(sort);
  94. }
  95. } else {
  96. page.SortConditions = [];
  97. }
  98. }
  99. return opt;
  100. }
  101. protected ResponseDataProcess(data: STData[]): STData[] {
  102. return data;
  103. }
  104. protected ReplaceFieldName(field: string): string {
  105. return field;
  106. }
  107. search(request: PageRequest) {
  108. if (!request) {
  109. return;
  110. }
  111. this.req.body = request;
  112. this.st.reload();
  113. }
  114. change(value: STChange) {
  115. if (value.type === 'checkbox') {
  116. this.selecteds = value.checkbox;
  117. } else if (value.type === 'radio') {
  118. this.selecteds = [value.radio];
  119. }
  120. }
  121. error(value: STError) {
  122. console.log(value);
  123. }
  124. // #endregion
  125. // #region 编辑
  126. /**
  127. * 默认由列配置 `STColumn[]` 来生成SFSchema,不需要可以重写定义自己的SFSchema
  128. */
  129. protected GetSFSchema(): SFSchema {
  130. let schema: SFSchema = { properties: this.ColumnsToSchemas(this.columns) };
  131. return schema;
  132. }
  133. protected ColumnsToSchemas(
  134. columns: OsharpSTColumn[],
  135. ): { [key: string]: SFSchema } {
  136. let properties: { [key: string]: SFSchema } = {};
  137. for (const column of columns) {
  138. if (!column.index || !column.editable || column.buttons) {
  139. continue;
  140. }
  141. let schema: SFSchema = this.alain.ToSFSchema(column);
  142. properties[column.index as string] = schema;
  143. }
  144. return properties;
  145. }
  146. protected GetSFUISchema(): SFUISchema {
  147. let ui: SFUISchema = {};
  148. return ui;
  149. }
  150. protected toEnum(items: { id: number; text: string }[]): SFSchemaEnumType[] {
  151. return items.map(item => {
  152. let e: SFSchemaEnumType = { value: item.id, label: item.text };
  153. return e;
  154. });
  155. }
  156. create() {
  157. if (!this.editModal) return;
  158. this.schema = this.GetSFSchema();
  159. this.ui = this.GetSFUISchema();
  160. this.editRow = {};
  161. this.editTitle = '新增';
  162. this.editModal.open();
  163. }
  164. edit(row: STData) {
  165. if (!row || !this.editModal) {
  166. return;
  167. }
  168. this.schema = this.GetSFSchema();
  169. this.ui = this.GetSFUISchema();
  170. this.editRow = row;
  171. this.editTitle = '编辑';
  172. this.editModal.open();
  173. }
  174. close() {
  175. if (!this.editModal) return;
  176. console.log(this.editModal);
  177. this.editModal.destroy();
  178. }
  179. save(value: STData) {
  180. let url = value.Id ? this.updateUrl : this.createUrl;
  181. this.http.post<AjaxResult>(url, [value]).subscribe(result => {
  182. this.osharp.ajaxResult(result, () => {
  183. this.st.reload();
  184. this.editModal.destroy();
  185. });
  186. });
  187. }
  188. delete(value: STData) {
  189. if (!value) {
  190. return;
  191. }
  192. this.http.post<AjaxResult>(this.deleteUrl, [value.Id]).subscribe(result => {
  193. this.osharp.ajaxResult(result, () => {
  194. this.st.reload();
  195. });
  196. });
  197. }
  198. // #endregion
  199. }

STComponentBase 基类的使用很简单,只需重写关键的 GetSTColumns 方法传入实体的列选项,即可完成一个管理列表的数据读取,查询,更新,删除等操作。

博客模块的组件实现

博客-Blog

  • 博客组件blog.component.ts
  1. import { Component, OnInit, Injector } from '@angular/core';
  2. import { SFUISchema } from '@delon/form';
  3. import { OsharpSTColumn } from '@shared/osharp/services/alain.types';
  4. import { STComponentBase, } from '@shared/osharp/components/st-component-base';
  5. import { STData } from '@delon/abc';
  6. import { AjaxResult } from '@shared/osharp/osharp.model';
  7. @Component({
  8. selector: 'app-blog',
  9. templateUrl: './blog.component.html',
  10. styles: []
  11. })
  12. export class BlogComponent extends STComponentBase implements OnInit {
  13. constructor(injector: Injector) {
  14. super(injector);
  15. this.moduleName = 'blog';
  16. }
  17. ngOnInit() {
  18. super.InitBase();
  19. this.createUrl = `api/admin/${this.moduleName}/apply`;
  20. }
  21. protected GetSTColumns(): OsharpSTColumn[] {
  22. let columns: OsharpSTColumn[] = [
  23. {
  24. title: '操作', fixed: 'left', width: 65, buttons: [{
  25. text: '操作', children: [
  26. { text: '审核', icon: 'flag', acl: 'Root.Admin.Blogs.Blog.Verify', iif: row => !row.IsEnabled, click: row => this.verify(row) },
  27. { text: '编辑', icon: 'edit', acl: 'Root.Admin.Blogs.Blog.Update', iif: row => row.Updatable, click: row => this.edit(row) },
  28. ]
  29. }]
  30. },
  31. { title: '编号', index: 'Id', sort: true, readOnly: true, editable: true, filterable: true, ftype: 'number' },
  32. { title: '博客地址', index: 'Url', sort: true, editable: true, filterable: true, ftype: 'string' },
  33. { title: '显示名称', index: 'Display', sort: true, editable: true, filterable: true, ftype: 'string' },
  34. { title: '已开通', index: 'IsEnabled', sort: true, filterable: true, type: 'yn' },
  35. { title: '作者编号', index: 'UserId', type: 'number' },
  36. { title: '创建时间', index: 'CreatedTime', sort: true, filterable: true, type: 'date' },
  37. ];
  38. return columns;
  39. }
  40. protected GetSFUISchema(): SFUISchema {
  41. let ui: SFUISchema = {
  42. '*': { spanLabelFixed: 100, grid: { span: 12 } },
  43. $Url: { grid: { span: 24 } },
  44. $Display: { grid: { span: 24 } },
  45. };
  46. return ui;
  47. }
  48. create() {
  49. if (!this.editModal) {
  50. return;
  51. }
  52. this.schema = this.GetSFSchema();
  53. this.ui = this.GetSFUISchema();
  54. this.editRow = {};
  55. this.editTitle = "申请博客";
  56. this.editModal.open();
  57. }
  58. save(value: STData) {
  59. // 申请博客
  60. if (!value.Id) {
  61. this.http.post<AjaxResult>(this.createUrl, value).subscribe(result => {
  62. this.osharp.ajaxResult(result, () => {
  63. this.st.reload();
  64. this.editModal.destroy();
  65. });
  66. });
  67. return;
  68. }
  69. // 审核博客
  70. if (value.Reason) {
  71. let url = 'api/admin/blog/verify';
  72. this.http.post<AjaxResult>(url, value).subscribe(result => {
  73. this.osharp.ajaxResult(result, () => {
  74. this.st.reload();
  75. this.editModal.destroy();
  76. });
  77. });
  78. return;
  79. }
  80. super.save(value);
  81. }
  82. verify(value: STData) {
  83. if (!value || !this.editModal) return;
  84. this.schema = {
  85. properties: {
  86. Id: { title: '编号', type: 'number', readOnly: true, default: value.Id },
  87. Name: { title: '博客名', type: 'string', readOnly: true, default: value.Display },
  88. IsEnabled: { title: '是否开通', type: 'boolean' },
  89. Reason: { title: '审核理由', type: 'string' }
  90. },
  91. required: ['Reason']
  92. };
  93. this.ui = {
  94. '*': { spanLabelFixed: 100, grid: { span: 12 } },
  95. $Id: { widget: 'text' },
  96. $Name: { widget: 'text', grid: { span: 24 } },
  97. $Reason: { widget: 'textarea', grid: { span: 24 } }
  98. };
  99. this.editRow = value;
  100. this.editTitle = "审核博客";
  101. this.editModal.open();
  102. }
  103. }
  • 博客组件模板blog.component.html
  1. <nz-card>
  2. <div>
  3. <button nz-button (click)="st.reload()"><i nz-icon nzType="reload" nzTheme="outline"></i>刷新</button>
  4. <button nz-button (click)="create()" acl="Root.Admin.Blogs.Blog.Apply" *ngIf="data.length == 0"><i nz-icon type="plus-circle" theme="outline"></i>申请</button>
  5. <osharp-ad-search [request]="request" [columns]="columns" (submited)="search($event)"></osharp-ad-search>
  6. </div>
  7. <st #st [data]="readUrl" [columns]="columns" [req]="req" [res]="res" [(pi)]="request.PageCondition.PageIndex" [(ps)]="request.PageCondition.PageSize" [page]="page" size="small" [scroll]="{x:'800px'}" multiSort
  8. (change)="change($event)" (error)="error($event)"></st>
  9. </nz-card>
  10. <nz-modal #modal [nzVisible]="false" [nzTitle]="editTitle" [nzClosable]="false" [nzFooter]="null">
  11. <sf #sf mode="edit" [schema]="schema" [ui]="ui" [formData]="editRow" button="none">
  12. <div class="modal-footer">
  13. <button nz-button type="button" (click)="close()">关闭</button>
  14. <button nz-button type="submit" [nzType]="'primary'" (click)="save(sf.value)" [disabled]="!sf.valid" [nzLoading]="http.loading" [acl]="'Root.Admin.Blogs.Blog.Update'">保存</button>
  15. </div>
  16. </sf>
  17. </nz-modal>

文章-Post

  • 文章组件post.component.ts
  1. import { Component, OnInit, Injector } from '@angular/core';
  2. import { SFUISchema } from '@delon/form';
  3. import { OsharpSTColumn } from '@shared/osharp/services/alain.types';
  4. import { STComponentBase, } from '@shared/osharp/components/st-component-base';
  5. @Component({
  6. selector: 'app-post',
  7. templateUrl: './post.component.html',
  8. styles: []
  9. })
  10. export class PostComponent extends STComponentBase implements OnInit {
  11. constructor(injector: Injector) {
  12. super(injector);
  13. this.moduleName = 'post';
  14. }
  15. ngOnInit() {
  16. super.InitBase();
  17. }
  18. protected GetSTColumns(): OsharpSTColumn[] {
  19. let columns: OsharpSTColumn[] = [
  20. {
  21. title: '操作', fixed: 'left', width: 65, buttons: [{
  22. text: '操作', children: [
  23. { text: '编辑', icon: 'edit', acl: 'Root.Admin.Blogs.Post.Update', iif: row => row.Updatable, click: row => this.edit(row) },
  24. { text: '删除', icon: 'delete', type: 'del', acl: 'Root.Admin.Blogs.Post.Delete', iif: row => row.Deletable, click: row => this.delete(row) },
  25. ]
  26. }]
  27. },
  28. { title: '编号', index: 'Id', sort: true, readOnly: true, editable: true, filterable: true, ftype: 'number' },
  29. { title: '文章标题', index: 'Title', sort: true, editable: true, filterable: true, ftype: 'string' },
  30. { title: '文章内容', index: 'Content', sort: true, editable: true, filterable: true, ftype: 'string' },
  31. { title: '博客编号', index: 'BlogId', readOnly: true, sort: true, filterable: true, type: 'number' },
  32. { title: '作者编号', index: 'UserId', readOnly: true, sort: true, filterable: true, type: 'number' },
  33. { title: '创建时间', index: 'CreatedTime', sort: true, filterable: true, type: 'date' },
  34. ];
  35. return columns;
  36. }
  37. protected GetSFUISchema(): SFUISchema {
  38. let ui: SFUISchema = {
  39. '*': { spanLabelFixed: 100, grid: { span: 12 } },
  40. $Title: { grid: { span: 24 } },
  41. $Content: { widget: 'textarea', grid: { span: 24 } }
  42. };
  43. return ui;
  44. }
  45. }
  • 文章组件模板post.component.html
  1. <nz-card>
  2. <div>
  3. <button nz-button (click)="st.reload()"><i nz-icon nzType="reload" nzTheme="outline"></i>刷新</button>
  4. <button nz-button (click)="create()" acl="Root.Admin.Blogs.Post.Create"><i nz-icon type="plus-circle" theme="outline"></i>新增</button>
  5. <osharp-ad-search [request]="request" [columns]="columns" (submited)="search($event)"></osharp-ad-search>
  6. </div>
  7. <st #st [data]="readUrl" [columns]="columns" [req]="req" [res]="res" [(pi)]="request.PageCondition.PageIndex" [(ps)]="request.PageCondition.PageSize" [page]="page" size="small"
  8. [scroll]="{x:'900px'}" multiSort (change)="change($event)" (error)="error($event)"></st>
  9. </nz-card>
  10. <nz-modal #modal [nzVisible]="false" [nzTitle]="editTitle" [nzClosable]="false" [nzFooter]="null">
  11. <sf #sf mode="edit" [schema]="schema" [ui]="ui" [formData]="editRow" button="none">
  12. <div class="modal-footer">
  13. <button nz-button type="button" (click)="close()">关闭</button>
  14. <button nz-button type="submit" [nzType]="'primary'" (click)="save(sf.value)" [disabled]="!sf.valid" [nzLoading]="http.loading" [acl]="'Root.Admin.Blogs.Post.Update'">保存</button>
  15. </div>
  16. </sf>
  17. </nz-modal>

模块路由 blogs.routing.ts

前端路由负责前端页面的连接导航,一个模块中的路由很简单,只要将组件导航起来即可。

  1. import { NgModule } from '@angular/core';
  2. import { Routes, RouterModule } from '@angular/router';
  3. import { ACLGuard } from '@delon/acl';
  4. import { BlogComponent } from './blog/blog.component';
  5. import { PostComponent } from './post/post.component';
  6. const routes: Routes = [
  7. { path: 'blog', component: BlogComponent, canActivate: [ACLGuard], data: { title: '博客管理', reuse: true, guard: 'Root.Admin.Blogs.Blog.Read' } },
  8. { path: 'post', component: PostComponent, canActivate: [ACLGuard], data: { title: '文章管理', reuse: true, guard: 'Root.Admin.Blogs.Post.Read' } },
  9. ];
  10. @NgModule({
  11. imports: [RouterModule.forChild(routes)],
  12. exports: [RouterModule]
  13. })
  14. export class BlogsRoutingModule { }

此外,还需要在根路由配置 routes.routing.ts 上注册当前模块的路由,并使用延迟加载特性

  1. { path: 'blogs', loadChildren: './blogs/blogs.module#BlogsModule', canActivateChild: [ACLGuard], data: { guard: 'Root.Admin.Blogs' } },

模块入口 blogs.module.ts

模块入口声明一个Angular模块,负责引入其他的公开模块,并声明自己的组件/服务

  1. import { NgModule } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { SharedModule } from '@shared';
  4. import { BlogsRoutingModule } from './blogs.routing';
  5. import { BlogComponent } from './blog/blog.component';
  6. import { PostComponent } from './post/post.component';
  7. @NgModule({
  8. imports: [
  9. CommonModule,
  10. SharedModule,
  11. BlogsRoutingModule
  12. ],
  13. declarations: [
  14. BlogComponent,
  15. PostComponent,
  16. ]
  17. })
  18. export class BlogsModule { }

菜单数据

菜单数据指的是后台管理界面左侧导航菜单,在 assets/osharp/app-data.json 文件中进行配置。

  1. {
  2. "app": {
  3. "name": "OSharp Framework",
  4. "description": "一个开源的基于 .NETCORE 的快速开发框架"
  5. },
  6. "menu": [{
  7. "text": "导航菜单",
  8. "i18n": "menu.nav",
  9. "group": true,
  10. "hideInBreadcrumb": true,
  11. "children": [{
  12. "text": "主页",
  13. "i18n": "menu.nav.home",
  14. "icon": "anticon-dashboard",
  15. "link": "/dashboard",
  16. "acl": "Root.Admin.Dashboard"
  17. }]
  18. }, {
  19. "text": "业务模块",
  20. "i18n": "menu.nav.business",
  21. "group": true,
  22. "hideInBreadcrumb": true,
  23. "children": [{
  24. "text": "博客模块",
  25. "group": "true",
  26. "icon": "anticon-border",
  27. "acl": "Root.Admin.Blogs",
  28. "children": [{
  29. "text": "博客管理",
  30. "link": "/blogs/blog",
  31. "acl": "Root.Admin.Blogs.Blog"
  32. }, {
  33. "text": "文章管理",
  34. "link": "/blogs/post",
  35. "acl": "Root.Admin.Blogs.Post"
  36. }]
  37. }]
  38. }, {
  39. "text": "权限模块",
  40. // ...
  41. }]
  42. }

前端权限控制

OSharp的Angular前端项目的权限控制,是基于 NG-ALAIN 的 ACL 功能来实现的。ACL 全称叫访问控制列表(Access Control List),是一种非常简单的基于角色权限控制方式。

前端权限控制流程

  • 代码实现时,基于ACL功能,给需要权限控制的节点配置需要的功能点字符串。配置原则为:执行当前功能主要需要涉及后端的哪个功能点,就在ACL设置哪个功能点的字符串
  • 用户登录时,缓存用户的所有可用功能点集合
  • 前端页面初始化或刷新时(前端路由跳转是无刷新的,只有主动F5或浏览器刷新时,才会刷新),从后端获取当前用户的可用功能点集合
  • 将功能点集合缓存到 ACLService 中,作为ACL权限判断的数据源,然后一切权限判断的事就交给ACL了
  • ACL 根据 数据源中是否包含设置的ACL功能点 来决定是否显示/隐藏菜单项或按钮,从而达到前端权限控制的目的

NG-ALAIN 的 ACL 模块的权限控制判断依赖可为 角色功能点,默认的设置中,角色数据类型是字符串,功能点数据类型是数值。OSharp的功能点是形如 Root.Admin.Blogs.Post 的字符串形式,要应用上 ACL,需要进行如下配置:

src/app/delon.module.ts 文件的 fnDelonACLConfig() 函数中进行配置

  1. export function fnDelonACLConfig(): DelonACLConfig {
  2. return {
  3. guard_url: '/exception/403',
  4. preCan: (roleOrAbility: ACLCanType) => {
  5. function isAbility(val: string) {
  6. return val && val.startsWith('Root.');
  7. }
  8. // 单个字符串,可能是角色也可能是功能点
  9. if (typeof roleOrAbility === 'string') {
  10. return isAbility(roleOrAbility) ? { ability: [roleOrAbility] } : { role: [roleOrAbility] };
  11. }
  12. // 字符串集合,每项可能是角色或是功能点,逐个处理每项
  13. if (Array.isArray(roleOrAbility) && roleOrAbility.length > 0 && typeof roleOrAbility[0] === 'string') {
  14. let abilities: string[] = [], roles: string[] = [];
  15. let type: ACLType = {};
  16. (roleOrAbility as string[]).forEach((val: string) => {
  17. if (isAbility(val)) abilities.push(val);
  18. else roles.push(val);
  19. });
  20. type.role = roles.length > 0 ? roles : null;
  21. type.ability = abilities.length > 0 ? abilities : null;
  22. return type;
  23. }
  24. return roleOrAbility;
  25. }
  26. } as DelonACLConfig;
  27. }

组件权限控制

组件中的权限控制

组件中的权限通常是按钮权限,例如:

  • 列表行操作按钮:

    通过 acl 控制功能权限,iif 控制数据权限,共同决定一个按钮是否可用。
  1. { text: '编辑', icon: 'edit', {==acl: 'Root.Admin.Blogs.Post.Update'==}, {==iif: row => row.Updatable==}, click: row => this.edit(row) },

组件模板的权限控制

组件模板中各个 html 元素,都可以进行权限控制:

  • 按钮权限:
  1. <button nz-button (click)="create()" {==acl="Root.Admin.Blogs.Post.Create"==}><i nz-icon type="plus-circle" theme="outline"></i>新增</button>

路由权限控制

路由的权限控制,通过 守卫路由 来实现,如果当前用户没有权限访问指定的路由链接,将会被拦截,未登录的用户将跳转到登录页,已登录的用户将跳转到 403 页面。

配置路由权限控制很简单,需要使用守卫路由 [ACLGuard] ,然后在路由的 data 中配置 guard 为需要的功能点字符串:

  1. { path: 'blog', component: BlogComponent, {==canActivate: [ACLGuard]==}, data: { title: '博客管理', reuse: true, {==guard: 'Root.Admin.Blogs.Blog.Read'==} } },

菜单权限控制

菜单数据上也可以配置ACL权限控制,没权限的菜单不会显示

  1. {
  2. "text": "博客模块",
  3. "group": "true",
  4. "icon": "anticon-border",
  5. "acl": "Root.Admin.Blogs",
  6. "children": [{
  7. "text": "博客管理",
  8. "link": "/blogs/blog",
  9. "acl": "Root.Admin.Blogs.Blog"
  10. }, {
  11. "text": "文章管理",
  12. "link": "/blogs/post",
  13. "acl": "Root.Admin.Blogs.Post"
  14. }]
  15. }

权限控制效果演示

博客信息

根据博客模块需求分析的设定,博客管理员博主 两个角色对 博客 的权限分别如下:

-- 博客管理员 博主
查看
申请
审核
修改

博主-博客

博主只能查看自己的博客数据,能申请博客,不能审核博客,申请成功之后,申请按钮隐藏。

博客管理员-博客

博客管理员不能申请博客,可以审核新增的博客,博客审核通过之后不能再次审核。

文章信息

根据博客模块需求分析的设定,博客管理员博主 两个角色对 文章 的权限分别如下:

-- 博客管理员 博主
查看
新增
修改
删除

博主-文章

博主能新增文章,只能查看、更新、删除自己的文章

博客管理员-文章

博客管理员不能新增文章,能查看、更新、删除所有文章

步步为营教程总结

本系列教程为OSharp入门初级教程,通过一个 博客模块 实例来演示了使用OSharp框架进行业务开发所涉及到的项目分层,代码布局组织,业务代码实现规范,以及业务实现过程中常用的框架基础设施。让开发人员对使用OSharp框架进行项目开发的过程、使用难度等方面有一个初步的认识。

这只是一个简单的业务演示,限于篇幅,不可能对框架的技术细节进行很详细的讲解,后边,我们将会分Pack模块来对每个模块的设计思路,技术细节进行详细的解说。

[开源]OSharpNS 步步为营系列 - 5. 添加前端Angular模块[完结]的更多相关文章

  1. [开源]OSharpNS 步步为营系列 - 4. 添加业务对外API

    什么是OSharp OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架.这个 ...

  2. [开源]OSharpNS 步步为营系列 - 2. 添加业务数据层

    什么是OSharp OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架.这个 ...

  3. [开源]OSharpNS 步步为营系列 - 3. 添加业务服务层

    什么是OSharp OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架.这个 ...

  4. [开源]OSharpNS 步步为营系列 - 1. 业务模块设计

    什么是OSharp OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架.这个 ...

  5. [开源]OSharpNS - .net core 快速开发框架 - 快速开始

    什么是OSharp OSharpNS全称OSharp Framework with .NetStandard2.0,是一个基于.NetStandard2.0开发的一个.NetCore快速开发框架.这个 ...

  6. 【2015上半年总结】js开源组件开发系列索引

    js开源组件开发系列一索引 2015.8 by 田想兵 个人网站 从3月份进入新公司以来,时经五个月,我以平均每周1个小组件的速度,已经完成的js组件有22个之余了,已基本上全部用到实际项目中,这些小 ...

  7. abp.zero 9.0框架的前端Angular使用说明

    abp.zero 9.0框架的前端Angular使用说明 目录 abp.zero 9.0框架的前端Angular使用说明 摘要 1 部署及启动 1.1 依赖包安装 1.2 使用yarn安装依赖包 1. ...

  8. Quartz.NET开源作业调度框架系列

    Quartz.NET是一个被广泛使用的开源作业调度框架 , 由于是用C#语言创建,可方便的用于winform和asp.net应用程序中.Quartz.NET提供了巨大的灵活性但又兼具简单性.开发人员可 ...

  9. 在ASP.NET 5中如何方便的添加前端库

    (此文章同时发表在本人微信公众号“dotNET每日精华文章”,欢迎右边二维码来关注.) 题记:ASP.NET 5和之前的ASP.NET版本有很大的不同,其中之一就是对前端库的管理不再使用Nuget,而 ...

随机推荐

  1. Spark- Transformation实战

    RDD的算子分为两类,是 Trans formation(Lazy),一类是 Action(触发任务执行RDD不存在真正要计算的数据,而是记录了RDD的转换关系(调用了什么方法,传入什么函数) RDD ...

  2. php设计模式课程---6、策略模式如何使用

    php设计模式课程---6.策略模式如何使用 一.总结 一句话总结:比如代码需求,做一饭店,有南北方不同菜系,不同分店有不同的饭菜汤的需求,代码怎么设计 从饭店有特色过渡到厨师有特色(南方厨师(南方饭 ...

  3. SQLite connection strings

    Basic Data Source=c:\mydb.db;Version=3; Version 2 is not supported by this class library. SQLite In- ...

  4. C 字节对齐.我的算法学习之路

    C/C++基础笔试题1.0(字节对齐) http://blog.csdn.net/dengyaolongacmblog/article/details/37559687 我的算法学习之路 http:/ ...

  5. hdu--2111--Saving HDU(贪心)

    #include<iostream> #include<vector> #include<algorithm> using namespace std; struc ...

  6. 实现两个窗口通信方法-postMessage

    此方案可解决跨域而且跨Iframe,而且http和https之间的交互 首先来看一下基本的语法 otherWindow.postMessage(message, targetOrigin, [tran ...

  7. mysql调优参考笔记

    之前一位童鞋发的: 5版邮件,在用户量很大的情况下,如果做了分布式,如果在后端mysql上执行:   mysql> show global status like 'Thread%';   Th ...

  8. bzoj 2300: [HAOI2011]防线修建 凸包

    题目大意: http://www.lydsy.com/JudgeOnline/problem.php?id=2300 题解 这道题让我们维护一个支持动态删除点的上凸壳 并且告诉了我们三个一定不会被删除 ...

  9. Popular Cows

    传送门(poj):http://poj.org/problem?id=2186 (bzoj):http://www.lydsy.com/JudgeOnline/problem.php?id=1051 ...

  10. S3C2410中文芯片手册-11.串口

    目录 11 UART Overview Featrues UART Operation Data Transmission Data Reception Auto Flow Control(AFC) ...