diff --git a/MiGu.Server/Configs/DeploymentCatalog.cs b/MiGu.Server/Configs/DeploymentCatalog.cs index ed31c68..424f925 100644 --- a/MiGu.Server/Configs/DeploymentCatalog.cs +++ b/MiGu.Server/Configs/DeploymentCatalog.cs @@ -27,11 +27,14 @@ public static class DeploymentCatalog new Option("ptl", "PTL 拣选系统", "module", "Pick-to-Light 亮灯拣选与播种"), }; - /// 功能模块 → PageCatalog 页面 Key 列表的映射。PTL 暂无专属配置页,未纳入映射(选中不影响菜单)。 + /// + /// 功能模块 → PageCatalog 页面 Key 列表的映射。PTL 暂无专属配置页,未纳入映射(选中不影响菜单)。 + /// 注意:Key 必须是 PageCatalog 当前有效 Key(不能用 LegacyKeyAliases 里的旧名,否则裁剪悄然失效)。 + /// public static readonly IReadOnlyDictionary ModuleToPages = new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["wms"] = new[] { "admin-config-location" }, + ["wms"] = new[] { "admin-config-facility" }, }; /// 所有「可被选型控制」的页面 Key(Module → 页 映射值的并集)。 diff --git a/MiGu.Server/Controllers/ConfigController.cs b/MiGu.Server/Controllers/ConfigController.cs index 0ae4897..ca5010b 100644 --- a/MiGu.Server/Controllers/ConfigController.cs +++ b/MiGu.Server/Controllers/ConfigController.cs @@ -6,7 +6,7 @@ using MiGu.Server.Configs; namespace MiGu.Server.Controllers; // AR-4: 全 class 加 [Authorize] —— 替代会话21 点名的「ConfigController 无鉴权 PUT 任意 section」漏洞。 -// GET (List/Get) 只要登录就放;PUT 强制 PlatformScope,避免运营人员误改业务配置。 +// GET (List/Get) 只要登录就放;PUT 按 scope 收紧:Platform 任意节,RCSMonitor 仅 ops 白名单。 [ApiController] [Authorize] [Route("api/config")] @@ -47,15 +47,24 @@ public class ConfigController : ControllerBase }); } - // 配置中心页面已有 PermissionGuard;此处仅要求登录即可保存,避免 RCSMonitor scope - // 账号在特殊场景下无法写入 ops.monitor(地图监控动作)备份字段。 + /// RCSMonitor scope 允许写入的 section 白名单(地图监控动作备份等运营自有配置)。 + private static readonly string[] MonitorWritableSections = { "ops" }; + + // Platform scope 可写任意 section;RCSMonitor 仅允许写 ops(保留运营端 + // 「地图监控动作 ops.monitor 备份」既有功能),其余 section(routing/auth/system 等)一律 403。 [HttpPut("{section}")] - [Authorize] public IActionResult Put(string section, [FromBody] JsonElement payload) { if (!ConfigStore.AllSections.Contains(section, StringComparer.OrdinalIgnoreCase)) return NotFound(new { message = $"未知 section: {section}" }); + var scope = User.FindFirst("scope")?.Value; + if (!string.Equals(scope, "Platform", StringComparison.OrdinalIgnoreCase) + && !MonitorWritableSections.Contains(section, StringComparer.OrdinalIgnoreCase)) + { + return StatusCode(403, new { message = $"当前账号无权修改配置节 {section}(需要 Platform 管理端权限)" }); + } + var env = _store.Put(section, payload); return Ok(new { diff --git a/MiGu.Server/Controllers/HealthController.cs b/MiGu.Server/Controllers/HealthController.cs index 8ccdd84..425b0ef 100644 --- a/MiGu.Server/Controllers/HealthController.cs +++ b/MiGu.Server/Controllers/HealthController.cs @@ -13,39 +13,33 @@ public class HealthController : ControllerBase public HealthController(SimpleLiteLauncher launcher) => _launcher = launcher; + /// 匿名存活探针:仅返回进程级状态,不暴露端口拓扑等部署细节。 [HttpGet] public IActionResult Get() { return Ok(new { status = "ok", - mode = "WebEnabled", startTime = StartTime, - uptimeSec = (long)(DateTimeOffset.UtcNow - StartTime).TotalSeconds, - ports = new - { - webApi = 7001, - webSocket = 7002, - platform = 8080, - vrender = 8223, - vehicle = 8222 - }, - architecture = "v1.5" + uptimeSec = (long)(DateTimeOffset.UtcNow - StartTime).TotalSeconds }); } /// /// SimpleLite 拉起配置诊断:查看当前 ExecutablePath、解析结果、端口是否已有服务。 /// 配置位置:MiGu.Server/appsettings.jsonSimpleLite 节点。 + /// 含服务器本地路径等敏感信息,要求登录。 /// [HttpGet("simplelite")] + [Authorize] public IActionResult GetSimpleLiteDiagnostics() => Ok(_launcher.GetDiagnostics()); /// /// 关闭 SimpleLite、同步最新 DLL、重新拉起。用于「前往站点」API 缺失时一键更新。 + /// 会终止本机全部 SimpleLite 进程并重启,仅 Platform 管理端可调。 /// [HttpPost("simplelite/restart-for-update")] - [Authorize] + [Authorize(Policy = "PlatformScope")] public IActionResult RestartSimpleLiteForUpdate([FromQuery] string launchMode = "webonly") { var result = _launcher.RestartForUpdate(launchMode); diff --git a/MiGu.Server/Controllers/LogsController.cs b/MiGu.Server/Controllers/LogsController.cs index 2ac880b..4302380 100644 --- a/MiGu.Server/Controllers/LogsController.cs +++ b/MiGu.Server/Controllers/LogsController.cs @@ -747,9 +747,9 @@ public sealed class LogsController : ControllerBase } b.Latest = e.Content; b.LatestTime = e.Time; - b.Recent.Add(e); - // 仅保留最近 maxPerTag 条,避免高频标签把内存撑爆。 - if (b.Recent.Count > maxPerTag) b.Recent.RemoveAt(0); + b.Recent.Enqueue(e); + // 仅保留最近 maxPerTag 条,避免高频标签把内存撑爆(Queue 头部出队 O(1))。 + if (b.Recent.Count > maxPerTag) b.Recent.Dequeue(); } private static object ToBookDto(Book b) => new @@ -831,6 +831,6 @@ public sealed class LogsController : ControllerBase public DateTime? LastTime { get; set; } public string Latest { get; set; } = ""; public DateTime? LatestTime { get; set; } - public List Recent { get; } = new(); + public Queue Recent { get; } = new(); } } diff --git a/MiGu.Server/Controllers/MapsContentController.cs b/MiGu.Server/Controllers/MapsContentController.cs index 9c0c278..878f8be 100644 --- a/MiGu.Server/Controllers/MapsContentController.cs +++ b/MiGu.Server/Controllers/MapsContentController.cs @@ -59,11 +59,11 @@ public class MapsContentController : ControllerBase return NotFound(new { message = $"地图文件不存在:{fileName}" }); var content = await System.IO.File.ReadAllTextAsync(fullPath, ct); + // 不返回 fullPath:避免向前端泄露服务器目录结构。 return Ok(new { name, fileName, - path = fullPath, content }); } diff --git a/MiGu.Server/Controllers/ProjectionController.cs b/MiGu.Server/Controllers/ProjectionController.cs deleted file mode 100644 index d3bdb0b..0000000 --- a/MiGu.Server/Controllers/ProjectionController.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace MiGu.Server.Controllers; - -/// -/// 投影 API 占位:真实落地时由 YARP 反代到 SimpleLite WebAPI 的 /api/projection/* 路径。 -/// 本地 Mock 数据仅用于无 SimpleLite 运行时的开发联调。 -/// -/// AR-4: 全 class 加 [Authorize] —— 任何登录用户都能读 mock 投影数据;未登录直接 401。 -/// -[ApiController] -[Authorize] -[Route("api/projection")] -public class ProjectionController : ControllerBase -{ - [HttpGet("sites")] - public IActionResult Sites() => Ok(new[] - { - new { id = "S001", name = "A 区-入库点", x = 1000, y = 2000 }, - new { id = "S002", name = "A 区-出库点", x = 3000, y = 2000 }, - new { id = "S003", name = "B 区-缓存区", x = 5000, y = 2000 } - }); - - [HttpGet("tracks")] - public IActionResult Tracks() => Ok(new[] - { - new { id = "T001", kind = "line", fromSiteId = "S001", toSiteId = "S002" }, - new { id = "T002", kind = "line", fromSiteId = "S002", toSiteId = "S003" } - }); - - [HttpGet("cars")] - public IActionResult Cars() => Ok(new[] - { - new { id = "C01", name = "AGV-001", state = "running", batterySoc = 0.86 }, - new { id = "C02", name = "AGV-002", state = "idle", batterySoc = 0.42 } - }); - - [HttpGet("missions")] - public IActionResult Missions() => Ok(new[] - { - new { id = "M01", name = "A 区送料 #1", status = "running", priority = 50 }, - new { id = "M02", name = "A→B 缓存搬运", status = "queued", priority = 60 } - }); -} diff --git a/MiGu.Server/Controllers/WizardController.cs b/MiGu.Server/Controllers/WizardController.cs index fbaca58..d553244 100644 --- a/MiGu.Server/Controllers/WizardController.cs +++ b/MiGu.Server/Controllers/WizardController.cs @@ -12,8 +12,8 @@ namespace MiGu.Server.Controllers; /// /// GET /api/wizard/options:可选项目录(导航方式 / 模块 / 业务场景模板)。 /// GET /api/wizard/profile:回显当前部署画像(含由导航选型推导的激活场景 id)。 -/// PUT /api/wizard/profile:保存并置 Configured=true -/// POST /api/wizard/reset:把 Configured 置回 false 以重新引导(保留草稿)。 +/// PUT /api/wizard/profile:保存并置 Configured=true(仅 Platform scope)。 +/// POST /api/wizard/reset:把 Configured 置回 false 以重新引导(仅 Platform scope)。 /// /// 说明:保存时即把选型固化为单一事实来源 deployment section,并同步联动 Launcher —— /// WriteActiveScenesplugins/active-scenes.json / 透传 --scenes, @@ -64,6 +64,7 @@ public class WizardController : ControllerBase } [HttpPut("profile")] + [Authorize(Policy = "PlatformScope")] public IActionResult SaveProfile([FromBody] SaveWizardRequest req) { if (req == null) @@ -92,6 +93,7 @@ public class WizardController : ControllerBase } [HttpPost("reset")] + [Authorize(Policy = "PlatformScope")] public IActionResult Reset() { var reset = _store.GetDeployment() with { Configured = false }; diff --git a/MiGu.Server/Launcher/SimpleLiteBuildSync.cs b/MiGu.Server/Launcher/SimpleLiteBuildSync.cs index be72cf2..fed4e82 100644 --- a/MiGu.Server/Launcher/SimpleLiteBuildSync.cs +++ b/MiGu.Server/Launcher/SimpleLiteBuildSync.cs @@ -50,13 +50,20 @@ public static class SimpleLiteBuildSync return true; } + /// + /// 探测 SimpleLite 是否带「前往站点」路由(区分新旧 DLL)。 + /// 必须用不存在的对象 id(-1):路由存在时后端进 handler 找不到对象,返回 JSON + /// success:false(无任何副作用);路由不存在时 EmbedIO 返回 HTML 404。 + /// 早期版本误用 car/0 + siteId=0 —— 一旦场景里真有 id=0 的车和站点,每次健康 + /// 检查都会真实派车,属严重副作用,严禁回退到真实 id。 + /// public static bool? ProbeGotoSiteRoute(int port = 8222) { try { using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(2) }; using var resp = client.PostAsync( - $"http://127.0.0.1:{port}/projection/reflection/car/0/goto-site?siteId=0", + $"http://127.0.0.1:{port}/projection/reflection/car/-1/goto-site?siteId=-1", null).GetAwaiter().GetResult(); var body = resp.Content.ReadAsStringAsync().GetAwaiter().GetResult(); if (resp.StatusCode == System.Net.HttpStatusCode.NotFound) diff --git a/MiGu.Server/Launcher/SimpleLiteLauncher.cs b/MiGu.Server/Launcher/SimpleLiteLauncher.cs index 7038752..73669c1 100644 --- a/MiGu.Server/Launcher/SimpleLiteLauncher.cs +++ b/MiGu.Server/Launcher/SimpleLiteLauncher.cs @@ -392,12 +392,33 @@ public sealed class SimpleLiteLauncher : IDisposable return result; } + private bool? _gotoSiteProbeCache; + private DateTimeOffset _gotoSiteProbeAt = DateTimeOffset.MinValue; + private static readonly TimeSpan GotoSiteProbeTtl = TimeSpan.FromSeconds(60); + /// 启动前诊断:当前配置、解析到的 exe、端口占用等(供 /api/health/simplelite 与启动日志)。 public SimpleLiteDiagnostics GetDiagnostics() { var resolved = ResolveExecutable(_opts.ExecutablePath); var projectionUp = TryConnect("127.0.0.1", _opts.ProjectionPort, TimeSpan.FromMilliseconds(400)); - var gotoSite = projectionUp ? SimpleLiteBuildSync.ProbeGotoSiteRoute(_opts.ProjectionPort) : null; + // 探测结果缓存 60s:诊断端点可能被前端轮询,避免每次都对 SimpleLite 发探测请求。 + bool? gotoSite = null; + if (projectionUp) + { + if (_gotoSiteProbeCache is bool cached && DateTimeOffset.UtcNow - _gotoSiteProbeAt < GotoSiteProbeTtl) + { + gotoSite = cached; + } + else + { + gotoSite = SimpleLiteBuildSync.ProbeGotoSiteRoute(_opts.ProjectionPort); + if (gotoSite is not null) + { + _gotoSiteProbeCache = gotoSite; + _gotoSiteProbeAt = DateTimeOffset.UtcNow; + } + } + } var deployHint = gotoSite == false ? "关闭 SimpleLite 后:运行 Migu2.0/scripts/redeploy-simplelite.ps1,或 POST /api/health/simplelite/restart-for-update" : null; diff --git a/MiGu.Server/Program.cs b/MiGu.Server/Program.cs index 312fc6f..ef65798 100644 --- a/MiGu.Server/Program.cs +++ b/MiGu.Server/Program.cs @@ -156,15 +156,6 @@ builder.Services.AddSingleton(); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(opt => { - // TokenValidationParameters 在第一次解析请求时从 JwtIssuer 拿,避免 ctor 顺序耦合。 - opt.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters - { - // 完整参数在 OnMessageReceived 里替换为 JwtIssuer.BuildValidationParameters() - ValidateIssuer = false, - ValidateAudience = false, - ValidateIssuerSigningKey = false, - ValidateLifetime = false, - }; opt.Events = new JwtBearerEvents { OnMessageReceived = ctx => @@ -175,13 +166,14 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) var cookie = ctx.Request.Cookies["simple.auth.token"]; if (!string.IsNullOrEmpty(cookie)) ctx.Token = cookie; } - // 用真实 JwtIssuer 参数替换占位 ValidationParameters。 - var issuer = ctx.HttpContext.RequestServices.GetRequiredService(); - ctx.Options.TokenValidationParameters = issuer.BuildValidationParameters(); return Task.CompletedTask; } }; }); +// TokenValidationParameters 由 JwtIssuer(DI 单例)启动期一次性提供, +// 替代旧的「每请求在 OnMessageReceived 里改写共享 Options」写法(并发坏味道)。 +builder.Services.AddOptions(JwtBearerDefaults.AuthenticationScheme) + .Configure((opt, issuer) => opt.TokenValidationParameters = issuer.BuildValidationParameters()); builder.Services.AddAuthorization(opts => { @@ -206,8 +198,9 @@ builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) .AddTransforms(tctx => { - // 只对 sl-route 注入 internal token;vrender-route(webVRender iframe 静态资源)不需要。 - if (tctx.Route.RouteId != "sl-route") return; + // 对全部 sl-* 路由(兜底 + map-edit/ai-config/reflection 管理面拆分路由)注入 + // internal token;vrender-route(webVRender iframe 静态资源)不需要。 + if (!tctx.Route.RouteId.StartsWith("sl-", StringComparison.Ordinal)) return; tctx.AddRequestTransform(rt => { var store = rt.HttpContext.RequestServices.GetRequiredService(); diff --git a/MiGu.Server/appsettings.json b/MiGu.Server/appsettings.json index 4f6b949..770f3e9 100644 --- a/MiGu.Server/appsettings.json +++ b/MiGu.Server/appsettings.json @@ -44,8 +44,48 @@ "Dispatch": { } }, + "_comment_ReverseProxy": "sl-route 兜底 AnyAuthed(投影只读 + SSE)。管理面路径(map-edit / ai-config / reflection 写操作)单独拆路由挂 PlatformScope,防止运营账号经反代直达地图编辑与任意反射调用。", "ReverseProxy": { "Routes": { + "sl-mapedit-route": { + "ClusterId": "sl-cluster", + "AuthorizationPolicy": "PlatformScope", + "Order": -2, + "Match": { "Path": "/api/sl/projection/map-edit/{**catch-all}" }, + "Transforms": [ + { "PathRemovePrefix": "/api/sl" } + ] + }, + "sl-aiconfig-route": { + "ClusterId": "sl-cluster", + "AuthorizationPolicy": "PlatformScope", + "Order": -2, + "Match": { "Path": "/api/sl/projection/ai-config/{**catch-all}" }, + "Transforms": [ + { "PathRemovePrefix": "/api/sl" } + ] + }, + "sl-reflection-selection-route": { + "ClusterId": "sl-cluster", + "AuthorizationPolicy": "AnyAuthed", + "Order": -3, + "Match": { "Path": "/api/sl/projection/reflection/selection/{**catch-all}" }, + "Transforms": [ + { "PathRemovePrefix": "/api/sl" } + ] + }, + "sl-reflection-write-route": { + "ClusterId": "sl-cluster", + "AuthorizationPolicy": "PlatformScope", + "Order": -1, + "Match": { + "Path": "/api/sl/projection/reflection/{**catch-all}", + "Methods": [ "POST", "PUT", "PATCH", "DELETE" ] + }, + "Transforms": [ + { "PathRemovePrefix": "/api/sl" } + ] + }, "sl-route": { "ClusterId": "sl-cluster", "AuthorizationPolicy": "AnyAuthed", diff --git a/frontends/apps/simple-platform-vue/package.json b/frontends/apps/simple-platform-vue/package.json index 3fba515..8b0ceb0 100644 --- a/frontends/apps/simple-platform-vue/package.json +++ b/frontends/apps/simple-platform-vue/package.json @@ -8,7 +8,7 @@ "dev": "vite", "build": "vue-tsc --noEmit && vite build", "preview": "vite preview", - "lint": "eslint . --ext .ts,.vue --fix" + "typecheck": "vue-tsc --noEmit" }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", diff --git a/frontends/apps/simple-platform-vue/src/api/mapEdit.ts b/frontends/apps/simple-platform-vue/src/api/mapEdit.ts index 9f11eb1..a0adbef 100644 --- a/frontends/apps/simple-platform-vue/src/api/mapEdit.ts +++ b/frontends/apps/simple-platform-vue/src/api/mapEdit.ts @@ -204,11 +204,9 @@ export const mapEditApi = { dashboardSummary: () => unwrap(http.get(`${BASE}/dashboard/summary`)), - // 资产上传:传 base64 + // 资产上传:JSON body 传 base64(后端按 JSON 解析;勿手动设 multipart 头 —— body 并非 multipart)。 uploadAsset: (filename: string, dataBase64: string) => - unwrap(http.post(`${BASE}/assets/upload`, { filename, data: dataBase64 }, { - headers: { 'Content-Type': 'multipart/form-data' } - })), + unwrap(http.post(`${BASE}/assets/upload`, { filename, data: dataBase64 })), // AI 生图 aiMapGenerate: (req: AiMapGenerateRequest) => @@ -335,7 +333,6 @@ export interface MapSaveResult { export interface MapContentResult { name: string fileName: string - path: string content: string } @@ -426,13 +423,25 @@ export const mapsApi = { * 当前未设置使用地图(400)/ 场景内有车辆任务(409)时 message 不含「已存在」,conflict=false,调用方直接提示。 */ async merge(sources: string[], target: string, overwrite = false): Promise { - const { data } = await http.post>(`${BASE}/maps/merge`, { - sources, - target, - overwrite - }) - if (data?.success) return { ok: true, data: data.data as MapMergeResult } - const conflict = data?.code === 409 && (data?.message ?? '').includes('已存在') - return { ok: false, conflict, message: data?.message ?? '合并失败' } + try { + const { data } = await http.post>(`${BASE}/maps/merge`, { + sources, + target, + overwrite + }) + if (data?.success) return { ok: true, data: data.data as MapMergeResult } + const conflict = data?.code === 409 && (data?.message ?? '').includes('已存在') + return { ok: false, conflict, message: data?.message ?? '合并失败' } + } catch (err) { + // 与 save() 对齐:后端以 HTTP 409 状态码返回时同样翻译为 conflict, + // 让调用方能弹「是否替换」确认而非直接报错。 + const ax = err as AxiosError> + const body = ax.response?.data + if (ax.response?.status === 409 || body?.code === 409) { + const message = body?.message ?? '目标地图已存在' + return { ok: false, conflict: message.includes('已存在'), message } + } + throw err + } } } diff --git a/frontends/apps/simple-platform-vue/src/components/DataTablePro.vue b/frontends/apps/simple-platform-vue/src/components/DataTablePro.vue deleted file mode 100644 index 19a238e..0000000 --- a/frontends/apps/simple-platform-vue/src/components/DataTablePro.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - - - diff --git a/frontends/apps/simple-platform-vue/src/components/Workspace3D.vue b/frontends/apps/simple-platform-vue/src/components/Workspace3D.vue index fa44148..9ffcb20 100644 --- a/frontends/apps/simple-platform-vue/src/components/Workspace3D.vue +++ b/frontends/apps/simple-platform-vue/src/components/Workspace3D.vue @@ -36,6 +36,7 @@ import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { Refresh, FullScreen, Loading } from '@element-plus/icons-vue' import type { Scope } from '@/types/auth' +import { defaultVrHost } from '@/utils/vrender' interface PickEvent { x: number; y: number } @@ -78,7 +79,7 @@ const lastPick = ref(null) const lastSelect = ref([]) const iframeSrc = ref('') -const resolvedHost = computed(() => props.host ?? (import.meta.env.VITE_VRENDER_HOST as string | undefined) ?? 'localhost:8223') +const resolvedHost = computed(() => props.host ?? defaultVrHost()) const vrUrl = computed(() => { const qs = new URLSearchParams() diff --git a/frontends/apps/simple-platform-vue/src/composables/useClipboard.ts b/frontends/apps/simple-platform-vue/src/composables/useClipboard.ts deleted file mode 100644 index c584d6e..0000000 --- a/frontends/apps/simple-platform-vue/src/composables/useClipboard.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { ref } from 'vue' -import type { SelectionItem } from './useSelection' - -/** - * 编辑器剪贴板:保存最近一次「复制」操作的对象快照(含字段值), - * 用于「粘贴 (Ctrl+V)」与「复制字段 (Copy Fields)」。 - * - * 粘贴策略:调用方在拿到目标坐标后,用 mapEditApi.batch 创建副本(带偏移)。 - * 复制字段:调用方调 mapEditApi.copyFieldsTo 把指定字段名写到目标对象(们)。 - * - * 注意:剪贴板里保存的是对象的"逻辑快照",不是 DOM 文本剪贴板。 - */ - -export interface ClipboardSnapshot { - items: Array<{ - kind: string - sourceId: number - typeName: string - /** 对象的几何 / 样式字段(含 x, y 用于偏移粘贴)。 */ - fields: Record - }> - fieldNames: string[] -} - -export function useClipboard() { - const data = ref(null) - - function copy(items: SelectionItem[], allFields: Record>) { - if (items.length === 0) { - data.value = null - return - } - const fieldNamesSet = new Set() - const snapshot: ClipboardSnapshot = { - items: items.map((it) => { - const f = allFields[it.id] ?? {} - Object.keys(f).forEach((k) => fieldNamesSet.add(k)) - return { - kind: it.kind, - sourceId: it.id, - typeName: it.typeName, - fields: f - } - }), - fieldNames: [] - } - snapshot.fieldNames = [...fieldNamesSet] - data.value = snapshot - } - - function clear() { - data.value = null - } - - return { data, copy, clear } -} diff --git a/frontends/apps/simple-platform-vue/src/layouts/AppShell.vue b/frontends/apps/simple-platform-vue/src/layouts/AppShell.vue index 1e98a4a..a5cebcc 100644 --- a/frontends/apps/simple-platform-vue/src/layouts/AppShell.vue +++ b/frontends/apps/simple-platform-vue/src/layouts/AppShell.vue @@ -109,7 +109,7 @@ import { computed } from 'vue' import { useRoute, useRouter } from 'vue-router' import { Fold, Expand, CaretBottom, Monitor, Setting, Histogram, Tools, - MapLocation, Van, Promotion, Box, OfficeBuilding, Notebook + MapLocation, Van, Promotion, Notebook } from '@element-plus/icons-vue' import { useAuthStore } from '@/stores/auth' import { useUiStore } from '@/stores/ui' @@ -209,8 +209,6 @@ const activePath = computed(() => { const currentTitle = computed(() => (route.meta.title as string | undefined) ?? '') -void Van; void Box; void OfficeBuilding - function onUserCommand(cmd: string) { if (cmd === 'logout') { auth.logout() diff --git a/frontends/apps/simple-platform-vue/src/utils/vrender.ts b/frontends/apps/simple-platform-vue/src/utils/vrender.ts new file mode 100644 index 0000000..6158bc9 --- /dev/null +++ b/frontends/apps/simple-platform-vue/src/utils/vrender.ts @@ -0,0 +1,11 @@ +/** + * webVRender (SimpleLite 3D 视口, 默认 :8223) 的 host 解析。 + * + * 优先级:显式 VITE_VRENDER_HOST > 当前页面 hostname:8223。 + * 不能写死 localhost —— 从远程浏览器访问平台时 iframe 会去连访问者本机而非服务器。 + */ +export function defaultVrHost(): string { + const env = import.meta.env.VITE_VRENDER_HOST as string | undefined + if (env && env.trim()) return env.trim() + return `${window.location.hostname}:8223` +} diff --git a/frontends/apps/simple-platform-vue/src/views/ServiceStatusView.vue b/frontends/apps/simple-platform-vue/src/views/ServiceStatusView.vue index d138b65..ecaec03 100644 --- a/frontends/apps/simple-platform-vue/src/views/ServiceStatusView.vue +++ b/frontends/apps/simple-platform-vue/src/views/ServiceStatusView.vue @@ -1,26 +1,50 @@ - - diff --git a/frontends/apps/simple-platform-vue/src/views/admin/MapEditorView.vue b/frontends/apps/simple-platform-vue/src/views/admin/MapEditorView.vue index 74cc0f5..a02e54e 100644 --- a/frontends/apps/simple-platform-vue/src/views/admin/MapEditorView.vue +++ b/frontends/apps/simple-platform-vue/src/views/admin/MapEditorView.vue @@ -176,6 +176,7 @@ import { promptSaveMode, resolveCurrentMapName } from '@/utils/projectSaveFlow' +import { defaultVrHost } from '@/utils/vrender' import { reflectionApi, normalizeViewportPayload, @@ -190,7 +191,7 @@ import { const auth = useAuthStore() const route = useRoute() const router = useRouter() -const vrHost = (import.meta.env.VITE_VRENDER_HOST as string | undefined) ?? 'localhost:8223' +const vrHost = defaultVrHost() // 当前正在编辑的「固定文件夹地图名」。从地图管理页带 ?map= 进入时载入; // 决定保存时的默认名称与「是否替换原地图」确认逻辑。新建地图(?new=1)时为空。 diff --git a/frontends/apps/simple-platform-vue/src/views/admin/MapManagementView.vue b/frontends/apps/simple-platform-vue/src/views/admin/MapManagementView.vue index 9733881..3798d50 100644 --- a/frontends/apps/simple-platform-vue/src/views/admin/MapManagementView.vue +++ b/frontends/apps/simple-platform-vue/src/views/admin/MapManagementView.vue @@ -153,7 +153,7 @@ async function loadMapContent(name: string) { viewingPath.value = '' try { const r = await mapsApi.readContent(name) - viewingPath.value = r.path + viewingPath.value = r.fileName viewingJsonRaw.value = r.content } catch (err) { ElMessage.error(`加载地图配置失败:${(err as Error).message}`) diff --git a/frontends/apps/simple-platform-vue/src/views/admin/MapMonitorView.vue b/frontends/apps/simple-platform-vue/src/views/admin/MapMonitorView.vue index 32f868a..d8dc9f3 100644 --- a/frontends/apps/simple-platform-vue/src/views/admin/MapMonitorView.vue +++ b/frontends/apps/simple-platform-vue/src/views/admin/MapMonitorView.vue @@ -100,6 +100,7 @@ import type { DeliveryTask } from '@/types/delivery' import type { SelectedObjectRef } from '@/types/workbench' import { fetchMonitorConfigCached, invalidateMonitorConfigCache } from '@/utils/monitorConfigCache' import type { MapFocusKind } from '@/utils/mapObjectFocus' +import { defaultVrHost } from '@/utils/vrender' defineProps<{ /** 只读模式(运营端复用 MapMonitorView 时传 true):3D 不可编辑,选中信息面板动作改用运维白名单。 */ @@ -109,7 +110,7 @@ defineProps<{ const auth = useAuthStore() const route = useRoute() const router = useRouter() -const vrHost = (import.meta.env.VITE_VRENDER_HOST as string | undefined) ?? 'localhost:8223' +const vrHost = defaultVrHost() const cars = ref([]) const missions = ref([]) diff --git a/frontends/package.json b/frontends/package.json index 711bac3..571ef8d 100644 --- a/frontends/package.json +++ b/frontends/package.json @@ -8,6 +8,6 @@ "dev": "pnpm --filter simple-platform-vue dev", "build": "pnpm --filter simple-platform-vue build", "preview": "pnpm --filter simple-platform-vue preview", - "lint": "pnpm --filter simple-platform-vue lint" + "typecheck": "pnpm --filter simple-platform-vue typecheck" } }