From 134c7480b301460c2ca4a980a96bb5f672a2a3ef Mon Sep 17 00:00:00 2001
From: ykkokluo <984278063@qq.com>
Date: Mon, 24 Aug 2026 16:16:58 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=A3=81=E5=AF=BC=E8=88=AA?=
=?UTF-8?q?=E5=86=85=E9=83=A8=E4=BA=A4=E7=AE=A1=E5=92=8C=E4=BF=A1=E5=8F=B7?=
=?UTF-8?q?=E4=BA=A4=E4=BA=92=E7=95=8C=E9=9D=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
MiGu.Server/Auth/PageCatalog.cs | 8 +
MiGu.Server/Configs/DeploymentCatalog.cs | 23 +-
MiGu.Server/Configs/DeploymentProfile.cs | 23 ++
.../Controllers/SignalDataController.cs | 112 +++++++
MiGu.Server/Controllers/WizardController.cs | 7 +-
.../Dashboard/DashboardShortcutCatalog.cs | 1 +
MiGu.Server/MiGu.Server.csproj | 4 +-
MiGu.Server/Program.cs | 2 +
MiGu.Server/Signal/SignalDataStore.cs | 112 +++++++
.../Signal/SignalModelSchemaResolver.cs | 275 ++++++++++++++++++
MiGu.Server/Signal/SignalTableManifest.cs | 40 +++
.../Signal/SignalTableManifestLoader.cs | 174 +++++++++++
.../simple-platform-vue/src/api/signalData.ts | 69 +++++
.../simple-platform-vue/src/api/wizard.ts | 21 +-
.../simple-platform-vue/src/config/navMenu.ts | 19 +-
.../src/config/quickEntries.ts | 2 +-
.../simple-platform-vue/src/router/index.ts | 7 +
.../simple-platform-vue/src/stores/auth.ts | 17 ++
.../simple-platform-vue/src/types/wizard.ts | 2 +-
.../src/views/WizardView.vue | 20 +-
.../src/views/admin/DataCenterView.vue | 266 +++++++++++++++++
21 files changed, 1188 insertions(+), 16 deletions(-)
create mode 100644 MiGu.Server/Controllers/SignalDataController.cs
create mode 100644 MiGu.Server/Signal/SignalDataStore.cs
create mode 100644 MiGu.Server/Signal/SignalModelSchemaResolver.cs
create mode 100644 MiGu.Server/Signal/SignalTableManifest.cs
create mode 100644 MiGu.Server/Signal/SignalTableManifestLoader.cs
create mode 100644 frontends/apps/simple-platform-vue/src/api/signalData.ts
create mode 100644 frontends/apps/simple-platform-vue/src/views/admin/DataCenterView.vue
diff --git a/MiGu.Server/Auth/PageCatalog.cs b/MiGu.Server/Auth/PageCatalog.cs
index e37191f..98a4676 100644
--- a/MiGu.Server/Auth/PageCatalog.cs
+++ b/MiGu.Server/Auth/PageCatalog.cs
@@ -44,6 +44,9 @@ public static class PageCatalog
new("admin-task-templates", "任务编排", "设计与编排", ScopePlatform),
new("admin-simple-fields", "字段管理", "设计与编排", ScopePlatform),
+ // ── 管理端 / Platform:数据中心(scene.signal,磁导航选型时显示) ──
+ new("admin-data-center", "数据中心", "数据中心", ScopePlatform),
+
// ── 管理端 / Platform:平台配置中心(聚合页,每个 Key 对齐前端聚合路由 route.name) ──
new("admin-config-strategy", "调度策略", "平台配置中心", ScopePlatform),
new("admin-vehicle-hub", "车辆运维", "平台配置中心", ScopePlatform),
@@ -86,6 +89,11 @@ public static class PageCatalog
["admin-config-map-monitor"] = "admin-config-ops-center",
["admin-config-system"] = "admin-config-system-center",
["admin-config-auth"] = "admin-config-system-center",
+ ["admin-data-center-stations"] = "admin-data-center",
+ ["admin-data-center-docks"] = "admin-data-center",
+ ["admin-data-center-handshake"] = "admin-data-center",
+ ["admin-data-center-release"] = "admin-data-center",
+ ["admin-data-center-mag-control"] = "admin-data-center",
};
/// 判断页面 Key 是否合法(用于角色保存时过滤掉脏数据 / 已下线页面)。
diff --git a/MiGu.Server/Configs/DeploymentCatalog.cs b/MiGu.Server/Configs/DeploymentCatalog.cs
index e893611..c1acef8 100644
--- a/MiGu.Server/Configs/DeploymentCatalog.cs
+++ b/MiGu.Server/Configs/DeploymentCatalog.cs
@@ -37,21 +37,40 @@ public static class DeploymentCatalog
["wms"] = new[] { "admin-config-facility", "admin-config-warehouse" },
};
- /// 所有「可被选型控制」的页面 Key(Module → 页 映射值的并集)。
+ ///
+ /// 场景插件 → 平台页面 Key。磁导航选型会并入 ,
+ /// 从而点亮「数据中心」菜单(PLC 握手 + 磁条交管配置)。
+ ///
+ public static readonly IReadOnlyDictionary SceneToPages =
+ new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ [DeploymentProfile.SignalSceneId] = new[] { "admin-data-center" },
+ };
+
+ /// 所有「可被选型控制」的页面 Key(Module / Scene 映射值的并集)。
public static IReadOnlyCollection TailorablePages()
{
var set = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (var v in ModuleToPages.Values) foreach (var p in v) set.Add(p);
+ foreach (var v in SceneToPages.Values) foreach (var p in v) set.Add(p);
return set;
}
- /// 当前部署画像下被「点亮」的可裁剪页(已启用 Module 映射到的页)。
+ /// 当前部署画像下被「点亮」的可裁剪页(已启用 Module / 激活场景映射到的页)。
public static IReadOnlyCollection EnabledPages(DeploymentProfile dp)
{
var set = new HashSet(StringComparer.OrdinalIgnoreCase);
if (dp == null) return set;
foreach (var m in dp.Modules ?? new List())
if (ModuleToPages.TryGetValue(m, out var ps)) foreach (var p in ps) set.Add(p);
+
+ var activeScenes = dp.ToLauncherSceneIds();
+ foreach (var (sceneId, pages) in SceneToPages)
+ {
+ if (!activeScenes.Contains(sceneId, StringComparer.OrdinalIgnoreCase)) continue;
+ foreach (var p in pages) set.Add(p);
+ }
+
return set;
}
diff --git a/MiGu.Server/Configs/DeploymentProfile.cs b/MiGu.Server/Configs/DeploymentProfile.cs
index 5128002..1436c89 100644
--- a/MiGu.Server/Configs/DeploymentProfile.cs
+++ b/MiGu.Server/Configs/DeploymentProfile.cs
@@ -64,4 +64,27 @@ public record DeploymentProfile(
}
return result;
}
+
+ /// 信号交互场景 id(PLC 握手 + 磁条交管)。
+ public const string SignalSceneId = "scene.signal";
+
+ /// 设备驱动场景 id(门/充电桩等,各类项目通用)。
+ public const string DeviceSceneId = "scene.device";
+
+ /// 是否选了磁导航。
+ public bool UsesMagnetic() =>
+ NavigationKinds?.Any(k => string.Equals(k, "magnetic", StringComparison.OrdinalIgnoreCase)) == true;
+
+ ///
+ /// 写入 active-scenes.json 的完整场景集合:导航插件 + 磁导航时并入 scene.signal + scene.device。
+ ///
+ public IReadOnlyList ToLauncherSceneIds()
+ {
+ var result = ToActiveSceneIds().ToList();
+ if (UsesMagnetic() && !result.Contains(SignalSceneId, StringComparer.OrdinalIgnoreCase))
+ result.Add(SignalSceneId);
+ if (!result.Contains(DeviceSceneId, StringComparer.OrdinalIgnoreCase))
+ result.Add(DeviceSceneId);
+ return result;
+ }
}
diff --git a/MiGu.Server/Controllers/SignalDataController.cs b/MiGu.Server/Controllers/SignalDataController.cs
new file mode 100644
index 0000000..cba2279
--- /dev/null
+++ b/MiGu.Server/Controllers/SignalDataController.cs
@@ -0,0 +1,112 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using MiGu.Server.Configs;
+using MiGu.Server.Signal;
+
+namespace MiGu.Server.Controllers;
+
+///
+/// 迷毂「数据中心」:把 scene.signal 的 Model JSON 以表格读写(PLC 握手 / 磁条交管)。
+/// 文件落在 SimpleLite 工作目录 Config/Signal/*.json,不依赖 SimpleLite 进程是否在跑。
+///
+[ApiController]
+[Authorize(Policy = "PlatformScope")]
+[Route("api/signal-data")]
+public sealed class SignalDataController : ControllerBase
+{
+ private readonly SignalDataStore _store;
+ private readonly ConfigStore _config;
+
+ public SignalDataController(SignalDataStore store, ConfigStore config)
+ {
+ _store = store;
+ _config = config;
+ }
+
+ [HttpGet]
+ public IActionResult List([FromQuery] bool summary = false)
+ {
+ var enabled = SignalEnabled();
+ var tables = summary
+ ? _store.GetTables().Select(ProjectSummary).ToList()
+ : _store.GetTables().Select(ProjectTable).ToList();
+ return Ok(new
+ {
+ signalEnabled = enabled,
+ workingDirectory = _store.ResolveWorkingDirectory(),
+ tables
+ });
+ }
+
+ [HttpGet("{id}")]
+ public IActionResult Get(string id)
+ {
+ var table = _store.Find(id);
+ if (table == null)
+ return NotFound(new { message = $"未知数据表:{id}" });
+ return Ok(ProjectTable(table));
+ }
+
+ [HttpPut("{id}")]
+ public IActionResult Save(string id, [FromBody] JsonElement body)
+ {
+ var table = _store.Find(id);
+ if (table == null)
+ return NotFound(new { message = $"未知数据表:{id}" });
+
+ if (!body.TryGetProperty("rows", out var rowsEl) || rowsEl.ValueKind != JsonValueKind.Array)
+ return BadRequest(new { message = "请求体需要 rows 数组" });
+
+ JsonArray rows;
+ try
+ {
+ rows = JsonNode.Parse(rowsEl.GetRawText()) as JsonArray ?? new JsonArray();
+ }
+ catch (Exception ex)
+ {
+ return BadRequest(new { message = $"rows 不是合法 JSON 数组:{ex.Message}" });
+ }
+
+ try
+ {
+ _store.SaveRows(table, rows);
+ }
+ catch (Exception ex)
+ {
+ return StatusCode(500, new { message = $"保存失败:{ex.Message}" });
+ }
+
+ return Ok(ProjectTable(table));
+ }
+
+ private object ProjectSummary(SignalTableDef table) => new
+ {
+ id = table.Id,
+ title = table.Title,
+ category = table.Category,
+ fileName = table.FileName
+ };
+
+ private object ProjectTable(SignalTableDef table)
+ {
+ var (path, error) = _store.ResolveFile(table, createDir: false);
+ var exists = path != null && System.IO.File.Exists(path);
+ return new
+ {
+ id = table.Id,
+ title = table.Title,
+ category = table.Category,
+ fileName = table.FileName,
+ exists,
+ error,
+ columns = table.Columns,
+ rows = _store.LoadRows(table)
+ };
+ }
+
+ private bool SignalEnabled() =>
+ _config.GetDeployment().ToLauncherSceneIds()
+ .Contains(DeploymentProfile.SignalSceneId, StringComparer.OrdinalIgnoreCase);
+}
diff --git a/MiGu.Server/Controllers/WizardController.cs b/MiGu.Server/Controllers/WizardController.cs
index 56354a4..cc076d2 100644
--- a/MiGu.Server/Controllers/WizardController.cs
+++ b/MiGu.Server/Controllers/WizardController.cs
@@ -84,9 +84,8 @@ public class WizardController : ControllerBase
// 平台 → 内核联动:把导航选型写入 SimpleLite 的 plugins/active-scenes.json(下次启动选择性加载;
// 已运行实例可由前端再调 POST /api/sl/projection/scenes/apply 触发增量 reload)。
// scene.device(门/充电桩/按钮盒驱动)为各类项目通用能力,向导暂无独立选项,固定并入激活集合;
- // scene.vda5050 等协议插件保持按需(不在集合则不加载)。基座 StandardScene.dll 由内核按
- // 清单 requiresCore 自动 alwaysLoad,无需在此声明。
- var sceneIds = profile.ToActiveSceneIds().Concat(new[] { "scene.device" }).Distinct().ToList();
+ // 磁导航选型时 ToLauncherSceneIds 会并入 scene.signal(PLC 握手 + 磁条交管)。
+ var sceneIds = profile.ToLauncherSceneIds();
var write = _launcher.WriteActiveScenes(sceneIds, alwaysLoad: null, source: "deployment-profile");
_log.LogInformation("部署向导已保存 by={User} nav=[{Nav}] scenes=[{Scenes}] activeScenesWritten={Ok}",
@@ -116,7 +115,7 @@ public class WizardController : ControllerBase
navigationKinds = dp.NavigationKinds,
scenarios = dp.Scenarios,
updatedBy = dp.UpdatedBy,
- activeSceneIds = dp.ToActiveSceneIds(),
+ activeSceneIds = dp.ToLauncherSceneIds(),
hiddenPages = DeploymentCatalog.HiddenPages(dp),
activeScenesWrite = write == null ? null : new
{
diff --git a/MiGu.Server/Dashboard/DashboardShortcutCatalog.cs b/MiGu.Server/Dashboard/DashboardShortcutCatalog.cs
index 2e01644..e416095 100644
--- a/MiGu.Server/Dashboard/DashboardShortcutCatalog.cs
+++ b/MiGu.Server/Dashboard/DashboardShortcutCatalog.cs
@@ -36,6 +36,7 @@ public static class DashboardShortcutCatalog
new("admin-scripts", "admin-scripts", PageCatalog.ScopePlatform),
new("admin-task-templates", "admin-task-templates", PageCatalog.ScopePlatform),
new("admin-simple-fields", "admin-simple-fields", PageCatalog.ScopePlatform),
+ new("admin-data-center", "admin-data-center", PageCatalog.ScopePlatform),
new("admin-config-strategy", "admin-config-strategy", PageCatalog.ScopePlatform),
new("admin-vehicle-hub", "admin-vehicle-hub", PageCatalog.ScopePlatform),
new("admin-config-facility", "admin-config-facility", PageCatalog.ScopePlatform),
diff --git a/MiGu.Server/MiGu.Server.csproj b/MiGu.Server/MiGu.Server.csproj
index 8b27a3a..d39329d 100644
--- a/MiGu.Server/MiGu.Server.csproj
+++ b/MiGu.Server/MiGu.Server.csproj
@@ -1,7 +1,8 @@
- net8.0
+ net8.0-windows
+ x64
enable
enable
MiGu.Server
@@ -24,6 +25,7 @@
+
diff --git a/MiGu.Server/Program.cs b/MiGu.Server/Program.cs
index e0d93cc..a35058e 100644
--- a/MiGu.Server/Program.cs
+++ b/MiGu.Server/Program.cs
@@ -8,6 +8,7 @@ using MiGu.Server.Configs;
using MiGu.Server.Launcher;
using MiGu.Server.OpenApi;
using MiGu.Server.Ota;
+using MiGu.Server.Signal;
using MiGu.DB.Kernel.Hosting;
using MiGu.Server.Persistence;
using Yarp.ReverseProxy.Transforms;
@@ -264,6 +265,7 @@ builder.Services.AddPlatformPersistence(builder.Configuration);
// 配置段绑定 appsettings.json:SimpleLite,可被环境变量 SIMPLELITE__XXX 覆盖。
builder.Services.Configure(builder.Configuration.GetSection("SimpleLite"));
builder.Services.AddSingleton();
+builder.Services.AddSingleton();
// OTA(WatchDog 编排):包库 / 任务 / 出站客户端
builder.Services.Configure(builder.Configuration.GetSection("Ota"));
diff --git a/MiGu.Server/Signal/SignalDataStore.cs b/MiGu.Server/Signal/SignalDataStore.cs
new file mode 100644
index 0000000..230b56a
--- /dev/null
+++ b/MiGu.Server/Signal/SignalDataStore.cs
@@ -0,0 +1,112 @@
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using MiGu.Server.Launcher;
+
+namespace MiGu.Server.Signal;
+
+public sealed record SignalColumn(string Key, string Label, string Type, IReadOnlyList? Options = null, string? Group = null);
+
+public sealed record SignalTableDef(
+ string Id,
+ string Title,
+ string Category,
+ string FileName,
+ IReadOnlyList Columns);
+
+///
+/// 读写 SimpleLite 工作目录 Config/Signal/*.json,供迷毂「数据中心」表格编辑。
+/// 表结构优先从 StandardScene.Signal.dll 反射;无插件时才读 signal-tables.json。
+///
+public sealed class SignalDataStore
+{
+ private static readonly JsonSerializerOptions FileJson = new()
+ {
+ WriteIndented = true,
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+ };
+
+ private static readonly object FileLock = new();
+
+ private readonly SimpleLiteLauncher _launcher;
+ private IReadOnlyList? _cachedTables;
+ private long _cachedSignature;
+
+ public SignalDataStore(SimpleLiteLauncher launcher) => _launcher = launcher;
+
+ public IReadOnlyList GetTables()
+ {
+ var sig = SignalTableManifestLoader.ComputeManifestSignature(_launcher);
+ if (_cachedTables == null || sig != _cachedSignature)
+ {
+ _cachedTables = SignalTableManifestLoader.Load(_launcher);
+ _cachedSignature = sig;
+ }
+
+ return _cachedTables;
+ }
+
+ public SignalTableDef? Find(string id) =>
+ GetTables().FirstOrDefault(t => string.Equals(t.Id, id, StringComparison.OrdinalIgnoreCase));
+
+ public string? ResolveWorkingDirectory() => _launcher.ResolveWorkingDirectory();
+
+ public (string? Path, string? Error) ResolveFile(SignalTableDef table, bool createDir)
+ {
+ var wd = _launcher.ResolveWorkingDirectory();
+ if (string.IsNullOrWhiteSpace(wd))
+ return (null, "未找到 SimpleLite 工作目录,无法定位 Config/Signal");
+
+ var dest = Path.GetFullPath(Path.Combine(wd, "Config", "Signal", table.FileName));
+ if (File.Exists(dest))
+ return (dest, null);
+
+ if (createDir)
+ {
+ try
+ {
+ var dir = Path.GetDirectoryName(dest);
+ if (!string.IsNullOrEmpty(dir))
+ Directory.CreateDirectory(dir);
+ }
+ catch (Exception ex)
+ {
+ return (null, ex.Message);
+ }
+ }
+
+ return (dest, null);
+ }
+
+ public JsonArray LoadRows(SignalTableDef table)
+ {
+ var (path, _) = ResolveFile(table, createDir: false);
+ if (path == null || !File.Exists(path))
+ return new JsonArray();
+
+ lock (FileLock)
+ {
+ var json = File.ReadAllText(path);
+ if (string.IsNullOrWhiteSpace(json))
+ return new JsonArray();
+ var node = JsonNode.Parse(json);
+ if (node is JsonArray arr)
+ return arr;
+ return new JsonArray();
+ }
+ }
+
+ public void SaveRows(SignalTableDef table, JsonArray rows)
+ {
+ var (path, error) = ResolveFile(table, createDir: true);
+ if (path == null)
+ throw new InvalidOperationException(error ?? "无法解析信号配置文件路径");
+
+ var dir = Path.GetDirectoryName(path);
+ if (!string.IsNullOrEmpty(dir))
+ Directory.CreateDirectory(dir);
+
+ lock (FileLock)
+ File.WriteAllText(path, rows.ToJsonString(FileJson));
+ }
+}
diff --git a/MiGu.Server/Signal/SignalModelSchemaResolver.cs b/MiGu.Server/Signal/SignalModelSchemaResolver.cs
new file mode 100644
index 0000000..e651f59
--- /dev/null
+++ b/MiGu.Server/Signal/SignalModelSchemaResolver.cs
@@ -0,0 +1,275 @@
+using System.ComponentModel;
+using System.Reflection;
+using System.Text.Json.Serialization;
+using StandardScene.Signal.Model;
+
+namespace MiGu.Server.Signal;
+
+///
+/// 从 StandardScene.Signal 程序集反射数据中心表和列。
+/// 优先使用 MiGu.Server 编译期引用的程序集;否则再从 SimpleLite plugins 加载。
+///
+public static class SignalModelSchemaResolver
+{
+ private const string ModelNamespace = "StandardScene.Signal.Model";
+ private const string AssemblyFileName = "StandardScene.Signal.dll";
+
+ public static string? FindAssemblyPath(string? pluginsDir)
+ {
+ if (string.IsNullOrWhiteSpace(pluginsDir))
+ return null;
+ var path = Path.Combine(pluginsDir, AssemblyFileName);
+ return File.Exists(path) ? path : null;
+ }
+
+ public static IReadOnlyList ResolveTables(string? pluginsDir, string? workingDirectory = null)
+ {
+ var assembly = TryGetSignalAssembly(pluginsDir, workingDirectory);
+ if (assembly == null)
+ return Array.Empty();
+
+ try
+ {
+ return BuildTablesFromAssembly(assembly);
+ }
+ catch
+ {
+ return Array.Empty();
+ }
+ }
+
+ public static IReadOnlyList ResolveColumns(string? modelName, string? pluginsDir, string? workingDirectory = null)
+ {
+ if (string.IsNullOrWhiteSpace(modelName))
+ return Array.Empty();
+
+ var assembly = TryGetSignalAssembly(pluginsDir, workingDirectory);
+ if (assembly == null)
+ return Array.Empty();
+
+ try
+ {
+ var type = assembly.GetType($"{ModelNamespace}.{modelName.Trim()}", throwOnError: false, ignoreCase: true);
+ if (type == null)
+ return Array.Empty();
+
+ return DiscoverColumns(type);
+ }
+ catch
+ {
+ return Array.Empty();
+ }
+ }
+
+ private static Assembly? TryGetSignalAssembly(string? pluginsDir, string? workingDirectory)
+ {
+ try
+ {
+ var referenced = typeof(PlcStationModel).Assembly;
+ if (HasModelTypes(referenced))
+ return referenced;
+ }
+ catch
+ {
+ /* 未引用插件工程时继续走 LoadFrom */
+ }
+
+ var assemblyPath = FindAssemblyPath(pluginsDir);
+ if (assemblyPath == null)
+ return null;
+
+ var probeDirs = BuildProbeDirs(pluginsDir, workingDirectory);
+ ResolveEventHandler? handler = null;
+ handler = (_, args) => ResolveAssembly(args.Name, probeDirs);
+ AppDomain.CurrentDomain.AssemblyResolve += handler;
+ try
+ {
+ var loaded = Assembly.LoadFrom(assemblyPath);
+ return HasModelTypes(loaded) ? loaded : null;
+ }
+ catch
+ {
+ return null;
+ }
+ finally
+ {
+ if (handler != null)
+ AppDomain.CurrentDomain.AssemblyResolve -= handler;
+ }
+ }
+
+ private static bool HasModelTypes(Assembly assembly)
+ {
+ return SafeGetTypes(assembly).Any(t =>
+ t != null && string.Equals(t.Namespace, ModelNamespace, StringComparison.Ordinal));
+ }
+
+ private static IReadOnlyList BuildProbeDirs(string? pluginsDir, string? workingDirectory)
+ {
+ var dirs = new List();
+ void Add(string? dir)
+ {
+ if (string.IsNullOrWhiteSpace(dir)) return;
+ var full = Path.GetFullPath(dir);
+ if (Directory.Exists(full) && !dirs.Contains(full, StringComparer.OrdinalIgnoreCase))
+ dirs.Add(full);
+ }
+
+ Add(pluginsDir);
+ Add(workingDirectory);
+ if (!string.IsNullOrWhiteSpace(pluginsDir))
+ Add(Path.GetDirectoryName(pluginsDir));
+ Add(AppContext.BaseDirectory);
+
+ return dirs;
+ }
+
+ private static Assembly? ResolveAssembly(string? assemblyName, IReadOnlyList probeDirs)
+ {
+ if (string.IsNullOrWhiteSpace(assemblyName))
+ return null;
+
+ string simpleName;
+ try
+ {
+ simpleName = new AssemblyName(assemblyName).Name ?? assemblyName;
+ }
+ catch
+ {
+ simpleName = assemblyName.Split(',')[0];
+ }
+
+ foreach (var dir in probeDirs)
+ {
+ var path = Path.Combine(dir, simpleName + ".dll");
+ if (!File.Exists(path))
+ continue;
+ try
+ {
+ return Assembly.LoadFrom(path);
+ }
+ catch
+ {
+ /* try next dir */
+ }
+ }
+
+ return null;
+ }
+
+ private static IReadOnlyList BuildTablesFromAssembly(Assembly assembly)
+ {
+ var found = new List<(int Order, SignalTableDef Table)>();
+ foreach (var type in SafeGetTypes(assembly))
+ {
+ if (type == null || !string.Equals(type.Namespace, ModelNamespace, StringComparison.Ordinal))
+ continue;
+
+ var attr = type.GetCustomAttributes(inherit: false)
+ .FirstOrDefault(a => a.GetType().Name == "SignalTableAttribute");
+ if (attr == null)
+ continue;
+
+ var attrType = attr.GetType();
+ var id = (attrType.GetProperty("Id")?.GetValue(attr) as string ?? "").Trim();
+ var fileName = (attrType.GetProperty("FileName")?.GetValue(attr) as string ?? "").Trim();
+ var title = (attrType.GetProperty("Title")?.GetValue(attr) as string ?? "").Trim();
+ var order = attrType.GetProperty("Order")?.GetValue(attr) as int? ?? 0;
+ if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(fileName))
+ continue;
+
+ var category = type.GetCustomAttribute()?.Category ?? "";
+ if (string.IsNullOrWhiteSpace(title))
+ title = type.GetCustomAttribute()?.DisplayName ?? id;
+
+ found.Add((order, new SignalTableDef(id, title, category, fileName, DiscoverColumns(type))));
+ }
+
+ return found.OrderBy(x => x.Order).ThenBy(x => x.Table.Id).Select(x => x.Table).ToList();
+ }
+
+ private static IEnumerable SafeGetTypes(Assembly assembly)
+ {
+ try
+ {
+ return assembly.GetTypes();
+ }
+ catch (ReflectionTypeLoadException ex)
+ {
+ return ex.Types.Where(t => t != null)!;
+ }
+ }
+
+ private static IReadOnlyList DiscoverColumns(Type type)
+ {
+ var list = new List();
+ foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(p => p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0)
+ .Where(p => p.GetCustomAttribute()?.Browsable != false)
+ .Where(p => !IsJsonIgnored(p))
+ .OrderBy(p => p.MetadataToken))
+ {
+ var label = prop.GetCustomAttribute()?.DisplayName;
+ if (string.IsNullOrWhiteSpace(label))
+ label = prop.Name;
+
+ var columnType = MapType(prop.PropertyType);
+ string[]? options = null;
+ var group = prop.GetCustomAttribute(inherit: false)?.Category;
+ if (string.IsNullOrWhiteSpace(group))
+ group = null;
+
+ var select = ReadSelectOptions(prop);
+ if (select is { Length: > 0 })
+ {
+ columnType = "enum";
+ options = select;
+ }
+ else if (columnType == "enum" && (Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType).IsEnum)
+ {
+ options = Enum.GetNames(Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
+ }
+
+ list.Add(new SignalColumn(prop.Name, label, columnType, options, group));
+ }
+
+ return list;
+ }
+
+ private static bool IsJsonIgnored(PropertyInfo prop)
+ {
+ if (prop.GetCustomAttribute() != null)
+ return true;
+
+ foreach (var attr in prop.GetCustomAttributes(inherit: true))
+ {
+ if (attr.GetType().FullName == "Newtonsoft.Json.JsonIgnoreAttribute")
+ return true;
+ }
+
+ return false;
+ }
+
+ private static string[]? ReadSelectOptions(PropertyInfo prop)
+ {
+ var attr = prop.GetCustomAttributes(inherit: false)
+ .FirstOrDefault(a => a.GetType().Name == "SignalSelectAttribute");
+ if (attr == null)
+ return null;
+ var options = attr.GetType().GetProperty("Options")?.GetValue(attr) as string[];
+ return options is { Length: > 0 } ? options : null;
+ }
+
+ private static string MapType(Type type)
+ {
+ var underlying = Nullable.GetUnderlyingType(type) ?? type;
+ if (underlying == typeof(bool))
+ return "bool";
+ if (underlying == typeof(int) || underlying == typeof(long) || underlying == typeof(short) ||
+ underlying == typeof(byte) || underlying == typeof(uint) || underlying == typeof(ulong))
+ return "int";
+ if (underlying.IsEnum)
+ return "enum";
+ return "string";
+ }
+}
diff --git a/MiGu.Server/Signal/SignalTableManifest.cs b/MiGu.Server/Signal/SignalTableManifest.cs
new file mode 100644
index 0000000..47e1d3a
--- /dev/null
+++ b/MiGu.Server/Signal/SignalTableManifest.cs
@@ -0,0 +1,40 @@
+namespace MiGu.Server.Signal;
+
+using System.Text.Json.Serialization;
+
+public sealed class SignalTableManifest
+{
+ public int Version { get; set; } = 1;
+
+ public List Tables { get; set; } = new();
+}
+
+public sealed class SignalTableManifestEntry
+{
+ public string Id { get; set; } = "";
+
+ public string Title { get; set; } = "";
+
+ public string Category { get; set; } = "";
+
+ public string FileName { get; set; } = "";
+
+ public string Model { get; set; } = "";
+
+ public List? Columns { get; set; }
+}
+
+public sealed class SignalTableColumnEntry
+{
+ public string Key { get; set; } = "";
+
+ public string Label { get; set; } = "";
+
+ public string Type { get; set; } = "string";
+
+ [JsonPropertyName("options")]
+ public string[]? Options { get; set; }
+
+ [JsonPropertyName("group")]
+ public string? Group { get; set; }
+}
diff --git a/MiGu.Server/Signal/SignalTableManifestLoader.cs b/MiGu.Server/Signal/SignalTableManifestLoader.cs
new file mode 100644
index 0000000..016bcbd
--- /dev/null
+++ b/MiGu.Server/Signal/SignalTableManifestLoader.cs
@@ -0,0 +1,174 @@
+using System.Text.Json;
+using MiGu.Server.Launcher;
+
+namespace MiGu.Server.Signal;
+
+///
+/// 优先从 plugins/StandardScene.Signal.dll 反射表和列(Model 上的 SignalTable / DisplayName)。
+/// 没有插件 DLL 时才读 signal-tables.json 或内置清单。
+///
+public static class SignalTableManifestLoader
+{
+ private const string ManifestFileName = "signal-tables.json";
+
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true
+ };
+
+ public static IReadOnlyList Load(SimpleLiteLauncher launcher)
+ {
+ var workingDirectory = launcher.ResolveWorkingDirectory();
+ var pluginsDir = launcher.ResolvePluginsDir();
+ var fromPlugin = SignalModelSchemaResolver.ResolveTables(pluginsDir, workingDirectory);
+ if (fromPlugin.Count > 0)
+ return fromPlugin;
+
+ var manifest = TryLoadManifest(launcher);
+ return manifest.Tables
+ .Where(t => !string.IsNullOrWhiteSpace(t.Id) && !string.IsNullOrWhiteSpace(t.FileName))
+ .Select(e => ToDef(e, pluginsDir, workingDirectory))
+ .ToList();
+ }
+
+ public static long ComputeManifestSignature(SimpleLiteLauncher launcher)
+ {
+ long sig = 0;
+ foreach (var path in ResolveManifestPaths(launcher))
+ {
+ if (!File.Exists(path)) continue;
+ try
+ {
+ var info = new FileInfo(path);
+ sig ^= info.LastWriteTimeUtc.Ticks;
+ sig ^= info.Length;
+ }
+ catch { /* ignore */ }
+ }
+
+ var pluginsDir = launcher.ResolvePluginsDir();
+ var dll = pluginsDir == null ? null : Path.Combine(pluginsDir, "StandardScene.Signal.dll");
+ if (dll != null && File.Exists(dll))
+ {
+ try
+ {
+ var info = new FileInfo(dll);
+ sig ^= info.LastWriteTimeUtc.Ticks;
+ }
+ catch { /* ignore */ }
+ }
+
+ return sig;
+ }
+
+ private static SignalTableManifest TryLoadManifest(SimpleLiteLauncher launcher)
+ {
+ foreach (var path in ResolveManifestPaths(launcher))
+ {
+ if (!File.Exists(path)) continue;
+ try
+ {
+ var json = File.ReadAllText(path);
+ var manifest = JsonSerializer.Deserialize(json, JsonOptions);
+ if (manifest?.Tables is { Count: > 0 })
+ return manifest;
+ }
+ catch { /* try next */ }
+ }
+
+ return JsonSerializer.Deserialize(EmbeddedFallbackJson, JsonOptions)
+ ?? new SignalTableManifest();
+ }
+
+ private static IEnumerable ResolveManifestPaths(SimpleLiteLauncher launcher)
+ {
+ var wd = launcher.ResolveWorkingDirectory();
+ if (!string.IsNullOrWhiteSpace(wd))
+ yield return Path.Combine(wd, "Config", "Signal", ManifestFileName);
+
+ var plugins = launcher.ResolvePluginsDir();
+ if (string.IsNullOrWhiteSpace(plugins)) yield break;
+
+ yield return Path.Combine(plugins, ManifestFileName);
+ yield return Path.Combine(plugins, "Config", "Signal", ManifestFileName);
+ }
+
+ private static SignalTableDef ToDef(SignalTableManifestEntry entry, string? pluginsDir, string? workingDirectory)
+ {
+ var columns = BuildColumns(entry, pluginsDir, workingDirectory);
+ return new SignalTableDef(
+ entry.Id.Trim(),
+ string.IsNullOrWhiteSpace(entry.Title) ? entry.Id.Trim() : entry.Title.Trim(),
+ entry.Category ?? "",
+ entry.FileName.Trim(),
+ columns);
+ }
+
+ private static IReadOnlyList BuildColumns(
+ SignalTableManifestEntry entry,
+ string? pluginsDir,
+ string? workingDirectory)
+ {
+ var reflected = SignalModelSchemaResolver.ResolveColumns(entry.Model, pluginsDir, workingDirectory);
+ if (reflected.Count > 0)
+ return reflected;
+
+ if (entry.Columns is { Count: > 0 })
+ {
+ return entry.Columns
+ .Where(c => !string.IsNullOrWhiteSpace(c.Key))
+ .Select(c => new SignalColumn(
+ c.Key.Trim(),
+ string.IsNullOrWhiteSpace(c.Label) ? c.Key.Trim() : c.Label.Trim(),
+ string.IsNullOrWhiteSpace(c.Type) ? "string" : c.Type.Trim(),
+ c.Options,
+ string.IsNullOrWhiteSpace(c.Group) ? null : c.Group.Trim()))
+ .ToList();
+ }
+
+ return Array.Empty();
+ }
+
+ private const string EmbeddedFallbackJson = """
+ {
+ "version": 1,
+ "tables": [
+ {
+ "id": "stations",
+ "title": "PLC机构",
+ "category": "PLC数据管理",
+ "fileName": "stations.json",
+ "model": "PlcStationModel"
+ },
+ {
+ "id": "docks",
+ "title": "机构工位",
+ "category": "PLC数据管理",
+ "fileName": "station-docks.json",
+ "model": "PlcStationDockModel"
+ },
+ {
+ "id": "handshake",
+ "title": "握手点",
+ "category": "握手点数据管理",
+ "fileName": "handshake-points.json",
+ "model": "HandshakePointModel"
+ },
+ {
+ "id": "release",
+ "title": "放行点",
+ "category": "放行点数据管理",
+ "fileName": "release-points.json",
+ "model": "ReleasePointModel"
+ },
+ {
+ "id": "mag-control",
+ "title": "磁条管控区",
+ "category": "磁条交管",
+ "fileName": "mag-control-areas.json",
+ "model": "MagControlAreaModel"
+ }
+ ]
+ }
+ """;
+}
diff --git a/frontends/apps/simple-platform-vue/src/api/signalData.ts b/frontends/apps/simple-platform-vue/src/api/signalData.ts
new file mode 100644
index 0000000..2fd9cc4
--- /dev/null
+++ b/frontends/apps/simple-platform-vue/src/api/signalData.ts
@@ -0,0 +1,69 @@
+import http from './http'
+
+const MOCK = import.meta.env.VITE_USE_MOCK === 'true'
+
+export interface SignalColumn {
+ key: string
+ label: string
+ type: 'string' | 'int' | 'bool' | 'enum' | string
+ options?: string[] | null
+ group?: string | null
+}
+
+export interface SignalTableSummary {
+ id: string
+ title: string
+ category: string
+ fileName: string
+}
+
+export interface SignalTableDto extends SignalTableSummary {
+ exists: boolean
+ error?: string | null
+ columns: SignalColumn[]
+ rows: Record[]
+}
+
+export interface SignalDataListDto {
+ signalEnabled: boolean
+ workingDirectory?: string | null
+ tables: SignalTableSummary[]
+}
+
+export async function listSignalTables(): Promise {
+ if (MOCK) {
+ return {
+ signalEnabled: true,
+ workingDirectory: 'D:\\工作\\stand\\SimpleLite',
+ tables: [
+ { id: 'stations', title: 'PLC机构', category: 'PLC数据管理', fileName: 'stations.json' },
+ { id: 'docks', title: '机构工位', category: 'PLC数据管理', fileName: 'station-docks.json' },
+ { id: 'handshake', title: '握手点', category: '握手点数据管理', fileName: 'handshake-points.json' },
+ { id: 'release', title: '放行点', category: '放行点数据管理', fileName: 'release-points.json' },
+ { id: 'mag-control', title: '磁条管控区', category: '磁条交管', fileName: 'mag-control-areas.json' }
+ ]
+ }
+ }
+ const { data } = await http.get('/signal-data', { params: { summary: true } })
+ return data
+}
+
+export async function getSignalTable(id: string): Promise {
+ if (MOCK) {
+ const list = await listSignalTables()
+ const meta = list.tables.find((t) => t.id === id)
+ if (!meta) throw new Error(`未知数据表:${id}`)
+ return { ...meta, exists: true, columns: [], rows: [] }
+ }
+ const { data } = await http.get(`/signal-data/${id}`)
+ return data
+}
+
+export async function saveSignalTable(id: string, rows: Record[]): Promise {
+ if (MOCK) {
+ const t = await getSignalTable(id)
+ return { ...t, rows: JSON.parse(JSON.stringify(rows)) }
+ }
+ const { data } = await http.put(`/signal-data/${id}`, { rows })
+ return data
+}
diff --git a/frontends/apps/simple-platform-vue/src/api/wizard.ts b/frontends/apps/simple-platform-vue/src/api/wizard.ts
index 95ade2d..a85c35a 100644
--- a/frontends/apps/simple-platform-vue/src/api/wizard.ts
+++ b/frontends/apps/simple-platform-vue/src/api/wizard.ts
@@ -29,6 +29,25 @@ const MOCK_OPTIONS: WizardOptions = {
}
}
+const NAV_SCENE: Record = {
+ magnetic: 'scene.mag',
+ qrcode: 'scene.qrlidar',
+ laser: 'scene.qrlidar'
+}
+
+function toLauncherSceneIds(kinds: string[]): string[] {
+ const result: string[] = []
+ for (const k of kinds) {
+ const id = NAV_SCENE[k] ?? `scene.${k}`
+ if (!result.includes(id)) result.push(id)
+ }
+ if (kinds.some((k) => k.toLowerCase() === 'magnetic') && !result.includes('scene.signal')) {
+ result.push('scene.signal')
+ }
+ if (result.length > 0 && !result.includes('scene.device')) result.push('scene.device')
+ return result
+}
+
let mockProfile: DeploymentProfileDto = {
configured: false,
platformType: 'standard',
@@ -61,7 +80,7 @@ export async function saveWizardProfile(req: SaveWizardRequest): Promise `scene.${k}`)
+ activeSceneIds: toLauncherSceneIds(req.navigationKinds ?? [])
}
return mockProfile
}
diff --git a/frontends/apps/simple-platform-vue/src/config/navMenu.ts b/frontends/apps/simple-platform-vue/src/config/navMenu.ts
index 1bfd9ae..10cc879 100644
--- a/frontends/apps/simple-platform-vue/src/config/navMenu.ts
+++ b/frontends/apps/simple-platform-vue/src/config/navMenu.ts
@@ -7,6 +7,7 @@ import {
DocumentCopy,
EditPen,
Files,
+ Grid,
Histogram,
Link,
List,
@@ -59,6 +60,16 @@ export const ADMIN_MENU: NavMenuItem[] = [
{ path: '/admin/simple-fields', label: '字段管理', icon: Collection, key: 'admin-simple-fields', group: '设计与编辑' }
]
},
+ {
+ path: '/admin/data-center', label: '数据中心', icon: Grid, key: 'admin-data-center', group: '数据中心',
+ children: [
+ { path: '/admin/data-center/stations', label: 'PLC机构', icon: Cpu, key: 'admin-data-center', group: '数据中心' },
+ { path: '/admin/data-center/docks', label: '机构工位', icon: Connection, key: 'admin-data-center', group: '数据中心' },
+ { path: '/admin/data-center/handshake', label: '握手点', icon: Connection, key: 'admin-data-center', group: '数据中心' },
+ { path: '/admin/data-center/release', label: '放行点', icon: Promotion, key: 'admin-data-center', group: '数据中心' },
+ { path: '/admin/data-center/mag-control', label: '磁条管控区', icon: Operation, key: 'admin-data-center', group: '数据中心' }
+ ]
+ },
{
path: '/admin/config', label: '平台配置中心', icon: Setting, group: '平台配置中心',
children: [
@@ -84,8 +95,12 @@ export const MONITOR_MENU: NavMenuItem[] = [
export function flattenNavMenu(items: NavMenuItem[]): NavMenuItem[] {
const out: NavMenuItem[] = []
for (const item of items) {
- if (item.children?.length) out.push(...flattenNavMenu(item.children))
- else if (item.key) out.push(item)
+ if (item.children?.length) {
+ if (item.key) out.push({ ...item, children: undefined })
+ out.push(...flattenNavMenu(item.children))
+ } else if (item.key) {
+ out.push(item)
+ }
}
return out
}
diff --git a/frontends/apps/simple-platform-vue/src/config/quickEntries.ts b/frontends/apps/simple-platform-vue/src/config/quickEntries.ts
index a71ae8a..0ed788c 100644
--- a/frontends/apps/simple-platform-vue/src/config/quickEntries.ts
+++ b/frontends/apps/simple-platform-vue/src/config/quickEntries.ts
@@ -107,7 +107,7 @@ function buildCatalog(scope: Scope): Map {
const menu = scope === 'RCSMonitor' ? MONITOR_MENU : ADMIN_MENU
for (const item of flattenNavMenu(menu)) {
const q = menuItemToQuick(item)
- if (q) map.set(q.key, q)
+ if (q && !map.has(q.key)) map.set(q.key, q)
}
return map
}
diff --git a/frontends/apps/simple-platform-vue/src/router/index.ts b/frontends/apps/simple-platform-vue/src/router/index.ts
index 4521811..01d132a 100644
--- a/frontends/apps/simple-platform-vue/src/router/index.ts
+++ b/frontends/apps/simple-platform-vue/src/router/index.ts
@@ -41,6 +41,13 @@ const routes: RouteRecordRaw[] = [
{ path: 'task-templates', name: 'admin-task-templates', component: () => import('@/views/admin/TaskTemplateView.vue'), meta: { title: '任务编排' } },
{ path: 'simple-fields', name: 'admin-simple-fields', component: () => import('@/views/admin/SimpleFieldManagementView.vue'), meta: { title: '字段管理' } },
{ path: 'project-properties', name: 'admin-project-properties', component: () => import('@/views/admin/ProjectPropertiesView.vue'), meta: { title: '项目属性' } },
+ { path: 'data-center', redirect: '/admin/data-center/stations' },
+ {
+ path: 'data-center/:tableId',
+ name: 'admin-data-center',
+ component: () => import('@/views/admin/DataCenterView.vue'),
+ meta: { title: '数据中心' }
+ },
// ── 平台配置中心:聚合页 + 独立业务页(page key = route.name,对齐后端 PageCatalog)。 ──
{ path: 'config/strategy', name: 'admin-config-strategy', component: () => import('@/views/admin/config/StrategyConfigView.vue'), meta: { title: '调度策略' } },
{ path: 'config/vehicle-hub', name: 'admin-vehicle-hub', component: () => import('@/views/shared/VehicleHubView.vue'), meta: { title: '车辆运维' } },
diff --git a/frontends/apps/simple-platform-vue/src/stores/auth.ts b/frontends/apps/simple-platform-vue/src/stores/auth.ts
index f2bb522..57caab9 100644
--- a/frontends/apps/simple-platform-vue/src/stores/auth.ts
+++ b/frontends/apps/simple-platform-vue/src/stores/auth.ts
@@ -185,6 +185,23 @@ export const useAuthStore = defineStore('auth', {
/** 向导保存成功后调用:清掉 needsWizard,避免守卫再次把用户导回 /wizard。 */
markWizardDone() {
this.needsWizard = false
+ },
+ /** 向导保存后刷新 allowedPages(数据中心等按选型裁剪的页),失败不登出。 */
+ async refreshPermissions() {
+ try {
+ const me = await apiGetMe()
+ this.user = me.user
+ this.scope = me.scope
+ this.runMode = me.runMode
+ this.effectivePermissions = me.effectivePermissions
+ this.needsWizard = me.needsWizard ?? false
+ localStorage.setItem(USER_KEY, JSON.stringify(me.user))
+ localStorage.setItem(SCOPE_KEY, me.scope)
+ localStorage.setItem(RUN_MODE_KEY, me.runMode)
+ localStorage.setItem(PERM_KEY, JSON.stringify(me.effectivePermissions))
+ } catch {
+ /* 保存已成功,权限下次进页 / 刷新再对齐 */
+ }
}
}
})
diff --git a/frontends/apps/simple-platform-vue/src/types/wizard.ts b/frontends/apps/simple-platform-vue/src/types/wizard.ts
index 3c84ad3..b617f80 100644
--- a/frontends/apps/simple-platform-vue/src/types/wizard.ts
+++ b/frontends/apps/simple-platform-vue/src/types/wizard.ts
@@ -47,7 +47,7 @@ export interface DeploymentProfileDto {
navigationKinds: string[]
scenarios: string[]
updatedBy: string
- /** 由导航选型推导的 SimpleLite 激活场景 id(如 scene.magnetic)。 */
+ /** 由导航选型推导的 SimpleLite 激活场景 id(如 scene.mag / scene.qrlidar / scene.signal)。 */
activeSceneIds: string[]
/** 被部署画像裁剪隐藏的页面 Key。 */
hiddenPages: string[]
diff --git a/frontends/apps/simple-platform-vue/src/views/WizardView.vue b/frontends/apps/simple-platform-vue/src/views/WizardView.vue
index 93e8338..f5c5bcd 100644
--- a/frontends/apps/simple-platform-vue/src/views/WizardView.vue
+++ b/frontends/apps/simple-platform-vue/src/views/WizardView.vue
@@ -117,11 +117,22 @@ const scenarioTemplates = computed(() => options.value?.
// 导航方式 → 内核场景 id 预览(与后端 DeploymentProfile.NavKindToSceneId 对齐)。
const NAV_SCENE: Record = {
- magnetic: 'scene.magnetic',
- qrcode: 'scene.qrcode',
- laser: 'scene.laser'
+ magnetic: 'scene.mag',
+ qrcode: 'scene.qrlidar',
+ laser: 'scene.qrlidar'
}
-const activeScenes = computed(() => sel.navigationKinds.map((k) => NAV_SCENE[k] ?? `scene.${k}`))
+const activeScenes = computed(() => {
+ const scenes: string[] = []
+ for (const k of sel.navigationKinds) {
+ const id = NAV_SCENE[k] ?? `scene.${k}`
+ if (!scenes.includes(id)) scenes.push(id)
+ }
+ if (sel.navigationKinds.includes('magnetic') && !scenes.includes('scene.signal')) {
+ scenes.push('scene.signal')
+ }
+ if (scenes.length > 0 && !scenes.includes('scene.device')) scenes.push('scene.device')
+ return scenes
+})
const canSave = computed(() => sel.navigationKinds.length > 0)
@@ -161,6 +172,7 @@ async function save() {
scenarios: sel.scenarios
})
auth.markWizardDone()
+ await auth.refreshPermissions()
ElMessage.success('部署配置已保存')
router.push(auth.scope === 'RCSMonitor' ? '/monitor/map' : '/admin/dashboard')
} catch (e) {
diff --git a/frontends/apps/simple-platform-vue/src/views/admin/DataCenterView.vue b/frontends/apps/simple-platform-vue/src/views/admin/DataCenterView.vue
new file mode 100644
index 0000000..1005edb
--- /dev/null
+++ b/frontends/apps/simple-platform-vue/src/views/admin/DataCenterView.vue
@@ -0,0 +1,266 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ truthy(row[col.key]) ? '是' : '否' }}
+
+ {{ formatCell(row[col.key]) }}
+
+
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+ {{ group.name }}
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+
+