待看名单增加覆盖功能

This commit is contained in:
2025-10-10 12:14:52 +08:00
parent d1082a8677
commit 4e34063373
5 changed files with 227 additions and 14 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ class DownloadRegistry {
// 加载现有注册表 // 加载现有注册表
await this.loadRegistry(); await this.loadRegistry();
logger.info(`下载记录注册表初始化完成,总共包含${Object.keys(this.registry.artists).length}个作者,${this.getTotalArtworkCount()}作品`); logger.info(`下载记录注册表初始化完成,总共包含${Object.keys(this.registry.artists).length}个作者,${this.getTotalArtworkCount()}个作品`);
} catch (error) { } catch (error) {
logger.error('下载记录注册表初始化失败:', error); logger.error('下载记录注册表初始化失败:', error);
throw error; throw error;
+31 -2
View File
@@ -56,11 +56,13 @@
:batch-urls="batchUrls" :batch-urls="batchUrls"
:auto-generate-title="autoGenerateTitle" :auto-generate-title="autoGenerateTitle"
:parsed-urls="parsedUrls" :parsed-urls="parsedUrls"
:import-mode="importMode"
@update:mode="addMode = $event" @update:mode="addMode = $event"
@update:title="addTitle = $event" @update:title="addTitle = $event"
@update:url="addUrl = $event" @update:url="addUrl = $event"
@update:batchUrls="batchUrls = $event" @update:batchUrls="batchUrls = $event"
@update:autoGenerateTitle="autoGenerateTitle = $event" @update:autoGenerateTitle="autoGenerateTitle = $event"
@update:importMode="importMode = $event"
@quickAdd="fillQuickAdd" @quickAdd="fillQuickAdd"
@save="saveAdd" @save="saveAdd"
@cancel="cancelAdd" @cancel="cancelAdd"
@@ -92,6 +94,7 @@ const addUrl = ref('');
const addMode = ref<'single' | 'batch'>('single'); const addMode = ref<'single' | 'batch'>('single');
const batchUrls = ref(''); const batchUrls = ref('');
const autoGenerateTitle = ref(true); const autoGenerateTitle = ref(true);
const importMode = ref<'merge' | 'overwrite'>('merge');
// 搜索和排序 // 搜索和排序
const searchQuery = ref(''); const searchQuery = ref('');
@@ -401,7 +404,29 @@ const saveAdd = async () => {
} }
} else { } else {
// 批量添加模式 // 批量添加模式
const urlsToAdd = parsedUrls.value.filter(item => !item.isDuplicate); let urlsToAdd = parsedUrls.value;
if (importMode.value === 'merge') {
// 重合模式:只添加不重复的URL
urlsToAdd = urlsToAdd.filter(item => !item.isDuplicate);
} else if (importMode.value === 'overwrite') {
// 覆盖模式:删除所有现有项目,然后添加所有新项目
if (urlsToAdd.length === 0) return;
// 确认覆盖操作
if (!confirm('覆盖模式将删除所有现有的待看项目并导入新的项目。确定要继续吗?')) {
return;
}
// 删除所有现有项目
const allItems = items.value;
for (const item of allItems) {
await watchlistStore.deleteItem(item.id);
}
console.log(`已删除 ${allItems.length} 个现有项目`);
}
if (urlsToAdd.length === 0) return; if (urlsToAdd.length === 0) return;
// 依次添加每个URL // 依次添加每个URL
@@ -422,7 +447,11 @@ const saveAdd = async () => {
if (successCount > 0) { if (successCount > 0) {
cancelAdd(); cancelAdd();
console.log(`成功添加 ${successCount} 个项目`); if (importMode.value === 'overwrite') {
console.log(`覆盖导入完成:成功添加 ${successCount} 个项目`);
} else {
console.log(`重合导入完成:成功添加 ${successCount} 个项目`);
}
} }
} }
}; };
@@ -52,6 +52,35 @@
<!-- 批量添加模式 --> <!-- 批量添加模式 -->
<template v-else> <template v-else>
<!-- 导入模式选择 -->
<div class="form-group">
<label>导入模式</label>
<div class="import-mode-selector">
<button @click="$emit('update:importMode', 'merge')"
:class="['import-mode-btn', { active: importMode === 'merge' }]"
type="button">
<div class="mode-icon">🔄</div>
<div class="mode-info">
<div class="mode-title">重合模式</div>
<div class="mode-desc">跳过已存在的项目只添加新项目</div>
</div>
</button>
<button @click="$emit('update:importMode', 'overwrite')"
:class="['import-mode-btn', { active: importMode === 'overwrite' }]"
type="button">
<div class="mode-icon"></div>
<div class="mode-info">
<div class="mode-title">覆盖模式</div>
<div class="mode-desc">清空现有数据重新导入所有项目</div>
</div>
</button>
</div>
<small class="form-help">
<strong>重合模式</strong>保留现有数据只添加新的项目推荐<br>
<strong>覆盖模式</strong>删除所有现有数据重新导入谨慎使用
</small>
</div>
<div class="form-group"> <div class="form-group">
<label>批量URL列表</label> <label>批量URL列表</label>
<textarea :value="batchUrls" <textarea :value="batchUrls"
@@ -83,7 +112,8 @@ http://localhost:3001/artist/103047332
<div class="preview-list"> <div class="preview-list">
<div v-for="(item, index) in parsedUrls" :key="index" class="preview-item"> <div v-for="(item, index) in parsedUrls" :key="index" class="preview-item">
<div class="preview-url">{{ item.path }}</div> <div class="preview-url">{{ item.path }}</div>
<div v-if="item.isDuplicate" class="preview-status duplicate">已存在</div> <div v-if="importMode === 'overwrite'" class="preview-status overwrite">将导入</div>
<div v-else-if="item.isDuplicate" class="preview-status duplicate">已存在</div>
<div v-else class="preview-status new">新增</div> <div v-else class="preview-status new">新增</div>
</div> </div>
</div> </div>
@@ -116,6 +146,7 @@ interface Props {
batchUrls: string; batchUrls: string;
autoGenerateTitle: boolean; autoGenerateTitle: boolean;
parsedUrls: ParsedUrl[]; parsedUrls: ParsedUrl[];
importMode: 'merge' | 'overwrite';
} }
defineProps<Props>(); defineProps<Props>();
@@ -128,6 +159,7 @@ const emit = defineEmits<{
'update:url': [value: string]; 'update:url': [value: string];
'update:batchUrls': [value: string]; 'update:batchUrls': [value: string];
'update:autoGenerateTitle': [value: boolean]; 'update:autoGenerateTitle': [value: boolean];
'update:importMode': [value: 'merge' | 'overwrite'];
quickAdd: [path: string, title: string]; quickAdd: [path: string, title: string];
}>(); }>();
@@ -378,6 +410,64 @@ const handleSave = () => {
color: white; color: white;
} }
.preview-status.overwrite {
background: var(--color-primary);
color: white;
}
/* 导入模式选择器样式 */
.import-mode-selector {
display: flex;
flex-direction: column;
gap: 12px;
}
.import-mode-btn {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border: 2px solid var(--color-border);
border-radius: 8px;
background: var(--color-bg-secondary);
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s ease;
text-align: left;
}
.import-mode-btn:hover {
background: var(--color-bg-tertiary);
border-color: var(--color-primary-light);
}
.import-mode-btn.active {
background: var(--color-primary-light);
border-color: var(--color-primary);
color: var(--color-primary);
}
.mode-icon {
font-size: 20px;
flex-shrink: 0;
}
.mode-info {
flex: 1;
}
.mode-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 2px;
}
.mode-desc {
font-size: 12px;
opacity: 0.8;
line-height: 1.3;
}
.modal-actions { .modal-actions {
display: flex; display: flex;
gap: 12px; gap: 12px;
@@ -7,9 +7,27 @@
<button @click="exportWatchlist" class="export-btn" title="导出待看名单"> <button @click="exportWatchlist" class="export-btn" title="导出待看名单">
<SvgIcon name="download" class="export-icon" /> <SvgIcon name="download" class="export-icon" />
</button> </button>
<button @click="triggerImport" class="import-btn" title="导入待看名单"> <div class="import-section">
<SvgIcon name="upload" class="import-icon" /> <button @click="triggerImport" class="import-btn" title="导入待看名单">
</button> <SvgIcon name="upload" class="import-icon" />
</button>
<div class="import-mode-selector">
<button
@click="importMode = 'merge'"
:class="['mode-btn', { active: importMode === 'merge' }]"
title="重合模式:跳过已存在的项目"
>
重合
</button>
<button
@click="importMode = 'overwrite'"
:class="['mode-btn', { active: importMode === 'overwrite' }]"
title="覆盖模式:删除所有现有项目后导入"
>
覆盖
</button>
</div>
</div>
<input <input
ref="fileInput" ref="fileInput"
type="file" type="file"
@@ -77,6 +95,9 @@ defineEmits<{
// 获取watchlist store实例 // 获取watchlist store实例
const watchlistStore = useWatchlistStore(); const watchlistStore = useWatchlistStore();
// 导入模式状态
const importMode = ref<'merge' | 'overwrite'>('merge');
// 文件输入引用 // 文件输入引用
const fileInput = ref<HTMLInputElement>(); const fileInput = ref<HTMLInputElement>();
@@ -97,8 +118,17 @@ const handleFileImport = async (event: Event) => {
if (!file) return; if (!file) return;
// 如果是覆盖模式,先确认
if (importMode.value === 'overwrite') {
const confirmed = confirm('覆盖模式将删除所有现有的待看项目,确定要继续吗?');
if (!confirmed) {
target.value = '';
return;
}
}
try { try {
const result = await watchlistStore.importWatchlist(file); const result = await watchlistStore.importWatchlist(file, importMode.value);
if (result.success) { if (result.success) {
alert(result.message); alert(result.message);
@@ -219,6 +249,49 @@ const handleFileImport = async (event: Event) => {
height: 14px; height: 14px;
} }
/* 导入区域样式 */
.import-section {
display: flex;
align-items: center;
gap: 8px;
position: relative;
}
.import-mode-selector {
display: flex;
background: var(--color-bg-tertiary);
border-radius: 4px;
padding: 2px;
gap: 2px;
}
.mode-btn {
padding: 4px 8px;
font-size: 11px;
border: none;
border-radius: 3px;
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.mode-btn:hover {
background: var(--color-bg-hover);
color: var(--color-text-primary);
}
.mode-btn.active {
background: var(--color-primary);
color: white;
font-weight: 500;
}
.mode-btn.active:hover {
background: var(--color-primary-dark);
}
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 768px) { @media (max-width: 768px) {
.watchlist-panel { .watchlist-panel {
+27 -6
View File
@@ -211,7 +211,7 @@ export const useWatchlistStore = defineStore('watchlist', () => {
}; };
// 导入待看名单数据 // 导入待看名单数据
const importWatchlist = async (file: File) => { const importWatchlist = async (file: File, importMode: 'merge' | 'overwrite' = 'merge') => {
try { try {
const text = await file.text(); const text = await file.text();
const importData = JSON.parse(text); const importData = JSON.parse(text);
@@ -225,11 +225,25 @@ export const useWatchlistStore = defineStore('watchlist', () => {
let successCount = 0; let successCount = 0;
let skipCount = 0; let skipCount = 0;
let errorCount = 0; let errorCount = 0;
let deletedCount = 0;
// 如果是覆盖模式,先删除所有现有项目
if (importMode === 'overwrite') {
const allItems = items.value;
for (const item of allItems) {
try {
await deleteItem(item.id);
deletedCount++;
} catch (err) {
console.error('删除项目失败:', item, err);
}
}
}
for (const item of importData.items) { for (const item of importData.items) {
try { try {
// 检查是否已存在(使用路径比较) // 在重合模式下检查是否已存在
if (hasUrl(item.url)) { if (importMode === 'merge' && hasUrl(item.url)) {
skipCount++; skipCount++;
continue; continue;
} }
@@ -251,17 +265,24 @@ export const useWatchlistStore = defineStore('watchlist', () => {
} }
} }
let message = '';
if (importMode === 'overwrite') {
message = `覆盖导入完成:删除 ${deletedCount} 项,成功添加 ${successCount} 项,失败 ${errorCount}`;
} else {
message = `重合导入完成:成功 ${successCount} 项,跳过 ${skipCount} 项,失败 ${errorCount}`;
}
return { return {
success: true, success: true,
message: `导入完成:成功 ${successCount} 项,跳过 ${skipCount} 项,失败 ${errorCount}`, message,
stats: { successCount, skipCount, errorCount } stats: { successCount, skipCount, errorCount, deletedCount }
}; };
} catch (err) { } catch (err) {
console.error('导入失败:', err); console.error('导入失败:', err);
return { return {
success: false, success: false,
message: err instanceof Error ? err.message : '导入失败', message: err instanceof Error ? err.message : '导入失败',
stats: { successCount: 0, skipCount: 0, errorCount: 0 } stats: { successCount: 0, skipCount: 0, errorCount: 0, deletedCount: 0 }
}; };
} }
}; };