@arronqzy/vue-view 0.1.25 → 0.1.26

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.
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { useI18n } from "@arronqzy/i18n/vue";
3
3
  import { computed, ref } from "vue";
4
- import { Card, Empty, Input, Switch, Tabs } from "ant-design-vue";
4
+ import { Empty, Input, Select, Tabs, Tooltip } from "ant-design-vue";
5
5
  import type { PanelElement, PanelLayer, ReferenceCopyMode } from "../types";
6
6
  import { getPanelMessages } from "../constants/messages";
7
7
  import {
@@ -48,7 +48,6 @@ const props = withDefaults(defineProps<MaterialSidebarProps>(), {
48
48
  const leftTab = ref<"materials" | "tree">("materials");
49
49
  const activeCategoryId = ref<MaterialCategoryId>("charts");
50
50
  const keyword = ref("");
51
- const treeKeyword = ref("");
52
51
  const referenceOnlyTree = ref(false);
53
52
  const draggingTreeNodeId = ref<string | null>(null);
54
53
  const dragOverLayerId = ref<string | null>(null);
@@ -56,14 +55,26 @@ const expandedKeys = ref<Record<string, boolean>>({ root: true });
56
55
 
57
56
  const categories = computed(() => { void locale.value; return getDefaultCategories(t); });
58
57
  const normalizedKeyword = computed(() => keyword.value.trim().toLowerCase());
59
- const isSearching = computed(() => normalizedKeyword.value.length > 0);
60
- const normalizedTreeKeyword = computed(() => treeKeyword.value.trim().toLowerCase());
61
- const isTreeSearching = computed(() => normalizedTreeKeyword.value.length > 0);
62
-
63
- const activeCategory = computed(
64
- () => categories.value.find((c) => c.id === activeCategoryId.value) ?? categories.value[0]
58
+ const isSearching = computed(
59
+ () => leftTab.value === "materials" && normalizedKeyword.value.length > 0
60
+ );
61
+ const isTreeSearching = computed(
62
+ () => leftTab.value === "tree" && normalizedKeyword.value.length > 0
63
+ );
64
+ const normalizedTreeKeyword = computed(() =>
65
+ isTreeSearching.value ? normalizedKeyword.value : ""
66
+ );
67
+ const searchPlaceholder = computed(() =>
68
+ leftTab.value === "tree"
69
+ ? t("panel.material.treeSearchPlaceholder")
70
+ : t("panel.material.searchPlaceholder")
65
71
  );
66
72
 
73
+ function onLeftTabChange(key: string | number) {
74
+ leftTab.value = key === "tree" ? "tree" : "materials";
75
+ keyword.value = "";
76
+ }
77
+
67
78
  const matchedItems = computed(() => {
68
79
  if (!isSearching.value) return [];
69
80
  const result: Array<MaterialItem & { categoryTitle: string }> = [];
@@ -78,6 +89,15 @@ const matchedItems = computed(() => {
78
89
  return result;
79
90
  });
80
91
 
92
+ const activeCategory = computed(
93
+ () =>
94
+ categories.value.find((c) => c.id === activeCategoryId.value) ?? categories.value[0]
95
+ );
96
+
97
+ const displayItems = computed(() =>
98
+ isSearching.value ? matchedItems.value : (activeCategory.value?.items ?? [])
99
+ );
100
+
81
101
  const elementsByLayer = computed(() => {
82
102
  const map = new Map<string, PanelElement[]>();
83
103
  for (const layer of props.layers) map.set(layer.id, []);
@@ -270,258 +290,301 @@ const treeSearchEmpty = computed(
270
290
  <template>
271
291
  <aside
272
292
  :class="[
273
- 'grid h-full w-full border-r border-border bg-muted/30 text-foreground',
293
+ 'rv-material-sidebar grid h-full min-h-0 w-full overflow-hidden border-r border-border/60 bg-muted/20 text-foreground',
274
294
  props.class ?? '',
275
295
  ]"
276
296
  style="grid-template-rows: auto 1fr"
277
297
  >
278
- <div class="border-b border-border bg-background/80 px-3 py-2 backdrop-blur-sm">
298
+ <div class="border-b border-border/50 bg-background/70 px-2 py-1.5 backdrop-blur-sm">
279
299
  <Input
280
300
  v-model:value="keyword"
281
301
  size="small"
282
- :placeholder="t('panel.material.searchPlaceholder')"
283
- class="text-xs"
302
+ :placeholder="searchPlaceholder"
303
+ :aria-label="searchPlaceholder"
304
+ class="text-[12px]"
284
305
  />
285
306
  </div>
286
307
 
287
- <Tabs v-model:active-key="leftTab" class="flex min-h-0 h-full flex-col">
308
+ <Tabs
309
+ :active-key="leftTab"
310
+ class="flex h-full min-h-0 flex-col overflow-hidden"
311
+ size="small"
312
+ @update:active-key="onLeftTabChange"
313
+ >
288
314
  <Tabs.TabPane key="materials" :tab="t('panel.material.tabMaterials')" class="min-h-0 flex-1">
289
- <div class="grid h-full min-h-0 grid-cols-[110px_1fr]">
290
- <div :class="`overflow-auto border-r border-border ${themedScrollbarClass}`">
291
- <button
292
- v-for="c in categories"
293
- :key="c.id"
294
- type="button"
295
- :class="[
296
- 'w-full cursor-pointer border-b border-border/40 px-2.5 py-2.5 text-left text-xs',
297
- c.id === activeCategoryId
298
- ? 'bg-accent text-accent-foreground'
299
- : 'text-muted-foreground hover:bg-accent/60 hover:text-accent-foreground',
300
- ]"
301
- @click="activeCategoryId = c.id"
315
+ <div class="flex h-full min-h-0 flex-col overflow-hidden">
316
+ <div
317
+ v-if="!isSearching"
318
+ class="shrink-0 border-b border-border/40 bg-background/80 px-2 py-1.5"
319
+ >
320
+ <Select
321
+ v-model:value="activeCategoryId"
322
+ size="small"
323
+ class="w-full text-[12px] font-semibold"
324
+ :aria-label="activeCategory?.title"
302
325
  >
303
- {{ c.title }}
304
- </button>
305
- </div>
306
-
307
- <div :class="`overflow-auto ${themedScrollbarClass}`">
308
- <div class="px-2.5 py-2.5 text-xs font-semibold">
309
- {{ isSearching ? t("panel.material.searchResults", { count: matchedItems.length }) : activeCategory.title }}
310
- </div>
311
- <div class="grid gap-2 px-2.5 pb-3">
312
- <button
313
- v-for="it in isSearching ? matchedItems : activeCategory.items"
314
- :key="isSearching ? `${it.id}-${(it as any).categoryTitle}` : it.id"
315
- type="button"
316
- draggable="true"
317
- class="cursor-pointer rounded-xl border border-border bg-card px-2 py-2 text-left text-xs text-card-foreground hover:bg-accent/60"
318
- @dragstart="onMaterialDragStart($event, it)"
326
+ <Select.Option
327
+ v-for="category in categories"
328
+ :key="category.id"
329
+ :value="category.id"
319
330
  >
320
- <div class="flex flex-col items-stretch gap-2">
321
- <MaterialPreview :id="it.id" />
322
- <div class="min-w-0">
323
- <div class="truncate">{{ it.title }}</div>
324
- <div
325
- v-if="isSearching"
326
- class="mt-1 text-[11px] text-muted-foreground"
327
- >
328
- {{ (it as any).categoryTitle }}
331
+ <span class="inline-flex w-full min-w-0 items-center justify-between gap-2">
332
+ <span class="truncate font-medium">{{ category.title }}</span>
333
+ <span class="tabular-nums text-[11px] font-normal text-muted-foreground">
334
+ {{ category.items.length }}
335
+ </span>
336
+ </span>
337
+ </Select.Option>
338
+ </Select>
339
+ </div>
340
+ <div :class="`min-h-0 flex-1 overflow-auto ${themedScrollbarClass}`">
341
+ <div class="px-2 pb-2.5 pt-1.5">
342
+ <div class="mb-1.5 flex items-baseline justify-between gap-2 px-0.5">
343
+ <div class="truncate text-[12px] font-semibold text-foreground/90">
344
+ <template v-if="isSearching">
345
+ {{ t("panel.material.searchResults", { count: matchedItems.length }) }}
346
+ </template>
347
+ <template v-else>{{ activeCategory?.title }}</template>
348
+ </div>
349
+ <div
350
+ v-if="!isSearching"
351
+ class="tabular-nums text-[11px] text-muted-foreground"
352
+ >
353
+ {{ activeCategory?.items.length ?? 0 }}
354
+ </div>
355
+ </div>
356
+ <div class="rv-material-grid">
357
+ <button
358
+ v-for="it in displayItems"
359
+ :key="isSearching ? `${it.id}-${(it as any).categoryTitle}` : it.id"
360
+ type="button"
361
+ draggable="true"
362
+ class="rv-material-card cursor-pointer rounded-lg border border-border/50 bg-card px-1.5 py-1.5 text-left text-card-foreground shadow-[0_1px_1px_hsl(var(--foreground)/0.03)] transition-colors hover:border-border hover:bg-accent/40"
363
+ @dragstart="onMaterialDragStart($event, it)"
364
+ >
365
+ <div class="flex flex-col gap-1">
366
+ <MaterialPreview :id="it.id" />
367
+ <div class="min-w-0 px-0.5">
368
+ <div class="truncate text-center text-[11px] font-normal text-muted-foreground">
369
+ {{ it.title }}
370
+ </div>
371
+ <div
372
+ v-if="isSearching"
373
+ class="mt-0.5 truncate text-center text-[10px] text-muted-foreground"
374
+ >
375
+ {{ (it as any).categoryTitle }}
376
+ </div>
329
377
  </div>
330
378
  </div>
379
+ </button>
380
+ <Empty
381
+ v-if="isSearching && matchedItems.length === 0"
382
+ class="py-5"
383
+ :description="t('panel.material.emptyMaterialsTitle')"
384
+ >
385
+ <template #description>
386
+ <span class="text-[12px] font-medium">{{ t("panel.material.emptyMaterialsTitle") }}</span>
387
+ <div class="text-[11px] text-muted-foreground">
388
+ {{ t("panel.material.emptyMaterialsDesc") }}
389
+ </div>
390
+ </template>
391
+ </Empty>
392
+ <div
393
+ v-else-if="!isSearching && (activeCategory?.items.length ?? 0) === 0"
394
+ class="px-1 py-3 text-[11px] text-muted-foreground"
395
+ >
396
+ {{ t("panel.material.emptyMaterialsTitle") }}
331
397
  </div>
332
- </button>
333
- <Empty
334
- v-if="isSearching && matchedItems.length === 0"
335
- class="py-5"
336
- :description="t('panel.material.emptyMaterialsTitle')"
337
- >
338
- <template #description>
339
- <span class="text-xs">{{ t("panel.material.emptyMaterialsTitle") }}</span>
340
- <div class="text-[11px] text-muted-foreground">
341
- {{ t("panel.material.emptyMaterialsDesc") }}
342
- </div>
343
- </template>
344
- </Empty>
398
+ </div>
345
399
  </div>
346
400
  </div>
347
401
  </div>
348
402
  </Tabs.TabPane>
349
403
 
350
404
  <Tabs.TabPane key="tree" :tab="t('panel.material.tabTree')" class="min-h-0 flex-1">
351
- <div :class="`h-full overflow-auto px-2 py-2 text-xs ${themedScrollbarClass}`">
352
- <div class="mb-2">
353
- <Input
354
- v-model:value="treeKeyword"
355
- size="small"
356
- :placeholder="t('panel.material.treeSearchPlaceholder')"
357
- class="text-xs"
358
- />
359
- </div>
360
- <div class="mb-2 flex items-center justify-between rounded border border-border bg-card px-2 py-1.5">
361
- <span class="text-[11px] text-muted-foreground">{{ t("panel.material.referenceOnlyTree") }}</span>
362
- <Switch v-model:checked="referenceOnlyTree" size="small" :aria-label="t('panel.material.referenceOnlyTree')" />
363
- </div>
364
-
365
- <div class="rounded border border-border bg-card">
366
- <div class="flex items-center gap-1 px-2 py-1.5 font-medium">
405
+ <div :class="`h-full overflow-auto px-1.5 py-1.5 text-xs ${themedScrollbarClass}`">
406
+ <div class="flex items-center gap-0.5 rounded-sm px-0.5 py-0.5 font-medium hover:bg-accent/40">
407
+ <button
408
+ type="button"
409
+ class="flex h-5 w-4 shrink-0 items-center justify-center rounded text-[11px] text-muted-foreground hover:bg-accent"
410
+ @click="setExpanded('root', !isExpanded('root', true))"
411
+ >
412
+ {{ isExpanded("root", true) ? "▾" : "▸" }}
413
+ </button>
414
+ <span class="min-w-0 flex-1 truncate text-[12px]">root</span>
415
+ <Tooltip :title="t('panel.material.referenceOnlyTree')">
367
416
  <button
368
417
  type="button"
369
- class="flex h-7 w-7 items-center justify-center rounded text-sm hover:bg-accent"
370
- @click="setExpanded('root', !isExpanded('root', true))"
418
+ class="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md border transition-colors"
419
+ :class="
420
+ referenceOnlyTree
421
+ ? 'border-primary/50 bg-primary/15 text-primary'
422
+ : 'border-border/70 bg-background/80 text-muted-foreground hover:bg-accent hover:text-accent-foreground'
423
+ "
424
+ :aria-label="t('panel.material.referenceOnlyTree')"
425
+ :aria-pressed="referenceOnlyTree"
426
+ @click="referenceOnlyTree = !referenceOnlyTree"
371
427
  >
372
- {{ isExpanded("root", true) ? "▾" : "▸" }}
428
+ <svg
429
+ viewBox="0 0 24 24"
430
+ class="h-3.5 w-3.5"
431
+ fill="none"
432
+ stroke="currentColor"
433
+ :stroke-width="referenceOnlyTree ? 2.2 : 2"
434
+ >
435
+ <path
436
+ d="M4 5h16l-5.5 6.5V18l-5 2.5v-9L4 5Z"
437
+ :fill="referenceOnlyTree ? 'currentColor' : 'none'"
438
+ :fill-opacity="referenceOnlyTree ? 0.2 : 0"
439
+ />
440
+ <circle cx="16.5" cy="16.5" r="2.2" />
441
+ <path d="M14.8 15.2 12 12.5" />
442
+ </svg>
373
443
  </button>
374
- <span>root</span>
375
- </div>
444
+ </Tooltip>
445
+ </div>
376
446
 
377
- <div v-show="isExpanded('root', true)" class="space-y-1 border-t border-border/60 py-1">
378
- <template v-for="layer in layers" :key="layer.id">
379
- <div v-if="!(isTreeSearching && getRootNodes(layer.id).length === 0)">
380
- <Card
381
- :class="[
382
- 'mb-2 overflow-hidden transition-shadow',
383
- layer.isMapping
384
- ? 'border-2 border-violet-500/70 bg-gradient-to-br from-violet-500/14 via-violet-600/10 to-fuchsia-500/12'
385
- : '',
386
- dragOverLayerId === layer.id
387
- ? getLayerDropState(layer).canDropIntoLayer
388
- ? 'ring-2 ring-primary/45 ring-offset-2 ring-offset-background'
389
- : 'ring-2 ring-destructive/45 ring-offset-2 ring-offset-background'
390
- : '',
391
- ]"
392
- size="small"
393
- :body-style="{ padding: '10px' }"
447
+ <div v-show="isExpanded('root', true)" class="ml-[7px] border-l border-border/45 pl-1.5">
448
+ <template v-for="layer in layers" :key="layer.id">
449
+ <div v-if="!(isTreeSearching && getRootNodes(layer.id).length === 0)">
450
+ <div
451
+ :class="[
452
+ 'flex min-w-0 items-center gap-0.5 rounded-sm py-0.5 pr-0.5 transition-colors',
453
+ layer.isMapping ? 'text-violet-800 dark:text-violet-200' : 'text-foreground',
454
+ dragOverLayerId === layer.id
455
+ ? getLayerDropState(layer).canDropIntoLayer
456
+ ? 'bg-primary/12 ring-1 ring-inset ring-primary/35'
457
+ : 'bg-destructive/10 ring-1 ring-inset ring-destructive/35'
458
+ : 'hover:bg-accent/50',
459
+ ]"
460
+ :title="t('panel.material.dragToThisLayer')"
461
+ @dragover="onLayerDragOver($event, layer)"
462
+ @dragleave="dragOverLayerId === layer.id && (dragOverLayerId = null)"
463
+ @drop="onLayerDrop($event, layer)"
464
+ >
465
+ <button
466
+ type="button"
467
+ class="flex h-5 w-4 shrink-0 items-center justify-center rounded text-[11px] text-muted-foreground hover:bg-accent"
468
+ @click="setExpanded(`layer:${layer.id}`, !isExpanded(`layer:${layer.id}`, true))"
394
469
  >
395
- <div class="mb-1.5 flex flex-wrap items-center gap-1">
396
- <span
397
- v-if="dragOverLayerId === layer.id"
398
- :class="[
399
- 'inline-flex h-5 w-5 shrink-0 items-center justify-center rounded text-[11px]',
400
- getLayerDropState(layer).canDropIntoLayer
401
- ? 'bg-primary/15 text-primary'
402
- : 'bg-destructive/15 text-destructive',
403
- ]"
404
- :title="
405
- getLayerDropState(layer).canDropIntoLayer
406
- ? t('panel.material.targetLayer')
407
- : getLayerDropState(layer).dropBlockReason
408
- "
409
- >
410
- ➜
411
- </span>
412
- <button
413
- type="button"
414
- class="flex h-7 w-7 shrink-0 items-center justify-center rounded text-sm text-muted-foreground hover:bg-accent"
415
- @click="
416
- setExpanded(`layer:${layer.id}`, !isExpanded(`layer:${layer.id}`, true))
417
- "
418
- >
419
- {{
420
- isExpanded(`layer:${layer.id}`, true) ? "▾" : "▸"
421
- }}
422
- </button>
423
- <span class="min-w-0 flex-1 truncate text-xs font-medium text-foreground">
424
- {{ layer.name }}
425
- <span class="font-normal text-muted-foreground">
426
- ({{ getRootNodes(layer.id).length }})
427
- </span>
428
- </span>
429
- <span
430
- v-if="layer.isMapping"
431
- class="shrink-0 rounded-md border-2 border-violet-600 bg-violet-500/22 px-1.5 py-0.5 text-[10px] font-semibold text-violet-950 dark:border-violet-400 dark:bg-violet-500/35 dark:text-violet-50"
432
- >
433
- {{ t("panel.material.mappingLayer") }}
434
- </span>
435
- <span
436
- v-if="dragOverLayerId === layer.id"
437
- :class="[
438
- 'ml-auto shrink-0 rounded px-1.5 py-0.5 text-[10px]',
439
- getLayerDropState(layer).canDropIntoLayer
440
- ? 'bg-primary/15 text-primary'
441
- : 'bg-destructive/15 text-destructive',
442
- ]"
443
- >
444
- {{
445
- getLayerDropState(layer).canDropIntoLayer
446
- ? t("panel.material.willMoveToLayer")
447
- : getLayerDropState(layer).dropBlockReason
448
- }}
449
- </span>
450
- </div>
451
-
452
- <div
453
- :class="[
454
- 'mb-2 rounded-md border border-dashed px-2 py-1.5 text-[10px] transition-colors',
455
- dragOverLayerId === layer.id
456
- ? getLayerDropState(layer).canDropIntoLayer
457
- ? 'border-primary/50 bg-primary/10 text-primary'
458
- : 'border-destructive/50 bg-destructive/10 text-destructive'
459
- : 'border-border/50 bg-muted/20 text-muted-foreground hover:border-border hover:bg-muted/35',
460
- ]"
461
- :title="t('panel.material.dragToThisLayer')"
462
- @dragover="onLayerDragOver($event, layer)"
463
- @dragleave="dragOverLayerId === layer.id && (dragOverLayerId = null)"
464
- @drop="onLayerDrop($event, layer)"
470
+ {{ isExpanded(`layer:${layer.id}`, true) ? "▾" : "▸" }}
471
+ </button>
472
+ <svg
473
+ viewBox="0 0 24 24"
474
+ class="h-3.5 w-3.5 shrink-0"
475
+ fill="none"
476
+ stroke="currentColor"
477
+ stroke-width="2"
478
+ >
479
+ <path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" />
480
+ </svg>
481
+ <div class="flex min-w-0 flex-1 items-center gap-0.5">
482
+ <span class="min-w-0 truncate text-[12px] font-medium">
483
+ {{ layer.name }}
484
+ </span>
485
+ <span
486
+ v-if="layer.isMapping"
487
+ class="shrink-0 rounded border border-violet-500/60 bg-violet-500/15 px-1 text-[10px] font-medium"
465
488
  >
466
- {{
467
- draggingTreeNodeId
468
- ? getLayerDropState(layer).canDropIntoLayer
469
- ? t("panel.material.releaseToMove")
470
- : getLayerDropState(layer).dropBlockReason || t("panel.material.cannotMoveToLayer")
471
- : t("panel.material.dragToLayer")
472
- }}
473
- </div>
474
-
475
- <div v-show="isExpanded(`layer:${layer.id}`, true)">
476
- <div
477
- v-if="getRootNodes(layer.id).length === 0"
478
- class="rounded border border-border/40 bg-muted/15 py-2 pl-3 text-[11px] text-muted-foreground"
479
- >
480
- {{ t("panel.material.emptyLayer") }}
481
- </div>
482
- <MaterialSidebarTreeNode
483
- v-for="node in getRootNodes(layer.id)"
484
- :key="node.id"
485
- :node="node"
486
- :level="2"
487
- :path="`${layer.id}/${node.id}`"
488
- :visited="new Set()"
489
- :selected-ids="selectedIds"
490
- :layer-by-id="layerById"
491
- :elements-by-layer="elementsByLayer"
492
- :children-by-grid-by-layer="childrenByGridByLayer"
493
- :expanded-keys="expandedKeys"
494
- :normalized-tree-keyword="normalizedTreeKeyword"
495
- :is-tree-searching="isTreeSearching"
496
- :dragging-tree-node-id="draggingTreeNodeId"
497
- @toggle-expanded="setExpanded"
498
- @select-node="(id, lid) => onSelectNode?.(id, lid)"
499
- @node-context-menu="(p) => onNodeContextMenu?.(p)"
500
- @delete-node="(id) => onDeleteNode?.(id)"
501
- @copy-node="(id, mode) => onCopyNode?.(id, mode)"
502
- @drag-start="(id) => (draggingTreeNodeId = id)"
503
- @drag-end="
504
- draggingTreeNodeId = null;
505
- dragOverLayerId = null;
506
- "
507
- />
508
- </div>
509
- </Card>
489
+ {{ t("panel.material.mappingLayer") }}
490
+ </span>
491
+ </div>
492
+ <span class="ml-auto shrink-0 pl-1 tabular-nums text-[11px] text-muted-foreground">
493
+ {{ getRootNodes(layer.id).length }}
494
+ </span>
510
495
  </div>
511
- </template>
512
496
 
513
- <Empty v-if="treeSearchEmpty" class="mx-2 my-2 py-5" :description="t('panel.material.emptyNodesTitle')">
514
- <template #description>
515
- <span class="text-xs">{{ t("panel.material.emptyNodesTitle") }}</span>
516
- <div class="text-[11px] text-muted-foreground">
517
- {{ t("panel.material.emptyNodesDesc") }}
497
+ <div
498
+ v-show="isExpanded(`layer:${layer.id}`, true)"
499
+ class="ml-[7px] border-l border-border/45 pl-1.5"
500
+ >
501
+ <div
502
+ v-if="getRootNodes(layer.id).length === 0"
503
+ class="py-1 pl-1 text-[11px] text-muted-foreground"
504
+ >
505
+ {{ t("panel.material.emptyLayer") }}
518
506
  </div>
519
- </template>
520
- </Empty>
521
- </div>
507
+ <MaterialSidebarTreeNode
508
+ v-for="node in getRootNodes(layer.id)"
509
+ :key="node.id"
510
+ :node="node"
511
+ :level="0"
512
+ :path="`${layer.id}/${node.id}`"
513
+ :visited="new Set()"
514
+ :selected-ids="selectedIds"
515
+ :layer-by-id="layerById"
516
+ :elements-by-layer="elementsByLayer"
517
+ :children-by-grid-by-layer="childrenByGridByLayer"
518
+ :expanded-keys="expandedKeys"
519
+ :normalized-tree-keyword="normalizedTreeKeyword"
520
+ :is-tree-searching="isTreeSearching"
521
+ :dragging-tree-node-id="draggingTreeNodeId"
522
+ @toggle-expanded="setExpanded"
523
+ @select-node="(id, lid) => onSelectNode?.(id, lid)"
524
+ @node-context-menu="(p) => onNodeContextMenu?.(p)"
525
+ @delete-node="(id) => onDeleteNode?.(id)"
526
+ @copy-node="(id, mode) => onCopyNode?.(id, mode)"
527
+ @drag-start="(id) => (draggingTreeNodeId = id)"
528
+ @drag-end="
529
+ draggingTreeNodeId = null;
530
+ dragOverLayerId = null;
531
+ "
532
+ />
533
+ </div>
534
+ </div>
535
+ </template>
536
+
537
+ <Empty v-if="treeSearchEmpty" class="my-2 py-5" :description="t('panel.material.emptyNodesTitle')">
538
+ <template #description>
539
+ <span class="text-xs">{{ t("panel.material.emptyNodesTitle") }}</span>
540
+ <div class="text-[11px] text-muted-foreground">
541
+ {{ t("panel.material.emptyNodesDesc") }}
542
+ </div>
543
+ </template>
544
+ </Empty>
522
545
  </div>
523
546
  </div>
524
547
  </Tabs.TabPane>
525
548
  </Tabs>
526
549
  </aside>
527
550
  </template>
551
+
552
+ <style scoped>
553
+ :deep(.ant-tabs) {
554
+ display: flex;
555
+ min-height: 0;
556
+ height: 100%;
557
+ flex-direction: column;
558
+ }
559
+ :deep(.ant-tabs-nav) {
560
+ margin: 0;
561
+ padding: 0.5rem 0.5rem 0;
562
+ flex-shrink: 0;
563
+ }
564
+ :deep(.ant-tabs-content-holder),
565
+ :deep(.ant-tabs-content) {
566
+ display: flex;
567
+ min-height: 0;
568
+ height: 100%;
569
+ flex: 1 1 auto;
570
+ flex-direction: column;
571
+ overflow: hidden;
572
+ }
573
+ /* 勿给未激活 pane 设 display:flex,否则会盖掉 antd 的 display:none 留下大块空白 */
574
+ :deep(.ant-tabs-tabpane-active) {
575
+ display: flex !important;
576
+ min-height: 0;
577
+ height: 100%;
578
+ flex: 1 1 auto;
579
+ flex-direction: column;
580
+ overflow: hidden;
581
+ }
582
+ :deep(.ant-select-selector) {
583
+ white-space: nowrap !important;
584
+ }
585
+ :deep(.ant-select-selection-item) {
586
+ overflow: hidden;
587
+ text-overflow: ellipsis;
588
+ white-space: nowrap !important;
589
+ }
590
+ </style>