这是整个系列的综合教程:只给一个实体类,走完全部 11 步,得到一个上线可用的后台模块。 每一步都给出可执行的代码和判断依据,哪一步不需要就跳过。
总览:11 步清单
text
1 写实体
2 生成 / 写页面
3 建菜单 + 按钮
4 角色授权
5 数据权限(需要行级隔离时)
6 高级查询与关联(需要时)
7 表单增强:文件 / 富文本 / 字典(需要时)
8 Excel 导入导出(需要时)
9 审批(需要时)
10 日志与审计
11 部署上线
演示项目里的 6 个模块就是按这个流程做的,合计 15 个文件 / 707 行(第 30 篇有统计口径)。
第 1 步:写实体
csharp
using FreeSql.DataAnnotations;
using System.ComponentModel;
namespace EasyAdminBlazor.Test.Products;
/// <summary>产品</summary>
[Table(Name = "product")]
public partial class Product : EntityFull
{
[DisplayName("产品名称")]
[Column(StringLength = 100)]
public string Title { get; set; } = string.Empty;
[DisplayName("产品图片")]
[Column(StringLength = 400)]
public string Image { get; set; } = string.Empty;
[DisplayName("价格")]
[Column(Precision = 10, Scale = 2)]
public decimal Price { get; set; }
[DisplayName("库存")]
public int Stock { get; set; }
[DisplayName("摘要")]
[Column(StringLength = 500)]
public string Excerpt { get; set; } = string.Empty;
[DisplayName("产品详情")]
[Column(StringLength = -2)]
public string Content { get; set; } = string.Empty;
[DisplayName("是否上架")]
public bool IsOnSale { get; set; } = true;
}
基类怎么选:
| 基类 | 带什么 | 什么时候用 |
|---|---|---|
Entity |
只有主键 | 纯关联表、字典表 |
EntityCreated |
主键 + 创建人/时间 | 只增不改的日志类数据 |
EntityFull |
主键 + 创建/修改人时间 + 软删除 | 业务实体的默认选择 |
ApprovalEntityFull |
EntityFull + 审批字段 |
需要审批的单据(第 9 步) |
字段约定:
[DisplayName]决定列头与表单标签,必须写;- 字符串写
StringLength,长文本用-2,不要留默认; - 金额用
decimal+Precision/Scale; - 软删除靠
EntityFull,不需要自己加IsDeleted。
第 2 步:生成或手写页面
方式 A:代码生成器
打开 /Admin/CrudGenerator,按 doc/代码生成.md 的说明配置:
| 配置区 | 关键项 |
|---|---|
| 基础设置 | 实体类型、路由路径、分页大小、编辑弹窗尺寸 |
| 基础设置 | 搜索 / 高级搜索 / 扩展按钮 / 导入 / 导出 / 多选 |
| 基础设置 | 草稿保存、保存不关闭、允许增删改、生成菜单 |
| 列配置 | 显示 / 筛选 / 搜索 / 编辑列宽 |
| 关系配置 | 多对一(下拉或弹框)、一对多/多对多(IncludeMany 或子表编辑) |
生成器还能识别类型并给出对应控件:
| 属性类型 | 表格列 | 编辑控件 |
|---|---|---|
string |
可筛选 + 可搜索 | BootstrapInput |
int / long / decimal |
可筛选 | BootstrapInput type="number" |
bool / bool? |
可筛选 | Switch / NullSwitch |
DateTime |
可筛选 | DateTimePicker |
[Flags] 枚举 |
位运算筛选 | MultiSelect |
| 普通枚举 | 可筛选 | Select |
| 多对一外键 | 显示关联名 + 筛选 | AdminSelectEntity |
| 一对多 / 多对多 | ------ | 子表编辑 / IncludeMany |
生成的文件会写进当前运行项目的 Components/ 下,子目录由路由路径决定。
方式 B:手写(演示项目的做法)
razor
@page "/Admin/Product"
@using ProductEntity = EasyAdminBlazor.Test.Products.Product
<AdminTable TItem="ProductEntity" TKey="long"
OnBeforeQuery="OnBeforeQuery"
EditDialogSize="Size.ExtraLarge"
EnableSaveWithoutClose EnableDraft
ShowImportButton ShowExportButton ShowExtendButtons
IsPagination ShowSearch ShowAdvancedSearch IsMultipleSelect>
<TableColumns>
<TableColumn @bind-Field="context.Title" Filterable="true" Searchable="true" />
<TableColumn @bind-Field="context.Price" Filterable="true" />
<TableColumn @bind-Field="context.Stock" Filterable="true" />
<TableColumn @bind-Field="context.Image" Filterable="true">
<Template Context="v">
@if (!string.IsNullOrEmpty(v.Row.Image))
{
<img src="@v.Row.Image" />
}
</Template>
</TableColumn>
<TableColumn @bind-Field="context.IsOnSale" Filterable="true" />
<TableColumn @bind-Field="context.CreatedTime" Filterable="true" />
</TableColumns>
<EditTemplate>
<ProductEdit item="context" />
</EditTemplate>
</AdminTable>
@code {
private void OnBeforeQuery(AdminQueryEventArgs<ProductEntity> e)
{
// 需要追加条件或 Include 时写这里
}
}
页面组件与实体同名时记得加
@using ... = ...别名,否则会撞类型。
第 3 步:建菜单 + 按钮
三种做法:
后台生成代码同时生成菜单
后台手工建 :系统管理 → 菜单 → 新增,类型选"增删改查"(会自动带 add / edit / remove 三个按钮),父级选一个分组。
代码补种(适合多环境一致交付,演示项目用的就是这种):
csharp
List<SysMenu> cudButtons() => new[]
{
new SysMenu { Label = "添加", Path = "add", Sort = 10011, Type = SysMenuType.Button },
new SysMenu { Label = "编辑", Path = "edit", Sort = 10012, Type = SysMenuType.Button },
new SysMenu { Label = "删除", Path = "remove", Sort = 10013, Type = SysMenuType.Button }
}.ToList();
repo.Insert(new[]
{
new SysMenu
{
Label = "产品",
Path = "Admin/Product",
ParentId = demoRoot.Id,
Sort = 10005,
Type = SysMenuType.Menu,
Childs = new List<SysMenu>(cudButtons())
}
});
路径必须与 @page 一致(不区分大小写),按钮必须挂在目标菜单下(第 08 篇)。
第 4 步:角色授权
角色页 → 选中角色 → 右侧菜单树勾选 → 保存。保存会调用 InvalidatePermissionCacheAsync(),权限立即生效。
自检:用一个非管理员账号登录,确认只看到勾选的菜单和按钮。
第 5 步:数据权限(需要行级隔离时)
实体实现 IDataPermission(并提供 IEntityCreated),然后开启:
razor
<AdminTable TItem="ProductEntity" TKey="long" UseDataPermission ...>
EntityFull 已经带了 CreatedUserId,所以只需要加一个组织字段:
csharp
public partial class Order : EntityFull, IDataPermission
{
[DisplayName("所属组织")]
public long OrgId { get; set; }
...
}
剩下的四件事框架自动做(第 09 篇):
| 路径 | 行为 |
|---|---|
| 查询 | 自动加组织过滤条件 |
| 新增 | AuditValue 自动写入当前用户的 OrgId |
| 修改 / 删除 | 按主键回查权限范围 |
| Excel 导入 | 逐行过滤越权数据 |
第 6 步:高级查询与关联
| 需求 | 做法 |
|---|---|
| 多列模糊搜索 | 列上标 Searchable="true" |
| 高级搜索 | 表格加 ShowAdvancedSearch |
| 列头筛选 | 列上标 Filterable="true" |
| 关联名称显示 | Template + v.Row.Classify?.ClassifyName |
| 关联实体筛选 | FilterProvider + AdminSelectEntityFilter(Generic) |
| 固定查询条件 | OnBeforeQuery 里 e.Select.Where(...) |
| 关联数据加载 | OnBeforeQuery 里 Include / IncludeMany |
| 多选筛选 | FilterProvider + AdminMultiSelectFilter |
两个纪律:导航属性必须写进 IgnoreSearchColumns;导出场景用 IsExport 跳过 Include(第 05、06 篇)。
第 7 步:表单增强
| 字段类型 | 组件 |
|---|---|
| 图片 / 附件 | AdminFileInput |
| 富文本 | AdminEditor(装 HtmlEditor 扩展后是 TinyMCE) |
| 字典单选 / 多选 | AdminDictSelect / AdminDictMultiSelect |
| 关联实体单选 | AdminSelectEntity / AdminSelectTable |
| 关联实体多选 | AdminMultiSelect |
| 树形选择 | AdminTree |
| 复选框列表 | AdminCheckboxListGeneric |
校验靠实体的数据注解 + BootstrapBlazor 输入组件(保存前框架会执行 EditContextCapture.Validate(),第 07 篇)。
第 8 步:Excel 导入导出
razor
<AdminTable ... ShowImportButton ShowExportButton>
加上这两个参数就获得:模板下载、5MB 上传、按可见列导入、按筛选条件导出。
需要业务校验时加回调:
csharp
private async Task OnBeforeImport(AdminImportEventArgs<ProductEntity> e)
{
var errors = e.Items.Where(x => x.Price < 0).Select(x => $"产品「{x.Title}」价格不能为负").ToList();
if (errors.Count > 0)
{
await SwalService.Warning("导入校验失败", string.Join("<br/>", errors.Take(20)));
e.Cancel = true;
}
}
数据权限过滤、UpdateColumns 白名单、操作日志都由框架负责(第 12 篇)。
第 9 步:审批
三处改动:
csharp
// 1) 实体换基类
public partial class Article : ApprovalEntityFull { ... }
csharp
// 2) 注册流程
.AddEasyAdminBlazorApproval(o =>
{
o.Flows.Add(new ApprovalFlowConfig
{
BillType = typeof(Article).FullName!,
Levels =
[
new ApprovalLevelConfig { Level = 1, Name = "部门主管审批", Kind = ApproverKind.OrgLeader, Offset = 1 },
new ApprovalLevelConfig { Level = 2, Name = "管理员审批", Kind = ApproverKind.Role, Value = "Administrator" }
]
});
})
razor
<!-- 3) 编辑模板加审批选项卡 + 列表加状态列 -->
<TabItem Text="@CommonLocalizer["审批"]">
<ApprovalActions TItem="Article" Bill="Model" OnChanged="OnApprovalChanged" />
</TabItem>
<TableColumn @bind-Field="context.ApprovalStatus" Filterable="true">
<Template Context="v">@v.Row.ApprovalStatusText</Template>
</TableColumn>
接入后自动获得:保存后提交/自动提交、审批中禁止修改删除、待办通知、审批中心、并发与事务保护(第 16--18 篇)。
第 10 步:日志与审计
| 需求 | 做法 |
|---|---|
| 程序日志 | 框架内置(DatabaseLogger,自动进错误日志页) |
| 操作审计 | 方法上标 [OperationLog("修改了xx")] |
| 登录日志 | 框架内置,登录/退出自动记录 |
| 排查问题 | 错误日志页按 TraceId / 用户 / 租户筛选 |
演示项目里连"暂停任务""恢复任务"这类操作都标了操作日志:
csharp
[OperationLog("恢复了任务")]
async Task ResumeTask(SchedulerTaskData task)
第 11 步:部署上线
按第 28 篇的 IIS 清单执行,其中与模块相关的自检:
- 新模块的菜单在生产库里也存在(用代码补种或导入菜单);
- 角色权限在生产环境重新确认(不同环境的角色数据不一定同步);
- 如果模块有文件字段,确认
uploads目录可写; - 如果模块是审批单据,确认审批人(组织负责人/角色)已维护;
- 生产环境
UseAutoSyncStructure关闭时,新表/新列已由脚本或预发布同步。
交付前自检清单
每做完一个模块,对着这张表过一遍:
| # | 检查项 |
|---|---|
| 1 | 实体:基类正确、[DisplayName] 齐全、长度/精度明确 |
| 2 | 页面:列可见性、筛选、搜索符合业务预期 |
| 3 | 页面:编辑模板字段与实体字段一一对应 |
| 4 | 菜单:路径与 @page 一致,类型正确 |
| 5 | 权限:按钮节点存在(add/edit/remove) |
| 6 | 权限:用非管理员账号验证过菜单与按钮 |
| 7 | 数据权限(如启用):另一个组织的数据确实看不到、改不动 |
| 8 | 查询:导航属性在 IgnoreSearchColumns 里 |
| 9 | 表单:必填/长度校验真的会拦 |
| 10 | 导入导出(如启用):导入的越权行被剔除、导出列正确 |
| 11 | 审批(如启用):提交/同意/驳回/撤回各走通一次,审批人配置正确 |
| 12 | 审计:关键操作能在操作日志里查到 |
小结
从一个 Entity 到一个完整模块,真正的工作量集中在三件事:
- 想清楚数据结构(第 1 步);
- 想清楚谁能看、谁能改(第 3--5 步);
- 想清楚业务规则(校验、导入、审批)。
其余部分------分页、排序、筛选、按钮权限、数据过滤、导入导出、事务并发、日志审计------都是"配置一下"或"写个回调"的事。
如果你正在用 .NET 10 + Blazor 做后台,可以把这篇当作新模块的开工清单。EasyAdminBlazor 的演示项目里有 6 个按这套流程做出来的真实模块,可以直接对照。