diff --git a/MiGu.Server/Auth/PageCatalog.cs b/MiGu.Server/Auth/PageCatalog.cs index 98a4676..96b80dd 100644 --- a/MiGu.Server/Auth/PageCatalog.cs +++ b/MiGu.Server/Auth/PageCatalog.cs @@ -29,6 +29,7 @@ public static class PageCatalog { // ── 管理端 / Platform:概览 ── new("admin-dashboard", "总览", "概览", ScopePlatform), + new("admin-setup", "初始配置", "概览", ScopePlatform), new("admin-map-monitor", "地图监控", "概览", ScopePlatform), new("admin-tasks", "任务管理", "概览", ScopePlatform), new("admin-alarms", "报警管理", "概览", ScopePlatform), @@ -44,7 +45,7 @@ public static class PageCatalog new("admin-task-templates", "任务编排", "设计与编排", ScopePlatform), new("admin-simple-fields", "字段管理", "设计与编排", ScopePlatform), - // ── 管理端 / Platform:数据中心(scene.signal,磁导航选型时显示) ── + // ── 管理端 / Platform:数据中心(scene.signal,选 SPS / Pack 时显示) ── new("admin-data-center", "数据中心", "数据中心", ScopePlatform), // ── 管理端 / Platform:平台配置中心(聚合页,每个 Key 对齐前端聚合路由 route.name) ── diff --git a/MiGu.Server/Auth/RbacStore.cs b/MiGu.Server/Auth/RbacStore.cs index 32a5cdd..1b3ca46 100644 --- a/MiGu.Server/Auth/RbacStore.cs +++ b/MiGu.Server/Auth/RbacStore.cs @@ -126,6 +126,12 @@ public sealed class RbacStore && !r.Pages.Contains("admin-simple-fields", StringComparer.OrdinalIgnoreCase) && r.Pages.Contains("admin-task-templates", StringComparer.OrdinalIgnoreCase)) r.Pages.Add("admin-simple-fields"); + + if (!r.Pages.Contains(PageCatalog.Wildcard) + && !r.Pages.Contains("admin-setup", StringComparer.OrdinalIgnoreCase) + && r.Pages.Contains("admin-cars", StringComparer.OrdinalIgnoreCase) + && r.Pages.Contains("admin-maps", StringComparer.OrdinalIgnoreCase)) + r.Pages.Add("admin-setup"); } private RbacSnapshot SeedDefault(IConfiguration config) diff --git a/MiGu.Server/Configs/DeploymentCatalog.cs b/MiGu.Server/Configs/DeploymentCatalog.cs index c1acef8..7434e75 100644 --- a/MiGu.Server/Configs/DeploymentCatalog.cs +++ b/MiGu.Server/Configs/DeploymentCatalog.cs @@ -38,8 +38,8 @@ public static class DeploymentCatalog }; /// - /// 场景插件 → 平台页面 Key。磁导航选型会并入 , - /// 从而点亮「数据中心」菜单(PLC 握手 + 磁条交管配置)。 + /// 场景插件 → 平台页面 Key。选 SPS / Pack 时 ToLauncherSceneIds 会并入 + /// ,从而点亮「数据中心」。 /// public static readonly IReadOnlyDictionary SceneToPages = new Dictionary(StringComparer.OrdinalIgnoreCase) diff --git a/MiGu.Server/Configs/DeploymentProfile.cs b/MiGu.Server/Configs/DeploymentProfile.cs index 1436c89..44887e8 100644 --- a/MiGu.Server/Configs/DeploymentProfile.cs +++ b/MiGu.Server/Configs/DeploymentProfile.cs @@ -65,23 +65,32 @@ public record DeploymentProfile( return result; } - /// 信号交互场景 id(PLC 握手 + 磁条交管)。 + /// 信号交互场景 id(PLC 握手)。仅 SPS / Pack 业务场景时加载。 public const string SignalSceneId = "scene.signal"; /// 设备驱动场景 id(门/充电桩等,各类项目通用)。 public const string DeviceSceneId = "scene.device"; + public const string ScenarioSps = "tpl-sps"; + public const string ScenarioPack = "tpl-pack"; + /// 是否选了磁导航。 public bool UsesMagnetic() => NavigationKinds?.Any(k => string.Equals(k, "magnetic", StringComparison.OrdinalIgnoreCase)) == true; + /// 是否选了需要 scene.signal 的业务场景(SPS 物料配送 / 电池 Pack 产线)。 + public bool UsesSignalPlugin() => + Scenarios?.Any(s => + string.Equals(s, ScenarioSps, StringComparison.OrdinalIgnoreCase) + || string.Equals(s, ScenarioPack, StringComparison.OrdinalIgnoreCase)) == true; + /// - /// 写入 active-scenes.json 的完整场景集合:导航插件 + 磁导航时并入 scene.signal + scene.device。 + /// 写入 active-scenes.json 的完整场景集合:导航插件 + SPS/Pack 时并入 scene.signal + scene.device。 /// public IReadOnlyList ToLauncherSceneIds() { var result = ToActiveSceneIds().ToList(); - if (UsesMagnetic() && !result.Contains(SignalSceneId, StringComparer.OrdinalIgnoreCase)) + if (UsesSignalPlugin() && !result.Contains(SignalSceneId, StringComparer.OrdinalIgnoreCase)) result.Add(SignalSceneId); if (!result.Contains(DeviceSceneId, StringComparer.OrdinalIgnoreCase)) result.Add(DeviceSceneId); diff --git a/MiGu.Server/Controllers/WizardController.cs b/MiGu.Server/Controllers/WizardController.cs index cc076d2..a553c36 100644 --- a/MiGu.Server/Controllers/WizardController.cs +++ b/MiGu.Server/Controllers/WizardController.cs @@ -84,7 +84,7 @@ public class WizardController : ControllerBase // 平台 → 内核联动:把导航选型写入 SimpleLite 的 plugins/active-scenes.json(下次启动选择性加载; // 已运行实例可由前端再调 POST /api/sl/projection/scenes/apply 触发增量 reload)。 // scene.device(门/充电桩/按钮盒驱动)为各类项目通用能力,向导暂无独立选项,固定并入激活集合; - // 磁导航选型时 ToLauncherSceneIds 会并入 scene.signal(PLC 握手 + 磁条交管)。 + // 选 SPS / Pack 时 ToLauncherSceneIds 会并入 scene.signal(PLC 握手)。 var sceneIds = profile.ToLauncherSceneIds(); var write = _launcher.WriteActiveScenes(sceneIds, alwaysLoad: null, source: "deployment-profile"); diff --git a/MiGu.Server/Dashboard/DashboardShortcutCatalog.cs b/MiGu.Server/Dashboard/DashboardShortcutCatalog.cs index e416095..eddd773 100644 --- a/MiGu.Server/Dashboard/DashboardShortcutCatalog.cs +++ b/MiGu.Server/Dashboard/DashboardShortcutCatalog.cs @@ -26,6 +26,7 @@ public static class DashboardShortcutCatalog private static readonly ShortcutDef[] PlatformShortcuts = [ new("admin-dashboard", "admin-dashboard", PageCatalog.ScopePlatform), + new("admin-setup", "admin-setup", PageCatalog.ScopePlatform), new("admin-map-monitor", "admin-map-monitor", PageCatalog.ScopePlatform), new("admin-maps", "admin-maps", PageCatalog.ScopePlatform), new("admin-map-editor", "admin-map-editor", PageCatalog.ScopePlatform), diff --git a/MiGu.Server/appsettings.Development.json b/MiGu.Server/appsettings.Development.json index 4d88230..f3100fa 100644 --- a/MiGu.Server/appsettings.Development.json +++ b/MiGu.Server/appsettings.Development.json @@ -11,8 +11,8 @@ }, "SimpleLite": { "Enabled": true, - "ExecutablePath": "D:\\Code\\Products\\MIGU2.0\\SimpleLite\\SimpleLite.exe", - "WorkingDirectory": "D:\\Code\\Products\\MIGU2.0\\SimpleLite", + "ExecutablePath": "D:\\工作\\stand\\SimpleLite\\SimpleLite.exe", + "WorkingDirectory": "D:\\工作\\stand\\SimpleLite", "ProjectionPort": 8222, "ReadinessTimeoutMs": 8000, "FollowParent": false diff --git a/MiGu.Server/appsettings.json b/MiGu.Server/appsettings.json index c0c9dac..e75c225 100644 --- a/MiGu.Server/appsettings.json +++ b/MiGu.Server/appsettings.json @@ -25,8 +25,8 @@ "_comment_SimpleLite": "登录后拉起 SimpleLite;改路径请编辑下方 SimpleLite 节点。诊断: http://localhost:8080/api/health/simplelite", "SimpleLite": { "Enabled": true, - "ExecutablePath": "D:\\Code\\Products\\MIGU2.0\\SimpleLite\\SimpleLite.exe", - "WorkingDirectory": "D:\\Code\\Products\\MIGU2.0\\SimpleLite", + "ExecutablePath": "D:\\工作\\stand\\SimpleLite\\SimpleLite.exe", + "WorkingDirectory": "D:\\工作\\stand\\SimpleLite", "Arguments": "", "ReadinessTimeoutMs": 15000, "ReadinessPollIntervalMs": 250, diff --git a/frontends/apps/simple-platform-vue/src/api/setup.ts b/frontends/apps/simple-platform-vue/src/api/setup.ts new file mode 100644 index 0000000..632bc63 --- /dev/null +++ b/frontends/apps/simple-platform-vue/src/api/setup.ts @@ -0,0 +1,98 @@ +import { listCars, listSites, listTracks } from '@/api/projection' +import { reflectionApi } from '@/api/reflection' +import { getWizardProfile } from '@/api/wizard' +import type { SetupCarParamRow, SetupStatus } from '@/types/setup' + +const IP_KEYS = ['address', 'ip'] +const PORT_KEYS = ['port', 'magport'] + +function pick(rows: Array<{ key: string; value: string }>, keys: string[]): string { + const set = new Set(keys) + const hit = rows.find((r) => set.has(r.key.toLowerCase())) + return (hit?.value ?? '').trim() +} + +function ipOk(v: string): boolean { + return v.length > 0 && v !== '0.0.0.0' +} + +function portOk(v: string): boolean { + const n = Number(v) + return Number.isFinite(n) && n > 0 && n <= 65535 +} + +async function inspectCarParams(rawId: number, name: string): Promise { + try { + const fields = await reflectionApi.getFields('car', rawId) + const address = pick(fields, IP_KEYS) + const port = pick(fields, PORT_KEYS) + return { id: rawId, name, address, port, paramsReady: ipOk(address) && portOk(port) } + } catch { + return { id: rawId, name, address: '', port: '', paramsReady: false } + } +} + +export async function loadSetupStatus(): Promise { + const empty: SetupStatus = { + carCount: 0, + carsWithParams: 0, + siteCount: 0, + trackCount: 0, + carsReady: false, + mapsReady: false, + incomplete: true, + cars: [], + navigationKinds: [], + scenarios: [], + modules: [] + } + + try { + const [cars, sites, tracks, profile] = await Promise.all([ + listCars().catch(() => []), + listSites().catch(() => []), + listTracks().catch(() => []), + getWizardProfile().catch(() => null) + ]) + + const inspected = await Promise.all( + cars.slice(0, 40).map((c) => { + const id = c.rawId ?? Number(String(c.id).replace(/^C/i, '')) + if (!Number.isFinite(id) || id <= 0) { + return Promise.resolve({ + id: 0, + name: c.name, + address: c.address ?? c.ip ?? '', + port: '', + paramsReady: ipOk(c.address ?? c.ip ?? '') + } satisfies SetupCarParamRow) + } + return inspectCarParams(id, c.name) + }) + ) + + const carsWithParams = inspected.filter((c) => c.paramsReady).length + const couldReadParams = inspected.some((c) => c.address || c.port || c.paramsReady) + const carsReady = cars.length >= 1 && (!couldReadParams || carsWithParams >= 1) + const mapsReady = sites.length >= 1 && tracks.length >= 1 + + return { + carCount: cars.length, + carsWithParams, + siteCount: sites.length, + trackCount: tracks.length, + carsReady, + mapsReady, + incomplete: !(carsReady && mapsReady), + cars: inspected, + navigationKinds: profile?.navigationKinds ?? [], + scenarios: profile?.scenarios ?? [], + modules: profile?.modules ?? [] + } + } catch (e) { + return { + ...empty, + error: e instanceof Error ? e.message : String(e) + } + } +} diff --git a/frontends/apps/simple-platform-vue/src/api/wizard.ts b/frontends/apps/simple-platform-vue/src/api/wizard.ts index a85c35a..a75a749 100644 --- a/frontends/apps/simple-platform-vue/src/api/wizard.ts +++ b/frontends/apps/simple-platform-vue/src/api/wizard.ts @@ -35,15 +35,14 @@ const NAV_SCENE: Record = { laser: 'scene.qrlidar' } -function toLauncherSceneIds(kinds: string[]): string[] { +function toLauncherSceneIds(kinds: string[], scenarios: 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') - } + const wantSignal = scenarios.some((s) => s === 'tpl-sps' || s === 'tpl-pack') + if (wantSignal && !result.includes('scene.signal')) result.push('scene.signal') if (result.length > 0 && !result.includes('scene.device')) result.push('scene.device') return result } @@ -80,7 +79,7 @@ export async function saveWizardProfile(req: SaveWizardRequest): Promise + + + + + + + + diff --git a/frontends/apps/simple-platform-vue/src/config/navMenu.ts b/frontends/apps/simple-platform-vue/src/config/navMenu.ts index 10cc879..29f2d2d 100644 --- a/frontends/apps/simple-platform-vue/src/config/navMenu.ts +++ b/frontends/apps/simple-platform-vue/src/config/navMenu.ts @@ -38,6 +38,7 @@ export interface NavMenuItem { export const ADMIN_MENU: NavMenuItem[] = [ { path: '/admin/dashboard', label: '总览', icon: Histogram, key: 'admin-dashboard', group: '概览' }, + { path: '/admin/setup', label: '初始配置', icon: SetUp, key: 'admin-setup', group: '概览' }, { path: '/admin/operations', label: '运营管理', icon: Monitor, group: '概览', children: [ diff --git a/frontends/apps/simple-platform-vue/src/layouts/AppShell.vue b/frontends/apps/simple-platform-vue/src/layouts/AppShell.vue index 24525c2..51895d5 100644 --- a/frontends/apps/simple-platform-vue/src/layouts/AppShell.vue +++ b/frontends/apps/simple-platform-vue/src/layouts/AppShell.vue @@ -80,6 +80,7 @@