待看名单增加导入导出,修复待看名单比较受baseurl影响的问题
This commit is contained in:
@@ -198,33 +198,11 @@ const getAddButtonTitle = () => {
|
|||||||
const isCurrentUrl = (url: string) => {
|
const isCurrentUrl = (url: string) => {
|
||||||
const currentUrl = currentPageUrl.value;
|
const currentUrl = currentPageUrl.value;
|
||||||
|
|
||||||
// 直接比较完整URL
|
// 使用store中的路径提取方法进行比较
|
||||||
if (currentUrl === url) {
|
const currentPath = watchlistStore.extractUrlPath(currentUrl);
|
||||||
return true;
|
const itemPath = watchlistStore.extractUrlPath(url);
|
||||||
}
|
|
||||||
|
|
||||||
// 比较路径部分
|
return currentPath === itemPath;
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 解析批量URL
|
// 解析批量URL
|
||||||
|
|||||||
@@ -4,6 +4,19 @@
|
|||||||
<h3>待看名单</h3>
|
<h3>待看名单</h3>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<span class="item-count-text">{{ itemCount }} 项</span>
|
<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="手动添加">
|
<button @click="$emit('showAddModal')" class="add-btn" title="手动添加">
|
||||||
<SvgIcon name="add" class="add-icon" />
|
<SvgIcon name="add" class="add-icon" />
|
||||||
</button>
|
</button>
|
||||||
@@ -27,6 +40,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useWatchlistStore } from '@/stores/watchlist';
|
||||||
import WatchlistControls from './WatchlistControls.vue';
|
import WatchlistControls from './WatchlistControls.vue';
|
||||||
import WatchlistContent from './WatchlistContent.vue';
|
import WatchlistContent from './WatchlistContent.vue';
|
||||||
import type { WatchlistItem } from '@/services/watchlist';
|
import type { WatchlistItem } from '@/services/watchlist';
|
||||||
@@ -58,6 +73,46 @@ defineEmits<{
|
|||||||
edit: [item: WatchlistItem];
|
edit: [item: WatchlistItem];
|
||||||
delete: [id: string];
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -120,7 +175,9 @@ defineEmits<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
.add-btn,
|
.add-btn,
|
||||||
.close-btn {
|
.close-btn,
|
||||||
|
.export-btn,
|
||||||
|
.import-btn {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -139,13 +196,25 @@ defineEmits<{
|
|||||||
color: var(--color-primary);
|
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 {
|
.close-btn:hover {
|
||||||
background: var(--color-danger-light);
|
background: var(--color-danger-light);
|
||||||
color: var(--color-danger);
|
color: var(--color-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-icon,
|
.add-icon,
|
||||||
.close-icon {
|
.close-icon,
|
||||||
|
.export-icon,
|
||||||
|
.import-icon {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|||||||
+124
-18
@@ -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 = () => {
|
const clearError = () => {
|
||||||
error.value = null;
|
error.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 提取作者ID的工具函数
|
// 提取URL路径的工具函数(忽略baseURL)
|
||||||
const extractAuthorId = (url: string) => {
|
const extractUrlPath = (url: string) => {
|
||||||
try {
|
try {
|
||||||
let path = '';
|
|
||||||
|
|
||||||
// 处理完整URL
|
// 处理完整URL
|
||||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
path = urlObj.pathname;
|
return urlObj.pathname + urlObj.search + urlObj.hash;
|
||||||
} else {
|
} 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/数字 的模式
|
// 匹配 /artist/数字 的模式
|
||||||
const match = path.match(/\/artist\/(\d+)/);
|
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 findSameAuthor = (url: string) => {
|
||||||
const authorId = extractAuthorId(url);
|
const authorId = extractAuthorId(url);
|
||||||
if (!authorId) return null;
|
if (!authorId) return null;
|
||||||
|
|
||||||
|
const targetPath = extractUrlPath(url);
|
||||||
|
|
||||||
return items.value.find(item => {
|
return items.value.find(item => {
|
||||||
const itemAuthorId = extractAuthorId(item.url);
|
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 {
|
return {
|
||||||
// 状态
|
// 状态
|
||||||
items,
|
items,
|
||||||
@@ -185,6 +286,11 @@ export const useWatchlistStore = defineStore('watchlist', () => {
|
|||||||
extractAuthorId,
|
extractAuthorId,
|
||||||
findSameAuthor,
|
findSameAuthor,
|
||||||
hasSameAuthor,
|
hasSameAuthor,
|
||||||
findItemsByAuthor
|
findItemsByAuthor,
|
||||||
|
// 导出导入功能
|
||||||
|
exportWatchlist,
|
||||||
|
importWatchlist,
|
||||||
|
// URL路径提取工具
|
||||||
|
extractUrlPath
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user