This commit is contained in:
root
2026-01-03 13:26:31 +05:00
parent e186e36a74
commit 606f5af28d
12 changed files with 2768 additions and 517 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -51,6 +51,13 @@ const menuItems = [
iconColor: 'text-primary-500',
route: '/marketing-analysis/analysis'
},
{
id: 'analysis-dashboard',
label: 'Dashboard',
icon: 'pi pi-chart-line',
iconColor: 'text-primary-500',
route: '/marketing-analysis/dashboard'
},
{
id: 'analyses-list',
label: 'Мои анализы',
@@ -162,16 +162,46 @@
<p class="text-sm font-semibold text-surface-900 dark:text-surface-0">{{ post.theme }}</p>
</div>
<!-- Изображение поста -->
<div v-if="hasImage(post) && !hasImageError(`${date}-${idx}`)" class="mb-3">
<div v-if="hasImage(post) && !hasImageError(`${date}-${idx}`)" class="mb-3" style="position: relative">
<img :src="getImageUrl(post)" :alt="post.theme" class="post-image" loading="lazy" @error="handleImageError(`${date}-${idx}`)" />
<Button
icon="pi pi-refresh"
size="small"
severity="secondary"
class="regenerate-image-btn"
:loading="isImageRegenerating(getPostIndex(post))"
:disabled="isImageRegenerating(getPostIndex(post))"
v-tooltip.top="'Регенерировать изображение'"
@click="handleRegenerateImage(post, getPostIndex(post))"
/>
</div>
<div v-else-if="hasImage(post) && hasImageError(`${date}-${idx}`)" class="mb-3 no-image-placeholder">
<div v-else-if="hasImage(post) && hasImageError(`${date}-${idx}`)" class="mb-3 no-image-placeholder" style="position: relative">
<i class="pi pi-image text-4xl text-surface-400 dark:text-surface-600"></i>
<p class="text-xs text-surface-500 dark:text-surface-400 mt-2">Изображение недоступно</p>
<Button
icon="pi pi-refresh"
size="small"
severity="secondary"
class="regenerate-image-btn"
:loading="isImageRegenerating(getPostIndex(post))"
:disabled="isImageRegenerating(getPostIndex(post))"
v-tooltip.top="'Регенерировать изображение'"
@click="handleRegenerateImage(post, getPostIndex(post))"
/>
</div>
<div v-else class="mb-3 no-image-placeholder">
<div v-else class="mb-3 no-image-placeholder" style="position: relative">
<i class="pi pi-image text-4xl text-surface-400 dark:text-surface-600"></i>
<p class="text-xs text-surface-500 dark:text-surface-400 mt-2">Изображение не сгенерировано</p>
<Button
icon="pi pi-refresh"
size="small"
severity="secondary"
class="regenerate-image-btn"
:loading="isImageRegenerating(getPostIndex(post))"
:disabled="isImageRegenerating(getPostIndex(post))"
v-tooltip.top="'Регенерировать изображение'"
@click="handleRegenerateImage(post, getPostIndex(post))"
/>
</div>
<div class="mb-3">
<p class="text-sm font-medium text-surface-600 dark:text-surface-400 mb-1">Текст поста:</p>
@@ -222,16 +252,46 @@
<p class="text-sm font-semibold text-surface-900 dark:text-surface-0">{{ post.theme }}</p>
</div>
<!-- Изображение поста -->
<div v-if="hasImage(post) && !hasImageError(idx)" class="mb-3">
<div v-if="hasImage(post) && !hasImageError(idx)" class="mb-3" style="position: relative">
<img :src="getImageUrl(post)" :alt="post.theme" class="post-image" loading="lazy" @error="handleImageError(idx)" />
<Button
icon="pi pi-refresh"
size="small"
severity="secondary"
class="regenerate-image-btn"
:loading="isImageRegenerating(idx)"
:disabled="isImageRegenerating(idx)"
v-tooltip.top="'Регенерировать изображение'"
@click="handleRegenerateImage(post, idx)"
/>
</div>
<div v-else-if="hasImage(post) && hasImageError(idx)" class="mb-3 no-image-placeholder">
<div v-else-if="hasImage(post) && hasImageError(idx)" class="mb-3 no-image-placeholder" style="position: relative">
<i class="pi pi-image text-4xl text-surface-400 dark:text-surface-600"></i>
<p class="text-xs text-surface-500 dark:text-surface-400 mt-2">Изображение недоступно</p>
<Button
icon="pi pi-refresh"
size="small"
severity="secondary"
class="regenerate-image-btn"
:loading="isImageRegenerating(idx)"
:disabled="isImageRegenerating(idx)"
v-tooltip.top="'Регенерировать изображение'"
@click="handleRegenerateImage(post, idx)"
/>
</div>
<div v-else class="mb-3 no-image-placeholder">
<div v-else class="mb-3 no-image-placeholder" style="position: relative">
<i class="pi pi-image text-4xl text-surface-400 dark:text-surface-600"></i>
<p class="text-xs text-surface-500 dark:text-surface-400 mt-2">Изображение не сгенерировано</p>
<Button
icon="pi pi-refresh"
size="small"
severity="secondary"
class="regenerate-image-btn"
:loading="isImageRegenerating(idx)"
:disabled="isImageRegenerating(idx)"
v-tooltip.top="'Регенерировать изображение'"
@click="handleRegenerateImage(post, idx)"
/>
</div>
<div class="mb-3">
<p class="text-sm font-medium text-surface-600 dark:text-surface-400 mb-1">Текст поста:</p>
@@ -329,6 +389,7 @@ const missingPlatforms = ref([]);
const imageErrors = ref(new Set()); // Track image load errors by post index
const imageBlobUrls = ref({}); // Cache blob URLs for images with auth (reactive object)
const executingTasks = ref(new Set()); // Track which tasks are currently being executed
const regeneratingImages = ref(new Set()); // Track which images are being regenerated (by post index)
// Computed
const postsByDate = computed(() => {
@@ -633,6 +694,117 @@ const isTaskExecuting = (taskId) => {
return taskId ? executingTasks.value.has(taskId) : false;
};
// Get post index in postCalendar array
const getPostIndex = (post) => {
if (!strategyData.value?.strategy?.postCalendar) return -1;
return strategyData.value.strategy.postCalendar.findIndex((p) => p === post);
};
// Check if image is regenerating
const isImageRegenerating = (postIndex) => {
return postIndex >= 0 && regeneratingImages.value.has(postIndex);
};
// Handle regenerate image
const handleRegenerateImage = async (post, postIndex) => {
if (!strategyId.value || postIndex < 0) {
toast.add({
severity: 'error',
summary: 'Ошибка',
detail: 'Не удалось определить параметры для регенерации изображения',
life: 3000
});
return;
}
// Add to regenerating set
regeneratingImages.value.add(postIndex);
try {
const result = await MarketingService.regenerateImage(strategyId.value, postIndex);
toast.add({
severity: 'success',
summary: 'Изображение регенерируется',
detail: 'Изображение успешно отправлено на регенерацию. Оно будет обновлено через несколько секунд.',
life: 5000
});
// Reload strategy data after a short delay to get updated image
setTimeout(async () => {
try {
const updatedStrategy = await MarketingService.getStrategy(strategyId.value);
if (updatedStrategy.status === 'completed' && updatedStrategy.strategy) {
// Update the post in strategyData
if (updatedStrategy.strategy.postCalendar && updatedStrategy.strategy.postCalendar[postIndex]) {
const updatedPost = updatedStrategy.strategy.postCalendar[postIndex];
// Update the post in current strategyData
if (strategyData.value?.strategy?.postCalendar) {
strategyData.value.strategy.postCalendar[postIndex] = updatedPost;
}
// Clear image error for this post
imageErrors.value.delete(postIndex);
imageErrors.value.delete(`${post.publishDate}-${postIndex}`);
// Reload image if it has a filename
if (updatedPost.imageFilename) {
// Remove old blob URL if exists
if (imageBlobUrls.value[updatedPost.imageFilename]) {
URL.revokeObjectURL(imageBlobUrls.value[updatedPost.imageFilename]);
}
// Load new image
const blobUrl = await MarketingService.loadImageAsBlobUrl(updatedPost.imageFilename);
if (blobUrl) {
imageBlobUrls.value[updatedPost.imageFilename] = blobUrl;
}
}
}
}
} catch (error) {
console.error('Ошибка при обновлении стратегии после регенерации:', error);
}
}, 3000);
} catch (error) {
console.error('Ошибка при регенерации изображения:', error);
// Handle specific error types
let errorMessage = error.message || 'Не удалось регенерировать изображение. Попробуйте позже.';
let errorSummary = 'Ошибка';
let errorLife = 5000;
if (error.code === 'RATE_LIMIT_EXCEEDED' || error.status === 429) {
errorSummary = 'Превышен лимит запросов';
errorMessage = error.message || 'Превышен лимит запросов к сервису генерации изображений. Пожалуйста, подождите несколько минут и попробуйте снова.';
errorLife = 7000;
} else if (error.code === 'UNAUTHORIZED' || error.status === 401) {
errorSummary = 'Ошибка авторизации';
errorMessage = 'Необходимо войти в систему';
} else if (error.code === 'FORBIDDEN' || error.status === 403) {
errorSummary = 'Доступ запрещен';
errorMessage = 'У вас нет доступа к этой операции';
} else if (error.code === 'NOT_FOUND' || error.status === 404) {
errorSummary = 'Не найдено';
errorMessage = 'Стратегия или пост не найдены';
} else if (error.code === 'VALIDATION_ERROR' || error.status === 400) {
errorSummary = 'Ошибка валидации';
errorMessage = error.message || 'Проверьте правильность данных';
}
toast.add({
severity: 'error',
summary: errorSummary,
detail: errorMessage,
life: errorLife
});
} finally {
// Remove from regenerating set
regeneratingImages.value.delete(postIndex);
}
};
// Export to CSV
const exportToCsv = () => {
if (!strategyData.value?.strategy?.postCalendar) return;
@@ -920,4 +1092,12 @@ onBeforeUnmount(() => {
background: var(--surface-800);
border-color: var(--surface-700);
}
.regenerate-image-btn {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
</style>