feat: ajout de la gestion des sources de contenu avec création de composants, formulaires et API pour l'importation, l'exportation et la configuration des sources de scraping.
This commit is contained in:
parent
32b4e4fbb2
commit
dac2f91998
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<div>
|
||||
<Toolbar :config="toolbarConfig" class="sticky top-16 z-10" />
|
||||
|
||||
<div class="container mx-auto px-4 py-6">
|
||||
<!-- Back Navigation -->
|
||||
<div class="mb-6">
|
||||
<button
|
||||
@click="goBack"
|
||||
class="flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors">
|
||||
<ArrowLeftIcon class="w-5 h-5" />
|
||||
<span>Retour aux configurations</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loadingCurrentSource" class="flex justify-center py-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="currentSourceError" class="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-lg p-4 mb-6">
|
||||
<div class="flex items-center">
|
||||
<ExclamationTriangleIcon class="w-5 h-5 text-red-400 mr-2" />
|
||||
<p class="text-red-800 dark:text-red-200">{{ currentSourceError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div v-else class="max-w-4xl mx-auto">
|
||||
<ContentSourceForm
|
||||
:source="currentSource"
|
||||
:saving="saving"
|
||||
:error="saveError"
|
||||
@submit="handleSubmit"
|
||||
@test="handleTest" />
|
||||
</div>
|
||||
|
||||
<!-- Test Results Modal -->
|
||||
<div v-if="showTestResults" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] overflow-hidden">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-600">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold">Résultats du test</h3>
|
||||
<button
|
||||
@click="showTestResults = false"
|
||||
class="text-gray-400 hover:text-gray-600">
|
||||
<XMarkIcon class="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 overflow-y-auto">
|
||||
<div v-if="testResults.success" class="space-y-4">
|
||||
<div class="flex items-center text-green-600 mb-4">
|
||||
<CheckCircleIcon class="w-5 h-5 mr-2" />
|
||||
<span class="font-medium">Test réussi !</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-medium mb-2">URL testée:</h4>
|
||||
<code class="block bg-gray-100 dark:bg-gray-700 p-2 rounded text-sm">
|
||||
{{ testResults.testedUrl }}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div v-if="testResults.images && testResults.images.length > 0">
|
||||
<h4 class="font-medium mb-2">Images trouvées ({{ testResults.images.length }}):</h4>
|
||||
<div class="grid grid-cols-2 gap-2 max-h-64 overflow-y-auto">
|
||||
<img
|
||||
v-for="(image, index) in testResults.images.slice(0, 6)"
|
||||
:key="index"
|
||||
:src="image"
|
||||
:alt="`Image ${index + 1}`"
|
||||
class="w-full h-32 object-cover rounded border"
|
||||
@error="handleImageError" />
|
||||
</div>
|
||||
<p v-if="testResults.images.length > 6" class="text-sm text-gray-500 mt-2">
|
||||
Et {{ testResults.images.length - 6 }} autres images...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div class="flex items-center text-red-600 mb-4">
|
||||
<XCircleIcon class="w-5 h-5 mr-2" />
|
||||
<span class="font-medium">Test échoué</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-medium mb-2">Erreur:</h4>
|
||||
<div class="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded p-3">
|
||||
<code class="text-sm text-red-800 dark:text-red-200">
|
||||
{{ testResults.error }}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div v-if="showSuccessMessage" class="fixed bottom-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg">
|
||||
Configuration {{ isEditing ? 'mise à jour' : 'créée' }} avec succès !
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
CheckCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
XCircleIcon,
|
||||
XMarkIcon
|
||||
} from '@heroicons/vue/24/outline';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import Toolbar from '../../../../shared/components/ui/Toolbar.vue';
|
||||
import { useContentSourceStore } from '../../application/store/contentSourceStore';
|
||||
import ContentSourceForm from '../components/ContentSourceForm.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const contentSourceStore = useContentSourceStore();
|
||||
|
||||
const {
|
||||
currentSource,
|
||||
loadingCurrentSource,
|
||||
currentSourceError,
|
||||
saving,
|
||||
saveError
|
||||
} = storeToRefs(contentSourceStore);
|
||||
|
||||
// Local state
|
||||
const showTestResults = ref(false);
|
||||
const showSuccessMessage = ref(false);
|
||||
const testResults = ref({});
|
||||
|
||||
const isEditing = computed(() => !!route.params.id);
|
||||
|
||||
// Load source if editing, clear if creating new
|
||||
onMounted(async () => {
|
||||
if (isEditing.value) {
|
||||
await contentSourceStore.loadSource(route.params.id);
|
||||
} else {
|
||||
// Clear current source immediately when creating new
|
||||
contentSourceStore.clearCurrentSource();
|
||||
}
|
||||
});
|
||||
|
||||
// Toolbar configuration
|
||||
const toolbarConfig = {
|
||||
leftSection: [],
|
||||
rightSection: []
|
||||
};
|
||||
|
||||
// Actions
|
||||
const goBack = () => {
|
||||
router.push({ name: 'scrapper-configurations' });
|
||||
};
|
||||
|
||||
const handleSubmit = async (formData) => {
|
||||
try {
|
||||
if (isEditing.value) {
|
||||
await contentSourceStore.updateSource(route.params.id, formData);
|
||||
} else {
|
||||
await contentSourceStore.createSource(formData);
|
||||
}
|
||||
|
||||
// Clear current source and errors before redirecting
|
||||
contentSourceStore.clearCurrentSource();
|
||||
contentSourceStore.clearErrors();
|
||||
|
||||
// Show success message briefly then redirect
|
||||
showSuccessMessage.value = true;
|
||||
|
||||
// Use nextTick to ensure the DOM is updated before redirecting
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
// Navigate back to list
|
||||
await router.push({ name: 'scrapper-configurations' });
|
||||
|
||||
// Hide success message after navigation
|
||||
showSuccessMessage.value = false;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sauvegarde:', error);
|
||||
// Don't redirect if there's an error
|
||||
}
|
||||
};
|
||||
|
||||
const handleTest = async ({ configuration, testData }) => {
|
||||
try {
|
||||
// Simulate test API call - You'll need to implement this endpoint
|
||||
const testUrl = configuration.chapterUrlFormat
|
||||
.replace('{slug}', testData.mangaSlug)
|
||||
.replace('{chapterNumber}', testData.chapterNumber);
|
||||
|
||||
// Mock test results for now
|
||||
testResults.value = {
|
||||
success: true,
|
||||
testedUrl: testUrl,
|
||||
images: [
|
||||
'https://via.placeholder.com/400x600/008000/FFFFFF?text=Page+1',
|
||||
'https://via.placeholder.com/400x600/FF0000/FFFFFF?text=Page+2',
|
||||
'https://via.placeholder.com/400x600/0000FF/FFFFFF?text=Page+3',
|
||||
'https://via.placeholder.com/400x600/FFA500/FFFFFF?text=Page+4'
|
||||
]
|
||||
};
|
||||
|
||||
showTestResults.value = true;
|
||||
} catch (error) {
|
||||
testResults.value = {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
showTestResults.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageError = (event) => {
|
||||
event.target.style.display = 'none';
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user