@arronqzy/vue-view 0.1.0 → 0.1.3

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 +10 -7
  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
@@ -0,0 +1,2033 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from "vue";
3
+ import { useI18n } from "@arronqzy/i18n/vue";
4
+ import { Button, Input, InputNumber, Modal, Select, Switch, Textarea, message } from "ant-design-vue";
5
+ import type {
6
+ Condition,
7
+ PanelTableConfig,
8
+ TableCellActions,
9
+ TableCellWidget,
10
+ TableColumnConfig,
11
+ TableStyleProps,
12
+ TableTooltipPlacement,
13
+ TableWidgetProps,
14
+ TableMockLocale,
15
+ } from "@arronqzy/view-table";
16
+ import {
17
+ coerceBooleanMapTarget,
18
+ createDefaultCondition,
19
+ findBooleanValueMapConflicts,
20
+ generateMockTableRows,
21
+ stringifyMockTableRows,
22
+ } from "@arronqzy/view-table";
23
+ import type { PanelElement } from "../../types";
24
+ import ConfigColorField from "../config/ConfigColorField.vue";
25
+ import ConfigFieldGroup from "../config/ConfigFieldGroup.vue";
26
+ import ConfigSection from "../config/ConfigSection.vue";
27
+ import ConfigHintIcon from "../ConfigHintIcon.vue";
28
+ import ConditionEditor from "./ConditionEditor.vue";
29
+ import JsonCodeEditor from "./JsonCodeEditor.vue";
30
+
31
+ const { t } = useI18n();
32
+
33
+ const WIDGET_OPTIONS: TableCellWidget[] = [
34
+ "text",
35
+ "tag",
36
+ "badge",
37
+ "link",
38
+ "progress",
39
+ "image",
40
+ "boolean",
41
+ ];
42
+
43
+ const TRANSFORM_MODES = ["auto", "records", "matrix", "path"] as const;
44
+
45
+ const props = withDefaults(
46
+ defineProps<{
47
+ element: PanelElement;
48
+ isEditable: boolean;
49
+ open: boolean;
50
+ forceOpen?: boolean;
51
+ updateElement: (
52
+ id: string,
53
+ patch: Partial<PanelElement>,
54
+ options?: { batchId?: string; meta?: Record<string, unknown> }
55
+ ) => void;
56
+ blueprintNodeOptions?: { id: string; label: string }[];
57
+ }>(),
58
+ { blueprintNodeOptions: () => [] }
59
+ );
60
+
61
+ const emit = defineEmits<{
62
+ "update:open": [value: boolean];
63
+ }>();
64
+
65
+ const selectedColumnIndex = ref(0);
66
+ const rowsEditorOpen = ref(false);
67
+ const rowsEditorDraft = ref("");
68
+ const mockOpen = ref(false);
69
+ const mockRowCount = ref(5);
70
+ const mockSeed = ref("");
71
+ const mockNamePrefix = ref("");
72
+ const mockLocale = ref<TableMockLocale>("zh");
73
+ const mockIncludeId = ref(true);
74
+ const mockIncludeTimestamp = ref(false);
75
+ const mockIncludeExtras = ref(true);
76
+ const mockStatusValues = ref("active,idle,pending,done");
77
+ const mockScoreMin = ref(0);
78
+ const mockScoreMax = ref(100);
79
+
80
+ function openRowsEditor() {
81
+ rowsEditorDraft.value = table.value.rowsText ?? "";
82
+ rowsEditorOpen.value = true;
83
+ }
84
+
85
+ /** Return a rejected Promise to keep Modal open on validation failure. */
86
+ function saveRowsEditor() {
87
+ const trimmed = rowsEditorDraft.value.trim();
88
+ if (!trimmed) {
89
+ updateTable({ rowsText: "" });
90
+ rowsEditorOpen.value = false;
91
+ return Promise.resolve();
92
+ }
93
+ try {
94
+ const parsed = JSON.parse(trimmed);
95
+ if (!Array.isArray(parsed)) {
96
+ message.error(t("panel.config.tableRowsEditNeedArray"));
97
+ return Promise.reject();
98
+ }
99
+ updateTable({ rowsText: JSON.stringify(parsed, null, 2) });
100
+ rowsEditorOpen.value = false;
101
+ return Promise.resolve();
102
+ } catch {
103
+ message.error(t("panel.config.tableRowsEditInvalidJson"));
104
+ return Promise.reject();
105
+ }
106
+ }
107
+
108
+ function applyMockRows() {
109
+ const rows = generateMockTableRows(columns.value, {
110
+ rowCount: mockRowCount.value ?? 5,
111
+ seed: mockSeed.value || undefined,
112
+ namePrefix: mockNamePrefix.value || undefined,
113
+ locale: mockLocale.value,
114
+ includeId: mockIncludeId.value,
115
+ includeTimestamp: mockIncludeTimestamp.value,
116
+ includeExtras: mockIncludeExtras.value,
117
+ statusValues: mockStatusValues.value.split(/[,,]/),
118
+ scoreMin: mockScoreMin.value ?? 0,
119
+ scoreMax: mockScoreMax.value ?? 100,
120
+ });
121
+ updateTable({ rowsText: stringifyMockTableRows(rows) });
122
+ mockOpen.value = false;
123
+ }
124
+
125
+ const table = computed(() => (props.element.table ?? {}) as PanelTableConfig);
126
+ const columns = computed(() => table.value.columns ?? []);
127
+
128
+ const selectedColumn = computed(() => {
129
+ const cols = columns.value;
130
+ if (cols.length === 0) return null;
131
+ const idx = Math.min(Math.max(0, selectedColumnIndex.value), cols.length - 1);
132
+ return { index: idx, column: cols[idx]! };
133
+ });
134
+
135
+ const booleanValueMapConflicts = computed(() => {
136
+ const col = selectedColumn.value?.column;
137
+ if (!col || col.widget !== "boolean") return [];
138
+ return findBooleanValueMapConflicts(col.valueMap);
139
+ });
140
+
141
+ function booleanMapSelectValue(value: unknown): "true" | "false" {
142
+ return coerceBooleanMapTarget(value) === false ? "false" : "true";
143
+ }
144
+
145
+ function blueprintSelectValue(id?: string): string {
146
+ return id?.trim() ? id : "__none__";
147
+ }
148
+
149
+ /** ant-design-vue Switch emits `CheckedType` (boolean | string). */
150
+ function asSwitchChecked(v: unknown): boolean {
151
+ return v === true;
152
+ }
153
+
154
+ /** ant-design-vue Select emits `SelectValue` (string | number | array | undefined). */
155
+ function asSelectString(v: unknown): string {
156
+ if (Array.isArray(v)) return String(v[0] ?? "");
157
+ return v == null ? "" : String(v);
158
+ }
159
+
160
+ function onBlueprintSelect(
161
+ index: number,
162
+ key: keyof TableCellActions,
163
+ v: unknown
164
+ ) {
165
+ const next = asSelectString(v);
166
+ patchActions(index, {
167
+ [key]: next === "__none__" || !next ? undefined : next,
168
+ });
169
+ }
170
+
171
+ function updateTable(patch: Partial<PanelTableConfig>) {
172
+ props.updateElement(props.element.id, {
173
+ table: { ...table.value, ...patch },
174
+ });
175
+ }
176
+
177
+ function updateTransform(
178
+ patch: Partial<NonNullable<PanelTableConfig["transform"]>>
179
+ ) {
180
+ updateTable({
181
+ transform: { ...(table.value.transform ?? {}), ...patch },
182
+ });
183
+ }
184
+
185
+ function updateTableStyle(patch: Partial<NonNullable<PanelTableConfig["tableStyle"]>>) {
186
+ updateTable({
187
+ tableStyle: { ...(table.value.tableStyle ?? {}), ...patch },
188
+ });
189
+ }
190
+
191
+ function updateHeaderStyle(patch: Partial<TableStyleProps>) {
192
+ updateTable({
193
+ headerStyle: { ...(table.value.headerStyle ?? {}), ...patch },
194
+ });
195
+ }
196
+
197
+ function setColumns(next: TableColumnConfig[]) {
198
+ updateTable({ columns: next });
199
+ }
200
+
201
+ function addColumn() {
202
+ const n = columns.value.length + 1;
203
+ const next: TableColumnConfig[] = [
204
+ ...columns.value,
205
+ {
206
+ id: `col-${Date.now().toString(36)}`,
207
+ field: `field${n}`,
208
+ title: t("panel.config.tableColumnUntitled", { n }),
209
+ width: 120,
210
+ widget: "text",
211
+ },
212
+ ];
213
+ setColumns(next);
214
+ selectedColumnIndex.value = next.length - 1;
215
+ }
216
+
217
+ function removeColumn(index: number) {
218
+ const next = columns.value.filter((_, i) => i !== index);
219
+ setColumns(next);
220
+ selectedColumnIndex.value = Math.max(0, Math.min(index, next.length - 1));
221
+ }
222
+
223
+ function moveColumn(index: number, delta: -1 | 1) {
224
+ const target = index + delta;
225
+ if (target < 0 || target >= columns.value.length) return;
226
+ const next = [...columns.value];
227
+ const [item] = next.splice(index, 1);
228
+ if (!item) return;
229
+ next.splice(target, 0, item);
230
+ setColumns(next);
231
+ selectedColumnIndex.value = target;
232
+ }
233
+
234
+ function patchColumn(index: number, patch: Partial<TableColumnConfig>) {
235
+ const next = columns.value.map((col, i) => (i === index ? { ...col, ...patch } : col));
236
+ setColumns(next);
237
+ }
238
+
239
+ function patchWidgetProps(index: number, patch: Partial<TableWidgetProps>) {
240
+ const col = columns.value[index];
241
+ if (!col) return;
242
+ patchColumn(index, {
243
+ widgetProps: { ...(col.widgetProps ?? {}), ...patch },
244
+ });
245
+ }
246
+
247
+ function patchActions(index: number, patch: Partial<TableCellActions>) {
248
+ const col = columns.value[index];
249
+ if (!col) return;
250
+ patchWidgetProps(index, {
251
+ actions: { ...(col.widgetProps?.actions ?? {}), ...patch },
252
+ });
253
+ }
254
+
255
+ const imageUploadHint = ref("");
256
+
257
+ async function uploadImageForColumn(index: number, file: File) {
258
+ try {
259
+ const base64 = await new Promise<string>((resolve, reject) => {
260
+ const reader = new FileReader();
261
+ reader.onload = () => resolve(String(reader.result ?? ""));
262
+ reader.onerror = () => reject(new Error("read failed"));
263
+ reader.readAsDataURL(file);
264
+ });
265
+ patchWidgetProps(index, { imageUrlMode: "static", imageUrl: base64 });
266
+ imageUploadHint.value = t("panel.config.uploadWrittenBase64");
267
+ try {
268
+ const form = new FormData();
269
+ form.append("file", file);
270
+ const resp = await fetch("/api/upload", { method: "POST", body: form });
271
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
272
+ const data = (await resp.json()) as { url?: string };
273
+ if (data.url) {
274
+ patchWidgetProps(index, { imageUrlMode: "static", imageUrl: data.url });
275
+ imageUploadHint.value = t("panel.config.uploadServerAndBase64");
276
+ }
277
+ } catch {
278
+ imageUploadHint.value = t("panel.config.uploadServerFailedKeepBase64");
279
+ }
280
+ } catch {
281
+ imageUploadHint.value = t("panel.messages.readImageFailed");
282
+ }
283
+ }
284
+
285
+ function onImageFileChange(index: number, e: Event) {
286
+ const input = e.target as HTMLInputElement;
287
+ const file = input.files?.[0];
288
+ input.value = "";
289
+ if (file) void uploadImageForColumn(index, file);
290
+ }
291
+
292
+ function addValueMapRule(index: number) {
293
+ const col = columns.value[index];
294
+ if (!col) return;
295
+ const valueMap = [
296
+ ...(col.valueMap ?? []),
297
+ {
298
+ when: createDefaultCondition(),
299
+ value: col.widget === "boolean" ? true : "",
300
+ },
301
+ ];
302
+ patchColumn(index, { valueMap });
303
+ }
304
+
305
+ function patchValueMapRule(
306
+ colIndex: number,
307
+ ruleIndex: number,
308
+ patch: { value?: string | boolean; when?: Condition }
309
+ ) {
310
+ const col = columns.value[colIndex];
311
+ if (!col?.valueMap) return;
312
+ const valueMap = col.valueMap.map((rule, i) => {
313
+ if (i !== ruleIndex) return rule;
314
+ return {
315
+ ...rule,
316
+ value: patch.value !== undefined ? patch.value : rule.value,
317
+ when: patch.when !== undefined ? patch.when : rule.when,
318
+ };
319
+ });
320
+ patchColumn(colIndex, { valueMap });
321
+ }
322
+
323
+ function removeValueMapRule(colIndex: number, ruleIndex: number) {
324
+ const col = columns.value[colIndex];
325
+ if (!col?.valueMap) return;
326
+ patchColumn(colIndex, {
327
+ valueMap: col.valueMap.filter((_, i) => i !== ruleIndex),
328
+ });
329
+ }
330
+
331
+ function addCellStyleRule(index: number) {
332
+ const col = columns.value[index];
333
+ if (!col) return;
334
+ const cellStyleRules = [
335
+ ...(col.cellStyleRules ?? []),
336
+ {
337
+ when: createDefaultCondition(),
338
+ style: { backgroundColor: "#fef3c7" },
339
+ },
340
+ ];
341
+ patchColumn(index, { cellStyleRules });
342
+ }
343
+
344
+ function patchCellStyleRule(
345
+ colIndex: number,
346
+ ruleIndex: number,
347
+ patch: { when?: Condition; backgroundColor?: string }
348
+ ) {
349
+ const col = columns.value[colIndex];
350
+ if (!col?.cellStyleRules) return;
351
+ const cellStyleRules = col.cellStyleRules.map((rule, i) => {
352
+ if (i !== ruleIndex) return rule;
353
+ return {
354
+ ...rule,
355
+ when: patch.when !== undefined ? patch.when : rule.when,
356
+ style: {
357
+ ...rule.style,
358
+ backgroundColor:
359
+ patch.backgroundColor !== undefined
360
+ ? patch.backgroundColor
361
+ : rule.style.backgroundColor,
362
+ },
363
+ };
364
+ });
365
+ patchColumn(colIndex, { cellStyleRules });
366
+ }
367
+
368
+ function removeCellStyleRule(colIndex: number, ruleIndex: number) {
369
+ const col = columns.value[colIndex];
370
+ if (!col?.cellStyleRules) return;
371
+ patchColumn(colIndex, {
372
+ cellStyleRules: col.cellStyleRules.filter((_, i) => i !== ruleIndex),
373
+ });
374
+ }
375
+
376
+ function addRowStyleRule() {
377
+ const rowStyleRules = [
378
+ ...(table.value.rowStyleRules ?? []),
379
+ {
380
+ when: createDefaultCondition(),
381
+ style: { backgroundColor: "#eff6ff" },
382
+ },
383
+ ];
384
+ updateTable({ rowStyleRules });
385
+ }
386
+
387
+ function patchRowStyleRule(
388
+ ruleIndex: number,
389
+ patch: { when?: Condition; backgroundColor?: string }
390
+ ) {
391
+ const rules = table.value.rowStyleRules ?? [];
392
+ const rowStyleRules = rules.map((rule, i) => {
393
+ if (i !== ruleIndex) return rule;
394
+ return {
395
+ ...rule,
396
+ when: patch.when !== undefined ? patch.when : rule.when,
397
+ style: {
398
+ ...rule.style,
399
+ backgroundColor:
400
+ patch.backgroundColor !== undefined
401
+ ? patch.backgroundColor
402
+ : rule.style.backgroundColor,
403
+ },
404
+ };
405
+ });
406
+ updateTable({ rowStyleRules });
407
+ }
408
+
409
+ function removeRowStyleRule(ruleIndex: number) {
410
+ updateTable({
411
+ rowStyleRules: (table.value.rowStyleRules ?? []).filter((_, i) => i !== ruleIndex),
412
+ });
413
+ }
414
+
415
+ function patchHrefRuleWhen(ruleIndex: number, when: Condition) {
416
+ if (!selectedColumn.value) return;
417
+ const next = [...(selectedColumn.value.column.widgetProps?.hrefRules ?? [])];
418
+ const rule = next[ruleIndex];
419
+ if (!rule) return;
420
+ next[ruleIndex] = { ...rule, when };
421
+ patchWidgetProps(selectedColumn.value.index, { hrefRules: next });
422
+ }
423
+
424
+ function patchProgressRuleWhen(ruleIndex: number, when: Condition) {
425
+ if (!selectedColumn.value) return;
426
+ const next = [...(selectedColumn.value.column.widgetProps?.progressRules ?? [])];
427
+ const rule = next[ruleIndex];
428
+ if (!rule) return;
429
+ next[ruleIndex] = { ...rule, when };
430
+ patchWidgetProps(selectedColumn.value.index, { progressRules: next });
431
+ }
432
+ </script>
433
+
434
+ <template>
435
+ <ConfigSection
436
+ :title="t('panel.config.sectionTable')"
437
+ :open="open"
438
+ :force-open="forceOpen"
439
+ @update:open="emit('update:open', $event)"
440
+ >
441
+ <ConfigFieldGroup :title="t('panel.config.groupTableData')">
442
+ <template #hint>{{ t("panel.config.groupTableDataHint") }}</template>
443
+ <label class="block space-y-1.5">
444
+ <div class="flex items-center gap-1">
445
+ <span>{{ t("panel.config.tableSource") }}</span>
446
+ <ConfigHintIcon :label="t('panel.config.tableSource')">
447
+ <div>{{ t("panel.config.tableSourceHint") }}</div>
448
+ </ConfigHintIcon>
449
+ </div>
450
+ <Input
451
+ size="small"
452
+ :value="table.source ?? ''"
453
+ placeholder="{scope?.list}"
454
+ :disabled="!isEditable"
455
+ @update:value="(v: unknown) => updateTable({ source: asSelectString(v) })"
456
+ />
457
+ </label>
458
+ <label class="block space-y-1.5">
459
+ <div class="flex items-center justify-between gap-2">
460
+ <div class="flex items-center gap-1">
461
+ <span>{{ t("panel.config.tableRowsText") }}</span>
462
+ <ConfigHintIcon :label="t('panel.config.tableRowsText')">
463
+ <div>{{ t("panel.config.tableRowsTextHint") }}</div>
464
+ </ConfigHintIcon>
465
+ </div>
466
+ <div class="flex items-center gap-0.5">
467
+ <Button
468
+ size="small"
469
+ type="text"
470
+ class="!px-1"
471
+ :disabled="!isEditable"
472
+ :title="t('panel.config.tableRowsEdit')"
473
+ :aria-label="t('panel.config.tableRowsEdit')"
474
+ @click="openRowsEditor"
475
+ >
476
+ <svg viewBox="0 0 24 24" class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="1.8">
477
+ <path d="M12 20h9" />
478
+ <path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z" />
479
+ </svg>
480
+ </Button>
481
+ <Button
482
+ size="small"
483
+ type="text"
484
+ class="!px-1"
485
+ :disabled="!isEditable"
486
+ :title="t('panel.config.tableRowsMock')"
487
+ :aria-label="t('panel.config.tableRowsMock')"
488
+ @click="mockOpen = true"
489
+ >
490
+ <svg viewBox="0 0 24 24" class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="1.8">
491
+ <path d="M12 3v3M12 18v3M3 12h3M18 12h3" />
492
+ <circle cx="12" cy="12" r="4" />
493
+ <path d="M5.6 5.6 7.7 7.7M16.3 16.3l2.1 2.1M16.3 7.7l2.1-2.1M5.6 18.4 7.7 16.3" />
494
+ </svg>
495
+ </Button>
496
+ </div>
497
+ </div>
498
+ <Textarea
499
+ :value="table.rowsText ?? ''"
500
+ :rows="8"
501
+ :disabled="!isEditable"
502
+ placeholder='[{"name":"A"}]'
503
+ @update:value="(v: unknown) => updateTable({ rowsText: asSelectString(v) })"
504
+ />
505
+ </label>
506
+ <details class="rounded-md border border-gray-200 bg-gray-50/60 px-2 py-1.5">
507
+ <summary class="cursor-pointer text-[11px] font-medium text-gray-600">
508
+ {{ t("panel.config.tableAdvancedData") }}
509
+ </summary>
510
+ <div class="mt-2 space-y-2 border-t border-gray-100 pt-2">
511
+ <p class="text-[10px] text-gray-500">{{ t("panel.config.tableAdvancedDataHint") }}</p>
512
+ <div class="grid grid-cols-2 gap-2">
513
+ <label class="block space-y-1.5">
514
+ <div class="flex items-center gap-1">
515
+ <span>{{ t("panel.config.tableTransformMode") }}</span>
516
+ <ConfigHintIcon :label="t('panel.config.tableTransformMode')">
517
+ <div>{{ t("panel.config.tableTransformModeHint") }}</div>
518
+ </ConfigHintIcon>
519
+ </div>
520
+ <Select
521
+ size="small"
522
+ class="w-full"
523
+ :value="table.transform?.mode ?? 'auto'"
524
+ :disabled="!isEditable"
525
+ @update:value="(v) => updateTransform({ mode: v as (typeof TRANSFORM_MODES)[number] })"
526
+ >
527
+ <Select.Option v-for="mode in TRANSFORM_MODES" :key="mode" :value="mode">
528
+ {{ t(`panel.config.tableMode.${mode}`) }}
529
+ </Select.Option>
530
+ </Select>
531
+ </label>
532
+ <label class="block space-y-1.5">
533
+ <div class="flex items-center gap-1">
534
+ <span>{{ t("panel.config.tableRowIdField") }}</span>
535
+ <ConfigHintIcon :label="t('panel.config.tableRowIdField')">
536
+ <div>{{ t("panel.config.tableRowIdFieldHint") }}</div>
537
+ </ConfigHintIcon>
538
+ </div>
539
+ <Input
540
+ size="small"
541
+ :value="table.transform?.rowIdField ?? ''"
542
+ :disabled="!isEditable"
543
+ @update:value="(v: unknown) => updateTransform({ rowIdField: asSelectString(v) })"
544
+ />
545
+ </label>
546
+ </div>
547
+ <label class="block space-y-1.5">
548
+ <div class="flex items-center gap-1">
549
+ <span>{{ t("panel.config.tableTransformPath") }}</span>
550
+ <ConfigHintIcon :label="t('panel.config.tableTransformPath')">
551
+ <div>{{ t("panel.config.tableTransformPathHint") }}</div>
552
+ </ConfigHintIcon>
553
+ </div>
554
+ <Input
555
+ size="small"
556
+ :value="table.transform?.path ?? ''"
557
+ placeholder="data.list"
558
+ :disabled="!isEditable"
559
+ @update:value="(v: unknown) => updateTransform({ path: asSelectString(v) })"
560
+ />
561
+ </label>
562
+ </div>
563
+ </details>
564
+ </ConfigFieldGroup>
565
+
566
+ <ConfigFieldGroup :title="t('panel.config.groupTableColumns')">
567
+ <template #hint>{{ t("panel.config.groupTableColumnsHint") }}</template>
568
+ <div class="flex justify-end">
569
+ <Button size="small" type="dashed" :disabled="!isEditable" @click="addColumn">
570
+ {{ t("panel.config.tableAddColumn") }}
571
+ </Button>
572
+ </div>
573
+ <div v-if="!columns.length" class="text-[11px] text-gray-500">
574
+ {{ t("panel.config.tableNoColumns") }}
575
+ </div>
576
+ <div class="space-y-1.5">
577
+ <div
578
+ v-for="(col, index) in columns"
579
+ :key="col.id ?? col.field ?? index"
580
+ class="flex items-center gap-1.5 rounded-md border px-1.5 py-1.5"
581
+ :class="
582
+ selectedColumn?.index === index
583
+ ? 'border-blue-400 bg-blue-50 ring-1 ring-blue-200'
584
+ : 'border-gray-200 hover:bg-gray-50'
585
+ "
586
+ role="button"
587
+ tabindex="0"
588
+ @click="selectedColumnIndex = index"
589
+ @keydown.enter.prevent="selectedColumnIndex = index"
590
+ >
591
+ <div class="flex shrink-0 flex-col gap-0.5">
592
+ <Button
593
+ size="small"
594
+ type="text"
595
+ class="!h-5 !w-5 !p-0 text-[10px]"
596
+ :disabled="!isEditable || index === 0"
597
+ :title="t('panel.config.tableMoveColumnUp')"
598
+ @click.stop="moveColumn(index, -1)"
599
+ >
600
+ ↑
601
+ </Button>
602
+ <Button
603
+ size="small"
604
+ type="text"
605
+ class="!h-5 !w-5 !p-0 text-[10px]"
606
+ :disabled="!isEditable || index >= columns.length - 1"
607
+ :title="t('panel.config.tableMoveColumnDown')"
608
+ @click.stop="moveColumn(index, 1)"
609
+ >
610
+ ↓
611
+ </Button>
612
+ </div>
613
+ <div class="min-w-0 flex-1">
614
+ <div class="truncate text-[11px] font-medium">
615
+ {{ col.title || col.field || t('panel.config.tableColumnUntitled', { n: index + 1 }) }}
616
+ </div>
617
+ <div class="truncate text-[10px] text-gray-500">
618
+ {{ col.field }} · {{ t(`panel.config.tableWidget.${col.widget ?? 'text'}`) }}
619
+ </div>
620
+ </div>
621
+ <label class="flex shrink-0 items-center gap-1 text-[10px] text-gray-500" @click.stop>
622
+ <span class="whitespace-nowrap">{{ t("panel.config.tableColumnWidth") }}</span>
623
+ <InputNumber
624
+ size="small"
625
+ class="!w-16"
626
+ :min="40"
627
+ :max="800"
628
+ :value="col.width"
629
+ :disabled="!isEditable"
630
+ @update:value="(v) => patchColumn(index, { width: typeof v === 'number' ? v : undefined })"
631
+ />
632
+ </label>
633
+ <Button
634
+ size="small"
635
+ danger
636
+ type="text"
637
+ class="!px-1"
638
+ :disabled="!isEditable"
639
+ :title="t('panel.config.tableRemoveColumn')"
640
+ @click.stop="removeColumn(index)"
641
+ >
642
+ ×
643
+ </Button>
644
+ </div>
645
+ </div>
646
+
647
+ <div v-if="selectedColumn" class="space-y-2 rounded border border-gray-200/70 bg-white p-2">
648
+ <div class="text-[11px] font-medium text-gray-600">
649
+ {{
650
+ t("panel.config.groupTableColumnDetail", {
651
+ name: selectedColumn.column.title || selectedColumn.column.field,
652
+ })
653
+ }}
654
+ </div>
655
+ <div class="grid grid-cols-2 gap-2">
656
+ <label class="block space-y-1">
657
+ <div class="flex items-center gap-1">
658
+ <span>{{ t("panel.config.tableColumnField") }}</span>
659
+ <ConfigHintIcon :label="t('panel.config.tableColumnField')">
660
+ <div>{{ t("panel.config.tableColumnFieldHint") }}</div>
661
+ </ConfigHintIcon>
662
+ </div>
663
+ <Input
664
+ size="small"
665
+ :value="selectedColumn.column.field"
666
+ :disabled="!isEditable"
667
+ @update:value="(v: unknown) => patchColumn(selectedColumn!.index, { field: asSelectString(v) })"
668
+ />
669
+ </label>
670
+ <label class="block space-y-1">
671
+ <div class="flex items-center gap-1">
672
+ <span>{{ t("panel.config.tableColumnTitle") }}</span>
673
+ <ConfigHintIcon :label="t('panel.config.tableColumnTitle')">
674
+ <div>{{ t("panel.config.tableColumnTitleHint") }}</div>
675
+ </ConfigHintIcon>
676
+ </div>
677
+ <Input
678
+ size="small"
679
+ :value="selectedColumn.column.title ?? ''"
680
+ :disabled="!isEditable"
681
+ @update:value="(v: unknown) => patchColumn(selectedColumn!.index, { title: asSelectString(v) })"
682
+ />
683
+ </label>
684
+ <label class="block space-y-1">
685
+ <div>{{ t("panel.config.tableColumnWidth") }}</div>
686
+ <InputNumber
687
+ size="small"
688
+ class="w-full"
689
+ :min="40"
690
+ :value="selectedColumn.column.width"
691
+ :disabled="!isEditable"
692
+ @update:value="(v) => patchColumn(selectedColumn!.index, { width: typeof v === 'number' ? v : undefined })"
693
+ />
694
+ </label>
695
+ <label class="block space-y-1">
696
+ <div class="flex items-center gap-1">
697
+ <span>{{ t("panel.config.tableColumnWidget") }}</span>
698
+ <ConfigHintIcon :label="t('panel.config.tableColumnWidget')">
699
+ <div>{{ t("panel.config.tableColumnWidgetHint") }}</div>
700
+ </ConfigHintIcon>
701
+ </div>
702
+ <Select
703
+ size="small"
704
+ class="w-full"
705
+ :value="selectedColumn.column.widget ?? 'text'"
706
+ :disabled="!isEditable"
707
+ @update:value="(v) => patchColumn(selectedColumn!.index, { widget: v as TableCellWidget })"
708
+ >
709
+ <Select.Option v-for="w in WIDGET_OPTIONS" :key="w" :value="w">
710
+ {{ t(`panel.config.tableWidget.${w}`) }}
711
+ </Select.Option>
712
+ </Select>
713
+ </label>
714
+ </div>
715
+
716
+ <div class="space-y-2 rounded-md border border-gray-200 bg-gray-50/70 p-2">
717
+ <div class="flex items-center justify-between gap-2">
718
+ <div class="inline-flex items-center gap-1 text-[11px] font-medium text-gray-600">
719
+ <span>{{ t("panel.config.tableTooltipEnabled") }}</span>
720
+ <ConfigHintIcon :label="t('panel.config.tableTooltipEnabled')">
721
+ <div>{{ t("panel.config.tableTooltipEnabledHint") }}</div>
722
+ </ConfigHintIcon>
723
+ </div>
724
+ <Switch
725
+ size="small"
726
+ :checked="Boolean(selectedColumn.column.tooltipEnabled)"
727
+ :disabled="!isEditable"
728
+ @update:checked="
729
+ (v) =>
730
+ patchColumn(selectedColumn!.index, { tooltipEnabled: asSwitchChecked(v) || undefined })
731
+ "
732
+ />
733
+ </div>
734
+ <template v-if="selectedColumn.column.tooltipEnabled">
735
+ <label class="block space-y-1">
736
+ <div>{{ t("panel.config.tableTooltipPlacementLabel") }}</div>
737
+ <Select
738
+ size="small"
739
+ class="w-full"
740
+ :value="selectedColumn.column.tooltipPlacement ?? 'top'"
741
+ :disabled="!isEditable"
742
+ @update:value="
743
+ (v) =>
744
+ patchColumn(selectedColumn!.index, {
745
+ tooltipPlacement: v as TableTooltipPlacement,
746
+ })
747
+ "
748
+ >
749
+ <Select.Option value="top">{{ t("panel.config.tableTooltipPlacement.top") }}</Select.Option>
750
+ <Select.Option value="right">{{ t("panel.config.tableTooltipPlacement.right") }}</Select.Option>
751
+ <Select.Option value="bottom">{{ t("panel.config.tableTooltipPlacement.bottom") }}</Select.Option>
752
+ <Select.Option value="left">{{ t("panel.config.tableTooltipPlacement.left") }}</Select.Option>
753
+ </Select>
754
+ </label>
755
+ <label class="block space-y-1">
756
+ <div class="flex items-center gap-1">
757
+ <span>{{ t("panel.config.tableTooltipTemplate") }}</span>
758
+ <ConfigHintIcon :label="t('panel.config.tableTooltipTemplate')">
759
+ <div>{{ t("panel.config.tableTooltipTemplateHint") }}</div>
760
+ </ConfigHintIcon>
761
+ </div>
762
+ <Input
763
+ size="small"
764
+ :value="selectedColumn.column.tooltipTemplate ?? ''"
765
+ :disabled="!isEditable"
766
+ placeholder="{current}"
767
+ @update:value="
768
+ (v: unknown) => {
769
+ const s = asSelectString(v).trim();
770
+ patchColumn(selectedColumn!.index, {
771
+ tooltipTemplate: s ? s : undefined,
772
+ });
773
+ }
774
+ "
775
+ />
776
+ </label>
777
+ </template>
778
+ </div>
779
+
780
+ <div class="space-y-2 rounded-md border border-gray-200 bg-gray-50/70 p-2">
781
+ <div class="inline-flex items-center gap-1 text-[11px] font-medium text-gray-600">
782
+ {{ t("panel.config.tableWidgetProps") }}
783
+ <ConfigHintIcon :label="t('panel.config.tableWidgetProps')">
784
+ <div>{{ t("panel.config.tableWidgetPropsHint") }}</div>
785
+ </ConfigHintIcon>
786
+ </div>
787
+
788
+ <template v-if="(selectedColumn.column.widget ?? 'text') === 'text'">
789
+ <div class="grid grid-cols-2 gap-2">
790
+ <label class="block space-y-1">
791
+ <div>{{ t("panel.config.tableTextFontSize") }}</div>
792
+ <InputNumber
793
+ size="small"
794
+ class="w-full"
795
+ :min="8"
796
+ :max="72"
797
+ :value="selectedColumn.column.widgetProps?.textStyle?.fontSize"
798
+ :disabled="!isEditable"
799
+ @update:value="
800
+ (v) =>
801
+ patchWidgetProps(selectedColumn!.index, {
802
+ textStyle: {
803
+ ...(selectedColumn!.column.widgetProps?.textStyle ?? {}),
804
+ fontSize: typeof v === 'number' ? v : undefined,
805
+ },
806
+ })
807
+ "
808
+ />
809
+ </label>
810
+ <label class="block space-y-1">
811
+ <div>{{ t("panel.config.tableTextFontWeight") }}</div>
812
+ <Select
813
+ size="small"
814
+ class="w-full"
815
+ :value="String(selectedColumn.column.widgetProps?.textStyle?.fontWeight ?? 'normal')"
816
+ :disabled="!isEditable"
817
+ @update:value="
818
+ (v) => {
819
+ const s = asSelectString(v);
820
+ patchWidgetProps(selectedColumn!.index, {
821
+ textStyle: {
822
+ ...(selectedColumn!.column.widgetProps?.textStyle ?? {}),
823
+ fontWeight: s === 'normal' ? undefined : s,
824
+ },
825
+ });
826
+ }
827
+ "
828
+ >
829
+ <Select.Option value="normal">{{ t("panel.config.tableTextWeightNormal") }}</Select.Option>
830
+ <Select.Option value="500">500</Select.Option>
831
+ <Select.Option value="600">600</Select.Option>
832
+ <Select.Option value="700">{{ t("panel.config.tableTextWeightBold") }}</Select.Option>
833
+ </Select>
834
+ </label>
835
+ <ConfigColorField
836
+ :label="t('panel.config.tableTextColor')"
837
+ :value="selectedColumn.column.widgetProps?.textStyle?.color ?? '#111827'"
838
+ :disabled="!isEditable"
839
+ @update:value="
840
+ (v) =>
841
+ patchWidgetProps(selectedColumn!.index, {
842
+ textStyle: {
843
+ ...(selectedColumn!.column.widgetProps?.textStyle ?? {}),
844
+ color: v || undefined,
845
+ },
846
+ })
847
+ "
848
+ />
849
+ <label class="block space-y-1">
850
+ <div>{{ t("panel.config.tableTextFontFamily") }}</div>
851
+ <Input
852
+ size="small"
853
+ :value="selectedColumn.column.widgetProps?.textStyle?.fontFamily ?? ''"
854
+ placeholder="inherit"
855
+ :disabled="!isEditable"
856
+ @update:value="
857
+ (v: unknown) =>
858
+ patchWidgetProps(selectedColumn!.index, {
859
+ textStyle: {
860
+ ...(selectedColumn!.column.widgetProps?.textStyle ?? {}),
861
+ fontFamily: asSelectString(v) || undefined,
862
+ },
863
+ })
864
+ "
865
+ />
866
+ </label>
867
+ <label class="block space-y-1">
868
+ <div>{{ t("panel.config.tableTextFontStyle") }}</div>
869
+ <Select
870
+ size="small"
871
+ class="w-full"
872
+ :value="selectedColumn.column.widgetProps?.textStyle?.fontStyle ?? 'normal'"
873
+ :disabled="!isEditable"
874
+ @update:value="
875
+ (v) =>
876
+ patchWidgetProps(selectedColumn!.index, {
877
+ textStyle: {
878
+ ...(selectedColumn!.column.widgetProps?.textStyle ?? {}),
879
+ fontStyle: asSelectString(v) as 'normal' | 'italic',
880
+ },
881
+ })
882
+ "
883
+ >
884
+ <Select.Option value="normal">{{ t("panel.config.tableTextStyleNormal") }}</Select.Option>
885
+ <Select.Option value="italic">{{ t("panel.config.tableTextStyleItalic") }}</Select.Option>
886
+ </Select>
887
+ </label>
888
+ <label class="block space-y-1">
889
+ <div>{{ t("panel.config.tableTextDecoration") }}</div>
890
+ <Select
891
+ size="small"
892
+ class="w-full"
893
+ :value="selectedColumn.column.widgetProps?.textStyle?.textDecoration ?? 'none'"
894
+ :disabled="!isEditable"
895
+ @update:value="
896
+ (v) =>
897
+ patchWidgetProps(selectedColumn!.index, {
898
+ textStyle: {
899
+ ...(selectedColumn!.column.widgetProps?.textStyle ?? {}),
900
+ textDecoration: asSelectString(v) as 'none' | 'underline' | 'line-through',
901
+ },
902
+ })
903
+ "
904
+ >
905
+ <Select.Option value="none">{{ t("panel.config.tableTextDecoNone") }}</Select.Option>
906
+ <Select.Option value="underline">{{ t("panel.config.tableTextDecoUnderline") }}</Select.Option>
907
+ <Select.Option value="line-through">{{ t("panel.config.tableTextDecoLineThrough") }}</Select.Option>
908
+ </Select>
909
+ </label>
910
+ <label class="block space-y-1">
911
+ <div class="inline-flex items-center gap-1">
912
+ {{ t("panel.config.tableTextOverflow") }}
913
+ <ConfigHintIcon :label="t('panel.config.tableTextOverflow')">
914
+ <div>{{ t("panel.config.tableTextOverflowHint") }}</div>
915
+ </ConfigHintIcon>
916
+ </div>
917
+ <Select
918
+ size="small"
919
+ class="w-full"
920
+ :value="selectedColumn.column.widgetProps?.textStyle?.overflow ?? 'ellipsis'"
921
+ :disabled="!isEditable"
922
+ @update:value="
923
+ (v: unknown) =>
924
+ patchWidgetProps(selectedColumn!.index, {
925
+ textStyle: {
926
+ ...(selectedColumn!.column.widgetProps?.textStyle ?? {}),
927
+ overflow: asSelectString(v) as 'ellipsis' | 'wrap',
928
+ },
929
+ })
930
+ "
931
+ >
932
+ <Select.Option value="ellipsis">{{ t("panel.config.tableTextOverflowEllipsis") }}</Select.Option>
933
+ <Select.Option value="wrap">{{ t("panel.config.tableTextOverflowWrap") }}</Select.Option>
934
+ </Select>
935
+ </label>
936
+ </div>
937
+ <label class="block space-y-1">
938
+ <div class="inline-flex items-center gap-1">
939
+ {{ t("panel.config.tableActionOnClick") }}
940
+ <ConfigHintIcon :label="t('panel.config.tableActionOnClick')">
941
+ <div>{{ t("panel.config.tableActionBlueprintHint") }}</div>
942
+ </ConfigHintIcon>
943
+ </div>
944
+ <Select
945
+ size="small"
946
+ class="w-full"
947
+ :value="blueprintSelectValue(selectedColumn.column.widgetProps?.actions?.onClickBlueprintNodeId)"
948
+ :disabled="!isEditable"
949
+ @update:value="(v) => onBlueprintSelect(selectedColumn!.index, 'onClickBlueprintNodeId', v)"
950
+ >
951
+ <Select.Option value="__none__">{{ t("panel.config.tableActionBlueprintNone") }}</Select.Option>
952
+ <Select.Option
953
+ v-for="opt in blueprintNodeOptions"
954
+ :key="opt.id"
955
+ :value="opt.id"
956
+ >
957
+ {{ opt.label }}
958
+ </Select.Option>
959
+ </Select>
960
+ </label>
961
+ </template>
962
+
963
+ <template v-else-if="selectedColumn.column.widget === 'image'">
964
+ <label class="block space-y-1">
965
+ <div class="flex items-center gap-1">
966
+ <span>{{ t("panel.config.tableImageUrlMode") }}</span>
967
+ <ConfigHintIcon :label="t('panel.config.tableImageUrlMode')">
968
+ <div>{{ t("panel.config.tableImageUrlModeHint") }}</div>
969
+ </ConfigHintIcon>
970
+ </div>
971
+ <Select
972
+ size="small"
973
+ class="w-full"
974
+ :value="selectedColumn.column.widgetProps?.imageUrlMode ?? 'field'"
975
+ :disabled="!isEditable"
976
+ @update:value="
977
+ (v) =>
978
+ patchWidgetProps(selectedColumn!.index, {
979
+ imageUrlMode: v as NonNullable<TableWidgetProps['imageUrlMode']>,
980
+ })
981
+ "
982
+ >
983
+ <Select.Option value="field">{{ t("panel.config.tableImageUrlModeField") }}</Select.Option>
984
+ <Select.Option value="static">{{ t("panel.config.tableImageUrlModeStatic") }}</Select.Option>
985
+ <Select.Option value="prefix">{{ t("panel.config.tableImageUrlModePrefix") }}</Select.Option>
986
+ <Select.Option value="template">{{ t("panel.config.tableImageUrlModeTemplate") }}</Select.Option>
987
+ </Select>
988
+ </label>
989
+
990
+ <label
991
+ v-if="
992
+ (selectedColumn.column.widgetProps?.imageUrlMode ?? 'field') === 'field' ||
993
+ (selectedColumn.column.widgetProps?.imageUrlMode ?? 'field') === 'prefix'
994
+ "
995
+ class="block space-y-1"
996
+ >
997
+ <div>{{ t("panel.config.tableImageUrlField") }}</div>
998
+ <Input
999
+ size="small"
1000
+ :value="selectedColumn.column.widgetProps?.imageUrlField ?? ''"
1001
+ :placeholder="selectedColumn.column.field"
1002
+ :disabled="!isEditable"
1003
+ @update:value="
1004
+ (v: unknown) => patchWidgetProps(selectedColumn!.index, { imageUrlField: asSelectString(v) || undefined })
1005
+ "
1006
+ />
1007
+ </label>
1008
+
1009
+ <div
1010
+ v-if="(selectedColumn.column.widgetProps?.imageUrlMode ?? 'field') === 'static'"
1011
+ class="space-y-1.5"
1012
+ >
1013
+ <label class="block space-y-1">
1014
+ <div class="flex items-center gap-1">
1015
+ <span>{{ t("panel.config.tableImageUrl") }}</span>
1016
+ <ConfigHintIcon :label="t('panel.config.tableImageUrl')">
1017
+ <div>{{ t("panel.config.tableImageUrlHint") }}</div>
1018
+ </ConfigHintIcon>
1019
+ </div>
1020
+ <Input
1021
+ size="small"
1022
+ :value="selectedColumn.column.widgetProps?.imageUrl ?? ''"
1023
+ placeholder="{scope?.avatar} / https://…"
1024
+ :disabled="!isEditable"
1025
+ @update:value="
1026
+ (v: unknown) => patchWidgetProps(selectedColumn!.index, { imageUrl: asSelectString(v) || undefined })
1027
+ "
1028
+ />
1029
+ </label>
1030
+ <div class="flex flex-wrap items-center gap-2">
1031
+ <label class="inline-flex cursor-pointer items-center">
1032
+ <span
1033
+ class="rounded border border-gray-300 bg-white px-2 py-0.5 text-[11px]"
1034
+ :class="!isEditable ? 'opacity-50' : ''"
1035
+ >
1036
+ {{ t("panel.config.tableImageUpload") }}
1037
+ </span>
1038
+ <input
1039
+ type="file"
1040
+ accept="image/*"
1041
+ class="hidden"
1042
+ :disabled="!isEditable"
1043
+ @change="onImageFileChange(selectedColumn!.index, $event)"
1044
+ />
1045
+ </label>
1046
+ <span v-if="imageUploadHint" class="text-[11px] text-gray-500">{{ imageUploadHint }}</span>
1047
+ </div>
1048
+ </div>
1049
+
1050
+ <div
1051
+ v-if="(selectedColumn.column.widgetProps?.imageUrlMode ?? 'field') === 'prefix'"
1052
+ class="grid grid-cols-2 gap-2"
1053
+ >
1054
+ <label class="block space-y-1">
1055
+ <div>{{ t("panel.config.tableImageUrlPrefix") }}</div>
1056
+ <Input
1057
+ size="small"
1058
+ :value="selectedColumn.column.widgetProps?.imageUrlPrefix ?? ''"
1059
+ placeholder="{scope?.cdn}/"
1060
+ :disabled="!isEditable"
1061
+ @update:value="
1062
+ (v: unknown) =>
1063
+ patchWidgetProps(selectedColumn!.index, { imageUrlPrefix: asSelectString(v) || undefined })
1064
+ "
1065
+ />
1066
+ </label>
1067
+ <label class="block space-y-1">
1068
+ <div>{{ t("panel.config.tableImageUrlSuffix") }}</div>
1069
+ <Input
1070
+ size="small"
1071
+ :value="selectedColumn.column.widgetProps?.imageUrlSuffix ?? ''"
1072
+ placeholder=".png"
1073
+ :disabled="!isEditable"
1074
+ @update:value="
1075
+ (v: unknown) =>
1076
+ patchWidgetProps(selectedColumn!.index, { imageUrlSuffix: asSelectString(v) || undefined })
1077
+ "
1078
+ />
1079
+ </label>
1080
+ </div>
1081
+
1082
+ <label
1083
+ v-if="(selectedColumn.column.widgetProps?.imageUrlMode ?? 'field') === 'template'"
1084
+ class="block space-y-1"
1085
+ >
1086
+ <div>{{ t("panel.config.tableImageUrlTemplate") }}</div>
1087
+ <Input
1088
+ size="small"
1089
+ :value="selectedColumn.column.widgetProps?.imageUrlTemplate ?? ''"
1090
+ placeholder="https://cdn/{row.id}.png"
1091
+ :disabled="!isEditable"
1092
+ @update:value="
1093
+ (v: unknown) =>
1094
+ patchWidgetProps(selectedColumn!.index, { imageUrlTemplate: asSelectString(v) || undefined })
1095
+ "
1096
+ />
1097
+ </label>
1098
+
1099
+ <div class="grid grid-cols-3 gap-2">
1100
+ <label class="block space-y-1">
1101
+ <div>{{ t("panel.config.tableImageWidth") }}</div>
1102
+ <InputNumber
1103
+ size="small"
1104
+ class="w-full"
1105
+ :min="12"
1106
+ :max="240"
1107
+ :value="selectedColumn.column.widgetProps?.imageWidth ?? 28"
1108
+ :disabled="!isEditable"
1109
+ @update:value="
1110
+ (v) =>
1111
+ patchWidgetProps(selectedColumn!.index, {
1112
+ imageWidth: typeof v === 'number' ? v : 28,
1113
+ })
1114
+ "
1115
+ />
1116
+ </label>
1117
+ <label class="block space-y-1">
1118
+ <div>{{ t("panel.config.tableImageHeight") }}</div>
1119
+ <InputNumber
1120
+ size="small"
1121
+ class="w-full"
1122
+ :min="12"
1123
+ :max="240"
1124
+ :value="selectedColumn.column.widgetProps?.imageHeight ?? 28"
1125
+ :disabled="!isEditable"
1126
+ @update:value="
1127
+ (v) =>
1128
+ patchWidgetProps(selectedColumn!.index, {
1129
+ imageHeight: typeof v === 'number' ? v : 28,
1130
+ })
1131
+ "
1132
+ />
1133
+ </label>
1134
+ <label class="block space-y-1">
1135
+ <div>{{ t("panel.config.tableImageObjectFit") }}</div>
1136
+ <Select
1137
+ size="small"
1138
+ class="w-full"
1139
+ :value="selectedColumn.column.widgetProps?.imageObjectFit ?? 'cover'"
1140
+ :disabled="!isEditable"
1141
+ @update:value="
1142
+ (v) =>
1143
+ patchWidgetProps(selectedColumn!.index, {
1144
+ imageObjectFit: v as NonNullable<TableWidgetProps['imageObjectFit']>,
1145
+ })
1146
+ "
1147
+ >
1148
+ <Select.Option value="cover">{{ t("panel.config.tableImageFitCover") }}</Select.Option>
1149
+ <Select.Option value="contain">{{ t("panel.config.tableImageFitContain") }}</Select.Option>
1150
+ <Select.Option value="fill">{{ t("panel.config.tableImageFitFill") }}</Select.Option>
1151
+ </Select>
1152
+ </label>
1153
+ </div>
1154
+ </template>
1155
+
1156
+ <template v-else-if="selectedColumn.column.widget === 'link'">
1157
+ <label class="block space-y-1">
1158
+ <div>{{ t("panel.config.tableLinkHrefField") }}</div>
1159
+ <Input
1160
+ size="small"
1161
+ :value="selectedColumn.column.widgetProps?.hrefField ?? ''"
1162
+ :placeholder="selectedColumn.column.field"
1163
+ :disabled="!isEditable"
1164
+ @update:value="
1165
+ (v: unknown) => patchWidgetProps(selectedColumn!.index, { hrefField: asSelectString(v) || undefined })
1166
+ "
1167
+ />
1168
+ </label>
1169
+ <label class="block space-y-1">
1170
+ <div>{{ t("panel.config.tableLinkHrefTemplate") }}</div>
1171
+ <Input
1172
+ size="small"
1173
+ :value="selectedColumn.column.widgetProps?.hrefTemplate ?? ''"
1174
+ placeholder="/detail/{row.id}"
1175
+ :disabled="!isEditable"
1176
+ @update:value="
1177
+ (v: unknown) =>
1178
+ patchWidgetProps(selectedColumn!.index, { hrefTemplate: asSelectString(v) || undefined })
1179
+ "
1180
+ />
1181
+ </label>
1182
+ <div class="space-y-1.5">
1183
+ <div class="flex items-center justify-between gap-2">
1184
+ <span class="inline-flex items-center gap-1 text-[11px] text-gray-600">
1185
+ {{ t("panel.config.tableHrefRules") }}
1186
+ <ConfigHintIcon :label="t('panel.config.tableHrefRules')">
1187
+ <div>{{ t("panel.config.tableHrefRulesHint") }}</div>
1188
+ </ConfigHintIcon>
1189
+ </span>
1190
+ <Button
1191
+ size="small"
1192
+ type="link"
1193
+ :disabled="!isEditable"
1194
+ @click="
1195
+ patchWidgetProps(selectedColumn!.index, {
1196
+ hrefRules: [
1197
+ ...(selectedColumn!.column.widgetProps?.hrefRules ?? []),
1198
+ { when: createDefaultCondition(), href: '' },
1199
+ ],
1200
+ })
1201
+ "
1202
+ >
1203
+ {{ t("panel.config.tableAddHrefRule") }}
1204
+ </Button>
1205
+ </div>
1206
+ <div
1207
+ v-for="(rule, ruleIndex) in selectedColumn.column.widgetProps?.hrefRules ?? []"
1208
+ :key="ruleIndex"
1209
+ class="space-y-1.5 rounded-md border border-gray-200 bg-gray-50/70 p-2"
1210
+ >
1211
+ <div class="flex items-center justify-between">
1212
+ <span class="text-[11px] text-gray-500">
1213
+ {{ t("panel.config.tableHrefRuleItem", { n: ruleIndex + 1 }) }}
1214
+ </span>
1215
+ <Button
1216
+ size="small"
1217
+ danger
1218
+ type="text"
1219
+ :disabled="!isEditable"
1220
+ @click="
1221
+ (() => {
1222
+ const next = (selectedColumn!.column.widgetProps?.hrefRules ?? []).filter(
1223
+ (_, i) => i !== ruleIndex
1224
+ );
1225
+ patchWidgetProps(selectedColumn!.index, {
1226
+ hrefRules: next.length ? next : undefined,
1227
+ });
1228
+ })()
1229
+ "
1230
+ >
1231
+ ×
1232
+ </Button>
1233
+ </div>
1234
+ <ConditionEditor
1235
+ :model-value="rule.when"
1236
+ :show-field="false"
1237
+ :disabled="!isEditable"
1238
+ @update:model-value="(when) => patchHrefRuleWhen(ruleIndex, when)"
1239
+ />
1240
+ <Input
1241
+ size="small"
1242
+ :value="rule.href"
1243
+ :placeholder="t('panel.config.tableHrefRuleHref')"
1244
+ :disabled="!isEditable"
1245
+ @update:value="
1246
+ (v: unknown) => {
1247
+ const next = [...(selectedColumn!.column.widgetProps?.hrefRules ?? [])];
1248
+ next[ruleIndex] = { ...rule, href: asSelectString(v) };
1249
+ patchWidgetProps(selectedColumn!.index, { hrefRules: next });
1250
+ }
1251
+ "
1252
+ />
1253
+ </div>
1254
+ </div>
1255
+ <label class="flex items-center justify-between gap-2">
1256
+ <span>{{ t("panel.config.tableLinkOpenNewTab") }}</span>
1257
+ <Switch
1258
+ size="small"
1259
+ :checked="selectedColumn.column.widgetProps?.openInNewTab !== false"
1260
+ :disabled="!isEditable"
1261
+ @update:checked="(v) => patchWidgetProps(selectedColumn!.index, { openInNewTab: asSwitchChecked(v) })"
1262
+ />
1263
+ </label>
1264
+ <label class="block space-y-1">
1265
+ <div class="inline-flex items-center gap-1">
1266
+ {{ t("panel.config.tableActionOnClick") }}
1267
+ <ConfigHintIcon :label="t('panel.config.tableActionOnClick')">
1268
+ <div>{{ t("panel.config.tableActionBlueprintHint") }}</div>
1269
+ </ConfigHintIcon>
1270
+ </div>
1271
+ <Select
1272
+ size="small"
1273
+ class="w-full"
1274
+ :value="blueprintSelectValue(selectedColumn.column.widgetProps?.actions?.onClickBlueprintNodeId)"
1275
+ :disabled="!isEditable"
1276
+ @update:value="(v) => onBlueprintSelect(selectedColumn!.index, 'onClickBlueprintNodeId', v)"
1277
+ >
1278
+ <Select.Option value="__none__">{{ t("panel.config.tableActionBlueprintNone") }}</Select.Option>
1279
+ <Select.Option
1280
+ v-for="opt in blueprintNodeOptions"
1281
+ :key="opt.id"
1282
+ :value="opt.id"
1283
+ >
1284
+ {{ opt.label }}
1285
+ </Select.Option>
1286
+ </Select>
1287
+ </label>
1288
+ </template>
1289
+
1290
+ <template v-else-if="selectedColumn.column.widget === 'progress'">
1291
+ <label class="block space-y-1">
1292
+ <div class="inline-flex items-center gap-1">
1293
+ {{ t("panel.config.tableProgressMode") }}
1294
+ <ConfigHintIcon :label="t('panel.config.tableProgressMode')">
1295
+ <div>{{ t("panel.config.tableProgressModeHint") }}</div>
1296
+ </ConfigHintIcon>
1297
+ </div>
1298
+ <Select
1299
+ size="small"
1300
+ class="w-full"
1301
+ :value="selectedColumn.column.widgetProps?.progressMode ?? 'field'"
1302
+ :disabled="!isEditable"
1303
+ @update:value="
1304
+ (v) =>
1305
+ patchWidgetProps(selectedColumn!.index, {
1306
+ progressMode: asSelectString(v) as NonNullable<TableWidgetProps['progressMode']>,
1307
+ })
1308
+ "
1309
+ >
1310
+ <Select.Option value="field">{{ t("panel.config.tableProgressModeField") }}</Select.Option>
1311
+ <Select.Option value="static">{{ t("panel.config.tableProgressModeStatic") }}</Select.Option>
1312
+ <Select.Option value="rules">{{ t("panel.config.tableProgressModeRules") }}</Select.Option>
1313
+ </Select>
1314
+ </label>
1315
+ <label
1316
+ v-if="(selectedColumn.column.widgetProps?.progressMode ?? 'field') === 'field'"
1317
+ class="block space-y-1"
1318
+ >
1319
+ <div>{{ t("panel.config.tableProgressField") }}</div>
1320
+ <Input
1321
+ size="small"
1322
+ :value="selectedColumn.column.widgetProps?.progressField ?? ''"
1323
+ :placeholder="selectedColumn.column.field"
1324
+ :disabled="!isEditable"
1325
+ @update:value="
1326
+ (v: unknown) =>
1327
+ patchWidgetProps(selectedColumn!.index, { progressField: asSelectString(v) || undefined })
1328
+ "
1329
+ />
1330
+ </label>
1331
+ <label
1332
+ v-else-if="selectedColumn.column.widgetProps?.progressMode === 'static'"
1333
+ class="block space-y-1"
1334
+ >
1335
+ <div>{{ t("panel.config.tableProgressStatic") }}</div>
1336
+ <Input
1337
+ size="small"
1338
+ :value="String(selectedColumn.column.widgetProps?.progressStatic ?? '')"
1339
+ placeholder="80 或 {scope?.pct}"
1340
+ :disabled="!isEditable"
1341
+ @update:value="
1342
+ (v: unknown) =>
1343
+ patchWidgetProps(selectedColumn!.index, { progressStatic: asSelectString(v) || undefined })
1344
+ "
1345
+ />
1346
+ </label>
1347
+ <div
1348
+ v-else-if="selectedColumn.column.widgetProps?.progressMode === 'rules'"
1349
+ class="space-y-1.5"
1350
+ >
1351
+ <div class="flex items-center justify-between gap-2">
1352
+ <span class="text-[11px] text-gray-600">{{ t("panel.config.tableProgressRules") }}</span>
1353
+ <Button
1354
+ size="small"
1355
+ type="link"
1356
+ :disabled="!isEditable"
1357
+ @click="
1358
+ patchWidgetProps(selectedColumn!.index, {
1359
+ progressRules: [
1360
+ ...(selectedColumn!.column.widgetProps?.progressRules ?? []),
1361
+ { when: createDefaultCondition(), value: 0 },
1362
+ ],
1363
+ })
1364
+ "
1365
+ >
1366
+ {{ t("panel.config.tableAddProgressRule") }}
1367
+ </Button>
1368
+ </div>
1369
+ <div
1370
+ v-for="(rule, ruleIndex) in selectedColumn.column.widgetProps?.progressRules ?? []"
1371
+ :key="ruleIndex"
1372
+ class="space-y-1.5 rounded-md border border-gray-200 bg-gray-50/70 p-2"
1373
+ >
1374
+ <div class="flex items-center justify-between">
1375
+ <span class="text-[11px] text-gray-500">
1376
+ {{ t("panel.config.tableProgressRuleItem", { n: ruleIndex + 1 }) }}
1377
+ </span>
1378
+ <Button
1379
+ size="small"
1380
+ danger
1381
+ type="text"
1382
+ :disabled="!isEditable"
1383
+ @click="
1384
+ (() => {
1385
+ const next = (selectedColumn!.column.widgetProps?.progressRules ?? []).filter(
1386
+ (_, i) => i !== ruleIndex
1387
+ );
1388
+ patchWidgetProps(selectedColumn!.index, {
1389
+ progressRules: next.length ? next : undefined,
1390
+ });
1391
+ })()
1392
+ "
1393
+ >
1394
+ ×
1395
+ </Button>
1396
+ </div>
1397
+ <ConditionEditor
1398
+ :model-value="rule.when"
1399
+ :show-field="false"
1400
+ :disabled="!isEditable"
1401
+ @update:model-value="(when) => patchProgressRuleWhen(ruleIndex, when)"
1402
+ />
1403
+ <Input
1404
+ size="small"
1405
+ :value="String(rule.value ?? '')"
1406
+ :placeholder="t('panel.config.tableProgressRuleValue')"
1407
+ :disabled="!isEditable"
1408
+ @update:value="
1409
+ (v: unknown) => {
1410
+ const next = [...(selectedColumn!.column.widgetProps?.progressRules ?? [])];
1411
+ const s = asSelectString(v);
1412
+ const num = Number(s);
1413
+ next[ruleIndex] = {
1414
+ ...rule,
1415
+ value: s !== '' && !Number.isNaN(num) ? num : s,
1416
+ };
1417
+ patchWidgetProps(selectedColumn!.index, { progressRules: next });
1418
+ }
1419
+ "
1420
+ />
1421
+ </div>
1422
+ </div>
1423
+ <label class="block space-y-1">
1424
+ <div class="inline-flex items-center gap-1">
1425
+ {{ t("panel.config.tableProgressDisplay") }}
1426
+ <ConfigHintIcon :label="t('panel.config.tableProgressDisplay')">
1427
+ <div>{{ t("panel.config.tableProgressDisplayHint") }}</div>
1428
+ </ConfigHintIcon>
1429
+ </div>
1430
+ <Select
1431
+ size="small"
1432
+ class="w-full"
1433
+ :value="
1434
+ selectedColumn.column.widgetProps?.progressDisplay ??
1435
+ (selectedColumn.column.widgetProps?.showLabel === false ? 'bar' : 'barLabel')
1436
+ "
1437
+ :disabled="!isEditable"
1438
+ @update:value="
1439
+ (v) =>
1440
+ patchWidgetProps(selectedColumn!.index, {
1441
+ progressDisplay: asSelectString(v) as NonNullable<TableWidgetProps['progressDisplay']>,
1442
+ showLabel: undefined,
1443
+ })
1444
+ "
1445
+ >
1446
+ <Select.Option value="barLabel">{{ t("panel.config.tableProgressDisplayBarLabel") }}</Select.Option>
1447
+ <Select.Option value="bar">{{ t("panel.config.tableProgressDisplayBar") }}</Select.Option>
1448
+ <Select.Option value="label">{{ t("panel.config.tableProgressDisplayLabel") }}</Select.Option>
1449
+ <Select.Option value="circle">{{ t("panel.config.tableProgressDisplayCircle") }}</Select.Option>
1450
+ </Select>
1451
+ </label>
1452
+ <div class="grid grid-cols-2 gap-2">
1453
+ <label class="block space-y-1">
1454
+ <div>{{ t("panel.config.tableProgressMax") }}</div>
1455
+ <InputNumber
1456
+ size="small"
1457
+ class="w-full"
1458
+ :min="1"
1459
+ :value="selectedColumn.column.widgetProps?.max ?? 100"
1460
+ :disabled="!isEditable"
1461
+ @update:value="
1462
+ (v) =>
1463
+ patchWidgetProps(selectedColumn!.index, {
1464
+ max: typeof v === 'number' ? v : 100,
1465
+ })
1466
+ "
1467
+ />
1468
+ </label>
1469
+ <label
1470
+ v-if="
1471
+ (selectedColumn.column.widgetProps?.progressDisplay ?? 'barLabel') === 'circle'
1472
+ "
1473
+ class="block space-y-1"
1474
+ >
1475
+ <div>{{ t("panel.config.tableProgressSize") }}</div>
1476
+ <InputNumber
1477
+ size="small"
1478
+ class="w-full"
1479
+ :min="16"
1480
+ :max="64"
1481
+ :value="selectedColumn.column.widgetProps?.progressSize ?? 28"
1482
+ :disabled="!isEditable"
1483
+ @update:value="
1484
+ (v) =>
1485
+ patchWidgetProps(selectedColumn!.index, {
1486
+ progressSize: typeof v === 'number' ? v : 28,
1487
+ })
1488
+ "
1489
+ />
1490
+ </label>
1491
+ </div>
1492
+ </template>
1493
+
1494
+ <template
1495
+ v-else-if="
1496
+ selectedColumn.column.widget === 'tag' || selectedColumn.column.widget === 'badge'
1497
+ "
1498
+ >
1499
+ <ConfigColorField
1500
+ :label="t('panel.config.tableTagColor')"
1501
+ :value="selectedColumn.column.widgetProps?.color ?? '#3b82f6'"
1502
+ :disabled="!isEditable"
1503
+ @update:value="
1504
+ (v) => patchWidgetProps(selectedColumn!.index, { color: v || undefined })
1505
+ "
1506
+ />
1507
+ <div class="space-y-1.5">
1508
+ <div class="flex items-center justify-between gap-2">
1509
+ <span class="inline-flex items-center gap-1 text-[11px] text-gray-600">
1510
+ {{ t("panel.config.tableColorMap") }}
1511
+ <ConfigHintIcon :label="t('panel.config.tableColorMap')">
1512
+ <div>{{ t("panel.config.tableColorMapHint") }}</div>
1513
+ </ConfigHintIcon>
1514
+ </span>
1515
+ <Button
1516
+ size="small"
1517
+ type="link"
1518
+ :disabled="!isEditable"
1519
+ @click="
1520
+ patchWidgetProps(selectedColumn!.index, {
1521
+ colorMapEntries: [
1522
+ ...(selectedColumn!.column.widgetProps?.colorMapEntries ?? []),
1523
+ { value: '', color: '#3b82f6' },
1524
+ ],
1525
+ })
1526
+ "
1527
+ >
1528
+ {{ t("panel.config.tableAddColorMap") }}
1529
+ </Button>
1530
+ </div>
1531
+ <div
1532
+ v-for="(entry, entryIndex) in selectedColumn.column.widgetProps?.colorMapEntries ?? []"
1533
+ :key="entryIndex"
1534
+ class="grid grid-cols-[1fr_1fr_auto] items-end gap-1.5 rounded-md border border-gray-200 bg-gray-50/70 p-2"
1535
+ >
1536
+ <label class="block space-y-1">
1537
+ <div class="text-[11px]">{{ t("panel.config.tableColorMapValue") }}</div>
1538
+ <Input
1539
+ size="small"
1540
+ :value="entry.value"
1541
+ placeholder="ok"
1542
+ :disabled="!isEditable"
1543
+ @update:value="
1544
+ (v: unknown) => {
1545
+ const next = [
1546
+ ...(selectedColumn!.column.widgetProps?.colorMapEntries ?? []),
1547
+ ];
1548
+ next[entryIndex] = { ...entry, value: asSelectString(v) };
1549
+ patchWidgetProps(selectedColumn!.index, { colorMapEntries: next });
1550
+ }
1551
+ "
1552
+ />
1553
+ </label>
1554
+ <ConfigColorField
1555
+ :label="t('panel.config.tableColorMapColor')"
1556
+ :value="entry.color"
1557
+ :disabled="!isEditable"
1558
+ @update:value="
1559
+ (v) => {
1560
+ const next = [...(selectedColumn!.column.widgetProps?.colorMapEntries ?? [])];
1561
+ next[entryIndex] = { ...entry, color: v || '#3b82f6' };
1562
+ patchWidgetProps(selectedColumn!.index, { colorMapEntries: next });
1563
+ }
1564
+ "
1565
+ />
1566
+ <Button
1567
+ size="small"
1568
+ danger
1569
+ type="text"
1570
+ :disabled="!isEditable"
1571
+ @click="
1572
+ (() => {
1573
+ const next = (selectedColumn!.column.widgetProps?.colorMapEntries ?? []).filter(
1574
+ (_, i) => i !== entryIndex
1575
+ );
1576
+ patchWidgetProps(selectedColumn!.index, {
1577
+ colorMapEntries: next.length ? next : undefined,
1578
+ });
1579
+ })()
1580
+ "
1581
+ >
1582
+ ×
1583
+ </Button>
1584
+ </div>
1585
+ </div>
1586
+ </template>
1587
+
1588
+ <template v-else-if="selectedColumn.column.widget === 'boolean'">
1589
+ <div class="grid grid-cols-2 gap-2">
1590
+ <label class="block space-y-1">
1591
+ <div>{{ t("panel.config.tableBooleanTrueLabel") }}</div>
1592
+ <Input
1593
+ size="small"
1594
+ :value="selectedColumn.column.widgetProps?.trueLabel ?? ''"
1595
+ :disabled="!isEditable"
1596
+ @update:value="
1597
+ (v: unknown) =>
1598
+ patchWidgetProps(selectedColumn!.index, { trueLabel: asSelectString(v) || undefined })
1599
+ "
1600
+ />
1601
+ </label>
1602
+ <label class="block space-y-1">
1603
+ <div>{{ t("panel.config.tableBooleanFalseLabel") }}</div>
1604
+ <Input
1605
+ size="small"
1606
+ :value="selectedColumn.column.widgetProps?.falseLabel ?? ''"
1607
+ :disabled="!isEditable"
1608
+ @update:value="
1609
+ (v: unknown) =>
1610
+ patchWidgetProps(selectedColumn!.index, { falseLabel: asSelectString(v) || undefined })
1611
+ "
1612
+ />
1613
+ </label>
1614
+ </div>
1615
+ <label class="block space-y-1">
1616
+ <div class="inline-flex items-center gap-1">
1617
+ {{ t("panel.config.tableActionOnTrue") }}
1618
+ <ConfigHintIcon :label="t('panel.config.tableActionOnTrue')">
1619
+ <div>{{ t("panel.config.tableActionBlueprintHint") }}</div>
1620
+ </ConfigHintIcon>
1621
+ </div>
1622
+ <Select
1623
+ size="small"
1624
+ class="w-full"
1625
+ :value="blueprintSelectValue(selectedColumn.column.widgetProps?.actions?.onTrueBlueprintNodeId)"
1626
+ :disabled="!isEditable"
1627
+ @update:value="(v) => onBlueprintSelect(selectedColumn!.index, 'onTrueBlueprintNodeId', v)"
1628
+ >
1629
+ <Select.Option value="__none__">{{ t("panel.config.tableActionBlueprintNone") }}</Select.Option>
1630
+ <Select.Option
1631
+ v-for="opt in blueprintNodeOptions"
1632
+ :key="opt.id"
1633
+ :value="opt.id"
1634
+ >
1635
+ {{ opt.label }}
1636
+ </Select.Option>
1637
+ </Select>
1638
+ </label>
1639
+ <label class="block space-y-1">
1640
+ <div class="inline-flex items-center gap-1">
1641
+ {{ t("panel.config.tableActionOnFalse") }}
1642
+ <ConfigHintIcon :label="t('panel.config.tableActionOnFalse')">
1643
+ <div>{{ t("panel.config.tableActionBlueprintHint") }}</div>
1644
+ </ConfigHintIcon>
1645
+ </div>
1646
+ <Select
1647
+ size="small"
1648
+ class="w-full"
1649
+ :value="blueprintSelectValue(selectedColumn.column.widgetProps?.actions?.onFalseBlueprintNodeId)"
1650
+ :disabled="!isEditable"
1651
+ @update:value="(v) => onBlueprintSelect(selectedColumn!.index, 'onFalseBlueprintNodeId', v)"
1652
+ >
1653
+ <Select.Option value="__none__">{{ t("panel.config.tableActionBlueprintNone") }}</Select.Option>
1654
+ <Select.Option
1655
+ v-for="opt in blueprintNodeOptions"
1656
+ :key="opt.id"
1657
+ :value="opt.id"
1658
+ >
1659
+ {{ opt.label }}
1660
+ </Select.Option>
1661
+ </Select>
1662
+ </label>
1663
+ </template>
1664
+ </div>
1665
+
1666
+ <div class="space-y-1.5 border-t border-gray-100 pt-2">
1667
+ <div class="flex items-center justify-between">
1668
+ <span class="inline-flex items-center gap-1 font-medium text-gray-600">
1669
+ {{ t("panel.config.tableValueMap") }}
1670
+ <ConfigHintIcon :label="t('panel.config.tableValueMap')">
1671
+ <div>
1672
+ {{
1673
+ selectedColumn.column.widget === "boolean"
1674
+ ? t("panel.config.tableValueMapBooleanHint")
1675
+ : t("panel.config.tableValueMapHint")
1676
+ }}
1677
+ </div>
1678
+ </ConfigHintIcon>
1679
+ </span>
1680
+ <Button size="small" type="link" :disabled="!isEditable" @click="addValueMapRule(selectedColumn.index)">
1681
+ {{ t("panel.config.tableAddValueMap") }}
1682
+ </Button>
1683
+ </div>
1684
+ <div
1685
+ v-if="selectedColumn.column.widget === 'boolean' && booleanValueMapConflicts.length"
1686
+ class="rounded-md border border-amber-300 bg-amber-50 px-2 py-1.5 text-[11px] text-amber-800"
1687
+ >
1688
+ <div v-for="conflict in booleanValueMapConflicts" :key="conflict.key">
1689
+ {{
1690
+ t("panel.config.tableValueMapBooleanConflict", {
1691
+ indexes: conflict.ruleIndexes.join(", "),
1692
+ })
1693
+ }}
1694
+ </div>
1695
+ </div>
1696
+ <div
1697
+ v-for="(rule, ruleIndex) in selectedColumn.column.valueMap ?? []"
1698
+ :key="ruleIndex"
1699
+ class="space-y-2 rounded-md border border-gray-200 bg-gray-50/70 p-2"
1700
+ >
1701
+ <div class="flex items-center justify-between gap-2">
1702
+ <div class="text-[11px] font-medium text-gray-500">
1703
+ {{ t("panel.config.tableMapItem", { n: ruleIndex + 1 }) }}
1704
+ </div>
1705
+ <Button
1706
+ size="small"
1707
+ danger
1708
+ type="text"
1709
+ :disabled="!isEditable"
1710
+ @click="removeValueMapRule(selectedColumn!.index, ruleIndex)"
1711
+ >
1712
+ ×
1713
+ </Button>
1714
+ </div>
1715
+ <ConditionEditor
1716
+ :model-value="rule.when"
1717
+ :show-field="false"
1718
+ :disabled="!isEditable"
1719
+ @update:model-value="(when) => patchValueMapRule(selectedColumn!.index, ruleIndex, { when })"
1720
+ />
1721
+ <Select
1722
+ v-if="selectedColumn.column.widget === 'boolean'"
1723
+ size="small"
1724
+ class="w-full"
1725
+ :value="booleanMapSelectValue(rule.value)"
1726
+ :disabled="!isEditable"
1727
+ @update:value="
1728
+ (v) =>
1729
+ patchValueMapRule(selectedColumn!.index, ruleIndex, {
1730
+ value: String(v) === 'true',
1731
+ })
1732
+ "
1733
+ >
1734
+ <Select.Option value="true">{{ t("panel.config.tableBooleanYes") }}</Select.Option>
1735
+ <Select.Option value="false">{{ t("panel.config.tableBooleanNo") }}</Select.Option>
1736
+ </Select>
1737
+ <Input
1738
+ v-else
1739
+ size="small"
1740
+ :value="String(rule.value ?? '')"
1741
+ placeholder="{current}"
1742
+ :disabled="!isEditable"
1743
+ @update:value="(v: unknown) => patchValueMapRule(selectedColumn!.index, ruleIndex, { value: asSelectString(v) })"
1744
+ />
1745
+ </div>
1746
+ <label class="block space-y-1">
1747
+ <div class="flex items-center gap-1">
1748
+ <span>{{ t("panel.config.tableDisplayTemplate") }}</span>
1749
+ <ConfigHintIcon :label="t('panel.config.tableDisplayTemplate')">
1750
+ <div>{{ t("panel.config.tableDisplayTemplateHint") }}</div>
1751
+ </ConfigHintIcon>
1752
+ </div>
1753
+ <Input
1754
+ size="small"
1755
+ :value="selectedColumn.column.displayTemplate ?? ''"
1756
+ :disabled="!isEditable"
1757
+ placeholder="{current}分"
1758
+ @update:value="
1759
+ (v: unknown) => {
1760
+ const s = asSelectString(v).trim();
1761
+ patchColumn(selectedColumn!.index, {
1762
+ displayTemplate: s ? s : undefined,
1763
+ });
1764
+ }
1765
+ "
1766
+ />
1767
+ </label>
1768
+ </div>
1769
+
1770
+ <div class="space-y-1.5 border-t border-gray-100 pt-2">
1771
+ <div class="flex items-center justify-between">
1772
+ <span class="inline-flex items-center gap-1 font-medium text-gray-600">
1773
+ {{ t("panel.config.tableCellStyleRules") }}
1774
+ <ConfigHintIcon :label="t('panel.config.tableCellStyleRules')">
1775
+ <div>{{ t("panel.config.tableCellStyleRulesHint") }}</div>
1776
+ </ConfigHintIcon>
1777
+ </span>
1778
+ <Button size="small" type="link" :disabled="!isEditable" @click="addCellStyleRule(selectedColumn.index)">
1779
+ {{ t("panel.config.tableAddCellStyleRule") }}
1780
+ </Button>
1781
+ </div>
1782
+ <div
1783
+ v-for="(rule, ruleIndex) in selectedColumn.column.cellStyleRules ?? []"
1784
+ :key="ruleIndex"
1785
+ class="space-y-2 rounded-md border border-gray-200 bg-gray-50/70 p-2"
1786
+ >
1787
+ <div class="flex items-center justify-between gap-2">
1788
+ <div class="text-[11px] font-medium text-gray-500">
1789
+ {{ t("panel.config.tableStyleRuleItem", { n: ruleIndex + 1 }) }}
1790
+ </div>
1791
+ <Button
1792
+ size="small"
1793
+ danger
1794
+ type="text"
1795
+ :disabled="!isEditable"
1796
+ @click="removeCellStyleRule(selectedColumn!.index, ruleIndex)"
1797
+ >
1798
+ ×
1799
+ </Button>
1800
+ </div>
1801
+ <ConditionEditor
1802
+ :model-value="rule.when"
1803
+ :show-field="true"
1804
+ :disabled="!isEditable"
1805
+ @update:model-value="(when) => patchCellStyleRule(selectedColumn!.index, ruleIndex, { when })"
1806
+ />
1807
+ <ConfigColorField
1808
+ :label="t('panel.config.tableBgColor')"
1809
+ :value="(rule.style.backgroundColor as string) ?? '#fef3c7'"
1810
+ :disabled="!isEditable"
1811
+ @update:value="(v) => patchCellStyleRule(selectedColumn!.index, ruleIndex, { backgroundColor: v || '#fef3c7' })"
1812
+ />
1813
+ </div>
1814
+ </div>
1815
+ </div>
1816
+ </ConfigFieldGroup>
1817
+
1818
+ <ConfigFieldGroup :title="t('panel.config.groupTableAppearance')">
1819
+ <template #hint>{{ t("panel.config.groupTableAppearanceHint") }}</template>
1820
+ <label class="flex items-center justify-between gap-2">
1821
+ <span>{{ t("panel.config.tableShowHeader") }}</span>
1822
+ <Switch
1823
+ size="small"
1824
+ :checked="table.showHeader !== false"
1825
+ :disabled="!isEditable"
1826
+ @update:checked="(v) => updateTable({ showHeader: asSwitchChecked(v) })"
1827
+ />
1828
+ </label>
1829
+ <label class="flex items-center justify-between gap-2">
1830
+ <span class="inline-flex items-center gap-1">
1831
+ {{ t("panel.config.tableStripe") }}
1832
+ <ConfigHintIcon :label="t('panel.config.tableStripe')">
1833
+ <div>{{ t("panel.config.tableStripeHint") }}</div>
1834
+ </ConfigHintIcon>
1835
+ </span>
1836
+ <Switch
1837
+ size="small"
1838
+ :checked="Boolean(table.stripe ?? table.tableStyle?.stripe)"
1839
+ :disabled="!isEditable"
1840
+ @update:checked="(v) => updateTable({ stripe: asSwitchChecked(v) })"
1841
+ />
1842
+ </label>
1843
+ <label class="block space-y-1.5">
1844
+ <div class="flex items-center gap-1">
1845
+ <span>{{ t("panel.config.tableRowHeight") }}</span>
1846
+ <ConfigHintIcon :label="t('panel.config.tableRowHeight')">
1847
+ <div>{{ t("panel.config.tableRowHeightHint") }}</div>
1848
+ </ConfigHintIcon>
1849
+ </div>
1850
+ <InputNumber
1851
+ size="small"
1852
+ class="w-full"
1853
+ :min="24"
1854
+ :max="120"
1855
+ :value="table.rowHeight ?? 36"
1856
+ :disabled="!isEditable"
1857
+ @update:value="(v) => updateTable({ rowHeight: typeof v === 'number' ? v : 36 })"
1858
+ />
1859
+ </label>
1860
+ <label class="block space-y-1.5">
1861
+ <div>{{ t("panel.config.tableEmpty") }}</div>
1862
+ <Input
1863
+ size="small"
1864
+ :value="table.emptyText ?? ''"
1865
+ :disabled="!isEditable"
1866
+ @update:value="(v: unknown) => updateTable({ emptyText: asSelectString(v) })"
1867
+ />
1868
+ </label>
1869
+ <div class="grid grid-cols-2 gap-2">
1870
+ <ConfigColorField
1871
+ :label="t('panel.config.tableHeaderBg')"
1872
+ :value="(table.headerStyle?.backgroundColor as string) ?? 'rgba(0,0,0,0.04)'"
1873
+ :disabled="!isEditable"
1874
+ @update:value="(v) => updateHeaderStyle({ backgroundColor: v || 'rgba(0,0,0,0.04)' })"
1875
+ />
1876
+ <label class="block space-y-1.5">
1877
+ <div>{{ t("panel.config.tableHeaderFontSize") }}</div>
1878
+ <InputNumber
1879
+ size="small"
1880
+ class="w-full"
1881
+ :min="10"
1882
+ :max="24"
1883
+ :value="table.headerStyle?.fontSize ?? 12"
1884
+ :disabled="!isEditable"
1885
+ @update:value="(v) => updateHeaderStyle({ fontSize: typeof v === 'number' ? v : 12 })"
1886
+ />
1887
+ </label>
1888
+ <ConfigColorField
1889
+ :label="t('panel.config.tableStyleColor')"
1890
+ :value="(table.tableStyle?.color as string) ?? '#111827'"
1891
+ :disabled="!isEditable"
1892
+ @update:value="(v) => updateTableStyle({ color: v || '#111827' })"
1893
+ />
1894
+ <label class="block space-y-1.5">
1895
+ <div>{{ t("panel.config.tableStyleFontSize") }}</div>
1896
+ <InputNumber
1897
+ size="small"
1898
+ class="w-full"
1899
+ :min="10"
1900
+ :max="24"
1901
+ :value="table.tableStyle?.fontSize ?? 12"
1902
+ :disabled="!isEditable"
1903
+ @update:value="(v) => updateTableStyle({ fontSize: typeof v === 'number' ? v : 12 })"
1904
+ />
1905
+ </label>
1906
+ </div>
1907
+ </ConfigFieldGroup>
1908
+
1909
+ <ConfigFieldGroup :title="t('panel.config.tableRowStyleRules')">
1910
+ <template #hint>{{ t("panel.config.tableRowStyleRulesHint") }}</template>
1911
+ <div class="flex justify-end">
1912
+ <Button size="small" type="dashed" :disabled="!isEditable" @click="addRowStyleRule">
1913
+ {{ t("panel.config.tableAddRowStyleRule") }}
1914
+ </Button>
1915
+ </div>
1916
+ <div v-if="!(table.rowStyleRules ?? []).length" class="text-[11px] text-gray-500">
1917
+ {{ t("panel.config.tableNoRowStyleRules") }}
1918
+ </div>
1919
+ <div
1920
+ v-for="(rule, ruleIndex) in table.rowStyleRules ?? []"
1921
+ :key="ruleIndex"
1922
+ class="space-y-2 rounded-md border border-gray-200 bg-gray-50/70 p-2"
1923
+ >
1924
+ <div class="flex items-center justify-between gap-2">
1925
+ <div class="text-[11px] font-medium text-gray-500">
1926
+ {{ t("panel.config.tableStyleRuleItem", { n: ruleIndex + 1 }) }}
1927
+ </div>
1928
+ <Button size="small" danger type="text" :disabled="!isEditable" @click="removeRowStyleRule(ruleIndex)">
1929
+ ×
1930
+ </Button>
1931
+ </div>
1932
+ <ConditionEditor
1933
+ :model-value="rule.when"
1934
+ :show-field="true"
1935
+ :disabled="!isEditable"
1936
+ @update:model-value="(when) => patchRowStyleRule(ruleIndex, { when })"
1937
+ />
1938
+ <ConfigColorField
1939
+ :label="t('panel.config.tableBgColor')"
1940
+ :value="(rule.style.backgroundColor as string) ?? '#eff6ff'"
1941
+ :disabled="!isEditable"
1942
+ @update:value="(v) => patchRowStyleRule(ruleIndex, { backgroundColor: v || '#eff6ff' })"
1943
+ />
1944
+ </div>
1945
+ </ConfigFieldGroup>
1946
+
1947
+ </ConfigSection>
1948
+
1949
+ <Modal
1950
+ v-model:open="rowsEditorOpen"
1951
+ :title="t('panel.config.tableRowsEditTitle')"
1952
+ :ok-text="t('common.save')"
1953
+ :cancel-text="t('common.cancel')"
1954
+ width="640px"
1955
+ :styles="{
1956
+ body: {
1957
+ height: '480px',
1958
+ overflow: 'hidden',
1959
+ display: 'flex',
1960
+ flexDirection: 'column',
1961
+ padding: '8px',
1962
+ },
1963
+ }"
1964
+ @ok="saveRowsEditor"
1965
+ >
1966
+ <div class="mb-1.5 shrink-0 text-xs leading-4 text-gray-500">
1967
+ {{ t("panel.config.tableRowsEditDesc") }}
1968
+ </div>
1969
+ <div class="relative min-h-0 flex-1 overflow-hidden">
1970
+ <div class="absolute inset-0">
1971
+ <JsonCodeEditor v-model="rowsEditorDraft" class="h-full" />
1972
+ </div>
1973
+ </div>
1974
+ </Modal>
1975
+
1976
+ <Modal
1977
+ v-model:open="mockOpen"
1978
+ :title="t('panel.config.tableRowsMockTitle')"
1979
+ :ok-text="t('panel.config.tableMockGenerate')"
1980
+ :cancel-text="t('common.cancel')"
1981
+ width="560px"
1982
+ @ok="applyMockRows"
1983
+ >
1984
+ <div class="mb-3 text-xs text-gray-500">{{ t("panel.config.tableRowsMockDesc") }}</div>
1985
+ <div class="grid grid-cols-2 gap-2">
1986
+ <label class="block space-y-1">
1987
+ <div class="text-[11px]">{{ t("panel.config.tableMockRowCount") }}</div>
1988
+ <InputNumber v-model:value="mockRowCount" class="w-full" :min="1" :max="200" />
1989
+ </label>
1990
+ <label class="block space-y-1">
1991
+ <div class="text-[11px]">{{ t("panel.config.tableMockLocale") }}</div>
1992
+ <Select v-model:value="mockLocale" class="w-full" size="small">
1993
+ <Select.Option value="zh">{{ t("panel.config.tableMockLocaleZh") }}</Select.Option>
1994
+ <Select.Option value="en">{{ t("panel.config.tableMockLocaleEn") }}</Select.Option>
1995
+ </Select>
1996
+ </label>
1997
+ <label class="block space-y-1">
1998
+ <div class="text-[11px]">{{ t("panel.config.tableMockNamePrefix") }}</div>
1999
+ <Input v-model:value="mockNamePrefix" size="small" placeholder="Demo" />
2000
+ </label>
2001
+ <label class="block space-y-1">
2002
+ <div class="text-[11px]">{{ t("panel.config.tableMockSeed") }}</div>
2003
+ <Input v-model:value="mockSeed" size="small" placeholder="optional" />
2004
+ </label>
2005
+ <label class="col-span-2 block space-y-1">
2006
+ <div class="text-[11px]">{{ t("panel.config.tableMockStatusValues") }}</div>
2007
+ <Input v-model:value="mockStatusValues" size="small" placeholder="active,idle,pending" />
2008
+ </label>
2009
+ <label class="block space-y-1">
2010
+ <div class="text-[11px]">{{ t("panel.config.tableMockScoreMin") }}</div>
2011
+ <InputNumber v-model:value="mockScoreMin" class="w-full" />
2012
+ </label>
2013
+ <label class="block space-y-1">
2014
+ <div class="text-[11px]">{{ t("panel.config.tableMockScoreMax") }}</div>
2015
+ <InputNumber v-model:value="mockScoreMax" class="w-full" />
2016
+ </label>
2017
+ </div>
2018
+ <div class="mt-3 flex flex-wrap gap-x-4 gap-y-2">
2019
+ <label class="inline-flex items-center gap-2 text-[11px]">
2020
+ <Switch v-model:checked="mockIncludeId" size="small" />
2021
+ <span>{{ t("panel.config.tableMockIncludeId") }}</span>
2022
+ </label>
2023
+ <label class="inline-flex items-center gap-2 text-[11px]">
2024
+ <Switch v-model:checked="mockIncludeTimestamp" size="small" />
2025
+ <span>{{ t("panel.config.tableMockIncludeTimestamp") }}</span>
2026
+ </label>
2027
+ <label class="inline-flex items-center gap-2 text-[11px]">
2028
+ <Switch v-model:checked="mockIncludeExtras" size="small" />
2029
+ <span>{{ t("panel.config.tableMockIncludeExtras") }}</span>
2030
+ </label>
2031
+ </div>
2032
+ </Modal>
2033
+ </template>