优化 TorrentCard 组件的样式和结构,更新优惠标签和媒体信息的显示方式,增强用户体验。添加新的优惠标签类以支持不同的优惠类型,并改进了更多来源对话框的布局和交互效果。

This commit is contained in:
jxxghp
2025-04-18 17:41:20 +08:00
parent 229264f2d0
commit a484fc2d39
+206 -426
View File
@@ -81,10 +81,19 @@ async function downloadTorrentFile() {
// 获取优惠类型样式 // 获取优惠类型样式
function getPromotionClass(downloadVolumeFactor: number | undefined, uploadVolumeFactor: number | undefined) { function getPromotionClass(downloadVolumeFactor: number | undefined, uploadVolumeFactor: number | undefined) {
if (!downloadVolumeFactor) return 'free-discount' if (!downloadVolumeFactor) return 'bg-success'
if (downloadVolumeFactor === 0) return 'free-discount' if (downloadVolumeFactor === 0) return 'bg-success'
else if (downloadVolumeFactor < 1) return 'percent-discount' else if (downloadVolumeFactor < 1) return 'bg-orange'
else if (uploadVolumeFactor !== undefined && uploadVolumeFactor > 1) return 'upload-bonus' else if (uploadVolumeFactor !== undefined && uploadVolumeFactor > 1) return 'bg-purple'
else return ''
}
// 获取优惠标签类
function getPromotionChipClass(downloadVolumeFactor: number | undefined, uploadVolumeFactor: number | undefined) {
if (!downloadVolumeFactor) return 'chip-free'
if (downloadVolumeFactor === 0) return 'chip-free'
else if (downloadVolumeFactor < 1) return 'chip-discount'
else if (uploadVolumeFactor !== undefined && uploadVolumeFactor > 1) return 'chip-bonus'
else return '' else return ''
} }
@@ -108,109 +117,147 @@ onMounted(() => {
:width="props.width || '100%'" :width="props.width || '100%'"
:variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'flat'" :variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'flat'"
@click="handleAddDownload(props.torrent)" @click="handleAddDownload(props.torrent)"
class="torrent-card h-full" class="h-full cursor-pointer transition-transform hover:-translate-y-1 duration-300 d-flex flex-column overflow-hidden torrent-card"
:class="{ 'downloaded-card': downloaded.includes(torrent?.enclosure || '') }" :class="{ 'border-success border-2 opacity-85': downloaded.includes(torrent?.enclosure || '') }"
hover
> >
<!-- 优惠标签 --> <!-- 优惠标签 -->
<div <div
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1" v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
class="discount-banner" class="discount-banner text-white px-2 py-1 text-sm font-weight-bold rounded-bl-lg"
:class="getPromotionClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)" :class="getPromotionClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)"
> >
{{ torrent?.volume_factor }} {{ torrent?.volume_factor }}
</div> </div>
<!-- 媒体标题 --> <!-- 媒体标题 -->
<div class="card-header"> <VCardItem class="pt-3 pb-0">
<div class="media-title-wrapper flex flex-row flex-wrap justify-start"> <div class="d-flex flex-row flex-wrap justify-start mb-2 pr-8">
<span class="media-title me-2"> <span class="text-h6 font-weight-bold text-truncate me-2">
{{ media?.title ?? meta?.name }} {{ media?.title ?? meta?.name }}
</span> </span>
<span v-if="meta?.season_episode" class="season-tag">{{ meta?.season_episode }}</span> <VChip v-if="meta?.season_episode" class="chip-season rounded-sm font-weight-bold" variant="elevated">
{{ meta?.season_episode }}
</VChip>
</div> </div>
<!-- 站点信息条 --> <!-- 站点信息条 -->
<div class="site-info"> <div class="d-flex justify-space-between align-center mb-3 flex-wrap">
<div class="d-flex align-center"> <div class="d-flex align-center">
<img <img
:alt="torrent?.site_name"
v-if="siteIcons[torrent?.site || 0]" v-if="siteIcons[torrent?.site || 0]"
:src="siteIcons[torrent?.site || 0]" :src="siteIcons[torrent?.site || 0]"
class="site-icon" :alt="torrent?.site_name"
class="mr-2 rounded"
width="20"
height="20"
/> />
<span v-else class="site-fallback">{{ torrent?.site_name?.substring(0, 1) }}</span> <VAvatar v-else size="20" class="mr-2 text-caption bg-surface-variant" color="surface-variant">
<span class="site-name">{{ torrent?.site_name }}</span> {{ torrent?.site_name?.substring(0, 1) }}
</VAvatar>
<span class="font-weight-bold text-body-2">{{ torrent?.site_name }}</span>
</div> </div>
<div class="seeder-peers"> <div class="d-flex align-center gap-3">
<span v-if="torrent?.seeders" class="seed-info"> <span v-if="torrent?.seeders" class="d-flex align-center font-weight-bold">
<VIcon size="small" color="success" icon="mdi-arrow-up"></VIcon>{{ torrent?.seeders }} <VIcon size="small" color="success" icon="mdi-arrow-up" class="mr-1"></VIcon>
{{ torrent?.seeders }}
</span> </span>
<span v-if="torrent?.peers" class="peer-info"> <span v-if="torrent?.peers" class="d-flex align-center font-weight-bold">
<VIcon size="small" color="warning" icon="mdi-arrow-down"></VIcon>{{ torrent?.peers }} <VIcon size="small" color="warning" icon="mdi-arrow-down" class="mr-1"></VIcon>
{{ torrent?.peers }}
</span> </span>
</div> </div>
</div> </div>
</div> </VCardItem>
<!-- 种子内容 --> <!-- 种子内容 -->
<div class="card-content"> <VCardText class="d-flex flex-column flex-grow-1 pa-3 overflow-hidden">
<!-- 种子标题 --> <!-- 种子标题 -->
<div class="torrent-title" :title="torrent?.title"> <div class="text-subtitle-2 font-weight-medium mb-2" :title="torrent?.title">
{{ torrent?.title }} {{ torrent?.title }}
</div> </div>
<!-- 种子描述 --> <!-- 种子描述 -->
<div <div
v-if="meta?.subtitle || torrent?.description" v-if="meta?.subtitle || torrent?.description"
class="torrent-desc grow" class="text-body-2 text-medium-emphasis mb-2"
:title="meta?.subtitle || torrent?.description" :title="meta?.subtitle || torrent?.description"
> >
{{ meta?.subtitle || torrent?.description }} {{ meta?.subtitle || torrent?.description }}
</div> </div>
<!-- 资源标签区 --> <!-- 资源标签区 -->
<div class="tags-container"> <div class="d-flex flex-wrap gap-1 mb-2">
<div v-if="meta?.edition" class="resource-tag edition">{{ meta?.edition }}</div> <!-- 版本标签 -->
<div v-if="meta?.resource_pix" class="resource-tag resolution">{{ meta?.resource_pix }}</div> <VChip v-if="meta?.edition" class="chip-edition rounded-sm" size="small" variant="elevated">
<div v-if="meta?.video_encode" class="resource-tag codec">{{ meta?.video_encode }}</div> {{ meta?.edition }}
<div v-if="meta?.resource_team" class="resource-tag team">{{ meta?.resource_team }}</div> </VChip>
<div v-for="(label, index) in torrent?.labels" :key="index" class="resource-tag label">{{ label }}</div>
<div v-if="torrent?.hit_and_run" class="resource-tag hr">H&R</div> <!-- 分辨率标签 -->
<div v-if="torrent?.freedate_diff" class="resource-tag expire">{{ torrent?.freedate_diff }}</div> <VChip v-if="meta?.resource_pix" class="chip-resolution rounded-sm" size="small" variant="elevated">
{{ meta?.resource_pix }}
</VChip>
<!-- 编码标签 -->
<VChip v-if="meta?.video_encode" class="chip-codec rounded-sm" size="small" variant="elevated">
{{ meta?.video_encode }}
</VChip>
<!-- 制作组标签 -->
<VChip v-if="meta?.resource_team" class="chip-team rounded-sm" size="small" variant="elevated">
{{ meta?.resource_team }}
</VChip>
<!-- 其他标签 -->
<VChip
v-for="(label, index) in torrent?.labels"
:key="index"
class="chip-label rounded-sm"
size="small"
variant="elevated"
>{{ label }}</VChip
>
<!-- 特殊标签 -->
<VChip v-if="torrent?.hit_and_run" class="chip-hr rounded-sm" size="small" variant="elevated">H&R</VChip>
<VChip v-if="torrent?.freedate_diff" class="chip-expire rounded-sm" size="small" variant="elevated">{{
torrent?.freedate_diff
}}</VChip>
</div> </div>
</div> </VCardText>
<!-- 卡片底部信息 --> <!-- 卡片底部信息 -->
<div class="card-footer"> <VCardActions class="border-t border-opacity-10 mt-auto pa-3">
<div class="more-sources-wrapper" v-if="props.more && props.more.length > 0"> <div v-if="props.more && props.more.length > 0">
<div class="more-sources-toggle" @click.stop="openMoreTorrentsDialog"> <VBtn
<VIcon :icon="showMoreTorrents ? 'mdi-chevron-up' : 'mdi-chevron-down'" size="small" class="me-1"></VIcon> variant="text"
<span>更多来源 ({{ props.more.length }})</span> color="primary"
</div> size="small"
class="pa-1 d-flex align-center"
@click.stop="openMoreTorrentsDialog"
>
<VIcon :icon="showMoreTorrents ? 'mdi-chevron-up' : 'mdi-chevron-down'" size="small" class="mr-1"></VIcon>
更多来源 ({{ props.more.length }})
</VBtn>
</div> </div>
<VSpacer /> <VSpacer />
<!-- 体积和详情按钮并排 --> <!-- 体积和详情按钮并排 -->
<div class="card-actions"> <div class="d-flex align-center">
<div v-if="torrent?.size" class="size-badge"> <VChip v-if="torrent?.size" color="secondary" size="small" variant="elevated" class="rounded-sm mr-2">
{{ formatFileSize(torrent.size) }} {{ formatFileSize(torrent.size) }}
</div> </VChip>
<VBtn <VBtn icon size="small" variant="text" color="primary" @click.stop="openTorrentDetail">
size="small" <VIcon icon="mdi-information-outline"></VIcon>
icon="mdi-information-outline" </VBtn>
variant="text"
color="primary"
class="detail-btn"
@click.stop="openTorrentDetail"
></VBtn>
</div> </div>
</div> </VCardActions>
</VCard> </VCard>
<!-- 更多来源对话框 - 改为独立对话框 --> <!-- 更多来源对话框 -->
<VDialog v-model="showMoreTorrents" max-width="380px" location="center"> <VDialog v-model="showMoreTorrents" max-width="380px" location="center">
<VCard> <VCard>
<VCardTitle class="py-2 d-flex align-center"> <VCardTitle class="py-2 d-flex align-center">
@@ -222,44 +269,67 @@ onMounted(() => {
<VDivider /> <VDivider />
<VCardText class="more-sources-content"> <VCardText class="more-sources-content">
<div <VList lines="one" density="compact">
v-for="(item, index) in props.more" <VListItem
:key="index" v-for="(item, index) in props.more"
@click.stop="handleAddDownload(item)" :key="index"
class="more-source-item cursor-pointer" @click.stop="handleAddDownload(item)"
> class="border-b border-opacity-5 hover:bg-primary-lighten-5"
<div class="source-site-info"> >
<img <template v-slot:prepend>
:alt="item.torrent_info?.site_name" <div class="d-flex align-center gap-1">
v-if="siteIcons[item.torrent_info?.site || 0]" <img
:src="siteIcons[item.torrent_info?.site || 0]" v-if="siteIcons[item.torrent_info?.site || 0]"
class="source-site-icon" :src="siteIcons[item.torrent_info?.site || 0]"
/> :alt="item.torrent_info?.site_name"
<span v-else class="source-site-fallback">{{ item.torrent_info?.site_name?.substring(0, 1) }}</span> width="16"
<span class="source-site-name">{{ item.torrent_info.site_name }}</span> height="16"
<span v-if="item.meta_info?.season_episode" class="season-tag source-season-tag"> class="rounded"
{{ item.meta_info.season_episode }} />
</span> <VAvatar v-else size="16" class="text-caption bg-surface-variant">
{{ item.torrent_info?.site_name?.substring(0, 1) }}
</VAvatar>
<span class="text-body-2 font-weight-bold">{{ item.torrent_info.site_name }}</span>
<span <VChip
v-if="item.torrent_info?.downloadvolumefactor !== 1 || item.torrent_info?.uploadvolumefactor !== 1" v-if="item.meta_info?.season_episode"
class="source-discount" class="chip-season rounded-sm ml-1"
:class=" size="x-small"
getPromotionClass(item.torrent_info?.downloadvolumefactor, item.torrent_info?.uploadvolumefactor) variant="elevated"
" >
> {{ item.meta_info.season_episode }}
{{ item.torrent_info?.volume_factor }} </VChip>
</span>
</div>
<div class="source-stats"> <VChip
<span class="source-size">{{ formatFileSize(item.torrent_info?.size) }}</span> v-if="item.torrent_info?.downloadvolumefactor !== 1 || item.torrent_info?.uploadvolumefactor !== 1"
<span class="source-seeders"> :class="
<VIcon size="x-small" color="success" icon="mdi-arrow-up"></VIcon> getPromotionChipClass(
{{ item.torrent_info?.seeders }} item.torrent_info?.downloadvolumefactor,
</span> item.torrent_info?.uploadvolumefactor,
</div> )
</div> "
size="x-small"
variant="elevated"
class="rounded-sm ml-1"
>
{{ item.torrent_info?.volume_factor }}
</VChip>
</div>
</template>
<template v-slot:append>
<div class="d-flex align-center gap-2">
<span class="text-caption font-weight-bold text-primary">
{{ formatFileSize(item.torrent_info?.size) }}
</span>
<span class="d-flex align-center text-caption font-weight-bold">
<VIcon size="small" color="success" icon="mdi-arrow-up" class="mr-1"></VIcon>
{{ item.torrent_info?.seeders }}
</span>
</div>
</template>
</VListItem>
</VList>
</VCardText> </VCardText>
</VCard> </VCard>
</VDialog> </VDialog>
@@ -280,381 +350,91 @@ onMounted(() => {
</template> </template>
<style scoped> <style scoped>
.torrent-card {
overflow: hidden;
border-radius: 12px;
box-shadow: none;
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
cursor: pointer;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
position: relative;
}
.torrent-card:hover {
transform: translateY(-4px);
border-color: rgba(var(--v-theme-primary), 0.3);
}
.discount-banner { .discount-banner {
position: absolute; position: absolute;
top: 0; inset-block-start: 0;
right: 0; inset-inline-end: 0;
color: white;
padding: 4px 10px;
font-weight: 600;
font-size: 0.9rem;
border-radius: 0 0 0 12px;
z-index: 2;
}
.free-discount {
background-color: #4caf50;
font-weight: 700;
}
.percent-discount {
background-color: #ff5722;
}
.upload-bonus {
background-color: #9c27b0;
}
.size-badge {
background-color: rgba(var(--v-theme-primary), 0.9);
color: white;
padding: 2px 8px;
font-weight: 600;
font-size: 0.8rem;
border-radius: 4px;
margin-right: 6px;
display: flex;
align-items: center;
}
.card-header {
padding: 12px 16px 0;
}
.media-title-wrapper {
margin-bottom: 8px;
padding-right: 2rem;
}
.media-title {
font-size: 1.125rem;
font-weight: 600;
margin: 0;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
color: rgba(var(--v-theme-on-surface), 0.87);
}
.season-tag {
font-size: 0.875rem;
background-color: #5c6bc0;
color: white;
padding: 2px 6px;
border-radius: 4px;
font-weight: 600;
display: inline-flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.site-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
flex-wrap: wrap;
}
.site-icon {
width: 20px;
height: 20px;
margin-right: 8px;
border-radius: 2px;
}
.site-fallback {
width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 8px;
font-weight: 700;
color: rgba(var(--v-theme-on-surface), 0.8);
background-color: rgba(var(--v-theme-on-surface), 0.1);
border-radius: 2px;
}
.site-name {
font-size: 0.875rem;
font-weight: 600;
color: rgba(var(--v-theme-on-surface), 0.85);
}
.seeder-peers {
display: flex;
align-items: center;
gap: 12px;
}
.seed-info {
font-size: 0.95rem;
display: flex;
align-items: center;
gap: 4px;
font-weight: 600;
}
.peer-info {
font-size: 0.95rem;
display: flex;
align-items: center;
gap: 4px;
font-weight: 600;
}
.card-content {
padding: 0 16px;
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
}
.torrent-title {
font-size: 0.9rem;
font-weight: 500;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
color: rgba(var(--v-theme-on-surface), 0.87);
margin-bottom: 8px;
}
.torrent-desc {
font-size: 0.85rem;
color: rgba(var(--v-theme-on-surface), 0.6);
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 8px;
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
}
.resource-tag {
font-size: 0.8rem;
padding: 3px 8px;
border-radius: 4px;
color: white;
font-weight: 700;
}
.edition {
background-color: #f44336;
}
.resolution {
background-color: #e91e63;
}
.codec {
background-color: #ff9800;
}
.team {
background-color: #03a9f4;
}
.expire {
background-color: #9c27b0;
}
.label {
background-color: #3f51b5;
}
.hr {
background-color: #000000;
}
.card-footer {
padding: 8px 16px;
display: flex;
align-items: center;
border-top: 1px solid rgba(var(--v-theme-on-surface), 0.08);
margin-top: auto;
}
.more-sources-wrapper {
position: relative;
}
.more-sources-toggle {
font-size: 0.875rem;
color: rgb(var(--v-theme-primary));
display: flex;
align-items: center;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.2s;
}
.more-sources-toggle:hover {
background-color: rgba(var(--v-theme-primary), 0.08);
} }
.more-sources-content { .more-sources-content {
max-height: 60vh; max-block-size: 60vh;
overflow-y: auto; overflow-y: auto;
} }
.more-source-item { /* 卡片悬停效果 */
padding: 8px 0; .torrent-card {
display: flex; border: 1px solid transparent;
justify-content: space-between;
align-items: center;
transition: background-color 0.2s;
border-bottom: 1px solid rgba(var(--v-theme-on-surface), 0.05);
} }
.more-source-item:hover { .torrent-card:hover {
background-color: rgba(var(--v-theme-primary), 0.05); border-color: rgba(var(--v-theme-primary), 0.3);
} }
.source-site-info { /* 优惠标签样式 */
display: flex; .bg-success {
align-items: center; background-color: #4caf50;
gap: 6px;
} }
.source-site-icon { .bg-orange {
width: 16px; background-color: #ff5722;
height: 16px;
border-radius: 2px;
} }
.source-site-fallback { .bg-purple {
width: 16px; background-color: #9c27b0;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.7rem;
color: rgba(var(--v-theme-on-surface), 0.8);
background-color: rgba(var(--v-theme-on-surface), 0.1);
border-radius: 2px;
} }
.source-site-name { .chip-season {
font-size: 0.875rem; background-color: #3f51b5;
font-weight: 600;
}
.source-season-tag {
font-size: 0.75rem;
padding: 1px 4px;
margin-left: 4px;
background-color: #5c6bc0;
}
.source-discount {
font-weight: 700;
font-size: 0.8rem;
margin-left: 6px;
padding: 1px 5px;
border-radius: 3px;
color: white; color: white;
} }
.source-stats { .chip-edition {
display: flex; background-color: #f44336;
align-items: center; color: white;
gap: 10px;
} }
.source-size { .chip-resolution {
font-size: 0.8rem; background-color: #7b1fa2;
font-weight: 600; color: white;
color: rgb(var(--v-theme-primary));
} }
.source-seeders { .chip-codec {
display: flex; background-color: #ff9800;
align-items: center; color: white;
gap: 2px;
font-weight: 600;
font-size: 0.8rem;
} }
.card-actions { .chip-team {
display: flex; background-color: #00897b;
align-items: center; color: white;
} }
.detail-btn { .chip-label {
border-radius: 50%; background-color: #5c6bc0;
min-width: 36px; color: white;
height: 36px;
} }
.downloaded-card { .chip-hr {
border: 2px solid #4caf50 !important; background-color: #212121;
opacity: 0.85; color: white;
} }
@media (max-width: 640px) { .chip-expire {
.resource-tag { background-color: #7e57c2;
font-size: 0.75rem; color: white;
padding: 2px 6px;
}
} }
.full-text { .chip-free {
white-space: normal; background-color: #4caf50;
word-break: break-word; color: white;
font-size: 14px;
line-height: 1.5;
} }
.menu-activator { .chip-discount {
width: 100%; background-color: #ff5722;
cursor: pointer; color: white;
} }
.break-words { .chip-bonus {
word-wrap: break-word; background-color: #9c27b0;
word-break: break-word; color: white;
}
.overflow-visible {
overflow: visible !important;
}
.whitespace-break-spaces {
white-space: normal !important;
} }
</style> </style>