音乐模块调整
This commit is contained in:
@@ -1,7 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { Cloud, Music2, MoreHorizontal, Play, Plus } from '@lucide/vue'
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Cloud,
|
||||
FolderOpen,
|
||||
HardDrive,
|
||||
ListPlus,
|
||||
MoreHorizontal,
|
||||
Music2,
|
||||
Play,
|
||||
Plus,
|
||||
Trash2
|
||||
} from '@lucide/vue'
|
||||
import type { PlayableItem } from '@/stores/feiniuStore'
|
||||
import { useFeiniuStore } from '@/stores/feiniuStore'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -17,10 +29,26 @@ import {
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
|
||||
const props = defineProps<{
|
||||
item: PlayableItem
|
||||
active?: boolean
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
item: PlayableItem
|
||||
/** 是否为当前播放项 */
|
||||
active?: boolean
|
||||
/** 列表内序号(从 0 开始) */
|
||||
index?: number
|
||||
/** 播放上下文:点击播放时以该列表建立队列 */
|
||||
context?: PlayableItem[]
|
||||
/** 显示专辑列 */
|
||||
showAlbum?: boolean
|
||||
/** 显示来源标识(歌单等混排列表) */
|
||||
showSource?: boolean
|
||||
/** 所属歌单 id(提供时可从菜单移除 / 移动) */
|
||||
playlistId?: string
|
||||
/** 歌单内的序号(用于移除 / 移动) */
|
||||
playlistIndex?: number
|
||||
}>(),
|
||||
{ active: false, index: 0, showAlbum: false, showSource: false }
|
||||
)
|
||||
|
||||
const store = useFeiniuStore()
|
||||
|
||||
@@ -28,10 +56,38 @@ const coverUrl = computed(() => {
|
||||
if (props.item.source === 'feiniu' && props.item.coverId && store.mediaPrefix) {
|
||||
return `${store.mediaPrefix}/cover?coverId=${encodeURIComponent(props.item.coverId)}&size=64`
|
||||
}
|
||||
return ''
|
||||
return props.item.coverUrl ?? ''
|
||||
})
|
||||
const coverFailed = ref(false)
|
||||
|
||||
const durationText = computed(() =>
|
||||
store.fmtDuration(props.item.durationMs ? props.item.durationMs / 1000 : undefined)
|
||||
)
|
||||
/** 本地文件没有时长信息时,退化为展示文件大小 */
|
||||
const fallbackText = computed(() => {
|
||||
if (props.item.durationMs) return ''
|
||||
const n = props.item.size
|
||||
if (!n || n <= 0) return ''
|
||||
if (n >= 1048576) return `${(n / 1048576).toFixed(1)} MB`
|
||||
if (n >= 1024) return `${(n / 1024).toFixed(0)} KB`
|
||||
return `${n} B`
|
||||
})
|
||||
|
||||
const durationText = computed(() => store.fmtDuration(props.item.durationMs ? props.item.durationMs / 1000 : undefined))
|
||||
const isCurrent = computed(
|
||||
() =>
|
||||
props.active ||
|
||||
(store.queueIndex >= 0 &&
|
||||
store.nowPlaying?.guid === props.item.guid &&
|
||||
store.nowPlaying?.source === props.item.source)
|
||||
)
|
||||
|
||||
const playlistLength = computed(
|
||||
() => (props.playlistId ? store.playlists.find((p) => p.id === props.playlistId)?.items.length ?? 1 : 1)
|
||||
)
|
||||
|
||||
function play() {
|
||||
store.playItem(props.item, props.context)
|
||||
}
|
||||
|
||||
function addToPlaylist(playlistId: string) {
|
||||
store.addToPlaylist(playlistId, [props.item])
|
||||
@@ -50,71 +106,150 @@ async function uploadToFeiniu() {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="group flex items-center gap-3 rounded-lg border px-3 py-2 transition-colors"
|
||||
:class="active ? 'border-primary bg-primary/10' : 'border-border hover:bg-muted/40'"
|
||||
class="group grid h-11 items-center gap-3 rounded-md px-2 outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring/50"
|
||||
:class="[
|
||||
showAlbum
|
||||
? 'grid-cols-[28px_36px_minmax(0,1fr)_minmax(0,0.75fr)_74px_28px]'
|
||||
: 'grid-cols-[28px_36px_minmax(0,1fr)_74px_28px]',
|
||||
isCurrent ? 'bg-accent/60' : 'hover:bg-accent/40'
|
||||
]"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@dblclick="play"
|
||||
@keydown.enter.prevent="play"
|
||||
>
|
||||
<!-- 序号 / 播放态指示:悬停时原位切换,不占用固定的空白列 -->
|
||||
<div class="flex size-7 items-center justify-center text-[11.5px] tabular-nums text-muted-foreground">
|
||||
<span v-if="isCurrent" class="music-eq" :class="{ 'is-paused': !store.playing }"><i /><i /><i /></span>
|
||||
<template v-else>
|
||||
<span class="group-hover:hidden">{{ index + 1 }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="hidden text-foreground group-hover:block"
|
||||
:aria-label="'播放 ' + item.title"
|
||||
@click.stop="play"
|
||||
>
|
||||
<Play class="size-3.5" />
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 封面 -->
|
||||
<img
|
||||
v-if="coverUrl"
|
||||
v-if="coverUrl && !coverFailed"
|
||||
:src="coverUrl"
|
||||
class="size-10 shrink-0 rounded-md object-cover"
|
||||
class="size-9 rounded object-cover"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
@error="($event.target as HTMLImageElement).style.display = 'none'"
|
||||
referrerpolicy="no-referrer"
|
||||
@error="coverFailed = true"
|
||||
/>
|
||||
<div v-else class="flex size-10 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
|
||||
<div v-else class="flex size-9 items-center justify-center rounded bg-muted text-muted-foreground">
|
||||
<Music2 class="size-4" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex size-10 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground opacity-0 transition-opacity group-hover:opacity-100"
|
||||
:disabled="store.playing && store.current?.guid === item.guid"
|
||||
@click="store.playItem(item)"
|
||||
>
|
||||
<Play class="size-4 translate-x-[1px]" />
|
||||
</button>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm font-medium">{{ item.title }}</div>
|
||||
<div class="truncate text-xs text-muted-foreground">
|
||||
{{ item.artistNames }}<template v-if="item.artistNames && item.album"> · </template>{{ item.album }}
|
||||
<span v-if="item.source === 'local'" class="ml-1 rounded bg-muted px-1 text-[10px]">本地</span>
|
||||
<!-- 标题 + 元信息 -->
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="truncate text-[13px]" :class="isCurrent ? 'font-medium text-foreground' : ''">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
<span
|
||||
v-if="showSource"
|
||||
class="shrink-0 text-muted-foreground"
|
||||
:title="item.source === 'local' ? '本地文件' : '飞牛 NAS'"
|
||||
>
|
||||
<HardDrive v-if="item.source === 'local'" class="size-3" />
|
||||
<Cloud v-else class="size-3" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="truncate text-[11.5px] text-muted-foreground">{{ item.artistNames || '—' }}</div>
|
||||
</div>
|
||||
|
||||
<span class="shrink-0 text-xs tabular-nums text-muted-foreground">{{ durationText }}</span>
|
||||
<!-- 专辑 -->
|
||||
<div v-if="showAlbum" class="truncate text-[11.5px] text-muted-foreground">
|
||||
{{ item.album || '' }}
|
||||
</div>
|
||||
|
||||
<!-- 时长(本地无时长时退化为文件大小) -->
|
||||
<div class="text-right text-[11.5px] tabular-nums text-muted-foreground">
|
||||
<span v-if="item.durationMs">{{ durationText }}</span>
|
||||
<span v-else-if="fallbackText" class="text-[11px]">{{ fallbackText }}</span>
|
||||
<span v-else>--:--</span>
|
||||
</div>
|
||||
|
||||
<!-- 操作菜单 -->
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button size="icon" variant="ghost" class="size-8">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="size-7 opacity-0 transition-opacity group-hover:opacity-100 data-[state=open]:opacity-100"
|
||||
:aria-label="'更多操作:' + item.title"
|
||||
>
|
||||
<MoreHorizontal class="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" class="w-48">
|
||||
<DropdownMenuLabel>{{ item.title }}</DropdownMenuLabel>
|
||||
<DropdownMenuLabel class="truncate">{{ item.title }}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem @click="store.playItem(item)">
|
||||
<DropdownMenuItem @click="play">
|
||||
<Play class="size-4" /> 播放
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
v-if="item.source === 'local' && store.fnosLoggedIn && store.libraryNasPath"
|
||||
:disabled="store.uploading"
|
||||
@click="uploadToFeiniu"
|
||||
>
|
||||
<Cloud class="size-4" /> 上传到飞牛曲库
|
||||
<DropdownMenuItem @click="store.playNext(item); toast.success('已加入下一首')">
|
||||
<ListPlus class="size-4" /> 下一首播放
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="store.addToQueue(item); toast.success('已加入队列')">
|
||||
<Plus class="size-4" /> 加入队列
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Plus class="size-4" /> 加入歌单
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent class="w-48 max-h-64 overflow-y-auto">
|
||||
<DropdownMenuSubContent class="max-h-64 w-48 overflow-y-auto">
|
||||
<DropdownMenuItem v-if="store.playlists.length === 0" disabled>还没有歌单</DropdownMenuItem>
|
||||
<DropdownMenuItem v-for="p in store.playlists" :key="p.id" @click="addToPlaylist(p.id)">
|
||||
{{ p.name }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
|
||||
<template v-if="item.source === 'local'">
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
v-if="store.webdavReady"
|
||||
:disabled="store.uploading"
|
||||
@click="uploadToFeiniu"
|
||||
>
|
||||
<Cloud class="size-4" /> 上传到飞牛曲库
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem disabled>
|
||||
<FolderOpen class="size-4" /> {{ item.dir || '未知目录' }}
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
|
||||
<template v-if="playlistId">
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
:disabled="(playlistIndex ?? 0) <= 0"
|
||||
@click="store.moveInPlaylist(playlistId, playlistIndex ?? 0, (playlistIndex ?? 0) - 1)"
|
||||
>
|
||||
<ArrowUp class="size-4" /> 上移
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
:disabled="(playlistIndex ?? 0) >= playlistLength - 1"
|
||||
@click="store.moveInPlaylist(playlistId, playlistIndex ?? 0, (playlistIndex ?? 0) + 1)"
|
||||
>
|
||||
<ArrowDown class="size-4" /> 下移
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
class="text-destructive focus:text-destructive"
|
||||
@click="store.removeFromPlaylist(playlistId, playlistIndex ?? 0)"
|
||||
>
|
||||
<Trash2 class="size-4" /> 从歌单移除
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user