| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596 |
- using System.Text.Json;
- using Admin.NET.Plugin.AiDOP.Entity;
- using Admin.NET.Plugin.AiDOP.Infrastructure;
- using SqlSugar;
- namespace Admin.NET.Plugin.AiDOP.Controllers;
- public partial class AidopKanbanController
- {
- private static readonly HashSet<string> DynamicDashboardModules = new(StringComparer.OrdinalIgnoreCase)
- {
- "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S9"
- };
- /// <summary>
- /// 通用动态详情看板:返回页面区域、组件配置、指标定义、当前值和趋势。
- /// </summary>
- [HttpGet("dashboard-page/{moduleCode}")]
- public async Task<IActionResult> GetDashboardPage(string moduleCode, [FromQuery] long factoryId = 1)
- {
- var mc = NormalizeDynamicDashboardModule(moduleCode);
- if (mc == null) return BadRequest(new { message = "moduleCode 仅支持 S1/S2/S3/S4/S5/S6/S7/S9" });
- var tenantId = AidopTenantHelper.GetTenantId(HttpContext);
- var kpis = await _db.Queryable<AdoSmartOpsKpiMaster>()
- .Where(x => x.TenantId == tenantId && x.ModuleCode == mc && x.IsEnabled)
- .OrderBy(x => x.MetricLevel)
- .OrderBy(x => x.SortNo)
- .ToListAsync();
- var kpiByCode = kpis.ToDictionary(x => x.MetricCode, StringComparer.OrdinalIgnoreCase);
- var pageConfig = (await _db.Queryable<AdoSmartOpsDashboardPageConfig>()
- .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && x.ModuleCode == mc)
- .ToListAsync()).FirstOrDefault();
- var persistedWidgets = await _db.Queryable<AdoSmartOpsDashboardWidget>()
- .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && x.ModuleCode == mc && x.IsEnabled == 1)
- .OrderBy(x => x.SortNo)
- .ToListAsync();
- var widgets = persistedWidgets.Count > 0
- ? persistedWidgets.Select(x => MapPersistedWidget(x, kpiByCode)).Where(x => x.MetricCodes.Count > 0).ToList()
- : BuildDefaultDashboardWidgets(mc, kpis);
- var values = await LoadDashboardValuesAsync(tenantId, factoryId, mc);
- var sections = BuildDashboardSections(widgets, values, kpiByCode);
- var tabs = BuildDashboardTabs(kpis);
- var navigationMode = ResolveNavigationMode(pageConfig?.NavigationMode, tabs.Count, mc);
- return Ok(new
- {
- moduleCode = mc,
- title = $"{mc} {ResolveDashboardModuleName(mc)}",
- subtitle = "动态运营指标看板",
- layoutMode = "section_grid",
- navigationMode,
- tabSource = pageConfig?.TabSource ?? "l1_metrics",
- defaultTabMetricCode = pageConfig?.DefaultTabMetricCode ?? tabs.FirstOrDefault()?.MetricCode,
- tabs,
- dataSource = new
- {
- metricDefinition = "ado_smart_ops_kpi_master",
- valueTables = new[] { "ado_s9_kpi_value_l1_day", "ado_s9_kpi_value_l2_day", "ado_s9_kpi_value_l3_day", "ado_s9_kpi_value_l4_day" },
- formulaStatus = "formula_expr_validated_pending_runtime_engine"
- },
- metrics = kpis.Select(x => BuildDashboardMetricPayload(x, values)).ToList(),
- sections
- });
- }
- /// <summary>
- /// 保存动态详情看板组件配置。前端配置页可逐步接入,当前 S1 默认布局无需先保存。
- /// </summary>
- [HttpPut("dashboard-page/{moduleCode}")]
- public async Task<IActionResult> PutDashboardPage(string moduleCode, [FromBody] DashboardPageSaveDto? body, [FromQuery] long factoryId = 1)
- {
- var mc = NormalizeDynamicDashboardModule(moduleCode);
- if (mc == null) return BadRequest(new { message = "moduleCode 仅支持 S1/S2/S3/S4/S5/S6/S7/S9" });
- if (body?.Widgets == null) return BadRequest(new { message = "widgets 不能为空" });
- var tenantId = AidopTenantHelper.GetTenantId(HttpContext);
- var kpiCodes = await _db.Queryable<AdoSmartOpsKpiMaster>()
- .Where(x => x.TenantId == tenantId && x.ModuleCode == mc && x.IsEnabled)
- .Select(x => x.MetricCode)
- .ToListAsync();
- var validCodes = new HashSet<string>(kpiCodes, StringComparer.OrdinalIgnoreCase);
- var now = DateTime.Now;
- var rows = new List<AdoSmartOpsDashboardWidget>();
- var sort = 0;
- foreach (var w in body.Widgets)
- {
- var metricCodes = (w.MetricCodes ?? new List<string>())
- .Where(x => !string.IsNullOrWhiteSpace(x))
- .Select(x => x.Trim())
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .ToList();
- if (metricCodes.Count == 0) continue;
- var invalid = metricCodes.FirstOrDefault(x => !validCodes.Contains(x));
- if (invalid != null) return BadRequest(new { message = $"无效指标编码: {invalid}" });
- sort += 10;
- rows.Add(new AdoSmartOpsDashboardWidget
- {
- TenantId = tenantId,
- FactoryId = factoryId,
- ModuleCode = mc,
- SectionCode = NormalizeSectionCode(w.SectionCode),
- TabMetricCode = string.IsNullOrWhiteSpace(w.TabMetricCode) ? null : w.TabMetricCode.Trim(),
- WidgetCode = string.IsNullOrWhiteSpace(w.WidgetCode) ? $"{mc}-W-{sort}" : w.WidgetCode.Trim(),
- WidgetType = NormalizeWidgetType(w.WidgetType),
- Title = string.IsNullOrWhiteSpace(w.Title) ? null : w.Title.Trim(),
- MetricLevel = Math.Max(1, Math.Min(4, w.MetricLevel)),
- MetricCodesJson = JsonSerializer.Serialize(metricCodes),
- Span = NormalizeSpan(w.Span),
- SortNo = w.SortNo > 0 ? w.SortNo : sort,
- StyleJson = string.IsNullOrWhiteSpace(w.StyleJson) ? null : w.StyleJson,
- IsEnabled = 1,
- UpdateTime = now
- });
- }
- try
- {
- _db.Ado.BeginTran();
- var navigationMode = NormalizeNavigationMode(body.NavigationMode);
- var tabSource = string.IsNullOrWhiteSpace(body.TabSource) ? "l1_metrics" : body.TabSource.Trim();
- var defaultTab = string.IsNullOrWhiteSpace(body.DefaultTabMetricCode) ? null : body.DefaultTabMetricCode.Trim();
- var pageRows = await _db.Updateable<AdoSmartOpsDashboardPageConfig>()
- .SetColumns(x => new AdoSmartOpsDashboardPageConfig
- {
- NavigationMode = navigationMode,
- TabSource = tabSource,
- DefaultTabMetricCode = defaultTab,
- Theme = "dark",
- UpdateTime = now
- })
- .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && x.ModuleCode == mc)
- .ExecuteCommandAsync();
- if (pageRows == 0)
- {
- await _db.Insertable(new AdoSmartOpsDashboardPageConfig
- {
- TenantId = tenantId,
- FactoryId = factoryId,
- ModuleCode = mc,
- NavigationMode = navigationMode,
- TabSource = tabSource,
- DefaultTabMetricCode = defaultTab,
- Theme = "dark",
- UpdateTime = now
- }).ExecuteCommandAsync();
- }
- await _db.Deleteable<AdoSmartOpsDashboardWidget>()
- .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && x.ModuleCode == mc)
- .ExecuteCommandAsync();
- if (rows.Count > 0) await _db.Insertable(rows).ExecuteCommandAsync();
- _db.Ado.CommitTran();
- }
- catch (Exception ex)
- {
- _db.Ado.RollbackTran();
- return BadRequest(new { message = ex.Message });
- }
- return Ok(new { ok = true, count = rows.Count });
- }
- private static string? NormalizeDynamicDashboardModule(string moduleCode)
- {
- var mc = (moduleCode ?? "").Trim().ToUpperInvariant();
- return DynamicDashboardModules.Contains(mc) ? mc : null;
- }
- private static string ResolveDashboardModuleName(string moduleCode) => moduleCode.ToUpperInvariant() switch
- {
- "S1" => "产销协同看板",
- "S2" => "订单排程看板",
- "S3" => "供应协同看板",
- "S4" => "采购执行看板",
- "S5" => "物料仓储看板",
- "S6" => "生产执行看板",
- "S7" => "成品仓储看板",
- "S9" => "运营指标看板",
- _ => "运营看板"
- };
- private static string NormalizeSectionCode(string? sectionCode)
- {
- var s = (sectionCode ?? "").Trim().ToLowerInvariant();
- return s is "overview" or "process_efficiency" or "quality_trend" or "resource_load"
- or "inventory_warning" or "breakdown" or "trend" or "detail" ? s : "overview";
- }
- private static string NormalizeWidgetType(string? widgetType)
- {
- var s = (widgetType ?? "").Trim().ToLowerInvariant();
- return s is "metric_card" or "traffic_light_card" or "progress_card" or "gauge"
- or "trend_line" or "line_trend" or "bar_compare" or "funnel_chart" or "heatmap"
- or "threshold_line" or "area_chart" or "metric_tree" or "metric_table" ? s : "metric_card";
- }
- private static string NormalizeNavigationMode(string? navigationMode)
- {
- var s = (navigationMode ?? "").Trim().ToLowerInvariant();
- return s is "tabs_by_l1" or "l1_cards" or "single_page" or "auto" ? s : "auto";
- }
- private static string ResolveNavigationMode(string? navigationMode, int tabCount, string moduleCode)
- {
- var mode = NormalizeNavigationMode(navigationMode);
- if (mode != "auto") return mode;
- if (string.Equals(moduleCode, "S1", StringComparison.OrdinalIgnoreCase) && tabCount > 0) return "l1_cards";
- return tabCount > 3 ? "tabs_by_l1" : "single_page";
- }
- private static int NormalizeSpan(int span)
- {
- if (span <= 0) return 6;
- if (span > 24) return 24;
- return span;
- }
- private static DashboardWidgetModel MapPersistedWidget(AdoSmartOpsDashboardWidget row, Dictionary<string, AdoSmartOpsKpiMaster> kpiByCode)
- {
- var codes = ParseMetricCodes(row.MetricCodesJson)
- .Where(kpiByCode.ContainsKey)
- .ToList();
- return new DashboardWidgetModel(
- row.SectionCode,
- row.TabMetricCode,
- row.WidgetCode,
- row.WidgetType,
- string.IsNullOrWhiteSpace(row.Title) && codes.Count > 0 ? kpiByCode[codes[0]].MetricName : row.Title,
- row.MetricLevel,
- codes,
- row.Span,
- row.SortNo,
- row.StyleJson);
- }
- private static List<DashboardWidgetModel> BuildDefaultDashboardWidgets(string moduleCode, List<AdoSmartOpsKpiMaster> kpis)
- {
- var result = new List<DashboardWidgetModel>();
- var l1 = kpis.Where(x => x.MetricLevel == 1).OrderBy(x => x.SortNo).Take(8).ToList();
- var l2 = kpis.Where(x => x.MetricLevel == 2).OrderBy(x => x.SortNo).ToList();
- var sort = 0;
- foreach (var k in l1)
- {
- sort += 10;
- var widgetType = ResolveDefaultWidgetType(k);
- result.Add(new DashboardWidgetModel(
- "overview",
- k.MetricCode,
- $"{moduleCode}-OV-{k.MetricCode}",
- widgetType,
- k.MetricName,
- 1,
- new List<string> { k.MetricCode },
- widgetType == "gauge" ? 6 : 6,
- sort,
- null));
- }
- foreach (var group in l2.GroupBy(x => x.ParentId).OrderBy(g => g.Min(x => x.SortNo)))
- {
- sort += 10;
- var metricCodes = group.OrderBy(x => x.SortNo).Select(x => x.MetricCode).ToList();
- var parent = group.Key == null ? null : kpis.FirstOrDefault(x => x.Id == group.Key);
- var parentName = parent?.MetricName ?? "未分组指标";
- var tabMetricCode = parent?.MetricCode;
- var sectionCode = ResolveDefaultSection(parentName);
- result.Add(new DashboardWidgetModel(
- sectionCode,
- tabMetricCode,
- $"{moduleCode}-BD-{group.Key ?? 0}",
- ResolveDefaultAnalysisWidgetType(parentName),
- parentName,
- 2,
- metricCodes,
- sectionCode == "process_efficiency" ? 12 : 8,
- sort,
- null));
- }
- foreach (var k in l1)
- {
- sort += 10;
- result.Add(new DashboardWidgetModel(
- ResolveDefaultSection(k.MetricName),
- k.MetricCode,
- $"{moduleCode}-TR-{k.MetricCode}",
- ResolveDefaultTrendWidgetType(k.MetricName),
- $"{k.MetricName}趋势",
- 1,
- new List<string> { k.MetricCode },
- 12,
- sort,
- null));
- }
- return result;
- }
- private static string ResolveDefaultWidgetType(AdoSmartOpsKpiMaster kpi)
- {
- var name = kpi.MetricName ?? "";
- if (name.Contains("满足率") || name.Contains("达成率") || name.Contains("合格率")) return "gauge";
- if (name.Contains("周期") || name.Contains("周转")) return "progress_card";
- return "metric_card";
- }
- private static string ResolveDefaultSection(string? metricName)
- {
- var name = metricName ?? "";
- if (name.Contains("满足率") || name.Contains("达成率") || name.Contains("质量")) return "quality_trend";
- if (name.Contains("人数") || name.Contains("人效") || name.Contains("负荷")) return "resource_load";
- if (name.Contains("库存") || name.Contains("周转")) return "inventory_warning";
- if (name.Contains("周期") || name.Contains("评审") || name.Contains("流程")) return "process_efficiency";
- return "breakdown";
- }
- private static string ResolveDefaultAnalysisWidgetType(string? metricName)
- {
- var name = metricName ?? "";
- if (name.Contains("满足率") || name.Contains("达成率")) return "line_trend";
- if (name.Contains("人数") || name.Contains("人效") || name.Contains("负荷")) return "heatmap";
- if (name.Contains("库存") || name.Contains("周转")) return "threshold_line";
- if (name.Contains("周期") || name.Contains("评审")) return "bar_compare";
- return "metric_tree";
- }
- private static string ResolveDefaultTrendWidgetType(string? metricName)
- {
- var name = metricName ?? "";
- if (name.Contains("库存")) return "area_chart";
- if (name.Contains("周转")) return "threshold_line";
- return "line_trend";
- }
- private async Task<Dictionary<string, DashboardMetricValueModel>> LoadDashboardValuesAsync(long tenantId, long factoryId, string moduleCode)
- {
- var result = new Dictionary<string, DashboardMetricValueModel>(StringComparer.OrdinalIgnoreCase);
- AidopTenantMigration.EnsureKpiValueL4Table(_db);
- foreach (var (table, level) in new[]
- {
- ("ado_s9_kpi_value_l1_day", 1),
- ("ado_s9_kpi_value_l2_day", 2),
- ("ado_s9_kpi_value_l3_day", 3),
- ("ado_s9_kpi_value_l4_day", 4)
- })
- {
- var bizDate = await ResolveMaxBizDateGenericAsync(table, tenantId, factoryId, moduleCode);
- var currentSql = $"""
- SELECT metric_code AS MetricCode, metric_value AS MetricValue, target_value AS TargetValue, status_color AS StatusColor, trend_flag AS TrendFlag
- FROM {table}
- WHERE tenant_id=@tenantId AND factory_id=@factoryId AND module_code=@moduleCode AND is_deleted=0 AND biz_date=@bizDate
- """;
- var rows = await _db.Ado.SqlQueryAsync<DashboardValueRow>(currentSql, new { tenantId, factoryId, moduleCode, bizDate });
- var trendSql = $"""
- SELECT metric_code AS MetricCode, biz_date AS BizDate, metric_value AS MetricValue
- FROM {table}
- WHERE tenant_id=@tenantId AND factory_id=@factoryId AND module_code=@moduleCode AND is_deleted=0
- AND biz_date >= DATE_SUB(@bizDate, INTERVAL 13 DAY)
- ORDER BY metric_code, biz_date
- """;
- var trends = await _db.Ado.SqlQueryAsync<DashboardTrendRow>(trendSql, new { tenantId, factoryId, moduleCode, bizDate });
- var trendByMetric = trends
- .Where(x => !string.IsNullOrWhiteSpace(x.MetricCode))
- .GroupBy(x => x.MetricCode!, StringComparer.OrdinalIgnoreCase)
- .ToDictionary(
- x => x.Key,
- x => x.Select(t => new DashboardTrendPointModel(t.BizDate.ToString("yyyy-MM-dd"), t.MetricValue)).ToList(),
- StringComparer.OrdinalIgnoreCase);
- foreach (var row in rows.Where(x => !string.IsNullOrWhiteSpace(x.MetricCode)))
- {
- result[row.MetricCode!] = new DashboardMetricValueModel(
- level,
- row.MetricValue,
- row.TargetValue,
- row.StatusColor,
- row.TrendFlag,
- trendByMetric.TryGetValue(row.MetricCode!, out var pts) ? pts : new List<DashboardTrendPointModel>());
- }
- }
- return result;
- }
- private static List<object> BuildDashboardSections(
- List<DashboardWidgetModel> widgets,
- Dictionary<string, DashboardMetricValueModel> values,
- Dictionary<string, AdoSmartOpsKpiMaster> kpiByCode)
- {
- var sectionMeta = new Dictionary<string, (string Title, int Sort)>(StringComparer.OrdinalIgnoreCase)
- {
- ["overview"] = ("核心指标", 10),
- ["process_efficiency"] = ("流程效率分析", 20),
- ["quality_trend"] = ("质量与满意度趋势", 30),
- ["resource_load"] = ("人效与资源负荷", 40),
- ["inventory_warning"] = ("库存预警", 50),
- ["breakdown"] = ("指标分解", 60),
- ["trend"] = ("趋势分析", 70),
- ["detail"] = ("明细列表", 80)
- };
- return widgets
- .GroupBy(x => NormalizeSectionCode(x.SectionCode))
- .OrderBy(x => sectionMeta.TryGetValue(x.Key, out var meta) ? meta.Sort : 99)
- .Select(g =>
- {
- var meta = sectionMeta.TryGetValue(g.Key, out var m) ? m : (g.Key, 99);
- return new
- {
- sectionCode = g.Key,
- title = meta.Item1,
- layout = g.Key == "overview" ? "card_grid" : "panel_grid",
- widgets = g.OrderBy(x => x.SortNo).Select(w => BuildDashboardWidgetPayload(w, values, kpiByCode)).ToList()
- };
- })
- .Cast<object>()
- .ToList();
- }
- private static object BuildDashboardWidgetPayload(
- DashboardWidgetModel widget,
- Dictionary<string, DashboardMetricValueModel> values,
- Dictionary<string, AdoSmartOpsKpiMaster> kpiByCode)
- {
- var metrics = widget.MetricCodes
- .Where(kpiByCode.ContainsKey)
- .Select(code => BuildDashboardMetricPayload(kpiByCode[code], values))
- .ToList();
- return new
- {
- sectionCode = widget.SectionCode,
- tabMetricCode = widget.TabMetricCode,
- widgetCode = widget.WidgetCode,
- widgetType = widget.WidgetType,
- title = widget.Title,
- metricLevel = widget.MetricLevel,
- span = widget.Span,
- sortNo = widget.SortNo,
- style = ParseStyleJson(widget.StyleJson),
- metrics
- };
- }
- private static object BuildDashboardMetricPayload(
- AdoSmartOpsKpiMaster kpi,
- Dictionary<string, DashboardMetricValueModel> values)
- {
- values.TryGetValue(kpi.MetricCode, out var value);
- var level = value?.Level ?? kpi.MetricLevel;
- var achievement = AidopS4KpiMerge.AchievementLevel(
- value?.MetricValue,
- value?.TargetValue,
- kpi.Direction ?? "higher_is_better",
- kpi.YellowThreshold,
- kpi.RedThreshold);
- var gap = AidopS4KpiMerge.GapValue(value?.MetricValue, value?.TargetValue);
- return new
- {
- id = kpi.Id,
- metricCode = kpi.MetricCode,
- metricName = kpi.MetricName,
- metricLevel = level,
- parentId = kpi.ParentId,
- unit = kpi.Unit ?? "",
- direction = kpi.Direction,
- formula = kpi.Formula,
- formulaExpr = kpi.FormulaExpr,
- formulaPreview = kpi.FormulaPreview,
- calcRule = kpi.CalcRule,
- dataSource = kpi.DataSource,
- department = kpi.Department,
- currentValue = value?.MetricValue,
- targetValue = value?.TargetValue,
- statusColor = achievement,
- isBlink = achievement == "red" && IsBeyondRedThreshold(value?.MetricValue, value?.TargetValue, kpi.Direction),
- trendFlag = value?.TrendFlag,
- gapValue = gap,
- gapLabel = FormatGapLabelGeneric(gap, kpi.Unit),
- trend = value?.Trend ?? new List<DashboardTrendPointModel>()
- };
- }
- private static List<string> ParseMetricCodes(string? json)
- {
- if (string.IsNullOrWhiteSpace(json)) return new List<string>();
- try
- {
- return JsonSerializer.Deserialize<List<string>>(json) ?? new List<string>();
- }
- catch
- {
- return json.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
- }
- }
- private static object ParseStyleJson(string? json)
- {
- if (string.IsNullOrWhiteSpace(json)) return new { };
- try
- {
- return JsonSerializer.Deserialize<object>(json) ?? new { };
- }
- catch
- {
- return new { raw = json };
- }
- }
- private static bool IsBeyondRedThreshold(decimal? current, decimal? target, string? direction)
- {
- if (current == null || target == null || target == 0) return false;
- var ratio = current.Value / target.Value * 100m;
- return string.Equals(direction, "lower_is_better", StringComparison.OrdinalIgnoreCase)
- ? ratio > 120m
- : ratio < 80m;
- }
- private static List<DashboardTabModel> BuildDashboardTabs(List<AdoSmartOpsKpiMaster> kpis)
- {
- return kpis
- .Where(x => x.MetricLevel == 1)
- .OrderBy(x => x.SortNo)
- .Select(x => new DashboardTabModel(x.MetricCode, x.MetricName, x.SortNo))
- .ToList();
- }
- public class DashboardPageSaveDto
- {
- public string? NavigationMode { get; set; }
- public string? TabSource { get; set; }
- public string? DefaultTabMetricCode { get; set; }
- public List<DashboardWidgetSaveDto>? Widgets { get; set; }
- }
- public class DashboardWidgetSaveDto
- {
- public string? SectionCode { get; set; }
- public string? TabMetricCode { get; set; }
- public string? WidgetCode { get; set; }
- public string? WidgetType { get; set; }
- public string? Title { get; set; }
- public int MetricLevel { get; set; }
- public List<string>? MetricCodes { get; set; }
- public int Span { get; set; }
- public int SortNo { get; set; }
- public string? StyleJson { get; set; }
- }
- private sealed record DashboardWidgetModel(
- string SectionCode,
- string? TabMetricCode,
- string WidgetCode,
- string WidgetType,
- string? Title,
- int MetricLevel,
- List<string> MetricCodes,
- int Span,
- int SortNo,
- string? StyleJson);
- private sealed record DashboardTabModel(string MetricCode, string Title, int SortNo);
- private sealed record DashboardMetricValueModel(
- int Level,
- decimal? MetricValue,
- decimal? TargetValue,
- string? StatusColor,
- string? TrendFlag,
- List<DashboardTrendPointModel> Trend);
- private sealed record DashboardTrendPointModel(string Date, decimal? Value);
- private sealed class DashboardValueRow
- {
- public string? MetricCode { get; set; }
- public decimal? MetricValue { get; set; }
- public decimal? TargetValue { get; set; }
- public string? StatusColor { get; set; }
- public string? TrendFlag { get; set; }
- }
- private sealed class DashboardTrendRow
- {
- public string? MetricCode { get; set; }
- public DateTime BizDate { get; set; }
- public decimal? MetricValue { get; set; }
- }
- }
|