mirror of
https://github.com/jiangrui1994/CloudSaver.git
synced 2026-01-10 15:18:46 +08:00
619 lines
15 KiB
Vue
619 lines
15 KiB
Vue
<template>
|
|
<div class="resource-card">
|
|
<!-- 详情弹窗 -->
|
|
<el-dialog
|
|
v-model="showDetail"
|
|
:title="currentResource?.title"
|
|
width="700px"
|
|
class="resource-detail-dialog"
|
|
destroy-on-close
|
|
>
|
|
<div v-if="currentResource" class="detail-content">
|
|
<div class="detail-cover">
|
|
<el-image
|
|
class="cover-image"
|
|
:src="getProxyImageUrl(currentResource.image as string)"
|
|
:fit="currentResource.image ? 'cover' : 'contain'"
|
|
/>
|
|
<el-tag
|
|
class="cloud-type"
|
|
:type="store.tagColor[currentResource.cloudType as keyof TagColor]"
|
|
effect="dark"
|
|
round
|
|
>
|
|
{{ currentResource.cloudType }}
|
|
</el-tag>
|
|
</div>
|
|
<div class="detail-info">
|
|
<h3 class="detail-title">
|
|
<el-link :href="currentResource.cloudLinks[0]" target="_blank" :underline="false">
|
|
{{ currentResource.title }}
|
|
</el-link>
|
|
</h3>
|
|
<div class="detail-description" v-html="currentResource.content" />
|
|
<div v-if="currentResource.tags?.length" class="detail-tags">
|
|
<div class="tags-list">
|
|
<el-tag
|
|
v-for="tag in currentResource.tags"
|
|
:key="tag"
|
|
class="tag-item"
|
|
@click="searchMovieforTag(tag)"
|
|
>
|
|
{{ tag }}
|
|
</el-tag>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<template #footer>
|
|
<div class="dialog-footer">
|
|
<el-button type="primary" plain @click="currentResource && handleJump(currentResource)"
|
|
>跳转</el-button
|
|
>
|
|
<el-button
|
|
v-if="currentResource?.isSupportSave"
|
|
type="primary"
|
|
@click="currentResource && handleSave(currentResource)"
|
|
>转存</el-button
|
|
>
|
|
</div>
|
|
</template>
|
|
</el-dialog>
|
|
|
|
<div v-for="group in store.resources" :key="group.id" class="resource-group">
|
|
<div
|
|
:class="{ 'group-header': true, 'is-active': group.displayList }"
|
|
@click="group.displayList = !group.displayList"
|
|
>
|
|
<el-link
|
|
class="group-title"
|
|
:href="`https://t.me/s/${group.id}`"
|
|
target="_blank"
|
|
:underline="false"
|
|
@click.stop
|
|
>
|
|
<el-image
|
|
:src="getProxyImageUrl(group.channelInfo.channelLogo)"
|
|
:fit="group.channelInfo.channelLogo ? 'cover' : 'contain'"
|
|
class="channel-logo"
|
|
scroll-container="#pc-resources-content"
|
|
loading="lazy"
|
|
/>
|
|
<span>{{ group.channelInfo.name }}</span>
|
|
<span class="item-count">({{ group.list.length }})</span>
|
|
</el-link>
|
|
|
|
<el-tooltip effect="dark" :content="group.displayList ? '收起' : '展开'" placement="top">
|
|
<el-button class="toggle-btn" type="text">
|
|
<el-icon :class="{ 'is-active': group.displayList }">
|
|
<ArrowDown />
|
|
</el-icon>
|
|
</el-button>
|
|
</el-tooltip>
|
|
</div>
|
|
|
|
<div v-if="group.displayList" class="group-content">
|
|
<div class="card-grid">
|
|
<el-card
|
|
v-for="resource in group.list"
|
|
:key="resource.messageId"
|
|
class="resource-card-item"
|
|
:body-style="{ padding: '0' }"
|
|
>
|
|
<div class="card-wrapper">
|
|
<div class="card-cover">
|
|
<el-image
|
|
loading="lazy"
|
|
class="cover-image"
|
|
:src="getProxyImageUrl(resource.image as string)"
|
|
:fit="resource.image ? 'cover' : 'contain'"
|
|
:alt="resource.title"
|
|
@click="showResourceDetail(resource)"
|
|
/>
|
|
<el-tag
|
|
class="cloud-type"
|
|
:type="store.tagColor[resource.cloudType as keyof TagColor]"
|
|
effect="dark"
|
|
round
|
|
size="small"
|
|
>
|
|
{{ resource.cloudType }}
|
|
</el-tag>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<el-link
|
|
class="card-title"
|
|
:href="resource.cloudLinks[0]"
|
|
target="_blank"
|
|
:underline="false"
|
|
>
|
|
{{ resource.title }}
|
|
</el-link>
|
|
|
|
<div
|
|
class="card-content"
|
|
@click="showResourceDetail(resource)"
|
|
v-html="resource.content"
|
|
/>
|
|
|
|
<div v-if="resource.tags?.length" class="card-tags">
|
|
<div class="tags-list">
|
|
<el-tag
|
|
v-for="tag in resource.tags"
|
|
:key="tag"
|
|
class="tag-item"
|
|
@click="searchMovieforTag(tag)"
|
|
>
|
|
{{ tag }}
|
|
</el-tag>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-footer">
|
|
<el-button type="primary" plain @click="handleJump(resource)">跳转</el-button>
|
|
<el-button
|
|
v-if="resource.isSupportSave"
|
|
type="primary"
|
|
@click="handleSave(resource)"
|
|
>转存</el-button
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</el-card>
|
|
</div>
|
|
|
|
<div class="load-more">
|
|
<el-button :loading="group.loading" @click="handleLoadMore(group.id)">
|
|
<el-icon><Plus /></el-icon>
|
|
加载更多
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useResourceStore } from "@/stores/resource";
|
|
import { ref } from "vue";
|
|
import type { ResourceItem, TagColor } from "@/types";
|
|
import { ArrowDown, Plus } from "@element-plus/icons-vue";
|
|
import { getProxyImageUrl } from "@/utils/image";
|
|
|
|
const store = useResourceStore();
|
|
|
|
const showDetail = ref(false);
|
|
const currentResource = ref<ResourceItem | null>(null);
|
|
|
|
const emit = defineEmits(["save", "loadMore", "jump", "searchMovieforTag"]);
|
|
|
|
const handleSave = (resource: ResourceItem) => {
|
|
if (showDetail.value) {
|
|
showDetail.value = false;
|
|
}
|
|
emit("save", resource);
|
|
};
|
|
|
|
const handleJump = (resource: ResourceItem) => {
|
|
emit("jump", resource);
|
|
};
|
|
|
|
const showResourceDetail = (resource: ResourceItem) => {
|
|
currentResource.value = resource;
|
|
showDetail.value = true;
|
|
};
|
|
|
|
const searchMovieforTag = (tag: string) => {
|
|
emit("searchMovieforTag", tag);
|
|
};
|
|
|
|
const handleLoadMore = (channelId: string) => {
|
|
emit("loadMore", channelId);
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
@import "@/styles/common.scss";
|
|
|
|
.resource-card {
|
|
position: relative;
|
|
height: 100%;
|
|
|
|
// 资源组
|
|
.resource-group {
|
|
background: var(--theme-card-bg);
|
|
backdrop-filter: var(--theme-blur);
|
|
-webkit-backdrop-filter: var(--theme-blur);
|
|
margin-bottom: 24px;
|
|
border-radius: var(--theme-radius);
|
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
transition: var(--theme-transition);
|
|
|
|
&:last-child {
|
|
margin-bottom: 100px;
|
|
}
|
|
}
|
|
|
|
// 组标题
|
|
.group-header {
|
|
@include flex-center;
|
|
justify-content: space-between;
|
|
padding: 12px 20px;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
position: sticky;
|
|
top: 0;
|
|
background: var(--theme-card-bg);
|
|
backdrop-filter: var(--theme-blur);
|
|
-webkit-backdrop-filter: var(--theme-blur);
|
|
z-index: 10;
|
|
border-radius: var(--theme-radius);
|
|
overflow: hidden;
|
|
cursor: pointer;
|
|
|
|
&.is-active {
|
|
border-radius: var(--theme-radius) var(--theme-radius) 0 0;
|
|
}
|
|
|
|
.group-title {
|
|
@include flex-center;
|
|
gap: 12px;
|
|
font-size: 16px;
|
|
color: var(--theme-text-primary);
|
|
transition: var(--theme-transition);
|
|
|
|
.channel-logo {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
overflow: hidden;
|
|
box-shadow: var(--theme-shadow-sm);
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.item-count {
|
|
font-size: 13px;
|
|
color: var(--theme-text-secondary);
|
|
}
|
|
|
|
&:hover {
|
|
color: var(--theme-primary);
|
|
transform: translateY(-1px);
|
|
}
|
|
}
|
|
|
|
.toggle-btn {
|
|
width: 32px;
|
|
height: 32px;
|
|
padding: 0;
|
|
color: var(--theme-text-regular);
|
|
transition: var(--theme-transition);
|
|
|
|
.el-icon {
|
|
font-size: 16px;
|
|
transition: transform 0.3s ease;
|
|
|
|
&.is-active {
|
|
transform: rotate(180deg);
|
|
}
|
|
}
|
|
|
|
&:hover {
|
|
color: var(--theme-primary);
|
|
transform: translateY(-1px);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 组内容
|
|
.group-content {
|
|
padding: 20px;
|
|
}
|
|
|
|
// 卡片网格
|
|
.card-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
gap: 24px;
|
|
grid-auto-rows: min-content;
|
|
}
|
|
|
|
// 资源卡片
|
|
.resource-card-item {
|
|
border-radius: var(--theme-radius);
|
|
transition: var(--theme-transition);
|
|
overflow: hidden;
|
|
max-width: 460px;
|
|
margin: 0 auto;
|
|
width: 100%;
|
|
height: fit-content;
|
|
|
|
&:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: var(--theme-shadow);
|
|
}
|
|
|
|
.card-wrapper {
|
|
display: flex;
|
|
gap: 20px;
|
|
padding: 16px;
|
|
height: 100%;
|
|
}
|
|
|
|
.card-cover {
|
|
position: relative;
|
|
width: 120px;
|
|
height: 180px;
|
|
flex-shrink: 0;
|
|
|
|
.cover-image {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
border-radius: var(--theme-radius);
|
|
cursor: pointer;
|
|
transition: opacity 0.3s ease;
|
|
|
|
&:hover {
|
|
opacity: 0.85;
|
|
}
|
|
}
|
|
|
|
.cloud-type {
|
|
position: absolute;
|
|
top: 8px;
|
|
left: 8px;
|
|
z-index: 1;
|
|
}
|
|
}
|
|
|
|
.card-body {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
|
|
.card-title {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
font-size: 16px;
|
|
line-height: 1.5;
|
|
color: var(--theme-text-primary);
|
|
word-break: break-word;
|
|
height: 3em;
|
|
transition: var(--theme-transition);
|
|
|
|
&:hover {
|
|
color: var(--theme-primary);
|
|
}
|
|
}
|
|
|
|
.card-content {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
color: var(--theme-text-regular);
|
|
cursor: pointer;
|
|
transition: color 0.3s ease;
|
|
|
|
&:hover {
|
|
color: var(--theme-text-primary);
|
|
}
|
|
}
|
|
|
|
.card-tags {
|
|
margin-top: auto;
|
|
max-height: 88px;
|
|
overflow: hidden;
|
|
|
|
.tags-label {
|
|
font-size: 13px;
|
|
color: var(--theme-text-secondary);
|
|
margin-right: 8px;
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.tags-list {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
max-height: 72px;
|
|
overflow: hidden;
|
|
|
|
.tag-item {
|
|
cursor: pointer;
|
|
transition: var(--theme-transition);
|
|
margin: 0;
|
|
height: 24px;
|
|
|
|
&:hover {
|
|
color: var(--theme-primary);
|
|
border-color: var(--theme-primary);
|
|
transform: translateY(-1px);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.card-footer {
|
|
@include flex-center;
|
|
justify-content: flex-end;
|
|
margin-top: 8px;
|
|
|
|
.el-button {
|
|
padding: 6px 16px;
|
|
font-size: 14px;
|
|
height: 32px;
|
|
min-width: 80px;
|
|
|
|
&:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: var(--theme-shadow-sm);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 加载更多
|
|
.load-more {
|
|
@include flex-center;
|
|
position: relative;
|
|
padding: 32px 0 8px;
|
|
margin-top: 16px;
|
|
|
|
&::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
top: 0;
|
|
height: 1px;
|
|
background: linear-gradient(
|
|
90deg,
|
|
transparent,
|
|
var(--el-border-color-lighter) 20%,
|
|
var(--el-border-color-lighter) 80%,
|
|
transparent
|
|
);
|
|
}
|
|
|
|
.el-button {
|
|
min-width: 160px;
|
|
height: 40px;
|
|
border-radius: 20px;
|
|
font-size: 14px;
|
|
color: var(--theme-text-regular);
|
|
background: var(--theme-card-bg);
|
|
border: 1px solid var(--el-border-color-lighter);
|
|
transition: var(--theme-transition);
|
|
position: relative;
|
|
overflow: hidden;
|
|
|
|
&::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
|
|
transform: translateX(-100%);
|
|
transition: transform 0.6s ease;
|
|
}
|
|
|
|
&:hover {
|
|
color: var(--theme-primary);
|
|
border-color: var(--theme-primary);
|
|
background: var(--el-color-primary-light-9);
|
|
|
|
&::after {
|
|
transform: translateX(100%);
|
|
}
|
|
}
|
|
|
|
&.is-loading {
|
|
color: var(--theme-text-secondary);
|
|
|
|
&::after {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.el-icon {
|
|
margin-right: 6px;
|
|
font-size: 16px;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 详情弹窗样式
|
|
.resource-detail-dialog {
|
|
:deep(.el-dialog__body) {
|
|
padding: 20px;
|
|
}
|
|
|
|
.detail-content {
|
|
display: flex;
|
|
gap: 24px;
|
|
}
|
|
|
|
.detail-cover {
|
|
position: relative;
|
|
width: 200px;
|
|
flex-shrink: 0;
|
|
|
|
.cover-image {
|
|
width: 100%;
|
|
height: 300px;
|
|
border-radius: var(--theme-radius);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.cloud-type {
|
|
position: absolute;
|
|
top: 8px;
|
|
left: 8px;
|
|
z-index: 1;
|
|
}
|
|
}
|
|
|
|
.detail-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
|
|
.detail-title {
|
|
font-size: 18px;
|
|
margin: 0 0 16px;
|
|
line-height: 1.5;
|
|
color: var(--theme-text-primary);
|
|
}
|
|
|
|
.detail-description {
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
color: var(--theme-text-regular);
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.detail-tags {
|
|
.tags-label {
|
|
font-size: 13px;
|
|
color: var(--theme-text-secondary);
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.tags-list {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin-top: 8px;
|
|
|
|
.tag-item {
|
|
cursor: pointer;
|
|
transition: var(--theme-transition);
|
|
|
|
&:hover {
|
|
color: var(--theme-primary);
|
|
border-color: var(--theme-primary);
|
|
transform: translateY(-1px);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.dialog-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
padding-top: 16px;
|
|
}
|
|
}
|
|
}
|
|
</style>
|