Files
pikasTech-HWLAB/web/hwlab-cloud-web/src/components/common/ResourceCollection.vue
T

142 lines
4.9 KiB
Vue

<!-- SPEC: PJ2026-010405 云端控制台. -->
<!-- Implementation reference: draft-2026-07-13-p0-cloud-console. -->
<!-- Responsibility: list/listitem 语义承载同一资源合同的卡片与列表视图 -->
<script setup lang="ts" generic="T extends object">
import type { Component } from "vue";
import { Grid2X2, List, Rows3, Rows4 } from "lucide-vue-next";
import EntityCard from "./EntityCard.vue";
import VirtualList from "./VirtualList.vue";
import ViewModeSwitch from "@/components/console/ViewModeSwitch.vue";
const viewOptions = [
{ value: "cards", label: "卡片", icon: Grid2X2 },
{ value: "list", label: "列表", icon: List }
];
const densityOptions = [
{ value: "comfortable", label: "舒适密度", icon: Rows4 },
{ value: "compact", label: "紧凑密度", icon: Rows3 }
];
withDefaults(defineProps<{
items: T[];
itemKey: (item: T) => string;
itemTitle?: (item: T) => string;
itemSubtitle?: (item: T) => string;
itemStatus?: (item: T) => string;
itemIcon?: (item: T) => Component;
view?: "cards" | "list";
density?: "compact" | "comfortable";
selectedId?: string | null;
ariaLabel?: string;
emptyText?: string;
showControls?: boolean;
virtualizeAfter?: number;
virtualItemHeight?: number;
virtualHeight?: number | string;
}>(), {
itemTitle: undefined,
itemSubtitle: undefined,
itemStatus: undefined,
itemIcon: undefined,
view: "cards",
density: "comfortable",
selectedId: null,
ariaLabel: "资源集合",
emptyText: "暂无资源。",
showControls: true,
virtualizeAfter: 80,
virtualItemHeight: 56,
virtualHeight: "min(62vh, 680px)"
});
const emitEvent = defineEmits<{
select: [item: T];
"update:view": [view: "cards" | "list"];
"update:density": [density: "compact" | "comfortable"];
}>();
function selectItem(item: T): void {
emitEvent("select", item);
}
</script>
<template>
<section class="resource-collection" :aria-label="ariaLabel">
<header v-if="showControls || $slots.toolbar" class="resource-collection-toolbar">
<slot name="toolbar"><span class="resource-collection-count">{{ items.length }} items</span></slot>
<div v-if="showControls" class="page-command-actions">
<ViewModeSwitch :model-value="view" :options="viewOptions" label="显示模式" @update:model-value="emitEvent('update:view', $event as 'cards' | 'list')" />
<ViewModeSwitch :model-value="density" :options="densityOptions" label="显示密度" @update:model-value="emitEvent('update:density', $event as 'compact' | 'comfortable')" />
</div>
</header>
<VirtualList
v-if="items.length >= virtualizeAfter && view === 'list'"
class="resource-collection-virtual"
:items="items"
:item-key="itemKey"
:item-height="virtualItemHeight"
:height="virtualHeight"
:aria-label="ariaLabel"
>
<template #default="{ item }">
<div class="resource-collection-item resource-collection-item--virtual">
<slot
v-if="$slots.list"
name="list"
:item="item"
:selected="itemKey(item) === selectedId"
:select="() => selectItem(item)"
/>
<EntityCard
v-else
:title="itemTitle?.(item) || itemKey(item)"
:subtitle="itemSubtitle?.(item)"
:status="itemStatus?.(item)"
:icon="itemIcon?.(item)"
:selected="itemKey(item) === selectedId"
@select="selectItem(item)"
/>
</div>
</template>
</VirtualList>
<div v-else-if="items.length" class="resource-collection-items" :data-view="view" :data-density="density" role="list" :aria-label="ariaLabel">
<div v-for="item in items" :key="itemKey(item)" class="resource-collection-item" role="listitem">
<slot
v-if="view === 'list' && $slots.list"
name="list"
:item="item"
:selected="itemKey(item) === selectedId"
:select="() => selectItem(item)"
/>
<EntityCard
v-else-if="view === 'list'"
:title="itemTitle?.(item) || itemKey(item)"
:subtitle="itemSubtitle?.(item)"
:status="itemStatus?.(item)"
:icon="itemIcon?.(item)"
:selected="itemKey(item) === selectedId"
@select="selectItem(item)"
/>
<slot
v-else-if="$slots.card"
name="card"
:item="item"
:selected="itemKey(item) === selectedId"
:select="() => selectItem(item)"
/>
<EntityCard
v-else
:title="itemTitle?.(item) || itemKey(item)"
:subtitle="itemSubtitle?.(item)"
:status="itemStatus?.(item)"
:icon="itemIcon?.(item)"
:selected="itemKey(item) === selectedId"
@select="selectItem(item)"
/>
</div>
</div>
<slot v-else name="empty"><p class="muted-box">{{ emptyText }}</p></slot>
</section>
</template>