Files
SPlayer/src/views/List/dj.vue
imsyy 309c323a14 🔧 build: support ESM and upgrade to Vite 5
- 临时解决下载歌曲无法正常播放 #113
2024-01-09 18:13:01 +08:00

603 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 电台播客页面 -->
<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>