Files
butterfliu/web/src/components/LibraryCard.vue
lzw-723 79d47c81e7
All checks were successful
Go CI / test-and-build (push) Successful in 13s
Web CI / lint-test-build (push) Successful in 31s
实现增量扫描
2026-04-11 14:38:23 +08:00

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>