This commit is contained in:
root
2026-01-12 09:15:14 +05:00
parent 9d57e38222
commit a60c128bce
+185 -72
View File
@@ -1,7 +1,7 @@
<template>
<div class="marketing-promotion-page bg-surface-50 dark:bg-surface-900 min-h-screen p-6">
<Toast />
<div class="max-w-[1400px] mx-auto">
<div class="w-full mx-auto px-4">
<!-- Форма запуска генерации стратегии -->
<div v-if="!strategyId && !strategyData" class="card">
<div class="card-header mb-4">
@@ -91,35 +91,34 @@
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0">Недельный план</h3>
</div>
<div class="card-content">
<div class="grid">
<div v-for="plan in strategyData.strategy.weeklyPlans" :key="plan.weekNumber" class="col-12 md:col-6 lg:col-4 mb-4">
<div class="week-plan-card p-4 border-round bg-surface-0 dark:bg-surface-800 border-1 border-surface-200 dark:border-surface-700">
<div class="flex align-items-center justify-content-between mb-3">
<!-- <h4 class="text-lg font-semibold text-surface-900 dark:text-surface-0">Неделя {{ plan.weekNumber }}</h4> -->
<div class="weekly-plans-container">
<div v-for="plan in strategyData.strategy.weeklyPlans" :key="plan.weekNumber" class="weekly-plan-item">
<div class="week-plan-card p-3 border-round bg-surface-0 dark:bg-surface-800 border-1 border-surface-200 dark:border-surface-700 square-card">
<div class="flex align-items-center justify-content-between mb-2">
<Tag :value="`Неделя ${plan.weekNumber}`" severity="info" />
</div>
<div v-if="plan.mainThemes && plan.mainThemes.length > 0" class="mb-3">
<p class="text-sm font-medium text-surface-600 dark:text-surface-400 mb-2">Основные темы:</p>
<div v-if="plan.mainThemes && plan.mainThemes.length > 0" class="mb-2">
<p class="text-xs font-medium text-surface-600 dark:text-surface-400 mb-1">Основные темы:</p>
<ul class="list-none p-0 m-0">
<li v-for="(theme, idx) in plan.mainThemes" :key="idx" class="flex align-items-start mb-2">
<i class="pi pi-circle-fill text-primary-500 text-xs mr-2 mt-1"></i>
<span class="text-surface-700 dark:text-surface-300 text-sm">{{ theme }}</span>
<li v-for="(theme, idx) in plan.mainThemes" :key="idx" class="flex align-items-start mb-1">
<i class="pi pi-circle-fill text-primary-500 text-xs mr-1 mt-1"></i>
<span class="text-surface-700 dark:text-surface-300 text-xs line-height-2">{{ theme }}</span>
</li>
</ul>
</div>
<div v-if="plan.contentRecommendations" class="mb-3">
<p class="text-sm font-medium text-surface-600 dark:text-surface-400 mb-2">Рекомендации:</p>
<p class="text-surface-700 dark:text-surface-300 text-sm whitespace-pre-line">
<div v-if="plan.contentRecommendations" class="mb-2">
<p class="text-xs font-medium text-surface-600 dark:text-surface-400 mb-1">Рекомендации:</p>
<p class="text-surface-700 dark:text-surface-300 text-xs whitespace-pre-line line-height-2">
{{ plan.contentRecommendations }}
</p>
</div>
<div v-if="plan.priorityPlatforms && plan.priorityPlatforms.length > 0">
<p class="text-sm font-medium text-surface-600 dark:text-surface-400 mb-2">Платформы:</p>
<div class="flex flex-wrap gap-2">
<Tag v-for="platform in plan.priorityPlatforms" :key="platform" :value="platform" severity="secondary" />
<p class="text-xs font-medium text-surface-600 dark:text-surface-400 mb-1">Платформы:</p>
<div class="flex flex-wrap gap-1">
<Tag v-for="platform in plan.priorityPlatforms" :key="platform" :value="platform" severity="secondary" class="text-xs" />
</div>
</div>
</div>
@@ -146,24 +145,24 @@
<h4 class="text-lg font-semibold text-surface-900 dark:text-surface-0 mb-3">
{{ formatDate(date) }}
</h4>
<div class="grid">
<div v-for="(post, idx) in posts" :key="idx" class="col-12 md:col-6 lg:col-4 mb-3">
<div class="post-card p-4 border-round bg-surface-0 dark:bg-surface-800 border-1 border-surface-200 dark:border-surface-700 h-full">
<div class="flex align-items-center justify-content-between mb-3">
<div class="flex gap-2 flex-wrap">
<Tag :value="post.platform" severity="info" />
<Tag :value="post.contentType" severity="secondary" />
<div class="posts-container">
<div v-for="(post, idx) in posts" :key="idx" class="post-item">
<div class="post-card p-3 border-round bg-surface-0 dark:bg-surface-800 border-1 border-surface-200 dark:border-surface-700 square-card">
<div class="flex align-items-center justify-content-between mb-2">
<div class="flex gap-1 flex-wrap">
<Tag :value="post.platform" severity="info" class="text-xs" />
<Tag :value="post.contentType" severity="secondary" class="text-xs" />
<Tag v-if="post.taskId" :value="`Задача: ${post.taskId.substring(0, 8)}...`" severity="success" class="text-xs" />
</div>
<span class="text-sm text-surface-500 dark:text-surface-400">{{ post.publishTime }}</span>
<span class="text-xs text-surface-500 dark:text-surface-400">{{ post.publishTime }}</span>
</div>
<div class="mb-2">
<p class="text-sm font-medium text-surface-600 dark:text-surface-400 mb-1">Тема:</p>
<p class="text-sm font-semibold text-surface-900 dark:text-surface-0">{{ post.theme }}</p>
<p class="text-xs font-medium text-surface-600 dark:text-surface-400 mb-1">Тема:</p>
<p class="text-xs font-semibold text-surface-900 dark:text-surface-0 line-height-2">{{ post.theme }}</p>
</div>
<!-- Изображение поста -->
<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}`)" />
<div v-if="hasImage(post) && !hasImageError(`${date}-${idx}`)" class="mb-2 post-media" style="position: relative">
<img :src="getImageUrl(post)" :alt="post.theme" class="post-image-small" loading="lazy" @error="handleImageError(`${date}-${idx}`)" />
<Button
icon="pi pi-refresh"
size="small"
@@ -175,9 +174,9 @@
@click="handleRegenerateImage(post, getPostIndex(post))"
/>
</div>
<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>
<div v-else-if="hasImage(post) && hasImageError(`${date}-${idx}`)" class="mb-2 no-image-placeholder-small post-media" style="position: relative">
<i class="pi pi-image text-2xl text-surface-400 dark:text-surface-600"></i>
<p class="text-xs text-surface-500 dark:text-surface-400 mt-1">Изображение недоступно</p>
<Button
icon="pi pi-refresh"
size="small"
@@ -189,9 +188,9 @@
@click="handleRegenerateImage(post, getPostIndex(post))"
/>
</div>
<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>
<div v-else class="mb-2 no-image-placeholder-small post-media" style="position: relative">
<i class="pi pi-image text-2xl text-surface-400 dark:text-surface-600"></i>
<p class="text-xs text-surface-500 dark:text-surface-400 mt-1">Изображение не сгенерировано</p>
<Button
icon="pi pi-refresh"
size="small"
@@ -203,27 +202,27 @@
@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>
<p class="text-sm text-surface-700 dark:text-surface-300 whitespace-pre-line line-height-3">{{ post.postText }}</p>
<div class="mb-2">
<p class="text-xs font-medium text-surface-600 dark:text-surface-400 mb-1">Текст поста:</p>
<p class="text-xs text-surface-700 dark:text-surface-300 whitespace-pre-line line-height-2">{{ post.postText }}</p>
</div>
<div v-if="post.hashtags && post.hashtags.length > 0" class="mb-3">
<p class="text-sm font-medium text-surface-600 dark:text-surface-400 mb-1">Хештеги:</p>
<div v-if="post.hashtags && post.hashtags.length > 0" class="mb-2">
<p class="text-xs font-medium text-surface-600 dark:text-surface-400 mb-1">Хештеги:</p>
<div class="flex flex-wrap gap-1">
<span v-for="(tag, tagIdx) in post.hashtags" :key="tagIdx" class="text-xs px-2 py-1 border-round bg-primary-50 dark:bg-primary-900 text-primary-700 dark:text-primary-300">
<span v-for="(tag, tagIdx) in post.hashtags" :key="tagIdx" class="text-xs px-1 py-0 border-round bg-primary-50 dark:bg-primary-900 text-primary-700 dark:text-primary-300">
{{ tag }}
</span>
</div>
</div>
<div class="flex gap-2">
<Button label="Копировать текст" icon="pi pi-copy" size="small" severity="secondary" class="flex-1" @click="handleCopyPost(post)" />
<div class="flex gap-1 mt-auto">
<Button label="Копировать" icon="pi pi-copy" size="small" severity="secondary" class="flex-1 text-xs" @click="handleCopyPost(post)" />
<Button
v-if="post.taskId"
label="Запустить"
icon="pi pi-play"
size="small"
severity="success"
class="flex-1"
class="flex-1 text-xs"
:loading="isTaskExecuting(post.taskId)"
:disabled="isTaskExecuting(post.taskId)"
@click="handleExecuteTask(post.taskId, post)"
@@ -236,24 +235,24 @@
</div>
<!-- Список всех постов -->
<div v-else class="grid">
<div v-for="(post, idx) in strategyData.strategy.postCalendar" :key="idx" class="col-12 md:col-6 lg:col-4 mb-3">
<div class="post-card p-4 border-round bg-surface-0 dark:bg-surface-800 border-1 border-surface-200 dark:border-surface-700 h-full">
<div class="flex align-items-center justify-content-between mb-3">
<div class="flex gap-2 flex-wrap">
<Tag :value="post.platform" severity="info" />
<Tag :value="post.contentType" severity="secondary" />
<div v-else class="posts-container">
<div v-for="(post, idx) in strategyData.strategy.postCalendar" :key="idx" class="post-item">
<div class="post-card p-3 border-round bg-surface-0 dark:bg-surface-800 border-1 border-surface-200 dark:border-surface-700 square-card">
<div class="flex align-items-center justify-content-between mb-2">
<div class="flex gap-1 flex-wrap">
<Tag :value="post.platform" severity="info" class="text-xs" />
<Tag :value="post.contentType" severity="secondary" class="text-xs" />
<Tag v-if="post.taskId" :value="`Задача: ${post.taskId.substring(0, 8)}...`" severity="success" class="text-xs" />
</div>
<span class="text-sm text-surface-500 dark:text-surface-400">{{ post.publishTime }}</span>
<span class="text-xs text-surface-500 dark:text-surface-400">{{ post.publishTime }}</span>
</div>
<div class="mb-2">
<p class="text-sm font-medium text-surface-600 dark:text-surface-400 mb-1">Тема:</p>
<p class="text-sm font-semibold text-surface-900 dark:text-surface-0">{{ post.theme }}</p>
<p class="text-xs font-medium text-surface-600 dark:text-surface-400 mb-1">Тема:</p>
<p class="text-xs font-semibold text-surface-900 dark:text-surface-0 line-height-2">{{ post.theme }}</p>
</div>
<!-- Изображение поста -->
<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)" />
<div v-if="hasImage(post) && !hasImageError(idx)" class="mb-2 post-media" style="position: relative">
<img :src="getImageUrl(post)" :alt="post.theme" class="post-image-small" loading="lazy" @error="handleImageError(idx)" />
<Button
icon="pi pi-refresh"
size="small"
@@ -265,9 +264,9 @@
@click="handleRegenerateImage(post, idx)"
/>
</div>
<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>
<div v-else-if="hasImage(post) && hasImageError(idx)" class="mb-2 no-image-placeholder-small post-media" style="position: relative">
<i class="pi pi-image text-2xl text-surface-400 dark:text-surface-600"></i>
<p class="text-xs text-surface-500 dark:text-surface-400 mt-1">Изображение недоступно</p>
<Button
icon="pi pi-refresh"
size="small"
@@ -279,9 +278,9 @@
@click="handleRegenerateImage(post, idx)"
/>
</div>
<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>
<div v-else class="mb-2 no-image-placeholder-small post-media" style="position: relative">
<i class="pi pi-image text-2xl text-surface-400 dark:text-surface-600"></i>
<p class="text-xs text-surface-500 dark:text-surface-400 mt-1">Изображение не сгенерировано</p>
<Button
icon="pi pi-refresh"
size="small"
@@ -293,27 +292,27 @@
@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>
<p class="text-sm text-surface-700 dark:text-surface-300 whitespace-pre-line line-height-3">{{ post.postText }}</p>
<div class="mb-2">
<p class="text-xs font-medium text-surface-600 dark:text-surface-400 mb-1">Текст поста:</p>
<p class="text-xs text-surface-700 dark:text-surface-300 whitespace-pre-line line-height-2">{{ post.postText }}</p>
</div>
<div v-if="post.hashtags && post.hashtags.length > 0" class="mb-3">
<p class="text-sm font-medium text-surface-600 dark:text-surface-400 mb-1">Хештеги:</p>
<div v-if="post.hashtags && post.hashtags.length > 0" class="mb-2">
<p class="text-xs font-medium text-surface-600 dark:text-surface-400 mb-1">Хештеги:</p>
<div class="flex flex-wrap gap-1">
<span v-for="(tag, tagIdx) in post.hashtags" :key="tagIdx" class="text-xs px-2 py-1 border-round bg-primary-50 dark:bg-primary-900 text-primary-700 dark:text-primary-300">
<span v-for="(tag, tagIdx) in post.hashtags" :key="tagIdx" class="text-xs px-1 py-0 border-round bg-primary-50 dark:bg-primary-900 text-primary-700 dark:text-primary-300">
{{ tag }}
</span>
</div>
</div>
<div class="flex gap-2">
<Button label="Копировать текст" icon="pi pi-copy" size="small" severity="secondary" class="flex-1" @click="handleCopyPost(post)" />
<div class="flex gap-1 mt-auto">
<Button label="Копировать" icon="pi pi-copy" size="small" severity="secondary" class="flex-1 text-xs" @click="handleCopyPost(post)" />
<Button
v-if="post.taskId"
label="Запустить"
icon="pi pi-play"
size="small"
severity="success"
class="flex-1"
class="flex-1 text-xs"
:loading="isTaskExecuting(post.taskId)"
:disabled="isTaskExecuting(post.taskId)"
@click="handleExecuteTask(post.taskId, post)"
@@ -382,7 +381,7 @@ const pollingInterval = ref(null);
const pollingAttempts = ref(0);
const maxPollingAttempts = 60; // 10 minutes with 10 second intervals
const pollingIntervalMs = 10000; // 10 seconds
const groupByDate = ref(true);
const groupByDate = ref(false);
const startingStrategy = ref(false);
const credentialsDialogVisible = ref(false);
const missingPlatforms = ref([]);
@@ -1023,6 +1022,8 @@ onBeforeUnmount(() => {
0 1px 3px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.24);
padding: 1.5rem;
width: 100%;
box-sizing: border-box;
}
.card-header {
@@ -1033,12 +1034,15 @@ onBeforeUnmount(() => {
.card-content {
padding-top: 0;
width: 100%;
}
.week-plan-card {
transition:
transform 0.2s,
box-shadow 0.2s;
max-width: 100%;
width: 100%;
}
.week-plan-card:hover {
@@ -1052,6 +1056,8 @@ onBeforeUnmount(() => {
box-shadow 0.2s;
display: flex;
flex-direction: column;
max-width: 100%;
width: 100%;
}
.post-card:hover {
@@ -1059,6 +1065,10 @@ onBeforeUnmount(() => {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.date-group {
width: 100%;
}
.date-group:last-child {
border-bottom: none;
padding-bottom: 0;
@@ -1100,4 +1110,107 @@ onBeforeUnmount(() => {
z-index: 10;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.square-card {
aspect-ratio: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
width: 100%;
}
.square-card::-webkit-scrollbar {
width: 4px;
}
.square-card::-webkit-scrollbar-track {
background: transparent;
}
.square-card::-webkit-scrollbar-thumb {
background: var(--surface-400);
border-radius: 2px;
}
.square-card::-webkit-scrollbar-thumb:hover {
background: var(--surface-500);
}
.post-image-small {
width: 100%;
height: 100%;
max-height: none;
border-radius: 6px;
object-fit: cover;
display: block;
margin: 0 auto;
}
.post-media {
flex: 1 1 auto;
min-height: 180px;
width: 100%;
display: flex;
align-items: stretch;
justify-content: center;
}
.no-image-placeholder-small.post-media {
flex: 1 1 auto;
min-height: 180px;
width: 100%;
}
.no-image-placeholder-small {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1rem;
background: var(--surface-50);
border: 1px dashed var(--surface-300);
border-radius: 6px;
min-height: 100px;
text-align: center;
}
.dark .no-image-placeholder-small {
background: var(--surface-800);
border-color: var(--surface-700);
}
.weekly-plans-container,
.posts-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
width: 100%;
box-sizing: border-box;
}
@media (max-width: 1200px) {
.weekly-plans-container,
.posts-container {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 768px) {
.weekly-plans-container,
.posts-container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 576px) {
.weekly-plans-container,
.posts-container {
grid-template-columns: 1fr;
}
}
.weekly-plan-item,
.post-item {
width: 100%;
}
</style>