@arronqzy/vue-view 0.1.0 → 0.1.2

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 (48) hide show
  1. package/package.json +9 -6
  2. package/src/panel/VueViewOnlinePreview.vue +15 -7
  3. package/src/panel/VueViewPanel.vue +81 -55
  4. package/src/panel/components/ConfigHintIcon.vue +4 -2
  5. package/src/panel/components/ElementsLayer.vue +15 -0
  6. package/src/panel/components/MaterialPreview.vue +19 -1
  7. package/src/panel/components/MaterialSidebar.vue +35 -31
  8. package/src/panel/components/MaterialSidebarTreeNode.vue +27 -26
  9. package/src/panel/components/MoveableLayer.vue +7 -4
  10. package/src/panel/components/PanelConfigSidebar.vue +57 -39
  11. package/src/panel/components/ViewElementScopePanel.vue +6 -4
  12. package/src/panel/components/WorkspaceConfigSidebar.vue +7 -1
  13. package/src/panel/components/WorkspaceProjectNav.vue +23 -16
  14. package/src/panel/components/WorkspaceStageSplit.vue +10 -7
  15. package/src/panel/components/config/ConfigColorField.vue +3 -1
  16. package/src/panel/components/config/PanelConfigAudioSection.vue +44 -43
  17. package/src/panel/components/config/PanelConfigChartSection.vue +79 -76
  18. package/src/panel/components/config/PanelConfigGeometrySection.vue +21 -19
  19. package/src/panel/components/config/PanelConfigGridChildSpan.vue +7 -5
  20. package/src/panel/components/config/PanelConfigGridSection.vue +10 -8
  21. package/src/panel/components/config/PanelConfigImageSection.vue +22 -21
  22. package/src/panel/components/config/PanelConfigMultiSelect.vue +42 -40
  23. package/src/panel/components/config/PanelConfigNodeInfo.vue +19 -16
  24. package/src/panel/components/config/PanelConfigReferenceSection.vue +11 -9
  25. package/src/panel/components/config/PanelConfigStyleSections.vue +27 -26
  26. package/src/panel/components/config/PanelConfigTextSection.vue +20 -17
  27. package/src/panel/components/config/PanelConfigVideoSection.vue +15 -14
  28. package/src/panel/components/config/shared.ts +0 -3
  29. package/src/panel/components/elementsLayerNodes.ts +37 -18
  30. package/src/panel/components/materialSidebarData.ts +73 -54
  31. package/src/panel/components/scope-config/ScopeTemplatePreviewHost.vue +7 -2
  32. package/src/panel/components/scope-config/ScopeTemplatePreviewPanel.vue +6 -3
  33. package/src/panel/components/scope-config/ScopeTemplateUsageHint.vue +7 -16
  34. package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.vue +4 -2
  35. package/src/panel/components/table/ConditionEditor.vue +181 -0
  36. package/src/panel/components/table/JsonCodeEditor.vue +148 -0
  37. package/src/panel/components/table/PanelConfigTableSection.vue +2033 -0
  38. package/src/panel/components/table/TableNodeContent.vue +557 -0
  39. package/src/panel/constants/messages.ts +76 -34
  40. package/src/panel/hooks/usePanelElements.ts +54 -41
  41. package/src/panel/hooks/useWorkspaceProjects.ts +8 -5
  42. package/src/panel/types.ts +4 -0
  43. package/src/panel/utils/panelElementDefaults.ts +73 -39
  44. package/src/panel/utils/scope-field-labels.ts +53 -39
  45. package/src/panel/utils/scope-template-spread.ts +57 -24
  46. package/src/panel/utils/scope-template-warnings.ts +23 -22
  47. package/src/panel/utils/scope-template.ts +80 -0
  48. package/src/tailwind.css +18 -10
@@ -10,10 +10,12 @@ import {
10
10
  type VNode,
11
11
  type Component,
12
12
  } from "vue";
13
+ import { useI18n } from "@arronqzy/i18n/vue";
13
14
  import * as echarts from "echarts";
14
15
  import type { PanelElement } from "../types";
15
16
  import { buildChartOption, CHART_TYPES } from "../utils/chartOptionBuilder";
16
17
  import { PREVIEW_LAYOUT_EVENT } from "../utils/panelStateIO";
18
+ import TableNodeContent from "./table/TableNodeContent.vue";
17
19
 
18
20
  export { CHART_TYPES };
19
21
 
@@ -62,8 +64,11 @@ export const TextNodeContent = defineComponent({
62
64
  },
63
65
  emits: ["change"],
64
66
  setup(p, { emit }) {
67
+ const { t } = useI18n();
65
68
  const nodeRef = ref<HTMLDivElement | null>(null);
66
- const html = computed(() => p.element.textHtml ?? "<p>双击输入文本</p>");
69
+ const html = computed(
70
+ () => p.element.textHtml ?? `<p>${t("panel.defaults.doubleClickTextHtml")}</p>`
71
+ );
67
72
  const textStyle = computed(() => ({
68
73
  fontFamily: p.element.textFontFamily || undefined,
69
74
  fontSize: p.element.textFontSize ? `${p.element.textFontSize}px` : undefined,
@@ -101,6 +106,7 @@ export const GridNodeContent = defineComponent({
101
106
  previewMode: { type: Boolean, default: false },
102
107
  },
103
108
  setup(p) {
109
+ const { t } = useI18n();
104
110
  const rows = computed(() => Math.max(1, Math.floor(p.element.gridRows ?? 2)));
105
111
  const cols = computed(() => Math.max(1, Math.floor(p.element.gridCols ?? 3)));
106
112
  const gap = computed(() => Math.max(0, p.element.gridGap ?? 8));
@@ -167,7 +173,9 @@ export const GridNodeContent = defineComponent({
167
173
  ? "border-primary/70 bg-primary/10"
168
174
  : "border-border/60 bg-muted/20",
169
175
  ].join(" "),
170
- title: `槽位 ${idx + 1}${occupied.value.has(idx) ? "(已占用)" : "(空)"}`,
176
+ title: occupied.value.has(idx)
177
+ ? t("panel.config.slotOccupied", { n: idx + 1 })
178
+ : t("panel.config.slotEmpty", { n: idx + 1 }),
171
179
  })
172
180
  )
173
181
  ),
@@ -266,6 +274,7 @@ export const AudioNodeContent = defineComponent({
266
274
  selected: { type: Boolean, default: false },
267
275
  },
268
276
  setup(p) {
277
+ const { t } = useI18n();
269
278
  const audioRef = ref<HTMLAudioElement | null>(null);
270
279
  const playing = ref(false);
271
280
  const src = computed(() => p.element.audioSrc || p.element.audioRemoteUrl || "");
@@ -369,12 +378,12 @@ export const AudioNodeContent = defineComponent({
369
378
  onMousedown: (e: Event) => e.stopPropagation(),
370
379
  onPointerdown: (e: Event) => e.stopPropagation(),
371
380
  onClick: togglePlay,
372
- title: !src.value ? "请先配置音频源" : playing.value ? "点击暂停" : "点击播放",
381
+ title: !src.value ? t("panel.config.configureAudioFirst") : playing.value ? t("panel.config.clickPause") : t("panel.config.clickPlay"),
373
382
  },
374
383
  [
375
384
  h("img", {
376
385
  src: poster.value,
377
- alt: "音频占位图",
386
+ alt: t("panel.config.audioPoster"),
378
387
  class: "h-full w-full object-cover",
379
388
  draggable: false,
380
389
  onDragstart: (e: Event) => e.preventDefault(),
@@ -389,7 +398,7 @@ export const AudioNodeContent = defineComponent({
389
398
  onMousedown: (e: Event) => e.stopPropagation(),
390
399
  onPointerdown: (e: Event) => e.stopPropagation(),
391
400
  onClick: togglePlay,
392
- title: !src.value ? "请先配置音频源" : playing.value ? "点击暂停" : "点击播放",
401
+ title: !src.value ? t("panel.config.configureAudioFirst") : playing.value ? t("panel.config.clickPause") : t("panel.config.clickPlay"),
393
402
  },
394
403
  [renderIcon()]
395
404
  ),
@@ -416,7 +425,7 @@ export const AudioNodeContent = defineComponent({
416
425
  class:
417
426
  "pointer-events-none absolute right-2 top-2 rounded bg-black/55 px-1.5 py-0.5 text-[10px] text-white",
418
427
  },
419
- !src.value ? "未配置音频" : playing.value ? "暂停" : "播放"
428
+ !src.value ? t("panel.config.audioNotConfigured") : playing.value ? t("panel.config.pause") : t("panel.config.play")
420
429
  ),
421
430
  h("audio", { ref: audioRef, src: src.value, preload: "metadata", class: "hidden" }),
422
431
  ]
@@ -429,11 +438,11 @@ export const AudioNodeContent = defineComponent({
429
438
  class:
430
439
  "flex h-full w-full items-center justify-center rounded border border-dashed border-border/70 bg-muted/15 px-2 text-[11px] text-muted-foreground",
431
440
  },
432
- "音频占位"
441
+ t("panel.config.audioPlaceholder")
433
442
  );
434
443
  }
435
444
  return h("div", { class: "relative flex h-full w-full items-center justify-center rounded border border-border/60 bg-muted/10 px-2" }, [
436
- h("div", { class: "pointer-events-none text-[11px] text-muted-foreground" }, playing.value ? "音频播放中" : "音频已就绪"),
445
+ h("div", { class: "pointer-events-none text-[11px] text-muted-foreground" }, playing.value ? t("panel.config.audioPlaying") : t("panel.config.audioReady")),
437
446
  h(
438
447
  "button",
439
448
  {
@@ -442,9 +451,9 @@ export const AudioNodeContent = defineComponent({
442
451
  onMousedown: (e: Event) => e.stopPropagation(),
443
452
  onPointerdown: (e: Event) => e.stopPropagation(),
444
453
  onClick: togglePlay,
445
- title: playing.value ? "点击暂停" : "点击播放",
454
+ title: playing.value ? t("panel.config.clickPause") : t("panel.config.clickPlay"),
446
455
  },
447
- playing.value ? "暂停" : "播放"
456
+ playing.value ? t("panel.config.pause") : t("panel.config.play")
448
457
  ),
449
458
  h("audio", { ref: audioRef, src: src.value, preload: "metadata", class: "hidden" }),
450
459
  ]);
@@ -459,6 +468,7 @@ export const VideoNodeContent = defineComponent({
459
468
  selected: { type: Boolean, default: false },
460
469
  },
461
470
  setup(p) {
471
+ const { t } = useI18n();
462
472
  const videoRef = ref<HTMLVideoElement | null>(null);
463
473
  const playing = ref(false);
464
474
  const src = computed(() => p.element.videoSrc || p.element.videoRemoteUrl || "");
@@ -504,7 +514,7 @@ export const VideoNodeContent = defineComponent({
504
514
  class:
505
515
  "flex h-full w-full items-center justify-center rounded border border-dashed border-border/70 bg-muted/15 px-2 text-[11px] text-muted-foreground",
506
516
  },
507
- "视频占位"
517
+ t("panel.config.videoPlaceholder")
508
518
  );
509
519
  }
510
520
  return h("div", { class: "relative h-full w-full p-1" }, [
@@ -521,9 +531,9 @@ export const VideoNodeContent = defineComponent({
521
531
  onMousedown: (e: Event) => e.stopPropagation(),
522
532
  onPointerdown: (e: Event) => e.stopPropagation(),
523
533
  onClick: togglePlay,
524
- title: playing.value ? "点击暂停" : "点击播放",
534
+ title: playing.value ? t("panel.config.clickPause") : t("panel.config.clickPlay"),
525
535
  },
526
- playing.value ? "暂停" : "播放"
536
+ playing.value ? t("panel.config.pause") : t("panel.config.play")
527
537
  ),
528
538
  ]);
529
539
  };
@@ -681,6 +691,7 @@ export const ReferenceNodeContent = defineComponent({
681
691
  previewMode: { type: Boolean, default: false },
682
692
  },
683
693
  setup(p) {
694
+ const { t } = useI18n();
684
695
  const sourceNodes = computed(() => {
685
696
  const fromDeep =
686
697
  p.element.refCopyMode === "deep"
@@ -715,7 +726,7 @@ export const ReferenceNodeContent = defineComponent({
715
726
 
716
727
  return () => {
717
728
  if (!layout.value || sourceNodes.value.length === 0) {
718
- const hintText = p.element.refLayerId ? "引用图层暂无节点" : "请在右侧选择引用图层";
729
+ const hintText = p.element.refLayerId ? t("panel.config.refLayerEmpty") : t("panel.config.selectRefLayer");
719
730
  return h("div", { class: "flex h-full w-full items-center justify-center" }, [
720
731
  h(
721
732
  "div",
@@ -777,7 +788,9 @@ export const ReferenceNodeContent = defineComponent({
777
788
  })
778
789
  : node.materialType === "geometry"
779
790
  ? h(GeometryNodeContent, { element: node })
780
- : h("div", { class: "h-full w-full" }),
791
+ : node.materialType === "table"
792
+ ? h(TableNodeContent, { element: node })
793
+ : h("div", { class: "h-full w-full" }),
781
794
  ]
782
795
  ),
783
796
  ]
@@ -794,6 +807,7 @@ export const ImageNodeContent = defineComponent({
794
807
  element: { type: Object as PropType<PanelElement>, required: true },
795
808
  },
796
809
  setup(p) {
810
+ const { t } = useI18n();
797
811
  return () => {
798
812
  if (hasBackgroundImage(p.element)) return null;
799
813
  return h(
@@ -802,7 +816,7 @@ export const ImageNodeContent = defineComponent({
802
816
  class:
803
817
  "flex h-full w-full items-center justify-center rounded border border-dashed border-border/70 bg-muted/15 px-2 text-[11px] text-muted-foreground",
804
818
  },
805
- "图片占位"
819
+ t("panel.config.imagePlaceholder")
806
820
  );
807
821
  };
808
822
  },
@@ -814,9 +828,14 @@ export const EmptyNodePlaceholder = defineComponent({
814
828
  element: { type: Object as PropType<PanelElement>, required: true },
815
829
  },
816
830
  setup(p) {
831
+ const { t } = useI18n();
817
832
  return () => {
818
- const labelMap: Record<string, string> = { video: "视频占位", audio: "音频占位" };
819
- const label = labelMap[p.element.materialType ?? ""] ?? `${p.element.materialType ?? "节点"} 占位`;
833
+ const labelMap: Record<string, string> = { video: t("panel.config.videoPlaceholder"), audio: t("panel.config.audioPlaceholder") };
834
+ const label =
835
+ labelMap[p.element.materialType ?? ""] ??
836
+ t("panel.config.materialPlaceholder", {
837
+ type: p.element.materialType ?? t("common.node"),
838
+ });
820
839
  return h(
821
840
  "div",
822
841
  {
@@ -1,3 +1,4 @@
1
+ import type { TranslateFn } from "@arronqzy/i18n";
1
2
  import type { PanelElement } from "../types";
2
3
 
3
4
  export type MaterialCategoryId = "charts" | "basic" | "media";
@@ -13,60 +14,72 @@ export type MaterialCategory = {
13
14
  items: MaterialItem[];
14
15
  };
15
16
 
16
- export const MATERIAL_LABEL_MAP: Record<string, string> = {
17
- bar: "柱状图",
18
- line: "折线图",
19
- pie: "饼图",
20
- area: "面积图",
21
- scatter: "散点图",
22
- radar: "雷达图",
23
- gauge: "仪表盘",
24
- funnel: "漏斗图",
25
- text: "文本",
26
- rect: "矩形",
27
- grid: "网格布局",
28
- image: "图片",
29
- video: "视频",
30
- audio: "音频",
31
- reference: "引用组件",
32
- geometry: "几何",
17
+ const MATERIAL_LABEL_KEYS: Record<string, string> = {
18
+ bar: "panel.material.bar",
19
+ line: "panel.material.line",
20
+ pie: "panel.material.pie",
21
+ area: "panel.material.area",
22
+ scatter: "panel.material.scatter",
23
+ radar: "panel.material.radar",
24
+ gauge: "panel.material.gauge",
25
+ funnel: "panel.material.funnel",
26
+ text: "panel.material.text",
27
+ rect: "panel.material.rect",
28
+ grid: "panel.material.grid",
29
+ image: "panel.material.image",
30
+ video: "panel.material.video",
31
+ audio: "panel.material.audio",
32
+ reference: "panel.material.reference",
33
+ geometry: "panel.material.geometry",
34
+ table: "panel.material.table",
33
35
  };
34
36
 
35
- export const defaultCategories: MaterialCategory[] = [
36
- {
37
- id: "charts",
38
- title: "图表",
39
- items: [
40
- { id: "bar", title: "柱状图" },
41
- { id: "line", title: "折线图" },
42
- { id: "pie", title: "饼图" },
43
- { id: "area", title: "面积图" },
44
- { id: "scatter", title: "散点图" },
45
- { id: "radar", title: "雷达图" },
46
- { id: "gauge", title: "仪表盘" },
47
- { id: "funnel", title: "漏斗图" },
48
- ],
49
- },
50
- {
51
- id: "basic",
52
- title: "基础",
53
- items: [
54
- { id: "text", title: "文本" },
55
- { id: "geometry", title: "几何" },
56
- { id: "grid", title: "网格布局" },
57
- { id: "image", title: "图片" },
58
- { id: "reference", title: "引用组件" },
59
- ],
60
- },
61
- {
62
- id: "media",
63
- title: "媒体",
64
- items: [
65
- { id: "video", title: "视频" },
66
- { id: "audio", title: "音频" },
67
- ],
68
- },
69
- ];
37
+ export function getMaterialLabelMap(t: TranslateFn): Record<string, string> {
38
+ const map: Record<string, string> = {};
39
+ for (const [id, key] of Object.entries(MATERIAL_LABEL_KEYS)) {
40
+ map[id] = t(key);
41
+ }
42
+ return map;
43
+ }
44
+
45
+ export function getDefaultCategories(t: TranslateFn): MaterialCategory[] {
46
+ return [
47
+ {
48
+ id: "charts",
49
+ title: t("panel.material.charts"),
50
+ items: [
51
+ { id: "bar", title: t("panel.material.bar") },
52
+ { id: "line", title: t("panel.material.line") },
53
+ { id: "pie", title: t("panel.material.pie") },
54
+ { id: "area", title: t("panel.material.area") },
55
+ { id: "scatter", title: t("panel.material.scatter") },
56
+ { id: "radar", title: t("panel.material.radar") },
57
+ { id: "gauge", title: t("panel.material.gauge") },
58
+ { id: "funnel", title: t("panel.material.funnel") },
59
+ ],
60
+ },
61
+ {
62
+ id: "basic",
63
+ title: t("panel.material.basic"),
64
+ items: [
65
+ { id: "text", title: t("panel.material.text") },
66
+ { id: "table", title: t("panel.material.table") },
67
+ { id: "geometry", title: t("panel.material.geometry") },
68
+ { id: "grid", title: t("panel.material.grid") },
69
+ { id: "image", title: t("panel.material.image") },
70
+ { id: "reference", title: t("panel.material.reference") },
71
+ ],
72
+ },
73
+ {
74
+ id: "media",
75
+ title: t("panel.material.media"),
76
+ items: [
77
+ { id: "video", title: t("panel.material.video") },
78
+ { id: "audio", title: t("panel.material.audio") },
79
+ ],
80
+ },
81
+ ];
82
+ }
70
83
 
71
84
  export const themedScrollbarClass =
72
85
  "scrollbar-thin [&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-muted/40 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/80 [&::-webkit-scrollbar-thumb]:hover:bg-border";
@@ -79,7 +92,13 @@ export function compareGridTreeChildOrder(a: PanelElement, b: PanelElement): num
79
92
  return a.id.localeCompare(b.id);
80
93
  }
81
94
 
82
- export function getNodeDisplayName(node: PanelElement) {
95
+ export function getNodeDisplayName(node: PanelElement, t?: TranslateFn) {
83
96
  const customName = node.name?.trim();
84
- return customName || node.chart?.title || MATERIAL_LABEL_MAP[node.materialType ?? ""] || node.id;
97
+ if (customName) return customName;
98
+ if (node.chart?.title) return node.chart.title;
99
+ if (t) {
100
+ const key = MATERIAL_LABEL_KEYS[node.materialType ?? ""];
101
+ if (key) return t(key);
102
+ }
103
+ return node.materialType || node.id;
85
104
  }
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { useI18n } from "@arronqzy/i18n/vue";
2
3
  import { computed, onWatcherCleanup, shallowRef, toValue, watch, type MaybeRef } from "vue";
3
4
  import type { PanelElement } from "../../types";
4
5
  import { hasScopeTemplate } from "../../utils/scope-template";
@@ -8,6 +9,8 @@ import {
8
9
  } from "../../utils/scope-template-preview";
9
10
  import ScopeTemplatePreviewPanel from "./ScopeTemplatePreviewPanel.vue";
10
11
 
12
+ const { t, locale } = useI18n();
13
+
11
14
  const PREVIEW_SLOT_ATTR = "data-scope-preview-slot";
12
15
  const PREVIEW_KEY_ATTR = "data-scope-preview-key";
13
16
 
@@ -95,8 +98,10 @@ function inferFieldIdFromDom(
95
98
  ): string | undefined {
96
99
  const label = input.closest("label");
97
100
  const labelText = label?.textContent ?? "";
98
- if (labelText.includes("类目")) return "chart.labelsText";
99
- if (labelText.includes("数值")) return "chart.valuesText";
101
+ if (labelText.includes(t("panel.scope.fieldChartLabelsText")) || labelText.includes("类目") || /labels?/i.test(labelText))
102
+ return "chart.labelsText";
103
+ if (labelText.includes(t("panel.scope.fieldChartValuesText")) || labelText.includes("数值") || /values?/i.test(labelText))
104
+ return "chart.valuesText";
100
105
  return undefined;
101
106
  }
102
107
 
@@ -1,10 +1,13 @@
1
1
  <script setup lang="ts">
2
+ import { useI18n } from "@arronqzy/i18n/vue";
2
3
  import { ref } from "vue";
3
4
  import {
4
5
  formatScopeTemplatePreview,
5
6
  resolveScopeTemplatePreview,
6
7
  } from "../../utils/scope-template-preview";
7
8
 
9
+ const { t, locale } = useI18n();
10
+
8
11
  const props = defineProps<{
9
12
  template: string;
10
13
  scope: unknown;
@@ -18,7 +21,7 @@ const previewValue = () =>
18
21
  const previewText = () => formatScopeTemplatePreview(previewValue());
19
22
  const previewType = () => {
20
23
  const value = previewValue();
21
- return Array.isArray(value) ? `数组 · ${value.length} 项` : typeof value;
24
+ return Array.isArray(value) ? t("panel.scope.previewArray", { count: value.length }) : typeof value;
22
25
  };
23
26
  </script>
24
27
 
@@ -29,9 +32,9 @@ const previewType = () => {
29
32
  class="flex w-full items-center justify-between gap-2 px-2 py-1 text-left text-[10px] text-muted-foreground hover:bg-accent/40"
30
33
  @click="isExpanded = !isExpanded"
31
34
  >
32
- <span class="font-medium text-foreground/80">解析预览</span>
35
+ <span class="font-medium text-foreground/80">{{ t("panel.scope.previewTitle") }}</span>
33
36
  <span class="shrink-0 text-[10px]">
34
- {{ previewType() }} · {{ isExpanded ? "收起" : "展开" }}
37
+ {{ previewType() }} · {{ isExpanded ? t("panel.scope.collapsePreview") : t("panel.scope.expandPreview") }}
35
38
  </span>
36
39
  </button>
37
40
  <pre
@@ -1,20 +1,11 @@
1
+ <script setup lang="ts">
2
+ import { useI18n } from "@arronqzy/i18n/vue";
3
+ const { t } = useI18n();
4
+ </script>
5
+
1
6
  <template>
2
7
  <div class="space-y-2">
3
- <p>
4
- 表单中可使用模版字符串引用 scope,例如
5
- <code class="rounded bg-muted px-1">{`{scope?.a||0}`}</code>
6
- 、
7
- <code class="rounded bg-muted px-1">{`{scope?.name||''}`}</code>
8
- 。支持 <code class="rounded bg-muted px-1">?.</code> 与
9
- <code class="rounded bg-muted px-1">||</code> 占位。
10
- </p>
11
- <p>
12
- 数组展开模版
13
- <code class="rounded bg-muted px-1">{`[...{scope?.a?.b}]`}</code>
14
- 、
15
- <code class="rounded bg-muted px-1">{`[...{scope?.data?.data?.statusName}]`}</code>
16
- :在文本字段输出逗号分隔字符串;在图表「类目」等数组字段会展开为 n
17
- 个独立项(如 3 条数据对应 3 个类目)。
18
- </p>
8
+ <p>{{ t("panel.scope.usageHint1") }}</p>
9
+ <p>{{ t("panel.scope.usageHint2") }}</p>
19
10
  </div>
20
11
  </template>
@@ -1,7 +1,9 @@
1
1
  <script setup lang="ts">
2
+ import { useI18n } from "@arronqzy/i18n/vue";
2
3
  import { onMounted, ref, watch } from "vue";
3
4
  import { useScopeConfig } from "./useScopeConfig";
4
5
 
6
+ const { t, locale } = useI18n();
5
7
  const WARNINGS_COLLAPSE_STORAGE_KEY = "panel:config-scope-warnings-collapsed";
6
8
 
7
9
  const ctx = useScopeConfig();
@@ -27,7 +29,7 @@ watch(isCollapsed, (next) => {
27
29
  >
28
30
  <div class="flex items-center justify-between gap-2">
29
31
  <div class="text-[11px] font-medium text-amber-700 dark:text-amber-300">
30
- 异常调用警告
32
+ {{ t("panel.scope.warningsTitle") }}
31
33
  <span
32
34
  class="ml-1.5 rounded bg-amber-500/15 px-1.5 py-0.5 text-[10px] font-semibold"
33
35
  >
@@ -39,7 +41,7 @@ watch(isCollapsed, (next) => {
39
41
  class="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent"
40
42
  @click="isCollapsed = !isCollapsed"
41
43
  >
42
- {{ isCollapsed ? "展开警告" : "收起警告" }}
44
+ {{ isCollapsed ? t("panel.scope.expandWarnings") : t("panel.scope.collapseWarnings") }}
43
45
  </button>
44
46
  </div>
45
47
  <ul
@@ -0,0 +1,181 @@
1
+ <script setup lang="ts">
2
+ import type { Condition, ConditionLeaf, ConditionOp } from "@arronqzy/view-table";
3
+ import {
4
+ addConditionItem,
5
+ getConditionLogic,
6
+ isConditionLeaf,
7
+ listConditionItems,
8
+ removeConditionItem,
9
+ setConditionLogic,
10
+ updateConditionLeaf,
11
+ type ConditionLogic,
12
+ } from "@arronqzy/view-table";
13
+ import { useI18n } from "@arronqzy/i18n/vue";
14
+ import { Button, Input, Select } from "ant-design-vue";
15
+ import { computed } from "vue";
16
+
17
+ const props = withDefaults(
18
+ defineProps<{
19
+ modelValue?: Condition;
20
+ showField?: boolean;
21
+ disabled?: boolean;
22
+ }>(),
23
+ { showField: false, disabled: false }
24
+ );
25
+
26
+ const emit = defineEmits<{
27
+ "update:modelValue": [value: Condition];
28
+ }>();
29
+
30
+ const { t } = useI18n();
31
+
32
+ const CONDITION_OPS: ConditionOp[] = [
33
+ "eq",
34
+ "neq",
35
+ "gt",
36
+ "gte",
37
+ "lt",
38
+ "lte",
39
+ "contains",
40
+ "startsWith",
41
+ "endsWith",
42
+ "in",
43
+ "empty",
44
+ "notEmpty",
45
+ "regex",
46
+ "truthy",
47
+ "falsy",
48
+ ];
49
+
50
+ const logic = computed(() => getConditionLogic(props.modelValue));
51
+ const items = computed(() => listConditionItems(props.modelValue));
52
+
53
+ function opLabel(op: ConditionOp) {
54
+ return t(`panel.config.tableOp.${op}`);
55
+ }
56
+
57
+ function leafOf(item: Condition): ConditionLeaf {
58
+ return isConditionLeaf(item) ? item : { op: "eq", value: "" };
59
+ }
60
+
61
+ function leafValueText(value: unknown): string {
62
+ if (value == null) return "";
63
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
64
+ return String(value);
65
+ }
66
+ try {
67
+ return JSON.stringify(value);
68
+ } catch {
69
+ return "";
70
+ }
71
+ }
72
+
73
+ function parseLeafValue(text: string): unknown {
74
+ const trimmed = text.trim();
75
+ if (trimmed === "") return "";
76
+ if (trimmed === "true") return true;
77
+ if (trimmed === "false") return false;
78
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed);
79
+ return text;
80
+ }
81
+
82
+ function setLogic(v: string) {
83
+ emit("update:modelValue", setConditionLogic(props.modelValue, v as ConditionLogic));
84
+ }
85
+
86
+ function patchLeaf(index: number, patch: Partial<ConditionLeaf>) {
87
+ emit("update:modelValue", updateConditionLeaf(props.modelValue, index, patch));
88
+ }
89
+ </script>
90
+
91
+ <template>
92
+ <div class="space-y-2">
93
+ <div class="flex items-center justify-between gap-2">
94
+ <label class="min-w-0 flex-1 space-y-1">
95
+ <div class="text-[11px] text-gray-500">{{ t("panel.config.tableConditionLogic") }}</div>
96
+ <Select
97
+ size="small"
98
+ class="w-full"
99
+ :value="logic"
100
+ :disabled="disabled"
101
+ @update:value="(v) => setLogic(String(v))"
102
+ >
103
+ <Select.Option value="and">{{ t("panel.config.tableConditionLogicAnd") }}</Select.Option>
104
+ <Select.Option value="or">{{ t("panel.config.tableConditionLogicOr") }}</Select.Option>
105
+ <Select.Option value="not">{{ t("panel.config.tableConditionLogicNot") }}</Select.Option>
106
+ </Select>
107
+ </label>
108
+ <Button
109
+ size="small"
110
+ class="mt-4"
111
+ :disabled="disabled"
112
+ @click="emit('update:modelValue', addConditionItem(modelValue))"
113
+ >
114
+ {{ t("panel.config.tableConditionAddItem") }}
115
+ </Button>
116
+ </div>
117
+ <div class="text-[10px] text-gray-500">{{ t("panel.config.tableConditionLogicHint") }}</div>
118
+ <div
119
+ v-for="(item, index) in items"
120
+ :key="index"
121
+ class="space-y-1.5 rounded-md border border-gray-200 bg-white/70 p-2"
122
+ >
123
+ <div class="flex items-center justify-between gap-2">
124
+ <div class="text-[10px] font-medium text-gray-500">
125
+ {{ t("panel.config.tableConditionItem", { n: index + 1 }) }}
126
+ </div>
127
+ <Button
128
+ size="small"
129
+ danger
130
+ type="text"
131
+ :disabled="disabled || items.length <= 1"
132
+ @click="emit('update:modelValue', removeConditionItem(modelValue, index))"
133
+ >
134
+ ×
135
+ </Button>
136
+ </div>
137
+ <div
138
+ class="grid grid-cols-1 gap-1.5"
139
+ :class="showField ? 'sm:grid-cols-3' : 'sm:grid-cols-2'"
140
+ >
141
+ <label class="block space-y-1">
142
+ <div class="text-[11px] text-gray-500">{{ t("panel.config.tableConditionOp") }}</div>
143
+ <Select
144
+ size="small"
145
+ class="w-full"
146
+ :value="leafOf(item).op"
147
+ :disabled="disabled"
148
+ @update:value="(v) => patchLeaf(index, { op: String(v) as ConditionOp })"
149
+ >
150
+ <Select.Option v-for="op in CONDITION_OPS" :key="op" :value="op">
151
+ {{ opLabel(op) }}
152
+ </Select.Option>
153
+ </Select>
154
+ </label>
155
+ <label v-if="showField" class="block space-y-1">
156
+ <div class="text-[11px] text-gray-500">{{ t("panel.config.tableConditionField") }}</div>
157
+ <Input
158
+ size="small"
159
+ :value="leafOf(item).field ?? ''"
160
+ :disabled="disabled"
161
+ :placeholder="t('panel.config.tableConditionFieldPlaceholder')"
162
+ @update:value="(v: string) => patchLeaf(index, { field: v || undefined })"
163
+ />
164
+ </label>
165
+ <label
166
+ v-if="!['empty', 'notEmpty', 'truthy', 'falsy'].includes(leafOf(item).op)"
167
+ class="block space-y-1"
168
+ >
169
+ <div class="text-[11px] text-gray-500">{{ t("panel.config.tableConditionValue") }}</div>
170
+ <Input
171
+ size="small"
172
+ :value="leafValueText(leafOf(item).value)"
173
+ :disabled="disabled"
174
+ :placeholder="t('panel.config.tableConditionValuePlaceholder')"
175
+ @update:value="(v: string) => patchLeaf(index, { value: parseLeafValue(v) })"
176
+ />
177
+ </label>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </template>