feat: 新增播客页面与组件

This commit is contained in:
alger
2026-02-04 20:14:12 +08:00
parent 3a3820cf52
commit ab901e633b
5 changed files with 1170 additions and 0 deletions
@@ -0,0 +1,154 @@
<script setup lang="ts">
import { usePodcastHistory } from '@/hooks/PodcastHistoryHook';
import { usePlayerStore } from '@/store';
import type { SongResult } from '@/types/music';
import type { DjProgram } from '@/types/podcast';
import { formatNumber, getImgUrl, secondToMinute } from '@/utils';
defineProps<{
programs: DjProgram[];
loading?: boolean;
}>();
const playerStore = usePlayerStore();
const { addPodcast } = usePodcastHistory();
const formatDate = (timestamp: number): string => {
const date = new Date(timestamp);
const now = new Date();
const diff = now.getTime() - date.getTime();
if (diff < 86400000) {
const hours = Math.floor(diff / 3600000);
if (hours < 1) {
const minutes = Math.floor(diff / 60000);
return `${minutes}分钟前`;
}
return `${hours}小时前`;
}
return `${date.getMonth() + 1}${date.getDate()}`;
};
const playProgram = async (program: DjProgram) => {
try {
const songData: SongResult = {
id: program.mainSong.id,
name: program.mainSong.name || program.name || '播客节目',
duration: program.mainSong.duration,
picUrl: program.coverUrl,
ar: [
{
id: program.radio.id,
name: program.radio.name,
picId: 0,
img1v1Id: 0,
briefDesc: '',
picUrl: '',
img1v1Url: '',
albumSize: 0,
alias: [],
trans: '',
musicSize: 0,
topicPerson: 0
}
],
al: {
id: program.radio.id,
name: program.radio.name,
picUrl: program.coverUrl,
type: '',
size: 0,
picId: 0,
blurPicUrl: '',
companyId: 0,
pic: 0,
picId_str: '',
publishTime: 0,
description: '',
tags: '',
company: '',
briefDesc: '',
artist: {
id: 0,
name: '',
picUrl: '',
alias: [],
albumSize: 0,
picId: 0,
img1v1Url: '',
img1v1Id: 0,
trans: '',
briefDesc: '',
musicSize: 0,
topicPerson: 0
},
songs: [],
alias: [],
status: 0,
copyrightId: 0,
commentThreadId: '',
artists: [],
subType: '',
onSale: false,
mark: 0
},
source: 'netease',
count: 0
};
await playerStore.setPlay(songData);
addPodcast(program);
} catch (error) {
console.error('播放节目失败:', error);
}
};
</script>
<template>
<div class="program-list">
<n-spin :show="loading">
<div v-if="programs.length === 0" class="text-center py-12 text-gray-400">暂无节目</div>
<div v-else class="space-y-2">
<div
v-for="program in programs"
:key="program.id"
class="flex items-center gap-4 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer group"
@click="playProgram(program)"
>
<div class="relative flex-shrink-0 w-16 h-16">
<img
:src="getImgUrl(program.coverUrl, '100y100')"
:alt="program.mainSong.name"
class="w-full h-full rounded object-cover"
/>
<div
class="absolute inset-0 bg-black/40 rounded opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
>
<i class="ri-play-fill text-white text-2xl"></i>
</div>
</div>
<div class="flex-1 min-w-0">
<h4 class="text-sm font-medium truncate">
{{ program.mainSong.name || program.name }}
</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 truncate mt-1">
{{ program.description }}
</p>
<div class="text-xs text-gray-400 mt-1">
{{ formatDate(program.createTime) }} ·
{{ secondToMinute(program.mainSong.duration / 1000) }}
</div>
</div>
<div class="flex-shrink-0 text-xs text-gray-400 text-right">
<div>{{ formatNumber(program.listenerCount) }} {{ $t('podcast.listeners') }}</div>
<div class="mt-1">{{ formatNumber(program.commentCount) }} 评论</div>
</div>
</div>
</div>
</n-spin>
</div>
</template>
@@ -0,0 +1,112 @@
<script setup lang="ts">
import { useMessage } from 'naive-ui';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { usePodcastStore, useUserStore } from '@/store';
import type { DjProgram, DjRadio } from '@/types/podcast';
import { formatNumber, getImgUrl } from '@/utils';
const props = defineProps<{
radio: DjRadio;
program?: DjProgram;
showSubscribeButton?: boolean;
animationDelay?: string;
}>();
const podcastStore = usePodcastStore();
const userStore = useUserStore();
const router = useRouter();
const message = useMessage();
const { t } = useI18n();
const isSubscribed = computed(() => podcastStore.isRadioSubscribed(props.radio.id));
const handleSubscribe = async (e: Event) => {
e.stopPropagation();
if (!userStore.user) {
message.warning(t('history.needLogin'));
return;
}
await podcastStore.toggleSubscribe(props.radio);
};
const goToDetail = () => {
router.push(`/podcast/radio/${props.radio.id}`);
};
</script>
<template>
<div
class="radio-card animate-item group flex flex-col rounded-2xl bg-neutral-50 dark:bg-neutral-900/50 p-4 cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-800/50 transition-all duration-300"
:style="{ animationDelay }"
@click="goToDetail"
>
<div class="relative overflow-hidden rounded-xl">
<img
:src="getImgUrl(radio.picUrl || program?.coverUrl || '', '200y200')"
:alt="radio.name"
class="w-full aspect-square object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div
v-if="showSubscribeButton && radio.subCount !== undefined"
class="absolute top-2 right-2 z-10"
>
<n-button
:type="isSubscribed ? 'default' : 'primary'"
size="small"
round
@click="handleSubscribe"
>
{{ isSubscribed ? t('podcast.subscribed') : t('podcast.subscribe') }}
</n-button>
</div>
<div
v-if="program"
class="absolute bottom-0 left-0 right-0 p-2 bg-black/40 backdrop-blur-sm text-white text-[10px] truncate"
>
{{ t('podcast.recentPlayed') }}: {{ program.mainSong?.name || program.name }}
</div>
</div>
<h3
class="mt-3 text-sm md:text-base font-semibold text-neutral-900 dark:text-white line-clamp-2 group-hover:text-primary transition-colors"
:title="radio.name"
>
{{ radio.name }}
</h3>
<p
v-if="radio.desc"
class="mt-1 text-xs md:text-sm text-neutral-500 dark:text-neutral-400 line-clamp-2"
>
{{ radio.desc }}
</p>
<div
v-if="radio.subCount !== undefined"
class="mt-2 flex items-center justify-between text-xs text-neutral-400"
>
<span>{{ formatNumber(radio.subCount) }} {{ t('podcast.subscribeCount') }}</span>
<span>{{ radio.programCount }} {{ t('podcast.programCount') }}</span>
</div>
</div>
</template>
<style scoped>
.animate-item {
animation: fadeInUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) backwards;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>