- 修改了前端 Singer.vue 文件,增加了对 keyword 的判断,以显示不同的标题 - 更新了后端 SingerServiceImpl 文件,改进了搜索逻辑,支持多字段查询
524 lines
12 KiB
Vue
524 lines
12 KiB
Vue
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useUserStore } from '../stores/user'
|
|
import { ElMessage } from 'element-plus'
|
|
import { Search } from '@element-plus/icons-vue'
|
|
import { getSingerList, getHotSinger, isSingerCollected, collectSinger, uncollectSinger } from '../api/singer'
|
|
import { getSingerTypeList } from '../api/singerType'
|
|
import { processImageUrl } from '../utils/imageUtils'
|
|
|
|
const router = useRouter()
|
|
const userStore = useUserStore()
|
|
|
|
// 歌手类型列表
|
|
const singerTypes = ref([{ id: null, name: '全部' }])
|
|
|
|
// 当前选中的歌手类型
|
|
const activeType = ref(null)
|
|
|
|
// 歌手列表
|
|
const singers = ref([])
|
|
|
|
// 分页参数
|
|
const currentPage = ref(1)
|
|
const pageSize = ref(20)
|
|
const total = ref(0)
|
|
|
|
// 加载状态
|
|
const loading = ref(false)
|
|
|
|
// 搜索关键词
|
|
const keyword = ref('')
|
|
|
|
// 热门歌手
|
|
const hotSingers = ref([])
|
|
const loadingHot = ref(false)
|
|
|
|
// 获取歌手类型列表
|
|
const fetchSingerTypes = async () => {
|
|
try {
|
|
const res = await getSingerTypeList()
|
|
if (res.code === 200) {
|
|
// 将获取到的类型添加到列表中
|
|
singerTypes.value = [
|
|
{ id: null, name: '全部' },
|
|
...res.data
|
|
]
|
|
} else {
|
|
ElMessage.error(res.message || '获取歌手类型失败')
|
|
}
|
|
} catch (error) {
|
|
console.error('获取歌手类型失败:', error)
|
|
ElMessage.error('获取歌手类型失败')
|
|
}
|
|
}
|
|
|
|
// 切换歌手类型
|
|
const changeType = (typeId) => {
|
|
activeType.value = typeId
|
|
currentPage.value = 1
|
|
fetchSingers()
|
|
}
|
|
|
|
// 获取歌手列表
|
|
const fetchSingers = async () => {
|
|
loading.value = true
|
|
try {
|
|
const res = await getSingerList(
|
|
currentPage.value,
|
|
pageSize.value,
|
|
keyword.value,
|
|
activeType.value
|
|
)
|
|
|
|
if (res.code === 200) {
|
|
singers.value = res.data.list || res.data.records || []
|
|
total.value = res.data.total || 0
|
|
|
|
// 处理图片URL
|
|
singers.value = singers.value.map(singer => ({
|
|
...singer,
|
|
avatar: processImageUrl(singer.avatar)
|
|
}))
|
|
updateSingerStatuses(singers.value)
|
|
} else {
|
|
ElMessage.error(res.message || '获取歌手列表失败')
|
|
}
|
|
} catch (error) {
|
|
console.error('获取歌手列表失败:', error)
|
|
ElMessage.error('获取歌手列表失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// 获取热门歌手
|
|
const fetchHotSingers = async () => {
|
|
loadingHot.value = true
|
|
try {
|
|
const res = await getHotSinger(8) // 获取8个热门歌手
|
|
if (res.code === 200) {
|
|
hotSingers.value = res.data || []
|
|
|
|
// 处理图片URL
|
|
hotSingers.value = hotSingers.value.map(singer => ({
|
|
...singer,
|
|
avatar: processImageUrl(singer.avatar)
|
|
}))
|
|
updateSingerStatuses(hotSingers.value)
|
|
} else {
|
|
ElMessage.error(res.message || '获取热门歌手失败')
|
|
}
|
|
} catch (error) {
|
|
console.error('获取热门歌手失败:', error)
|
|
ElMessage.error('获取热门歌手失败')
|
|
} finally {
|
|
loadingHot.value = false
|
|
}
|
|
}
|
|
|
|
// 处理搜索
|
|
const handleSearch = () => {
|
|
currentPage.value = 1
|
|
fetchSingers()
|
|
}
|
|
|
|
// 处理分页变化
|
|
const handlePageChange = (page) => {
|
|
currentPage.value = page
|
|
fetchSingers()
|
|
}
|
|
|
|
// 处理每页数量变化
|
|
const handleSizeChange = (size) => {
|
|
pageSize.value = size
|
|
currentPage.value = 1
|
|
fetchSingers()
|
|
}
|
|
|
|
// 查看歌手详情
|
|
const viewSingerDetail = (id) => {
|
|
router.push(`/singer/${id}`)
|
|
}
|
|
|
|
// 收藏/取消收藏歌手
|
|
const toggleCollect = async (singer, event) => {
|
|
event.stopPropagation()
|
|
|
|
if (!userStore.isLoggedIn) {
|
|
ElMessage.warning('请先登录')
|
|
router.push('/login')
|
|
return
|
|
}
|
|
|
|
try {
|
|
let res
|
|
if (singer.collected) {
|
|
res = await uncollectSinger(singer.id)
|
|
} else {
|
|
res = await collectSinger(singer.id)
|
|
}
|
|
|
|
if (res.code === 200) {
|
|
singer.collected = !singer.collected
|
|
ElMessage.success(singer.collected ? '收藏成功' : '已取消收藏')
|
|
} else {
|
|
ElMessage.error(res.message || '操作失败')
|
|
}
|
|
} catch (error) {
|
|
console.error('收藏操作失败:', error)
|
|
ElMessage.error('操作失败,请稍后重试')
|
|
}
|
|
}
|
|
|
|
// 更新歌手收藏状态
|
|
const updateSingerStatuses = async (list) => {
|
|
if (!userStore.isLoggedIn || !list || list.length === 0) {
|
|
return
|
|
}
|
|
|
|
const promises = list.map(singer => isSingerCollected(singer.id))
|
|
try {
|
|
const results = await Promise.all(promises)
|
|
results.forEach((res, index) => {
|
|
if (res.code === 200) {
|
|
list[index].collected = res.data
|
|
}
|
|
})
|
|
} catch (error) {
|
|
console.error('批量获取歌手收藏状态失败:', error)
|
|
}
|
|
}
|
|
|
|
// 格式化粉丝数
|
|
const formatFans = (count) => {
|
|
if (count < 10000) {
|
|
return count.toString()
|
|
} else if (count < 100000000) {
|
|
return (count / 10000).toFixed(1) + '万'
|
|
} else {
|
|
return (count / 100000000).toFixed(1) + '亿'
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchSingerTypes()
|
|
fetchSingers()
|
|
fetchHotSingers()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="singer-container">
|
|
<div class="singer-header">
|
|
<h1>歌手</h1>
|
|
<div class="search-container">
|
|
<el-input
|
|
v-model="keyword"
|
|
placeholder="搜索歌手"
|
|
clearable
|
|
@keyup.enter="handleSearch"
|
|
>
|
|
<template #suffix>
|
|
<el-icon class="el-input__icon" @click="handleSearch">
|
|
<Search />
|
|
</el-icon>
|
|
</template>
|
|
</el-input>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 歌手类型选择 -->
|
|
<div class="singer-types">
|
|
<el-radio-group v-model="activeType" @change="changeType">
|
|
<el-radio-button
|
|
v-for="type in singerTypes"
|
|
:key="type.id"
|
|
:label="type.id"
|
|
>
|
|
{{ type.name }}
|
|
</el-radio-button>
|
|
</el-radio-group>
|
|
</div>
|
|
|
|
<!-- 热门歌手 -->
|
|
<div class="hot-singers" v-if="!keyword && activeType === null && currentPage === 1">
|
|
<h2>热门歌手</h2>
|
|
<div class="singer-list" v-loading="loadingHot">
|
|
<div
|
|
v-for="singer in hotSingers"
|
|
:key="singer.id"
|
|
class="singer-item"
|
|
@click="viewSingerDetail(singer.id)"
|
|
>
|
|
<div class="singer-avatar">
|
|
<img :src="singer.avatar" :alt="singer.name">
|
|
<div class="singer-overlay">
|
|
<el-button
|
|
circle
|
|
:icon="singer.collected ? 'StarFilled' : 'Star'"
|
|
:type="singer.collected ? 'warning' : 'primary'"
|
|
@click="(event) => toggleCollect(singer, event)"
|
|
></el-button>
|
|
</div>
|
|
</div>
|
|
<div class="singer-info">
|
|
<div class="singer-name">{{ singer.name }}</div>
|
|
<div class="singer-stats">
|
|
<span>{{ singer.musicCount || 0 }}首歌曲</span>
|
|
|
|
<span>{{ formatFans(singer.fansCount || 0) }}粉丝</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 歌手列表 -->
|
|
<div class="all-singers">
|
|
<h2 v-if="keyword">搜索结果</h2>
|
|
<h2 v-else-if="activeType === null && currentPage === 1">全部歌手</h2>
|
|
<div class="singer-list" v-loading="loading">
|
|
<div
|
|
v-for="singer in singers"
|
|
:key="singer.id"
|
|
class="singer-item"
|
|
@click="viewSingerDetail(singer.id)"
|
|
>
|
|
<div class="singer-avatar">
|
|
<img :src="singer.avatar" :alt="singer.name">
|
|
<div class="singer-overlay">
|
|
<el-button
|
|
circle
|
|
:icon="singer.collected ? 'StarFilled' : 'Star'"
|
|
:type="singer.collected ? 'warning' : 'primary'"
|
|
@click="(event) => toggleCollect(singer, event)"
|
|
></el-button>
|
|
</div>
|
|
</div>
|
|
<div class="singer-info">
|
|
<div class="singer-name">{{ singer.name }}</div>
|
|
<div class="singer-stats">
|
|
<span>{{ singer.musicCount || 0 }}首歌曲</span>
|
|
|
|
<span>{{ formatFans(singer.fansCount || 0) }}粉丝</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 空状态 -->
|
|
<el-empty
|
|
v-if="singers.length === 0 && !loading"
|
|
description="暂无歌手"
|
|
></el-empty>
|
|
|
|
<!-- 分页 -->
|
|
<div class="pagination" v-if="total > 0">
|
|
<el-pagination
|
|
v-model:current-page="currentPage"
|
|
v-model:page-size="pageSize"
|
|
:page-sizes="[20, 40, 60, 80]"
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
:total="total"
|
|
@size-change="handleSizeChange"
|
|
@current-change="handlePageChange"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.singer-container {
|
|
padding: 20px 20px 80px 20px;
|
|
min-height: calc(100vh - 140px);
|
|
background-color: transparent;
|
|
border-radius: 15px;
|
|
}
|
|
|
|
.singer-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 30px;
|
|
padding: 20px;
|
|
background-color: rgba(255, 255, 255, 0.85);
|
|
border-radius: 12px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.singer-header h1 {
|
|
margin: 0;
|
|
font-size: 1.8rem;
|
|
color: #ff6b81;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.search-container {
|
|
width: 300px;
|
|
}
|
|
|
|
.search-container :deep(.el-input__wrapper) {
|
|
border-radius: 20px;
|
|
}
|
|
|
|
.search-container .el-icon {
|
|
color: #ff6b81;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.search-container .el-icon:hover {
|
|
color: #ff4757;
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.hot-singers,
|
|
.all-singers {
|
|
margin-top: 30px;
|
|
}
|
|
|
|
.hot-singers h2,
|
|
.all-singers h2 {
|
|
margin-bottom: 20px;
|
|
font-size: 1.5rem;
|
|
color: #ff6b81;
|
|
}
|
|
|
|
.pagination {
|
|
margin-top: 30px;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.pagination :deep(.el-pagination.is-background .btn-next),
|
|
.pagination :deep(.el-pagination.is-background .btn-prev),
|
|
.pagination :deep(.el-pagination.is-background .el-pager li) {
|
|
background-color: #fff9f9;
|
|
border-radius: 50%;
|
|
margin: 0 5px;
|
|
}
|
|
|
|
.pagination :deep(.el-pagination.is-background .el-pager li:not(.is-disabled).is-active) {
|
|
background-color: #ffb6c1;
|
|
color: white;
|
|
}
|
|
|
|
.singer-types {
|
|
margin-bottom: 30px;
|
|
overflow-x: auto;
|
|
white-space: nowrap;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.singer-types :deep(.el-radio-button__inner) {
|
|
border-radius: 20px !important;
|
|
margin-right: 10px;
|
|
border: none;
|
|
background-color: #fff9f9;
|
|
color: #666;
|
|
}
|
|
|
|
.singer-types :deep(.el-radio-button__orig-radio:checked + .el-radio-button__inner) {
|
|
background-color: #ffb6c1;
|
|
color: white;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.singer-list {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
gap: 25px;
|
|
}
|
|
|
|
.singer-item {
|
|
background-color: #fff9f9;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
box-shadow: 0 2px 12px rgba(255, 107, 129, 0.1);
|
|
transition: all 0.3s;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.singer-item:hover {
|
|
transform: translateY(-8px);
|
|
box-shadow: 0 8px 20px rgba(255, 107, 129, 0.2);
|
|
}
|
|
|
|
.singer-avatar {
|
|
position: relative;
|
|
width: 100%;
|
|
padding-top: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.singer-avatar img {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
transition: transform 0.3s;
|
|
}
|
|
|
|
.singer-item:hover .singer-avatar img {
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.singer-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: linear-gradient(to bottom, rgba(0,0,0,0.1), rgba(255, 182, 193, 0.7));
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.singer-item:hover .singer-overlay {
|
|
opacity: 1;
|
|
}
|
|
|
|
.singer-overlay .el-button {
|
|
background-color: rgba(255, 255, 255, 0.8);
|
|
color: #ff6b81;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.singer-overlay .el-button:hover {
|
|
background-color: white;
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.singer-info {
|
|
padding: 15px;
|
|
}
|
|
|
|
.singer-name {
|
|
font-weight: 600;
|
|
margin-bottom: 8px;
|
|
text-align: center;
|
|
color: #333;
|
|
}
|
|
|
|
.singer-stats {
|
|
display: flex;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
font-size: 0.8rem;
|
|
color: #666;
|
|
}
|
|
|
|
.singer-stats span {
|
|
white-space: nowrap;
|
|
}
|
|
</style>
|