mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-03 14:20:50 +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>
|
||||
<div class="album-item" @click="handleClick">
|
||||
<n-image
|
||||
:src="getImgUrl(item.picUrl || '', '100y100')"
|
||||
class="album-item-img"
|
||||
lazy
|
||||
preview-disabled
|
||||
/>
|
||||
<div class="album-item-info">
|
||||
<div class="album-item-name">
|
||||
<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>
|
||||
<history-item
|
||||
:image-url="getImgUrl(item.picUrl || '', '100y100')"
|
||||
:name="item.name"
|
||||
:description="getDescription()"
|
||||
:count="item.count"
|
||||
:show-count="showCount"
|
||||
:show-delete="showDelete"
|
||||
@click="emit('click', item)"
|
||||
@delete="emit('delete', item)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import HistoryItem from '@/components/common/HistoryItem.vue';
|
||||
import type { AlbumHistoryItem } from '@/store/modules/playHistory';
|
||||
import { getImgUrl } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
item: AlbumHistoryItem;
|
||||
showCount?: boolean;
|
||||
showDelete?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
showCount: false,
|
||||
showDelete: false
|
||||
});
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
item: AlbumHistoryItem;
|
||||
showCount?: boolean;
|
||||
showDelete?: boolean;
|
||||
}>(),
|
||||
{
|
||||
showCount: false,
|
||||
showDelete: false
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [item: AlbumHistoryItem];
|
||||
@@ -49,64 +39,8 @@ const { t } = useI18n();
|
||||
|
||||
const getDescription = () => {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (props.item.artist?.name) {
|
||||
parts.push(props.item.artist.name);
|
||||
}
|
||||
|
||||
if (props.item.size !== undefined) {
|
||||
parts.push(t('common.songCount', { count: props.item.size }));
|
||||
}
|
||||
|
||||
if (props.item.artist?.name) 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');
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
emit('click', props.item);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
emit('delete', props.item);
|
||||
};
|
||||
</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>
|
||||
|
||||
21
src/renderer/components/common/EmptyState.vue
Normal file
21
src/renderer/components/common/EmptyState.vue
Normal file
@@ -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>
|
||||
48
src/renderer/components/common/HistoryItem.vue
Normal file
48
src/renderer/components/common/HistoryItem.vue
Normal file
@@ -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>
|
||||
<div class="playlist-item" @click="handleClick">
|
||||
<n-image
|
||||
:src="getImgUrl(item.coverImgUrl || item.picUrl || '', '100y100')"
|
||||
class="playlist-item-img"
|
||||
lazy
|
||||
preview-disabled
|
||||
/>
|
||||
<div class="playlist-item-info">
|
||||
<div class="playlist-item-name">
|
||||
<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>
|
||||
<history-item
|
||||
:image-url="getImgUrl(item.coverImgUrl || item.picUrl || '', '100y100')"
|
||||
:name="item.name"
|
||||
:description="getDescription()"
|
||||
:count="item.count"
|
||||
:show-count="showCount"
|
||||
:show-delete="showDelete"
|
||||
@click="emit('click', item)"
|
||||
@delete="emit('delete', item)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import HistoryItem from '@/components/common/HistoryItem.vue';
|
||||
import type { PlaylistHistoryItem } from '@/store/modules/playHistory';
|
||||
import { getImgUrl } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
item: PlaylistHistoryItem;
|
||||
showCount?: boolean;
|
||||
showDelete?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
showCount: false,
|
||||
showDelete: false
|
||||
});
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
item: PlaylistHistoryItem;
|
||||
showCount?: boolean;
|
||||
showDelete?: boolean;
|
||||
}>(),
|
||||
{
|
||||
showCount: false,
|
||||
showDelete: false
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [item: PlaylistHistoryItem];
|
||||
@@ -49,64 +39,9 @@ const { t } = useI18n();
|
||||
|
||||
const getDescription = () => {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (props.item.trackCount !== undefined) {
|
||||
if (props.item.trackCount !== undefined)
|
||||
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');
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
emit('click', props.item);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
emit('delete', props.item);
|
||||
};
|
||||
</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']"
|
||||
>
|
||||
<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')"
|
||||
lazy
|
||||
preview-disabled
|
||||
|
||||
@@ -110,7 +110,7 @@ defineExpose({
|
||||
-moz-user-select: none;
|
||||
-ms-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 {
|
||||
|
||||
@@ -178,7 +178,7 @@ const formatDuration = (ms: number): string => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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 {
|
||||
@apply bg-gray-50 dark:bg-gray-700;
|
||||
|
||||
@@ -143,14 +143,14 @@ const onPlayMusic = () => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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 {
|
||||
@apply bg-gray-50 dark:bg-gray-800;
|
||||
}
|
||||
|
||||
.song-item-img {
|
||||
@apply w-10 h-10 rounded-lg mr-3;
|
||||
@apply w-10 h-10 rounded-xl mr-3;
|
||||
}
|
||||
|
||||
.song-item-content {
|
||||
|
||||
@@ -140,7 +140,7 @@ const onPlayMusic = () => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mini-song-item {
|
||||
@apply p-2 rounded-2xl;
|
||||
@apply p-2 rounded-xl;
|
||||
|
||||
&:hover {
|
||||
@apply bg-light-100 dark:bg-dark-100;
|
||||
|
||||
@@ -159,7 +159,7 @@ const onPlayNext = () => {
|
||||
}
|
||||
|
||||
.song-item-img {
|
||||
@apply w-12 h-12 rounded-2xl mr-4;
|
||||
@apply w-12 h-12 rounded-xl mr-4;
|
||||
}
|
||||
|
||||
.song-item-content {
|
||||
|
||||
@@ -195,9 +195,9 @@ watch(
|
||||
--n-rail-height: 3px;
|
||||
--n-rail-color: 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-color: #1ed760;
|
||||
--n-handle-color: #22c55e;
|
||||
|
||||
&:hover {
|
||||
--n-handle-size: 10px; /* 鼠标悬停时显示滑块 */
|
||||
|
||||
Reference in New Issue
Block a user