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

MVC视图中三级嵌套集合的整洁展示方案咨询

需求:在MVC视图中整洁展示三级嵌套集合数据

我需要在MVC视图里呈现一个三级嵌套的集合数据(账户→地点→支付方式,每个支付方式关联账单数量),先后尝试了两种方案但都不满意,寻求合适的实现方式:

尝试1:嵌套表格实现

最初用嵌套表格渲染,虽然能展示数据,但布局混乱、无法对齐,也没法正常使用表头,效果很差。

视图代码:

<table class="table table-hover" style="vertical-align: middle">
    <tbody>
        @*Accounts*@
        @for (var counterAccount = 0; counterAccount < Model.RecordsToBeSubmitted.Count; counterAccount++)
        {
            var counterAccountSafe = counterAccount;
            <tr>
                <th scope="row">@Model.RecordsToBeSubmitted[counterAccountSafe].Account</th>
                <td>
                    <table class="table">
                        <tbody>
                            @*Locations*@
                            @for (var counterLocation = 0; counterLocation < Model.RecordsToBeSubmitted[counterAccountSafe].Locations.Count; counterLocation++)
                            {
                                var counterLocationSafe = counterLocation;
                                <tr>
                                    <td>@Model.RecordsToBeSubmitted[counterAccountSafe].Locations[counterLocationSafe].Location</td>
                                    <td>
                                        <table class="table">
                                            <tbody>
                                                @*Pay Modes*@
                                                @for (var counterPayMode = 0; counterPayMode < Model.RecordsToBeSubmitted[counterAccountSafe].Locations[counterLocationSafe].PaymentModes.Count; counterPayMode++)
                                                {
                                                    var counterPayModeSafe = counterPayMode;
                                                    <tr>
                                                        <td>@Model.RecordsToBeSubmitted[counterAccountSafe].Locations[counterLocationSafe].PaymentModes[counterPayModeSafe].PaymentMode</td>
                                                        <td>@Model.RecordsToBeSubmitted[counterAccountSafe].Locations[counterLocationSafe].PaymentModes[counterPayModeSafe].BillDetails.Count</td>
                                                    </tr>
                                                }
                                            </tbody>
                                        </table>
                                    </td>
                                </tr>
                            }
                        </tbody>
                    </table>
                </td>
            </tr>
        }
    </tbody>
</table>

原始嵌套模型类:

public class GrpSummaryResult : IBdo
{
    [DataMember]
    public bool IsProcessable { get; set; }
    [DataMember]
    public int TotalBills { get; set; }
    [DataMember]
    public IList<GrpSummaryGroupAccount> Accounts { get; set; }
}

[DataContract(Namespace = Constants.BdoNamespace)]
public class GrpSummaryGroupAccount : IBdo
{
    [DataMember]
    public string Account { get; set; }
    [DataMember]
    public IList<GrpSummaryGroupLocation> Locations { get; set; }
}

[DataContract(Namespace = Constants.BdoNamespace)]
public class GrpSummaryGroupLocation : IBdo
{
    [DataMember]
    public int Location { get; set; }
    [DataMember]
    public IList<GrpSummaryGroupPaymentMode> PaymentModes { get; set; }
}

[DataContract(Namespace = Constants.BdoNamespace)]
public class GrpSummaryGroupPaymentMode : IBdo
{
    [DataMember]
    public int PaymentModeId { get; set; }
    [DataMember]
    public string PaymentMode { get; set; }
    [DataMember]
    public IList<BillInfoDetail> BillDetails { get; set; }
}

[DataContract(Namespace = Constants.BdoNamespace)]
public class BillInfoDetail : IBdo
{
    [DataMember]
    public string BillNumber { get; set; }
    [DataMember]
    public string ServiceName { get; set; }
    [DataMember]
    public decimal Fees { get; set; }
}

尝试2:扁平数据+Kendo PivotGrid

后来改用扁平数据模型,但PivotGrid的展示效果不符合预期,不需要它的透视、配置等额外功能,只需要简洁的层级展示。

扁平数据模型:

[DataContract(Namespace = Constants.BdoNamespace)]
public class BillInfoDetail : IBdo
{
    [DataMember]
    public string FocusReferenceNumber { get => $"{BillNumber}-{PaymentModeId}"; private set { } }
    // 原有BillInfoDetail字段...
    [DataMember]
    public BillInfo Bill { get; set; }
}

public class BillInfo : IBdo
{
    [DataMember]
    public string BillNumber { get; set; }
    [DataMember]
    public int PayModeId { get; set; }
    [DataMember]
    public int CashierId { get; set; }
    [DataMember]
    public string PayMode { get; set; }
    [DataMember]
    public string CustomerCode { get; set; }
    [DataMember]
    public string CustomerName { get; set; }
    [DataMember]
    public string AccountGroup { get; set; }
}

PivotGrid实现代码:

@(Html.Kendo().PivotConfigurator()
    .Name("configurator")
    .HtmlAttributes(new { @class = "hidden-on-narrow" })
    .Filterable(true)
    .Sortable()
    .Height(580)
)

@(Html.Kendo().PivotGrid<DA.Systems.Focus.Business.BDO.BillInfoDetail>()
    .Name("pivotGrid")
    .HtmlAttributes(new { @class = "hidden-on-narrow" })
    .Filterable(true)
    .Configurator("#configurator")
    .ColumnWidth(120)
    .Height(570)
    .BindTo(Model.FlatData)
    .DataSource(dataSource => dataSource
        .Ajax()
        .Schema(schema => schema
            .Model(m => m.Field("Account", typeof(string)).From("Bill.AccountGroup"))
            .Model(m => m.Field("PaymentMode", typeof(string)).From("Bill.PayMode"))
            .Cube(cube => cube
                .Dimensions(dimensions => {
                    dimensions.Add("Account").Caption("Accounts");
                    dimensions.Add(model => model.CashierId).Caption("Location");
                    dimensions.Add("PaymentMode").Caption("Payment Modes");
                })
                .Measures(measures => {
                    measures.Add("Bills").Format("{0}").Field(model => model.FocusReferenceNumber).AggregateName("count");
                })
            ))
        .Columns(columns => {
            columns.Add("Account").Expand(true);
            columns.Add("PaymentMode");
        })
        .Measures(measures => measures.Values("Bills"))
        .Events(e => e.Error("onError"))
    )
)

推荐解决方案:Kendo Hierarchical Grid(层级网格)

既然你有权限使用Kendo MVC,**Hierarchical Grid(层级展开网格)**是最适合的选择——它原生支持多级嵌套数据的展开/折叠,布局整齐,自带表头对齐,不需要额外复杂的CSS,也没有PivotGrid的冗余功能。

实现步骤:

1. 复用原始嵌套模型(不需要扁平数据)

直接使用你最初的GrpSummaryResult、GrpSummaryGroupAccount等嵌套模型,不需要转换为扁平结构。

2. 视图中配置Kendo Hierarchical Grid

@(Html.Kendo().Grid<GrpSummaryGroupAccount>()
    .Name("hierarchicalGrid")
    .Columns(columns => {
        columns.Bound(c => c.Account).Title("账户");
        columns.Command(cmd => cmd.Expand()).Width(60); // 展开/折叠按钮
    })
    .Sortable()
    .Pageable(false)
    .DataSource(dataSource => dataSource
        .Ajax()
        .Read(read => read.Action("GetAccounts", "YourController")) // 替换为你的控制器Action,返回Accounts列表
    )
    .DetailTemplate(@<text>
        <!-- 第二层级:地点 -->
        @(Html.Kendo().Grid<GrpSummaryGroupLocation>()
            .Name("grid_#=Account#") // 唯一命名避免冲突
            .Columns(columns => {
                columns.Bound(l => l.Location).Title("地点ID");
                columns.Command(cmd => cmd.Expand()).Width(60);
            })
            .Sortable()
            .Pageable(false)
            .DataSource(dataSource => dataSource
                .Ajax()
                .Read(read => read.Action("GetLocations", "YourController")
                    .Data("function() { return { account: '#=Account#' }; }")) // 传递父级账户参数
            )
            .DetailTemplate(@<text>
                <!-- 第三层级:支付方式 + 账单数量 -->
                @(Html.Kendo().Grid<GrpSummaryGroupPaymentMode>()
                    .Name("grid_#=Account#_#=Location#")
                    .Columns(columns => {
                        columns.Bound(p => p.PaymentMode).Title("支付方式");
                        columns.Bound(p => p.BillDetails.Count).Title("账单数量").Width(120);
                    })
                    .Sortable()
                    .Pageable(false)
                    .DataSource(dataSource => dataSource
                        .Ajax()
                        .Read(read => read.Action("GetPaymentModes", "YourController")
                            .Data("function() { return { account: '#=Account#', location: '#=Location#' }; }")) // 传递父级参数
                    )
                )
            </text>)
        )
    </text>)
)

3. 控制器中添加对应的读取Action

public ActionResult GetAccounts([DataSourceRequest] DataSourceRequest request)
{
    var result = // 从业务逻辑获取GrpSummaryResult中的Accounts列表
    return Json(result.ToDataSourceResult(request));
}

public ActionResult GetLocations(string account, [DataSourceRequest] DataSourceRequest request)
{
    var accountData = // 根据account获取对应的GrpSummaryGroupAccount对象
    return Json(accountData.Locations.ToDataSourceResult(request));
}

public ActionResult GetPaymentModes(string account, int location, [DataSourceRequest] DataSourceRequest request)
{
    var accountData = // 根据account获取账户对象
    var locationData = accountData.Locations.First(l => l.Location == location);
    return Json(locationData.PaymentModes.ToDataSourceResult(request));
}

替代方案:纯HTML+CSS分组列表(无Kendo依赖)

如果不想用Kendo,也可以用Bootstrap的折叠组件+嵌套列表来实现,布局同样整齐:

<div class="container mt-4">
    @foreach (var account in Model.RecordsToBeSubmitted)
    {
        <div class="card mb-3">
            <div class="card-header" data-bs-toggle="collapse" data-bs-target="#account-@account.Account">
                <h5 class="mb-0">@account.Account</h5>
            </div>
            <div id="account-@account.Account" class="collapse show">
                <div class="card-body p-0">
                    @foreach (var location in account.Locations)
                    {
                        <div class="card border-0">
                            <div class="card-header" data-bs-toggle="collapse" data-bs-target="#location-@account.Account-@location.Location">
                                <h6 class="mb-0">地点:@location.Location</h6>
                            </div>
                            <div id="location-@account.Account-@location.Location" class="collapse show">
                                <div class="card-body">
                                    <table class="table table-sm mb-0">
                                        <thead>
                                            <tr>
                                                <th>支付方式</th>
                                                <th>账单数量</th>
                                            </tr>
                                        </thead>
                                        <tbody>
                                            @foreach (var payMode in location.PaymentModes)
                                            {
                                                <tr>
                                                    <td>@payMode.PaymentMode</td>
                                                    <td>@payMode.BillDetails.Count</td>
                                                </tr>
                                            }
                                        </tbody>
                                    </table>
                                </div>
                            </div>
                        </div>
                    }
                </div>
            </div>
        </div>
    }
</div>

这个方案依赖Bootstrap的折叠组件,不需要额外JS(除了Bootstrap的JS),布局整洁,支持展开/折叠,表头对齐也没问题。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 05:20:47