mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 03:14:57 +08:00
603 lines
16 KiB
Vue
603 lines
16 KiB
Vue
<!-- 电台播客页面 -->
|
||
<template>
|
||
<div v-if="djId" class="dj">
|
||
<Transition name="fade" mode="out-in">
|
||
<div v-if="djDetail && Object.keys(djDetail)?.length" class="detail">
|
||
<div class="cover">
|
||
<!-- 封面 -->
|
||
<n-image
|
||
:src="djDetail.coverSize.l"
|
||
:previewed-img-props="{ style: { borderRadius: '8px' } }"
|
||
:preview-src="djDetail.cover"
|
||
class="cover-img"
|
||
show-toolbar-tooltip
|
||
@load="
|
||
(e) => {
|
||
e.target.style.opacity = 1;
|
||
}
|
||
"
|
||
>
|
||
<template #placeholder>
|
||
<div class="cover-loading">
|
||
<img class="loading-img" src="/images/pic/song.jpg?assest" alt="song" />
|
||
</div>
|
||
</template>
|
||
</n-image>
|
||
<!-- 封面背板 -->
|
||
<n-image :src="djDetail.coverSize.m" class="cover-shadow" preview-disabled />
|
||
</div>
|
||
<div class="data">
|
||
<!-- 名称 -->
|
||
<n-text class="name">
|
||
{{ djDetail.name || "未知电台" }}
|
||
</n-text>
|
||
<div class="creator">
|
||
<n-avatar
|
||
:src="(djDetail.creator?.avatarUrl + '?param=300y$300').replace(/^http:/, 'https:')"
|
||
fallback-src="/images/pic/avatar.jpg?assest"
|
||
round
|
||
/>
|
||
<n-text class="nickname">{{ djDetail.creator?.nickname || "未知创建者" }}</n-text>
|
||
<n-text v-if="djDetail.createTime" class="create-time" depth="3">
|
||
{{ getTimestampTime(djDetail.createTime) }} 创建
|
||
</n-text>
|
||
<!-- 标签 -->
|
||
<n-tag
|
||
v-if="djDetail?.tags"
|
||
:bordered="false"
|
||
class="tags"
|
||
round
|
||
@click="
|
||
router.push({
|
||
path: '/dj-type',
|
||
query: {
|
||
type: djDetail.tags.id,
|
||
name: djDetail.tags.name,
|
||
},
|
||
})
|
||
"
|
||
>
|
||
{{ djDetail.tags.name }}
|
||
</n-tag>
|
||
</div>
|
||
<!-- 简介 -->
|
||
<n-ellipsis
|
||
v-if="djDetail.desc"
|
||
:tooltip="false"
|
||
class="description"
|
||
expand-trigger="click"
|
||
line-clamp="2"
|
||
>
|
||
<n-text depth="3">{{ djDetail.desc }}</n-text>
|
||
</n-ellipsis>
|
||
<n-text v-else class="description">太懒了吧,连简介都没写</n-text>
|
||
<!-- 数量 -->
|
||
<n-flex class="num">
|
||
<div v-if="djDetail?.count" class="num-item">
|
||
<n-icon depth="3" size="18">
|
||
<SvgIcon icon="music-note" />
|
||
</n-icon>
|
||
<n-text depth="3">{{ djDetail.count }}</n-text>
|
||
</div>
|
||
<div v-if="djDetail?.updateTime" class="num-item">
|
||
<n-icon depth="3" size="18">
|
||
<SvgIcon icon="clock" />
|
||
</n-icon>
|
||
<n-text depth="3">{{ getTimestampTime(djDetail.updateTime) }} 更新</n-text>
|
||
</div>
|
||
</n-flex>
|
||
</div>
|
||
</div>
|
||
<div v-else class="detail">
|
||
<n-skeleton class="cover" />
|
||
<div class="data">
|
||
<n-skeleton :repeat="4" text />
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
<!-- 功能区 -->
|
||
<n-flex class="menu" justify="space-between">
|
||
<n-flex class="left">
|
||
<n-button
|
||
:disabled="djData === 'empty'"
|
||
:focusable="false"
|
||
type="primary"
|
||
class="play"
|
||
tag="div"
|
||
circle
|
||
strong
|
||
secondary
|
||
@click="playAllSongs(djData, 'dj')"
|
||
>
|
||
<template #icon>
|
||
<n-icon size="32">
|
||
<SvgIcon icon="play-arrow-rounded" />
|
||
</n-icon>
|
||
</template>
|
||
</n-button>
|
||
<n-button
|
||
:focusable="false"
|
||
class="like"
|
||
size="large"
|
||
tag="div"
|
||
round
|
||
strong
|
||
secondary
|
||
@click="likeOrDislike(djId)"
|
||
>
|
||
<template #icon>
|
||
<n-icon>
|
||
<SvgIcon
|
||
:icon="isLikeOrDislike(djId) ? 'favorite-outline-rounded' : 'favorite-rounded'"
|
||
/>
|
||
</n-icon>
|
||
</template>
|
||
{{ isLikeOrDislike(djId) ? "订阅电台" : "取消订阅" }}
|
||
</n-button>
|
||
<n-dropdown :options="moreOptions" trigger="hover" placement="bottom-start">
|
||
<n-button :focusable="false" class="more" size="large" tag="div" circle strong secondary>
|
||
<template #icon>
|
||
<n-icon>
|
||
<SvgIcon icon="format-list-bulleted" />
|
||
</n-icon>
|
||
</template>
|
||
</n-button>
|
||
</n-dropdown>
|
||
</n-flex>
|
||
<n-flex class="right">
|
||
<!-- 模糊搜索 -->
|
||
<Transition name="fade" mode="out-in">
|
||
<n-input
|
||
v-if="djData !== 'empty' && djData?.length"
|
||
v-model:value="searchValue"
|
||
:input-props="{ autoComplete: false }"
|
||
class="search"
|
||
placeholder="模糊搜索"
|
||
clearable
|
||
@input="localSearch"
|
||
>
|
||
<template #prefix>
|
||
<n-icon size="18">
|
||
<SvgIcon icon="search-rounded" />
|
||
</n-icon>
|
||
</template>
|
||
</n-input>
|
||
</Transition>
|
||
</n-flex>
|
||
</n-flex>
|
||
<!-- 列表 -->
|
||
<Transition name="fade" mode="out-in">
|
||
<div v-if="djData !== 'empty'" class="list">
|
||
<Transition name="fade" mode="out-in">
|
||
<div v-if="!searchValue" class="song-list">
|
||
<SongList :data="djData" type="dj" />
|
||
<!-- 分页 -->
|
||
<Pagination
|
||
v-if="djData?.length"
|
||
:totalCount="totalCount"
|
||
:pageNumber="pageNumber"
|
||
@pageNumberChange="pageNumberChange"
|
||
/>
|
||
</div>
|
||
<SongList v-else-if="searchData?.length" :data="searchData" type="dj" />
|
||
<n-empty
|
||
v-else
|
||
:description="`搜不到关于 ${searchValue} 的任何节目`"
|
||
style="margin-top: 60px"
|
||
size="large"
|
||
>
|
||
<template #icon>
|
||
<n-icon>
|
||
<SvgIcon icon="search-off" />
|
||
</n-icon>
|
||
</template>
|
||
</n-empty>
|
||
</Transition>
|
||
</div>
|
||
<n-empty v-else class="empty" description="这个电台还没有节目哦" />
|
||
</Transition>
|
||
</div>
|
||
<div v-else class="title">
|
||
<n-text class="key">参数不完整</n-text>
|
||
<n-button :focusable="false" class="back" strong secondary @click="router.go(-1)">
|
||
返回上一页
|
||
</n-button>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { NIcon } from "naive-ui";
|
||
import { useRouter } from "vue-router";
|
||
import { storeToRefs } from "pinia";
|
||
import { siteData, siteSettings } from "@/stores";
|
||
import { getDjDetail, getDjProgram, likeDj } from "@/api/dj";
|
||
import { fuzzySearch } from "@/utils/helper";
|
||
import { isLogin } from "@/utils/auth";
|
||
import { getTimestampTime } from "@/utils/timeTools";
|
||
import { playAllSongs } from "@/utils/Player";
|
||
import debounce from "@/utils/debounce";
|
||
import formatData from "@/utils/formatData";
|
||
import SvgIcon from "@/components/Global/SvgIcon";
|
||
|
||
const router = useRouter();
|
||
const data = siteData();
|
||
const settings = siteSettings();
|
||
const { userLikeData } = storeToRefs(data);
|
||
const { loadSize } = storeToRefs(settings);
|
||
|
||
// 电台数据
|
||
const djId = ref(router.currentRoute.value.query.id);
|
||
const pageNumber = ref(Number(router.currentRoute.value.query?.page) || 1);
|
||
const djDetail = ref(null);
|
||
const djData = ref(null);
|
||
|
||
// 模糊搜索数据
|
||
const searchValue = ref(null);
|
||
const searchData = ref([]);
|
||
const totalCount = ref(0);
|
||
|
||
// 图标渲染
|
||
const renderIcon = (icon) => {
|
||
return () => h(NIcon, null, { default: () => h(SvgIcon, { icon }, null) });
|
||
};
|
||
|
||
// 更多操作数据
|
||
const moreOptions = [
|
||
{
|
||
label: "打开源页面链接",
|
||
key: "open",
|
||
props: {
|
||
onclick: () => {
|
||
const id = djId.value;
|
||
if (id) window.open(`https://music.163.com/#/djradio?id=${id}`);
|
||
},
|
||
},
|
||
icon: renderIcon("link"),
|
||
},
|
||
];
|
||
|
||
// 获取电台信息
|
||
const getDjDetailData = async (id) => {
|
||
try {
|
||
if (!id) return false;
|
||
// 清空数据
|
||
djDetail.value = null;
|
||
djData.value = null;
|
||
// 获取数据
|
||
const detail = await getDjDetail(id);
|
||
// 基础信息
|
||
djDetail.value = formatData(detail.data, "dj")[0];
|
||
} catch (error) {
|
||
console.error("获取电台信息出错:", error);
|
||
$message.error("获取电台信息出现错误");
|
||
}
|
||
};
|
||
|
||
// 获取电台全部节目
|
||
const getDjProgramData = async (id, limit = loadSize.value, offset = 0) => {
|
||
try {
|
||
djData.value = [];
|
||
const result = await getDjProgram(id, limit, offset);
|
||
console.log(result);
|
||
// 数据总数
|
||
totalCount.value = result.count;
|
||
if (totalCount.value === 0) return (djData.value = "empty");
|
||
// 处理数据
|
||
djData.value = formatData(result.programs, "dj");
|
||
} catch (error) {
|
||
console.error("获取电台节目错误:", error);
|
||
$message.error("获取电台节目出现错误");
|
||
}
|
||
};
|
||
|
||
// 节目模糊搜索
|
||
const localSearch = debounce((val) => {
|
||
const searchValue = val?.trim();
|
||
// 是否为空
|
||
if (!searchValue || searchValue === "") {
|
||
return true;
|
||
}
|
||
// 返回结果
|
||
const result = fuzzySearch(searchValue, djData.value);
|
||
searchData.value = result;
|
||
}, 300);
|
||
|
||
// 判断收藏还是取消
|
||
const isLikeOrDislike = (id) => {
|
||
const djs = userLikeData.value.djs;
|
||
if (djs.length) {
|
||
return !djs.some((item) => item.id === Number(id));
|
||
}
|
||
return true;
|
||
};
|
||
|
||
// 订阅 / 取消订阅电台
|
||
const likeOrDislike = debounce(async (id) => {
|
||
try {
|
||
if (!isLogin()) return $message.warning("请登录后使用");
|
||
const type = isLikeOrDislike(id) ? 1 : 0;
|
||
const result = await likeDj(id, type);
|
||
if (result.code === 200) {
|
||
$message.success((type === 1 ? "订阅" : "取消订阅") + "成功");
|
||
// 更新用户电台
|
||
await data.setUserLikeDjs();
|
||
} else {
|
||
$message.error((type === 1 ? "订阅" : "取消订阅") + "失败,请重试");
|
||
}
|
||
} catch (error) {
|
||
console.error("订阅出错:", error);
|
||
$message.error("订阅操作出现错误");
|
||
}
|
||
}, 300);
|
||
|
||
// 页数变化
|
||
const pageNumberChange = (page) => {
|
||
router.push({
|
||
path: "/dj",
|
||
query: { id: djId.value, page },
|
||
});
|
||
};
|
||
|
||
// 监听路由变化
|
||
watch(
|
||
() => router.currentRoute.value,
|
||
async (val) => {
|
||
if (val.name === "dj") {
|
||
// 更改参数
|
||
pageNumber.value = Number(val.query?.page) || 1;
|
||
djId.value = val.query?.id;
|
||
// 调用接口
|
||
await getDjDetailData(djId.value);
|
||
await getDjProgramData(
|
||
djId.value,
|
||
loadSize.value,
|
||
(pageNumber.value - 1) * settings.loadSize,
|
||
);
|
||
}
|
||
},
|
||
);
|
||
|
||
onMounted(async () => {
|
||
await getDjDetailData(djId.value);
|
||
await getDjProgramData(djId.value, loadSize.value, (pageNumber.value - 1) * settings.loadSize);
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.dj {
|
||
.detail {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: stretch;
|
||
margin-bottom: 20px;
|
||
.cover {
|
||
position: relative;
|
||
display: flex;
|
||
width: 200px;
|
||
height: 200px;
|
||
min-width: 200px;
|
||
margin-right: 20px;
|
||
border-radius: 8px;
|
||
.cover-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 8px;
|
||
z-index: 1;
|
||
transition:
|
||
filter 0.3s,
|
||
transform 0.3s;
|
||
:deep(img) {
|
||
width: 100%;
|
||
opacity: 0;
|
||
transition: opacity 0.35s ease-in-out;
|
||
}
|
||
&:active {
|
||
transform: scale(0.98);
|
||
}
|
||
}
|
||
.cover-shadow {
|
||
position: absolute;
|
||
top: 4px;
|
||
height: 100%;
|
||
width: 100%;
|
||
filter: blur(16px) opacity(0.6);
|
||
transform: scale(0.92, 0.96);
|
||
z-index: 0;
|
||
background-size: cover;
|
||
aspect-ratio: 1/1;
|
||
}
|
||
}
|
||
.data {
|
||
width: 100%;
|
||
.name {
|
||
font-size: 30px;
|
||
font-weight: bold;
|
||
margin-bottom: 12px;
|
||
-webkit-line-clamp: 2;
|
||
}
|
||
.creator {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
.n-avatar {
|
||
width: 28px;
|
||
height: 28px;
|
||
margin-right: 8px;
|
||
}
|
||
.nickname {
|
||
transition: color 0.3s;
|
||
cursor: pointer;
|
||
&:hover {
|
||
color: var(--main-color);
|
||
}
|
||
}
|
||
.create-time {
|
||
margin-left: 12px;
|
||
font-size: 13px;
|
||
}
|
||
.tags {
|
||
margin-left: 12px;
|
||
font-size: 13px;
|
||
padding: 0 16px;
|
||
line-height: 0;
|
||
cursor: pointer;
|
||
transition:
|
||
transform 0.3s,
|
||
background-color 0.3s,
|
||
color 0.3s;
|
||
&:hover {
|
||
background-color: var(--main-second-color);
|
||
color: var(--main-color);
|
||
}
|
||
&:active {
|
||
transform: scale(0.95);
|
||
}
|
||
}
|
||
}
|
||
.num {
|
||
margin-top: 12px;
|
||
.num-item {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
.n-icon {
|
||
margin-right: 4px;
|
||
// color: var(--main-color);
|
||
}
|
||
}
|
||
}
|
||
.description {
|
||
margin-top: 12px;
|
||
.n-text {
|
||
display: initial;
|
||
}
|
||
}
|
||
:deep(.n-skeleton) {
|
||
&:first-child {
|
||
width: 60%;
|
||
margin-top: 0;
|
||
height: 40px;
|
||
}
|
||
height: 30px;
|
||
margin-top: 12px;
|
||
border-radius: 8px;
|
||
}
|
||
}
|
||
}
|
||
.menu {
|
||
align-items: center;
|
||
margin: 26px 0;
|
||
.left {
|
||
align-items: center;
|
||
.play {
|
||
--n-width: 46px;
|
||
--n-height: 46px;
|
||
}
|
||
}
|
||
.right {
|
||
.search {
|
||
height: 40px;
|
||
width: 130px;
|
||
display: flex;
|
||
align-items: center;
|
||
border-radius: 40px;
|
||
transition:
|
||
width 0.3s,
|
||
background-color 0.3s;
|
||
&.n-input--focus {
|
||
width: 200px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
@media (max-width: 700px) {
|
||
.detail {
|
||
.cover {
|
||
width: 140px;
|
||
height: 140px;
|
||
min-width: 140px;
|
||
}
|
||
.data {
|
||
.name {
|
||
font-size: 20px;
|
||
margin-bottom: 4px;
|
||
}
|
||
.creator {
|
||
.n-avatar {
|
||
width: 20px;
|
||
height: 20px;
|
||
margin-right: 6px;
|
||
}
|
||
.nickname {
|
||
font-size: 12px;
|
||
}
|
||
.create-time {
|
||
margin-left: 6px;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
.tags {
|
||
.pl-tags {
|
||
font-size: 12px;
|
||
padding: 0 12px;
|
||
}
|
||
}
|
||
.num,
|
||
.description {
|
||
display: none !important;
|
||
}
|
||
}
|
||
}
|
||
.menu {
|
||
margin: 20px 0;
|
||
.left {
|
||
.play {
|
||
--n-width: 40px;
|
||
--n-height: 40px;
|
||
.n-icon {
|
||
font-size: 22px !important;
|
||
}
|
||
}
|
||
.like {
|
||
--n-height: 36px;
|
||
--n-font-size: 13px;
|
||
--n-padding: 0 16px;
|
||
--n-icon-size: 18px;
|
||
:deep(.n-button__icon) {
|
||
margin: 0;
|
||
}
|
||
:deep(.n-button__content) {
|
||
display: none;
|
||
}
|
||
}
|
||
.more {
|
||
--n-height: 36px;
|
||
--n-font-size: 13px;
|
||
--n-icon-size: 18px;
|
||
}
|
||
}
|
||
.right {
|
||
.search {
|
||
height: 36px;
|
||
width: 130px;
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.title {
|
||
display: flex;
|
||
flex-direction: column;
|
||
.key {
|
||
margin: 10px 0;
|
||
font-size: 36px;
|
||
font-weight: bold;
|
||
margin-right: 8px;
|
||
}
|
||
.back {
|
||
width: 98px;
|
||
}
|
||
}
|
||
</style>
|