第七章:二次开发实战-创建自定义模块
目录
1. 二次开发概述
1.1 开发模式选择
Admin.NET推荐的二次开发模式是创建独立的应用层项目,这样可以:
- 与主框架解耦,便于升级
- 独立管理业务代码
- 保持代码结构清晰
推荐项目结构:
Admin.NET/
├── Admin.NET.Core/ # 核心层(不修改)
├── Admin.NET.Application/ # 示例应用层(参考用)
├── Admin.NET.Web.Core/ # Web核心层(不修改)
├── Admin.NET.Web.Entry/ # Web入口层
├── MyCompany.Application/ # 自定义应用层(新建)
│ ├── Entity/ # 业务实体
│ ├── Service/ # 业务服务
│ ├── Dto/ # 数据传输对象
│ ├── EventBus/ # 事件处理
│ └── Const/ # 常量定义
└── Plugins/ # 插件项目
1.2 创建应用层项目
步骤1:创建类库项目
dotnet new classlib -n MyCompany.Application -f net8.0
步骤2:添加项目引用
在MyCompany.Application.csproj中添加:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<!-- 引用Core层 -->
<ProjectReference Include="..\Admin.NET.Core\Admin.NET.Core.csproj" />
</ItemGroup>
</Project>
步骤3:在Web.Entry中引用
<ItemGroup>
<ProjectReference Include="..\MyCompany.Application\MyCompany.Application.csproj" />
</ItemGroup>
步骤4:创建GlobalUsings.cs
// MyCompany.Application/GlobalUsings.cs
global using System;
global using System.Collections.Generic;
global using System.ComponentModel;
global using System.ComponentModel.DataAnnotations;
global using System.Linq;
global using System.Threading.Tasks;
global using Admin.NET.Core;
global using Furion.DynamicApiController;
global using Mapster;
global using Microsoft.AspNetCore.Mvc;
global using SqlSugar;
2. 创建业务实体
2.1 实体设计原则
- 继承合适的基类(EntityBase、EntityBaseData、EntityTenant)
- 使用SqlSugar特性标注
- 添加注释和数据校验
- 考虑索引设计
2.2 创建实体类
// MyCompany.Application/Entity/Product.cs
namespace MyCompany.Application;
/// <summary>
/// 产品表
/// </summary>
[SugarTable(null, "产品表")]
public class Product : EntityTenant
{
/// <summary>
/// 产品编码
/// </summary>
[SugarColumn(ColumnDescription = "产品编码", Length = 64)]
[Required, MaxLength(64)]
public string Code { get; set; }
/// <summary>
/// 产品名称
/// </summary>
[SugarColumn(ColumnDescription = "产品名称", Length = 128)]
[Required, MaxLength(128)]
public string Name { get; set; }
/// <summary>
/// 产品分类Id
/// </summary>
[SugarColumn(ColumnDescription = "产品分类Id")]
public long CategoryId { get; set; }
/// <summary>
/// 产品分类
/// </summary>
[Navigate(NavigateType.OneToOne, nameof(CategoryId))]
public ProductCategory Category { get; set; }
/// <summary>
/// 规格型号
/// </summary>
[SugarColumn(ColumnDescription = "规格型号", Length = 256)]
public string? Specification { get; set; }
/// <summary>
/// 单位
/// </summary>
[SugarColumn(ColumnDescription = "单位", Length = 16)]
public string? Unit { get; set; }
/// <summary>
/// 单价
/// </summary>
[SugarColumn(ColumnDescription = "单价", DecimalDigits = 2)]
public decimal Price { get; set; }
/// <summary>
/// 库存数量
/// </summary>
[SugarColumn(ColumnDescription = "库存数量")]
public int StockQty { get; set; }
/// <summary>
/// 产品图片
/// </summary>
[SugarColumn(ColumnDescription = "产品图片", Length = 512)]
public string? ImageUrl { get; set; }
/// <summary>
/// 产品描述
/// </summary>
[SugarColumn(ColumnDescription = "产品描述", ColumnDataType = StaticConfig.CodeFirst_BigString)]
public string? Description { get; set; }
/// <summary>
/// 状态(1正常 0停用)
/// </summary>
[SugarColumn(ColumnDescription = "状态")]
public StatusEnum Status { get; set; } = StatusEnum.Enable;
/// <summary>
/// 排序
/// </summary>
[SugarColumn(ColumnDescription = "排序")]
public int OrderNo { get; set; } = 100;
/// <summary>
/// 备注
/// </summary>
[SugarColumn(ColumnDescription = "备注", Length = 512)]
public string? Remark { get; set; }
}
/// <summary>
/// 产品分类表
/// </summary>
[SugarTable(null, "产品分类表")]
public class ProductCategory : EntityTenant
{
/// <summary>
/// 父级Id
/// </summary>
[SugarColumn(ColumnDescription = "父级Id")]
public long Pid { get; set; }
/// <summary>
/// 分类名称
/// </summary>
[SugarColumn(ColumnDescription = "分类名称", Length = 64)]
[Required, MaxLength(64)]
public string Name { get; set; }
/// <summary>
/// 分类编码
/// </summary>
[SugarColumn(ColumnDescription = "分类编码", Length = 64)]
public string? Code { get; set; }
/// <summary>
/// 排序
/// </summary>
[SugarColumn(ColumnDescription = "排序")]
public int OrderNo { get; set; } = 100;
/// <summary>
/// 状态
/// </summary>
[SugarColumn(ColumnDescription = "状态")]
public StatusEnum Status { get; set; } = StatusEnum.Enable;
/// <summary>
/// 备注
/// </summary>
[SugarColumn(ColumnDescription = "备注", Length = 256)]
public string? Remark { get; set; }
/// <summary>
/// 子分类
/// </summary>
[SugarColumn(IsIgnore = true)]
public List<ProductCategory> Children { get; set; }
}
2.3 创建DTO
// MyCompany.Application/Dto/ProductDto.cs
namespace MyCompany.Application;
/// <summary>
/// 产品分页查询输入
/// </summary>
public class PageProductInput : BasePageInput
{
/// <summary>
/// 产品编码
/// </summary>
public string? Code { get; set; }
/// <summary>
/// 产品名称
/// </summary>
public string? Name { get; set; }
/// <summary>
/// 分类Id
/// </summary>
public long? CategoryId { get; set; }
/// <summary>
/// 状态
/// </summary>
public StatusEnum? Status { get; set; }
}
/// <summary>
/// 添加产品输入
/// </summary>
public class AddProductInput
{
/// <summary>
/// 产品编码
/// </summary>
[Required(ErrorMessage = "产品编码不能为空")]
[MaxLength(64, ErrorMessage = "产品编码最大长度为64")]
public string Code { get; set; }
/// <summary>
/// 产品名称
/// </summary>
[Required(ErrorMessage = "产品名称不能为空")]
[MaxLength(128, ErrorMessage = "产品名称最大长度为128")]
public string Name { get; set; }
/// <summary>
/// 分类Id
/// </summary>
[Required(ErrorMessage = "请选择产品分类")]
public long CategoryId { get; set; }
/// <summary>
/// 规格型号
/// </summary>
public string? Specification { get; set; }
/// <summary>
/// 单位
/// </summary>
public string? Unit { get; set; }
/// <summary>
/// 单价
/// </summary>
[Range(0, double.MaxValue, ErrorMessage = "单价不能小于0")]
public decimal Price { get; set; }
/// <summary>
/// 库存数量
/// </summary>
[Range(0, int.MaxValue, ErrorMessage = "库存数量不能小于0")]
public int StockQty { get; set; }
/// <summary>
/// 产品图片
/// </summary>
public string? ImageUrl { get; set; }
/// <summary>
/// 产品描述
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 状态
/// </summary>
public StatusEnum Status { get; set; } = StatusEnum.Enable;
/// <summary>
/// 排序
/// </summary>
public int OrderNo { get; set; } = 100;
/// <summary>
/// 备注
/// </summary>
public string? Remark { get; set; }
}
/// <summary>
/// 更新产品输入
/// </summary>
public class UpdateProductInput : AddProductInput
{
/// <summary>
/// 主键Id
/// </summary>
[Required(ErrorMessage = "Id不能为空")]
public long Id { get; set; }
}
/// <summary>
/// 产品输出
/// </summary>
public class ProductOutput
{
/// <summary>
/// 主键Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 产品编码
/// </summary>
public string Code { get; set; }
/// <summary>
/// 产品名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 分类Id
/// </summary>
public long CategoryId { get; set; }
/// <summary>
/// 分类名称
/// </summary>
public string? CategoryName { get; set; }
/// <summary>
/// 规格型号
/// </summary>
public string? Specification { get; set; }
/// <summary>
/// 单位
/// </summary>
public string? Unit { get; set; }
/// <summary>
/// 单价
/// </summary>
public decimal Price { get; set; }
/// <summary>
/// 库存数量
/// </summary>
public int StockQty { get; set; }
/// <summary>
/// 产品图片
/// </summary>
public string? ImageUrl { get; set; }
/// <summary>
/// 状态
/// </summary>
public StatusEnum Status { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime? CreateTime { get; set; }
}
3. 创建服务层
3.1 产品服务
// MyCompany.Application/Service/ProductService.cs
namespace MyCompany.Application;
/// <summary>
/// 产品服务
/// </summary>
[ApiDescriptionSettings(Order = 200)]
public class ProductService : IDynamicApiController, ITransient
{
private readonly SqlSugarRepository<Product> _productRep;
private readonly SqlSugarRepository<ProductCategory> _categoryRep;
private readonly SysCacheService _cacheService;
public ProductService(
SqlSugarRepository<Product> productRep,
SqlSugarRepository<ProductCategory> categoryRep,
SysCacheService cacheService)
{
_productRep = productRep;
_categoryRep = categoryRep;
_cacheService = cacheService;
}
/// <summary>
/// 获取产品分页列表
/// </summary>
[DisplayName("获取产品分页列表")]
public async Task<SqlSugarPagedList<ProductOutput>> Page(PageProductInput input)
{
return await _productRep.AsQueryable()
.LeftJoin<ProductCategory>((p, c) => p.CategoryId == c.Id)
.WhereIF(!string.IsNullOrWhiteSpace(input.Code), (p, c) => p.Code.Contains(input.Code))
.WhereIF(!string.IsNullOrWhiteSpace(input.Name), (p, c) => p.Name.Contains(input.Name))
.WhereIF(input.CategoryId.HasValue, (p, c) => p.CategoryId == input.CategoryId)
.WhereIF(input.Status.HasValue, (p, c) => p.Status == input.Status)
.OrderBy((p, c) => p.OrderNo)
.OrderByDescending((p, c) => p.CreateTime)
.Select((p, c) => new ProductOutput
{
Id = p.Id,
Code = p.Code,
Name = p.Name,
CategoryId = p.CategoryId,
CategoryName = c.Name,
Specification = p.Specification,
Unit = p.Unit,
Price = p.Price,
StockQty = p.StockQty,
ImageUrl = p.ImageUrl,
Status = p.Status,
CreateTime = p.CreateTime
})
.ToPagedListAsync(input.Page, input.PageSize);
}
/// <summary>
/// 获取产品详情
/// </summary>
[DisplayName("获取产品详情")]
public async Task<Product> GetDetail([FromQuery] long id)
{
var product = await _productRep.AsQueryable()
.Includes(p => p.Category)
.FirstAsync(p => p.Id == id);
return product ?? throw Oops.Oh("产品不存在");
}
/// <summary>
/// 添加产品
/// </summary>
[ApiDescriptionSettings(Name = "Add"), HttpPost]
[DisplayName("添加产品")]
public async Task<long> Add(AddProductInput input)
{
// 验证编码唯一
var exist = await _productRep.IsAnyAsync(p => p.Code == input.Code);
if (exist)
throw Oops.Oh("产品编码已存在");
// 验证分类存在
var category = await _categoryRep.GetByIdAsync(input.CategoryId);
if (category == null)
throw Oops.Oh("产品分类不存在");
var product = input.Adapt<Product>();
await _productRep.InsertAsync(product);
return product.Id;
}
/// <summary>
/// 更新产品
/// </summary>
[ApiDescriptionSettings(Name = "Update"), HttpPost]
[DisplayName("更新产品")]
public async Task Update(UpdateProductInput input)
{
var product = await _productRep.GetByIdAsync(input.Id);
if (product == null)
throw Oops.Oh("产品不存在");
// 验证编码唯一(排除自身)
var exist = await _productRep.IsAnyAsync(p => p.Code == input.Code && p.Id != input.Id);
if (exist)
throw Oops.Oh("产品编码已存在");
input.Adapt(product);
await _productRep.UpdateAsync(product);
}
/// <summary>
/// 删除产品
/// </summary>
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
[DisplayName("删除产品")]
public async Task Delete([FromBody] DeleteInput input)
{
var product = await _productRep.GetByIdAsync(input.Id);
if (product == null)
throw Oops.Oh("产品不存在");
await _productRep.FakeDeleteAsync(product);
}
/// <summary>
/// 批量删除产品
/// </summary>
[ApiDescriptionSettings(Name = "BatchDelete"), HttpPost]
[DisplayName("批量删除产品")]
public async Task BatchDelete([FromBody] List<long> ids)
{
await _productRep.Context.Updateable<Product>()
.SetColumns(p => p.IsDelete == true)
.Where(p => ids.Contains(p.Id))
.ExecuteCommandAsync();
}
/// <summary>
/// 修改产品状态
/// </summary>
[ApiDescriptionSettings(Name = "SetStatus"), HttpPost]
[DisplayName("修改产品状态")]
public async Task SetStatus([FromBody] SetStatusInput input)
{
var product = await _productRep.GetByIdAsync(input.Id);
if (product == null)
throw Oops.Oh("产品不存在");
product.Status = input.Status;
await _productRep.UpdateAsync(product);
}
/// <summary>
/// 导出产品
/// </summary>
[ApiDescriptionSettings(Name = "Export"), HttpPost]
[DisplayName("导出产品")]
public async Task<IActionResult> Export(PageProductInput input)
{
var list = await _productRep.AsQueryable()
.LeftJoin<ProductCategory>((p, c) => p.CategoryId == c.Id)
.WhereIF(!string.IsNullOrWhiteSpace(input.Code), (p, c) => p.Code.Contains(input.Code))
.WhereIF(!string.IsNullOrWhiteSpace(input.Name), (p, c) => p.Name.Contains(input.Name))
.Select((p, c) => new ProductOutput
{
Id = p.Id,
Code = p.Code,
Name = p.Name,
CategoryName = c.Name,
Specification = p.Specification,
Unit = p.Unit,
Price = p.Price,
StockQty = p.StockQty,
Status = p.Status,
CreateTime = p.CreateTime
})
.ToListAsync();
var result = await list.ExportExcel();
return result;
}
}
/// <summary>
/// 删除输入
/// </summary>
public class DeleteInput
{
/// <summary>
/// Id
/// </summary>
[Required]
public long Id { get; set; }
}
/// <summary>
/// 设置状态输入
/// </summary>
public class SetStatusInput
{
/// <summary>
/// Id
/// </summary>
[Required]
public long Id { get; set; }
/// <summary>
/// 状态
/// </summary>
public StatusEnum Status { get; set; }
}
3.2 产品分类服务
// MyCompany.Application/Service/ProductCategoryService.cs
namespace MyCompany.Application;
/// <summary>
/// 产品分类服务
/// </summary>
[ApiDescriptionSettings(Order = 199)]
public class ProductCategoryService : IDynamicApiController, ITransient
{
private readonly SqlSugarRepository<ProductCategory> _categoryRep;
private readonly SqlSugarRepository<Product> _productRep;
public ProductCategoryService(
SqlSugarRepository<ProductCategory> categoryRep,
SqlSugarRepository<Product> productRep)
{
_categoryRep = categoryRep;
_productRep = productRep;
}
/// <summary>
/// 获取分类树形列表
/// </summary>
[DisplayName("获取分类树形列表")]
public async Task<List<ProductCategory>> GetTree()
{
return await _categoryRep.AsQueryable()
.Where(c => c.Status == StatusEnum.Enable)
.OrderBy(c => c.OrderNo)
.ToTreeAsync(c => c.Children, c => c.Pid, 0);
}
/// <summary>
/// 获取分类列表
/// </summary>
[DisplayName("获取分类列表")]
public async Task<List<ProductCategory>> GetList()
{
return await _categoryRep.AsQueryable()
.OrderBy(c => c.OrderNo)
.ToListAsync();
}
/// <summary>
/// 添加分类
/// </summary>
[ApiDescriptionSettings(Name = "Add"), HttpPost]
[DisplayName("添加分类")]
public async Task<long> Add(AddCategoryInput input)
{
// 验证名称唯一
var exist = await _categoryRep.IsAnyAsync(c =>
c.Name == input.Name && c.Pid == input.Pid);
if (exist)
throw Oops.Oh("分类名称已存在");
var category = input.Adapt<ProductCategory>();
await _categoryRep.InsertAsync(category);
return category.Id;
}
/// <summary>
/// 更新分类
/// </summary>
[ApiDescriptionSettings(Name = "Update"), HttpPost]
[DisplayName("更新分类")]
public async Task Update(UpdateCategoryInput input)
{
var category = await _categoryRep.GetByIdAsync(input.Id);
if (category == null)
throw Oops.Oh("分类不存在");
// 不能将自己设为父级
if (input.Pid == input.Id)
throw Oops.Oh("不能将自己设为父级分类");
input.Adapt(category);
await _categoryRep.UpdateAsync(category);
}
/// <summary>
/// 删除分类
/// </summary>
[ApiDescriptionSettings(Name = "Delete"), HttpPost]
[DisplayName("删除分类")]
public async Task Delete([FromBody] DeleteInput input)
{
// 检查是否有子分类
var hasChildren = await _categoryRep.IsAnyAsync(c => c.Pid == input.Id);
if (hasChildren)
throw Oops.Oh("请先删除子分类");
// 检查是否有关联产品
var hasProducts = await _productRep.IsAnyAsync(p => p.CategoryId == input.Id);
if (hasProducts)
throw Oops.Oh("该分类下有产品,无法删除");
await _categoryRep.DeleteByIdAsync(input.Id);
}
}
/// <summary>
/// 添加分类输入
/// </summary>
public class AddCategoryInput
{
/// <summary>
/// 父级Id
/// </summary>
public long Pid { get; set; }
/// <summary>
/// 分类名称
/// </summary>
[Required(ErrorMessage = "分类名称不能为空")]
[MaxLength(64)]
public string Name { get; set; }
/// <summary>
/// 分类编码
/// </summary>
public string? Code { get; set; }
/// <summary>
/// 排序
/// </summary>
public int OrderNo { get; set; } = 100;
/// <summary>
/// 备注
/// </summary>
public string? Remark { get; set; }
}
/// <summary>
/// 更新分类输入
/// </summary>
public class UpdateCategoryInput : AddCategoryInput
{
/// <summary>
/// Id
/// </summary>
[Required]
public long Id { get; set; }
}
4. 创建API接口
服务类实现IDynamicApiController接口后,Furion会自动生成API接口,无需手动创建Controller。
生成的API路径规则:
- 服务名:
ProductService->/api/product - 方法名:
GetPage->GET /api/product/page - 方法名:
Add->POST /api/product/add
自定义API配置:
// 自定义API分组
[ApiDescriptionSettings("产品管理", Name = "Product", Order = 200)]
public class ProductService : IDynamicApiController, ITransient
{
// 自定义方法名
[ApiDescriptionSettings(Name = "List")]
public async Task<List<Product>> GetList() { }
// 自定义HTTP方法
[HttpGet]
public async Task<Product> GetDetail(long id) { }
// 不生成API
[NonAction]
public void PrivateMethod() { }
}
5. 前端页面开发
5.1 创建API接口
// api/product/index.ts
import request from '@/utils/request'
export interface ProductQuery {
code?: string
name?: string
categoryId?: number
status?: number
page: number
pageSize: number
}
export interface ProductInfo {
id: number
code: string
name: string
categoryId: number
categoryName?: string
specification?: string
unit?: string
price: number
stockQty: number
imageUrl?: string
status: number
createTime?: string
}
export interface AddProductParams {
code: string
name: string
categoryId: number
specification?: string
unit?: string
price: number
stockQty: number
imageUrl?: string
description?: string
status: number
orderNo: number
remark?: string
}
export const productApi = {
// 分页查询
getPage(params: ProductQuery) {
return request({
url: '/api/product/page',
method: 'get',
params
})
},
// 获取详情
getDetail(id: number) {
return request({
url: '/api/product/detail',
method: 'get',
params: { id }
})
},
// 新增
add(data: AddProductParams) {
return request({
url: '/api/product/add',
method: 'post',
data
})
},
// 更新
update(data: AddProductParams & { id: number }) {
return request({
url: '/api/product/update',
method: 'post',
data
})
},
// 删除
delete(id: number) {
return request({
url: '/api/product/delete',
method: 'post',
data: { id }
})
},
// 修改状态
setStatus(id: number, status: number) {
return request({
url: '/api/product/setStatus',
method: 'post',
data: { id, status }
})
},
// 导出
export(params: ProductQuery) {
return request({
url: '/api/product/export',
method: 'post',
data: params,
responseType: 'blob'
})
}
}
// 分类API
export const productCategoryApi = {
getTree() {
return request({
url: '/api/productCategory/tree',
method: 'get'
})
},
getList() {
return request({
url: '/api/productCategory/list',
method: 'get'
})
},
add(data: any) {
return request({
url: '/api/productCategory/add',
method: 'post',
data
})
},
update(data: any) {
return request({
url: '/api/productCategory/update',
method: 'post',
data
})
},
delete(id: number) {
return request({
url: '/api/productCategory/delete',
method: 'post',
data: { id }
})
}
}
5.2 创建列表页面
<!-- views/product/index.vue -->
<template>
<div class="product-container">
<!-- 搜索栏 -->
<el-card class="search-card">
<el-form :model="queryParams" inline>
<el-form-item label="产品编码">
<el-input v-model="queryParams.code" placeholder="请输入产品编码" clearable />
</el-form-item>
<el-form-item label="产品名称">
<el-input v-model="queryParams.name" placeholder="请输入产品名称" clearable />
</el-form-item>
<el-form-item label="产品分类">
<el-tree-select
v-model="queryParams.categoryId"
:data="categoryTree"
:props="{ label: 'name', value: 'id', children: 'children' }"
placeholder="请选择分类"
clearable
check-strictly
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择" clearable>
<el-option label="启用" :value="1" />
<el-option label="停用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 表格 -->
<el-card class="table-card">
<template #header>
<div class="toolbar">
<el-button type="primary" v-auth="'product:add'" @click="handleAdd">
<el-icon><Plus /></el-icon>新增
</el-button>
<el-button type="danger" v-auth="'product:delete'" :disabled="!selectedIds.length" @click="handleBatchDelete">
批量删除
</el-button>
<el-button v-auth="'product:export'" @click="handleExport">
<el-icon><Download /></el-icon>导出
</el-button>
</div>
</template>
<el-table
v-loading="loading"
:data="tableData"
border
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="code" label="产品编码" width="120" />
<el-table-column prop="name" label="产品名称" min-width="150" />
<el-table-column prop="categoryName" label="所属分类" width="120" />
<el-table-column prop="specification" label="规格型号" width="120" />
<el-table-column prop="unit" label="单位" width="80" />
<el-table-column prop="price" label="单价" width="100">
<template #default="{ row }">
¥
</template>
</el-table-column>
<el-table-column prop="stockQty" label="库存" width="80" />
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
@change="handleStatusChange(row)"
/>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link v-auth="'product:edit'" @click="handleEdit(row)">
编辑
</el-button>
<el-popconfirm title="确定要删除吗?" @confirm="handleDelete(row.id)">
<template #reference>
<el-button type="danger" link v-auth="'product:delete'">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="getList"
@current-change="getList"
/>
</el-card>
<!-- 表单对话框 -->
<ProductForm
v-model="formVisible"
:data="currentRow"
:category-tree="categoryTree"
@success="getList"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { productApi, productCategoryApi } from '@/api/product'
import type { ProductInfo, ProductQuery } from '@/api/product'
import { ElMessage, ElMessageBox } from 'element-plus'
import ProductForm from './form.vue'
// 查询参数
const queryParams = reactive<ProductQuery>({
code: '',
name: '',
categoryId: undefined,
status: undefined,
page: 1,
pageSize: 10
})
// 表格数据
const tableData = ref<ProductInfo[]>([])
const total = ref(0)
const loading = ref(false)
const selectedIds = ref<number[]>([])
// 分类树
const categoryTree = ref([])
// 表单
const formVisible = ref(false)
const currentRow = ref<ProductInfo | null>(null)
// 获取列表
const getList = async () => {
loading.value = true
try {
const res = await productApi.getPage(queryParams)
tableData.value = res.data.items
total.value = res.data.total
} finally {
loading.value = false
}
}
// 获取分类树
const getCategoryTree = async () => {
const res = await productCategoryApi.getTree()
categoryTree.value = res.data
}
// 搜索
const handleSearch = () => {
queryParams.page = 1
getList()
}
// 重置
const handleReset = () => {
queryParams.code = ''
queryParams.name = ''
queryParams.categoryId = undefined
queryParams.status = undefined
queryParams.page = 1
getList()
}
// 选择变化
const handleSelectionChange = (rows: ProductInfo[]) => {
selectedIds.value = rows.map(r => r.id)
}
// 新增
const handleAdd = () => {
currentRow.value = null
formVisible.value = true
}
// 编辑
const handleEdit = (row: ProductInfo) => {
currentRow.value = row
formVisible.value = true
}
// 删除
const handleDelete = async (id: number) => {
try {
await productApi.delete(id)
ElMessage.success('删除成功')
getList()
} catch (error) {
console.error(error)
}
}
// 批量删除
const handleBatchDelete = async () => {
try {
await ElMessageBox.confirm('确定要删除选中的产品吗?', '提示')
// 调用批量删除接口
ElMessage.success('删除成功')
getList()
} catch {
// 取消
}
}
// 修改状态
const handleStatusChange = async (row: ProductInfo) => {
try {
await productApi.setStatus(row.id, row.status)
ElMessage.success('修改成功')
} catch {
row.status = row.status === 1 ? 0 : 1
}
}
// 导出
const handleExport = async () => {
try {
const res = await productApi.export(queryParams)
const blob = new Blob([res as any])
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '产品列表.xlsx'
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
console.error(error)
}
}
onMounted(() => {
getList()
getCategoryTree()
})
</script>
<style scoped lang="scss">
.product-container {
padding: 16px;
.search-card {
margin-bottom: 16px;
}
.toolbar {
display: flex;
gap: 10px;
}
.el-pagination {
margin-top: 16px;
justify-content: flex-end;
}
}
</style>
6. 权限配置
6.1 添加菜单
在系统管理-菜单管理中添加产品管理菜单:
-- 目录
INSERT INTO sys_menu (id, pid, type, title, name, path, component, icon, order_no, status)
VALUES (2000000001, 0, 1, '产品管理', 'product', '/product', '', 'product', 100, 1);
-- 菜单-产品列表
INSERT INTO sys_menu (id, pid, type, title, name, path, component, permission, icon, order_no, status)
VALUES (2000000002, 2000000001, 2, '产品列表', 'productList', '/product/list', 'product/index', 'product:list', '', 1, 1);
-- 按钮权限
INSERT INTO sys_menu (id, pid, type, title, permission, order_no, status)
VALUES
(2000000003, 2000000002, 3, '新增产品', 'product:add', 1, 1),
(2000000004, 2000000002, 3, '编辑产品', 'product:edit', 2, 1),
(2000000005, 2000000002, 3, '删除产品', 'product:delete', 3, 1),
(2000000006, 2000000002, 3, '导出产品', 'product:export', 4, 1);
6.2 配置路由
// router/modules/product.ts
export default {
path: '/product',
name: 'Product',
component: Layout,
redirect: '/product/list',
meta: { title: '产品管理', icon: 'product' },
children: [
{
path: 'list',
name: 'ProductList',
component: () => import('@/views/product/index.vue'),
meta: { title: '产品列表', permission: 'product:list' }
},
{
path: 'category',
name: 'ProductCategory',
component: () => import('@/views/product/category/index.vue'),
meta: { title: '产品分类', permission: 'productCategory:list' }
}
]
}
7. 代码生成器使用
7.1 配置代码生成
- 登录系统,进入”开发工具”-“代码生成”
- 点击”新增”,配置生成参数:
- 数据库:选择目标数据库
- 表名:选择要生成的表
- 功能名称:填写中文功能名
- 业务名称:填写英文业务名
- 命名空间:如
MyCompany.Application - 生成方式:压缩包/直接生成
- 配置字段属性:
- 显示名称
- 是否列表显示
- 是否表单显示
- 查询方式
- 控件类型
7.2 生成代码
点击”预览”查看生成效果,确认无误后点击”生成”。
生成的代码包括:
- 后端实体类
- 后端服务类
- 后端DTO
- 前端API接口
- 前端列表页面
- 前端表单组件
- 菜单SQL脚本
8. 完整示例:订单管理模块
8.1 实体设计
// 订单表
[SugarTable(null, "订单表")]
public class Order : EntityTenant
{
[SugarColumn(ColumnDescription = "订单编号", Length = 32)]
public string OrderNo { get; set; }
[SugarColumn(ColumnDescription = "客户Id")]
public long CustomerId { get; set; }
[SugarColumn(ColumnDescription = "订单金额", DecimalDigits = 2)]
public decimal TotalAmount { get; set; }
[SugarColumn(ColumnDescription = "订单状态")]
public OrderStatusEnum Status { get; set; }
[SugarColumn(ColumnDescription = "备注", Length = 512)]
public string? Remark { get; set; }
[Navigate(NavigateType.OneToMany, nameof(OrderItem.OrderId))]
public List<OrderItem> Items { get; set; }
}
// 订单明细表
[SugarTable(null, "订单明细表")]
public class OrderItem : EntityTenant
{
[SugarColumn(ColumnDescription = "订单Id")]
public long OrderId { get; set; }
[SugarColumn(ColumnDescription = "产品Id")]
public long ProductId { get; set; }
[SugarColumn(ColumnDescription = "产品名称", Length = 128)]
public string ProductName { get; set; }
[SugarColumn(ColumnDescription = "单价", DecimalDigits = 2)]
public decimal Price { get; set; }
[SugarColumn(ColumnDescription = "数量")]
public int Qty { get; set; }
[SugarColumn(ColumnDescription = "小计", DecimalDigits = 2)]
public decimal Amount { get; set; }
}
// 订单状态枚举
public enum OrderStatusEnum
{
[Description("待确认")]
Pending = 0,
[Description("已确认")]
Confirmed = 1,
[Description("已发货")]
Shipped = 2,
[Description("已完成")]
Completed = 3,
[Description("已取消")]
Cancelled = 9
}
8.2 订单服务
[ApiDescriptionSettings("订单管理", Order = 100)]
public class OrderService : IDynamicApiController, ITransient
{
private readonly SqlSugarRepository<Order> _orderRep;
private readonly SqlSugarRepository<OrderItem> _itemRep;
private readonly SqlSugarRepository<Product> _productRep;
private readonly ISqlSugarClient _db;
public OrderService(
SqlSugarRepository<Order> orderRep,
SqlSugarRepository<OrderItem> itemRep,
SqlSugarRepository<Product> productRep,
ISqlSugarClient db)
{
_orderRep = orderRep;
_itemRep = itemRep;
_productRep = productRep;
_db = db;
}
/// <summary>
/// 创建订单
/// </summary>
[HttpPost]
public async Task<long> Create(CreateOrderInput input)
{
// 生成订单号
var orderNo = $"ORD{DateTime.Now:yyyyMMddHHmmss}{new Random().Next(1000, 9999)}";
// 计算订单金额
decimal totalAmount = 0;
var items = new List<OrderItem>();
foreach (var item in input.Items)
{
var product = await _productRep.GetByIdAsync(item.ProductId);
if (product == null)
throw Oops.Oh($"产品不存在:{item.ProductId}");
if (product.StockQty < item.Qty)
throw Oops.Oh($"产品库存不足:{product.Name}");
var orderItem = new OrderItem
{
ProductId = product.Id,
ProductName = product.Name,
Price = product.Price,
Qty = item.Qty,
Amount = product.Price * item.Qty
};
items.Add(orderItem);
totalAmount += orderItem.Amount;
}
// 事务处理
try
{
_db.Ado.BeginTran();
// 创建订单
var order = new Order
{
OrderNo = orderNo,
CustomerId = input.CustomerId,
TotalAmount = totalAmount,
Status = OrderStatusEnum.Pending,
Remark = input.Remark
};
await _orderRep.InsertAsync(order);
// 创建订单明细
items.ForEach(i => i.OrderId = order.Id);
await _itemRep.InsertRangeAsync(items);
// 扣减库存
foreach (var item in input.Items)
{
await _db.Updateable<Product>()
.SetColumns(p => p.StockQty == p.StockQty - item.Qty)
.Where(p => p.Id == item.ProductId)
.ExecuteCommandAsync();
}
_db.Ado.CommitTran();
return order.Id;
}
catch
{
_db.Ado.RollbackTran();
throw;
}
}
}
总结
本章详细介绍了Admin.NET二次开发的完整流程:
- 开发模式:创建独立应用层项目
- 实体设计:继承基类、添加特性
- 服务开发:实现业务逻辑
- API接口:动态API自动生成
- 前端开发:API封装、页面组件
- 权限配置:菜单和按钮权限
- 代码生成:使用生成器加速开发
- 完整示例:订单管理模块
掌握这些内容后,你可以独立完成业务模块的开发。在下一章中,我们将学习更多高级功能的扩展开发。