.
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
:sortField="'createdAt'"
|
||||
:sortOrder="-1"
|
||||
filterDisplay="row"
|
||||
:globalFilterFields="['product', 'location', 'clientType', 'status']"
|
||||
:globalFilterFields="['product', 'location', 'clientType', 'status', 'analysisType']"
|
||||
v-model:filters="filters"
|
||||
:filters="filters"
|
||||
dataKey="analysisId"
|
||||
@@ -54,6 +54,13 @@
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="analysisType" header="Тип анализа" :sortable="true" style="min-width: 180px">
|
||||
<template #body="slotProps">
|
||||
<Tag v-if="slotProps.data.analysisType" :value="getAnalysisTypeLabel(slotProps.data.analysisType)" severity="secondary" />
|
||||
<span v-else class="text-surface-400">—</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="status" header="Статус" :sortable="true" style="min-width: 120px">
|
||||
<template #body="slotProps">
|
||||
<Tag :value="getStatusLabel(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
|
||||
@@ -309,6 +316,18 @@ const getStatusIcon = (status) => {
|
||||
return icons[status] || 'pi pi-circle';
|
||||
};
|
||||
|
||||
// Get analysis type label
|
||||
const getAnalysisTypeLabel = (type) => {
|
||||
const labels = {
|
||||
РЫНОК: 'Анализ рынка',
|
||||
КОНКУРЕНТЫ: 'Анализ конкурентов',
|
||||
ЦА: 'Анализ целевой аудитории',
|
||||
КАНАЛЫ: 'Анализ маркетинговых каналов',
|
||||
SWOT: 'SWOT-анализ'
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
|
||||
// Load on mount
|
||||
onMounted(() => {
|
||||
loadAnalyses();
|
||||
|
||||
@@ -33,6 +33,22 @@
|
||||
<small v-if="errors.differentiator" class="p-error">{{ errors.differentiator }}</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="analysisType" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Тип анализа </label>
|
||||
<Dropdown
|
||||
id="analysisType"
|
||||
v-model="formData.analysisType"
|
||||
:options="analysisTypeOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Выберите тип анализа"
|
||||
class="w-full"
|
||||
:class="{ 'p-invalid': errors.analysisType }"
|
||||
/>
|
||||
<small v-if="errors.analysisType" class="p-error">{{ errors.analysisType }}</small>
|
||||
<small class="text-surface-500 dark:text-surface-400 mt-1 block">Выберите тип анализа, который будет сгенерирован</small>
|
||||
</div>
|
||||
|
||||
<Button type="submit" label="Начать анализ" icon="pi pi-send" class="w-full p-button-primary" :loading="submitting" />
|
||||
|
||||
<p class="text-sm text-surface-500 dark:text-surface-400 mt-3 text-center">На основе ваших ответов будет создан подробный и полезный маркетинговый отчет</p>
|
||||
@@ -77,7 +93,12 @@
|
||||
<div class="card">
|
||||
<div class="card-header mb-4">
|
||||
<div class="flex justify-content-between align-items-center">
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Результаты анализа</h2>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Результаты анализа</h2>
|
||||
<div v-if="currentAnalysisType" class="mt-2">
|
||||
<Tag :value="getAnalysisTypeLabel(currentAnalysisType)" severity="info" />
|
||||
</div>
|
||||
</div>
|
||||
<Button label="Скачать PDF" icon="pi pi-download" @click="handleDownloadPdf" :loading="downloadingPdf" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,22 +200,32 @@ import ProgressSpinner from 'primevue/progressspinner';
|
||||
import Tag from 'primevue/tag';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { onBeforeUnmount, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const toast = useToast();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// Form data
|
||||
const formData = ref({
|
||||
product: '',
|
||||
location: '',
|
||||
client: null,
|
||||
differentiator: ''
|
||||
differentiator: '',
|
||||
analysisType: null
|
||||
});
|
||||
|
||||
const clientOptions = ['B2B клиенты', 'B2C клиенты', 'Частные лица', 'Корпорации', 'Малый бизнес'];
|
||||
|
||||
const analysisTypeOptions = [
|
||||
{ label: 'РЫНОК - Анализ рынка', value: 'РЫНОК' },
|
||||
{ label: 'КОНКУРЕНТЫ - Анализ конкурентов', value: 'КОНКУРЕНТЫ' },
|
||||
{ label: 'ЦА - Анализ целевой аудитории', value: 'ЦА' },
|
||||
{ label: 'КАНАЛЫ - Анализ маркетинговых каналов', value: 'КАНАЛЫ' },
|
||||
{ label: 'SWOT - SWOT-анализ', value: 'SWOT' }
|
||||
];
|
||||
|
||||
const submitting = ref(false);
|
||||
const errors = ref({});
|
||||
|
||||
@@ -202,6 +233,7 @@ const errors = ref({});
|
||||
const analysisId = ref(null);
|
||||
const status = ref(null);
|
||||
const report = ref(null);
|
||||
const currentAnalysisType = ref(null);
|
||||
const estimatedCompletionTime = ref(null);
|
||||
const pollingInterval = ref(null);
|
||||
const pollingAttempts = ref(0);
|
||||
@@ -241,6 +273,16 @@ const validateForm = () => {
|
||||
errors.value.differentiator = 'Описание уникальности не должно превышать 500 символов';
|
||||
}
|
||||
|
||||
// AnalysisType validation
|
||||
if (!formData.value.analysisType) {
|
||||
errors.value.analysisType = 'Пожалуйста, выберите тип анализа';
|
||||
} else {
|
||||
const validTypes = ['РЫНОК', 'КОНКУРЕНТЫ', 'ЦА', 'КАНАЛЫ', 'SWOT'];
|
||||
if (!validTypes.includes(formData.value.analysisType)) {
|
||||
errors.value.analysisType = 'Выбран недопустимый тип анализа';
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(errors.value).length === 0;
|
||||
};
|
||||
|
||||
@@ -268,6 +310,10 @@ const startPolling = (id) => {
|
||||
if (result.status === 'completed' && result.report) {
|
||||
stopPolling();
|
||||
report.value = result.report;
|
||||
// Сохраняем тип анализа из результата, если он есть
|
||||
if (result.analysisType) {
|
||||
currentAnalysisType.value = result.analysisType;
|
||||
}
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Анализ завершен',
|
||||
@@ -317,11 +363,13 @@ const handleSubmit = async () => {
|
||||
product: formData.value.product.trim(),
|
||||
location: formData.value.location.trim(),
|
||||
client: formData.value.client,
|
||||
differentiator: formData.value.differentiator.trim()
|
||||
differentiator: formData.value.differentiator.trim(),
|
||||
analysisType: formData.value.analysisType
|
||||
});
|
||||
|
||||
analysisId.value = result.analysisId;
|
||||
status.value = result.status;
|
||||
currentAnalysisType.value = formData.value.analysisType;
|
||||
estimatedCompletionTime.value = result.estimatedCompletionTime;
|
||||
|
||||
toast.add({
|
||||
@@ -379,13 +427,15 @@ const resetAnalysis = () => {
|
||||
analysisId.value = null;
|
||||
status.value = null;
|
||||
report.value = null;
|
||||
currentAnalysisType.value = null;
|
||||
estimatedCompletionTime.value = null;
|
||||
pollingAttempts.value = 0;
|
||||
formData.value = {
|
||||
product: '',
|
||||
location: '',
|
||||
client: null,
|
||||
differentiator: ''
|
||||
differentiator: '',
|
||||
analysisType: null
|
||||
};
|
||||
errors.value = {};
|
||||
};
|
||||
@@ -407,6 +457,18 @@ const renderMarkdown = (markdown) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Get analysis type label
|
||||
const getAnalysisTypeLabel = (type) => {
|
||||
const labels = {
|
||||
РЫНОК: 'Анализ рынка',
|
||||
КОНКУРЕНТЫ: 'Анализ конкурентов',
|
||||
ЦА: 'Анализ целевой аудитории',
|
||||
КАНАЛЫ: 'Анализ маркетинговых каналов',
|
||||
SWOT: 'SWOT-анализ'
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
|
||||
// Format date time
|
||||
const formatDateTime = (dateString) => {
|
||||
if (!dateString) return '';
|
||||
@@ -438,6 +500,39 @@ const goToStrategy = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Load analysis from query params
|
||||
onMounted(async () => {
|
||||
const queryAnalysisId = route.query.analysisId;
|
||||
if (queryAnalysisId) {
|
||||
// Try to load existing analysis
|
||||
try {
|
||||
analysisId.value = queryAnalysisId;
|
||||
const result = await MarketingService.getAnalysisResult(queryAnalysisId);
|
||||
status.value = result.status;
|
||||
|
||||
// Сохраняем тип анализа, если он есть
|
||||
if (result.analysisType) {
|
||||
currentAnalysisType.value = result.analysisType;
|
||||
}
|
||||
|
||||
if (result.status === 'completed' && result.report) {
|
||||
report.value = result.report;
|
||||
} else if (result.status === 'processing' || result.status === 'queued') {
|
||||
estimatedCompletionTime.value = result.estimatedCompletionTime;
|
||||
startPolling(queryAnalysisId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке анализа:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: error.message || 'Не удалось загрузить анализ',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup on unmount
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling();
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
<template>
|
||||
<div class="marketing-credentials-page bg-surface-50 dark:bg-surface-900 min-h-screen p-6">
|
||||
<Toast />
|
||||
<div class="max-w-[1200px] mx-auto">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header mb-4">
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Управление credentials социальных сетей</h2>
|
||||
<p class="text-sm text-surface-500 dark:text-surface-400 mt-2">Настройте access tokens для платформ, на которых вы хотите автоматически публиковать контент</p>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<form @submit.prevent="handleSaveCredentials" class="space-y-4">
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-4">
|
||||
<div class="field">
|
||||
<label for="platform" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300">
|
||||
Платформа <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<Dropdown
|
||||
id="platform"
|
||||
v-model="formData.platform"
|
||||
:options="platformOptions"
|
||||
placeholder="Выберите платформу"
|
||||
class="w-full"
|
||||
:class="{ 'p-invalid': errors.platform }"
|
||||
/>
|
||||
<small v-if="errors.platform" class="p-error">{{ errors.platform }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="field">
|
||||
<label for="credentials" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300">
|
||||
Access Token <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<InputText
|
||||
id="credentials"
|
||||
v-model="formData.credentials"
|
||||
type="password"
|
||||
placeholder="Введите access token"
|
||||
class="w-full"
|
||||
:class="{ 'p-invalid': errors.credentials }"
|
||||
/>
|
||||
<small v-if="errors.credentials" class="p-error">{{ errors.credentials }}</small>
|
||||
<small class="text-surface-500 dark:text-surface-400 mt-1 block">
|
||||
Токен будет зашифрован перед сохранением
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-2">
|
||||
<div class="field">
|
||||
<label class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> </label>
|
||||
<Button
|
||||
type="submit"
|
||||
label="Сохранить"
|
||||
icon="pi pi-save"
|
||||
class="w-full"
|
||||
:loading="saving"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header mb-4">
|
||||
<div class="flex justify-content-between align-items-center">
|
||||
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0">Настроенные платформы</h3>
|
||||
<Button label="Обновить" icon="pi pi-refresh" severity="secondary" @click="loadCredentials" :loading="loading" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<DataTable
|
||||
:value="credentials"
|
||||
:loading="loading"
|
||||
paginator
|
||||
:rows="10"
|
||||
:rowsPerPageOptions="[5, 10, 20]"
|
||||
dataKey="platform"
|
||||
:emptyMessage="'Нет настроенных платформ'"
|
||||
>
|
||||
<Column field="platform" header="Платформа" :sortable="true" style="min-width: 150px">
|
||||
<template #body="slotProps">
|
||||
<Tag :value="slotProps.data.platform" severity="info" />
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="hasCredentials" header="Статус" :sortable="true" style="min-width: 120px">
|
||||
<template #body="slotProps">
|
||||
<Tag
|
||||
:value="slotProps.data.hasCredentials ? 'Настроено' : 'Не настроено'"
|
||||
:severity="slotProps.data.hasCredentials ? 'success' : 'warning'"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="createdAt" header="Создано" :sortable="true" style="min-width: 180px">
|
||||
<template #body="slotProps">
|
||||
<span v-if="slotProps.data.createdAt">{{ formatDateTime(slotProps.data.createdAt) }}</span>
|
||||
<span v-else class="text-surface-400">—</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="updatedAt" header="Обновлено" :sortable="true" style="min-width: 180px">
|
||||
<template #body="slotProps">
|
||||
<span v-if="slotProps.data.updatedAt">{{ formatDateTime(slotProps.data.updatedAt) }}</span>
|
||||
<span v-else class="text-surface-400">—</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Действия" style="min-width: 150px">
|
||||
<template #body="slotProps">
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
size="small"
|
||||
v-tooltip.top="'Удалить credentials'"
|
||||
@click="confirmDelete(slotProps.data.platform)"
|
||||
:disabled="!slotProps.data.hasCredentials"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Диалог подтверждения удаления -->
|
||||
<Dialog
|
||||
v-model:visible="deleteDialogVisible"
|
||||
:header="'Подтверждение удаления'"
|
||||
:style="{ width: '450px' }"
|
||||
:modal="true"
|
||||
>
|
||||
<div class="confirmation-content">
|
||||
<i class="pi pi-exclamation-triangle mr-3" style="font-size: 2rem" />
|
||||
<span>Вы уверены, что хотите удалить credentials для платформы <b>{{ platformToDelete }}</b>?</span>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="Отмена" icon="pi pi-times" @click="deleteDialogVisible = false" severity="secondary" />
|
||||
<Button label="Удалить" icon="pi pi-check" @click="handleDeleteCredentials" severity="danger" :loading="deleting" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MarketingService from '@/service/MarketingService';
|
||||
import Button from 'primevue/button';
|
||||
import Column from 'primevue/column';
|
||||
import DataTable from 'primevue/datatable';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Tag from 'primevue/tag';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
// Form data
|
||||
const formData = ref({
|
||||
platform: null,
|
||||
credentials: ''
|
||||
});
|
||||
|
||||
const platformOptions = ['facebook', 'instagram', 'linkedin', 'telegram', 'tiktok', 'youtube'];
|
||||
|
||||
const saving = ref(false);
|
||||
const errors = ref({});
|
||||
|
||||
// Data
|
||||
const credentials = ref([]);
|
||||
const loading = ref(false);
|
||||
const deleteDialogVisible = ref(false);
|
||||
const platformToDelete = ref(null);
|
||||
const deleting = ref(false);
|
||||
|
||||
// Validation
|
||||
const validateForm = () => {
|
||||
errors.value = {};
|
||||
|
||||
if (!formData.value.platform) {
|
||||
errors.value.platform = 'Пожалуйста, выберите платформу';
|
||||
}
|
||||
|
||||
if (!formData.value.credentials || formData.value.credentials.trim().length === 0) {
|
||||
errors.value.credentials = 'Access token обязателен';
|
||||
} else if (formData.value.credentials.trim().length < 10) {
|
||||
errors.value.credentials = 'Access token слишком короткий';
|
||||
}
|
||||
|
||||
return Object.keys(errors.value).length === 0;
|
||||
};
|
||||
|
||||
// Load credentials
|
||||
const loadCredentials = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await MarketingService.getAllCredentials();
|
||||
credentials.value = data || [];
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке credentials:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: error.message || 'Не удалось загрузить список credentials',
|
||||
life: 5000
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle save credentials
|
||||
const handleSaveCredentials = async () => {
|
||||
if (!validateForm()) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка валидации',
|
||||
detail: 'Пожалуйста, исправьте ошибки в форме',
|
||||
life: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
await MarketingService.saveCredentials(formData.value.platform, formData.value.credentials.trim());
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Успешно',
|
||||
detail: `Credentials для ${formData.value.platform} успешно сохранены`,
|
||||
life: 3000
|
||||
});
|
||||
|
||||
// Reset form
|
||||
formData.value = {
|
||||
platform: null,
|
||||
credentials: ''
|
||||
};
|
||||
errors.value = {};
|
||||
|
||||
// Reload credentials list
|
||||
await loadCredentials();
|
||||
} catch (error) {
|
||||
console.error('Ошибка при сохранении credentials:', error);
|
||||
|
||||
// Handle specific error codes
|
||||
if (error.code === 'VALIDATION_ERROR') {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка валидации',
|
||||
detail: error.message || 'Проверьте правильность введенных данных',
|
||||
life: 5000
|
||||
});
|
||||
} else if (error.code === 'UNAUTHORIZED') {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка авторизации',
|
||||
detail: 'Необходимо войти в систему',
|
||||
life: 5000
|
||||
});
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: error.message || 'Не удалось сохранить credentials',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Confirm delete
|
||||
const confirmDelete = (platform) => {
|
||||
platformToDelete.value = platform;
|
||||
deleteDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// Handle delete credentials
|
||||
const handleDeleteCredentials = async () => {
|
||||
if (!platformToDelete.value) return;
|
||||
|
||||
deleting.value = true;
|
||||
|
||||
try {
|
||||
await MarketingService.deleteCredentials(platformToDelete.value);
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Успешно',
|
||||
detail: `Credentials для ${platformToDelete.value} успешно удалены`,
|
||||
life: 3000
|
||||
});
|
||||
|
||||
deleteDialogVisible.value = false;
|
||||
platformToDelete.value = null;
|
||||
|
||||
// Reload credentials list
|
||||
await loadCredentials();
|
||||
} catch (error) {
|
||||
console.error('Ошибка при удалении credentials:', error);
|
||||
|
||||
if (error.code === 'NOT_FOUND') {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Не найдено',
|
||||
detail: `Credentials для ${platformToDelete.value} не найдены`,
|
||||
life: 5000
|
||||
});
|
||||
} else if (error.code === 'UNAUTHORIZED') {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка авторизации',
|
||||
detail: 'Необходимо войти в систему',
|
||||
life: 5000
|
||||
});
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: error.message || 'Не удалось удалить credentials',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Format date time
|
||||
const formatDateTime = (dateString) => {
|
||||
if (!dateString) return '—';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
// Load on mount
|
||||
onMounted(() => {
|
||||
loadCredentials();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.marketing-credentials-page {
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--surface-card);
|
||||
border-radius: 12px;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.12),
|
||||
0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.confirmation-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -72,6 +72,13 @@ const menuItems = [
|
||||
iconColor: 'text-primary-500',
|
||||
route: '/marketing-analysis/strategies'
|
||||
},
|
||||
{
|
||||
id: 'credentials',
|
||||
label: 'Credentials социальных сетей',
|
||||
icon: 'pi pi-key',
|
||||
iconColor: 'text-primary-500',
|
||||
route: '/marketing-analysis/credentials'
|
||||
},
|
||||
{
|
||||
id: 'results',
|
||||
label: 'Результаты',
|
||||
|
||||
@@ -77,7 +77,17 @@
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Стратегия продвижения</h2>
|
||||
<p class="text-sm text-surface-500 dark:text-surface-400 mt-1">Длительность: {{ strategyData.durationWeeks }} {{ pluralize(strategyData.durationWeeks, 'неделя', 'недели', 'недель') }}</p>
|
||||
</div>
|
||||
<Button label="Создать новую стратегию" icon="pi pi-plus" severity="secondary" @click="resetStrategy" />
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
label="Запустить стратегию"
|
||||
icon="pi pi-play"
|
||||
severity="success"
|
||||
@click="handleStartStrategy"
|
||||
:loading="startingStrategy"
|
||||
:disabled="startingStrategy"
|
||||
/>
|
||||
<Button label="Создать новую стратегию" icon="pi pi-plus" severity="secondary" @click="resetStrategy" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -215,12 +225,48 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Диалог для настройки credentials -->
|
||||
<Dialog
|
||||
v-model:visible="credentialsDialogVisible"
|
||||
:header="'Настройка credentials'"
|
||||
:style="{ width: '600px' }"
|
||||
:modal="true"
|
||||
>
|
||||
<div class="credentials-dialog-content">
|
||||
<p class="text-surface-700 dark:text-surface-300 mb-4">
|
||||
Для запуска стратегии необходимо настроить credentials для следующих платформ:
|
||||
</p>
|
||||
<div class="mb-4">
|
||||
<Tag
|
||||
v-for="platform in missingPlatforms"
|
||||
:key="platform"
|
||||
:value="platform"
|
||||
severity="warning"
|
||||
class="mr-2 mb-2"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-surface-600 dark:text-surface-400 text-sm">
|
||||
Перейдите на страницу управления credentials для настройки необходимых платформ.
|
||||
</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="Отмена" icon="pi pi-times" @click="credentialsDialogVisible = false" severity="secondary" />
|
||||
<Button
|
||||
label="Настроить credentials"
|
||||
icon="pi pi-cog"
|
||||
@click="goToCredentials"
|
||||
severity="info"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MarketingService from '@/service/MarketingService';
|
||||
import Button from 'primevue/button';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import MultiSelect from 'primevue/multiselect';
|
||||
@@ -229,10 +275,11 @@ import Tag from 'primevue/tag';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const toast = useToast();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// Form data
|
||||
const formData = ref({
|
||||
@@ -255,6 +302,9 @@ const pollingAttempts = ref(0);
|
||||
const maxPollingAttempts = 60; // 10 minutes with 10 second intervals
|
||||
const pollingIntervalMs = 10000; // 10 seconds
|
||||
const groupByDate = ref(true);
|
||||
const startingStrategy = ref(false);
|
||||
const credentialsDialogVisible = ref(false);
|
||||
const missingPlatforms = ref([]);
|
||||
|
||||
// Computed
|
||||
const postsByDate = computed(() => {
|
||||
@@ -462,6 +512,98 @@ const toggleGroupByDate = () => {
|
||||
groupByDate.value = !groupByDate.value;
|
||||
};
|
||||
|
||||
// Handle start strategy
|
||||
const handleStartStrategy = async () => {
|
||||
if (!strategyId.value) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: 'ID стратегии не найден',
|
||||
life: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
startingStrategy.value = true;
|
||||
|
||||
try {
|
||||
const result = await MarketingService.startStrategy(strategyId.value);
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Стратегия запущена',
|
||||
detail: `Стратегия успешно запущена. Создано задач: ${result.tasksCreated}. Платформы: ${result.platforms?.join(', ') || 'N/A'}`,
|
||||
life: 5000
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка при запуске стратегии:', error);
|
||||
|
||||
// Handle specific error codes
|
||||
if (error.code === 'MISSING_CREDENTIALS') {
|
||||
// Extract platform from error message (format: "Credentials not found for platform: facebook")
|
||||
const platformMatch = error.message?.match(/platform:\s*(\w+)/i) || error.message?.match(/for platform\s+(\w+)/i);
|
||||
if (platformMatch) {
|
||||
missingPlatforms.value = [platformMatch[1].toLowerCase()];
|
||||
} else {
|
||||
// Try to get platforms from strategy data
|
||||
const platforms = strategyData.value?.priorityPlatforms || strategyData.value?.strategy?.postCalendar?.map(p => p.platform).filter((v, i, a) => a.indexOf(v) === i) || [];
|
||||
missingPlatforms.value = platforms.length > 0 ? platforms : ['facebook']; // Default to facebook if unknown
|
||||
}
|
||||
credentialsDialogVisible.value = true;
|
||||
} else if (error.code === 'INVALID_STATUS') {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Стратегия не готова',
|
||||
detail: error.message || 'Стратегия еще не завершена. Дождитесь завершения генерации.',
|
||||
life: 5000
|
||||
});
|
||||
} else if (error.code === 'UNAUTHORIZED') {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка авторизации',
|
||||
detail: 'Необходимо войти в систему',
|
||||
life: 5000
|
||||
});
|
||||
} else if (error.code === 'NOT_FOUND') {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Не найдено',
|
||||
detail: 'Стратегия не найдена',
|
||||
life: 5000
|
||||
});
|
||||
} else if (error.code === 'FORBIDDEN') {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Доступ запрещен',
|
||||
detail: 'У вас нет доступа к этой стратегии',
|
||||
life: 5000
|
||||
});
|
||||
} else if (error.code === 'VALIDATION_ERROR') {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка валидации',
|
||||
detail: error.message || 'Проверьте правильность данных',
|
||||
life: 5000
|
||||
});
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: error.message || 'Не удалось запустить стратегию. Попробуйте позже.',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
startingStrategy.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Go to credentials page
|
||||
const goToCredentials = () => {
|
||||
credentialsDialogVisible.value = false;
|
||||
router.push({ name: 'marketing-credentials' });
|
||||
};
|
||||
|
||||
// Format date
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
|
||||
Reference in New Issue
Block a user