Add automatic AreaCity catalog bootstrap and region tree management

This commit is contained in:
biss committed 2026-08-29 20:18:44 +08:00
1 parent e39c265f66
commit 6d2b042d46
10 files changed
+42870 -5

No files matched your search

@@ -0,0 +1,4 @@
@using ElectionSystem.Api.Modules.Organization.Domain
<div class="region-tree__node" role="treeitem" aria-expanded="@(Node.Children is not null ? Node.Expanded : null)"><button class="region-tree__toggle" @onclick="() => OnToggle.InvokeAsync(Node)">@((Node.Children is null || Node.Children.Count > 0) ? (Node.Expanded ? "−" : "+") : "·")</button><span class="region-tree__level">@Text(Node.Level)</span><strong>@Node.Name</strong><code>@Node.Code</code></div>
@if (Node.Expanded && Node.Children is { Count: > 0 }) { <div class="region-tree__children" role="group">@foreach (var child in Node.Children) { <RegionNodeView Node="child" OnToggle="OnToggle" /> }</div> }
@code { [Parameter, EditorRequired] public RegionTreeNode Node { get; set; } = default!; [Parameter, EditorRequired] public EventCallback<RegionTreeNode> OnToggle { get; set; } private static string Text(AdministrativeRegionLevel x) => x switch { AdministrativeRegionLevel.Province=>"省",AdministrativeRegionLevel.City=>"市",AdministrativeRegionLevel.District=>"区县",AdministrativeRegionLevel.Township=>"镇街",_=>"村/社区"}; }
@@ -0,0 +1,3 @@
using ElectionSystem.Api.Modules.Organization.Domain;
namespace ElectionSystem.Api.Components.Pages.Admin;
public sealed class RegionTreeNode(Guid id, string code, string name, AdministrativeRegionLevel level, bool active) { public Guid Id { get; } = id; public string Code { get; } = code; public string Name { get; } = name; public AdministrativeRegionLevel Level { get; } = level; public bool Active { get; } = active; public bool Expanded { get; set; } public List<RegionTreeNode>? Children { get; set; } }
@@ -7,7 +7,11 @@
<PageTitle>行政区划</PageTitle>
<section class="admin-page"><div class="admin-page__heading"><div><p class="eyebrow">基础资料 / 地域</p><h1>行政区划目录</h1><p>省、市、区县、镇街来自受控 AreaCity 数据版本;村/社区只可使用本地确认目录。</p></div><a class="admin-button" href="/admin/region-import">导入四级目录</a></div>
@if (!loaded) { <p class="admin-loading">正在加载目录…</p> } else { <div class="admin-table-wrap"><table class="admin-table"><thead><tr><th>层级</th><th>名称</th><th>代码</th><th>数据来源</th><th>版本</th><th>状态</th></tr></thead><tbody>@if (rows.Count == 0) { <tr><td colspan="6" class="admin-empty">尚未导入行政区划目录。请先导入 AreaCity 四级 CSV。</td></tr> } @foreach (var row in rows) { <tr><td>@Level(row.Level)</td><td>@row.Name</td><td class="admin-mono">@row.Code</td><td>@row.Source</td><td>@row.Version</td><td>@(row.Active ? "启用" : "停用")</td></tr> }</tbody></table></div> }</section>
@code { [CascadingParameter] private Task<AuthenticationState> AuthenticationStateTask { get; set; } = default!; private bool loaded; private readonly List<Row> rows = [];
protected override async Task OnInitializedAsync() { var id = RegionScopeService.GetRequiredUserId((await AuthenticationStateTask).User); var scope = await Scope.GetManagedRegionIdsAsync(id, CancellationToken.None); rows.AddRange(await Db.AdministrativeRegions.Where(x => scope.Contains(x.Id)).OrderBy(x => x.Level).ThenBy(x => x.Code).Take(500).Select(x => new Row(x.Level, x.Name, x.Code, x.Source, x.SourceVersion, x.IsActive)).ToListAsync()); loaded = true; }
private static string Level(AdministrativeRegionLevel value) => value switch { AdministrativeRegionLevel.Province => "省", AdministrativeRegionLevel.City => "市", AdministrativeRegionLevel.District => "区/县", AdministrativeRegionLevel.Township => "镇/街道", _ => "村/社区" }; private sealed record Row(AdministrativeRegionLevel Level, string Name, string Code, string Source, string Version, bool Active); }
@if (message is not null) { <div class="admin-alert">@message</div> }
@if (systemAdmin) { <EditForm Model="draft" OnValidSubmit="CreateVillageAsync" class="admin-form"><h2>在线新增村/社区</h2><label>所属镇/街道<select @bind="draft.ParentId" required><option value="">请选择镇/街道</option>@foreach (var town in townships) { <option value="@town.Id">@town.Name(@town.Code)</option> }</select></label><label>村/社区代码<input @bind="draft.Code" required maxlength="32" /></label><label>名称<input @bind="draft.Name" required maxlength="128" /></label><label>本地目录版本<input @bind="draft.SourceVersion" required maxlength="64" placeholder="如 2026-08" /></label><button class="admin-button" type="submit">保存村/社区</button></EditForm> }
@if (!loaded) { <p class="admin-loading">正在加载目录…</p> } else if (roots.Count == 0) { <p class="admin-empty">尚未导入行政区划目录。</p> } else { <div class="region-tree" role="tree">@foreach (var node in roots) { <RegionNodeView Node="node" OnToggle="ToggleAsync" /> }</div> }</section>
@code { [CascadingParameter] private Task<AuthenticationState> AuthenticationStateTask { get; set; } = default!; private bool loaded, systemAdmin; private string? message; private VillageDraft draft = new(); private HashSet<Guid> scope = []; private readonly List<RegionTreeNode> roots = []; private readonly List<Township> townships = [];
protected override async Task OnInitializedAsync() { var id = RegionScopeService.GetRequiredUserId((await AuthenticationStateTask).User); systemAdmin = await Db.UserRegionalRoleAssignments.AnyAsync(x => x.UserId == id && x.Role == RegionalRole.SystemAdministrator && x.RegionId == null); scope = await Scope.GetManagedRegionIdsAsync(id, CancellationToken.None); roots.Clear(); var rootIds = systemAdmin ? null : await Db.UserRegionalRoleAssignments.Where(x => x.UserId == id && x.RegionId.HasValue && x.Role != RegionalRole.CountingStationClerk && x.Role != RegionalRole.Supervisor).Select(x => x.RegionId!.Value).Distinct().ToListAsync(); var rootQuery = Db.AdministrativeRegions.Where(x => systemAdmin ? x.ParentId == null : rootIds!.Contains(x.Id)); roots.AddRange(await rootQuery.OrderBy(x => x.Code).Select(x => new RegionTreeNode(x.Id,x.Code,x.Name,x.Level,x.IsActive)).ToListAsync()); if (systemAdmin) { townships.Clear(); townships.AddRange(await Db.AdministrativeRegions.Where(x => x.IsActive && x.Level == AdministrativeRegionLevel.Township).OrderBy(x => x.Code).Select(x => new Township(x.Id, x.Code, x.Name)).ToListAsync()); } loaded = true; }
private async Task ToggleAsync(RegionTreeNode node) { if (node.Children is null) node.Children = await Db.AdministrativeRegions.Where(x => x.ParentId == node.Id && scope.Contains(x.Id)).OrderBy(x => x.Code).Select(x => new RegionTreeNode(x.Id,x.Code,x.Name,x.Level,x.IsActive)).ToListAsync(); node.Expanded = !node.Expanded; }
private async Task CreateVillageAsync() { if (!draft.ParentId.HasValue || string.IsNullOrWhiteSpace(draft.Code) || string.IsNullOrWhiteSpace(draft.Name) || string.IsNullOrWhiteSpace(draft.SourceVersion)) { message = "请完整填写村/社区信息。"; return; } var parent = await Db.AdministrativeRegions.SingleOrDefaultAsync(x => x.Id == draft.ParentId && x.IsActive && x.Level == AdministrativeRegionLevel.Township); if (parent is null) { message = "请选择有效的镇/街道。"; return; } if (await Db.AdministrativeRegions.AnyAsync(x => x.Code == draft.Code.Trim() && x.SourceVersion == draft.SourceVersion.Trim())) { message = "该代码与目录版本已存在。"; return; } Db.AdministrativeRegions.Add(new AdministrativeRegion { ParentId = parent.Id, Code = draft.Code.Trim(), Name = draft.Name.Trim(), Level = AdministrativeRegionLevel.Village, Source = "LocalApprovedVillageImport", SourceVersion = draft.SourceVersion.Trim() }); await Db.SaveChangesAsync(); message = "村/社区已保存。"; draft = new(); await OnInitializedAsync(); }
private static string Level(AdministrativeRegionLevel value) => value switch { AdministrativeRegionLevel.Province => "省", AdministrativeRegionLevel.City => "市", AdministrativeRegionLevel.District => "区/县", AdministrativeRegionLevel.Township => "镇/街道", _ => "村/社区" }; private sealed record Township(Guid Id, string Code, string Name); private sealed class VillageDraft { public Guid? ParentId { get; set; } public string Code { get; set; } = ""; public string Name { get; set; } = ""; public string SourceVersion { get; set; } = ""; } }
Loaded 3 of 10 files, more files were not shown because too many files have changed in this diff. Show more