181 lines
4.5 KiB
Vue
181 lines
4.5 KiB
Vue
<template>
|
|
<div class="library-card">
|
|
<div class="card-icon">
|
|
<FolderOpen :size="32" />
|
|
</div>
|
|
<h4 class="card-name">{{ props.name }}</h4>
|
|
|
|
<div v-if="scanStatus" class="scan-status" :class="{ running: scanStatus.running, failed: !!scanStatus.error }">
|
|
<span class="scan-status-title">
|
|
{{ scanStatus.running ? '扫描中' : scanStatus.error ? '扫描失败' : '最近一次扫描' }}
|
|
</span>
|
|
<span v-if="scanStatus.running && scanStatus.report" class="scan-status-text">
|
|
{{ scanStatus.report.processed }} / {{ scanStatus.report.total_files || 0 }}
|
|
</span>
|
|
<span v-else-if="scanStatus.report" class="scan-status-text">
|
|
新增 {{ scanStatus.report.added || 0 }} · 变更 {{ scanStatus.report.updated || 0 }} · 删除 {{ scanStatus.report.deleted || 0 }} · 跳过 {{ scanStatus.report.skipped || 0 }} · 失败 {{ scanStatus.report.failed_files?.length || 0 }}
|
|
</span>
|
|
<span v-else-if="scanStatus.error" class="scan-status-text">{{ scanStatus.error }}</span>
|
|
</div>
|
|
|
|
<DropDown>
|
|
<template v-slot:trigger>
|
|
<button class="card-menu-btn" aria-label="更多操作"><EllipsisVertical :size="16" /></button>
|
|
</template>
|
|
<button class="dropdown-item" @click="viewSongs">
|
|
<ListMusic class="dropdown-icon" :size="16" />
|
|
<span class="dropdown-text">查看歌曲</span>
|
|
</button>
|
|
<button class="dropdown-item" :disabled="scanStatus?.running" @click="scanCard">
|
|
<RotateCw class="dropdown-icon" :class="{ spinning: scanStatus?.running }" :size="16" />
|
|
<span class="dropdown-text">{{ scanStatus?.running ? '扫描中' : '扫描' }}</span>
|
|
</button>
|
|
<button class="dropdown-item danger" type="button" @click="deleteCard">
|
|
<Trash2 class="dropdown-icon" :size="16" />
|
|
<span class="dropdown-text">删除</span>
|
|
</button>
|
|
</DropDown>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { EllipsisVertical, FolderOpen, ListMusic, RotateCw, Trash2 } from 'lucide-vue-next'
|
|
import DropDown from './DropDown.vue'
|
|
|
|
const props = defineProps({
|
|
name: String,
|
|
id: Number,
|
|
scanStatus: Object,
|
|
})
|
|
const emit = defineEmits(['delete', 'scan', 'viewSongs'])
|
|
|
|
const scanCard = () => {
|
|
if (props.scanStatus?.running) return
|
|
emit('scan', props.id)
|
|
}
|
|
const deleteCard = () => {
|
|
emit('delete', props.id)
|
|
}
|
|
const viewSongs = () => {
|
|
emit('viewSongs', props.id)
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.library-card {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--size-2);
|
|
background: linear-gradient(135deg, var(--gray-1) 0%, var(--gray-2) 100%);
|
|
border: 1px solid var(--border);
|
|
aspect-ratio: var(--ratio-square);
|
|
border-radius: var(--radius-3);
|
|
padding: var(--size-6);
|
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
overflow: visible;
|
|
}
|
|
|
|
.library-card:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: var(--shadow-3);
|
|
border-color: var(--gray-4);
|
|
}
|
|
|
|
.card-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: var(--radius-3);
|
|
background-color: var(--brand-light);
|
|
color: var(--brand);
|
|
margin-bottom: var(--size-1);
|
|
}
|
|
|
|
.card-name {
|
|
font-size: var(--font-size-1);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
text-align: center;
|
|
word-break: break-word;
|
|
line-height: 1.3;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.scan-status {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 2px;
|
|
min-height: 40px;
|
|
text-align: center;
|
|
}
|
|
|
|
.scan-status-title {
|
|
font-size: var(--font-size-0);
|
|
color: var(--text-secondary);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.scan-status-text {
|
|
font-size: var(--font-size-0);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.scan-status.running .scan-status-title {
|
|
color: var(--brand);
|
|
}
|
|
|
|
.scan-status.failed .scan-status-title {
|
|
color: var(--red-7);
|
|
}
|
|
|
|
.card-menu-btn {
|
|
position: absolute;
|
|
top: var(--size-2);
|
|
right: var(--size-2);
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: var(--size-1);
|
|
border-radius: var(--radius-1);
|
|
color: var(--text-muted);
|
|
transition: all 0.15s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
opacity: 0;
|
|
}
|
|
|
|
.library-card:hover .card-menu-btn {
|
|
opacity: 1;
|
|
}
|
|
|
|
.card-menu-btn:hover {
|
|
background-color: var(--gray-3);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.dropdown-item:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.spinning {
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
from {
|
|
transform: rotate(0deg);
|
|
}
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
</style>
|