mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-24 16:27:23 +08:00
refactor(ui): 统一 SongItem 圆角、抽象 HistoryItem、新增 EmptyState、修复主题色
- SongItem 5 变体容器/图片圆角统一为 rounded-xl(12px): BaseSongItem(rounded-3xl→xl) / Standard(img rounded-2xl→xl) / Compact(rounded-lg→xl) / List(rounded-lg→xl) / Mini(rounded-2xl→xl) - 抽象 HistoryItem.vue:AlbumItem 和 PlaylistItem 提取共享 UI 组件, 消除 ~80 行重复样式代码,同时迁移至内联 Tailwind class - 新增 EmptyState.vue:统一空状态组件(icon + text,暗色模式完整适配) - 动画时长:SearchItem 图片 hover duration-700→duration-500 - MobilePlayBar:进度条颜色 Spotify #1ed760→项目主色 #22c55e
This commit is contained in:
@@ -1,44 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="album-item" @click="handleClick">
|
<history-item
|
||||||
<n-image
|
:image-url="getImgUrl(item.picUrl || '', '100y100')"
|
||||||
:src="getImgUrl(item.picUrl || '', '100y100')"
|
:name="item.name"
|
||||||
class="album-item-img"
|
:description="getDescription()"
|
||||||
lazy
|
:count="item.count"
|
||||||
preview-disabled
|
:show-count="showCount"
|
||||||
/>
|
:show-delete="showDelete"
|
||||||
<div class="album-item-info">
|
@click="emit('click', item)"
|
||||||
<div class="album-item-name">
|
@delete="emit('delete', item)"
|
||||||
<n-ellipsis :line-clamp="1">{{ item.name }}</n-ellipsis>
|
/>
|
||||||
</div>
|
|
||||||
<div class="album-item-desc">
|
|
||||||
{{ getDescription() }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="showCount && item.count" class="album-item-count">
|
|
||||||
{{ item.count }}
|
|
||||||
</div>
|
|
||||||
<div v-if="showDelete" class="album-item-delete" @click.stop="handleDelete">
|
|
||||||
<i class="iconfont icon-close"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import HistoryItem from '@/components/common/HistoryItem.vue';
|
||||||
import type { AlbumHistoryItem } from '@/store/modules/playHistory';
|
import type { AlbumHistoryItem } from '@/store/modules/playHistory';
|
||||||
import { getImgUrl } from '@/utils';
|
import { getImgUrl } from '@/utils';
|
||||||
|
|
||||||
interface Props {
|
const props = withDefaults(
|
||||||
item: AlbumHistoryItem;
|
defineProps<{
|
||||||
showCount?: boolean;
|
item: AlbumHistoryItem;
|
||||||
showDelete?: boolean;
|
showCount?: boolean;
|
||||||
}
|
showDelete?: boolean;
|
||||||
|
}>(),
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
{
|
||||||
showCount: false,
|
showCount: false,
|
||||||
showDelete: false
|
showDelete: false
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
click: [item: AlbumHistoryItem];
|
click: [item: AlbumHistoryItem];
|
||||||
@@ -49,64 +39,8 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
const getDescription = () => {
|
const getDescription = () => {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
if (props.item.artist?.name) parts.push(props.item.artist.name);
|
||||||
if (props.item.artist?.name) {
|
if (props.item.size !== undefined) parts.push(t('common.songCount', { count: props.item.size }));
|
||||||
parts.push(props.item.artist.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.item.size !== undefined) {
|
|
||||||
parts.push(t('common.songCount', { count: props.item.size }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts.join(' · ') || t('history.noDescription');
|
return parts.join(' · ') || t('history.noDescription');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
emit('click', props.item);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
|
||||||
emit('delete', props.item);
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.album-item {
|
|
||||||
@apply flex items-center px-2 py-2 rounded-xl cursor-pointer;
|
|
||||||
@apply transition-all duration-200;
|
|
||||||
@apply bg-light-100 dark:bg-dark-100;
|
|
||||||
@apply hover:bg-light-200 dark:hover:bg-dark-200;
|
|
||||||
@apply mb-2;
|
|
||||||
|
|
||||||
&-img {
|
|
||||||
@apply flex items-center justify-center rounded-xl;
|
|
||||||
@apply w-[60px] h-[60px] flex-shrink-0;
|
|
||||||
@apply bg-light-300 dark:bg-dark-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-info {
|
|
||||||
@apply ml-3 flex-1 min-w-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-name {
|
|
||||||
@apply text-gray-900 dark:text-white text-base mb-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-desc {
|
|
||||||
@apply text-sm text-gray-500 dark:text-gray-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-count {
|
|
||||||
@apply px-4 text-lg text-center min-w-[60px];
|
|
||||||
@apply text-gray-600 dark:text-gray-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-delete {
|
|
||||||
@apply cursor-pointer rounded-full border-2 w-8 h-8 flex justify-center items-center;
|
|
||||||
@apply border-gray-400 dark:border-gray-600;
|
|
||||||
@apply text-gray-600 dark:text-gray-400;
|
|
||||||
@apply hover:border-red-500 hover:text-red-500;
|
|
||||||
@apply transition-all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center py-20 text-neutral-400 dark:text-neutral-500"
|
||||||
|
>
|
||||||
|
<i :class="[icon, 'text-5xl mb-4 opacity-40']" />
|
||||||
|
<p class="text-sm">{{ text }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
icon?: string;
|
||||||
|
text?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
icon: 'ri-music-2-line',
|
||||||
|
text: ''
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-3 px-2 py-2 mb-2 rounded-xl cursor-pointer transition-colors duration-200 bg-light-100 dark:bg-dark-100 hover:bg-light-200 dark:hover:bg-dark-200"
|
||||||
|
@click="$emit('click')"
|
||||||
|
>
|
||||||
|
<n-image
|
||||||
|
:src="imageUrl"
|
||||||
|
class="w-[60px] h-[60px] flex-shrink-0 rounded-xl bg-light-300 dark:bg-dark-300"
|
||||||
|
lazy
|
||||||
|
preview-disabled
|
||||||
|
/>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="text-base text-gray-900 dark:text-white mb-1">
|
||||||
|
<n-ellipsis :line-clamp="1">{{ name }}</n-ellipsis>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400 truncate">{{ description }}</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showCount && count"
|
||||||
|
class="px-4 text-lg text-center min-w-[60px] text-gray-600 dark:text-gray-400 flex-shrink-0"
|
||||||
|
>
|
||||||
|
{{ count }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showDelete"
|
||||||
|
class="cursor-pointer rounded-full border-2 w-8 h-8 flex flex-shrink-0 justify-center items-center border-gray-400 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:border-red-500 hover:text-red-500 transition-colors duration-200"
|
||||||
|
@click.stop="$emit('delete')"
|
||||||
|
>
|
||||||
|
<i class="iconfont icon-close" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
imageUrl: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
count?: number;
|
||||||
|
showCount?: boolean;
|
||||||
|
showDelete?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
click: [];
|
||||||
|
delete: [];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
@@ -1,44 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="playlist-item" @click="handleClick">
|
<history-item
|
||||||
<n-image
|
:image-url="getImgUrl(item.coverImgUrl || item.picUrl || '', '100y100')"
|
||||||
:src="getImgUrl(item.coverImgUrl || item.picUrl || '', '100y100')"
|
:name="item.name"
|
||||||
class="playlist-item-img"
|
:description="getDescription()"
|
||||||
lazy
|
:count="item.count"
|
||||||
preview-disabled
|
:show-count="showCount"
|
||||||
/>
|
:show-delete="showDelete"
|
||||||
<div class="playlist-item-info">
|
@click="emit('click', item)"
|
||||||
<div class="playlist-item-name">
|
@delete="emit('delete', item)"
|
||||||
<n-ellipsis :line-clamp="1">{{ item.name }}</n-ellipsis>
|
/>
|
||||||
</div>
|
|
||||||
<div class="playlist-item-desc">
|
|
||||||
{{ getDescription() }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="showCount && item.count" class="playlist-item-count">
|
|
||||||
{{ item.count }}
|
|
||||||
</div>
|
|
||||||
<div v-if="showDelete" class="playlist-item-delete" @click.stop="handleDelete">
|
|
||||||
<i class="iconfont icon-close"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import HistoryItem from '@/components/common/HistoryItem.vue';
|
||||||
import type { PlaylistHistoryItem } from '@/store/modules/playHistory';
|
import type { PlaylistHistoryItem } from '@/store/modules/playHistory';
|
||||||
import { getImgUrl } from '@/utils';
|
import { getImgUrl } from '@/utils';
|
||||||
|
|
||||||
interface Props {
|
const props = withDefaults(
|
||||||
item: PlaylistHistoryItem;
|
defineProps<{
|
||||||
showCount?: boolean;
|
item: PlaylistHistoryItem;
|
||||||
showDelete?: boolean;
|
showCount?: boolean;
|
||||||
}
|
showDelete?: boolean;
|
||||||
|
}>(),
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
{
|
||||||
showCount: false,
|
showCount: false,
|
||||||
showDelete: false
|
showDelete: false
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
click: [item: PlaylistHistoryItem];
|
click: [item: PlaylistHistoryItem];
|
||||||
@@ -49,64 +39,9 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
const getDescription = () => {
|
const getDescription = () => {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
if (props.item.trackCount !== undefined)
|
||||||
if (props.item.trackCount !== undefined) {
|
|
||||||
parts.push(t('user.playlist.trackCount', { count: props.item.trackCount }));
|
parts.push(t('user.playlist.trackCount', { count: props.item.trackCount }));
|
||||||
}
|
if (props.item.creator?.nickname) parts.push(props.item.creator.nickname);
|
||||||
|
|
||||||
if (props.item.creator?.nickname) {
|
|
||||||
parts.push(props.item.creator.nickname);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts.join(' · ') || t('history.noDescription');
|
return parts.join(' · ') || t('history.noDescription');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
emit('click', props.item);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
|
||||||
emit('delete', props.item);
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.playlist-item {
|
|
||||||
@apply flex items-center px-2 py-2 rounded-xl cursor-pointer;
|
|
||||||
@apply transition-all duration-200;
|
|
||||||
@apply bg-light-100 dark:bg-dark-100;
|
|
||||||
@apply hover:bg-light-200 dark:hover:bg-dark-200;
|
|
||||||
@apply mb-2;
|
|
||||||
|
|
||||||
&-img {
|
|
||||||
@apply flex items-center justify-center rounded-xl;
|
|
||||||
@apply w-[60px] h-[60px] flex-shrink-0;
|
|
||||||
@apply bg-light-300 dark:bg-dark-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-info {
|
|
||||||
@apply ml-3 flex-1 min-w-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-name {
|
|
||||||
@apply text-gray-900 dark:text-white text-base mb-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-desc {
|
|
||||||
@apply text-sm text-gray-500 dark:text-gray-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-count {
|
|
||||||
@apply px-4 text-lg text-center min-w-[60px];
|
|
||||||
@apply text-gray-600 dark:text-gray-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-delete {
|
|
||||||
@apply cursor-pointer rounded-full border-2 w-8 h-8 flex justify-center items-center;
|
|
||||||
@apply border-gray-400 dark:border-gray-600;
|
|
||||||
@apply text-gray-600 dark:text-gray-400;
|
|
||||||
@apply hover:border-red-500 hover:text-red-500;
|
|
||||||
@apply transition-all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
:class="[item.type === 'mv' ? 'aspect-video' : 'aspect-square']"
|
:class="[item.type === 'mv' ? 'aspect-video' : 'aspect-square']"
|
||||||
>
|
>
|
||||||
<n-image
|
<n-image
|
||||||
class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||||
:src="getImgUrl(item.picUrl, item.type === 'mv' ? '400y225' : '400y400')"
|
:src="getImgUrl(item.picUrl, item.type === 'mv' ? '400y225' : '400y400')"
|
||||||
lazy
|
lazy
|
||||||
preview-disabled
|
preview-disabled
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ defineExpose({
|
|||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@apply rounded-3xl p-3 flex items-center transition bg-transparent dark:text-white text-gray-900;
|
@apply rounded-xl p-3 flex items-center transition bg-transparent dark:text-white text-gray-900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-ellipsis {
|
.text-ellipsis {
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ const formatDuration = (ms: number): string => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.compact-song-item {
|
.compact-song-item {
|
||||||
@apply rounded-lg p-2 h-12 mb-1 border-b dark:border-gray-800 border-gray-100;
|
@apply rounded-xl p-2 h-12 mb-1 border-b dark:border-gray-800 border-gray-100;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply bg-gray-50 dark:bg-gray-700;
|
@apply bg-gray-50 dark:bg-gray-700;
|
||||||
|
|||||||
@@ -143,14 +143,14 @@ const onPlayMusic = () => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.list-song-item {
|
.list-song-item {
|
||||||
@apply p-2 rounded-lg mb-2 border dark:border-gray-800 border-gray-200;
|
@apply p-2 rounded-xl mb-2 border dark:border-gray-800 border-gray-200;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply bg-gray-50 dark:bg-gray-800;
|
@apply bg-gray-50 dark:bg-gray-800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-item-img {
|
.song-item-img {
|
||||||
@apply w-10 h-10 rounded-lg mr-3;
|
@apply w-10 h-10 rounded-xl mr-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-item-content {
|
.song-item-content {
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ const onPlayMusic = () => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.mini-song-item {
|
.mini-song-item {
|
||||||
@apply p-2 rounded-2xl;
|
@apply p-2 rounded-xl;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply bg-light-100 dark:bg-dark-100;
|
@apply bg-light-100 dark:bg-dark-100;
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ const onPlayNext = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.song-item-img {
|
.song-item-img {
|
||||||
@apply w-12 h-12 rounded-2xl mr-4;
|
@apply w-12 h-12 rounded-xl mr-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-item-content {
|
.song-item-content {
|
||||||
|
|||||||
@@ -195,9 +195,9 @@ watch(
|
|||||||
--n-rail-height: 3px;
|
--n-rail-height: 3px;
|
||||||
--n-rail-color: rgba(255, 255, 255, 0.15);
|
--n-rail-color: rgba(255, 255, 255, 0.15);
|
||||||
--n-rail-color-dark: rgba(255, 255, 255, 0.15);
|
--n-rail-color-dark: rgba(255, 255, 255, 0.15);
|
||||||
--n-fill-color: #1ed760; /* Spotify绿色,可调整为其他绿色 */
|
--n-fill-color: #22c55e;
|
||||||
--n-handle-size: 0px; /* 隐藏滑块 */
|
--n-handle-size: 0px; /* 隐藏滑块 */
|
||||||
--n-handle-color: #1ed760;
|
--n-handle-color: #22c55e;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
--n-handle-size: 10px; /* 鼠标悬停时显示滑块 */
|
--n-handle-size: 10px; /* 鼠标悬停时显示滑块 */
|
||||||
|
|||||||
Reference in New Issue
Block a user