232 lines
5.8 KiB
Vue
232 lines
5.8 KiB
Vue
<template>
|
|
<div class="card-list-section">
|
|
<div class="section-header">
|
|
<h3 class="section-title">{{ title }}</h3>
|
|
<el-button
|
|
v-if="showViewAll"
|
|
link
|
|
type="primary"
|
|
class="section-link"
|
|
@click="$emit('view-all')"
|
|
>
|
|
{{ viewAllLabel }}
|
|
</el-button>
|
|
</div>
|
|
<div class="section-content">
|
|
<el-empty v-if="showEmpty" :description="emptyText" />
|
|
<el-card
|
|
v-for="item in items"
|
|
:key="resolveItemKey(item)"
|
|
class="section-item"
|
|
>
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span class="item-name">{{ resolveItemTitle(item) }}</span>
|
|
<AppTag v-if="resolveTag(item)" :type="resolveTag(item).type" size="small">
|
|
{{ resolveTag(item).label }}
|
|
</AppTag>
|
|
</div>
|
|
</template>
|
|
|
|
<p v-if="resolveItemDescription(item) || fallbackDescription" class="item-description">
|
|
{{ resolveItemDescription(item) || fallbackDescription }}
|
|
</p>
|
|
|
|
<div v-if="resolveMetaRows(item).length > 0" class="item-meta">
|
|
<div
|
|
v-for="(row, index) in resolveMetaRows(item)"
|
|
:key="`${row.label}-${index}`"
|
|
class="meta-item"
|
|
>
|
|
<span class="meta-label">{{ row.label }}:</span>
|
|
<span class="meta-value">{{ row.value }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="showActions" class="item-actions">
|
|
<el-button
|
|
v-if="secondaryActionLabel"
|
|
size="small"
|
|
@click="$emit('secondary-action', item)"
|
|
>
|
|
{{ secondaryActionLabel }}
|
|
</el-button>
|
|
<el-button v-if="actionLabel" size="small" type="primary" @click="$emit('open', item)">
|
|
{{ actionLabel }}
|
|
</el-button>
|
|
</div>
|
|
</el-card>
|
|
|
|
<div v-if="showLoadMore" class="load-more">
|
|
<el-button :loading="loading" plain @click="$emit('load-more')">
|
|
{{ loading ? loadingLabel : loadMoreLabel }}
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue';
|
|
import AppTag from '@/components/base/AppTag.vue';
|
|
|
|
const props = defineProps({
|
|
title: { type: String, default: '' },
|
|
items: { type: Array, default: () => [] },
|
|
emptyText: { type: String, default: '' },
|
|
fallbackDescription: { type: String, default: '' },
|
|
actionLabel: { type: String, default: '' },
|
|
secondaryActionLabel: { type: String, default: '' },
|
|
tagType: { type: String, default: '' },
|
|
tagLabel: { type: String, default: '' },
|
|
tagMapper: { type: Function, default: null },
|
|
metaMapper: { type: Function, default: null },
|
|
showViewAll: { type: Boolean, default: false },
|
|
viewAllLabel: { type: String, default: '' },
|
|
showLoadMore: { type: Boolean, default: false },
|
|
loading: { type: Boolean, default: false },
|
|
loadMoreLabel: { type: String, default: '' },
|
|
loadingLabel: { type: String, default: '' },
|
|
itemKeyMapper: { type: Function, default: null },
|
|
itemTitleMapper: { type: Function, default: null },
|
|
itemDescriptionMapper: { type: Function, default: null }
|
|
});
|
|
|
|
defineEmits(['open', 'secondary-action', 'load-more', 'view-all']);
|
|
|
|
const showEmpty = computed(() => props.items.length === 0 && props.emptyText);
|
|
const showActions = computed(() => Boolean(props.actionLabel || props.secondaryActionLabel));
|
|
|
|
const resolveItemKey = (item) => {
|
|
if (props.itemKeyMapper) return props.itemKeyMapper(item);
|
|
return item?.externalId || item?.id || item?.external_id || item?.name;
|
|
};
|
|
|
|
const resolveItemTitle = (item) => {
|
|
if (props.itemTitleMapper) return props.itemTitleMapper(item);
|
|
return item?.name || '';
|
|
};
|
|
|
|
const resolveItemDescription = (item) => {
|
|
if (props.itemDescriptionMapper) return props.itemDescriptionMapper(item);
|
|
return item?.description || '';
|
|
};
|
|
|
|
const resolveTag = (item) => {
|
|
if (props.tagMapper) return props.tagMapper(item);
|
|
if (props.tagType && props.tagLabel) {
|
|
return { type: props.tagType, label: props.tagLabel };
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const resolveMetaRows = (item) => {
|
|
if (!props.metaMapper) return [];
|
|
return props.metaMapper(item) || [];
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.card-list-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
background-color: var(--bg-white);
|
|
border-radius: var(--border-radius-md);
|
|
box-shadow: var(--shadow-sm);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: var(--spacing-md);
|
|
border-bottom: 1px solid var(--border-color);
|
|
background-color: var(--bg-white);
|
|
}
|
|
|
|
.section-title {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.section-link {
|
|
padding: 0;
|
|
}
|
|
|
|
.section-content {
|
|
padding: var(--spacing-md);
|
|
}
|
|
|
|
.section-item {
|
|
margin-bottom: var(--spacing-md);
|
|
transition: box-shadow 0.3s ease;
|
|
}
|
|
|
|
.section-item:hover {
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.item-name {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--text-dark);
|
|
}
|
|
|
|
.item-description {
|
|
margin: var(--spacing-sm) 0;
|
|
color: var(--text-medium);
|
|
}
|
|
|
|
.item-meta {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-xs);
|
|
margin-top: var(--spacing-sm);
|
|
}
|
|
|
|
.meta-item {
|
|
display: flex;
|
|
gap: var(--spacing-xs);
|
|
font-size: 12px;
|
|
color: var(--text-light);
|
|
}
|
|
|
|
.meta-label {
|
|
color: var(--text-medium);
|
|
}
|
|
|
|
.item-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: var(--spacing-sm);
|
|
}
|
|
|
|
.load-more {
|
|
text-align: center;
|
|
margin-top: var(--spacing-md);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.section-header {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: var(--spacing-md);
|
|
}
|
|
}
|
|
|
|
.dark-mode .section-item {
|
|
background-color: var(--bg-white);
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
</style>
|