You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ABP Framework MVC Razor自定义CRUD UI:复杂表单与本地化问题

ABP Framework 复杂父子表单(Order-OrderDetails)实现指南

一、核心:Order-OrderDetails 父子表单实现

1. 实体层(遵循ABP聚合根规范)

// Order 聚合根(封装业务规则,确保数据一致性)
public class Order : AggregateRoot<Guid>
{
    public string Customer { get; set; }
    public DateTime OrderDate { get; set; }
    public string Currency { get; set; }
    public List<OrderDetail> OrderDetails { get; set; } = new List<OrderDetail>();

    // ABP聚合根要求私有无参构造函数
    private Order() {}

    public Order(Guid id, string customer, DateTime orderDate, string currency) : base(id)
    {
        Customer = customer;
        OrderDate = orderDate;
        Currency = currency;
    }

    // 封装添加明细的业务逻辑
    public void AddDetail(string stockCode, decimal price, int quantity)
    {
        OrderDetails.Add(new OrderDetail(Guid.NewGuid(), stockCode, price, quantity));
    }
}

// OrderDetail 子实体
public class OrderDetail : Entity<Guid>
{
    public string StockCode { get; set; }
    public decimal Price { get; set; }
    public int Quantity { get; set; }

    // 外键关联聚合根
    public Guid OrderId { get; set; }
    public Order Order { get; set; }

    private OrderDetail() {}

    public OrderDetail(Guid id, string stockCode, decimal price, int quantity) : base(id)
    {
        StockCode = stockCode;
        Price = price;
        Quantity = quantity;
    }
}

2. DTO层(适配UI与验证)

public class OrderCreateDto
{
    [Required]
    [Display(Name = "Customer")]
    public string Customer { get; set; }

    [Required]
    [Display(Name = "OrderDate")]
    public DateTime OrderDate { get; set; }

    [Required]
    [Display(Name = "Currency")]
    public string Currency { get; set; }

    [Required]
    public List<OrderDetailCreateDto> OrderDetails { get; set; } = new List<OrderDetailCreateDto>();
}

public class OrderDetailCreateDto
{
    [Required]
    [Display(Name = "StockCode")]
    public string StockCode { get; set; }

    [Required]
    [Range(0.01, double.MaxValue)]
    [Display(Name = "Price")]
    public decimal Price { get; set; }

    [Required]
    [Range(1, int.MaxValue)]
    [Display(Name = "Quantity")]
    public int Quantity { get; set; }
}

3. UI层(动态表单+多行明细录入)

<abp-dynamic-form abp-model="OrderCreateDto" asp-page="/Orders/CreateModal">
    <abp-modal class="custom-order-modal">
        <abp-modal-header title="@L["NewOrder"].Value"></abp-modal-header>
        <abp-modal-body>
            <!-- 主订单动态表单 -->
            <abp-form-content />
            
            <!-- 订单明细多行录入区域 -->
            <div class="mt-4">
                <h5>@L["OrderDetails"].Value</h5>
                <table class="table table-bordered">
                    <thead>
                        <tr>
                            <th>@L["StockCode"].Value</th>
                            <th>@L["Price"].Value</th>
                            <th>@L["Quantity"].Value</th>
                            <th>@L["Actions"].Value</th>
                        </tr>
                    </thead>
                    <tbody id="order-details-body">
                        <!-- 初始空行 -->
                        <tr class="detail-row">
                            <td><input type="text" class="form-control" name="OrderDetails[0].StockCode" required /></td>
                            <td><input type="number" step="0.01" class="form-control" name="OrderDetails[0].Price" required /></td>
                            <td><input type="number" class="form-control" name="OrderDetails[0].Quantity" required /></td>
                            <td><button type="button" class="btn btn-danger btn-sm remove-row">@L["Delete"].Value</button></td>
                        </tr>
                    </tbody>
                </table>
                <button type="button" class="btn btn-primary btn-sm mt-2" id="add-detail-row">@L["AddRow"].Value</button>
            </div>
        </abp-modal-body>
        <abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
    </abp-modal>
</abp-dynamic-form>

<!-- 多行录入交互逻辑 -->
<script>
    let rowIndex = 1;
    // 添加新行
    document.getElementById('add-detail-row').addEventListener('click', () => {
        const body = document.getElementById('order-details-body');
        const newRow = document.querySelector('.detail-row').cloneNode(true);
        
        // 更新表单字段索引,确保模型绑定正常
        newRow.querySelectorAll('input').forEach(input => {
            input.name = input.name.replace('[0]', `[${rowIndex}]`);
            input.value = '';
        });
        
        // 绑定删除事件
        newRow.querySelector('.remove-row').addEventListener('click', function() {
            this.closest('.detail-row').remove();
        });
        
        body.appendChild(newRow);
        rowIndex++;
    });

    // 初始行删除事件绑定
    document.querySelectorAll('.remove-row').forEach(btn => {
        btn.addEventListener('click', function() {
            this.closest('.detail-row').remove();
        });
    });
</script>

4. 数据存储(确保事务与一致性)

public class OrderAppService : ApplicationService, IOrderAppService
{
    private readonly IRepository<Order, Guid> _orderRepository;

    public OrderAppService(IRepository<Order, Guid> orderRepository)
    {
        _orderRepository = orderRepository;
    }

    [UnitOfWork]
    public async Task CreateAsync(OrderCreateDto input)
    {
        // 从DTO转换为聚合根,封装业务逻辑
        var order = new Order(GuidGenerator.Create(), input.Customer, input.OrderDate, input.Currency);
        
        foreach (var detailDto in input.OrderDetails)
        {
            order.AddDetail(detailDto.StockCode, detailDto.Price, detailDto.Quantity);
        }

        // 保存聚合根,自动关联保存子实体
        await _orderRepository.InsertAsync(order);
    }
}

二、表单尺寸宽度调整

通过自定义CSS修改模态框与表单字段宽度,不破坏ABP原有样式规则:

/* 修改模态框整体宽度 */
.custom-order-modal .modal-dialog {
    max-width: 900px;
    width: 90%; /* 响应式适配 */
}

/* 调整表单标签与输入框的比例 */
.abp-form-content .form-group .col-sm-2 {
    flex: 0 0 20%;
}
.abp-form-content .form-group .col-sm-9 {
    flex: 0 0 78%;
}

三、本地化调整

  1. 在项目 Localization/[项目名]/zh-Hans.json 中添加对应键值:
{
  "culture": "zh-Hans",
  "texts": {
    "NewOrder": "新建订单",
    "OrderDetails": "订单明细",
    "StockCode": "库存编码",
    "Price": "单价",
    "Quantity": "数量",
    "Actions": "操作",
    "Delete": "删除",
    "AddRow": "添加行",
    "Customer": "客户",
    "OrderDate": "订单日期",
    "Currency": "币种"
  }
}
  1. 动态表单会自动读取DTO上[Display(Name = "XXX")]对应的本地化键,无需额外配置。

四、验证与规则保障

  • 前端:ABP动态表单会自动根据DTO的验证属性([Required]、[Range]等)生成HTML5验证与提示。
  • 后端:ABP会自动在应用服务层执行DTO验证,确保非法数据无法进入业务逻辑。
  • 聚合根封装:通过AddDetail方法管控子实体的创建,确保业务规则不被绕过。

内容的提问来源于stack exchange,提问作者3ddark

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 10:55:05