功能:新增单部电影重新刮削功能,优化刮削日志展示样式

This commit is contained in:
liRQ 2026-06-26 14:55:48 +08:00
parent 69494434e9
commit 83e23b7365
5 changed files with 214 additions and 19 deletions

View File

@ -414,6 +414,7 @@
@batch-delete="handleBatchDelete" @batch-delete="handleBatchDelete"
@toggle-multi-select-exit="toggleMultiSelectMode" @toggle-multi-select-exit="toggleMultiSelectMode"
@scrape-status="handleScrapeStatus" @scrape-status="handleScrapeStatus"
@rescrape="handleScrapeMovie"
/> />
</div> </div>
</section> </section>
@ -558,17 +559,33 @@
</div> </div>
<template v-else> <template v-else>
<div class="scraper-log-list"> <div class="scraper-log-list">
<div v-for="log in scraperLogs" :key="log.id" class="log-entry"> <div v-for="log in scraperLogs" :key="log.id" class="log-entry log-entry--expandable">
<div class="log-entry__header"> <!-- 日志头部可点击展开 -->
<div> <div class="log-entry__header" @click="toggleLogDetail(log.id)">
<div class="log-time">{{ log.createTime || log.create_time }}</div> <div class="log-entry__summary">
<span class="log-entry__toggle">{{ log.showDetail ? '▾' : '▸' }}</span>
<span class="log-entry__time">{{ log.scanTime || log.createTime || '-' }}</span>
<span class="log-entry__desc">{{ log.description || '刮削任务执行记录' }}</span>
</div> </div>
<div class="log-stats"> <div class="log-entry__stats">
<span class="stat-total">总共: {{ log.totalProcessed || log.total_processed || 0 }}</span> <span class="stat-total">总共: {{ log.totalMovies || log.total || 0 }}</span>
<span class="stat-success">成功: {{ log.totalSuccess || log.total_success || 0 }}</span> <span class="stat-success">成功: {{ log.successCount || log.success || 0 }}</span>
<span class="stat-fail">失败: {{ log.totalFailed || log.total_failed || 0 }}</span> <span class="stat-fail">失败: {{ log.failCount || log.failed || 0 }}</span>
</div> </div>
</div> </div>
<!-- 详细日志可展开 -->
<Transition name="slide-down">
<div v-if="log.showDetail && log.detailList && log.detailList.length > 0" class="log-entry__detail">
<div v-for="(item, idx) in log.detailList" :key="idx" class="detail-item">
<span class="detail-item__name">{{ item.movieName || item.title || '未知电影' }}</span>
<span class="detail-item__status" :class="`detail-item__status--${item.status}`">
{{ item.status === 'success' ? '✓ 成功' : item.status === 'failed' ? '✗ 失败' : '⋯ 处理中' }}
</span>
<span v-if="item.message" class="detail-item__message">{{ item.message }}</span>
</div>
</div>
</Transition>
</div> </div>
</div> </div>
@ -797,6 +814,7 @@ import {
requestState, requestState,
resetData, resetData,
saveTmdbConfig as apiSaveTmdbConfig, saveTmdbConfig as apiSaveTmdbConfig,
scrapeMovie,
updateDrive, updateDrive,
updateFolderInDrive, updateFolderInDrive,
updateMovieInDrive, updateMovieInDrive,
@ -1695,6 +1713,14 @@ function handleLoadMoreScraperLogs() {
loadMoreScraperLogs() loadMoreScraperLogs()
} }
// /
function toggleLogDetail(logId) {
const log = scraperLogs.value.find(l => l.id === logId)
if (log) {
log.showDetail = !log.showDetail
}
}
// TMDB // TMDB
function handleTmdbConfig() { function handleTmdbConfig() {
showUserMenu.value = false showUserMenu.value = false
@ -2578,6 +2604,36 @@ async function doBatchScrapeAll() {
} }
} }
/**
* 重新刮削单部电影
*/
async function handleScrapeMovie(node) {
if (!node || node.type !== 'file') {
setErrorMessage('只有电影文件才能重新刮削')
return
}
driveLoading.value = true
try {
const result = await scrapeMovie(node.id)
if (result?.code === 200) {
setSuccessMessage(`电影“${node.title}”刮削请求已提交`)
//
await loadDriveFolders(activeSheetId.value)
await loadDuplicateItems()
} else {
setErrorMessage(result?.message || '刮削失败')
}
} catch (err) {
console.error('刮削失败', err)
setErrorMessage(err?.payload?.message || err?.message || '刮削失败')
} finally {
driveLoading.value = false
}
}
/** /**
* 批量刮削选中的电影文件 * 批量刮削选中的电影文件
*/ */

View File

@ -747,6 +747,19 @@ export function batchScrapeAll() {
}) })
} }
// ==================== 单部影片刮削 API ====================
/**
* 重新刮削单部电影
* @param {number} movieId - 电影ID
* @returns {Promise} 返回格式{ code: 200, message: "刮削完成", data: { movieId, title, rating, posterUrl } }
*/
export function scrapeMovie(movieId) {
return request(`${API_BASE_URL}/tmdb-scraper/scrape?movieId=${movieId}`, {
method: 'POST',
})
}
// ==================== 刮削状态 API ==================== // ==================== 刮削状态 API ====================
/** /**

View File

@ -26,6 +26,7 @@
@edit="handleEdit" @edit="handleEdit"
@delete="handleDelete" @delete="handleDelete"
@scrape-status="handleScrapeStatus" @scrape-status="handleScrapeStatus"
@rescrape="handleRescrape"
/> />
</div> </div>
</div> </div>
@ -53,7 +54,7 @@ const props = defineProps({
}, },
}) })
const emit = defineEmits(['add-folder', 'add-file', 'expand', 'edit', 'delete', 'toggle-select', 'batch-delete', 'toggle-multi-select-exit', 'scrape-status']) const emit = defineEmits(['add-folder', 'add-file', 'expand', 'edit', 'delete', 'toggle-select', 'batch-delete', 'toggle-multi-select-exit', 'scrape-status', 'rescrape'])
function handleAddFolder(parentId) { function handleAddFolder(parentId) {
emit('add-folder', parentId) emit('add-folder', parentId)
@ -79,6 +80,10 @@ function handleScrapeStatus(node) {
emit('scrape-status', node) emit('scrape-status', node)
} }
function handleRescrape(node) {
emit('rescrape', node)
}
function handleToggleSelect(nodeId) { function handleToggleSelect(nodeId) {
emit('toggle-select', nodeId) emit('toggle-select', nodeId)
} }

View File

@ -74,6 +74,15 @@
> >
+📄 +📄
</button> </button>
<button
v-if="node.type === 'file'"
type="button"
class="action-btn action-btn--rescrape"
title="重新刮削"
@click="handleRescrape"
>
🔄
</button>
<button <button
v-if="node.type === 'file'" v-if="node.type === 'file'"
type="button" type="button"
@ -144,6 +153,7 @@
@edit="$emit('edit', $event)" @edit="$emit('edit', $event)"
@delete="$emit('delete', $event)" @delete="$emit('delete', $event)"
@scrape-status="$emit('scrape-status', $event)" @scrape-status="$emit('scrape-status', $event)"
@rescrape="$emit('rescrape', $event)"
/> />
</div> </div>
</Transition> </Transition>
@ -176,7 +186,7 @@ const props = defineProps({
}, },
}) })
const emit = defineEmits(['add-folder', 'add-file', 'expand', 'edit', 'delete', 'toggle-select', 'scrape-status']) const emit = defineEmits(['add-folder', 'add-file', 'expand', 'edit', 'delete', 'toggle-select', 'scrape-status', 'rescrape'])
const isSelected = computed(() => props.selectedIds.has(props.node.id)) const isSelected = computed(() => props.selectedIds.has(props.node.id))
@ -270,6 +280,11 @@ function handleScrapeStatus() {
emit('scrape-status', props.node) emit('scrape-status', props.node)
} }
function handleRescrape() {
console.log('[TreeNode] handleRescrape called, node:', props.node)
emit('rescrape', props.node)
}
function getScrapeStatusText(status) { function getScrapeStatusText(status) {
const statusMap = { const statusMap = {
scraping: '刮削中', scraping: '刮削中',

View File

@ -2171,6 +2171,115 @@ canvas {
color: #b91c1c; color: #b91c1c;
} }
/* ===== 可展开刮削日志样式 ===== */
.log-entry--expandable {
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
}
.log-entry--expandable:active {
transform: scale(0.995);
}
.log-entry__toggle {
display: inline-block;
width: 20px;
text-align: center;
color: #6366f1;
font-weight: bold;
transition: transform 0.2s ease;
flex-shrink: 0;
}
.log-entry__summary {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.log-entry__time {
color: #64748b;
font-size: 13px;
font-family: monospace;
}
.log-entry__desc {
color: #475569;
font-size: 14px;
font-weight: 500;
}
/* 展开后的详细日志面板 */
.log-entry__detail {
margin-top: 12px;
padding: 16px;
background: rgba(255, 255, 255, 0.8);
border-radius: 12px;
border: 1px solid rgba(99, 102, 241, 0.1);
display: flex;
flex-direction: column;
gap: 8px;
}
/* 详细日志项 */
.detail-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: rgba(249, 250, 251, 0.8);
border-radius: 8px;
font-size: 13px;
transition: background 0.15s ease;
}
.detail-item:hover {
background: rgba(243, 244, 246, 0.9);
}
.detail-item__name {
flex: 1;
font-weight: 500;
color: #1e293b;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.detail-item__status {
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
.detail-item__status--success {
background: rgba(34, 197, 94, 0.12);
color: #15803d;
}
.detail-item__status--failed {
background: rgba(239, 68, 68, 0.12);
color: #b91c1c;
}
.detail-item__status--pending {
background: rgba(245, 158, 11, 0.12);
color: #b45309;
}
.detail-item__message {
color: #64748b;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 300px;
}
.log-time { .log-time {
font-weight: 600; font-weight: 600;
color: var(--ink); color: var(--ink);
@ -3272,19 +3381,16 @@ canvas {
} }
/* ====== 网盘操作加载遮罩层 ====== */ /* ====== 网盘操作加载遮罩层 ====== */
/* 覆盖整个管理区域,显示旋转动画和提示文字 */ /* 覆盖整个视口,显示旋转动画和提示文字,始终固定在屏幕中央 */
.drive-loading-overlay { .drive-loading-overlay {
position: absolute; /* 相对于 manage-layout 定位 */ position: fixed; /* 固定定位,覆盖整个视口 */
top: 0; inset: 0; /* 等价于 top: 0; left: 0; right: 0; bottom: 0 */
left: 0; background: rgba(255, 255, 255, 0.92); /* 半透明白色背景 */
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.88); /* 半透明白色背景 */
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 9999; /* 确保在所有内容之上 */ z-index: 10000; /* 确保在所有内容之上,包括全局加载遮罩 */
border-radius: 8px; backdrop-filter: blur(4px); /* 毛玻璃效果,增强视觉层次 */
} }
/* 加载动画容器垂直居中显示spinner和文字 */ /* 加载动画容器垂直居中显示spinner和文字 */