@arronqzy/react-view 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/styles.css +519 -9
  2. package/package.json +12 -4
  3. package/src/core/template.tsx +12 -2
  4. package/src/panel/ReactViewOnlinePreview.tsx +18 -8
  5. package/src/panel/ReactViewPanel.tsx +386 -170
  6. package/src/panel/ai/AssistantChatPanel.tsx +895 -0
  7. package/src/panel/ai/AssistantMarkdown.tsx +256 -0
  8. package/src/panel/ai/applyAssistantAction.ts +652 -0
  9. package/src/panel/ai/revealConfigField.ts +137 -0
  10. package/src/panel/components/ConfigHintIcon.tsx +6 -2
  11. package/src/panel/components/ElementsLayer.tsx +37 -18
  12. package/src/panel/components/MaterialSidebar.tsx +157 -109
  13. package/src/panel/components/MoveableLayer.tsx +7 -6
  14. package/src/panel/components/PanelConfigSidebar.tsx +434 -389
  15. package/src/panel/components/ViewElementScopePanel.tsx +9 -5
  16. package/src/panel/components/WorkspaceConfigSidebar.tsx +5 -2
  17. package/src/panel/components/WorkspaceProjectNav.tsx +25 -14
  18. package/src/panel/components/WorkspaceStageSplit.tsx +3 -1
  19. package/src/panel/components/scope-config/ScopeTemplatePreviewHost.tsx +11 -7
  20. package/src/panel/components/scope-config/ScopeTemplateUsageHint.tsx +6 -18
  21. package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +5 -2
  22. package/src/panel/components/table/ConditionEditor.tsx +205 -0
  23. package/src/panel/components/table/JsonCodeEditor.tsx +115 -0
  24. package/src/panel/components/table/PanelConfigTableSection.tsx +2192 -0
  25. package/src/panel/components/table/TableNodeContent.tsx +544 -0
  26. package/src/panel/constants/messages.ts +76 -34
  27. package/src/panel/hooks/usePanelElements.ts +64 -52
  28. package/src/panel/hooks/useWorkspaceProjects.ts +11 -8
  29. package/src/panel/types.ts +4 -0
  30. package/src/panel/utils/panelElementDefaults.ts +69 -39
  31. package/src/panel/utils/scope-field-labels.ts +51 -38
  32. package/src/panel/utils/scope-template-preview.ts +10 -6
  33. package/src/panel/utils/scope-template-spread.ts +44 -21
  34. package/src/panel/utils/scope-template-warnings.ts +28 -16
  35. package/src/panel/utils/scope-template.ts +82 -2
  36. package/src/tailwind.css +17 -9
@@ -1,4 +1,5 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { useI18n } from "@arronqzy/i18n/react";
2
3
  import type { State } from "@arronqzy/rx-store";
3
4
  import type { PanelElement } from "./types";
4
5
 
@@ -14,6 +15,14 @@ import { SelectLayer } from "./components/SelectLayer";
14
15
  import { MoveableLayer } from "./components/MoveableLayer";
15
16
  import { buildChartOption, CHART_TYPES } from "./utils/chartOptionBuilder";
16
17
  import { MaterialSidebar } from "./components/MaterialSidebar";
18
+ import { AssistantChatPanel } from "./ai/AssistantChatPanel";
19
+ import {
20
+ listLayersFromStore,
21
+ listPanelElementsFromStore,
22
+ getActiveLayerIdFromStore,
23
+ type ApplyAssistantDeps,
24
+ } from "./ai/applyAssistantAction";
25
+ import { revealPanelConfigFromPatch } from "./ai/revealConfigField";
17
26
  import {
18
27
  BlueprintGraph,
19
28
  BlueprintMetaDialog,
@@ -55,7 +64,7 @@ import {
55
64
  useViewScopeStoreVersion,
56
65
  } from "./scope/view-scope-store";
57
66
  import { resolvePanelElementScope } from "./utils/scope-template";
58
- import { PANEL_MESSAGES } from "./constants/messages";
67
+ import { getPanelMessages } from "./constants/messages";
59
68
  import { PANEL_Z_INDEX } from "./constants/zIndex";
60
69
  import { useRafThrottledScroll } from "./hooks/useRafThrottledScroll";
61
70
  import {
@@ -67,6 +76,7 @@ import {
67
76
  AlertDialogFooter,
68
77
  AlertDialogHeader,
69
78
  AlertDialogTitle,
79
+ Button,
70
80
  Checkbox,
71
81
  Dialog,
72
82
  DialogContent,
@@ -274,16 +284,20 @@ function IconImage() {
274
284
  );
275
285
  }
276
286
 
277
- function formatRelativeTime(timestamp: number, now: number): string {
287
+ function formatRelativeTime(
288
+ timestamp: number,
289
+ now: number,
290
+ t: (key: string, params?: Record<string, string | number>) => string
291
+ ): string {
278
292
  const diff = Math.max(0, now - timestamp);
279
293
  const sec = Math.floor(diff / 1000);
280
- if (sec < 60) return `${sec}秒前`;
294
+ if (sec < 60) return t("common.secondsAgo", { sec });
281
295
  const min = Math.floor(sec / 60);
282
- if (min < 60) return `${min}分钟前`;
296
+ if (min < 60) return t("common.minutesAgo", { min });
283
297
  const hour = Math.floor(min / 60);
284
- if (hour < 24) return `${hour}小时前`;
298
+ if (hour < 24) return t("common.hoursAgo", { hour });
285
299
  const day = Math.floor(hour / 24);
286
- return `${day}天前`;
300
+ return t("common.daysAgo", { day });
287
301
  }
288
302
 
289
303
  function getSelectedTargetsFromIds(
@@ -333,7 +347,12 @@ export type ReactViewPanelProps = {
333
347
  className?: string;
334
348
  };
335
349
 
336
- export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelProps) {
350
+ export function ReactViewPanel({
351
+ initialZoom = 1,
352
+ className,
353
+ }: ReactViewPanelProps) {
354
+ const { t, locale, setLocale } = useI18n();
355
+ const messages = useMemo(() => getPanelMessages(t), [t]);
337
356
  const THEME_STORAGE_KEY = "panel:theme";
338
357
  const TITLE_ICON_STORAGE_KEY = "panel:titleIconDataUrl";
339
358
  const { setTheme } = useTheme();
@@ -435,16 +454,19 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
435
454
  const [isHistoryPanelExpanded, setIsHistoryPanelExpanded] = useState(false);
436
455
  const [historyNow, setHistoryNow] = useState(Date.now());
437
456
  const [historyKeyword, setHistoryKeyword] = useState("");
438
- const [productName, setProductName] = useState("未命名产物");
457
+ const [productName, setProductName] = useState(t("panel.defaults.unnamedProduct"));
439
458
  const [titleIconDataUrl, setTitleIconDataUrl] = useState<string>("");
440
459
  const [titleIconPreviewOpen, setTitleIconPreviewOpen] = useState(false);
441
460
  const [titleIconZoom, setTitleIconZoom] = useState(1.6);
442
461
  const [pendingPreviewLayerId, setPendingPreviewLayerId] = useState<string | null>(null);
443
462
  const [panelFontSize, setPanelFontSize] = useState<"sm" | "md" | "lg">("md");
444
463
  const [blueprintOpen, setBlueprintOpen] = useState(false);
464
+ const [assistantOpen, setAssistantOpen] = useState(false);
445
465
  const [blueprintGraph, setBlueprintGraph] = useState(() => BlueprintGraph.empty());
466
+ const blueprintGraphRef = useRef(blueprintGraph);
467
+ blueprintGraphRef.current = blueprintGraph;
446
468
  const [blueprintMeta, setBlueprintMeta] = useState<BlueprintMetaDraft>({
447
- name: "未命名蓝图",
469
+ name: t("panel.defaults.unnamedBlueprint"),
448
470
  remark: "",
449
471
  });
450
472
  const [blueprintLibraryItems, setBlueprintLibraryItems] = useState<
@@ -836,7 +858,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
836
858
  const blueprintDebugSession = useBlueprintDebugSession({
837
859
  graph: blueprintGraph,
838
860
  blueprintId: activeBlueprintLibraryId,
839
- blueprintName: blueprintMeta.name || "未命名蓝图",
861
+ blueprintName: blueprintMeta.name || t("panel.defaults.unnamedBlueprint"),
840
862
  resolveLibraryBlueprint,
841
863
  libraryNameById: blueprintLibraryNameById,
842
864
  onExecutionBlocked: handleBlueprintExecutionBlocked,
@@ -902,8 +924,8 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
902
924
  toast({
903
925
  title:
904
926
  removed > 0
905
- ? `已清空 IndexedDB 中 ${removed} 条蓝图日志`
906
- : "IndexedDB 中暂无已保存日志",
927
+ ? messages.clearedBlueprintLogs(removed)
928
+ : messages.noSavedBlueprintLogs,
907
929
  });
908
930
  },
909
931
  hasSavedRuns: blueprintDebugSession.totalSavedRunCount > 0,
@@ -954,7 +976,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
954
976
  async (id: string) => {
955
977
  const record = await getBlueprintLibraryRecord(id);
956
978
  if (!record) {
957
- toast({ title: "蓝图不存在或已被删除" });
979
+ toast({ title: t("panel.messages.blueprintNotFound") });
958
980
  void refreshBlueprintLibrary();
959
981
  return;
960
982
  }
@@ -1026,7 +1048,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1026
1048
 
1027
1049
  const existing = await getBlueprintLibraryRecord(activeBlueprintLibraryId);
1028
1050
  if (!existing) {
1029
- toast({ title: "蓝图不存在或已被删除" });
1051
+ toast({ title: t("panel.messages.blueprintNotFound") });
1030
1052
  void refreshBlueprintLibrary();
1031
1053
  setActiveBlueprintLibraryId(null);
1032
1054
  setBlueprintSyncedDocument(null);
@@ -1043,7 +1065,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1043
1065
  await putBlueprintLibraryRecord(record);
1044
1066
  setBlueprintSyncedDocument(blueprintGraph.document);
1045
1067
  await refreshBlueprintLibrary();
1046
- toast({ title: `已同步「${record.name}」` });
1068
+ toast({ title: messages.blueprintSynced(record.name) });
1047
1069
  }, [
1048
1070
  activeBlueprintLibraryId,
1049
1071
  blueprintGraph.document,
@@ -1080,7 +1102,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1080
1102
  setActiveBlueprintLibraryId(record.id);
1081
1103
  setBlueprintSyncedDocument(blueprintGraph.document);
1082
1104
  await refreshBlueprintLibrary();
1083
- toast({ title: "蓝图已保存到本地" });
1105
+ toast({ title: t("panel.messages.blueprintSavedLocal") });
1084
1106
  },
1085
1107
  [activeBlueprintLibraryId, blueprintGraph.document, refreshBlueprintLibrary]
1086
1108
  );
@@ -1097,7 +1119,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1097
1119
  downloadBlueprintExport(
1098
1120
  buildBlueprintExportPayload(blueprintGraph.document, meta)
1099
1121
  );
1100
- toast({ title: "蓝图已导出" });
1122
+ toast({ title: t("panel.messages.blueprintExported") });
1101
1123
  return;
1102
1124
  }
1103
1125
  await saveBlueprintToLibrary(meta);
@@ -1109,7 +1131,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1109
1131
  async (id: string, name: string) => {
1110
1132
  const updated = await updateBlueprintLibraryMeta(id, { name });
1111
1133
  if (!updated) {
1112
- toast({ title: "重命名失败" });
1134
+ toast({ title: t("panel.messages.renameFailed") });
1113
1135
  void refreshBlueprintLibrary();
1114
1136
  return;
1115
1137
  }
@@ -1117,7 +1139,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1117
1139
  if (activeBlueprintLibraryId === id) {
1118
1140
  setBlueprintMeta((prev) => ({ ...prev, name }));
1119
1141
  }
1120
- toast({ title: "蓝图已重命名" });
1142
+ toast({ title: t("panel.messages.blueprintRenamed") });
1121
1143
  },
1122
1144
  [activeBlueprintLibraryId, refreshBlueprintLibrary]
1123
1145
  );
@@ -1130,7 +1152,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1130
1152
  setActiveBlueprintLibraryId(null);
1131
1153
  setBlueprintSyncedDocument(null);
1132
1154
  }
1133
- toast({ title: "蓝图已从库中删除" });
1155
+ toast({ title: t("panel.messages.blueprintDeletedFromLibrary") });
1134
1156
  },
1135
1157
  [activeBlueprintLibraryId, refreshBlueprintLibrary]
1136
1158
  );
@@ -1150,7 +1172,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1150
1172
  snapshotWorkspaceBlueprint();
1151
1173
  }
1152
1174
  await loadBlueprintFromLibrary(record.id);
1153
- toast({ title: "蓝图已导入并加载" });
1175
+ toast({ title: t("panel.messages.blueprintImported") });
1154
1176
  },
1155
1177
  [
1156
1178
  activeBlueprintLibraryId,
@@ -1170,7 +1192,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1170
1192
  workspaceBlueprintRef.current = {
1171
1193
  document: record.blueprintDocument,
1172
1194
  meta: {
1173
- name: record.blueprintMeta?.name ?? "未命名蓝图",
1195
+ name: record.blueprintMeta?.name ?? t("panel.defaults.unnamedBlueprint"),
1174
1196
  remark: record.blueprintMeta?.remark ?? "",
1175
1197
  },
1176
1198
  };
@@ -1198,7 +1220,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1198
1220
  onProjectApplied: handleWorkspaceProjectApplied,
1199
1221
  });
1200
1222
 
1201
- useBlueprintPageLifecycle({
1223
+ const { triggerBlueprintNode } = useBlueprintPageLifecycle({
1202
1224
  graph: blueprintGraph,
1203
1225
  active: blueprintOpen,
1204
1226
  bootKey: workspaceProjects.activeProjectId ?? undefined,
@@ -1213,7 +1235,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1213
1235
  const handleWorkspaceCreateProject = useCallback(async () => {
1214
1236
  const result = await workspaceProjects.handleCreateProject();
1215
1237
  if (result?.name) {
1216
- toast({ title: `已创建工作区「${result.name}」` });
1238
+ toast({ title: messages.workspaceCreated(result.name) });
1217
1239
  }
1218
1240
  return result;
1219
1241
  }, [workspaceProjects]);
@@ -1225,12 +1247,12 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1225
1247
  await workspaceProjects.handleOpenProject(id);
1226
1248
  setSelectedIds([]);
1227
1249
  toast({
1228
- title: "工作区已加载",
1229
- description: "视图与蓝图已恢复",
1250
+ title: t("panel.messages.workspaceLoaded"),
1251
+ description: t("panel.messages.workspaceLoadedDesc"),
1230
1252
  });
1231
1253
  } catch (error) {
1232
1254
  toast({
1233
- title: error instanceof Error ? error.message : "打开工作区失败",
1255
+ title: error instanceof Error ? error.message : t("panel.messages.openWorkspaceFailed"),
1234
1256
  });
1235
1257
  }
1236
1258
  },
@@ -1241,11 +1263,11 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1241
1263
  try {
1242
1264
  const name = await workspaceProjects.handleSyncProject();
1243
1265
  if (name) {
1244
- toast({ title: `已同步「${name}」` });
1266
+ toast({ title: messages.blueprintSynced(name) });
1245
1267
  }
1246
1268
  } catch (error) {
1247
1269
  toast({
1248
- title: error instanceof Error ? error.message : "同步失败",
1270
+ title: error instanceof Error ? error.message : t("panel.messages.syncFailed"),
1249
1271
  });
1250
1272
  }
1251
1273
  }, [workspaceProjects]);
@@ -1253,7 +1275,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1253
1275
  const handleWorkspaceDeleteProject = useCallback(
1254
1276
  async (id: string) => {
1255
1277
  await workspaceProjects.handleDeleteProject(id);
1256
- toast({ title: "工作区已删除" });
1278
+ toast({ title: t("panel.messages.workspaceDeleted") });
1257
1279
  },
1258
1280
  [workspaceProjects]
1259
1281
  );
@@ -1264,7 +1286,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1264
1286
  await workspaceProjects.handlePreviewProject(projectId, options);
1265
1287
  } catch (error) {
1266
1288
  toast({
1267
- title: error instanceof Error ? error.message : "打开预览失败",
1289
+ title: error instanceof Error ? error.message : t("panel.messages.openPreviewFailed"),
1268
1290
  });
1269
1291
  }
1270
1292
  },
@@ -1303,42 +1325,200 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1303
1325
  const layer = layerById.get(el.layerId);
1304
1326
  return !layer?.locked;
1305
1327
  });
1328
+
1329
+ const selectedIdsRef = useRef(selectedIds);
1330
+ selectedIdsRef.current = selectedIds;
1331
+ const zoomRef = useRef(zoom);
1332
+ zoomRef.current = zoom;
1333
+ const blueprintOpenRef = useRef(blueprintOpen);
1334
+ blueprintOpenRef.current = blueprintOpen;
1335
+ const workspaceLiveRef = useRef({
1336
+ activeProjectId: workspaceProjects.activeProjectId,
1337
+ activeProjectName: workspaceProjects.activeProjectName,
1338
+ dirty: workspaceProjects.dirty,
1339
+ projectCount: workspaceProjects.projects.length,
1340
+ });
1341
+ workspaceLiveRef.current = {
1342
+ activeProjectId: workspaceProjects.activeProjectId,
1343
+ activeProjectName: workspaceProjects.activeProjectName,
1344
+ dirty: workspaceProjects.dirty,
1345
+ projectCount: workspaceProjects.projects.length,
1346
+ };
1347
+
1348
+ const assistantDeps = useMemo<ApplyAssistantDeps>(
1349
+ () => ({
1350
+ addElementFromMaterial,
1351
+ updateElement,
1352
+ deleteElement,
1353
+ duplicateElement,
1354
+ setSelectedIds: (ids) => {
1355
+ selectedIdsRef.current = ids;
1356
+ selectViewElements(ids);
1357
+ // Newly added nodes may not be in DOM until next paint — refresh Moveable targets.
1358
+ requestAnimationFrame(() => {
1359
+ setSelectedTargets(
1360
+ getSelectedTargetsFromIds(canvasRef.current, selectedIdsRef.current)
1361
+ );
1362
+ });
1363
+ },
1364
+ getSelectedIds: () => selectedIdsRef.current,
1365
+ revealConfigFromPatch: (patch) => {
1366
+ window.setTimeout(() => {
1367
+ revealPanelConfigFromPatch(patch);
1368
+ }, 60);
1369
+ },
1370
+ focusWorkspace: (area) => {
1371
+ if (area === "blueprint") {
1372
+ setConfigFocus("blueprint");
1373
+ setBlueprintOpen(true);
1374
+ } else {
1375
+ focusViewConfig();
1376
+ }
1377
+ },
1378
+ getElementIds: () =>
1379
+ new Set(listPanelElementsFromStore().map((el) => el.id)),
1380
+ getElement: (id) => listPanelElementsFromStore().find((el) => el.id === id),
1381
+ listElements: () => listPanelElementsFromStore(),
1382
+ getLayers: () => listLayersFromStore(),
1383
+ getActiveLayerId: () => getActiveLayerIdFromStore(),
1384
+ setActiveLayer,
1385
+ addLayer,
1386
+ renameLayer,
1387
+ setLayerLocked: (id, locked) => {
1388
+ const layer = listLayersFromStore().find((l) => l.id === id);
1389
+ if (!layer) return;
1390
+ if (!!layer.locked !== locked) toggleLayerLock(id);
1391
+ },
1392
+ deleteLayer: (id) => {
1393
+ const result = deleteLayer(id, { mode: "remove" });
1394
+ return result.ok
1395
+ ? { ok: true }
1396
+ : { ok: false, reason: result.reason };
1397
+ },
1398
+ bringElementsToFront,
1399
+ sendElementsToBack,
1400
+ bringElementsForward,
1401
+ sendElementsBackward,
1402
+ getZoom: () => zoomRef.current.x,
1403
+ setZoomAbsolute: (value) => {
1404
+ const next = Math.min(4, Math.max(0.25, value));
1405
+ const rounded = Number(next.toFixed(4));
1406
+ zoomRef.current = { x: rounded, y: rounded };
1407
+ setZoom({ x: rounded, y: rounded });
1408
+ },
1409
+ adjustZoomDelta: (delta) => {
1410
+ const next = Math.min(4, Math.max(0.25, zoomRef.current.x + delta));
1411
+ const rounded = Number(next.toFixed(4));
1412
+ zoomRef.current = { x: rounded, y: rounded };
1413
+ setZoom({ x: rounded, y: rounded });
1414
+ },
1415
+ fitViewport: () => {
1416
+ zoomRef.current = { x: 1, y: 1 };
1417
+ setZoom({ x: 1, y: 1 });
1418
+ },
1419
+ undo,
1420
+ redo,
1421
+ setBlueprintGraph,
1422
+ getBlueprintGraph: () => blueprintGraphRef.current,
1423
+ setBlueprintOpen,
1424
+ getBlueprintOpen: () => blueprintOpenRef.current,
1425
+ runBlueprintAll: () => blueprintDebugSession.runAll(),
1426
+ getWorkspace: () => ({ ...workspaceLiveRef.current }),
1427
+ workspaceSave: () => workspaceProjects.handleSaveProject(),
1428
+ workspaceSync: () => workspaceProjects.handleSyncProject(),
1429
+ workspaceCreate: async (name) => {
1430
+ if (name?.trim()) setProductName(name.trim());
1431
+ return workspaceProjects.handleCreateProject();
1432
+ },
1433
+ workspaceOpen: (id) => workspaceProjects.handleOpenProject(id),
1434
+ workspacePreview: async (id) => {
1435
+ const target =
1436
+ id ?? workspaceLiveRef.current.activeProjectId ?? undefined;
1437
+ if (!target) {
1438
+ const created = await workspaceProjects.handleCreateProject();
1439
+ await openOnlinePreviewForProject(created.id, { syncFirst: false });
1440
+ return;
1441
+ }
1442
+ await openOnlinePreviewForProject(target, { syncFirst: true });
1443
+ },
1444
+ exportPanelJson: () => JSON.stringify(exportPanelData(), null, 2),
1445
+ importPanelJson: (json) => {
1446
+ const parsed = JSON.parse(json) as State;
1447
+ const ok = importPanelData(parsed);
1448
+ if (ok) {
1449
+ selectedIdsRef.current = [];
1450
+ setSelectedIds([]);
1451
+ }
1452
+ return ok;
1453
+ },
1454
+ setTheme: (theme) => applyTheme(theme === "dark"),
1455
+ setLocale,
1456
+ setPanelFontSize,
1457
+ }),
1458
+ [
1459
+ addElementFromMaterial,
1460
+ updateElement,
1461
+ deleteElement,
1462
+ duplicateElement,
1463
+ selectViewElements,
1464
+ focusViewConfig,
1465
+ setActiveLayer,
1466
+ addLayer,
1467
+ renameLayer,
1468
+ toggleLayerLock,
1469
+ deleteLayer,
1470
+ bringElementsToFront,
1471
+ sendElementsToBack,
1472
+ bringElementsForward,
1473
+ sendElementsBackward,
1474
+ undo,
1475
+ redo,
1476
+ setBlueprintGraph,
1477
+ blueprintDebugSession,
1478
+ workspaceProjects,
1479
+ openOnlinePreviewForProject,
1480
+ exportPanelData,
1481
+ importPanelData,
1482
+ applyTheme,
1483
+ setLocale,
1484
+ ]
1485
+ );
1306
1486
  const showActionHint = useCallback((message: string) => {
1307
1487
  toast({
1308
- title: "操作受限",
1488
+ title: t("panel.messages.operationRestricted"),
1309
1489
  description: message,
1310
1490
  });
1311
1491
  }, []);
1312
1492
  const hintUnlockNodeForDelete = useCallback(() => {
1313
- showActionHint(PANEL_MESSAGES.nodeDeleteLocked);
1493
+ showActionHint(messages.nodeDeleteLocked);
1314
1494
  }, [showActionHint]);
1315
1495
  const hintUnlockLayerForDelete = useCallback(() => {
1316
- showActionHint(PANEL_MESSAGES.nodeDeleteLayerLocked);
1496
+ showActionHint(messages.nodeDeleteLayerLocked);
1317
1497
  }, [showActionHint]);
1318
1498
  const hintLockedNodesInBatchDelete = useCallback(() => {
1319
- showActionHint(PANEL_MESSAGES.nodeBatchDeleteContainsLocked);
1499
+ showActionHint(messages.nodeBatchDeleteContainsLocked);
1320
1500
  }, [showActionHint]);
1321
1501
  const hintNodeNotFound = useCallback(() => {
1322
- showActionHint(PANEL_MESSAGES.nodeNotFound);
1502
+ showActionHint(messages.nodeNotFound);
1323
1503
  }, [showActionHint]);
1324
1504
  const hintUnlockNodeForMove = useCallback(() => {
1325
- showActionHint(PANEL_MESSAGES.nodeMoveLocked);
1505
+ showActionHint(messages.nodeMoveLocked);
1326
1506
  }, [showActionHint]);
1327
1507
  const hintUnlockSourceLayerForMove = useCallback(() => {
1328
- showActionHint(PANEL_MESSAGES.nodeMoveSourceLayerLocked);
1508
+ showActionHint(messages.nodeMoveSourceLayerLocked);
1329
1509
  }, [showActionHint]);
1330
1510
  const hintTargetLayerNotFound = useCallback(() => {
1331
- showActionHint(PANEL_MESSAGES.targetLayerNotFound);
1511
+ showActionHint(messages.targetLayerNotFound);
1332
1512
  }, [showActionHint]);
1333
1513
  const hintUnlockTargetLayerForMove = useCallback(() => {
1334
- showActionHint(PANEL_MESSAGES.nodeMoveTargetLayerLocked);
1514
+ showActionHint(messages.nodeMoveTargetLayerLocked);
1335
1515
  }, [showActionHint]);
1336
1516
  const getLayerDeleteBlockReason = useCallback(
1337
1517
  (layerId: string) => {
1338
1518
  const targetLayer = layerById.get(layerId);
1339
- if (!targetLayer) return "图层不存在";
1340
- if (!targetLayer.editable) return "默认图层不可删除";
1341
- if (targetLayer.locked) return "锁定图层不可删除";
1519
+ if (!targetLayer) return t("panel.messages.layerNotFound");
1520
+ if (!targetLayer.editable) return t("panel.layers.cannotDeleteDefault");
1521
+ if (targetLayer.locked) return t("panel.layers.cannotDeleteLocked");
1342
1522
  if (targetLayer.isMapping) return null;
1343
1523
  const hasBlockingRef = allElements.some((el) => {
1344
1524
  if (el.layerId === layerId) return false;
@@ -1346,7 +1526,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1346
1526
  if (el.refLayerId !== layerId) return false;
1347
1527
  return (el.refCopyMode ?? "shallow") !== "deep";
1348
1528
  });
1349
- if (hasBlockingRef) return "该图层仍被浅拷贝引用,请先删除引用节点或改为深拷贝";
1529
+ if (hasBlockingRef) return t("panel.messages.shallowRefBlockingDelete");
1350
1530
  return null;
1351
1531
  },
1352
1532
  [allElements, layerById]
@@ -1397,7 +1577,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1397
1577
  const parsed = JSON.parse(text) as State;
1398
1578
  const ok = importPanelData(parsed);
1399
1579
  if (!ok) {
1400
- window.alert("导入失败:文件格式不正确");
1580
+ window.alert(messages.importInvalidFormat);
1401
1581
  } else {
1402
1582
  setSelectedIds([]);
1403
1583
  }
@@ -1556,7 +1736,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1556
1736
  const previewFaviconLink = faviconHrefWithVersion
1557
1737
  ? `<link rel="icon" type="image/png" href="${faviconHrefWithVersion}" /><link rel="shortcut icon" type="image/png" href="${faviconHrefWithVersion}" />`
1558
1738
  : "";
1559
- const previewTitle = productName.trim() || "未命名产物";
1739
+ const previewTitle = productName.trim() || t("panel.defaults.unnamedProduct");
1560
1740
  const escapedPreviewTitle = previewTitle
1561
1741
  .replace(/&/g, "&amp;")
1562
1742
  .replace(/</g, "&lt;")
@@ -1678,7 +1858,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1678
1858
  }
1679
1859
  } catch (error) {
1680
1860
  toast({
1681
- title: error instanceof Error ? error.message : "打开预览失败",
1861
+ title: error instanceof Error ? error.message : t("panel.messages.openPreviewFailed"),
1682
1862
  });
1683
1863
  }
1684
1864
  })();
@@ -1768,59 +1948,70 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1768
1948
  <nav className="panel-font-root relative z-30 flex items-center gap-2 border-b border-border bg-background/95 px-2 text-foreground">
1769
1949
  <Menubar className="h-8 border-0 bg-transparent p-0 shadow-none">
1770
1950
  <MenubarMenu>
1771
- <MenubarTrigger className="px-2 py-1 text-xs font-normal">文件</MenubarTrigger>
1951
+ <MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.file")}</MenubarTrigger>
1772
1952
  <MenubarContent className="z-[10100]">
1773
- {/* <MenubarItem onClick={handlePreviewLayer}>预览</MenubarItem> */}
1774
- <MenubarItem onClick={handleExport}>导出</MenubarItem>
1775
- <MenubarItem onClick={() => importInputRef.current?.click()}>导入</MenubarItem>
1953
+ {/* <MenubarItem onClick={handlePreviewLayer}>{t("panel.workspace.previewDocTitle")}</MenubarItem> */}
1954
+ <MenubarItem onClick={handleExport}>{t("panel.menubar.export")}</MenubarItem>
1955
+ <MenubarItem onClick={() => importInputRef.current?.click()}>{t("panel.menubar.import")}</MenubarItem>
1776
1956
  </MenubarContent>
1777
1957
  </MenubarMenu>
1778
1958
  <MenubarMenu>
1779
- <MenubarTrigger className="px-2 py-1 text-xs font-normal">编辑</MenubarTrigger>
1959
+ <MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.edit")}</MenubarTrigger>
1780
1960
  <MenubarContent className="z-[10100]">
1781
- <MenubarItem disabled={!canUndo} onClick={undo}>撤销</MenubarItem>
1782
- <MenubarItem disabled={!canRedo} onClick={redo}>重做</MenubarItem>
1961
+ <MenubarItem disabled={!canUndo} onClick={undo}>{t("panel.menubar.undo")}</MenubarItem>
1962
+ <MenubarItem disabled={!canRedo} onClick={redo}>{t("panel.menubar.redo")}</MenubarItem>
1783
1963
  <MenubarSeparator />
1784
- <MenubarItem disabled={!hasUnlockedSelection} onClick={() => bringElementsForward(selectedIds)}>上移一层</MenubarItem>
1785
- <MenubarItem disabled={!hasUnlockedSelection} onClick={() => sendElementsBackward(selectedIds)}>下移一层</MenubarItem>
1786
- <MenubarItem disabled={!hasUnlockedSelection} onClick={() => bringElementsToFront(selectedIds)}>置顶</MenubarItem>
1787
- <MenubarItem disabled={!hasUnlockedSelection} onClick={() => sendElementsToBack(selectedIds)}>置底</MenubarItem>
1964
+ <MenubarItem disabled={!hasUnlockedSelection} onClick={() => bringElementsForward(selectedIds)}>{t("panel.menubar.bringForward")}</MenubarItem>
1965
+ <MenubarItem disabled={!hasUnlockedSelection} onClick={() => sendElementsBackward(selectedIds)}>{t("panel.menubar.sendBackward")}</MenubarItem>
1966
+ <MenubarItem disabled={!hasUnlockedSelection} onClick={() => bringElementsToFront(selectedIds)}>{t("panel.menubar.bringToFront")}</MenubarItem>
1967
+ <MenubarItem disabled={!hasUnlockedSelection} onClick={() => sendElementsToBack(selectedIds)}>{t("panel.menubar.sendToBack")}</MenubarItem>
1788
1968
  </MenubarContent>
1789
1969
  </MenubarMenu>
1790
1970
  <MenubarMenu>
1791
- <MenubarTrigger className="px-2 py-1 text-xs font-normal">视图</MenubarTrigger>
1971
+ <MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.view")}</MenubarTrigger>
1792
1972
  <MenubarContent className="z-[10100]">
1793
- <MenubarItem onClick={() => adjustUniformZoom((z) => z - 0.1)}>缩小</MenubarItem>
1794
- <MenubarItem onClick={() => adjustUniformZoom((z) => z + 0.1)}>放大</MenubarItem>
1973
+ <MenubarItem onClick={() => adjustUniformZoom((z) => z - 0.1)}>{t("panel.menubar.zoomOut")}</MenubarItem>
1974
+ <MenubarItem onClick={() => adjustUniformZoom((z) => z + 0.1)}>{t("panel.menubar.zoomIn")}</MenubarItem>
1795
1975
  <MenubarSeparator />
1796
1976
  <MenubarItem
1797
1977
  onClick={() => {
1798
1978
  applyTheme(!isDark);
1799
1979
  }}
1800
1980
  >
1801
- {isDark ? "切换到浅色" : "切换到深色"}
1981
+ {isDark ? t("panel.menubar.switchToLight") : t("panel.menubar.switchToDark")}
1802
1982
  </MenubarItem>
1803
1983
  </MenubarContent>
1804
1984
  </MenubarMenu>
1805
1985
  <MenubarMenu>
1806
- <MenubarTrigger className="px-2 py-1 text-xs font-normal">蓝图</MenubarTrigger>
1986
+ <MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.blueprint")}</MenubarTrigger>
1807
1987
  <MenubarContent className="z-[10100]">
1808
- <MenubarItem onClick={openBlueprintExportDialog}>导出</MenubarItem>
1988
+ <MenubarItem onClick={openBlueprintExportDialog}>{t("panel.menubar.export")}</MenubarItem>
1809
1989
  <MenubarItem onClick={() => blueprintImportInputRef.current?.click()}>
1810
- 导入
1990
+ {t("panel.menubar.import")}
1811
1991
  </MenubarItem>
1812
1992
  </MenubarContent>
1813
1993
  </MenubarMenu>
1814
1994
  <MenubarMenu>
1815
- <MenubarTrigger className="px-2 py-1 text-xs font-normal">设置</MenubarTrigger>
1995
+ <MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.settings")}</MenubarTrigger>
1816
1996
  <MenubarContent className="z-[10100]">
1817
1997
  <MenubarRadioGroup
1818
1998
  value={panelFontSize}
1819
1999
  onValueChange={(value) => setPanelFontSize(value as "sm" | "md" | "lg")}
1820
2000
  >
1821
- <MenubarRadioItem value="sm">字体:小</MenubarRadioItem>
1822
- <MenubarRadioItem value="md">字体:中</MenubarRadioItem>
1823
- <MenubarRadioItem value="lg">字体:大</MenubarRadioItem>
2001
+ <MenubarRadioItem value="sm">{t("panel.menubar.fontSmall")}</MenubarRadioItem>
2002
+ <MenubarRadioItem value="md">{t("panel.menubar.fontMedium")}</MenubarRadioItem>
2003
+ <MenubarRadioItem value="lg">{t("panel.menubar.fontLarge")}</MenubarRadioItem>
2004
+ </MenubarRadioGroup>
2005
+ <MenubarSeparator />
2006
+ <div className="px-2 py-1.5 text-[11px] text-muted-foreground">
2007
+ {t("panel.theme.language")}
2008
+ </div>
2009
+ <MenubarRadioGroup
2010
+ value={locale}
2011
+ onValueChange={(value) => setLocale(value as "zh-CN" | "en-US")}
2012
+ >
2013
+ <MenubarRadioItem value="zh-CN">{t("panel.theme.zhCN")}</MenubarRadioItem>
2014
+ <MenubarRadioItem value="en-US">{t("panel.theme.enUS")}</MenubarRadioItem>
1824
2015
  </MenubarRadioGroup>
1825
2016
  </MenubarContent>
1826
2017
  </MenubarMenu>
@@ -1837,6 +2028,15 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1837
2028
  onPreviewProject={openOnlinePreviewForProject}
1838
2029
  />
1839
2030
  <div className="flex-1" />
2031
+ <Button
2032
+ type="button"
2033
+ variant={assistantOpen ? "secondary" : "outline"}
2034
+ size="sm"
2035
+ className="h-7 shrink-0 px-2 text-xs"
2036
+ onClick={() => setAssistantOpen((v) => !v)}
2037
+ >
2038
+ {assistantOpen ? t("panel.ai.hide") : t("panel.ai.open")}
2039
+ </Button>
1840
2040
  </nav>
1841
2041
  <ResizablePanelGroup direction="horizontal" className="min-h-0 w-full flex-1">
1842
2042
  <ResizablePanel defaultSize={15} minSize={15}>
@@ -1938,10 +2138,10 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1938
2138
  <Input
1939
2139
  value={productName}
1940
2140
  onChange={(e) => setProductName(e.target.value)}
1941
- placeholder="请输入当前构建产物名称"
2141
+ placeholder={t("panel.menubar.productNamePlaceholder")}
1942
2142
  className="h-7 w-[220px] text-xs focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none"
1943
2143
  />
1944
- <span className="text-[11px] text-muted-foreground">产物名称</span>
2144
+ <span className="text-[11px] text-muted-foreground">{t("panel.menubar.productName")}</span>
1945
2145
  <TooltipProvider delayDuration={150}>
1946
2146
  <Tooltip>
1947
2147
  <TooltipTrigger asChild>
@@ -1949,12 +2149,12 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1949
2149
  type="button"
1950
2150
  onClick={() => titleIconInputRef.current?.click()}
1951
2151
  className="rounded border border-border p-1 hover:bg-accent"
1952
- aria-label="上传 title 图标"
2152
+ aria-label={t("panel.menubar.uploadTitleIcon")}
1953
2153
  >
1954
2154
  <IconImage />
1955
2155
  </button>
1956
2156
  </TooltipTrigger>
1957
- <TooltipContent className="z-[10000]">上传 title 图标</TooltipContent>
2157
+ <TooltipContent className="z-[10000]">{t("panel.menubar.uploadTitleIcon")}</TooltipContent>
1958
2158
  </Tooltip>
1959
2159
  {titleIconDataUrl ? (
1960
2160
  <Tooltip>
@@ -1963,12 +2163,12 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1963
2163
  type="button"
1964
2164
  onClick={() => setTitleIconDataUrl("")}
1965
2165
  className="rounded border border-border p-1 hover:bg-accent"
1966
- aria-label="清除 title 图标"
2166
+ aria-label={t("panel.menubar.clearTitleIcon")}
1967
2167
  >
1968
2168
  <IconClose />
1969
2169
  </button>
1970
2170
  </TooltipTrigger>
1971
- <TooltipContent className="z-[10000]">清除 title 图标</TooltipContent>
2171
+ <TooltipContent className="z-[10000]">{t("panel.menubar.clearTitleIcon")}</TooltipContent>
1972
2172
  </Tooltip>
1973
2173
  ) : null}
1974
2174
  {titleIconDataUrl ? (
@@ -1978,17 +2178,17 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1978
2178
  type="button"
1979
2179
  onClick={() => setTitleIconPreviewOpen(true)}
1980
2180
  className="h-7 shrink-0 flex min-w-[92px] items-center gap-1.5 rounded border border-border bg-muted/30 px-2 py-1 hover:bg-accent/40"
1981
- aria-label="查看 title 图标大图"
2181
+ aria-label={t("panel.menubar.viewTitleIcon")}
1982
2182
  >
1983
2183
  <img
1984
2184
  src={titleIconDataUrl}
1985
- alt="title 图标缩略图"
2185
+ alt={t("panel.menubar.titleIconThumbAlt")}
1986
2186
  className="h-5 w-5 shrink-0 rounded border border-border/60 object-cover"
1987
2187
  />
1988
2188
  <span className="text-[10px] text-muted-foreground">Title Icon</span>
1989
2189
  </button>
1990
2190
  </TooltipTrigger>
1991
- <TooltipContent className="z-[10000]">点击查看 title 图标大图</TooltipContent>
2191
+ <TooltipContent className="z-[10000]">{t("panel.menubar.viewTitleIconHint")}</TooltipContent>
1992
2192
  </Tooltip>
1993
2193
  ) : null}
1994
2194
  <Tooltip>
@@ -1998,12 +2198,12 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
1998
2198
  onClick={undo}
1999
2199
  disabled={!canUndo}
2000
2200
  className="rounded border border-border p-1 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40"
2001
- aria-label="撤销"
2201
+ aria-label={t("panel.menubar.undo")}
2002
2202
  >
2003
2203
  <IconUndo />
2004
2204
  </button>
2005
2205
  </TooltipTrigger>
2006
- <TooltipContent className="z-[10000]">撤销(Cmd/Ctrl + Z)</TooltipContent>
2206
+ <TooltipContent className="z-[10000]">{t("panel.menubar.undoShortcut")}</TooltipContent>
2007
2207
  </Tooltip>
2008
2208
  <Tooltip>
2009
2209
  <TooltipTrigger asChild>
@@ -2012,12 +2212,12 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2012
2212
  onClick={redo}
2013
2213
  disabled={!canRedo}
2014
2214
  className="rounded border border-border p-1 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40"
2015
- aria-label="重做"
2215
+ aria-label={t("panel.menubar.redo")}
2016
2216
  >
2017
2217
  <IconRedo />
2018
2218
  </button>
2019
2219
  </TooltipTrigger>
2020
- <TooltipContent className="z-[10000]">重做(Cmd/Ctrl + Shift + Z)</TooltipContent>
2220
+ <TooltipContent className="z-[10000]">{t("panel.menubar.redoShortcut")}</TooltipContent>
2021
2221
  </Tooltip>
2022
2222
  <Tooltip>
2023
2223
  <TooltipTrigger asChild>
@@ -2029,7 +2229,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2029
2229
  "rounded border border-border p-1 hover:bg-accent",
2030
2230
  isHistoryPanelExpanded ? "bg-accent/60" : "",
2031
2231
  ].join(" ")}
2032
- aria-label={isHistoryPanelExpanded ? "收起操作历史" : "展开操作历史"}
2232
+ aria-label={isHistoryPanelExpanded ? t("panel.history.collapse") : t("panel.history.expand")}
2033
2233
  >
2034
2234
  <IconHistory />
2035
2235
  </button>
@@ -2039,7 +2239,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2039
2239
  style={{ zIndex: PANEL_Z_INDEX.historyPopover }}
2040
2240
  >
2041
2241
  <div className="mb-1 flex items-center justify-between">
2042
- <span className="text-[11px] font-semibold text-muted-foreground">操作历史</span>
2242
+ <span className="text-[11px] font-semibold text-muted-foreground">{t("panel.history.title")}</span>
2043
2243
  <span className="text-[11px] text-muted-foreground">
2044
2244
  {history.length > 0 ? `${historyCursor + 1}/${history.length}` : "0/0"}
2045
2245
  </span>
@@ -2047,7 +2247,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2047
2247
  <Input
2048
2248
  value={historyKeyword}
2049
2249
  onChange={(e) => setHistoryKeyword(e.target.value)}
2050
- placeholder="搜索历史,如:删除、图层、缩放..."
2250
+ placeholder={t("panel.history.searchPlaceholder")}
2051
2251
  className="mb-2 h-7 text-xs"
2052
2252
  />
2053
2253
  <div
@@ -2058,9 +2258,9 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2058
2258
  <EmptyIcon className="h-8 w-8">
2059
2259
  <IconHistory />
2060
2260
  </EmptyIcon>
2061
- <EmptyTitle className="text-xs">暂无操作历史</EmptyTitle>
2261
+ <EmptyTitle className="text-xs">{t("panel.history.emptyTitle")}</EmptyTitle>
2062
2262
  <EmptyDescription className="text-[11px]">
2063
- 进行一次操作后,这里会展示历史记录。
2263
+ {t("panel.history.emptyDesc")}
2064
2264
  </EmptyDescription>
2065
2265
  </Empty>
2066
2266
  ) : (
@@ -2091,7 +2291,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2091
2291
  <div className="flex items-center justify-between gap-2">
2092
2292
  <span className="truncate">{item.label}</span>
2093
2293
  <span className="shrink-0 text-[10px] text-muted-foreground">
2094
- {formatRelativeTime(item.timestamp, historyNow)}
2294
+ {formatRelativeTime(item.timestamp, historyNow, t)}
2095
2295
  </span>
2096
2296
  </div>
2097
2297
  </button>
@@ -2120,9 +2320,9 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2120
2320
  <path d="m20 20-3.5-3.5" />
2121
2321
  </svg>
2122
2322
  </EmptyIcon>
2123
- <EmptyTitle className="text-xs">未匹配到历史项</EmptyTitle>
2323
+ <EmptyTitle className="text-xs">{t("panel.history.noMatchTitle")}</EmptyTitle>
2124
2324
  <EmptyDescription className="text-[11px]">
2125
- 尝试更换关键词,例如删除、图层、缩放。
2325
+ {t("panel.history.noMatchDesc")}
2126
2326
  </EmptyDescription>
2127
2327
  </Empty>
2128
2328
  ) : null}
@@ -2132,7 +2332,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2132
2332
  </div>
2133
2333
  </TooltipTrigger>
2134
2334
  <TooltipContent className="z-[10000]">
2135
- {isHistoryPanelExpanded ? "收起操作历史" : "展开操作历史"}
2335
+ {isHistoryPanelExpanded ? t("panel.history.collapse") : t("panel.history.expand")}
2136
2336
  </TooltipContent>
2137
2337
  </Tooltip>
2138
2338
  </TooltipProvider>
@@ -2142,7 +2342,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2142
2342
  type="button"
2143
2343
  disabled={!hasUnlockedSelection}
2144
2344
  className="rounded border border-border p-1 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40"
2145
- aria-label="节点层级操作"
2345
+ aria-label={t("panel.menubar.nodeZOrderAria")}
2146
2346
  >
2147
2347
  <IconLayers />
2148
2348
  </button>
@@ -2153,28 +2353,28 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2153
2353
  onClick={() => bringElementsForward(selectedIds)}
2154
2354
  >
2155
2355
  <span className="mr-2 inline-flex"><IconForward /></span>
2156
- 上移一层
2356
+ {t("panel.menubar.bringForward")}
2157
2357
  </DropdownMenuItem>
2158
2358
  <DropdownMenuItem
2159
2359
  disabled={!hasUnlockedSelection}
2160
2360
  onClick={() => sendElementsBackward(selectedIds)}
2161
2361
  >
2162
2362
  <span className="mr-2 inline-flex"><IconBackward /></span>
2163
- 下移一层
2363
+ {t("panel.menubar.sendBackward")}
2164
2364
  </DropdownMenuItem>
2165
2365
  <DropdownMenuItem
2166
2366
  disabled={!hasUnlockedSelection}
2167
2367
  onClick={() => bringElementsToFront(selectedIds)}
2168
2368
  >
2169
2369
  <span className="mr-2 inline-flex"><IconBringFront /></span>
2170
- 置顶
2370
+ {t("panel.menubar.bringToFront")}
2171
2371
  </DropdownMenuItem>
2172
2372
  <DropdownMenuItem
2173
2373
  disabled={!hasUnlockedSelection}
2174
2374
  onClick={() => sendElementsToBack(selectedIds)}
2175
2375
  >
2176
2376
  <span className="mr-2 inline-flex"><IconSendBack /></span>
2177
- 置底
2377
+ {t("panel.menubar.sendToBack")}
2178
2378
  </DropdownMenuItem>
2179
2379
  </DropdownMenuContent>
2180
2380
  </DropdownMenu>
@@ -2183,11 +2383,11 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2183
2383
  data-blueprint-toggle
2184
2384
  className="flex shrink-0 items-center gap-2 rounded-md border border-border bg-muted/30 px-2 py-1"
2185
2385
  >
2186
- <span className="text-[11px] text-muted-foreground">蓝图</span>
2386
+ <span className="text-[11px] text-muted-foreground">{t("panel.menubar.blueprint")}</span>
2187
2387
  <Switch
2188
2388
  checked={blueprintOpen}
2189
2389
  onCheckedChange={setBlueprintOpen}
2190
- aria-label="显示蓝图面板"
2390
+ aria-label={t("panel.menubar.showBlueprintPanel")}
2191
2391
  />
2192
2392
  </div>
2193
2393
  <TooltipProvider delayDuration={150}>
@@ -2197,7 +2397,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2197
2397
  <Switch
2198
2398
  checked={isDark}
2199
2399
  onCheckedChange={applyTheme}
2200
- aria-label="切换主题"
2400
+ aria-label={t("panel.menubar.toggleTheme")}
2201
2401
  className="data-[state=checked]:bg-primary/80 data-[state=unchecked]:bg-secondary"
2202
2402
  />
2203
2403
  <div
@@ -2231,7 +2431,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2231
2431
  </div>
2232
2432
  </div>
2233
2433
  </TooltipTrigger>
2234
- <TooltipContent className="z-[10000]">切换深色/浅色主题</TooltipContent>
2434
+ <TooltipContent className="z-[10000]">{t("panel.menubar.toggleThemeHint")}</TooltipContent>
2235
2435
  </Tooltip>
2236
2436
  <Tooltip>
2237
2437
  <TooltipTrigger asChild>
@@ -2243,7 +2443,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2243
2443
  -
2244
2444
  </button>
2245
2445
  </TooltipTrigger>
2246
- <TooltipContent className="z-[10000]">缩小画布</TooltipContent>
2446
+ <TooltipContent className="z-[10000]">{t("panel.menubar.zoomOutCanvas")}</TooltipContent>
2247
2447
  </Tooltip>
2248
2448
  </TooltipProvider>
2249
2449
  <span className="w-20 text-center text-xs">
@@ -2262,7 +2462,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2262
2462
  +
2263
2463
  </button>
2264
2464
  </TooltipTrigger>
2265
- <TooltipContent className="z-[10000]">放大画布</TooltipContent>
2465
+ <TooltipContent className="z-[10000]">{t("panel.menubar.zoomInCanvas")}</TooltipContent>
2266
2466
  </Tooltip>
2267
2467
  </TooltipProvider>
2268
2468
  </div>
@@ -2278,7 +2478,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2278
2478
  try {
2279
2479
  await handleImportFile(file);
2280
2480
  } catch {
2281
- window.alert("导入失败:JSON 解析错误");
2481
+ window.alert(messages.importJsonParseError);
2282
2482
  }
2283
2483
  }}
2284
2484
  />
@@ -2294,7 +2494,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2294
2494
  try {
2295
2495
  await handleBlueprintImportFile(file);
2296
2496
  } catch {
2297
- window.alert("蓝图导入失败:文件格式不正确");
2497
+ window.alert(messages.blueprintImportInvalid);
2298
2498
  }
2299
2499
  }}
2300
2500
  />
@@ -2311,7 +2511,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2311
2511
  const normalizedDataUrl = await normalizeTitleIconFile(file);
2312
2512
  setTitleIconDataUrl(normalizedDataUrl);
2313
2513
  } catch {
2314
- window.alert("图标读取失败,请重试");
2514
+ window.alert(messages.iconReadFailed);
2315
2515
  }
2316
2516
  }}
2317
2517
  />
@@ -2427,7 +2627,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2427
2627
  }}
2428
2628
  onDropMaterial={({ materialId, x, y }) => {
2429
2629
  if (activeLayer?.locked) {
2430
- showActionHint(PANEL_MESSAGES.activeLayerLockedCannotAdd);
2630
+ showActionHint(messages.activeLayerLockedCannotAdd);
2431
2631
  return;
2432
2632
  }
2433
2633
  addElementFromMaterial(materialId, x, y);
@@ -2441,6 +2641,9 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2441
2641
  onSelectIds={selectViewElements}
2442
2642
  updateElement={updateElement}
2443
2643
  layerLocked={Boolean(activeLayer?.locked)}
2644
+ onTableCellAction={(payload) => {
2645
+ void triggerBlueprintNode(payload.blueprintNodeId, payload);
2646
+ }}
2444
2647
  />
2445
2648
 
2446
2649
  <SelectLayer
@@ -2477,13 +2680,13 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2477
2680
  if (!node) return;
2478
2681
  const layer = layerById.get(node.layerId);
2479
2682
  if (layer?.locked) {
2480
- showActionHint(PANEL_MESSAGES.nodeLayerLockedCannotCopy);
2683
+ showActionHint(messages.nodeLayerLockedCannotCopy);
2481
2684
  return;
2482
2685
  }
2483
2686
  setCopiedNodeId(contextMenuNodeId);
2484
2687
  }}
2485
2688
  >
2486
- 复制节点
2689
+ {t("panel.menubar.duplicateNode")}
2487
2690
  </DropdownMenuItem>
2488
2691
  {contextMenuNodeId &&
2489
2692
  byId.get(contextMenuNodeId)?.mappingSourceNodeId ? (
@@ -2495,7 +2698,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2495
2698
  if (!sourceNodeId) return;
2496
2699
  const sourceNode = byId.get(sourceNodeId);
2497
2700
  if (!sourceNode) {
2498
- showActionHint(PANEL_MESSAGES.sourceNodeNotFound);
2701
+ showActionHint(messages.sourceNodeNotFound);
2499
2702
  return;
2500
2703
  }
2501
2704
  if (activeLayerId !== sourceNode.layerId) {
@@ -2505,7 +2708,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2505
2708
  setContextMenuNodeId(null);
2506
2709
  }}
2507
2710
  >
2508
- 定位到源节点
2711
+ {t("panel.menubar.locateSourceNode")}
2509
2712
  </DropdownMenuItem>
2510
2713
  ) : null}
2511
2714
  <DropdownMenuItem
@@ -2522,7 +2725,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2522
2725
  setContextMenuNodeId(null);
2523
2726
  }}
2524
2727
  >
2525
- 在映射图层打开
2728
+ {t("panel.menubar.openInMappingLayer")}
2526
2729
  </DropdownMenuItem>
2527
2730
  <DropdownMenuItem
2528
2731
  onClick={() => {
@@ -2553,7 +2756,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2553
2756
  return !!layerById.get(el.layerId)?.locked;
2554
2757
  });
2555
2758
  if (hasLockedLayerNode) {
2556
- showActionHint(PANEL_MESSAGES.selectionContainsLockedLayerNode);
2759
+ showActionHint(messages.selectionContainsLockedLayerNode);
2557
2760
  return;
2558
2761
  }
2559
2762
  deleteElements(selectedIds);
@@ -2577,8 +2780,8 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2577
2780
  }}
2578
2781
  >
2579
2782
  {contextMenuNodeId && selectedIds.includes(contextMenuNodeId) && selectedIds.length > 1
2580
- ? `删除选中节点(${selectedIds.length})`
2581
- : "删除节点"}
2783
+ ? t("panel.menubar.deleteSelectedNodes", { count: selectedIds.length })
2784
+ : t("panel.menubar.deleteNode")}
2582
2785
  </DropdownMenuItem>
2583
2786
  </>
2584
2787
  ) : (
@@ -2589,7 +2792,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2589
2792
  if (!copiedNodeId) return;
2590
2793
  const sourceNode = byId.get(copiedNodeId);
2591
2794
  if (sourceNode && layerById.get(sourceNode.layerId)?.locked) {
2592
- showActionHint(PANEL_MESSAGES.sourceLayerLockedCannotPaste);
2795
+ showActionHint(messages.sourceLayerLockedCannotPaste);
2593
2796
  return;
2594
2797
  }
2595
2798
  const canvasEl = canvasRef.current;
@@ -2623,7 +2826,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2623
2826
  );
2624
2827
  }}
2625
2828
  >
2626
- 粘贴节点
2829
+ {t("panel.menubar.pasteNode")}
2627
2830
  </DropdownMenuItem>
2628
2831
  <DropdownMenuItem
2629
2832
  disabled={elements.length === 0}
@@ -2631,7 +2834,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2631
2834
  selectViewElements(elements.map((el) => el.id));
2632
2835
  }}
2633
2836
  >
2634
- 全选当前图层
2837
+ {t("panel.menubar.selectAllInLayer")}
2635
2838
  </DropdownMenuItem>
2636
2839
  </>
2637
2840
  )}
@@ -2645,20 +2848,20 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2645
2848
  <TooltipProvider delayDuration={120}>
2646
2849
  <Tabs value={activeLayerId} onValueChange={setActiveLayer}>
2647
2850
  <div className="mb-1 flex items-center gap-2">
2648
- <div className="text-[11px] font-semibold text-muted-foreground">图层</div>
2851
+ <div className="text-[11px] font-semibold text-muted-foreground">{t("panel.layers.title")}</div>
2649
2852
  <Tooltip>
2650
2853
  <TooltipTrigger asChild>
2651
2854
  <button
2652
2855
  type="button"
2653
2856
  onClick={() => setIsLayerPanelExpanded((v) => !v)}
2654
- aria-label={isLayerPanelExpanded ? "收起图层输入区" : "展开图层输入区"}
2857
+ aria-label={isLayerPanelExpanded ? t("panel.layers.collapseInputAria") : t("panel.layers.expandInputAria")}
2655
2858
  className="rounded border border-border p-1 hover:bg-accent"
2656
2859
  >
2657
2860
  <IconChevron expanded={isLayerPanelExpanded} />
2658
2861
  </button>
2659
2862
  </TooltipTrigger>
2660
2863
  <TooltipContent className="z-[10000]">
2661
- {isLayerPanelExpanded ? "收起输入区" : "展开输入区"}
2864
+ {isLayerPanelExpanded ? t("panel.layers.collapseInput") : t("panel.layers.expandInput")}
2662
2865
  </TooltipContent>
2663
2866
  </Tooltip>
2664
2867
  <Tooltip>
@@ -2666,13 +2869,13 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2666
2869
  <button
2667
2870
  type="button"
2668
2871
  onClick={addLayer}
2669
- aria-label="新增图层"
2872
+ aria-label={t("panel.layers.add")}
2670
2873
  className="rounded border border-border p-1 hover:bg-accent"
2671
2874
  >
2672
2875
  <IconPlus />
2673
2876
  </button>
2674
2877
  </TooltipTrigger>
2675
- <TooltipContent className="z-[10000]">新增图层</TooltipContent>
2878
+ <TooltipContent className="z-[10000]">{t("panel.layers.add")}</TooltipContent>
2676
2879
  </Tooltip>
2677
2880
  <Tooltip>
2678
2881
  <TooltipTrigger asChild>
@@ -2682,7 +2885,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2682
2885
  if (!canMergeLayers) return;
2683
2886
  setIsMergingLayers(true);
2684
2887
  }}
2685
- aria-label="合并图层"
2888
+ aria-label={t("panel.layers.merge")}
2686
2889
  disabled={!canMergeLayers}
2687
2890
  className="rounded border border-border p-1 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40"
2688
2891
  >
@@ -2690,7 +2893,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2690
2893
  </button>
2691
2894
  </TooltipTrigger>
2692
2895
  <TooltipContent className="z-[10000]">
2693
- {canMergeLayers ? "合并图层" : "至少勾选 2 个图层后可合并"}
2896
+ {canMergeLayers ? t("panel.layers.merge") : t("panel.layers.mergeNeedTwo")}
2694
2897
  </TooltipContent>
2695
2898
  </Tooltip>
2696
2899
  </div>
@@ -2707,12 +2910,12 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2707
2910
  <span className="truncate">{layer.name}</span>
2708
2911
  {layer.isMapping ? (
2709
2912
  <span className="rounded border border-primary/40 bg-primary/10 px-1 text-[10px] text-primary">
2710
- 映射
2913
+ {t("panel.layers.mapping")}
2711
2914
  </span>
2712
2915
  ) : null}
2713
2916
  {layer.isPrimary ? (
2714
2917
  <span className="rounded border border-emerald-500/40 bg-emerald-500/10 px-1 text-[10px] text-emerald-600">
2715
- 主
2918
+ {t("panel.layers.primary")}
2716
2919
  </span>
2717
2920
  ) : null}
2718
2921
  {layer.locked ? <IconLock locked /> : null}
@@ -2742,7 +2945,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2742
2945
  }
2743
2946
  }}
2744
2947
  className="h-7 min-w-0 flex-1"
2745
- placeholder="请输入图层名称"
2948
+ placeholder={t("panel.layers.namePlaceholder")}
2746
2949
  autoFocus
2747
2950
  />
2748
2951
  ) : (
@@ -2750,12 +2953,12 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2750
2953
  )}
2751
2954
  {activeLayer.isMapping ? (
2752
2955
  <span className="rounded border border-primary/40 bg-primary/10 px-1 text-[10px] text-primary">
2753
- 映射图层
2956
+ {t("panel.layers.mappingLayer")}
2754
2957
  </span>
2755
2958
  ) : null}
2756
2959
  {activeLayer.isPrimary ? (
2757
2960
  <span className="rounded border border-emerald-500/40 bg-emerald-500/10 px-1 text-[10px] text-emerald-600">
2758
- 主图层
2961
+ {t("panel.layers.primaryLayer")}
2759
2962
  </span>
2760
2963
  ) : null}
2761
2964
  <div className="flex-1" />
@@ -2765,7 +2968,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2765
2968
  type="button"
2766
2969
  onClick={() => setPrimaryLayer(activeLayer.id)}
2767
2970
  disabled={activeLayer.isPrimary}
2768
- aria-label={activeLayer.isPrimary ? "当前主图层" : "设为主图层"}
2971
+ aria-label={activeLayer.isPrimary ? t("panel.layers.isPrimary") : t("panel.layers.setPrimary")}
2769
2972
  className={[
2770
2973
  "rounded border px-2 py-1 text-[11px] disabled:cursor-not-allowed disabled:opacity-60",
2771
2974
  activeLayer.isPrimary
@@ -2773,11 +2976,11 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2773
2976
  : "border-border hover:bg-accent",
2774
2977
  ].join(" ")}
2775
2978
  >
2776
- {activeLayer.isPrimary ? "当前主图层" : "设为主图层"}
2979
+ {activeLayer.isPrimary ? t("panel.layers.isPrimary") : t("panel.layers.setPrimary")}
2777
2980
  </button>
2778
2981
  </TooltipTrigger>
2779
2982
  <TooltipContent className="z-[10000]">
2780
- {activeLayer.isPrimary ? "当前主图层(预览使用)" : "设为主图层(预览使用)"}
2983
+ {activeLayer.isPrimary ? t("panel.layers.isPrimaryHint") : t("panel.layers.setPrimaryHint")}
2781
2984
  </TooltipContent>
2782
2985
  </Tooltip>
2783
2986
  <Tooltip>
@@ -2786,14 +2989,14 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2786
2989
  type="button"
2787
2990
  onClick={() => toggleLayerLock(activeLayer.id)}
2788
2991
  disabled={!activeLayer.editable}
2789
- aria-label={activeLayer.locked ? "解锁图层" : "锁定图层"}
2992
+ aria-label={activeLayer.locked ? t("panel.layers.unlock") : t("panel.layers.lock")}
2790
2993
  className="rounded border border-border p-1 disabled:opacity-40"
2791
2994
  >
2792
2995
  <IconLock locked={activeLayer.locked} />
2793
2996
  </button>
2794
2997
  </TooltipTrigger>
2795
2998
  <TooltipContent className="z-[10000]">
2796
- {activeLayer.locked ? "解锁图层" : "锁定图层"}
2999
+ {activeLayer.locked ? t("panel.layers.unlock") : t("panel.layers.lock")}
2797
3000
  </TooltipContent>
2798
3001
  </Tooltip>
2799
3002
  <Tooltip>
@@ -2806,7 +3009,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2806
3009
  setEditingLayerId(null);
2807
3010
  setEditingLayerName("");
2808
3011
  }}
2809
- aria-label="保存图层名称"
3012
+ aria-label={t("panel.layers.saveName")}
2810
3013
  className="rounded border border-border p-1 hover:bg-accent"
2811
3014
  >
2812
3015
  <IconCheck />
@@ -2819,7 +3022,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2819
3022
  setEditingLayerName(activeLayer.name);
2820
3023
  }}
2821
3024
  disabled={!activeLayer.editable}
2822
- aria-label="重命名图层"
3025
+ aria-label={t("panel.layers.rename")}
2823
3026
  className="rounded border border-border p-1 disabled:opacity-40"
2824
3027
  >
2825
3028
  <IconEdit />
@@ -2827,7 +3030,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2827
3030
  )}
2828
3031
  </TooltipTrigger>
2829
3032
  <TooltipContent className="z-[10000]">
2830
- {editingLayerId === activeLayer.id ? "保存图层名称" : "重命名图层"}
3033
+ {editingLayerId === activeLayer.id ? t("panel.layers.saveName") : t("panel.layers.rename")}
2831
3034
  </TooltipContent>
2832
3035
  </Tooltip>
2833
3036
  {editingLayerId === activeLayer.id ? (
@@ -2839,13 +3042,13 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2839
3042
  setEditingLayerId(null);
2840
3043
  setEditingLayerName("");
2841
3044
  }}
2842
- aria-label="取消编辑图层名称"
3045
+ aria-label={t("panel.layers.cancelEditName")}
2843
3046
  className="rounded border border-border p-1 hover:bg-accent"
2844
3047
  >
2845
3048
  <IconClose />
2846
3049
  </button>
2847
3050
  </TooltipTrigger>
2848
- <TooltipContent className="z-[10000]">取消编辑</TooltipContent>
3051
+ <TooltipContent className="z-[10000]">{t("panel.layers.cancelEdit")}</TooltipContent>
2849
3052
  </Tooltip>
2850
3053
  ) : null}
2851
3054
  <Tooltip>
@@ -2866,10 +3069,10 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2866
3069
  disabled={!activeLayer.editable || activeLayer.locked}
2867
3070
  aria-label={
2868
3071
  !activeLayer.editable
2869
- ? "默认图层不可删除"
3072
+ ? t("panel.layers.cannotDeleteDefault")
2870
3073
  : activeLayer.locked
2871
- ? "锁定图层不可删除"
2872
- : "删除图层"
3074
+ ? t("panel.layers.cannotDeleteLocked")
3075
+ : t("panel.layers.delete")
2873
3076
  }
2874
3077
  className="rounded border border-border p-1 disabled:opacity-40"
2875
3078
  >
@@ -2878,10 +3081,10 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2878
3081
  </TooltipTrigger>
2879
3082
  <TooltipContent className="z-[10000]">
2880
3083
  {!activeLayer.editable
2881
- ? "默认图层不可删除"
3084
+ ? t("panel.layers.cannotDeleteDefault")
2882
3085
  : activeLayer.locked
2883
- ? "锁定图层不可删除"
2884
- : "删除图层"}
3086
+ ? t("panel.layers.cannotDeleteLocked")
3087
+ : t("panel.layers.delete")}
2885
3088
  </TooltipContent>
2886
3089
  </Tooltip>
2887
3090
  </div>
@@ -2889,7 +3092,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2889
3092
 
2890
3093
  {isLayerPanelExpanded ? (
2891
3094
  <div className="rounded border border-border bg-card px-2 py-1.5 text-xs">
2892
- <div className="mb-1 text-muted-foreground">选择合并图层</div>
3095
+ <div className="mb-1 text-muted-foreground">{t("panel.layers.selectMergeLayers")}</div>
2893
3096
  <div className="flex flex-wrap gap-2">
2894
3097
  {layers.map((layer) => (
2895
3098
  <label key={layer.id} className="flex items-center gap-1.5">
@@ -2901,10 +3104,10 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2901
3104
  />
2902
3105
  <span
2903
3106
  className="max-w-[140px] truncate"
2904
- title={layer.isMapping ? "映射图层不允许合并" : layer.name}
3107
+ title={layer.isMapping ? t("panel.layers.mappingCannotMerge") : layer.name}
2905
3108
  >
2906
3109
  {layer.name}
2907
- {layer.isMapping ? "(不可合并)" : ""}
3110
+ {layer.isMapping ? t("panel.layers.cannotMergeSuffix") : ""}
2908
3111
  </span>
2909
3112
  </label>
2910
3113
  ))}
@@ -2916,12 +3119,12 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2916
3119
 
2917
3120
  {isLayerPanelExpanded && isMergingLayers ? (
2918
3121
  <div className="mt-2 flex items-center gap-2 rounded border border-border bg-card px-2 py-1.5 text-xs">
2919
- <span className="text-muted-foreground">合并后图层名</span>
3122
+ <span className="text-muted-foreground">{t("panel.layers.mergedName")}</span>
2920
3123
  <Input
2921
3124
  value={mergeLayerName}
2922
3125
  onChange={(e) => setMergeLayerName(e.target.value)}
2923
3126
  className="h-7 min-w-0 flex-1"
2924
- placeholder="可空,留空将随机命名"
3127
+ placeholder={t("panel.layers.mergedNamePlaceholder")}
2925
3128
  autoFocus
2926
3129
  />
2927
3130
  <Tooltip>
@@ -2934,14 +3137,14 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2934
3137
  setIsMergingLayers(false);
2935
3138
  setMergeLayerName("");
2936
3139
  }}
2937
- aria-label="确认合并图层"
3140
+ aria-label={t("panel.layers.confirmMergeAria")}
2938
3141
  disabled={!canMergeLayers}
2939
3142
  className="rounded border border-border p-1 hover:bg-accent"
2940
3143
  >
2941
3144
  <IconCheck />
2942
3145
  </button>
2943
3146
  </TooltipTrigger>
2944
- <TooltipContent className="z-[10000]">确认合并</TooltipContent>
3147
+ <TooltipContent className="z-[10000]">{t("panel.layers.confirmMerge")}</TooltipContent>
2945
3148
  </Tooltip>
2946
3149
  <Tooltip>
2947
3150
  <TooltipTrigger asChild>
@@ -2951,13 +3154,13 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2951
3154
  setIsMergingLayers(false);
2952
3155
  setMergeLayerName("");
2953
3156
  }}
2954
- aria-label="取消合并图层"
3157
+ aria-label={t("panel.layers.cancelMergeAria")}
2955
3158
  className="rounded border border-border p-1 hover:bg-accent"
2956
3159
  >
2957
3160
  <IconClose />
2958
3161
  </button>
2959
3162
  </TooltipTrigger>
2960
- <TooltipContent className="z-[10000]">取消合并</TooltipContent>
3163
+ <TooltipContent className="z-[10000]">{t("panel.layers.cancelMerge")}</TooltipContent>
2961
3164
  </Tooltip>
2962
3165
  </div>
2963
3166
  ) : null}
@@ -2981,6 +3184,10 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
2981
3184
  }
2982
3185
  onUpdateBlueprintNode={handleUpdateBlueprintNode}
2983
3186
  blueprintLibraryOptions={blueprintLibraryOptions}
3187
+ blueprintNodeOptions={blueprintGraph.document.nodes.map((node) => ({
3188
+ id: node.id,
3189
+ label: `${node.label || node.id}${node.nodeType ? ` (${node.nodeType})` : ""}`,
3190
+ }))}
2984
3191
  selectedElement={selectedElement}
2985
3192
  selectedElements={selectedElements}
2986
3193
  viewElementScope={selectedElementScope}
@@ -3015,6 +3222,11 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
3015
3222
  </div>
3016
3223
  </ResizablePanel>
3017
3224
  </ResizablePanelGroup>
3225
+ <AssistantChatPanel
3226
+ deps={assistantDeps}
3227
+ open={assistantOpen}
3228
+ onOpenChange={setAssistantOpen}
3229
+ />
3018
3230
  <AlertDialog
3019
3231
  open={mappingDeleteConfirmOpen}
3020
3232
  onOpenChange={(open) => {
@@ -3024,13 +3236,15 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
3024
3236
  >
3025
3237
  <AlertDialogContent overlayClassName="bg-transparent pointer-events-none">
3026
3238
  <AlertDialogHeader>
3027
- <AlertDialogTitle>确认删除</AlertDialogTitle>
3239
+ <AlertDialogTitle>{t("panel.layers.confirmDeleteTitle")}</AlertDialogTitle>
3028
3240
  <AlertDialogDescription>
3029
- 将删除 {mappingDeleteImpactCount} 个关联映射图层,是否继续?
3241
+ {t("panel.layers.confirmDeleteMappingBody", {
3242
+ count: mappingDeleteImpactCount,
3243
+ })}
3030
3244
  </AlertDialogDescription>
3031
3245
  </AlertDialogHeader>
3032
3246
  <AlertDialogFooter>
3033
- <AlertDialogCancel>取消</AlertDialogCancel>
3247
+ <AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
3034
3248
  <AlertDialogAction
3035
3249
  onClick={() => {
3036
3250
  const fn = mappingDeleteProceedRef.current;
@@ -3039,7 +3253,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
3039
3253
  fn?.();
3040
3254
  }}
3041
3255
  >
3042
- 继续删除
3256
+ {t("panel.layers.continueDelete")}
3043
3257
  </AlertDialogAction>
3044
3258
  </AlertDialogFooter>
3045
3259
  </AlertDialogContent>
@@ -3058,9 +3272,11 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
3058
3272
  overlayClassName="bg-transparent pointer-events-none"
3059
3273
  >
3060
3274
  <DialogHeader>
3061
- <DialogTitle>确认删除图层</DialogTitle>
3275
+ <DialogTitle>{t("panel.layers.confirmDeleteLayerTitle")}</DialogTitle>
3062
3276
  <DialogDescription>
3063
- 即将删除图层:{deletingLayer?.name ?? "-"}
3277
+ {t("panel.layers.aboutToDeleteLayer", {
3278
+ name: deletingLayer?.name ?? "-",
3279
+ })}
3064
3280
  </DialogDescription>
3065
3281
  </DialogHeader>
3066
3282
  {deletingLayer ? (
@@ -3073,7 +3289,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
3073
3289
  <div className="flex items-center gap-3">
3074
3290
  <label className="flex items-center gap-1.5">
3075
3291
  <RadioGroupItem value="move" />
3076
- 节点迁移到
3292
+ {t("panel.layers.migrateNodesTo")}
3077
3293
  </label>
3078
3294
  <Select
3079
3295
  value={deleteTargetLayerId || "__none__"}
@@ -3083,10 +3299,10 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
3083
3299
  disabled={deletingLayerMode !== "move" || deleteTargetCandidates.length === 0}
3084
3300
  >
3085
3301
  <SelectTrigger className="h-8 w-[220px]">
3086
- <SelectValue placeholder="选择目标图层" />
3302
+ <SelectValue placeholder={t("panel.layers.selectTargetLayer")} />
3087
3303
  </SelectTrigger>
3088
3304
  <SelectContent>
3089
- <SelectItem value="__none__">请选择图层</SelectItem>
3305
+ <SelectItem value="__none__">{t("panel.layers.pleaseSelectLayer")}</SelectItem>
3090
3306
  {deleteTargetCandidates.map((l) => (
3091
3307
  <SelectItem key={l.id} value={l.id}>
3092
3308
  {l.name}
@@ -3098,7 +3314,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
3098
3314
  ) : null}
3099
3315
  <label className="flex items-center gap-1.5">
3100
3316
  <RadioGroupItem value="remove" />
3101
- {deletingLayer.isMapping ? "删除映射图层及其节点" : "同时删除该图层下所有节点"}
3317
+ {deletingLayer.isMapping ? t("panel.layers.deleteMappingWithNodes") : t("panel.layers.deleteAllNodesToo")}
3102
3318
  </label>
3103
3319
  </RadioGroup>
3104
3320
  ) : null}
@@ -3112,7 +3328,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
3112
3328
  }}
3113
3329
  className="rounded border border-border px-3 py-1.5 text-sm hover:bg-accent"
3114
3330
  >
3115
- 取消
3331
+ {t("common.cancel")}
3116
3332
  </button>
3117
3333
  <button
3118
3334
  type="button"
@@ -3132,7 +3348,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
3132
3348
  }}
3133
3349
  className="rounded bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90"
3134
3350
  >
3135
- 确认删除
3351
+ {t("panel.layers.confirmDeleteTitle")}
3136
3352
  </button>
3137
3353
  </DialogFooter>
3138
3354
  </DialogContent>
@@ -3146,8 +3362,8 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
3146
3362
  overlayClassName="bg-transparent pointer-events-none"
3147
3363
  >
3148
3364
  <DialogHeader>
3149
- <DialogTitle>Title 图标预览</DialogTitle>
3150
- <DialogDescription>用于预览页标签页 icon 的当前图片</DialogDescription>
3365
+ <DialogTitle>{t("panel.menubar.titleIconPreviewTitle")}</DialogTitle>
3366
+ <DialogDescription>{t("panel.menubar.titleIconPreviewDesc")}</DialogDescription>
3151
3367
  </DialogHeader>
3152
3368
  <div className="flex items-center justify-end gap-2">
3153
3369
  <button
@@ -3172,7 +3388,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
3172
3388
  onClick={() => setTitleIconZoom(1.6)}
3173
3389
  className="rounded border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-accent"
3174
3390
  >
3175
- 重置
3391
+ {t("common.reset")}
3176
3392
  </button>
3177
3393
  </div>
3178
3394
  {titleIconDataUrl ? (
@@ -3189,7 +3405,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
3189
3405
  >
3190
3406
  <img
3191
3407
  src={titleIconDataUrl}
3192
- alt="title 图标大图预览"
3408
+ alt={t("panel.menubar.titleIconPreviewAlt")}
3193
3409
  className="mx-auto rounded border border-border/70 object-contain"
3194
3410
  style={{
3195
3411
  width: 200,