待看名单增加导入导出,修复待看名单比较受baseurl影响的问题
This commit is contained in:
@@ -198,33 +198,11 @@ const getAddButtonTitle = () => {
|
||||
const isCurrentUrl = (url: string) => {
|
||||
const currentUrl = currentPageUrl.value;
|
||||
|
||||
// 直接比较完整URL
|
||||
if (currentUrl === url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 比较路径部分
|
||||
try {
|
||||
const currentUrlObj = new URL(currentUrl);
|
||||
const currentPath = currentUrlObj.pathname + currentUrlObj.search;
|
||||
|
||||
// 如果是完整URL,提取路径部分比较
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
const urlObj = new URL(url);
|
||||
const urlPath = urlObj.pathname + urlObj.search;
|
||||
return currentPath === urlPath;
|
||||
}
|
||||
|
||||
// 如果是相对路径,直接比较
|
||||
let relativePath = url;
|
||||
if (!relativePath.startsWith('/')) {
|
||||
relativePath = '/' + relativePath;
|
||||
}
|
||||
|
||||
return currentPath === relativePath;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
// 使用store中的路径提取方法进行比较
|
||||
const currentPath = watchlistStore.extractUrlPath(currentUrl);
|
||||
const itemPath = watchlistStore.extractUrlPath(url);
|
||||
|
||||
return currentPath === itemPath;
|
||||
};
|
||||
|
||||
// 解析批量URL
|
||||
|
||||
@@ -4,6 +4,19 @@
|
||||
<h3>待看名单</h3>
|
||||
<div class="header-actions">
|
||||
<span class="item-count-text">{{ itemCount }} 项</span>
|
||||
<button @click="exportWatchlist" class="export-btn" title="导出待看名单">
|
||||
<SvgIcon name="download" class="export-icon" />
|
||||
</button>
|
||||
<button @click="triggerImport" class="import-btn" title="导入待看名单">
|
||||
<SvgIcon name="upload" class="import-icon" />
|
||||
</button>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept=".json"
|
||||
style="display: none"
|
||||
@change="handleFileImport"
|
||||
/>
|
||||
<button @click="$emit('showAddModal')" class="add-btn" title="手动添加">
|
||||
<SvgIcon name="add" class="add-icon" />
|
||||
</button>
|
||||
@@ -27,6 +40,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useWatchlistStore } from '@/stores/watchlist';
|
||||
import WatchlistControls from './WatchlistControls.vue';
|
||||
import WatchlistContent from './WatchlistContent.vue';
|
||||
import type { WatchlistItem } from '@/services/watchlist';
|
||||
@@ -58,6 +73,46 @@ defineEmits<{
|
||||
edit: [item: WatchlistItem];
|
||||
delete: [id: string];
|
||||
}>();
|
||||
|
||||
// 获取watchlist store实例
|
||||
const watchlistStore = useWatchlistStore();
|
||||
|
||||
// 文件输入引用
|
||||
const fileInput = ref<HTMLInputElement>();
|
||||
|
||||
// 导出功能
|
||||
const exportWatchlist = () => {
|
||||
watchlistStore.exportWatchlist();
|
||||
};
|
||||
|
||||
// 触发导入文件选择
|
||||
const triggerImport = () => {
|
||||
fileInput.value?.click();
|
||||
};
|
||||
|
||||
// 处理文件导入
|
||||
const handleFileImport = async (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const result = await watchlistStore.importWatchlist(file);
|
||||
|
||||
if (result.success) {
|
||||
alert(result.message);
|
||||
} else {
|
||||
alert(`导入失败: ${result.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导入过程中发生错误:', error);
|
||||
alert('导入过程中发生错误,请检查文件格式');
|
||||
} finally {
|
||||
// 清空文件输入
|
||||
target.value = '';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -120,7 +175,9 @@ defineEmits<{
|
||||
}
|
||||
|
||||
.add-btn,
|
||||
.close-btn {
|
||||
.close-btn,
|
||||
.export-btn,
|
||||
.import-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
@@ -139,13 +196,25 @@ defineEmits<{
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.export-btn:hover {
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.import-btn:hover {
|
||||
background: var(--color-info-light);
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: var(--color-danger-light);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.add-icon,
|
||||
.close-icon {
|
||||
.close-icon,
|
||||
.export-icon,
|
||||
.import-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
+125
-19
@@ -104,34 +104,32 @@ export const useWatchlistStore = defineStore('watchlist', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 检查URL是否已存在
|
||||
const hasUrl = (url: string) => {
|
||||
return items.value.some(item => item.url === url);
|
||||
};
|
||||
|
||||
// 根据URL查找项目
|
||||
const findByUrl = (url: string) => {
|
||||
return items.value.find(item => item.url === url);
|
||||
};
|
||||
|
||||
// 清除错误
|
||||
const clearError = () => {
|
||||
error.value = null;
|
||||
};
|
||||
|
||||
// 提取作者ID的工具函数
|
||||
const extractAuthorId = (url: string) => {
|
||||
// 提取URL路径的工具函数(忽略baseURL)
|
||||
const extractUrlPath = (url: string) => {
|
||||
try {
|
||||
let path = '';
|
||||
|
||||
// 处理完整URL
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
const urlObj = new URL(url);
|
||||
path = urlObj.pathname;
|
||||
return urlObj.pathname + urlObj.search + urlObj.hash;
|
||||
} else {
|
||||
// 处理相对路径
|
||||
path = url.startsWith('/') ? url : '/' + url;
|
||||
return url.startsWith('/') ? url : '/' + url;
|
||||
}
|
||||
} catch {
|
||||
// 如果解析失败,返回原始URL
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
// 提取作者ID的工具函数
|
||||
const extractAuthorId = (url: string) => {
|
||||
try {
|
||||
const path = extractUrlPath(url);
|
||||
|
||||
// 匹配 /artist/数字 的模式
|
||||
const match = path.match(/\/artist\/(\d+)/);
|
||||
@@ -141,14 +139,35 @@ export const useWatchlistStore = defineStore('watchlist', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 检查URL是否已存在(只比较路径部分)
|
||||
const hasUrl = (url: string) => {
|
||||
const targetPath = extractUrlPath(url);
|
||||
return items.value.some(item => {
|
||||
const itemPath = extractUrlPath(item.url);
|
||||
return itemPath === targetPath;
|
||||
});
|
||||
};
|
||||
|
||||
// 根据URL查找项目(只比较路径部分)
|
||||
const findByUrl = (url: string) => {
|
||||
const targetPath = extractUrlPath(url);
|
||||
return items.value.find(item => {
|
||||
const itemPath = extractUrlPath(item.url);
|
||||
return itemPath === targetPath;
|
||||
});
|
||||
};
|
||||
|
||||
// 检查是否有相同作者但不同页面的项目
|
||||
const findSameAuthor = (url: string) => {
|
||||
const authorId = extractAuthorId(url);
|
||||
if (!authorId) return null;
|
||||
|
||||
const targetPath = extractUrlPath(url);
|
||||
|
||||
return items.value.find(item => {
|
||||
const itemAuthorId = extractAuthorId(item.url);
|
||||
return itemAuthorId === authorId && item.url !== url;
|
||||
const itemPath = extractUrlPath(item.url);
|
||||
return itemAuthorId === authorId && itemPath !== targetPath;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -165,6 +184,88 @@ export const useWatchlistStore = defineStore('watchlist', () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 导出待看名单数据
|
||||
const exportWatchlist = () => {
|
||||
const exportData = {
|
||||
version: '1.0',
|
||||
exportTime: new Date().toISOString(),
|
||||
items: items.value.map(item => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
createdAt: item.createdAt,
|
||||
updatedAt: item.updatedAt
|
||||
}))
|
||||
};
|
||||
|
||||
const dataStr = JSON.stringify(exportData, null, 2);
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(dataBlob);
|
||||
link.download = `watchlist-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(link.href);
|
||||
};
|
||||
|
||||
// 导入待看名单数据
|
||||
const importWatchlist = async (file: File) => {
|
||||
try {
|
||||
const text = await file.text();
|
||||
const importData = JSON.parse(text);
|
||||
|
||||
// 验证数据格式
|
||||
if (!importData.items || !Array.isArray(importData.items)) {
|
||||
throw new Error('无效的导入文件格式');
|
||||
}
|
||||
|
||||
// 统计导入结果
|
||||
let successCount = 0;
|
||||
let skipCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const item of importData.items) {
|
||||
try {
|
||||
// 检查是否已存在(使用路径比较)
|
||||
if (hasUrl(item.url)) {
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 添加项目
|
||||
const success = await addItem({
|
||||
url: item.url,
|
||||
title: item.title
|
||||
});
|
||||
|
||||
if (success) {
|
||||
successCount++;
|
||||
} else {
|
||||
errorCount++;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('导入项目失败:', item, err);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `导入完成:成功 ${successCount} 项,跳过 ${skipCount} 项,失败 ${errorCount} 项`,
|
||||
stats: { successCount, skipCount, errorCount }
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('导入失败:', err);
|
||||
return {
|
||||
success: false,
|
||||
message: err instanceof Error ? err.message : '导入失败',
|
||||
stats: { successCount: 0, skipCount: 0, errorCount: 0 }
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// 状态
|
||||
items,
|
||||
@@ -185,6 +286,11 @@ export const useWatchlistStore = defineStore('watchlist', () => {
|
||||
extractAuthorId,
|
||||
findSameAuthor,
|
||||
hasSameAuthor,
|
||||
findItemsByAuthor
|
||||
findItemsByAuthor,
|
||||
// 导出导入功能
|
||||
exportWatchlist,
|
||||
importWatchlist,
|
||||
// URL路径提取工具
|
||||
extractUrlPath
|
||||
};
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user