@arronqzy/vue-view 0.1.6 → 0.1.8

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 (34) hide show
  1. package/package.json +8 -7
  2. package/src/panel/VueViewOnlinePreview.vue +28 -2
  3. package/src/panel/VueViewPanel.vue +120 -66
  4. package/src/panel/components/BusyOverlay.vue +24 -0
  5. package/src/panel/components/ConfigHintIcon.vue +10 -10
  6. package/src/panel/components/ElementsLayer.vue +51 -8
  7. package/src/panel/components/MaterialPreview.vue +8 -0
  8. package/src/panel/components/PanelConfigSidebar.vue +11 -0
  9. package/src/panel/components/WorkspaceConfigSidebar.vue +1 -0
  10. package/src/panel/components/WorkspaceProjectNav.vue +36 -8
  11. package/src/panel/components/config/ConfigColorField.vue +33 -9
  12. package/src/panel/components/config/PanelConfigAudioSection.vue +32 -18
  13. package/src/panel/components/config/PanelConfigImageSection.vue +13 -9
  14. package/src/panel/components/config/PanelConfigScene3dSection.vue +505 -0
  15. package/src/panel/components/config/PanelConfigStyleSections.vue +13 -9
  16. package/src/panel/components/config/PanelConfigTextSection.vue +20 -6
  17. package/src/panel/components/config/PanelConfigVideoSection.vue +13 -9
  18. package/src/panel/components/config/shared.ts +16 -21
  19. package/src/panel/components/elementsLayerNodes.ts +64 -59
  20. package/src/panel/components/materialSidebarData.ts +2 -0
  21. package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.vue +1 -1
  22. package/src/panel/components/table/PanelConfigTableSection.vue +39 -25
  23. package/src/panel/components/table/TableNodeContent.vue +36 -15
  24. package/src/panel/hooks/usePanelElements.ts +13 -1
  25. package/src/panel/hooks/useWorkspaceProjects.ts +128 -57
  26. package/src/panel/library/workspace-project-cache.ts +7 -2
  27. package/src/panel/library/workspace-project-db.ts +29 -11
  28. package/src/panel/library/workspace-snapshot.ts +21 -4
  29. package/src/panel/scope/view-scope-store.ts +8 -0
  30. package/src/panel/types.ts +4 -2
  31. package/src/panel/utils/async-work.ts +153 -0
  32. package/src/panel/utils/gridPlacement.ts +29 -0
  33. package/src/panel/utils/panelElementDefaults.ts +37 -1
  34. package/src/panel/utils/scope-autocomplete.ts +0 -114
@@ -41,15 +41,19 @@ function patchStyle(patch: Partial<PanelElementStyle>) {
41
41
  }
42
42
 
43
43
  async function handleUploadBackgroundImage(file: File) {
44
- const base64 = await readFileAsDataUrl(file, msgs().readImageFailed);
45
- patchStyle({ backgroundImage: `url("${base64}")` });
46
- uploadStatus.value = t("panel.config.uploadWrittenBase64");
47
- const url = await uploadFileToRemote(file);
48
- if (url) {
49
- patchStyle({ backgroundImageRemoteUrl: url });
50
- uploadStatus.value = t("panel.config.uploadServerAndBase64");
51
- } else {
52
- uploadStatus.value = t("panel.config.uploadServerFailedKeepBase64");
44
+ try {
45
+ const base64 = await readFileAsDataUrl(file, msgs().readImageFailed);
46
+ patchStyle({ backgroundImage: `url("${base64}")` });
47
+ uploadStatus.value = t("panel.config.uploadWrittenBase64");
48
+ const url = await uploadFileToRemote(file);
49
+ if (url) {
50
+ patchStyle({ backgroundImageRemoteUrl: url });
51
+ uploadStatus.value = t("panel.config.uploadServerAndBase64");
52
+ } else {
53
+ uploadStatus.value = t("panel.config.uploadServerFailedKeepBase64");
54
+ }
55
+ } catch (error) {
56
+ uploadStatus.value = error instanceof Error ? error.message : msgs().readImageFailed;
53
57
  }
54
58
  }
55
59
 
@@ -3,6 +3,7 @@ import { useI18n } from "@arronqzy/i18n/vue";
3
3
  import { onMounted, ref, watch } from "vue";
4
4
  import { Checkbox, Input, InputNumber, Select } from "ant-design-vue";
5
5
  import type { PanelElement } from "../../types";
6
+ import { cssTextLineHeight, cssTextAlignStyle } from "../../utils/panelElementDefaults";
6
7
  import ConfigColorField from "./ConfigColorField.vue";
7
8
  import ConfigFieldGroup from "./ConfigFieldGroup.vue";
8
9
  import ConfigSection from "./ConfigSection.vue";
@@ -87,15 +88,16 @@ watch(() => [props.element.id, props.element.textHtml], syncEditorHtml);
87
88
  </div>
88
89
  <div
89
90
  ref="textEditorRef"
90
- class="min-h-[120px] rounded border border-gray-200 bg-white px-2 py-1.5 text-xs leading-6 outline-none"
91
+ data-panel-user-text=""
92
+ class="min-h-[120px] rounded border border-gray-200 bg-white px-2 py-1.5 leading-6 outline-none"
91
93
  :contenteditable="isEditable"
92
94
  :style="{
93
95
  fontFamily: element.textFontFamily || undefined,
94
96
  fontSize: element.textFontSize ? `${element.textFontSize}px` : undefined,
95
97
  fontWeight: element.textFontWeight || undefined,
96
98
  color: element.textColor || undefined,
97
- lineHeight: element.textLineHeight ? String(element.textLineHeight) : undefined,
98
- textAlign: element.textAlign ?? 'left',
99
+ lineHeight: cssTextLineHeight(element.textLineHeight),
100
+ ...cssTextAlignStyle(element.textAlign),
99
101
  }"
100
102
  @input="(e) => {
101
103
  const nextHtml = (e.target as HTMLDivElement).innerHTML;
@@ -156,20 +158,32 @@ watch(() => [props.element.id, props.element.textHtml], syncEditorHtml);
156
158
  >
157
159
  <Select.Option value="left">{{ t("panel.config.alignLeft") }}</Select.Option>
158
160
  <Select.Option value="center">{{ t("panel.config.alignCenter") }}</Select.Option>
161
+ <Select.Option value="middle">{{ t("panel.config.alignMiddle") }}</Select.Option>
159
162
  <Select.Option value="right">{{ t("panel.config.alignRight") }}</Select.Option>
160
163
  <Select.Option value="justify">{{ t("panel.config.alignJustify") }}</Select.Option>
161
164
  </Select>
162
165
  </label>
163
166
  <label class="block space-y-1">
164
- <div>{{ t("panel.config.lineHeight") }}</div>
167
+ <div class="flex items-center justify-between gap-2">
168
+ <span>{{ t("panel.config.lineHeight") }}</span>
169
+ <label class="flex items-center gap-1 font-normal">
170
+ <Checkbox
171
+ :checked="element.textLineHeight === 'auto'"
172
+ :disabled="!isEditable"
173
+ @update:checked="(v) => patch({ textLineHeight: v ? 'auto' : 1.6 })"
174
+ />
175
+ <span>{{ t("panel.config.lineHeightAuto") }}</span>
176
+ </label>
177
+ </div>
165
178
  <InputNumber
166
179
  size="small"
167
180
  class="w-full"
168
181
  :min="1"
169
182
  :max="3"
170
183
  :step="0.1"
171
- :value="element.textLineHeight ?? 1.6"
172
- :disabled="!isEditable"
184
+ :value="element.textLineHeight === 'auto' ? undefined : (element.textLineHeight ?? 1.6)"
185
+ :disabled="!isEditable || element.textLineHeight === 'auto'"
186
+ :placeholder="t('panel.config.lineHeightAuto')"
173
187
  @update:value="(v) => {
174
188
  const n = Number(v);
175
189
  if (!Number.isNaN(n)) patch({ textLineHeight: Math.min(3, Math.max(1, n)) });
@@ -35,15 +35,19 @@ function patch(patch: Partial<PanelElement>) {
35
35
  }
36
36
 
37
37
  async function handleUploadVideoFile(file: File) {
38
- const base64 = await readFileAsDataUrl(file, msgs().readVideoFailed);
39
- patch({ videoSrc: base64 });
40
- videoStatus.value = msgs().videoLocalSaved;
41
- const url = await uploadFileToRemote(file);
42
- if (url) {
43
- patch({ videoRemoteUrl: url });
44
- videoStatus.value = msgs().videoRemoteUploaded;
45
- } else {
46
- videoStatus.value = msgs().videoServerUploadFailed;
38
+ try {
39
+ const base64 = await readFileAsDataUrl(file, msgs().readVideoFailed, "video");
40
+ patch({ videoSrc: base64 });
41
+ videoStatus.value = msgs().videoLocalSaved;
42
+ const url = await uploadFileToRemote(file);
43
+ if (url) {
44
+ patch({ videoRemoteUrl: url });
45
+ videoStatus.value = msgs().videoRemoteUploaded;
46
+ } else {
47
+ videoStatus.value = msgs().videoServerUploadFailed;
48
+ }
49
+ } catch (error) {
50
+ videoStatus.value = error instanceof Error ? error.message : msgs().readVideoFailed;
47
51
  }
48
52
  }
49
53
 
@@ -1,4 +1,6 @@
1
1
  import type { PanelChartConfig } from "../../types";
2
+ import { resolveLocale, tForLocale } from "@arronqzy/i18n";
3
+ import { runBusyTask } from "../../utils/async-work";
2
4
 
3
5
  export function mergeOptionPatch(
4
6
  base: Record<string, unknown> | undefined,
@@ -38,29 +40,22 @@ export function sectionMatchesSearch(
38
40
  );
39
41
  }
40
42
 
41
- export function readFileAsDataUrl(
42
- file: File,
43
- errorMessage: string
44
- ): Promise<string> {
45
- return new Promise((resolve, reject) => {
46
- const reader = new FileReader();
47
- reader.onload = () => resolve(String(reader.result ?? ""));
48
- reader.onerror = () => reject(new Error(errorMessage));
49
- reader.readAsDataURL(file);
50
- });
51
- }
43
+ export { readFileAsDataUrl } from "../../utils/async-work";
52
44
 
53
45
  export async function uploadFileToRemote(file: File): Promise<string | undefined> {
54
- try {
55
- const form = new FormData();
56
- form.append("file", file);
57
- const resp = await fetch("/api/upload", { method: "POST", body: form });
58
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
59
- const data = (await resp.json()) as { url?: string };
60
- return data.url;
61
- } catch {
62
- return undefined;
63
- }
46
+ const t = tForLocale(resolveLocale());
47
+ return runBusyTask(t("common.uploadingFile"), async () => {
48
+ try {
49
+ const form = new FormData();
50
+ form.append("file", file);
51
+ const resp = await fetch("/api/upload", { method: "POST", body: form });
52
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
53
+ const data = (await resp.json()) as { url?: string };
54
+ return data.url;
55
+ } catch {
56
+ return undefined;
57
+ }
58
+ });
64
59
  }
65
60
 
66
61
  export function patchChart(
@@ -15,7 +15,9 @@ import * as echarts from "echarts";
15
15
  import type { PanelElement } from "../types";
16
16
  import { buildChartOption, CHART_TYPES } from "../utils/chartOptionBuilder";
17
17
  import { PREVIEW_LAYOUT_EVENT } from "../utils/panelStateIO";
18
+ import { cssTextLineHeight, cssTextAlignStyle } from "../utils/panelElementDefaults";
18
19
  import TableNodeContent from "./table/TableNodeContent.vue";
20
+ import Scene3dNodeContent from "@arronqzy/view-scene3d/vue";
19
21
 
20
22
  export { CHART_TYPES };
21
23
 
@@ -74,8 +76,8 @@ export const TextNodeContent = defineComponent({
74
76
  fontSize: p.element.textFontSize ? `${p.element.textFontSize}px` : undefined,
75
77
  fontWeight: p.element.textFontWeight || undefined,
76
78
  color: p.element.textColor || undefined,
77
- lineHeight: p.element.textLineHeight ? String(p.element.textLineHeight) : undefined,
78
- textAlign: p.element.textAlign ?? "left",
79
+ lineHeight: cssTextLineHeight(p.element.textLineHeight),
80
+ ...cssTextAlignStyle(p.element.textAlign),
79
81
  }));
80
82
  watch(
81
83
  html,
@@ -89,7 +91,8 @@ export const TextNodeContent = defineComponent({
89
91
  return () =>
90
92
  h("div", {
91
93
  ref: nodeRef,
92
- class: "h-full w-full overflow-auto break-words p-2 text-sm leading-relaxed outline-none",
94
+ "data-panel-user-text": "",
95
+ class: "h-full w-full overflow-auto break-words p-2 outline-none",
93
96
  style: textStyle.value,
94
97
  contentEditable: p.editable,
95
98
  onInput: (e: Event) => emit("change", (e.currentTarget as HTMLDivElement).innerHTML),
@@ -127,31 +130,10 @@ export const GridNodeContent = defineComponent({
127
130
  });
128
131
  return set;
129
132
  });
130
- const occupiedBlocks = computed(() =>
131
- p.allElements
132
- .filter(
133
- (el) =>
134
- el.parentGridId === p.element.id &&
135
- el.layerId === p.element.layerId &&
136
- el.gridSlotIndex !== undefined
137
- )
138
- .map((el) => {
139
- const start = Math.max(0, Math.floor(el.gridSlotIndex ?? 0));
140
- const baseRow = Math.floor(start / cols.value);
141
- const baseCol = start % cols.value;
142
- const rowSpan = Math.max(1, Math.min(rows.value - baseRow, Math.floor(el.gridRowSpan ?? 1)));
143
- const colSpan = Math.max(1, Math.min(cols.value - baseCol, Math.floor(el.gridColSpan ?? 1)));
144
- return {
145
- id: el.id,
146
- rowStart: baseRow + 1,
147
- rowEnd: baseRow + rowSpan + 1,
148
- colStart: baseCol + 1,
149
- colEnd: baseCol + colSpan + 1,
150
- };
151
- })
152
- );
153
133
  return () => {
154
134
  if (p.previewMode) return h("div", { class: "relative h-full w-full" });
135
+ const total = rows.value * cols.value;
136
+ const emptyCount = total - occupied.value.size;
155
137
  const gridStyle = {
156
138
  display: "grid",
157
139
  gridTemplateColumns: `repeat(${cols.value}, minmax(0, 1fr))`,
@@ -160,40 +142,58 @@ export const GridNodeContent = defineComponent({
160
142
  padding: `${padding.value}px`,
161
143
  boxSizing: "border-box" as const,
162
144
  };
163
- return h("div", { class: "relative h-full w-full" }, [
164
- h(
165
- "div",
166
- { class: "h-full w-full", style: gridStyle },
167
- Array.from({ length: rows.value * cols.value }).map((_, idx) =>
168
- h("div", {
169
- key: idx,
170
- class: [
171
- "rounded border border-dashed",
172
- occupied.value.has(idx)
173
- ? "border-primary/70 bg-primary/10"
174
- : "border-border/60 bg-muted/20",
175
- ].join(" "),
176
- title: occupied.value.has(idx)
177
- ? t("panel.config.slotOccupied", { n: idx + 1 })
178
- : t("panel.config.slotEmpty", { n: idx + 1 }),
179
- })
180
- )
181
- ),
182
- h(
183
- "div",
184
- { class: "pointer-events-none absolute inset-0", style: gridStyle },
185
- occupiedBlocks.value.map((block) =>
186
- h("div", {
187
- key: block.id,
188
- class: "rounded border border-primary/70 bg-primary/20",
189
- style: {
190
- gridColumn: `${block.colStart} / ${block.colEnd}`,
191
- gridRow: `${block.rowStart} / ${block.rowEnd}`,
192
- },
145
+ return h(
146
+ "div",
147
+ {
148
+ class: "pointer-events-none relative h-full w-full overflow-hidden rounded-md",
149
+ style:
150
+ emptyCount > 0
151
+ ? {
152
+ border: "2px dashed rgba(14, 165, 233, 0.7)",
153
+ background: "rgba(14, 165, 233, 0.08)",
154
+ }
155
+ : undefined,
156
+ },
157
+ [
158
+ h(
159
+ "div",
160
+ { class: "h-full w-full", style: gridStyle },
161
+ Array.from({ length: total }).map((_, idx) => {
162
+ const filled = occupied.value.has(idx);
163
+ return h(
164
+ "div",
165
+ {
166
+ key: idx,
167
+ class: filled
168
+ ? "pointer-events-none"
169
+ : "pointer-events-auto flex flex-col items-center justify-center gap-0.5 rounded-md text-[11px] font-semibold",
170
+ style: filled
171
+ ? undefined
172
+ : {
173
+ border: "2px dashed rgba(2, 132, 199, 0.85)",
174
+ background: "rgba(186, 230, 253, 0.72)",
175
+ color: "rgba(3, 105, 161, 0.95)",
176
+ },
177
+ title: filled
178
+ ? t("panel.config.slotOccupied", { n: idx + 1 })
179
+ : t("panel.config.slotEmpty", { n: idx + 1 }),
180
+ },
181
+ filled ? undefined : h("span", String(idx + 1))
182
+ );
193
183
  })
194
- )
195
- ),
196
- ]);
184
+ ),
185
+ occupied.value.size === 0
186
+ ? h(
187
+ "div",
188
+ {
189
+ class:
190
+ "pointer-events-none absolute bottom-1.5 left-0 right-0 text-center text-[11px] font-medium text-sky-800/90",
191
+ },
192
+ t("panel.config.gridDropHint")
193
+ )
194
+ : null,
195
+ ]
196
+ );
197
197
  };
198
198
  },
199
199
  });
@@ -788,6 +788,11 @@ export const ReferenceNodeContent = defineComponent({
788
788
  })
789
789
  : node.materialType === "geometry"
790
790
  ? h(GeometryNodeContent, { element: node })
791
+ : node.materialType === "scene3d"
792
+ ? h(Scene3dNodeContent, {
793
+ config: node.scene3d,
794
+ previewMode: p.previewMode,
795
+ })
791
796
  : node.materialType === "table"
792
797
  ? h(TableNodeContent, { element: node })
793
798
  : h("div", { class: "h-full w-full" }),
@@ -31,6 +31,7 @@ const MATERIAL_LABEL_KEYS: Record<string, string> = {
31
31
  audio: "panel.material.audio",
32
32
  reference: "panel.material.reference",
33
33
  geometry: "panel.material.geometry",
34
+ scene3d: "panel.material.scene3d",
34
35
  table: "panel.material.table",
35
36
  };
36
37
 
@@ -65,6 +66,7 @@ export function getDefaultCategories(t: TranslateFn): MaterialCategory[] {
65
66
  { id: "text", title: t("panel.material.text") },
66
67
  { id: "table", title: t("panel.material.table") },
67
68
  { id: "geometry", title: t("panel.material.geometry") },
69
+ { id: "scene3d", title: t("panel.material.scene3d") },
68
70
  { id: "grid", title: t("panel.material.grid") },
69
71
  { id: "image", title: t("panel.material.image") },
70
72
  { id: "reference", title: t("panel.material.reference") },
@@ -14,7 +14,7 @@ import {
14
14
  getScopeAutocompleteSuggestions,
15
15
  parseScopeAutocomplete,
16
16
  type ScopeAutocompleteState,
17
- } from "../../utils/scope-autocomplete";
17
+ } from "@arronqzy/blueprint-dsl";
18
18
 
19
19
  type DropdownState = {
20
20
  input: HTMLInputElement | HTMLTextAreaElement;
@@ -27,6 +27,7 @@ import ConfigSection from "../config/ConfigSection.vue";
27
27
  import ConfigHintIcon from "../ConfigHintIcon.vue";
28
28
  import ConditionEditor from "./ConditionEditor.vue";
29
29
  import JsonCodeEditor from "./JsonCodeEditor.vue";
30
+ import { readFileAsDataUrl, runBusyTask } from "../../utils/async-work";
30
31
 
31
32
  const { t } = useI18n();
32
33
 
@@ -256,29 +257,27 @@ const imageUploadHint = ref("");
256
257
 
257
258
  async function uploadImageForColumn(index: number, file: File) {
258
259
  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");
260
+ await runBusyTask(t("common.uploadingFile"), async () => {
261
+ const base64 = await readFileAsDataUrl(file, t("panel.messages.readImageFailed"), "image");
262
+ patchWidgetProps(index, { imageUrlMode: "static", imageUrl: base64 });
263
+ imageUploadHint.value = t("panel.config.uploadWrittenBase64");
264
+ try {
265
+ const form = new FormData();
266
+ form.append("file", file);
267
+ const resp = await fetch("/api/upload", { method: "POST", body: form });
268
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
269
+ const data = (await resp.json()) as { url?: string };
270
+ if (data.url) {
271
+ patchWidgetProps(index, { imageUrlMode: "static", imageUrl: data.url });
272
+ imageUploadHint.value = t("panel.config.uploadServerAndBase64");
273
+ }
274
+ } catch {
275
+ imageUploadHint.value = t("panel.config.uploadServerFailedKeepBase64");
276
276
  }
277
- } catch {
278
- imageUploadHint.value = t("panel.config.uploadServerFailedKeepBase64");
279
- }
280
- } catch {
281
- imageUploadHint.value = t("panel.messages.readImageFailed");
277
+ });
278
+ } catch (error) {
279
+ imageUploadHint.value =
280
+ error instanceof Error ? error.message : t("panel.messages.readImageFailed");
282
281
  }
283
282
  }
284
283
 
@@ -785,7 +784,7 @@ function patchProgressRuleWhen(ruleIndex: number, when: Condition) {
785
784
  </ConfigHintIcon>
786
785
  </div>
787
786
 
788
- <template v-if="(selectedColumn.column.widget ?? 'text') === 'text'">
787
+ <template v-if="(selectedColumn.column.widget ?? 'text') !== 'image'">
789
788
  <div class="grid grid-cols-2 gap-2">
790
789
  <label class="block space-y-1">
791
790
  <div>{{ t("panel.config.tableTextFontSize") }}</div>
@@ -934,6 +933,9 @@ function patchProgressRuleWhen(ruleIndex: number, when: Condition) {
934
933
  </Select>
935
934
  </label>
936
935
  </div>
936
+ </template>
937
+
938
+ <template v-if="(selectedColumn.column.widget ?? 'text') === 'text'">
937
939
  <label class="block space-y-1">
938
940
  <div class="inline-flex items-center gap-1">
939
941
  {{ t("panel.config.tableActionOnClick") }}
@@ -1867,11 +1869,21 @@ function patchProgressRuleWhen(ruleIndex: number, when: Condition) {
1867
1869
  />
1868
1870
  </label>
1869
1871
  <div class="grid grid-cols-2 gap-2">
1872
+ <ConfigColorField
1873
+ :label="t('panel.config.tableBodyBg')"
1874
+ :value="(table.tableStyle?.backgroundColor as string) ?? ''"
1875
+ :disabled="!isEditable"
1876
+ allow-transparent
1877
+ fallback-hex="#ffffff"
1878
+ @update:value="(v) => updateTableStyle({ backgroundColor: v || undefined })"
1879
+ />
1870
1880
  <ConfigColorField
1871
1881
  :label="t('panel.config.tableHeaderBg')"
1872
- :value="(table.headerStyle?.backgroundColor as string) ?? 'rgba(0,0,0,0.04)'"
1882
+ :value="(table.headerStyle?.backgroundColor as string) ?? ''"
1873
1883
  :disabled="!isEditable"
1874
- @update:value="(v) => updateHeaderStyle({ backgroundColor: v || 'rgba(0,0,0,0.04)' })"
1884
+ allow-transparent
1885
+ fallback-hex="#f3f4f6"
1886
+ @update:value="(v) => updateHeaderStyle({ backgroundColor: v || undefined })"
1875
1887
  />
1876
1888
  <label class="block space-y-1.5">
1877
1889
  <div>{{ t("panel.config.tableHeaderFontSize") }}</div>
@@ -1939,6 +1951,8 @@ function patchProgressRuleWhen(ruleIndex: number, when: Condition) {
1939
1951
  :label="t('panel.config.tableBgColor')"
1940
1952
  :value="(rule.style.backgroundColor as string) ?? '#eff6ff'"
1941
1953
  :disabled="!isEditable"
1954
+ allow-transparent
1955
+ fallback-hex="#eff6ff"
1942
1956
  @update:value="(v) => patchRowStyleRule(ruleIndex, { backgroundColor: v || '#eff6ff' })"
1943
1957
  />
1944
1958
  </div>
@@ -8,8 +8,11 @@ import {
8
8
  resolveCellDisplay,
9
9
  resolveRowDisplay,
10
10
  stylePropsToCss,
11
+ tableTextStyleToCss,
11
12
  resolveRawTableInput,
12
13
  resolveProgressDisplay,
14
+ resolveTableBodyBackground,
15
+ resolveTableHeaderBackground,
13
16
  type PanelTableConfig,
14
17
  type NormalizedColumn,
15
18
  type NormalizedRow,
@@ -102,7 +105,7 @@ const headerTextColor = computed(
102
105
  );
103
106
 
104
107
  const rootStyle = computed(() => ({
105
- background: (config.value.tableStyle?.backgroundColor as string) || "#fff",
108
+ background: resolveTableBodyBackground(config.value.tableStyle?.backgroundColor),
106
109
  border:
107
110
  config.value.tableStyle?.borderWidth != null
108
111
  ? `${config.value.tableStyle.borderWidth}px solid ${config.value.tableStyle.borderColor ?? "rgba(0,0,0,0.12)"}`
@@ -126,7 +129,7 @@ const headerRowStyle = computed(() => ({
126
129
  : ("sticky" as const),
127
130
  top: 0,
128
131
  zIndex: 2,
129
- background: (config.value.headerStyle?.backgroundColor as string) || "rgba(0,0,0,0.04)",
132
+ background: resolveTableHeaderBackground(config.value.headerStyle?.backgroundColor),
130
133
  borderBottom: "1px solid rgba(0,0,0,0.1)",
131
134
  ...headerStyle.value,
132
135
  color: headerTextColor.value,
@@ -145,7 +148,8 @@ function headerJustify(align: NormalizedColumn["align"]) {
145
148
  function cellCommonStyle(cell: CellDisplay): Record<string, string | number> {
146
149
  const style = stylePropsToCss(cell.style);
147
150
  const textOverflow = cell.widgetProps?.textStyle?.overflow ?? "ellipsis";
148
- const isWrap = cell.widget === "text" && textOverflow === "wrap";
151
+ const isWrap =
152
+ textOverflow === "wrap" && cell.widget !== "image" && cell.widget !== "progress";
149
153
  return {
150
154
  ...style,
151
155
  overflow: "hidden",
@@ -189,6 +193,7 @@ const TableCellView = defineComponent({
189
193
  return (): VNode => {
190
194
  const cell = p.cell;
191
195
  const common = cellCommonStyle(cell);
196
+ const textCss = tableTextStyleToCss(cell.widgetProps?.textStyle);
192
197
  let node: VNode;
193
198
 
194
199
  if (cell.widget === "tag" || cell.widget === "badge") {
@@ -208,6 +213,7 @@ const TableCellView = defineComponent({
208
213
  lineHeight: "20px",
209
214
  color: "#fff",
210
215
  background: bg,
216
+ ...textCss,
211
217
  },
212
218
  },
213
219
  [
@@ -243,6 +249,7 @@ const TableCellView = defineComponent({
243
249
  textDecoration: "underline",
244
250
  overflow: "hidden",
245
251
  textOverflow: "ellipsis",
252
+ ...textCss,
246
253
  },
247
254
  },
248
255
  cell.text || cell.href
@@ -254,7 +261,14 @@ const TableCellView = defineComponent({
254
261
  const trackColor = cell.color ?? "#3b82f6";
255
262
  const label = h(
256
263
  "span",
257
- { style: { fontSize: 11, color: "rgba(0,0,0,0.55)", flex: "0 0 auto" } },
264
+ {
265
+ style: {
266
+ fontSize: 11,
267
+ color: "rgba(0,0,0,0.55)",
268
+ flex: "0 0 auto",
269
+ ...textCss,
270
+ },
271
+ },
258
272
  cell.text
259
273
  );
260
274
 
@@ -295,8 +309,16 @@ const TableCellView = defineComponent({
295
309
  y: "50%",
296
310
  "dominant-baseline": "central",
297
311
  "text-anchor": "middle",
298
- "font-size": Math.max(8, Math.round(size * 0.32)),
299
- fill: "rgba(0,0,0,0.65)",
312
+ "font-size":
313
+ typeof textCss.fontSize === "number"
314
+ ? textCss.fontSize
315
+ : Math.max(8, Math.round(size * 0.32)),
316
+ fill: typeof textCss.color === "string" ? textCss.color : "rgba(0,0,0,0.65)",
317
+ "font-family":
318
+ typeof textCss.fontFamily === "string" ? textCss.fontFamily : undefined,
319
+ "font-weight": textCss.fontWeight,
320
+ "font-style":
321
+ typeof textCss.fontStyle === "string" ? textCss.fontStyle : undefined,
300
322
  },
301
323
  String(Math.round(pct))
302
324
  ),
@@ -343,7 +365,7 @@ const TableCellView = defineComponent({
343
365
  },
344
366
  draggable: false,
345
367
  })
346
- : h("span", { style: { fontSize: 11, opacity: 0.5 } }, "—"),
368
+ : h("span", { style: { fontSize: 11, opacity: 0.5, ...textCss } }, "—"),
347
369
  ]);
348
370
  } else if (cell.widget === "boolean") {
349
371
  const on = Boolean(cell.booleanValue);
@@ -392,22 +414,21 @@ const TableCellView = defineComponent({
392
414
  }),
393
415
  ]
394
416
  ),
395
- h("span", { style: { marginLeft: 6, fontSize: 12 } }, cell.text),
417
+ h("span", { style: { marginLeft: 6, fontSize: 12, ...textCss } }, cell.text),
396
418
  ]);
397
419
  } else {
398
- const textStyle = cell.widgetProps?.textStyle;
399
420
  const canClickText = Boolean(cell.widgetProps?.actions?.onClickBlueprintNodeId);
400
421
  node = h(
401
422
  "div",
402
423
  {
403
424
  style: {
404
425
  ...common,
405
- fontFamily: textStyle?.fontFamily,
406
- fontStyle: textStyle?.fontStyle,
407
- textDecoration:
408
- textStyle?.textDecoration === "none" ? undefined : textStyle?.textDecoration,
426
+ ...textCss,
409
427
  cursor: canClickText ? "pointer" : undefined,
410
- color: canClickText && !(common as { color?: string }).color ? "#2563eb" : common.color,
428
+ color:
429
+ canClickText && !(common as { color?: string }).color && !textCss.color
430
+ ? "#2563eb"
431
+ : (textCss.color as string | undefined) ?? (common as { color?: string }).color,
411
432
  },
412
433
  onClick: (e: Event) => {
413
434
  if (!canClickText) return;
@@ -504,7 +525,7 @@ const TableRowView = defineComponent({
504
525
  </div>
505
526
  </div>
506
527
 
507
- <div ref="parentRef" class="min-h-0 flex-1 overflow-auto">
528
+ <div ref="parentRef" class="min-h-0 flex-1 overflow-auto" style="background: transparent">
508
529
  <div
509
530
  v-if="model.rows.length === 0"
510
531
  class="flex h-full flex-col items-center justify-center gap-2 px-3 text-center text-xs text-gray-400"