增加下载同步功能,可以导出已下载作品。避免另一个设备的重复下载,修复日志bug
This commit is contained in:
@@ -8,6 +8,7 @@ import { useUpdateStore } from '@/stores/update'
|
||||
import SettingsWidget from '@/components/common/SettingsWidget.vue'
|
||||
import DownloadProgressWidget from '@/components/common/DownloadProgressWidget.vue'
|
||||
import WatchlistWidget from '@/components/common/WatchlistWidget.vue'
|
||||
import RegistryWidget from '@/components/common/RegistryWidget.vue'
|
||||
import UpdateChecker from '@/components/common/UpdateChecker.vue'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -96,6 +97,9 @@ onMounted(async () => {
|
||||
<!-- 设置小组件 - 只在登录时显示 -->
|
||||
<SettingsWidget v-if="isLoggedIn" />
|
||||
|
||||
<!-- 下载注册表管理小组件 - 只在登录时显示 -->
|
||||
<RegistryWidget v-if="isLoggedIn" />
|
||||
|
||||
<!-- 下载进度小组件 - 只在登录时显示,在下载管理页面隐藏 -->
|
||||
<DownloadProgressWidget v-if="showDownloadWidget" />
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ export const actionIcons = {
|
||||
'empty2': 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z',
|
||||
'refresh': 'M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z',
|
||||
'cleanup': 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z',
|
||||
|
||||
'down':"M18.414 10.656a2 2 0 0 0-2.828 0L14 12.242V5a2 2 0 0 0-4 0v7.242l-1.586-1.586a2 2 0 1 0-2.828 2.828L12 19.898l6.414-6.414a2 2 0 0 0 0-2.828"
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
<template>
|
||||
<div class="registry-widget">
|
||||
<!-- 注册表管理按钮 -->
|
||||
<button @click="togglePanel" class="registry-toggle" :class="{ active: isOpen }" title="下载注册表管理">
|
||||
<SvgIcon name="down" class="registry-icon" />
|
||||
</button>
|
||||
|
||||
<!-- 注册表管理面板 -->
|
||||
<div v-if="isOpen" class="registry-panel">
|
||||
<div class="registry-header">
|
||||
<h3>下载注册表管理</h3>
|
||||
<button @click="togglePanel" class="close-btn" title="关闭">
|
||||
<SvgIcon name="close" class="close-icon" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="registry-content">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading">
|
||||
<LoadingSpinner text="处理中..." />
|
||||
</div>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<div v-else-if="error" class="error">
|
||||
<ErrorMessage :error="error" @dismiss="clearError" />
|
||||
</div>
|
||||
|
||||
<!-- 成功提示 -->
|
||||
<div v-if="successMessage" class="success-message">
|
||||
<div class="success-content">
|
||||
<SvgIcon name="success" class="success-icon" />
|
||||
<span>{{ successMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="registry-stats">
|
||||
<h4>统计信息</h4>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">作者数量:</span>
|
||||
<span class="stat-value">{{ stats?.totalArtists || 0 }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">作品数量:</span>
|
||||
<span class="stat-value">{{ stats?.totalArtworks || 0 }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">最后更新:</span>
|
||||
<span class="stat-value">{{ formatDate(stats?.lastUpdated) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置选项 -->
|
||||
<div class="registry-config">
|
||||
<h4>配置选项</h4>
|
||||
<div class="config-form">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="radio" v-model="detectionMethod" value="registry" @change="updateDetectionMethod" />
|
||||
使用注册表检测
|
||||
</label>
|
||||
<small>优先使用JSON注册表检测作品是否已下载</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="radio" v-model="detectionMethod" value="scan" @change="updateDetectionMethod" />
|
||||
使用扫盘检测
|
||||
</label>
|
||||
<small>直接扫描文件系统检测作品是否已下载</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="radio" v-model="detectionMethod" value="hybrid" @change="updateDetectionMethod" />
|
||||
混合检测模式
|
||||
</label>
|
||||
<small>优先使用注册表检测,失败时回退到扫盘检测</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="registry-actions">
|
||||
<h4>管理操作</h4>
|
||||
<div class="action-buttons">
|
||||
<button @click="refreshStats" class="btn btn-secondary" :disabled="loading">
|
||||
<SvgIcon name="refresh" class="btn-icon" />
|
||||
刷新统计
|
||||
</button>
|
||||
|
||||
<button @click="exportRegistry" class="btn btn-primary" :disabled="loading">
|
||||
<SvgIcon name="download" class="btn-icon" />
|
||||
导出注册表
|
||||
</button>
|
||||
|
||||
<label class="btn btn-primary" :class="{ disabled: loading }">
|
||||
<SvgIcon name="upload" class="btn-icon" />
|
||||
导入注册表
|
||||
<input type="file" @change="handleFileImport" accept=".json" style="display: none;" :disabled="loading" />
|
||||
</label>
|
||||
|
||||
<button @click="rebuildRegistry" class="btn btn-warning" :disabled="loading">
|
||||
<SvgIcon name="rebuild" class="btn-icon" />
|
||||
同步文件系统
|
||||
</button>
|
||||
|
||||
<button @click="cleanupRegistry" class="btn btn-danger" :disabled="loading">
|
||||
<SvgIcon name="trash" class="btn-icon" />
|
||||
清理注册表
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useRegistryStore } from '@/stores/registry';
|
||||
import SvgIcon from './SvgIcon.vue';
|
||||
import LoadingSpinner from './LoadingSpinner.vue';
|
||||
import ErrorMessage from './ErrorMessage.vue';
|
||||
|
||||
const registryStore = useRegistryStore();
|
||||
const isOpen = ref(false);
|
||||
const successMessage = ref<string | null>(null);
|
||||
|
||||
// 从store中获取响应式数据
|
||||
const { stats, loading, error, config } = storeToRefs(registryStore);
|
||||
|
||||
// 检测方法选择 - 不设置默认值,等待从后端配置初始化
|
||||
const detectionMethod = ref<'registry' | 'scan' | 'hybrid'>();
|
||||
|
||||
// 切换面板显示
|
||||
const togglePanel = () => {
|
||||
isOpen.value = !isOpen.value;
|
||||
if (isOpen.value) {
|
||||
refreshStats();
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新统计信息
|
||||
const refreshStats = async () => {
|
||||
await registryStore.fetchStats();
|
||||
};
|
||||
|
||||
// 导出注册表
|
||||
const exportRegistry = async () => {
|
||||
const result = await registryStore.exportRegistry();
|
||||
if (result.success) {
|
||||
showSuccess('注册表导出成功');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理文件导入
|
||||
const handleFileImport = async (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const result = await registryStore.importRegistry(file);
|
||||
if (result.success) {
|
||||
showSuccess(`注册表导入成功,处理了 ${result.data?.imported || 0} 条记录`);
|
||||
}
|
||||
|
||||
// 清空文件输入
|
||||
target.value = '';
|
||||
};
|
||||
|
||||
// 重建注册表
|
||||
const rebuildRegistry = async () => {
|
||||
if (!confirm('确定要同步文件系统到注册表吗?这将扫描整个下载目录并添加新发现的作品,可能需要一些时间。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await registryStore.rebuildRegistry();
|
||||
if (result.success) {
|
||||
showSuccess(`文件系统同步完成,新增 ${result.data?.addedArtworks || 0} 个作品,跳过 ${result.data?.skippedArtworks || 0} 个已存在作品`);
|
||||
}
|
||||
};
|
||||
|
||||
// 清理注册表
|
||||
const cleanupRegistry = async () => {
|
||||
if (!confirm('确定要清理注册表吗?这将移除不存在的文件记录。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await registryStore.cleanupRegistry();
|
||||
if (result.success) {
|
||||
showSuccess(`注册表清理完成,移除了 ${result.data?.removedArtworks || 0} 条无效记录`);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新检测方法配置
|
||||
const updateDetectionMethod = async () => {
|
||||
let useRegistryCheck = false;
|
||||
let fallbackToScan = false;
|
||||
|
||||
switch (detectionMethod.value) {
|
||||
case 'registry':
|
||||
useRegistryCheck = true;
|
||||
fallbackToScan = false;
|
||||
break;
|
||||
case 'scan':
|
||||
useRegistryCheck = false;
|
||||
fallbackToScan = false;
|
||||
break;
|
||||
case 'hybrid':
|
||||
useRegistryCheck = true;
|
||||
fallbackToScan = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const result = await registryStore.updateConfig({
|
||||
useRegistryCheck,
|
||||
fallbackToScan
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
showSuccess('配置更新成功');
|
||||
}
|
||||
};
|
||||
|
||||
// 更新配置(保留原方法以防其他地方调用)
|
||||
const updateConfig = async () => {
|
||||
const result = await registryStore.updateConfig({
|
||||
useRegistryCheck: config.value.useRegistryCheck,
|
||||
fallbackToScan: config.value.fallbackToScan
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
showSuccess('配置更新成功');
|
||||
}
|
||||
};
|
||||
|
||||
// 清除错误
|
||||
const clearError = () => {
|
||||
registryStore.clearError();
|
||||
};
|
||||
|
||||
// 显示成功消息
|
||||
const showSuccess = (message: string) => {
|
||||
successMessage.value = message;
|
||||
setTimeout(() => {
|
||||
successMessage.value = null;
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString?: string): string => {
|
||||
if (!dateString) return '未知';
|
||||
return new Date(dateString).toLocaleString('zh-CN');
|
||||
};
|
||||
|
||||
// 初始化检测方法
|
||||
const initDetectionMethod = () => {
|
||||
if (config.value.useRegistryCheck && config.value.fallbackToScan) {
|
||||
detectionMethod.value = 'hybrid';
|
||||
} else if (config.value.useRegistryCheck && !config.value.fallbackToScan) {
|
||||
detectionMethod.value = 'registry';
|
||||
} else {
|
||||
detectionMethod.value = 'scan';
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(async () => {
|
||||
// 从后端获取配置并初始化检测方法
|
||||
try {
|
||||
await registryStore.fetchConfig();
|
||||
initDetectionMethod();
|
||||
} catch (error) {
|
||||
console.error('获取配置失败:', error);
|
||||
// 如果获取配置失败,使用默认值
|
||||
detectionMethod.value = 'hybrid';
|
||||
}
|
||||
|
||||
// 初始化时加载统计数据
|
||||
refreshStats();
|
||||
});
|
||||
|
||||
// 监听配置变化,自动更新检测方法
|
||||
watch(config, () => {
|
||||
initDetectionMethod();
|
||||
}, { deep: true });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.registry-widget {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
left: 1rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.registry-toggle {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 50%;
|
||||
background: #3b82f6;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.registry-toggle:hover {
|
||||
background: #2563eb;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.registry-toggle.active {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.registry-icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.registry-panel {
|
||||
position: absolute;
|
||||
bottom: 4rem;
|
||||
left: 0;
|
||||
width: 400px;
|
||||
max-height: 600px;
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid #e5e7eb;
|
||||
overflow: hidden;
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.registry-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.5rem;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.registry-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.375rem;
|
||||
color: #6b7280;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #e5e7eb;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.registry-content {
|
||||
padding: 1.5rem;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.loading, .error {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: #dcfce7;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.success-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #166534;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.registry-stats, .registry-config, .registry-actions {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.registry-stats h4, .registry-config h4, .registry-actions h4 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.config-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-group small {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:disabled, .btn.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6b7280;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning:hover:not(:disabled) {
|
||||
background: #d97706;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.registry-panel {
|
||||
width: calc(100vw - 2rem);
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -213,6 +213,40 @@ class DownloadService {
|
||||
eventSource.close();
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 获取下载注册表统计信息
|
||||
*/
|
||||
async getRegistryStats() {
|
||||
return apiService.get('/api/download/registry/stats');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出下载注册表
|
||||
*/
|
||||
async exportRegistry() {
|
||||
return apiService.get('/api/download/registry/export');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入下载注册表
|
||||
*/
|
||||
async importRegistry(registryData: any) {
|
||||
return apiService.post('/api/download/registry/import', { registryData });
|
||||
}
|
||||
|
||||
/**
|
||||
* 重建下载注册表
|
||||
*/
|
||||
async rebuildRegistry() {
|
||||
return apiService.post('/api/download/registry/rebuild');
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理下载注册表
|
||||
*/
|
||||
async cleanupRegistry() {
|
||||
return apiService.post('/api/download/registry/cleanup');
|
||||
}
|
||||
}
|
||||
|
||||
export default new DownloadService();
|
||||
export default new DownloadService();
|
||||
@@ -0,0 +1,227 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import downloadService from '@/services/download';
|
||||
import { getApiBaseUrl } from '@/services/api';
|
||||
|
||||
export interface RegistryStats {
|
||||
totalArtists: number;
|
||||
totalArtworks: number;
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
export interface RegistryConfig {
|
||||
useRegistryCheck: boolean;
|
||||
fallbackToScan: boolean;
|
||||
}
|
||||
|
||||
export const useRegistryStore = defineStore('registry', () => {
|
||||
// 状态
|
||||
const stats = ref<RegistryStats | null>(null);
|
||||
const config = ref<RegistryConfig>({
|
||||
useRegistryCheck: true,
|
||||
fallbackToScan: false
|
||||
});
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
// 获取注册表统计信息
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await downloadService.getRegistryStats();
|
||||
if (response.success) {
|
||||
// 映射API响应数据到组件期望的格式
|
||||
stats.value = {
|
||||
totalArtists: response.data.artistCount || 0,
|
||||
totalArtworks: response.data.artworkCount || 0,
|
||||
lastUpdated: response.data.updated_at || response.data.created_at || ''
|
||||
};
|
||||
} else {
|
||||
throw new Error(response.error || '获取统计信息失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '获取统计信息失败';
|
||||
console.error('获取注册表统计信息失败:', err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 导出注册表
|
||||
const exportRegistry = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await downloadService.exportRegistry();
|
||||
|
||||
// 创建下载链接
|
||||
const blob = new Blob([JSON.stringify(response, null, 2)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `download-registry-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
return { success: true };
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '导出注册表失败';
|
||||
console.error('导出注册表失败:', err);
|
||||
return { success: false, error: error.value };
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 导入注册表
|
||||
const importRegistry = async (file: File) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const text = await file.text();
|
||||
const registryData = JSON.parse(text);
|
||||
|
||||
const response = await downloadService.importRegistry(registryData);
|
||||
if (response.success) {
|
||||
// 刷新统计信息
|
||||
await fetchStats();
|
||||
return { success: true, data: response.data };
|
||||
} else {
|
||||
throw new Error(response.error || '导入注册表失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '导入注册表失败';
|
||||
console.error('导入注册表失败:', err);
|
||||
return { success: false, error: error.value };
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 重建注册表
|
||||
const rebuildRegistry = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await downloadService.rebuildRegistry();
|
||||
if (response.success) {
|
||||
// 刷新统计信息
|
||||
await fetchStats();
|
||||
return { success: true, data: response.data };
|
||||
} else {
|
||||
throw new Error(response.error || '重建注册表失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '重建注册表失败';
|
||||
console.error('重建注册表失败:', err);
|
||||
return { success: false, error: error.value };
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 清理注册表
|
||||
const cleanupRegistry = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await downloadService.cleanupRegistry();
|
||||
if (response.success) {
|
||||
// 刷新统计信息
|
||||
await fetchStats();
|
||||
return { success: true, data: response.data };
|
||||
} else {
|
||||
throw new Error(response.error || '清理注册表失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '清理注册表失败';
|
||||
console.error('清理注册表失败:', err);
|
||||
return { success: false, error: error.value };
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取配置
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const response = await fetch(`${getApiBaseUrl()}/api/download/registry/config`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
config.value = result.data;
|
||||
return result.data;
|
||||
} else {
|
||||
throw new Error(result.error || '获取配置失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '获取配置失败';
|
||||
console.error('获取配置失败:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
// 更新配置
|
||||
const updateConfig = async (newConfig: Partial<RegistryConfig>) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await fetch(`${getApiBaseUrl()}/api/download/registry/config`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(newConfig),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
config.value = { ...config.value, ...result.data };
|
||||
return { success: true, data: result.data };
|
||||
} else {
|
||||
throw new Error(result.error || '更新配置失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '更新配置失败';
|
||||
console.error('更新配置失败:', err);
|
||||
return { success: false, error: error.value };
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 清除错误
|
||||
const clearError = () => {
|
||||
error.value = null;
|
||||
};
|
||||
|
||||
return {
|
||||
// 状态
|
||||
stats,
|
||||
config,
|
||||
loading,
|
||||
error,
|
||||
|
||||
// 方法
|
||||
fetchStats,
|
||||
exportRegistry,
|
||||
importRegistry,
|
||||
rebuildRegistry,
|
||||
cleanupRegistry,
|
||||
fetchConfig,
|
||||
updateConfig,
|
||||
clearError
|
||||
};
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { getApiBaseUrl } from '@/services/api'
|
||||
|
||||
export interface RepositoryConfig {
|
||||
downloadDir: string
|
||||
@@ -183,7 +184,16 @@ export const useRepositoryStore = defineStore('repository', () => {
|
||||
|
||||
// 检查作品是否已下载
|
||||
const checkArtworkDownloaded = async (artworkId: number) => {
|
||||
return await apiCall(`/check-downloaded/${artworkId}`)
|
||||
// 使用新的下载检测API,支持注册表检测和回退机制
|
||||
const response = await fetch(`${getApiBaseUrl()}/api/download/check/${artworkId}`)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
const result = await response.json()
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'API调用失败')
|
||||
}
|
||||
return result.data
|
||||
}
|
||||
|
||||
// 检查目录是否存在
|
||||
@@ -223,4 +233,4 @@ export const useRepositoryStore = defineStore('repository', () => {
|
||||
checkDirectoryExists,
|
||||
migrateFromOldToNew,
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -258,7 +258,7 @@ const handleDownload = async () => {
|
||||
// 清理下载状态
|
||||
downloading.value = true;
|
||||
|
||||
// 如果已经下载过,则强制重新下载(跳过现有文件检查)
|
||||
// 如果已经下载过,则强制重新下载(不跳过现有文件)
|
||||
const skipExisting = !isDownloaded.value;
|
||||
const response = await downloadService.downloadArtwork(artwork.value.id, {
|
||||
skipExisting
|
||||
|
||||
Reference in New Issue
Block a user