[開源]OSharpNS 步步為營系列 - 5. 添加前端Angular模塊[完結]


什么是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

博客模塊涉及的代碼文件布局如下:

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           文章組件文件

業務組件

組件Component是Angular應用程序的最小組織單元,是完成數據展現和業務操作的基本場所。

一個組件通常包含 組件類組件模板 兩個部分,如需要,還可包含 組件樣式

STComponentBase

為方便實現各個數據實體的通用管理列表,OSharp定義了一個通用列表組件基類 STComponentBase,基於這個基類,只需要傳入幾個關鍵的配置信息,即可很方便的實現一個后台管理的實體列表信息。STComponentBase主要特點如下:

  • 使用了 NG-ALAIN 的 STComponent 實現數據表格
  • 使用 SFComponent + NzModalComponent 實現數據的 添加/編輯 操作
  • 封裝了一個通用的高級查詢組件AdSearchComponent,可以很方便實現數據的多條件/條件組無級嵌套數據查詢功能
  • 對列表組件進行統一的界面布局,使各列表風格一致
  • 提供了對列表數據的 讀取/添加/編輯/刪除 的默認實現
  • 極易擴展其他表格功能

STComponentBase 代碼實現如下:

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 => this.RequestProcess(opt),
    };
    return req;
  }

  protected GetSTRes(): STRes {
    let res: STRes = {
      reName: { list: 'Rows', total: 'Total' },
      process: data => 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 = [value.radio];
    }
  }

  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[],
  ): { [key: string]: SFSchema } {
    let properties: { [key: string]: SFSchema } = {};
    for (const column of columns) {
      if (!column.index || !column.editable || column.buttons) {
        continue;
      }
      let schema: SFSchema = this.alain.ToSFSchema(column);
      properties[column.index as string] = schema;
    }
    return properties;
  }

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

  protected toEnum(items: { id: number; text: string }[]): SFSchemaEnumType[] {
    return items.map(item => {
      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<AjaxResult>(url, [value]).subscribe(result => {
      this.osharp.ajaxResult(result, () => {
        this.st.reload();
        this.editModal.destroy();
      });
    });
  }

  delete(value: STData) {
    if (!value) {
      return;
    }
    this.http.post<AjaxResult>(this.deleteUrl, [value.Id]).subscribe(result => {
      this.osharp.ajaxResult(result, () => {
        this.st.reload();
      });
    });
  }

  // #endregion
}

STComponentBase 基類的使用很簡單,只需重寫關鍵的 GetSTColumns 方法傳入實體的列選項,即可完成一個管理列表的數據讀取,查詢,更新,刪除等操作。

博客模塊的組件實現

博客-Blog

  • 博客組件blog.component.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 => !row.IsEnabled, click: row => this.verify(row) },
            { text: '編輯', icon: 'edit', acl: 'Root.Admin.Blogs.Blog.Update', iif: row => row.Updatable, click: row => 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<AjaxResult>(this.createUrl, value).subscribe(result => {
        this.osharp.ajaxResult(result, () => {
          this.st.reload();
          this.editModal.destroy();
        });
      });
      return;
    }
    // 審核博客
    if (value.Reason) {
      let url = 'api/admin/blog/verify';
      this.http.post<AjaxResult>(url, value).subscribe(result => {
        this.osharp.ajaxResult(result, () => {
          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();
  }
}
  • 博客組件模板blog.component.html
<nz-card>
  <div>
    <button nz-button (click)="st.reload()"><i nz-icon nzType="reload" nzTheme="outline"></i>刷新</button>
    <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>
    <osharp-ad-search [request]="request" [columns]="columns" (submited)="search($event)"></osharp-ad-search>
  </div>
  <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
    (change)="change($event)" (error)="error($event)"></st>
</nz-card>

<nz-modal #modal [nzVisible]="false" [nzTitle]="editTitle" [nzClosable]="false" [nzFooter]="null">
  <sf #sf mode="edit" [schema]="schema" [ui]="ui" [formData]="editRow" button="none">
    <div class="modal-footer">
      <button nz-button type="button" (click)="close()">關閉</button>
      <button nz-button type="submit" [nzType]="'primary'" (click)="save(sf.value)" [disabled]="!sf.valid" [nzLoading]="http.loading" [acl]="'Root.Admin.Blogs.Blog.Update'">保存</button>
    </div>
  </sf>
</nz-modal>

文章-Post

  • 文章組件post.component.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 => row.Updatable, click: row => this.edit(row) },
            { text: '刪除', icon: 'delete', type: 'del', acl: 'Root.Admin.Blogs.Post.Delete', iif: row => row.Deletable, click: row => 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;
  }
}
  • 文章組件模板post.component.html
<nz-card>
  <div>
    <button nz-button (click)="st.reload()"><i nz-icon nzType="reload" nzTheme="outline"></i>刷新</button>
    <button nz-button (click)="create()" acl="Root.Admin.Blogs.Post.Create"><i nz-icon type="plus-circle" theme="outline"></i>新增</button>
    <osharp-ad-search [request]="request" [columns]="columns" (submited)="search($event)"></osharp-ad-search>
  </div>
  <st #st [data]="readUrl" [columns]="columns" [req]="req" [res]="res" [(pi)]="request.PageCondition.PageIndex" [(ps)]="request.PageCondition.PageSize" [page]="page" size="small"
    [scroll]="{x:'900px'}" multiSort (change)="change($event)" (error)="error($event)"></st>
</nz-card>

<nz-modal #modal [nzVisible]="false" [nzTitle]="editTitle" [nzClosable]="false" [nzFooter]="null">
  <sf #sf mode="edit" [schema]="schema" [ui]="ui" [formData]="editRow" button="none">
    <div class="modal-footer">
      <button nz-button type="button" (click)="close()">關閉</button>
      <button nz-button type="submit" [nzType]="'primary'" (click)="save(sf.value)" [disabled]="!sf.valid" [nzLoading]="http.loading" [acl]="'Root.Admin.Blogs.Post.Update'">保存</button>
    </div>
  </sf>
</nz-modal>

模塊路由 blogs.routing.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: [ACLGuard], data: { title: '博客管理', reuse: true, guard: 'Root.Admin.Blogs.Blog.Read' } },
  { path: 'post', component: PostComponent, canActivate: [ACLGuard], data: { title: '文章管理', reuse: true, guard: 'Root.Admin.Blogs.Post.Read' } },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class BlogsRoutingModule { }

此外,還需要在根路由配置 routes.routing.ts 上注冊當前模塊的路由,並使用延遲加載特性

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

模塊入口 blogs.module.ts

模塊入口聲明一個Angular模塊,負責引入其他的公開模塊,並聲明自己的組件/服務

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 { }

菜單數據

菜單數據指的是后台管理界面左側導航菜單,在 assets/osharp/app-data.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": "權限模塊",
    // ...
  }]
}    

前端權限控制

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() 函數中進行配置

export function fnDelonACLConfig(): DelonACLConfig {
  return {
    guard_url: '/exception/403',
    preCan: (roleOrAbility: ACLCanType) => {
      function isAbility(val: string) {
        return val && val.startsWith('Root.');
      }

      // 單個字符串,可能是角色也可能是功能點
      if (typeof roleOrAbility === 'string') {
        return isAbility(roleOrAbility) ? { ability: [roleOrAbility] } : { role: [roleOrAbility] };
      }
      // 字符串集合,每項可能是角色或是功能點,逐個處理每項
      if (Array.isArray(roleOrAbility) && roleOrAbility.length > 0 && typeof roleOrAbility[0] === 'string') {
        let abilities: string[] = [], roles: string[] = [];
        let type: ACLType = {};
        (roleOrAbility as string[]).forEach((val: string) => {
          if (isAbility(val)) abilities.push(val);
          else roles.push(val);
        });
        type.role = roles.length > 0 ? roles : null;
        type.ability = abilities.length > 0 ? abilities : null;
        return type;
      }
      return roleOrAbility;
    }
  } as DelonACLConfig;
}

組件權限控制

組件中的權限控制

組件中的權限通常是按鈕權限,例如:

  • 列表行操作按鈕:
    通過 acl 控制功能權限,iif 控制數據權限,共同決定一個按鈕是否可用。
{ text: '編輯', icon: 'edit', {==acl: 'Root.Admin.Blogs.Post.Update'==}, {==iif: row => row.Updatable==}, click: row => this.edit(row) },

組件模板的權限控制

組件模板中各個 html 元素,都可以進行權限控制:

  • 按鈕權限:
<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 為需要的功能點字符串:

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

菜單權限控制

菜單數據上也可以配置ACL權限控制,沒權限的菜單不會顯示

{
  "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"
  }]
}

權限控制效果演示

博客信息

根據博客模塊需求分析的設定,博客管理員博主 兩個角色對 博客 的權限分別如下:

-- 博客管理員 博主
查看
申請
審核
修改

博主-博客

博主只能查看自己的博客數據,能申請博客,不能審核博客,申請成功之后,申請按鈕隱藏。

博客管理員-博客

博客管理員不能申請博客,可以審核新增的博客,博客審核通過之后不能再次審核。

文章信息

根據博客模塊需求分析的設定,博客管理員博主 兩個角色對 文章 的權限分別如下:

-- 博客管理員 博主
查看
新增
修改
刪除

博主-文章

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

博客管理員-文章

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

步步為營教程總結

本系列教程為OSharp入門初級教程,通過一個 博客模塊 實例來演示了使用OSharp框架進行業務開發所涉及到的項目分層,代碼布局組織,業務代碼實現規范,以及業務實現過程中常用的框架基礎設施。讓開發人員對使用OSharp框架進行項目開發的過程、使用難度等方面有一個初步的認識。

這只是一個簡單的業務演示,限於篇幅,不可能對框架的技術細節進行很詳細的講解,后邊,我們將會分Pack模塊來對每個模塊的設計思路,技術細節進行詳細的解說。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM