.
This commit is contained in:
+5
-1
@@ -54,11 +54,15 @@ export const API_CONFIG = {
|
||||
MARKETING_ANALYSIS_START: '/api/marketing/analysis/start',
|
||||
MARKETING_ANALYSIS_GET: '/api/marketing/analysis',
|
||||
MARKETING_ANALYSIS_DOWNLOAD: '/api/marketing/analysis',
|
||||
MARKETING_ANALYSIS_MY: '/api/marketing/analysis/my',
|
||||
MARKETING_ANALYSIS_HISTORY: '/api/marketing/analysis',
|
||||
|
||||
// MarketingStrategyController - Стратегия продвижения
|
||||
MARKETING_STRATEGY_GENERATE: '/api/marketing/analysis/strategy/generate',
|
||||
MARKETING_STRATEGY_GET: '/api/marketing/analysis/strategy',
|
||||
MARKETING_STRATEGY_BY_ANALYSIS: '/api/marketing/analysis'
|
||||
MARKETING_STRATEGY_BY_ANALYSIS: '/api/marketing/analysis',
|
||||
MARKETING_STRATEGY_MY: '/api/marketing/analysis/strategy/my',
|
||||
MARKETING_STRATEGY_HISTORY: '/api/marketing/analysis/strategy'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -243,6 +243,16 @@ const router = createRouter({
|
||||
name: 'marketing-results',
|
||||
component: () => import('@/views/pages/marketing/MarketingResults.vue')
|
||||
},
|
||||
{
|
||||
path: '/marketing-analysis/analyses',
|
||||
name: 'marketing-analyses-list',
|
||||
component: () => import('@/views/pages/marketing/MarketingAnalysesList.vue')
|
||||
},
|
||||
{
|
||||
path: '/marketing-analysis/strategies',
|
||||
name: 'marketing-strategies-list',
|
||||
component: () => import('@/views/pages/marketing/MarketingStrategiesList.vue')
|
||||
},
|
||||
{
|
||||
path: '/pages/notfound',
|
||||
name: 'notfound',
|
||||
|
||||
@@ -71,6 +71,59 @@ class MarketingService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение списка всех анализов текущего пользователя
|
||||
* GET /api/marketing/analysis/my
|
||||
* @returns {Promise<Array>} Массив анализов с полями analysisId, product, location, clientType, differentiator, status, userId, createdAt, completedAt, statusHistory
|
||||
*/
|
||||
async getMyAnalyses() {
|
||||
try {
|
||||
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_ANALYSIS_MY}`);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || result.error?.message || 'Ошибка при получении списка анализов');
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || 'Ошибка при получении списка анализов');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении списка анализов:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение детальной истории анализа
|
||||
* GET /api/marketing/analysis/{analysisId}/history
|
||||
* @param {string} analysisId - Идентификатор анализа
|
||||
* @returns {Promise<Object>} Объект анализа с полной историей статусов
|
||||
*/
|
||||
async getAnalysisHistory(analysisId) {
|
||||
try {
|
||||
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_ANALYSIS_HISTORY}/${analysisId}/history`);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || result.error?.message || 'Ошибка при получении истории анализа');
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || 'Ошибка при получении истории анализа');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении истории анализа:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Скачивание PDF отчета
|
||||
* GET /api/marketing/analysis/{analysisId}/download
|
||||
@@ -212,6 +265,59 @@ class MarketingService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение списка всех стратегий текущего пользователя
|
||||
* GET /api/marketing/analysis/strategy/my
|
||||
* @returns {Promise<Array>} Массив стратегий с полями strategyId, analysisId, status, userId, durationWeeks, priorityPlatforms, createdAt, completedAt, statusHistory
|
||||
*/
|
||||
async getMyStrategies() {
|
||||
try {
|
||||
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_STRATEGY_MY}`);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || result.error?.message || 'Ошибка при получении списка стратегий');
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || 'Ошибка при получении списка стратегий');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении списка стратегий:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение детальной истории стратегии
|
||||
* GET /api/marketing/analysis/strategy/{strategyId}/history
|
||||
* @param {string} strategyId - Идентификатор стратегии
|
||||
* @returns {Promise<Object>} Объект стратегии с полной историей статусов
|
||||
*/
|
||||
async getStrategyHistory(strategyId) {
|
||||
try {
|
||||
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_STRATEGY_HISTORY}/${strategyId}/history`);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || result.error?.message || 'Ошибка при получении истории стратегии');
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || 'Ошибка при получении истории стратегии');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении истории стратегии:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MarketingService();
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
<template>
|
||||
<div class="marketing-analyses-list-page bg-surface-50 dark:bg-surface-900 min-h-screen p-6">
|
||||
<Toast />
|
||||
<div class="max-w-[1400px] mx-auto">
|
||||
<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>
|
||||
<Button label="Создать новый анализ" icon="pi pi-plus" severity="success" @click="goToCreateAnalysis" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<DataTable
|
||||
:value="analyses"
|
||||
:loading="loading"
|
||||
paginator
|
||||
:rows="10"
|
||||
:rowsPerPageOptions="[5, 10, 20, 50]"
|
||||
sortMode="multiple"
|
||||
removableSort
|
||||
:sortField="'createdAt'"
|
||||
:sortOrder="-1"
|
||||
filterDisplay="row"
|
||||
:globalFilterFields="['product', 'location', 'clientType', 'status']"
|
||||
v-model:filters="filters"
|
||||
:filters="filters"
|
||||
dataKey="analysisId"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search" />
|
||||
<InputText v-model="filters['global'].value" placeholder="Поиск анализов..." />
|
||||
</span>
|
||||
<Button label="Обновить" icon="pi pi-refresh" severity="secondary" @click="loadAnalyses" :loading="loading" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Column field="product" header="Продукт" :sortable="true" style="min-width: 200px">
|
||||
<template #body="slotProps">
|
||||
<span class="font-semibold">{{ slotProps.data.product }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="location" header="Локация" :sortable="true" style="min-width: 150px">
|
||||
<template #body="slotProps">
|
||||
<span>{{ slotProps.data.location }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="clientType" header="Тип клиента" :sortable="true" style="min-width: 150px">
|
||||
<template #body="slotProps">
|
||||
<Tag :value="slotProps.data.clientType" severity="info" />
|
||||
</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)" />
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<Dropdown v-model="filterModel.value" :options="statusOptions" placeholder="Все статусы" class="p-column-filter" @change="filterCallback()">
|
||||
<template #option="slotProps">
|
||||
<Tag :value="slotProps.option.label" :severity="slotProps.option.severity" />
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="createdAt" header="Создан" :sortable="true" style="min-width: 180px">
|
||||
<template #body="slotProps">
|
||||
<span>{{ formatDateTime(slotProps.data.createdAt) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="completedAt" header="Завершен" :sortable="true" style="min-width: 180px">
|
||||
<template #body="slotProps">
|
||||
<span v-if="slotProps.data.completedAt">{{ formatDateTime(slotProps.data.completedAt) }}</span>
|
||||
<span v-else class="text-surface-400">—</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Действия" style="min-width: 250px">
|
||||
<template #body="slotProps">
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<Button icon="pi pi-eye" severity="info" size="small" v-tooltip.top="'Просмотр деталей'" @click="viewAnalysis(slotProps.data.analysisId)" :disabled="slotProps.data.status !== 'completed'" />
|
||||
<Button icon="pi pi-history" severity="secondary" size="small" v-tooltip.top="'История статусов'" @click="viewHistory(slotProps.data.analysisId)" />
|
||||
<Button icon="pi pi-download" severity="success" size="small" v-tooltip.top="'Скачать PDF'" @click="downloadPdf(slotProps.data.analysisId)" :disabled="slotProps.data.status !== 'completed'" />
|
||||
<Button icon="pi pi-magic" severity="warning" size="small" v-tooltip.top="'Создать стратегию'" @click="createStrategy(slotProps.data.analysisId)" :disabled="slotProps.data.status !== 'completed'" />
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Диалог истории статусов -->
|
||||
<Dialog v-model:visible="historyDialogVisible" :header="'История статусов анализа'" :style="{ width: '600px' }" :modal="true">
|
||||
<div v-if="historyLoading" class="text-center py-4">
|
||||
<ProgressSpinner />
|
||||
</div>
|
||||
<div v-else-if="statusHistory && statusHistory.length > 0" class="status-history">
|
||||
<Timeline :value="statusHistory" align="left" class="w-full">
|
||||
<template #marker="slotProps">
|
||||
<span class="flex w-2rem h-2rem align-items-center justify-content-center text-white border-circle z-1" :class="getStatusColorClass(slotProps.item.status)">
|
||||
<i :class="getStatusIcon(slotProps.item.status)"></i>
|
||||
</span>
|
||||
</template>
|
||||
<template #content="slotProps">
|
||||
<div class="flex flex-column">
|
||||
<div class="flex align-items-center gap-2 mb-2">
|
||||
<Tag :value="getStatusLabel(slotProps.item.status)" :severity="getStatusSeverity(slotProps.item.status)" />
|
||||
<span class="text-sm text-surface-500 dark:text-surface-400">{{ formatDateTime(slotProps.item.timestamp) }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-surface-700 dark:text-surface-300 m-0">{{ slotProps.item.message }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</Timeline>
|
||||
</div>
|
||||
<div v-else class="text-center py-4 text-surface-500">История статусов недоступна</div>
|
||||
<template #footer>
|
||||
<Button label="Закрыть" icon="pi pi-times" @click="historyDialogVisible = false" severity="secondary" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MarketingService from '@/service/MarketingService';
|
||||
import { FilterMatchMode } from '@primevue/core/api';
|
||||
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 ProgressSpinner from 'primevue/progressspinner';
|
||||
import Tag from 'primevue/tag';
|
||||
import Timeline from 'primevue/timeline';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const toast = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
// Data
|
||||
const analyses = ref([]);
|
||||
const loading = ref(false);
|
||||
const historyLoading = ref(false);
|
||||
const statusHistory = ref([]);
|
||||
const historyDialogVisible = ref(false);
|
||||
const currentAnalysisId = ref(null);
|
||||
|
||||
// Filters
|
||||
const filters = ref({
|
||||
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
||||
status: { value: null, matchMode: FilterMatchMode.EQUALS }
|
||||
});
|
||||
|
||||
// Status options for filter
|
||||
const statusOptions = [
|
||||
{ label: 'В очереди', value: 'queued', severity: 'info' },
|
||||
{ label: 'Обрабатывается', value: 'processing', severity: 'warning' },
|
||||
{ label: 'Завершен', value: 'completed', severity: 'success' },
|
||||
{ label: 'Ошибка', value: 'failed', severity: 'danger' }
|
||||
];
|
||||
|
||||
// Load analyses
|
||||
const loadAnalyses = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await MarketingService.getMyAnalyses();
|
||||
analyses.value = data || [];
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке списка анализов:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: error.message || 'Не удалось загрузить список анализов',
|
||||
life: 5000
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// View analysis details
|
||||
const viewAnalysis = (analysisId) => {
|
||||
router.push({
|
||||
name: 'marketing-analysis',
|
||||
query: { analysisId }
|
||||
});
|
||||
};
|
||||
|
||||
// View history
|
||||
const viewHistory = async (analysisId) => {
|
||||
currentAnalysisId.value = analysisId;
|
||||
historyDialogVisible.value = true;
|
||||
historyLoading.value = true;
|
||||
statusHistory.value = [];
|
||||
|
||||
try {
|
||||
const data = await MarketingService.getAnalysisHistory(analysisId);
|
||||
statusHistory.value = data.statusHistory || [];
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке истории:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: error.message || 'Не удалось загрузить историю статусов',
|
||||
life: 5000
|
||||
});
|
||||
} finally {
|
||||
historyLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Download PDF
|
||||
const downloadPdf = async (analysisId) => {
|
||||
try {
|
||||
await MarketingService.downloadPdfFile(analysisId);
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'PDF скачан',
|
||||
detail: 'PDF отчет успешно скачан',
|
||||
life: 3000
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка при скачивании PDF:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: error.message || 'Не удалось скачать PDF отчет',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Create strategy
|
||||
const createStrategy = (analysisId) => {
|
||||
router.push({
|
||||
name: 'marketing-promotion',
|
||||
query: { analysisId }
|
||||
});
|
||||
};
|
||||
|
||||
// Navigate to create analysis
|
||||
const goToCreateAnalysis = () => {
|
||||
router.push({ name: 'marketing-analysis' });
|
||||
};
|
||||
|
||||
// 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'
|
||||
});
|
||||
};
|
||||
|
||||
// Get status label
|
||||
const getStatusLabel = (status) => {
|
||||
const labels = {
|
||||
queued: 'В очереди',
|
||||
processing: 'Обрабатывается',
|
||||
completed: 'Завершен',
|
||||
failed: 'Ошибка'
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
// Get status severity
|
||||
const getStatusSeverity = (status) => {
|
||||
const severities = {
|
||||
queued: 'info',
|
||||
processing: 'warning',
|
||||
completed: 'success',
|
||||
failed: 'danger'
|
||||
};
|
||||
return severities[status] || null;
|
||||
};
|
||||
|
||||
// Get status color class for timeline
|
||||
const getStatusColorClass = (status) => {
|
||||
const classes = {
|
||||
queued: 'bg-blue-500',
|
||||
processing: 'bg-yellow-500',
|
||||
completed: 'bg-green-500',
|
||||
failed: 'bg-red-500'
|
||||
};
|
||||
return classes[status] || 'bg-surface-500';
|
||||
};
|
||||
|
||||
// Get status icon
|
||||
const getStatusIcon = (status) => {
|
||||
const icons = {
|
||||
queued: 'pi pi-clock',
|
||||
processing: 'pi pi-spin pi-spinner',
|
||||
completed: 'pi pi-check',
|
||||
failed: 'pi pi-times'
|
||||
};
|
||||
return icons[status] || 'pi pi-circle';
|
||||
};
|
||||
|
||||
// Load on mount
|
||||
onMounted(() => {
|
||||
loadAnalyses();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.marketing-analyses-list-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;
|
||||
}
|
||||
|
||||
.status-history {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -51,6 +51,13 @@ const menuItems = [
|
||||
iconColor: 'text-primary-500',
|
||||
route: '/marketing-analysis/analysis'
|
||||
},
|
||||
{
|
||||
id: 'analyses-list',
|
||||
label: 'Мои анализы',
|
||||
icon: 'pi pi-list',
|
||||
iconColor: 'text-primary-500',
|
||||
route: '/marketing-analysis/analyses'
|
||||
},
|
||||
{
|
||||
id: 'promotion',
|
||||
label: 'Продвижение',
|
||||
@@ -58,6 +65,13 @@ const menuItems = [
|
||||
iconColor: 'text-primary-500',
|
||||
route: '/marketing-analysis/promotion'
|
||||
},
|
||||
{
|
||||
id: 'strategies-list',
|
||||
label: 'Мои стратегии',
|
||||
icon: 'pi pi-briefcase',
|
||||
iconColor: 'text-primary-500',
|
||||
route: '/marketing-analysis/strategies'
|
||||
},
|
||||
{
|
||||
id: 'results',
|
||||
label: 'Результаты',
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
<template>
|
||||
<div class="marketing-strategies-list-page bg-surface-50 dark:bg-surface-900 min-h-screen p-6">
|
||||
<Toast />
|
||||
<div class="max-w-[1400px] mx-auto">
|
||||
<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>
|
||||
<Button label="Создать новую стратегию" icon="pi pi-plus" severity="success" @click="goToCreateStrategy" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<DataTable
|
||||
:value="strategies"
|
||||
:loading="loading"
|
||||
paginator
|
||||
:rows="10"
|
||||
:rowsPerPageOptions="[5, 10, 20, 50]"
|
||||
sortMode="multiple"
|
||||
removableSort
|
||||
:sortField="'createdAt'"
|
||||
:sortOrder="-1"
|
||||
filterDisplay="row"
|
||||
:globalFilterFields="['analysisId', 'status']"
|
||||
v-model:filters="filters"
|
||||
:filters="filters"
|
||||
dataKey="strategyId"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search" />
|
||||
<InputText v-model="filters['global'].value" placeholder="Поиск стратегий..." />
|
||||
</span>
|
||||
<Button label="Обновить" icon="pi pi-refresh" severity="secondary" @click="loadStrategies" :loading="loading" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Column field="analysisId" header="ID анализа" :sortable="true" style="min-width: 200px">
|
||||
<template #body="slotProps">
|
||||
<span class="font-mono text-sm">{{ slotProps.data.analysisId }}</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)" />
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<Dropdown v-model="filterModel.value" :options="statusOptions" placeholder="Все статусы" class="p-column-filter" @change="filterCallback()">
|
||||
<template #option="slotProps">
|
||||
<Tag :value="slotProps.option.label" :severity="slotProps.option.severity" />
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="durationWeeks" header="Длительность" :sortable="true" style="min-width: 120px">
|
||||
<template #body="slotProps">
|
||||
<span>{{ slotProps.data.durationWeeks }} {{ pluralize(slotProps.data.durationWeeks, 'неделя', 'недели', 'недель') }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="priorityPlatforms" header="Платформы" style="min-width: 200px">
|
||||
<template #body="slotProps">
|
||||
<div v-if="slotProps.data.priorityPlatforms && slotProps.data.priorityPlatforms.length > 0" class="flex flex-wrap gap-1">
|
||||
<Tag v-for="platform in slotProps.data.priorityPlatforms" :key="platform" :value="platform" severity="secondary" />
|
||||
</div>
|
||||
<span v-else class="text-surface-400">—</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="createdAt" header="Создана" :sortable="true" style="min-width: 180px">
|
||||
<template #body="slotProps">
|
||||
<span>{{ formatDateTime(slotProps.data.createdAt) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="completedAt" header="Завершена" :sortable="true" style="min-width: 180px">
|
||||
<template #body="slotProps">
|
||||
<span v-if="slotProps.data.completedAt">{{ formatDateTime(slotProps.data.completedAt) }}</span>
|
||||
<span v-else class="text-surface-400">—</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Действия" style="min-width: 200px">
|
||||
<template #body="slotProps">
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<Button icon="pi pi-eye" severity="info" size="small" v-tooltip.top="'Просмотр деталей'" @click="viewStrategy(slotProps.data.strategyId)" :disabled="slotProps.data.status !== 'completed'" />
|
||||
<Button icon="pi pi-history" severity="secondary" size="small" v-tooltip.top="'История статусов'" @click="viewHistory(slotProps.data.strategyId)" />
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Диалог истории статусов -->
|
||||
<Dialog v-model:visible="historyDialogVisible" :header="'История статусов стратегии'" :style="{ width: '600px' }" :modal="true">
|
||||
<div v-if="historyLoading" class="text-center py-4">
|
||||
<ProgressSpinner />
|
||||
</div>
|
||||
<div v-else-if="statusHistory && statusHistory.length > 0" class="status-history">
|
||||
<Timeline :value="statusHistory" align="left" class="w-full">
|
||||
<template #marker="slotProps">
|
||||
<span class="flex w-2rem h-2rem align-items-center justify-content-center text-white border-circle z-1" :class="getStatusColorClass(slotProps.item.status)">
|
||||
<i :class="getStatusIcon(slotProps.item.status)"></i>
|
||||
</span>
|
||||
</template>
|
||||
<template #content="slotProps">
|
||||
<div class="flex flex-column">
|
||||
<div class="flex align-items-center gap-2 mb-2">
|
||||
<Tag :value="getStatusLabel(slotProps.item.status)" :severity="getStatusSeverity(slotProps.item.status)" />
|
||||
<span class="text-sm text-surface-500 dark:text-surface-400">{{ formatDateTime(slotProps.item.timestamp) }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-surface-700 dark:text-surface-300 m-0">{{ slotProps.item.message }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</Timeline>
|
||||
</div>
|
||||
<div v-else class="text-center py-4 text-surface-500">История статусов недоступна</div>
|
||||
<template #footer>
|
||||
<Button label="Закрыть" icon="pi pi-times" @click="historyDialogVisible = false" severity="secondary" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MarketingService from '@/service/MarketingService';
|
||||
import { FilterMatchMode } from '@primevue/core/api';
|
||||
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 ProgressSpinner from 'primevue/progressspinner';
|
||||
import Tag from 'primevue/tag';
|
||||
import Timeline from 'primevue/timeline';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const toast = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
// Data
|
||||
const strategies = ref([]);
|
||||
const loading = ref(false);
|
||||
const historyLoading = ref(false);
|
||||
const statusHistory = ref([]);
|
||||
const historyDialogVisible = ref(false);
|
||||
const currentStrategyId = ref(null);
|
||||
|
||||
// Filters
|
||||
const filters = ref({
|
||||
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
||||
status: { value: null, matchMode: FilterMatchMode.EQUALS }
|
||||
});
|
||||
|
||||
// Status options for filter
|
||||
const statusOptions = [
|
||||
{ label: 'В очереди', value: 'queued', severity: 'info' },
|
||||
{ label: 'Обрабатывается', value: 'processing', severity: 'warning' },
|
||||
{ label: 'Завершена', value: 'completed', severity: 'success' },
|
||||
{ label: 'Ошибка', value: 'failed', severity: 'danger' }
|
||||
];
|
||||
|
||||
// Load strategies
|
||||
const loadStrategies = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await MarketingService.getMyStrategies();
|
||||
strategies.value = data || [];
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке списка стратегий:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: error.message || 'Не удалось загрузить список стратегий',
|
||||
life: 5000
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// View strategy details
|
||||
const viewStrategy = (strategyId) => {
|
||||
router.push({
|
||||
name: 'marketing-promotion',
|
||||
query: { strategyId }
|
||||
});
|
||||
};
|
||||
|
||||
// View history
|
||||
const viewHistory = async (strategyId) => {
|
||||
currentStrategyId.value = strategyId;
|
||||
historyDialogVisible.value = true;
|
||||
historyLoading.value = true;
|
||||
statusHistory.value = [];
|
||||
|
||||
try {
|
||||
const data = await MarketingService.getStrategyHistory(strategyId);
|
||||
statusHistory.value = data.statusHistory || [];
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке истории:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: error.message || 'Не удалось загрузить историю статусов',
|
||||
life: 5000
|
||||
});
|
||||
} finally {
|
||||
historyLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Navigate to create strategy
|
||||
const goToCreateStrategy = () => {
|
||||
router.push({ name: 'marketing-promotion' });
|
||||
};
|
||||
|
||||
// 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'
|
||||
});
|
||||
};
|
||||
|
||||
// Get status label
|
||||
const getStatusLabel = (status) => {
|
||||
const labels = {
|
||||
queued: 'В очереди',
|
||||
processing: 'Обрабатывается',
|
||||
completed: 'Завершена',
|
||||
failed: 'Ошибка'
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
// Get status severity
|
||||
const getStatusSeverity = (status) => {
|
||||
const severities = {
|
||||
queued: 'info',
|
||||
processing: 'warning',
|
||||
completed: 'success',
|
||||
failed: 'danger'
|
||||
};
|
||||
return severities[status] || null;
|
||||
};
|
||||
|
||||
// Get status color class for timeline
|
||||
const getStatusColorClass = (status) => {
|
||||
const classes = {
|
||||
queued: 'bg-blue-500',
|
||||
processing: 'bg-yellow-500',
|
||||
completed: 'bg-green-500',
|
||||
failed: 'bg-red-500'
|
||||
};
|
||||
return classes[status] || 'bg-surface-500';
|
||||
};
|
||||
|
||||
// Get status icon
|
||||
const getStatusIcon = (status) => {
|
||||
const icons = {
|
||||
queued: 'pi pi-clock',
|
||||
processing: 'pi pi-spin pi-spinner',
|
||||
completed: 'pi pi-check',
|
||||
failed: 'pi pi-times'
|
||||
};
|
||||
return icons[status] || 'pi pi-circle';
|
||||
};
|
||||
|
||||
// Pluralize
|
||||
const pluralize = (count, one, few, many) => {
|
||||
const mod10 = count % 10;
|
||||
const mod100 = count % 100;
|
||||
|
||||
if (mod10 === 1 && mod100 !== 11) return one;
|
||||
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return few;
|
||||
return many;
|
||||
};
|
||||
|
||||
// Load on mount
|
||||
onMounted(() => {
|
||||
loadStrategies();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.marketing-strategies-list-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;
|
||||
}
|
||||
|
||||
.status-history {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user