@arronqzy/react-view 1.0.5 → 1.0.9

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 +15 -7
  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 { useEffect, useRef, useState } from "react";
2
+ import { useI18n } from "@arronqzy/i18n/react";
2
3
 
3
4
  import { formatViewElementScope } from "../utils/scope-template";
4
5
  import { ConfigHintIcon } from "./ConfigHintIcon";
@@ -10,7 +11,10 @@ export type ViewElementScopePanelProps = {
10
11
  scope: unknown;
11
12
  };
12
13
 
13
- export function ViewElementScopePanel({ scope }: ViewElementScopePanelProps) {
14
+ export function ViewElementScopePanel({
15
+ scope,
16
+ }: ViewElementScopePanelProps) {
17
+ const { t } = useI18n();
14
18
  const [isCollapsed, setIsCollapsed] = useState(false);
15
19
  const [scopeUpdated, setScopeUpdated] = useState(false);
16
20
  const prevScopeSerializedRef = useRef<string | null>(null);
@@ -44,13 +48,13 @@ export function ViewElementScopePanel({ scope }: ViewElementScopePanelProps) {
44
48
  <div className="mt-2 border-t border-border/50 pt-2">
45
49
  <div className="flex items-center justify-between gap-2">
46
50
  <div className="flex min-w-0 items-center gap-1.5">
47
- <div className="text-[11px] font-medium text-foreground">Scope 数据</div>
48
- <ConfigHintIcon label="Scope 模版" contentClassName="max-w-[380px]">
51
+ <div className="text-[11px] font-medium text-foreground">{t("panel.scope.dataTitle")}</div>
52
+ <ConfigHintIcon label={t("panel.scope.templateHintLabel")} contentClassName="max-w-[380px]">
49
53
  <ScopeTemplateUsageHint />
50
54
  </ConfigHintIcon>
51
55
  {scopeUpdated ? (
52
56
  <span className="shrink-0 rounded bg-sky-500/15 px-1.5 py-0.5 text-[10px] font-medium text-sky-700 dark:text-sky-300">
53
- 已更新
57
+ {t("panel.scope.updated")}
54
58
  </span>
55
59
  ) : null}
56
60
  </div>
@@ -59,7 +63,7 @@ export function ViewElementScopePanel({ scope }: ViewElementScopePanelProps) {
59
63
  className="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent"
60
64
  onClick={() => setIsCollapsed((prev) => !prev)}
61
65
  >
62
- {isCollapsed ? "展开 Scope" : "收起 Scope"}
66
+ {isCollapsed ? t("panel.scope.expand") : t("panel.scope.collapse")}
63
67
  </button>
64
68
  </div>
65
69
  {!isCollapsed ? (
@@ -1,4 +1,5 @@
1
1
  import { useMemo } from "react";
2
+ import { useI18n } from "@arronqzy/i18n/react";
2
3
 
3
4
  import {
4
5
  BlueprintExecutionLogPanel,
@@ -82,6 +83,8 @@ export function WorkspaceConfigSidebar({
82
83
  allViewElements,
83
84
  ...viewPanelProps
84
85
  }: WorkspaceConfigSidebarProps) {
86
+ const { t } = useI18n();
87
+
85
88
  const viewElementOptions = useMemo(
86
89
  () =>
87
90
  allViewElements.map((el) => ({
@@ -136,9 +139,9 @@ export function WorkspaceConfigSidebar({
136
139
  <path d="M8 7l8 4M8 17l8-4" />
137
140
  </svg>
138
141
  </EmptyIcon>
139
- <EmptyTitle>未选中蓝图节点</EmptyTitle>
142
+ <EmptyTitle>{t("panel.workspace.configEmptyTitle")}</EmptyTitle>
140
143
  <EmptyDescription>
141
- 在蓝图面板中选中一个节点后,这里会显示对应的配置。
144
+ {t("panel.workspace.configEmptyDesc")}
142
145
  </EmptyDescription>
143
146
  </Empty>
144
147
  </div>
@@ -1,4 +1,5 @@
1
1
  import React, { useState } from "react";
2
+ import { useI18n } from "@arronqzy/i18n/react";
2
3
  import {
3
4
  AlertDialog,
4
5
  AlertDialogAction,
@@ -52,6 +53,8 @@ export function WorkspaceProjectNav({
52
53
  onDeleteProject,
53
54
  onPreviewProject,
54
55
  }: WorkspaceProjectNavProps) {
56
+ const { t } = useI18n();
57
+
55
58
  const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
56
59
  const [busy, setBusy] = useState(false);
57
60
 
@@ -81,11 +84,11 @@ export function WorkspaceProjectNav({
81
84
  disabled={busy}
82
85
  onClick={() => void runAction(onCreateProject)}
83
86
  >
84
- 创建工作区
87
+ {t("panel.workspace.create")}
85
88
  </Button>
86
89
  </TooltipTrigger>
87
90
  <TooltipContent className="z-[10100]">
88
- 以当前产物名称新建一条 IndexedDB 工作区记录,不会覆盖已有工作区
91
+ {t("panel.workspace.createHint")}
89
92
  </TooltipContent>
90
93
  </Tooltip>
91
94
 
@@ -100,13 +103,17 @@ export function WorkspaceProjectNav({
100
103
  disabled={busy || !dirty}
101
104
  onClick={() => void runAction(onSyncProject)}
102
105
  >
103
- 同步{dirty ? " *" : ""}
106
+ {dirty ? t("panel.workspace.syncDirty") : t("common.sync")}
104
107
  </Button>
105
108
  </TooltipTrigger>
106
109
  <TooltipContent className="z-[10100]">
107
110
  {dirty
108
- ? `同步更新「${activeProjectName ?? "当前工作区"}」到 IndexedDB`
109
- : "当前工作区已与 IndexedDB 同步"}
111
+ ? t("panel.workspace.syncUpdate", {
112
+ name:
113
+ activeProjectName ??
114
+ t("panel.workspace.currentWorkspaceFallback"),
115
+ })
116
+ : t("panel.workspace.syncUpToDate")}
110
117
  </TooltipContent>
111
118
  </Tooltip>
112
119
  ) : null}
@@ -120,12 +127,14 @@ export function WorkspaceProjectNav({
120
127
  className="h-7 max-w-[200px] truncate px-2 text-xs"
121
128
  disabled={busy}
122
129
  >
123
- {activeProjectName ? `工作区:${activeProjectName}` : "已保存工作区"}
130
+ {activeProjectName
131
+ ? t("panel.workspace.workspaceNamed", { name: activeProjectName })
132
+ : t("panel.workspace.savedWorkspaces")}
124
133
  </Button>
125
134
  </DropdownMenuTrigger>
126
135
  <DropdownMenuContent align="start" className="z-[10100] w-72">
127
136
  {projects.length === 0 ? (
128
- <DropdownMenuItem disabled>暂无已保存工作区</DropdownMenuItem>
137
+ <DropdownMenuItem disabled>{t("panel.workspace.noSavedWorkspaces")}</DropdownMenuItem>
129
138
  ) : (
130
139
  projects.map((project) => (
131
140
  <div key={project.id} className="px-1 py-0.5">
@@ -160,7 +169,7 @@ export function WorkspaceProjectNav({
160
169
  })
161
170
  }
162
171
  >
163
- 预览
172
+ {t("panel.workspace.previewDocTitle")}
164
173
  </Button>
165
174
  <Button
166
175
  type="button"
@@ -169,7 +178,7 @@ export function WorkspaceProjectNav({
169
178
  className="h-7 shrink-0 px-2 text-[10px] text-destructive hover:text-destructive"
170
179
  onClick={() => setPendingDeleteId(project.id)}
171
180
  >
172
- 删除
181
+ {t("common.delete")}
173
182
  </Button>
174
183
  </div>
175
184
  </div>
@@ -177,7 +186,7 @@ export function WorkspaceProjectNav({
177
186
  )}
178
187
  {/* <DropdownMenuSeparator />
179
188
  <DropdownMenuItem onClick={() => void runAction(onCreateProject)}>
180
- 创建工作区
189
+ {t("panel.workspace.create")}
181
190
  </DropdownMenuItem> */}
182
191
  </DropdownMenuContent>
183
192
  </DropdownMenu>
@@ -192,13 +201,15 @@ export function WorkspaceProjectNav({
192
201
  >
193
202
  <AlertDialogContent className="z-[10150]">
194
203
  <AlertDialogHeader>
195
- <AlertDialogTitle>删除工作区?</AlertDialogTitle>
204
+ <AlertDialogTitle>{t("panel.workspace.deleteConfirmTitle")}</AlertDialogTitle>
196
205
  <AlertDialogDescription>
197
- 将永久删除 IndexedDB 中的「{pendingDelete?.name ?? ""}」,此操作不可恢复。
206
+ {t("panel.workspace.deleteConfirmBody", {
207
+ name: pendingDelete?.name ?? "",
208
+ })}
198
209
  </AlertDialogDescription>
199
210
  </AlertDialogHeader>
200
211
  <AlertDialogFooter>
201
- <AlertDialogCancel>取消</AlertDialogCancel>
212
+ <AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
202
213
  <AlertDialogAction
203
214
  className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
204
215
  onClick={() => {
@@ -210,7 +221,7 @@ export function WorkspaceProjectNav({
210
221
  });
211
222
  }}
212
223
  >
213
- 删除
224
+ {t("common.delete")}
214
225
  </AlertDialogAction>
215
226
  </AlertDialogFooter>
216
227
  </AlertDialogContent>
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useRef, useState, type ComponentRef, type ReactNode } from "react";
2
+ import { useI18n } from "@arronqzy/i18n/react";
2
3
 
3
4
  import {
4
5
  BluePrintReactRoot,
@@ -58,6 +59,7 @@ function BlueprintPanel({
58
59
  canSyncBlueprint?: boolean;
59
60
  blueprintDebug?: BlueprintDebugToolbarProps;
60
61
  }) {
62
+ const { t } = useI18n();
61
63
  const wrapRef = useRef<HTMLDivElement>(null);
62
64
  const [layoutReady, setLayoutReady] = useState(false);
63
65
 
@@ -104,7 +106,7 @@ function BlueprintPanel({
104
106
  <BluePrintReactRoot style={{ width: "100%", height: "100%" }} {...blueprintProps} />
105
107
  ) : (
106
108
  <div className="flex h-full items-center justify-center text-[11px] text-muted-foreground">
107
- {blueprintOpen ? "画布加载中…" : ""}
109
+ {blueprintOpen ? t("panel.workspace.canvasLoading") : ""}
108
110
  </div>
109
111
  )}
110
112
  </div>
@@ -6,6 +6,7 @@ import {
6
6
  useState,
7
7
  type RefObject,
8
8
  } from "react";
9
+ import { useI18n } from "@arronqzy/i18n/react";
9
10
  import { createPortal } from "react-dom";
10
11
 
11
12
  import type { PanelElement } from "../../types";
@@ -92,8 +93,9 @@ function inferFieldIdFromDom(
92
93
  ): string | undefined {
93
94
  const label = input.closest("label");
94
95
  const labelText = label?.textContent ?? "";
95
- if (labelText.includes("类目")) return "chart.labelsText";
96
- if (labelText.includes("数值")) return "chart.valuesText";
96
+ // Match both zh-CN and en-US config labels
97
+ if (/类目|categor|labels/i.test(labelText)) return "chart.labelsText";
98
+ if (/数值|values/i.test(labelText)) return "chart.valuesText";
97
99
  return undefined;
98
100
  }
99
101
 
@@ -106,11 +108,12 @@ function ScopeTemplatePreviewPanel({
106
108
  scope: unknown;
107
109
  fieldId?: string;
108
110
  }) {
111
+ const { t } = useI18n();
109
112
  const [isExpanded, setIsExpanded] = useState(false);
110
113
  const previewValue = resolveScopeTemplatePreview(template, scope, fieldId);
111
114
  const previewText = formatScopeTemplatePreview(previewValue);
112
115
  const previewType = Array.isArray(previewValue)
113
- ? `数组 · ${previewValue.length} 项`
116
+ ? t("panel.scope.previewArray", { count: previewValue.length })
114
117
  : typeof previewValue;
115
118
 
116
119
  return (
@@ -120,9 +123,9 @@ function ScopeTemplatePreviewPanel({
120
123
  className="flex w-full items-center justify-between gap-2 px-2 py-1 text-left text-[10px] text-muted-foreground hover:bg-accent/40"
121
124
  onClick={() => setIsExpanded((prev) => !prev)}
122
125
  >
123
- <span className="font-medium text-foreground/80">解析预览</span>
126
+ <span className="font-medium text-foreground/80">{t("panel.scope.previewTitle")}</span>
124
127
  <span className="shrink-0 text-[10px]">
125
- {previewType} · {isExpanded ? "收起" : "展开"}
128
+ {previewType} · {isExpanded ? t("panel.scope.collapsePreview") : t("panel.scope.expandPreview")}
126
129
  </span>
127
130
  </button>
128
131
  {isExpanded ? (
@@ -143,12 +146,13 @@ export function ScopeTemplatePreviewHost({
143
146
  element: PanelElement | null;
144
147
  containerRef?: RefObject<HTMLElement | null>;
145
148
  }) {
149
+ const { t } = useI18n();
146
150
  const keySeed = useId();
147
151
  const [anchors, setAnchors] = useState<PreviewAnchor[]>([]);
148
152
 
149
153
  const templateFields = useMemo(
150
- () => (element ? collectElementScopeTemplateFields(element) : []),
151
- [element]
154
+ () => (element ? collectElementScopeTemplateFields(element, t) : []),
155
+ [element, t]
152
156
  );
153
157
 
154
158
  const scanAnchors = useCallback(() => {
@@ -1,24 +1,12 @@
1
+ import { useI18n } from "@arronqzy/i18n/react";
2
+
1
3
  export function ScopeTemplateUsageHint() {
4
+ const { t } = useI18n();
5
+
2
6
  return (
3
7
  <div className="space-y-2">
4
- <p>
5
- 表单中可使用模版字符串引用 scope,例如{" "}
6
- <code className="rounded bg-muted px-1">{`{scope?.a||0}`}</code>
7
- 、
8
- <code className="rounded bg-muted px-1">{`{scope?.name||''}`}</code>
9
- 。支持 <code className="rounded bg-muted px-1">?.</code> 与{" "}
10
- <code className="rounded bg-muted px-1">||</code> 占位。
11
- </p>
12
- <p>
13
- 数组展开模版{" "}
14
- <code className="rounded bg-muted px-1">{`[...{scope?.a?.b}]`}</code>
15
- 、
16
- <code className="rounded bg-muted px-1">
17
- {`[...{scope?.data?.data?.statusName}]`}
18
- </code>
19
- :在文本字段输出逗号分隔字符串;在图表「类目」等数组字段会展开为 n
20
- 个独立项(如 3 条数据对应 3 个类目)。
21
- </p>
8
+ <p>{t("panel.scope.usageHint1")}</p>
9
+ <p>{t("panel.scope.usageHint2")}</p>
22
10
  </div>
23
11
  );
24
12
  }
@@ -1,10 +1,13 @@
1
1
  import { useEffect, useState } from "react";
2
+ import { useI18n } from "@arronqzy/i18n/react";
2
3
 
3
4
  import { useScopeConfig } from "./ScopeConfigContext";
4
5
 
5
6
  const WARNINGS_COLLAPSE_STORAGE_KEY = "panel:config-scope-warnings-collapsed";
6
7
 
7
8
  export function ScopeTemplateWarningsPanel() {
9
+ const { t } = useI18n();
10
+
8
11
  const ctx = useScopeConfig();
9
12
  const [isCollapsed, setIsCollapsed] = useState(false);
10
13
 
@@ -26,7 +29,7 @@ export function ScopeTemplateWarningsPanel() {
26
29
  <div className="mt-2 border-t border-border/50 pt-2">
27
30
  <div className="flex items-center justify-between gap-2">
28
31
  <div className="text-[11px] font-medium text-amber-700 dark:text-amber-300">
29
- 异常调用警告
32
+ {t("panel.scope.warningsTitle")}
30
33
  <span className="ml-1.5 rounded bg-amber-500/15 px-1.5 py-0.5 text-[10px] font-semibold">
31
34
  {ctx.warnings.length}
32
35
  </span>
@@ -36,7 +39,7 @@ export function ScopeTemplateWarningsPanel() {
36
39
  className="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent"
37
40
  onClick={() => setIsCollapsed((prev) => !prev)}
38
41
  >
39
- {isCollapsed ? "展开警告" : "收起警告"}
42
+ {isCollapsed ? t("panel.scope.expandWarnings") : t("panel.scope.collapseWarnings")}
40
43
  </button>
41
44
  </div>
42
45
  {!isCollapsed ? (
@@ -0,0 +1,205 @@
1
+ import type { Condition, ConditionLeaf, ConditionOp } from "@arronqzy/view-table";
2
+ import {
3
+ addConditionItem,
4
+ getConditionLogic,
5
+ isConditionLeaf,
6
+ listConditionItems,
7
+ removeConditionItem,
8
+ setConditionLogic,
9
+ updateConditionLeaf,
10
+ type ConditionLogic,
11
+ } from "@arronqzy/view-table";
12
+ import { useI18n } from "@arronqzy/i18n/react";
13
+ import {
14
+ Button,
15
+ Input,
16
+ Select,
17
+ SelectContent,
18
+ SelectItem,
19
+ SelectTrigger,
20
+ SelectValue,
21
+ } from "@arronqzy/ui";
22
+ import React from "react";
23
+
24
+ const CONDITION_OPS: ConditionOp[] = [
25
+ "eq",
26
+ "neq",
27
+ "gt",
28
+ "gte",
29
+ "lt",
30
+ "lte",
31
+ "contains",
32
+ "startsWith",
33
+ "endsWith",
34
+ "in",
35
+ "empty",
36
+ "notEmpty",
37
+ "regex",
38
+ "truthy",
39
+ "falsy",
40
+ ];
41
+
42
+ function leafValueText(value: unknown): string {
43
+ if (value == null) return "";
44
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
45
+ return String(value);
46
+ }
47
+ try {
48
+ return JSON.stringify(value);
49
+ } catch {
50
+ return "";
51
+ }
52
+ }
53
+
54
+ function parseLeafValue(text: string): unknown {
55
+ const trimmed = text.trim();
56
+ if (trimmed === "") return "";
57
+ if (trimmed === "true") return true;
58
+ if (trimmed === "false") return false;
59
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed);
60
+ return text;
61
+ }
62
+
63
+ export function ConditionEditor({
64
+ value,
65
+ onChange,
66
+ showField = false,
67
+ fieldClass = "space-y-1",
68
+ inputClass = "h-8 text-[11px]",
69
+ }: {
70
+ value: Condition | undefined;
71
+ onChange: (next: Condition) => void;
72
+ showField?: boolean;
73
+ fieldClass?: string;
74
+ inputClass?: string;
75
+ }) {
76
+ const { t } = useI18n();
77
+ const logic = getConditionLogic(value);
78
+ const items = listConditionItems(value);
79
+
80
+ const opLabel = (op: ConditionOp) => t(`panel.config.tableOp.${op}`);
81
+
82
+ const logicLabel =
83
+ logic === "or"
84
+ ? t("panel.config.tableConditionLogicOr")
85
+ : logic === "not"
86
+ ? t("panel.config.tableConditionLogicNot")
87
+ : t("panel.config.tableConditionLogicAnd");
88
+
89
+ return (
90
+ <div className="space-y-2">
91
+ <div className="flex items-center justify-between gap-2">
92
+ <label className={`${fieldClass} min-w-0 flex-1`}>
93
+ <div className="text-[11px] text-muted-foreground">{t("panel.config.tableConditionLogic")}</div>
94
+ <Select
95
+ value={logic}
96
+ onValueChange={(next) => onChange(setConditionLogic(value, next as ConditionLogic))}
97
+ >
98
+ <SelectTrigger className={inputClass}>
99
+ <SelectValue>{logicLabel}</SelectValue>
100
+ </SelectTrigger>
101
+ <SelectContent>
102
+ <SelectItem value="and">{t("panel.config.tableConditionLogicAnd")}</SelectItem>
103
+ <SelectItem value="or">{t("panel.config.tableConditionLogicOr")}</SelectItem>
104
+ <SelectItem value="not">{t("panel.config.tableConditionLogicNot")}</SelectItem>
105
+ </SelectContent>
106
+ </Select>
107
+ </label>
108
+ <Button
109
+ type="button"
110
+ variant="outline"
111
+ size="sm"
112
+ className="mt-4 h-7 shrink-0 px-2 text-[11px]"
113
+ onClick={() => onChange(addConditionItem(value))}
114
+ >
115
+ {t("panel.config.tableConditionAddItem")}
116
+ </Button>
117
+ </div>
118
+ <div className="text-[10px] text-muted-foreground">{t("panel.config.tableConditionLogicHint")}</div>
119
+ {items.map((item, index) => {
120
+ const leaf: ConditionLeaf = isConditionLeaf(item)
121
+ ? item
122
+ : { op: "eq", value: "" };
123
+ const needsValue = !["empty", "notEmpty", "truthy", "falsy"].includes(leaf.op);
124
+ return (
125
+ <div
126
+ key={`cond-${index}`}
127
+ className="space-y-1.5 rounded-md border border-border/50 bg-background/70 p-2"
128
+ >
129
+ <div className="flex items-center justify-between gap-2">
130
+ <div className="text-[10px] font-medium text-muted-foreground">
131
+ {t("panel.config.tableConditionItem", { n: index + 1 })}
132
+ </div>
133
+ <Button
134
+ type="button"
135
+ variant="ghost"
136
+ size="sm"
137
+ className="h-6 px-1.5 text-[11px] text-destructive"
138
+ disabled={items.length <= 1}
139
+ onClick={() => onChange(removeConditionItem(value, index))}
140
+ >
141
+ ×
142
+ </Button>
143
+ </div>
144
+ <div className={`grid grid-cols-1 gap-1.5 ${showField ? "sm:grid-cols-3" : "sm:grid-cols-2"}`}>
145
+ <label className={fieldClass}>
146
+ <div className="text-[11px] text-muted-foreground">{t("panel.config.tableConditionOp")}</div>
147
+ <Select
148
+ value={leaf.op}
149
+ onValueChange={(next) =>
150
+ onChange(updateConditionLeaf(value, index, { op: next as ConditionOp }))
151
+ }
152
+ >
153
+ <SelectTrigger className={inputClass}>
154
+ <SelectValue>{opLabel(leaf.op)}</SelectValue>
155
+ </SelectTrigger>
156
+ <SelectContent>
157
+ {CONDITION_OPS.map((op) => (
158
+ <SelectItem key={op} value={op}>
159
+ {opLabel(op)}
160
+ </SelectItem>
161
+ ))}
162
+ </SelectContent>
163
+ </Select>
164
+ </label>
165
+ {showField ? (
166
+ <label className={fieldClass}>
167
+ <div className="text-[11px] text-muted-foreground">{t("panel.config.tableConditionField")}</div>
168
+ <Input
169
+ value={leaf.field ?? ""}
170
+ onChange={(e) =>
171
+ onChange(
172
+ updateConditionLeaf(value, index, {
173
+ field: e.target.value || undefined,
174
+ })
175
+ )
176
+ }
177
+ className={inputClass}
178
+ placeholder={t("panel.config.tableConditionFieldPlaceholder")}
179
+ />
180
+ </label>
181
+ ) : null}
182
+ {needsValue ? (
183
+ <label className={fieldClass}>
184
+ <div className="text-[11px] text-muted-foreground">{t("panel.config.tableConditionValue")}</div>
185
+ <Input
186
+ value={leafValueText(leaf.value)}
187
+ onChange={(e) =>
188
+ onChange(
189
+ updateConditionLeaf(value, index, {
190
+ value: parseLeafValue(e.target.value),
191
+ })
192
+ )
193
+ }
194
+ className={inputClass}
195
+ placeholder={t("panel.config.tableConditionValuePlaceholder")}
196
+ />
197
+ </label>
198
+ ) : null}
199
+ </div>
200
+ </div>
201
+ );
202
+ })}
203
+ </div>
204
+ );
205
+ }
@@ -0,0 +1,115 @@
1
+ import React, { useMemo } from "react";
2
+ import { Button } from "@arronqzy/ui";
3
+ import { useI18n } from "@arronqzy/i18n/react";
4
+ import CodeMirror from "@uiw/react-codemirror";
5
+ import { json } from "@codemirror/lang-json";
6
+ import { oneDark } from "@codemirror/theme-one-dark";
7
+ import { EditorView } from "@codemirror/view";
8
+
9
+ export function JsonCodeEditor({
10
+ value,
11
+ onChange,
12
+ className,
13
+ }: {
14
+ value: string;
15
+ onChange: (next: string) => void;
16
+ className?: string;
17
+ }) {
18
+ const { t } = useI18n();
19
+ const extensions = useMemo(
20
+ () => [
21
+ json(),
22
+ EditorView.lineWrapping,
23
+ EditorView.theme({
24
+ "&": {
25
+ height: "100%",
26
+ maxHeight: "100%",
27
+ fontSize: "13px",
28
+ backgroundColor: "transparent",
29
+ },
30
+ "&.cm-editor": {
31
+ height: "100%",
32
+ maxHeight: "100%",
33
+ backgroundColor: "transparent",
34
+ },
35
+ ".cm-scroller": {
36
+ overflow: "auto",
37
+ fontFamily:
38
+ "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
39
+ lineHeight: "1.55",
40
+ },
41
+ ".cm-content": { padding: "8px 0", caretColor: "#e2e8f0" },
42
+ ".cm-gutters": {
43
+ backgroundColor: "transparent",
44
+ borderRight: "1px solid rgba(148,163,184,0.12)",
45
+ color: "#64748b",
46
+ },
47
+ ".cm-activeLineGutter": { backgroundColor: "rgba(148,163,184,0.08)" },
48
+ ".cm-activeLine": { backgroundColor: "rgba(148,163,184,0.06)" },
49
+ }),
50
+ ],
51
+ []
52
+ );
53
+
54
+ const formatJson = () => {
55
+ const trimmed = value.trim();
56
+ if (!trimmed) return;
57
+ try {
58
+ onChange(JSON.stringify(JSON.parse(trimmed), null, 2));
59
+ } catch {
60
+ // keep as-is; parent validates on save
61
+ }
62
+ };
63
+
64
+ return (
65
+ <div
66
+ className={[
67
+ "flex h-full min-h-0 max-h-full flex-col overflow-hidden rounded-xl border border-slate-700/60 bg-[#0c1220] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]",
68
+ className,
69
+ ]
70
+ .filter(Boolean)
71
+ .join(" ")}
72
+ >
73
+ <div className="flex shrink-0 items-center justify-between gap-2 border-b border-white/5 px-2.5 py-1.5">
74
+ <div className="flex min-w-0 items-center gap-2">
75
+ <span className="rounded-md bg-sky-500/15 px-1.5 py-0.5 font-mono text-[10px] font-medium tracking-wide text-sky-300">
76
+ JSON
77
+ </span>
78
+ <span className="truncate text-[11px] text-slate-400">
79
+ {t("panel.config.tableRowsEditEditorHint")}
80
+ </span>
81
+ </div>
82
+ <Button
83
+ type="button"
84
+ variant="ghost"
85
+ size="sm"
86
+ className="h-6 shrink-0 rounded-md px-2 text-[11px] text-slate-300 hover:bg-white/10 hover:text-white"
87
+ onClick={formatJson}
88
+ >
89
+ {t("panel.config.tableRowsEditFormat")}
90
+ </Button>
91
+ </div>
92
+ <div className="relative min-h-0 flex-1 overflow-hidden">
93
+ <div className="absolute inset-0 overflow-hidden">
94
+ <CodeMirror
95
+ value={value}
96
+ height="100%"
97
+ maxHeight="100%"
98
+ theme={oneDark}
99
+ extensions={extensions}
100
+ basicSetup={{
101
+ lineNumbers: true,
102
+ foldGutter: true,
103
+ highlightActiveLine: true,
104
+ bracketMatching: true,
105
+ autocompletion: true,
106
+ }}
107
+ onChange={(next) => onChange(next)}
108
+ className="h-full max-h-full [&_.cm-editor]:h-full [&_.cm-editor]:max-h-full [&_.cm-editor]:outline-none"
109
+ aria-label={t("panel.config.tableRowsEditTitle")}
110
+ />
111
+ </div>
112
+ </div>
113
+ </div>
114
+ );
115
+ }