@arronqzy/vue-view 0.1.21 → 0.1.22

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/vue-view",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Vue 3 visual view editor for Abuilder (Ant Design Vue)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,13 +23,13 @@
23
23
  "selecto": "^1.26.3",
24
24
  "uuid": "^13.0.0",
25
25
  "vue": "^3.5.13",
26
- "@arronqzy/blueprint-dsl": "1.0.16",
27
- "@arronqzy/view-scene3d": "0.1.5",
26
+ "@arronqzy/blueprint-dsl": "1.0.17",
27
+ "@arronqzy/i18n": "0.1.17",
28
28
  "@arronqzy/view-table": "0.1.8",
29
- "@arronqzy/i18n": "0.1.16",
29
+ "@arronqzy/rx-store": "1.0.8",
30
+ "@arronqzy/vue-blueprint": "0.1.17",
30
31
  "@arronqzy/vue-rx-store": "0.1.6",
31
- "@arronqzy/vue-blueprint": "0.1.16",
32
- "@arronqzy/rx-store": "1.0.8"
32
+ "@arronqzy/view-scene3d": "0.1.5"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "vue": "^3.4.0"
@@ -39,8 +39,8 @@
39
39
  "eslint": "^8.57.0",
40
40
  "typescript": "^5.5.4",
41
41
  "vue-tsc": "^2.2.0",
42
- "@arronqzy/typescript-config": "1.0.0",
43
- "@arronqzy/eslint-config": "0.0.0"
42
+ "@arronqzy/eslint-config": "0.0.0",
43
+ "@arronqzy/typescript-config": "1.0.0"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"
@@ -2,6 +2,7 @@
2
2
  import { useI18n } from "@arronqzy/i18n/vue";
3
3
  import { computed, onMounted, onUnmounted, ref, watch } from "vue";
4
4
  import {
5
+ useBlueprintDslLocaleSync,
5
6
  BlueprintGraph,
6
7
  documentToRunnableGraph,
7
8
  getBlueprintLibraryRecord,
@@ -22,6 +23,7 @@ import {
22
23
  useViewScopeStoreVersion,
23
24
  } from "./scope/view-scope-store";
24
25
  import { resolvePanelElementScope } from "./utils/scope-template";
26
+ import { localizePanelElement } from "./utils/panelElementDefaults";
25
27
  import {
26
28
  getWorkspaceProject,
27
29
  type WorkspaceProjectRecord,
@@ -40,6 +42,7 @@ import {
40
42
  import { applyPreviewSceneFill, readOutputScale } from "./utils/outputScale";
41
43
 
42
44
  const { t, locale } = useI18n();
45
+ useBlueprintDslLocaleSync();
43
46
 
44
47
  const PREVIEW_BOOT_PHASES: PageLifecyclePhase[] = ["mounted"];
45
48
 
@@ -194,7 +197,9 @@ const scopedLayerElements = computed(() => {
194
197
  const sceneBounds = computed(() => computePanelSceneBounds(scopedLayerElements.value));
195
198
 
196
199
  const displayElements = computed(() =>
197
- scopedLayerElements.value.map((el) => {
200
+ scopedLayerElements.value.map((source) => {
201
+ void locale.value;
202
+ const el = localizePanelElement(source, t);
198
203
  const x = el.x - sceneBounds.value.minX;
199
204
  const y = el.y - sceneBounds.value.minY;
200
205
  if (outputScale.value) return { ...el, x, y };
@@ -2,6 +2,7 @@
2
2
  import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from "vue";
3
3
  import type { State } from "@arronqzy/rx-store";
4
4
  import {
5
+ useBlueprintDslLocaleSync,
5
6
  ApiCollectionManagerDialog,
6
7
  BlueprintGraph,
7
8
  abortClockNode,
@@ -72,12 +73,14 @@ import {
72
73
  import { resolvePanelElementScope } from "./utils/scope-template";
73
74
  import { useRafThrottledScroll } from "./hooks/useRafThrottledScroll";
74
75
  import { useI18n } from "@arronqzy/i18n/vue";
76
+ import { relocalizeMessage } from "@arronqzy/i18n";
75
77
  import { getPanelMessages } from "./constants/messages";
76
78
  import BusyOverlay from "./components/BusyOverlay.vue";
77
79
  import { assertFileSize, parseJsonText, runBusyTask } from "./utils/async-work";
78
80
  import "../tailwind.css";
79
81
 
80
82
  const { t, locale, setLocale } = useI18n();
83
+ useBlueprintDslLocaleSync();
81
84
  const panelMessages = () => getPanelMessages(t);
82
85
 
83
86
  const props = withDefaults(
@@ -152,6 +155,18 @@ const workspaceBlueprintRef = shallowRef<{
152
155
  } | null>(null);
153
156
  const configFocus = ref<WorkspaceConfigFocus>("view");
154
157
  const productName = ref(t("panel.defaults.unnamedProduct"));
158
+ const displayProductName = computed(() => {
159
+ void locale.value;
160
+ return relocalizeMessage(productName.value, "panel.defaults.unnamedProduct", t);
161
+ });
162
+ const displayBlueprintName = computed(() => {
163
+ void locale.value;
164
+ return relocalizeMessage(
165
+ blueprintMeta.value.name,
166
+ ["panel.defaults.unnamedBlueprint", "blueprint.dialog.unnamedBlueprint"],
167
+ t
168
+ );
169
+ });
155
170
  const titleIconDataUrl = ref("");
156
171
  const editingLayerId = ref<string | null>(null);
157
172
  const editingLayerName = ref("");
@@ -346,7 +361,7 @@ function handleViewScopeUpdate(viewElementIds: string[], scope: unknown) {
346
361
  const blueprintDebugSession = useBlueprintDebugSession({
347
362
  graph: blueprintGraph,
348
363
  blueprintId: activeBlueprintLibraryId,
349
- blueprintName: computed(() => { void locale.value; return blueprintMeta.value.name || t("panel.defaults.unnamedBlueprint"); }),
364
+ blueprintName: computed(() => displayBlueprintName.value || t("panel.defaults.unnamedBlueprint")),
350
365
  resolveLibraryBlueprint,
351
366
  libraryNameById: blueprintLibraryNameById,
352
367
  onExecutionBlocked: handleBlueprintExecutionBlocked,
@@ -892,8 +907,9 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
892
907
  <div class="flex h-full min-h-0 flex-col">
893
908
  <div class="flex flex-wrap items-center gap-2 border-b bg-white px-3 py-2">
894
909
  <Input
895
- v-model:value="productName"
910
+ :value="displayProductName"
896
911
  size="small"
912
+ @update:value="(value: string) => (productName = value)"
897
913
  class="w-[200px]"
898
914
  :placeholder="t('panel.menubar.productName')"
899
915
  />
@@ -919,7 +935,7 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
919
935
  :blueprint-props="blueprintCanvasProps"
920
936
  :blueprint-library-items="blueprintLibraryItems"
921
937
  :active-blueprint-library-id="activeBlueprintLibraryId"
922
- :current-blueprint-label="blueprintMeta.name"
938
+ :current-blueprint-label="displayBlueprintName"
923
939
  :can-sync-blueprint="blueprintLibraryDirty"
924
940
  :blueprint-debug="blueprintDebugToolbar"
925
941
  :on-select-blueprint-library-item="(id) => void handleSelectBlueprintLibraryItem(id)"
@@ -26,11 +26,24 @@ const resolvedLabel = computed(() => props.label || t("common.hint"));
26
26
  type="button"
27
27
  :class="
28
28
  buttonClass ??
29
- 'inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border text-[10px] leading-none text-muted-foreground hover:bg-accent/50'
29
+ 'inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring'
30
30
  "
31
31
  :aria-label="t('common.hintAria', { label: resolvedLabel })"
32
32
  >
33
- ?
33
+ <svg
34
+ viewBox="0 0 24 24"
35
+ fill="none"
36
+ stroke="currentColor"
37
+ stroke-width="2"
38
+ stroke-linecap="round"
39
+ stroke-linejoin="round"
40
+ class="h-3.5 w-3.5"
41
+ aria-hidden="true"
42
+ >
43
+ <circle cx="12" cy="12" r="10" />
44
+ <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
45
+ <path d="M12 17h.01" />
46
+ </svg>
34
47
  </button>
35
48
  </Tooltip>
36
49
  </template>
@@ -141,7 +141,12 @@ function onPosterFileChange(e: Event) {
141
141
  >
142
142
  <ConfigFieldGroup :title="t('panel.config.groupAudioSource')">
143
143
  <label class="block space-y-1" data-config-field="audioRemoteUrl">
144
- <div>{{ t("panel.config.audioUrl") }}</div>
144
+ <div class="flex items-center gap-1">
145
+ {{ t("panel.config.audioUrl") }}
146
+ <ConfigHintIcon :label="t('panel.config.audioUrl')">
147
+ {{ t("panel.config.urlScopeHint") }}
148
+ </ConfigHintIcon>
149
+ </div>
145
150
  <Input
146
151
  size="small"
147
152
  class="font-mono text-[11px]"
@@ -152,9 +157,7 @@ function onPosterFileChange(e: Event) {
152
157
  audioRemoteUrl: v || undefined,
153
158
  audioSrc: v || element.audioSrc,
154
159
  })"
155
- />
156
- <p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
157
- </label>
160
+ /> </label>
158
161
  <div class="flex items-center gap-2">
159
162
  <label
160
163
  class="inline-flex cursor-pointer items-center rounded border border-gray-200 px-2 py-1 text-[11px] hover:bg-gray-50"
@@ -146,27 +146,33 @@ function onOptionJsonChange(v: string) {
146
146
  </ConfigFieldGroup>
147
147
  <ConfigFieldGroup :title="t('panel.config.groupData')">
148
148
  <label class="block space-y-1" data-config-field="chart.labelsText">
149
- <div>{{ t("panel.config.labelsCsv") }}</div>
149
+ <div class="flex items-center gap-1">
150
+ {{ t("panel.config.labelsCsv") }}
151
+ <ConfigHintIcon :label="t('panel.config.labelsCsv')">
152
+ {{ t("panel.config.scopeTemplateHint") }}
153
+ </ConfigHintIcon>
154
+ </div>
150
155
  <Input
151
156
  size="small"
152
157
  :value="getChartLabelsDisplayText(element.chart)"
153
158
  :disabled="!isEditable"
154
159
  :placeholder="t('panel.config.labelsCsvPlaceholder')"
155
160
  @update:value="(v: string) => updateChart({ labelsText: v })"
156
- />
157
- <p class="text-[10px] text-gray-500">{{ t("panel.config.scopeTemplateHint") }}</p>
158
- </label>
161
+ /> </label>
159
162
  <label class="block space-y-1" data-config-field="chart.valuesText">
160
- <div>{{ t("panel.config.valuesCsv") }}</div>
163
+ <div class="flex items-center gap-1">
164
+ {{ t("panel.config.valuesCsv") }}
165
+ <ConfigHintIcon :label="t('panel.config.valuesCsv')">
166
+ {{ t("panel.config.scopeTemplateHint") }}
167
+ </ConfigHintIcon>
168
+ </div>
161
169
  <Input
162
170
  size="small"
163
171
  :value="getChartValuesDisplayText(element.chart)"
164
172
  :disabled="!isEditable"
165
173
  :placeholder="t('panel.config.valuesCsvPlaceholder')"
166
174
  @update:value="(v: string) => updateChart({ valuesText: v })"
167
- />
168
- <p class="text-[10px] text-gray-500">{{ t("panel.config.scopeTemplateHint") }}</p>
169
- </label>
175
+ /> </label>
170
176
  </ConfigFieldGroup>
171
177
 
172
178
  <ConfigFieldGroup
@@ -74,7 +74,8 @@ function onFileChange(e: Event) {
74
74
  <div class="flex items-center gap-1">
75
75
  <span>{{ t("panel.config.imageUrlCss") }}</span>
76
76
  <ConfigHintIcon :label="t('panel.config.imageUrlHintLabel')">
77
- {{ t("panel.config.imageUrlHint") }}
77
+ <p>{{ t("panel.config.imageUrlHint") }}</p>
78
+ <p class="mt-1">{{ t("panel.config.urlScopeHint") }}</p>
78
79
  </ConfigHintIcon>
79
80
  </div>
80
81
  <Input
@@ -85,7 +86,6 @@ function onFileChange(e: Event) {
85
86
  :placeholder="t('panel.config.urlScopePlaceholder')"
86
87
  @update:value="(v: string) => patchStyle(patchBackgroundImageFromInput(String(v ?? '')))"
87
88
  />
88
- <p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
89
89
  </label>
90
90
  <div class="flex items-center gap-2">
91
91
  <label
@@ -10,6 +10,7 @@ import {
10
10
  type ViewportOverflowMode,
11
11
  } from "../../utils/viewportPlacement";
12
12
  import { patchBackgroundImageFromInput } from "../../utils/background-image-style";
13
+ import ConfigHintIcon from "../ConfigHintIcon.vue";
13
14
 
14
15
  const { t, locale } = useI18n();
15
16
  const props = defineProps<{
@@ -434,7 +435,12 @@ function setNodeCardExpanded(id: string, open: boolean) {
434
435
 
435
436
  <template v-if="el.materialType === 'image'">
436
437
  <label class="block space-y-1" data-config-field="style.backgroundImageRemoteUrl">
437
- <div>{{ t("panel.config.backgroundImageUrl") }}</div>
438
+ <div class="flex items-center gap-1">
439
+ {{ t("panel.config.backgroundImageUrl") }}
440
+ <ConfigHintIcon :label="t('panel.config.backgroundImageUrl')">
441
+ {{ t("panel.config.urlScopeHint") }}
442
+ </ConfigHintIcon>
443
+ </div>
438
444
  <Input
439
445
  size="small"
440
446
  class="font-mono text-[11px]"
@@ -447,7 +453,6 @@ function setNodeCardExpanded(id: string, open: boolean) {
447
453
  },
448
454
  })"
449
455
  />
450
- <p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
451
456
  </label>
452
457
  <label class="block space-y-1">
453
458
  <div>{{ t("panel.config.objectFit") }}</div>
@@ -344,6 +344,7 @@ function onTagHtmlInput(modelId: string, objectName: string, event: Event) {
344
344
  @update:open="emit('update:open', $event)"
345
345
  >
346
346
  <ConfigFieldGroup :title="t('panel.config.groupScene3dPurpose')">
347
+ <template #hint>{{ t("panel.config.scene3dPurposeHint") }}</template>
347
348
  <Select
348
349
  size="small"
349
350
  class="w-full"
@@ -356,7 +357,6 @@ function onTagHtmlInput(modelId: string, objectName: string, event: Event) {
356
357
  ]"
357
358
  @update:value="(v) => patch(purposePresetPatch(String(v) as Scene3dPurpose))"
358
359
  />
359
- <div class="text-[10px] text-gray-500">{{ t("panel.config.scene3dPurposeHint") }}</div>
360
360
  </ConfigFieldGroup>
361
361
 
362
362
  <ConfigFieldGroup :title="t('panel.config.groupScene3dModels')">
@@ -374,7 +374,12 @@ function onTagHtmlInput(modelId: string, objectName: string, event: Event) {
374
374
  />
375
375
  </div>
376
376
  <label class="block space-y-1" data-config-field="scene3d.modelUrl">
377
- <div class="text-[10px] text-gray-500">{{ t("panel.config.scene3dModelUrl") }}</div>
377
+ <div class="flex items-center gap-1 text-[10px] text-gray-500">
378
+ {{ t("panel.config.scene3dModelUrl") }}
379
+ <ConfigHintIcon :label="t('panel.config.scene3dModelUrl')">
380
+ {{ t("panel.config.urlScopeHint") }}
381
+ </ConfigHintIcon>
382
+ </div>
378
383
  <div class="flex gap-1.5">
379
384
  <Input
380
385
  size="small"
@@ -392,7 +397,6 @@ function onTagHtmlInput(modelId: string, objectName: string, event: Event) {
392
397
  {{ t("common.add") }}
393
398
  </Button>
394
399
  </div>
395
- <p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
396
400
  </label>
397
401
  <div v-if="config.models.length === 0" class="text-[11px] text-gray-500">
398
402
  {{ t("panel.config.scene3dNoModels") }}
@@ -477,7 +481,7 @@ function onTagHtmlInput(modelId: string, objectName: string, event: Event) {
477
481
  </ConfigFieldGroup>
478
482
 
479
483
  <ConfigFieldGroup :title="t('panel.config.groupScene3dCamera')">
480
- <div class="text-[10px] text-gray-500">{{ t("panel.config.scene3dCameraEditDesc") }}</div>
484
+ <template #hint>{{ t("panel.config.scene3dCameraEditDesc") }}</template>
481
485
  <div class="grid grid-cols-3 gap-1">
482
486
  <Input
483
487
  v-for="(axis, index) in ['x', 'y', 'z']"
@@ -9,6 +9,7 @@ import { getPanelMessages } from "../../constants/messages";
9
9
  import ConfigColorField from "./ConfigColorField.vue";
10
10
  import ConfigFieldGroup from "./ConfigFieldGroup.vue";
11
11
  import ConfigSection from "./ConfigSection.vue";
12
+ import ConfigHintIcon from "../ConfigHintIcon.vue";
12
13
 
13
14
  const { t, locale } = useI18n();
14
15
  const msgs = () => getPanelMessages(t);
@@ -89,7 +90,12 @@ function onFileChange(e: Event) {
89
90
  :default-open="false"
90
91
  >
91
92
  <label class="block space-y-1" data-config-field="style.backgroundImage">
92
- <div>{{ t("panel.config.backgroundImage") }}</div>
93
+ <div class="flex items-center gap-1">
94
+ {{ t("panel.config.backgroundImage") }}
95
+ <ConfigHintIcon :label="t('panel.config.backgroundImage')">
96
+ {{ t("panel.config.urlScopeHint") }}
97
+ </ConfigHintIcon>
98
+ </div>
93
99
  <Input
94
100
  size="small"
95
101
  class="font-mono text-[11px]"
@@ -97,9 +103,7 @@ function onFileChange(e: Event) {
97
103
  :disabled="!isEditable"
98
104
  :placeholder="t('panel.config.urlScopePlaceholder')"
99
105
  @update:value="(v: string) => patchStyle(patchBackgroundImageFromInput(String(v ?? '')))"
100
- />
101
- <p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
102
- </label>
106
+ /> </label>
103
107
  <div class="flex items-center gap-2">
104
108
  <label
105
109
  class="inline-flex cursor-pointer items-center rounded border border-gray-200 px-2 py-1 text-[11px] hover:bg-gray-50"
@@ -7,6 +7,7 @@ import { cssTextLineHeight, cssTextAlignStyle } from "../../utils/panelElementDe
7
7
  import ConfigColorField from "./ConfigColorField.vue";
8
8
  import ConfigFieldGroup from "./ConfigFieldGroup.vue";
9
9
  import ConfigSection from "./ConfigSection.vue";
10
+ import ConfigHintIcon from "../ConfigHintIcon.vue";
10
11
 
11
12
  const { t, locale } = useI18n();
12
13
  const props = defineProps<{
@@ -106,7 +107,12 @@ watch(() => [props.element.id, props.element.textHtml], syncEditorHtml);
106
107
  }"
107
108
  />
108
109
  <label class="block space-y-1">
109
- <div>{{ t("panel.config.textHtmlSource") }}</div>
110
+ <div class="flex items-center gap-1">
111
+ {{ t("panel.config.textHtmlSource") }}
112
+ <ConfigHintIcon :label="t('panel.config.textHtmlSource')">
113
+ {{ t("panel.config.scopeTemplateHint") }}
114
+ </ConfigHintIcon>
115
+ </div>
110
116
  <textarea
111
117
  data-config-field="textHtml"
112
118
  class="w-full rounded border border-gray-200 bg-white px-2 py-1.5 font-mono text-[11px] leading-relaxed outline-none"
@@ -116,9 +122,7 @@ watch(() => [props.element.id, props.element.textHtml], syncEditorHtml);
116
122
  :disabled="!isEditable"
117
123
  :placeholder="t('panel.config.textHtmlSourcePlaceholder')"
118
124
  @input="(e) => patch({ textHtml: (e.target as HTMLTextAreaElement).value || '<p><br/></p>' })"
119
- />
120
- <p class="text-[10px] text-gray-500">{{ t("panel.config.scopeTemplateHint") }}</p>
121
- </label>
125
+ /> </label>
122
126
  </ConfigFieldGroup>
123
127
  <ConfigFieldGroup :title="t('panel.config.groupTextStyle')">
124
128
  <label class="block space-y-1">
@@ -7,6 +7,7 @@ import { readFileAsDataUrl, uploadFileToRemote } from "./shared";
7
7
  import { getPanelMessages } from "../../constants/messages";
8
8
  import ConfigFieldGroup from "./ConfigFieldGroup.vue";
9
9
  import ConfigSection from "./ConfigSection.vue";
10
+ import ConfigHintIcon from "../ConfigHintIcon.vue";
10
11
 
11
12
  const { t, locale } = useI18n();
12
13
  const msgs = () => getPanelMessages(t);
@@ -67,7 +68,12 @@ function onVideoFileChange(e: Event) {
67
68
  >
68
69
  <ConfigFieldGroup :title="t('panel.config.groupVideoSource')">
69
70
  <label class="block space-y-1" data-config-field="videoRemoteUrl">
70
- <div>{{ t("panel.config.videoUrl") }}</div>
71
+ <div class="flex items-center gap-1">
72
+ {{ t("panel.config.videoUrl") }}
73
+ <ConfigHintIcon :label="t('panel.config.videoUrl')">
74
+ {{ t("panel.config.urlScopeHint") }}
75
+ </ConfigHintIcon>
76
+ </div>
71
77
  <Input
72
78
  size="small"
73
79
  class="font-mono text-[11px]"
@@ -78,9 +84,7 @@ function onVideoFileChange(e: Event) {
78
84
  videoRemoteUrl: v || undefined,
79
85
  videoSrc: v || element.videoSrc,
80
86
  })"
81
- />
82
- <p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
83
- </label>
87
+ /> </label>
84
88
  <div class="flex items-center gap-2">
85
89
  <label
86
90
  class="inline-flex cursor-pointer items-center rounded border border-gray-200 px-2 py-1 text-[11px] hover:bg-gray-50"
@@ -13,6 +13,7 @@ import {
13
13
  import { useI18n } from "@arronqzy/i18n/vue";
14
14
  import { Button, Input, Select } from "ant-design-vue";
15
15
  import { computed } from "vue";
16
+ import ConfigHintIcon from "../ConfigHintIcon.vue";
16
17
 
17
18
  const props = withDefaults(
18
19
  defineProps<{
@@ -92,7 +93,12 @@ function patchLeaf(index: number, patch: Partial<ConditionLeaf>) {
92
93
  <div class="space-y-2">
93
94
  <div class="flex items-center justify-between gap-2">
94
95
  <label class="min-w-0 flex-1 space-y-1">
95
- <div class="text-[11px] text-gray-500">{{ t("panel.config.tableConditionLogic") }}</div>
96
+ <div class="flex items-center gap-1 text-[11px] text-gray-500">
97
+ {{ t("panel.config.tableConditionLogic") }}
98
+ <ConfigHintIcon :label="t('panel.config.tableConditionLogic')">
99
+ {{ t("panel.config.tableConditionLogicHint") }}
100
+ </ConfigHintIcon>
101
+ </div>
96
102
  <Select
97
103
  size="small"
98
104
  class="w-full"
@@ -114,7 +120,6 @@ function patchLeaf(index: number, patch: Partial<ConditionLeaf>) {
114
120
  {{ t("panel.config.tableConditionAddItem") }}
115
121
  </Button>
116
122
  </div>
117
- <div class="text-[10px] text-gray-500">{{ t("panel.config.tableConditionLogicHint") }}</div>
118
123
  <div
119
124
  v-for="(item, index) in items"
120
125
  :key="index"
@@ -504,10 +504,16 @@ function patchProgressRuleWhen(ruleIndex: number, when: Condition) {
504
504
  </label>
505
505
  <details class="rounded-md border border-gray-200 bg-gray-50/60 px-2 py-1.5">
506
506
  <summary class="cursor-pointer text-[11px] font-medium text-gray-600">
507
- {{ t("panel.config.tableAdvancedData") }}
507
+ <span class="inline-flex items-center gap-1 align-middle">
508
+ {{ t("panel.config.tableAdvancedData") }}
509
+ <span class="inline-flex" @click.prevent>
510
+ <ConfigHintIcon :label="t('panel.config.tableAdvancedData')">
511
+ {{ t("panel.config.tableAdvancedDataHint") }}
512
+ </ConfigHintIcon>
513
+ </span>
514
+ </span>
508
515
  </summary>
509
516
  <div class="mt-2 space-y-2 border-t border-gray-100 pt-2">
510
- <p class="text-[10px] text-gray-500">{{ t("panel.config.tableAdvancedDataHint") }}</p>
511
517
  <div class="grid grid-cols-2 gap-2">
512
518
  <label class="block space-y-1.5">
513
519
  <div class="flex items-center gap-1">
@@ -1338,7 +1344,7 @@ function patchProgressRuleWhen(ruleIndex: number, when: Condition) {
1338
1344
  <Input
1339
1345
  size="small"
1340
1346
  :value="String(selectedColumn.column.widgetProps?.progressStatic ?? '')"
1341
- placeholder="80 或 {scope?.pct}"
1347
+ :placeholder="t('panel.config.tableProgressStaticPlaceholder')"
1342
1348
  :disabled="!isEditable"
1343
1349
  @update:value="
1344
1350
  (v: unknown) =>
@@ -1756,7 +1762,7 @@ function patchProgressRuleWhen(ruleIndex: number, when: Condition) {
1756
1762
  size="small"
1757
1763
  :value="selectedColumn.column.displayTemplate ?? ''"
1758
1764
  :disabled="!isEditable"
1759
- placeholder="{current}分"
1765
+ :placeholder="t('panel.config.tableDisplayTemplatePlaceholder')"
1760
1766
  @update:value="
1761
1767
  (v: unknown) => {
1762
1768
  const s = asSelectString(v).trim();
@@ -22,6 +22,8 @@ import {
22
22
  getDefaultSizeByMaterial,
23
23
  getDefaultTableConfig,
24
24
  getDefaultTextContent,
25
+ localizeLayers,
26
+ localizePanelElement,
25
27
  normalizePrimaryLayer,
26
28
  randomId,
27
29
  } from "../utils/panelElementDefaults";
@@ -122,7 +124,8 @@ export function usePanelElements() {
122
124
  const layers = computed(() => {
123
125
  const rawLayers =
124
126
  (stateRef.value.variables?.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
125
- return normalizePrimaryLayer(rawLayers);
127
+ void locale.value;
128
+ return localizeLayers(normalizePrimaryLayer(rawLayers), t);
126
129
  });
127
130
 
128
131
  const activeLayerId = computed(
@@ -133,13 +136,26 @@ export function usePanelElements() {
133
136
  const allElementsCache = shallowRef<PanelElement[]>([]);
134
137
  const elementsCache = shallowRef<PanelElement[]>([]);
135
138
 
139
+ let localizedCache = { locale: locale.value, map: new WeakMap<PanelElement, PanelElement>() };
140
+
136
141
  const allElements = computed(() => {
137
142
  const next = materializePanelElements(
138
143
  stateRef.value.root.children,
139
144
  allElementsCache.value
140
145
  );
141
146
  allElementsCache.value = next;
142
- return next;
147
+ if (localizedCache.locale !== locale.value) {
148
+ localizedCache = { locale: locale.value, map: new WeakMap() };
149
+ }
150
+ const { map } = localizedCache;
151
+ return next.map((el) => {
152
+ let localized = map.get(el);
153
+ if (!localized) {
154
+ localized = localizePanelElement(el, t);
155
+ map.set(el, localized);
156
+ }
157
+ return localized;
158
+ });
143
159
  });
144
160
 
145
161
  const elements = computed(() => {
@@ -1,5 +1,5 @@
1
1
  import type { TranslateFn } from "@arronqzy/i18n";
2
- import { tForLocale } from "@arronqzy/i18n";
2
+ import { relocalizeMessage, tForLocale } from "@arronqzy/i18n";
3
3
  import { createDefaultTableConfig, type PanelTableConfig } from "@arronqzy/view-table";
4
4
  import { createDefaultScene3dConfig } from "@arronqzy/view-scene3d";
5
5
  import type { PanelChartConfig, PanelLayer, PanelElement } from "../types";
@@ -35,6 +35,52 @@ export function getDefaultNodeName(
35
35
  return key ? t(key) : materialType;
36
36
  }
37
37
 
38
+ const DEFAULT_LAYER_NAME_KEYS = [
39
+ "panel.defaults.layer1",
40
+ "panel.defaults.layerN",
41
+ "panel.defaults.mappingLayerN",
42
+ "panel.defaults.layerRandom",
43
+ ];
44
+
45
+ /** Persisted default layer names ("图层1" / "Layer 2") follow the current locale. */
46
+ export function localizeLayerName(name: string, t: TranslateFn): string {
47
+ return relocalizeMessage(name, DEFAULT_LAYER_NAME_KEYS, t);
48
+ }
49
+
50
+ export function localizeLayers(layers: PanelLayer[], t: TranslateFn): PanelLayer[] {
51
+ return layers.map((layer) => {
52
+ const name = localizeLayerName(layer.name, t);
53
+ return name === layer.name ? layer : { ...layer, name };
54
+ });
55
+ }
56
+
57
+ const DEFAULT_TEXT_HTML_RE = /^<p>(.*)<\/p>$/s;
58
+
59
+ /**
60
+ * Default node name, chart title and text content are stored in the creation locale;
61
+ * re-render them in the current one. Returns the same object when nothing changes.
62
+ */
63
+ export function localizePanelElement(element: PanelElement, t: TranslateFn): PanelElement {
64
+ const key = DEFAULT_NODE_NAME_KEYS[element.materialType ?? ""];
65
+ if (!key) return element;
66
+ const name = element.name ? relocalizeMessage(element.name, key, t) : element.name;
67
+ const title = element.chart?.title ? relocalizeMessage(element.chart.title, key, t) : undefined;
68
+ let textHtml = element.textHtml;
69
+ const textInner = textHtml ? DEFAULT_TEXT_HTML_RE.exec(textHtml)?.[1] : undefined;
70
+ if (textInner) {
71
+ const localized = relocalizeMessage(textInner, "panel.defaults.doubleClickTextHtml", t);
72
+ if (localized !== textInner) textHtml = `<p>${localized}</p>`;
73
+ }
74
+ const titleChanged = title !== undefined && title !== element.chart?.title;
75
+ if (name === element.name && !titleChanged && textHtml === element.textHtml) return element;
76
+ return {
77
+ ...element,
78
+ name,
79
+ textHtml,
80
+ ...(titleChanged && element.chart ? { chart: { ...element.chart, title } } : null),
81
+ };
82
+ }
83
+
38
84
  export function getDefaultLayer(t: TranslateFn = tForLocale("zh-CN")): PanelLayer {
39
85
  return {
40
86
  id: DEFAULT_LAYER_ID,