142 lines
4.9 KiB
Vue
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>
|