.
This commit is contained in:
@@ -0,0 +1,539 @@
|
||||
# API Документация: Маркетинговый анализ (Frontend/AI Agent)
|
||||
|
||||
## Базовый URL
|
||||
|
||||
```
|
||||
https://api.konturai.kz
|
||||
```
|
||||
|
||||
## Обзор
|
||||
|
||||
API для генерации маркетингового анализа на основе данных о бизнесе. Процесс состоит из двух этапов:
|
||||
|
||||
1. **Запуск анализа** - создание задачи и начало асинхронной обработки
|
||||
2. **Получение результатов** - проверка статуса и получение готового отчета
|
||||
|
||||
Анализ выполняется асинхронно и занимает примерно 5-10 минут.
|
||||
|
||||
---
|
||||
|
||||
## Эндпоинты
|
||||
|
||||
### 1. Запуск маркетингового анализа
|
||||
|
||||
**POST** `/api/marketing/analysis/start`
|
||||
|
||||
Создает новую задачу на генерацию маркетингового анализа и запускает асинхронную обработку.
|
||||
|
||||
#### Заголовки запроса
|
||||
|
||||
```
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
#### Тело запроса (JSON)
|
||||
|
||||
| Поле | Тип | Обязательный | Описание | Пример значения |
|
||||
| ---------------- | ------ | ------------ | ------------------------------ | -------------------------------- |
|
||||
| `product` | string | ✅ | Название продукта или услуги | "Веб-разработка" |
|
||||
| `location` | string | ✅ | Географическая локация работы | "Алматы, Казахстан" |
|
||||
| `client` | string | ✅ | Тип целевой аудитории | "B2B клиенты" |
|
||||
| `differentiator` | string | ✅ | Уникальные особенности бизнеса | "Быстрая разработка за 2 недели" |
|
||||
|
||||
#### Валидация полей
|
||||
|
||||
**`product`** (string, обязательное)
|
||||
|
||||
- Минимальная длина: 3 символа
|
||||
- Максимальная длина: 200 символов
|
||||
- Разрешены: буквы, цифры, пробелы, дефисы, запятые
|
||||
|
||||
**`location`** (string, обязательное)
|
||||
|
||||
- Минимальная длина: 2 символа
|
||||
- Максимальная длина: 150 символов
|
||||
- Разрешены: буквы, цифры, пробелы, запятые, дефисы
|
||||
|
||||
**`client`** (string, обязательное)
|
||||
|
||||
- Допустимые значения (точно):
|
||||
- `"B2B клиенты"`
|
||||
- `"B2C клиенты"`
|
||||
- `"Частные лица"`
|
||||
- `"Корпорации"`
|
||||
- `"Малый бизнес"`
|
||||
|
||||
**`differentiator`** (string, обязательное)
|
||||
|
||||
- Минимальная длина: 10 символов
|
||||
- Максимальная длина: 500 символов
|
||||
- Разрешены любые символы
|
||||
|
||||
#### Пример запроса
|
||||
|
||||
```json
|
||||
{
|
||||
"product": "Разработка мобильных приложений",
|
||||
"location": "Нур-Султан, Казахстан",
|
||||
"client": "B2B клиенты",
|
||||
"differentiator": "Специализируемся на быстрой разработке MVP за 4 недели с использованием современных технологий"
|
||||
}
|
||||
```
|
||||
|
||||
#### Пример успешного ответа (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Анализ запущен успешно",
|
||||
"data": {
|
||||
"analysisId": "507f1f77bcf86cd799439011",
|
||||
"status": "processing",
|
||||
"estimatedCompletionTime": "2025-01-20T15:38:00",
|
||||
"message": "Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Структура ответа
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
| ------------------------------ | ------- | --------------------------------------------------------- |
|
||||
| `success` | boolean | Флаг успешности операции |
|
||||
| `message` | string | Сообщение о результате операции |
|
||||
| `data.analysisId` | string | Уникальный идентификатор анализа (MongoDB ObjectId) |
|
||||
| `data.status` | string | Статус анализа: `"processing"` |
|
||||
| `data.estimatedCompletionTime` | string | ISO 8601 дата/время ожидаемого завершения (LocalDateTime) |
|
||||
| `data.message` | string | Информационное сообщение для пользователя |
|
||||
|
||||
#### Пример ошибки валидации (400 Bad Request)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Ошибка валидации",
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Ошибка валидации входных данных",
|
||||
"details": {
|
||||
"product": "Поле 'product' должно содержать от 3 до 200 символов",
|
||||
"client": "Поле 'client' должно быть одним из: B2B клиенты, B2C клиенты, Частные лица, Корпорации, Малый бизнес"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Получение результата анализа
|
||||
|
||||
**GET** `/api/marketing/analysis/{analysisId}`
|
||||
|
||||
Возвращает статус и результаты анализа по идентификатору.
|
||||
|
||||
#### Параметры пути
|
||||
|
||||
| Параметр | Тип | Описание |
|
||||
| ------------ | ------ | --------------------- |
|
||||
| `analysisId` | string | Идентификатор анализа |
|
||||
|
||||
#### Пример запроса
|
||||
|
||||
```
|
||||
GET /api/marketing/analysis/507f1f77bcf86cd799439011
|
||||
```
|
||||
|
||||
#### Пример ответа (когда анализ завершен - 200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Операция выполнена успешно",
|
||||
"data": {
|
||||
"analysisId": "507f1f77bcf86cd799439011",
|
||||
"status": "completed",
|
||||
"createdAt": "2025-01-20T15:30:00",
|
||||
"completedAt": "2025-01-20T15:38:00",
|
||||
"report": {
|
||||
"summary": "Краткое резюме анализа...",
|
||||
"targetAudience": {
|
||||
"description": "Описание целевой аудитории...",
|
||||
"channels": ["Instagram", "LinkedIn", "Telegram"]
|
||||
},
|
||||
"recommendations": ["Рекомендация 1", "Рекомендация 2", "Рекомендация 3"],
|
||||
"strategy": {
|
||||
"duration": "2 недели",
|
||||
"channels": ["Instagram", "Telegram", "21MC"],
|
||||
"contentTypes": ["посты", "сторис", "баннеры"]
|
||||
},
|
||||
"pdfUrl": "/api/marketing/analysis/507f1f77bcf86cd799439011/download"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Пример ответа (когда анализ еще обрабатывается - 200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Операция выполнена успешно",
|
||||
"data": {
|
||||
"analysisId": "507f1f77bcf86cd799439011",
|
||||
"status": "processing",
|
||||
"createdAt": "2025-01-20T15:30:00",
|
||||
"completedAt": null,
|
||||
"report": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Статусы анализа
|
||||
|
||||
| Статус | Описание |
|
||||
| ------------ | ----------------------------- |
|
||||
| `queued` | Запрос в очереди на обработку |
|
||||
| `processing` | Анализ выполняется |
|
||||
| `completed` | Анализ завершен успешно |
|
||||
| `failed` | Анализ завершился с ошибкой |
|
||||
|
||||
#### Структура ответа
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
| ---------------------------------------- | ------- | ------------------------------------------------------ |
|
||||
| `success` | boolean | Флаг успешности операции |
|
||||
| `message` | string | Сообщение о результате операции |
|
||||
| `data.analysisId` | string | Уникальный идентификатор анализа |
|
||||
| `data.status` | string | Статус анализа |
|
||||
| `data.createdAt` | string | ISO 8601 дата/время создания |
|
||||
| `data.completedAt` | string | ISO 8601 дата/время завершения (null если не завершен) |
|
||||
| `data.report` | object | Объект с результатами (null если не завершен) |
|
||||
| `data.report.summary` | string | Краткое резюме анализа |
|
||||
| `data.report.targetAudience` | object | Информация о целевой аудитории |
|
||||
| `data.report.targetAudience.description` | string | Описание целевой аудитории |
|
||||
| `data.report.targetAudience.channels` | array | Список рекомендуемых каналов |
|
||||
| `data.report.recommendations` | array | Список рекомендаций (массив строк) |
|
||||
| `data.report.strategy` | object | Маркетинговая стратегия |
|
||||
| `data.report.strategy.duration` | string | Длительность кампании (например, "2 недели") |
|
||||
| `data.report.strategy.channels` | array | Каналы коммуникации (массив строк) |
|
||||
| `data.report.strategy.contentTypes` | array | Типы контента (массив строк) |
|
||||
| `data.report.pdfUrl` | string | URL для скачивания PDF отчета |
|
||||
|
||||
#### Пример ошибки (404 Not Found)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Анализ не найден",
|
||||
"error": {
|
||||
"code": "NOT_FOUND",
|
||||
"message": "Анализ с указанным ID не найден"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Скачивание PDF отчета
|
||||
|
||||
**GET** `/api/marketing/analysis/{analysisId}/download`
|
||||
|
||||
Возвращает PDF файл с полным маркетинговым отчетом.
|
||||
|
||||
#### Параметры пути
|
||||
|
||||
| Параметр | Тип | Описание |
|
||||
| ------------ | ------ | --------------------- |
|
||||
| `analysisId` | string | Идентификатор анализа |
|
||||
|
||||
#### Пример запроса
|
||||
|
||||
```
|
||||
GET /api/marketing/analysis/507f1f77bcf86cd799439011/download
|
||||
```
|
||||
|
||||
#### Успешный ответ (200 OK)
|
||||
|
||||
- **Content-Type**: `application/pdf`
|
||||
- **Content-Disposition**: `attachment; filename="marketing_analysis_507f1f77bcf86cd799439011_1234567890.pdf"`
|
||||
- **Body**: Бинарные данные PDF файла
|
||||
|
||||
#### Ошибки
|
||||
|
||||
- **404 Not Found** - Анализ не найден или PDF еще не сгенерирован
|
||||
- **500 Internal Server Error** - Ошибка при получении файла
|
||||
|
||||
---
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
### Коды ошибок
|
||||
|
||||
| Код | HTTP статус | Описание |
|
||||
| ----------------------- | ----------- | ------------------------------- |
|
||||
| `VALIDATION_ERROR` | 400 | Ошибка валидации входных данных |
|
||||
| `NOT_FOUND` | 404 | Ресурс не найден |
|
||||
| `INTERNAL_SERVER_ERROR` | 500 | Внутренняя ошибка сервера |
|
||||
|
||||
### Формат ошибки
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Описание ошибки",
|
||||
"error": {
|
||||
"code": "ERROR_CODE",
|
||||
"message": "Детальное сообщение об ошибке",
|
||||
"details": {
|
||||
"field1": "Сообщение об ошибке для поля 1",
|
||||
"field2": "Сообщение об ошибке для поля 2"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Примечание**: Поле `details` присутствует только для ошибок валидации (`VALIDATION_ERROR`).
|
||||
|
||||
---
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### JavaScript/TypeScript (Fetch API)
|
||||
|
||||
#### Запуск анализа
|
||||
|
||||
```javascript
|
||||
async function startMarketingAnalysis(data) {
|
||||
const response = await fetch('https://api.konturai.kz/api/marketing/analysis/start', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product: 'Разработка мобильных приложений',
|
||||
location: 'Нур-Султан, Казахстан',
|
||||
client: 'B2B клиенты',
|
||||
differentiator: 'Специализируемся на быстрой разработке MVP за 4 недели'
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
console.log('Analysis ID:', result.data.analysisId);
|
||||
return result.data.analysisId;
|
||||
} else {
|
||||
console.error('Error:', result.error);
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Проверка статуса и получение результата
|
||||
|
||||
```javascript
|
||||
async function getAnalysisResult(analysisId) {
|
||||
const response = await fetch(`https://api.konturai.kz/api/marketing/analysis/${analysisId}`);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const { status, report } = result.data;
|
||||
|
||||
if (status === 'completed' && report) {
|
||||
console.log('Analysis completed!');
|
||||
console.log('Summary:', report.summary);
|
||||
console.log('Recommendations:', report.recommendations);
|
||||
return report;
|
||||
} else if (status === 'processing') {
|
||||
console.log('Analysis is still processing...');
|
||||
return null; // Повторить запрос позже
|
||||
} else if (status === 'failed') {
|
||||
throw new Error('Analysis failed');
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Полный цикл с polling
|
||||
|
||||
```javascript
|
||||
async function waitForAnalysisCompletion(analysisId, maxAttempts = 60, intervalMs = 10000) {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const result = await getAnalysisResult(analysisId);
|
||||
|
||||
if (result) {
|
||||
return result; // Анализ завершен
|
||||
}
|
||||
|
||||
// Ждем перед следующей проверкой
|
||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||
}
|
||||
|
||||
throw new Error('Analysis timeout');
|
||||
}
|
||||
|
||||
// Использование
|
||||
async function runFullAnalysis() {
|
||||
try {
|
||||
// 1. Запускаем анализ
|
||||
const analysisId = await startMarketingAnalysis({
|
||||
product: 'Веб-разработка',
|
||||
location: 'Алматы, Казахстан',
|
||||
client: 'B2B клиенты',
|
||||
differentiator: 'Быстрая разработка за 2 недели'
|
||||
});
|
||||
|
||||
console.log(`Analysis started: ${analysisId}`);
|
||||
|
||||
// 2. Ждем завершения (проверяем каждые 10 секунд, максимум 10 минут)
|
||||
const report = await waitForAnalysisCompletion(analysisId, 60, 10000);
|
||||
|
||||
// 3. Используем результаты
|
||||
console.log('Report summary:', report.summary);
|
||||
console.log('Channels:', report.targetAudience.channels);
|
||||
console.log('PDF URL:', report.pdfUrl);
|
||||
|
||||
return report;
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Скачивание PDF
|
||||
|
||||
```javascript
|
||||
async function downloadPdf(analysisId) {
|
||||
const response = await fetch(`https://api.konturai.kz/api/marketing/analysis/${analysisId}/download`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to download PDF');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `marketing_analysis_${analysisId}.pdf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
import time
|
||||
|
||||
BASE_URL = "https://api.konturai.kz"
|
||||
|
||||
def start_analysis(product, location, client, differentiator):
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/api/marketing/analysis/start",
|
||||
json={
|
||||
"product": product,
|
||||
"location": location,
|
||||
"client": client,
|
||||
"differentiator": differentiator
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if data["success"]:
|
||||
return data["data"]["analysisId"]
|
||||
else:
|
||||
raise Exception(data["error"]["message"])
|
||||
|
||||
def get_analysis_result(analysis_id):
|
||||
response = requests.get(
|
||||
f"{BASE_URL}/api/marketing/analysis/{analysis_id}"
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()["data"]
|
||||
|
||||
def wait_for_completion(analysis_id, max_attempts=60, interval=10):
|
||||
for _ in range(max_attempts):
|
||||
result = get_analysis_result(analysis_id)
|
||||
|
||||
if result["status"] == "completed":
|
||||
return result["report"]
|
||||
elif result["status"] == "failed":
|
||||
raise Exception("Analysis failed")
|
||||
|
||||
time.sleep(interval)
|
||||
|
||||
raise Exception("Analysis timeout")
|
||||
|
||||
# Использование
|
||||
analysis_id = start_analysis(
|
||||
product="Разработка мобильных приложений",
|
||||
location="Нур-Султан, Казахстан",
|
||||
client="B2B клиенты",
|
||||
differentiator="Быстрая разработка MVP за 4 недели"
|
||||
)
|
||||
|
||||
print(f"Analysis started: {analysis_id}")
|
||||
|
||||
report = wait_for_completion(analysis_id)
|
||||
print(f"Summary: {report['summary']}")
|
||||
print(f"Channels: {report['targetAudience']['channels']}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Рекомендации по интеграции
|
||||
|
||||
### 1. Polling стратегия
|
||||
|
||||
Рекомендуется проверять статус анализа каждые 10-15 секунд. Максимальное время ожидания - 10-15 минут.
|
||||
|
||||
### 2. Обработка ошибок
|
||||
|
||||
Всегда проверяйте поле `success` в ответе и обрабатывайте ошибки соответствующим образом.
|
||||
|
||||
### 3. Валидация на клиенте
|
||||
|
||||
Перед отправкой запроса рекомендуется валидировать данные на клиенте:
|
||||
|
||||
- Проверка длины полей
|
||||
- Проверка допустимых значений для `client`
|
||||
- Проверка обязательных полей
|
||||
|
||||
### 4. UX рекомендации
|
||||
|
||||
- Показывайте индикатор загрузки во время обработки
|
||||
- Отображайте примерное время завершения
|
||||
- Предоставьте возможность отменить ожидание и проверить результат позже
|
||||
- Сохраняйте `analysisId` для последующей проверки статуса
|
||||
|
||||
### 5. Кэширование
|
||||
|
||||
После получения результатов можно кэшировать их локально, используя `analysisId` как ключ.
|
||||
|
||||
---
|
||||
|
||||
## Примечания
|
||||
|
||||
1. **Формат даты**: Все даты возвращаются в формате ISO 8601 без timezone (LocalDateTime)
|
||||
2. **Идентификаторы**: Используются MongoDB ObjectId (24 символа hex)
|
||||
3. **Асинхронность**: Анализ выполняется асинхронно, не блокируя запрос
|
||||
4. **Таймауты**: Рекомендуется устанавливать таймаут на запросы (минимум 30 секунд для запуска анализа)
|
||||
5. **Rate Limiting**: В будущем может быть добавлено ограничение на количество запросов
|
||||
|
||||
---
|
||||
|
||||
## Поддержка
|
||||
|
||||
При возникновении проблем с API обращайтесь в техническую поддержку с указанием:
|
||||
|
||||
- `analysisId` (если есть)
|
||||
- Время запроса
|
||||
- Описание проблемы
|
||||
- Код ошибки (если есть)
|
||||
@@ -0,0 +1,358 @@
|
||||
# API Документация: Маркетинговый анализ
|
||||
|
||||
## Обзор
|
||||
|
||||
API для запуска маркетингового анализа на основе данных о бизнесе пользователя. Эндпоинт принимает информацию о продукте, локации, типе клиентов и уникальных особенностях бизнеса, затем генерирует маркетинговый отчет.
|
||||
|
||||
## Эндпоинт
|
||||
|
||||
**POST** `/api/marketing/analysis/start`
|
||||
|
||||
### Базовый URL
|
||||
|
||||
```
|
||||
https://api.konturai.kz/api/marketing/analysis/start
|
||||
```
|
||||
|
||||
## Запрос
|
||||
|
||||
### Заголовки
|
||||
|
||||
```
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {access_token} // Опционально, если требуется аутентификация
|
||||
```
|
||||
|
||||
### Тело запроса (JSON)
|
||||
|
||||
| Поле | Тип | Обязательный | Описание | Пример значения |
|
||||
| ---------------- | ------ | ------------ | ------------------------------ | -------------------------------- |
|
||||
| `product` | string | ✅ | Название продукта или услуги | "Веб-разработка" |
|
||||
| `location` | string | ✅ | Географическая локация работы | "Алматы, Казахстан" |
|
||||
| `client` | string | ✅ | Тип целевой аудитории | "B2B клиенты" |
|
||||
| `differentiator` | string | ✅ | Уникальные особенности бизнеса | "Быстрая разработка за 2 недели" |
|
||||
|
||||
### Валидация полей
|
||||
|
||||
#### `product` (string, обязательное)
|
||||
|
||||
- **Минимальная длина**: 3 символа
|
||||
- **Максимальная длина**: 200 символов
|
||||
- **Паттерн**: Разрешены буквы, цифры, пробелы, дефисы, запятые
|
||||
- **Ошибка валидации**: `"product": "Поле 'product' должно содержать от 3 до 200 символов"`
|
||||
|
||||
#### `location` (string, обязательное)
|
||||
|
||||
- **Минимальная длина**: 2 символа
|
||||
- **Максимальная длина**: 150 символов
|
||||
- **Паттерн**: Разрешены буквы, цифры, пробелы, запятые, дефисы
|
||||
- **Ошибка валидации**: `"location": "Поле 'location' должно содержать от 2 до 150 символов"`
|
||||
|
||||
#### `client` (string, обязательное)
|
||||
|
||||
- **Допустимые значения**:
|
||||
- `"B2B клиенты"`
|
||||
- `"B2C клиенты"`
|
||||
- `"Частные лица"`
|
||||
- `"Корпорации"`
|
||||
- `"Малый бизнес"`
|
||||
- **Ошибка валидации**: `"client": "Поле 'client' должно быть одним из: B2B клиенты, B2C клиенты, Частные лица, Корпорации, Малый бизнес"`
|
||||
|
||||
#### `differentiator` (string, обязательное)
|
||||
|
||||
- **Минимальная длина**: 10 символов
|
||||
- **Максимальная длина**: 500 символов
|
||||
- **Паттерн**: Разрешены любые символы
|
||||
- **Ошибка валидации**: `"differentiator": "Поле 'differentiator' должно содержать от 10 до 500 символов"`
|
||||
|
||||
### Пример запроса
|
||||
|
||||
```json
|
||||
{
|
||||
"product": "Разработка мобильных приложений",
|
||||
"location": "Нур-Султан, Казахстан",
|
||||
"client": "B2B клиенты",
|
||||
"differentiator": "Специализируемся на быстрой разработке MVP за 4 недели с использованием современных технологий"
|
||||
}
|
||||
```
|
||||
|
||||
## Ответ
|
||||
|
||||
### Успешный ответ (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"analysisId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"status": "processing",
|
||||
"estimatedCompletionTime": "2025-01-20T15:30:00Z",
|
||||
"message": "Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Поля ответа
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
| ------------------------------ | ------- | --------------------------------------------------------- |
|
||||
| `success` | boolean | Флаг успешности операции |
|
||||
| `data.analysisId` | string | Уникальный идентификатор анализа (UUID) |
|
||||
| `data.status` | string | Статус анализа: `"processing"`, `"completed"`, `"failed"` |
|
||||
| `data.estimatedCompletionTime` | string | ISO 8601 дата/время ожидаемого завершения анализа |
|
||||
| `data.message` | string | Информационное сообщение для пользователя |
|
||||
|
||||
### Асинхронная обработка (202 Accepted)
|
||||
|
||||
Если анализ требует длительной обработки, сервер может вернуть статус 202:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"analysisId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"status": "queued",
|
||||
"queuePosition": 3,
|
||||
"estimatedWaitTime": 300,
|
||||
"message": "Запрос добавлен в очередь. Примерное время ожидания: 5 минут."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
### Ошибки валидации (400 Bad Request)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Ошибка валидации входных данных",
|
||||
"details": {
|
||||
"product": "Поле 'product' обязательно для заполнения",
|
||||
"client": "Недопустимое значение поля 'client'"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ошибка аутентификации (401 Unauthorized)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "UNAUTHORIZED",
|
||||
"message": "Требуется аутентификация"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ошибка сервера (500 Internal Server Error)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "INTERNAL_SERVER_ERROR",
|
||||
"message": "Произошла внутренняя ошибка сервера. Попробуйте позже."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ошибка таймаута (504 Gateway Timeout)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "TIMEOUT",
|
||||
"message": "Превышено время ожидания ответа от сервиса анализа"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Получение результатов анализа
|
||||
|
||||
После успешного запуска анализа, результаты можно получить по идентификатору:
|
||||
|
||||
**GET** `/api/marketing/analysis/{analysisId}`
|
||||
|
||||
### Пример запроса
|
||||
|
||||
```
|
||||
GET /api/marketing/analysis/550e8400-e29b-41d4-a716-446655440000
|
||||
```
|
||||
|
||||
### Пример ответа (когда анализ завершен)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"analysisId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"status": "completed",
|
||||
"createdAt": "2025-01-20T15:00:00Z",
|
||||
"completedAt": "2025-01-20T15:08:00Z",
|
||||
"report": {
|
||||
"summary": "Краткое резюме анализа...",
|
||||
"targetAudience": {
|
||||
"description": "Описание целевой аудитории...",
|
||||
"channels": ["Instagram", "LinkedIn", "Telegram"]
|
||||
},
|
||||
"recommendations": ["Рекомендация 1", "Рекомендация 2"],
|
||||
"strategy": {
|
||||
"duration": "2 недели",
|
||||
"channels": ["Instagram", "Telegram", "21MC"],
|
||||
"contentTypes": ["посты", "сторис", "баннеры"]
|
||||
},
|
||||
"pdfUrl": "/api/marketing/analysis/550e8400-e29b-41d4-a716-446655440000/download"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Статусы анализа
|
||||
|
||||
- `queued` - Запрос в очереди на обработку
|
||||
- `processing` - Анализ выполняется
|
||||
- `completed` - Анализ завершен успешно
|
||||
- `failed` - Анализ завершился с ошибкой
|
||||
|
||||
## Рекомендации по реализации
|
||||
|
||||
### 1. Валидация на бэкенде
|
||||
|
||||
```java
|
||||
// Пример валидации (Java/Spring Boot)
|
||||
@PostMapping("/api/marketing/analysis/start")
|
||||
public ResponseEntity<?> startAnalysis(@Valid @RequestBody MarketingAnalysisRequest request) {
|
||||
// Валидация выполняется автоматически через @Valid
|
||||
// Дополнительная бизнес-логика валидации
|
||||
if (!isValidClientType(request.getClient())) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(new ErrorResponse("VALIDATION_ERROR", "Недопустимый тип клиента"));
|
||||
}
|
||||
// Обработка запроса
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Асинхронная обработка
|
||||
|
||||
Рекомендуется использовать асинхронную обработку для длительных операций:
|
||||
|
||||
```java
|
||||
@Async
|
||||
public CompletableFuture<AnalysisResult> processAnalysis(MarketingAnalysisRequest request) {
|
||||
// Длительная обработка
|
||||
// Генерация отчета
|
||||
// Сохранение результатов
|
||||
return CompletableFuture.completedFuture(result);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Хранение данных
|
||||
|
||||
Рекомендуемая структура таблицы в БД:
|
||||
|
||||
```sql
|
||||
CREATE TABLE marketing_analysis (
|
||||
id UUID PRIMARY KEY,
|
||||
product VARCHAR(200) NOT NULL,
|
||||
location VARCHAR(150) NOT NULL,
|
||||
client_type VARCHAR(50) NOT NULL,
|
||||
differentiator TEXT NOT NULL,
|
||||
status VARCHAR(20) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
completed_at TIMESTAMP,
|
||||
user_id UUID, -- Если требуется аутентификация
|
||||
report_data JSONB, -- JSON с результатами анализа
|
||||
CONSTRAINT valid_client_type CHECK (client_type IN (
|
||||
'B2B клиенты', 'B2C клиенты', 'Частные лица', 'Корпорации', 'Малый бизнес'
|
||||
)),
|
||||
CONSTRAINT valid_status CHECK (status IN (
|
||||
'queued', 'processing', 'completed', 'failed'
|
||||
))
|
||||
);
|
||||
```
|
||||
|
||||
### 4. Интеграция с AI сервисом
|
||||
|
||||
Если используется внешний AI сервис для генерации анализа:
|
||||
|
||||
```java
|
||||
public AnalysisResult generateAnalysis(MarketingAnalysisRequest request) {
|
||||
// Подготовка промпта для AI
|
||||
String prompt = String.format(
|
||||
"Проанализируй бизнес:\n" +
|
||||
"Продукт: %s\n" +
|
||||
"Локация: %s\n" +
|
||||
"Клиенты: %s\n" +
|
||||
"Уникальность: %s\n" +
|
||||
"Создай маркетинговую стратегию...",
|
||||
request.getProduct(),
|
||||
request.getLocation(),
|
||||
request.getClient(),
|
||||
request.getDifferentiator()
|
||||
);
|
||||
|
||||
// Вызов AI API
|
||||
return aiService.generateReport(prompt);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Обработка ошибок
|
||||
|
||||
```java
|
||||
@ExceptionHandler(ValidationException.class)
|
||||
public ResponseEntity<ErrorResponse> handleValidationException(ValidationException e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(new ErrorResponse("VALIDATION_ERROR", e.getMessage(), e.getDetails()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponse> handleGenericException(Exception e) {
|
||||
log.error("Unexpected error", e);
|
||||
return ResponseEntity.status(500)
|
||||
.body(new ErrorResponse("INTERNAL_SERVER_ERROR", "Внутренняя ошибка сервера"));
|
||||
}
|
||||
```
|
||||
|
||||
## Тестирование
|
||||
|
||||
### Примеры тестовых запросов
|
||||
|
||||
#### Успешный запрос
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.konturai.kz/api/marketing/analysis/start \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"product": "Разработка мобильных приложений",
|
||||
"location": "Алматы, Казахстан",
|
||||
"client": "B2B клиенты",
|
||||
"differentiator": "Специализируемся на быстрой разработке MVP за 4 недели"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Запрос с ошибкой валидации
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.konturai.kz/api/marketing/analysis/start \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"product": "AB",
|
||||
"location": "Алматы",
|
||||
"client": "Неверный тип",
|
||||
"differentiator": "Коротко"
|
||||
}'
|
||||
```
|
||||
|
||||
## Примечания
|
||||
|
||||
1. **Аутентификация**: Если требуется аутентификация, используйте JWT токен в заголовке `Authorization`
|
||||
2. **Rate Limiting**: Рекомендуется ограничить количество запросов на пользователя (например, 10 запросов в час)
|
||||
3. **Кэширование**: Можно кэшировать результаты для одинаковых запросов
|
||||
4. **Логирование**: Все запросы должны логироваться для отладки и аналитики
|
||||
5. **Мониторинг**: Отслеживайте время выполнения анализа и процент успешных завершений
|
||||
@@ -0,0 +1,922 @@
|
||||
# API Документация: Генерация стратегии продвижения (Frontend/AI Agent)
|
||||
|
||||
## Базовый URL
|
||||
|
||||
```
|
||||
https://api.konturai.kz
|
||||
```
|
||||
|
||||
## Обзор
|
||||
|
||||
API для генерации детальной стратегии продвижения продукта на основе маркетингового анализа. Стратегия включает:
|
||||
|
||||
1. **Недельный план** - темы и рекомендации по контенту для каждой недели
|
||||
2. **Календарь постов** - детальный план публикаций с датами, платформами, текстами и хештегами
|
||||
|
||||
Процесс состоит из двух этапов:
|
||||
|
||||
1. **Запуск генерации стратегии** - создание задачи и начало асинхронной обработки
|
||||
2. **Получение результатов** - проверка статуса и получение готовой стратегии
|
||||
|
||||
Генерация стратегии выполняется асинхронно и занимает примерно 3-5 минут.
|
||||
|
||||
**Важно**: Для генерации стратегии требуется завершенный маркетинговый анализ. Сначала необходимо получить `analysisId` из завершенного анализа.
|
||||
|
||||
---
|
||||
|
||||
## Эндпоинты
|
||||
|
||||
### 1. Запуск генерации стратегии продвижения
|
||||
|
||||
**POST** `/api/marketing/strategy/generate`
|
||||
|
||||
Создает новую задачу на генерацию стратегии продвижения и запускает асинхронную обработку.
|
||||
|
||||
#### Параметры запроса
|
||||
|
||||
| Параметр | Тип | Расположение | Обязательный | Описание |
|
||||
| ------------------- | ------- | ------------ | ------------ | --------------------------------------- |
|
||||
| `analysisId` | string | Query | ✅ | ID завершенного маркетингового анализа |
|
||||
| `durationWeeks` | integer | Body | ❌ | Длительность стратегии в неделях (1-12) |
|
||||
| `priorityPlatforms` | array | Body | ❌ | Приоритетные платформы для продвижения |
|
||||
|
||||
#### Заголовки запроса
|
||||
|
||||
```
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
#### Тело запроса (JSON, опционально)
|
||||
|
||||
| Поле | Тип | Обязательный | Описание | Пример значения |
|
||||
| ------------------- | ------- | ------------ | --------------------------------------- | ------------------------------------- |
|
||||
| `durationWeeks` | integer | ❌ | Длительность стратегии в неделях (1-12) | 4 |
|
||||
| `priorityPlatforms` | array | ❌ | Список приоритетных платформ | ["Instagram", "LinkedIn", "Telegram"] |
|
||||
|
||||
#### Валидация полей
|
||||
|
||||
**`durationWeeks`** (integer, опциональное)
|
||||
|
||||
- Минимальное значение: 1
|
||||
- Максимальное значение: 12
|
||||
- По умолчанию: 4 (если не указано)
|
||||
|
||||
**`priorityPlatforms`** (array, опциональное)
|
||||
|
||||
- Допустимые платформы: `Instagram`, `Facebook`, `LinkedIn`, `Telegram`, `TikTok`, `YouTube`, `21MC` и другие
|
||||
- Если не указано, используются все популярные платформы
|
||||
|
||||
#### Пример запроса
|
||||
|
||||
```http
|
||||
POST /api/marketing/strategy/generate?analysisId=507f1f77bcf86cd799439011
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"durationWeeks": 4,
|
||||
"priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"]
|
||||
}
|
||||
```
|
||||
|
||||
Или без тела запроса (используются значения по умолчанию):
|
||||
|
||||
```http
|
||||
POST /api/marketing/strategy/generate?analysisId=507f1f77bcf86cd799439011
|
||||
```
|
||||
|
||||
#### Пример успешного ответа (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Генерация стратегии запущена успешно. Результаты будут готовы в течение 3-5 минут.",
|
||||
"data": {
|
||||
"strategyId": "507f1f77bcf86cd799439012",
|
||||
"analysisId": "507f1f77bcf86cd799439011",
|
||||
"status": "queued",
|
||||
"createdAt": "2025-01-20T15:40:00",
|
||||
"durationWeeks": 4,
|
||||
"priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Структура ответа
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
| ------------------------ | ------- | ----------------------------------------------------------------------- |
|
||||
| `success` | boolean | Флаг успешности операции |
|
||||
| `message` | string | Сообщение о результате операции |
|
||||
| `data.strategyId` | string | Уникальный идентификатор стратегии (MongoDB ObjectId) |
|
||||
| `data.analysisId` | string | ID маркетингового анализа |
|
||||
| `data.status` | string | Статус стратегии: `"queued"`, `"processing"`, `"completed"`, `"failed"` |
|
||||
| `data.createdAt` | string | ISO 8601 дата/время создания |
|
||||
| `data.durationWeeks` | integer | Длительность стратегии в неделях |
|
||||
| `data.priorityPlatforms` | array | Список приоритетных платформ |
|
||||
|
||||
#### Пример ошибки (404 Not Found - анализ не найден)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Анализ не найден",
|
||||
"error": {
|
||||
"code": "INVALID_ANALYSIS",
|
||||
"message": "Анализ с ID 507f1f77bcf86cd799439011 не найден"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Пример ошибки (400 Bad Request - анализ не завершен)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Анализ еще не завершен",
|
||||
"error": {
|
||||
"code": "ANALYSIS_NOT_COMPLETED",
|
||||
"message": "Анализ еще не завершен. Статус: processing"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Пример ошибки валидации (400 Bad Request)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Ошибка валидации",
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Ошибка валидации входных данных",
|
||||
"details": {
|
||||
"durationWeeks": "Длительность стратегии должна быть не менее 1 недели"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Получение стратегии по ID
|
||||
|
||||
**GET** `/api/marketing/strategy/{strategyId}`
|
||||
|
||||
Возвращает статус и результаты стратегии по идентификатору.
|
||||
|
||||
#### Параметры пути
|
||||
|
||||
| Параметр | Тип | Описание |
|
||||
| ------------ | ------ | ----------------------- |
|
||||
| `strategyId` | string | Идентификатор стратегии |
|
||||
|
||||
#### Пример запроса
|
||||
|
||||
```
|
||||
GET /api/marketing/strategy/507f1f77bcf86cd799439012
|
||||
```
|
||||
|
||||
#### Пример ответа (когда стратегия завершена - 200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Операция выполнена успешно",
|
||||
"data": {
|
||||
"strategyId": "507f1f77bcf86cd799439012",
|
||||
"analysisId": "507f1f77bcf86cd799439011",
|
||||
"status": "completed",
|
||||
"createdAt": "2025-01-20T15:40:00",
|
||||
"completedAt": "2025-01-20T15:43:00",
|
||||
"durationWeeks": 4,
|
||||
"priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"],
|
||||
"strategy": {
|
||||
"weeklyPlans": [
|
||||
{
|
||||
"weekNumber": 1,
|
||||
"mainThemes": [
|
||||
"Презентация продукта",
|
||||
"Ключевые преимущества",
|
||||
"Решение проблем клиентов"
|
||||
],
|
||||
"contentRecommendations": "Сфокусируйтесь на представлении продукта и его основных преимуществах. Используйте визуальный контент для привлечения внимания. Подчеркните уникальные особенности, которые выделяют ваш продукт на рынке.",
|
||||
"priorityPlatforms": ["Instagram", "LinkedIn"]
|
||||
},
|
||||
{
|
||||
"weekNumber": 2,
|
||||
"mainThemes": [
|
||||
"Кейсы успешных клиентов",
|
||||
"Отзывы и рекомендации",
|
||||
"Демонстрация результатов"
|
||||
],
|
||||
"contentRecommendations": "Публикуйте реальные истории успеха ваших клиентов. Используйте отзывы и рекомендации для повышения доверия. Покажите конкретные результаты и достижения.",
|
||||
"priorityPlatforms": ["Instagram", "Telegram"]
|
||||
},
|
||||
{
|
||||
"weekNumber": 3,
|
||||
"mainThemes": [
|
||||
"Образовательный контент",
|
||||
"Советы и рекомендации",
|
||||
"Индустриальные инсайты"
|
||||
],
|
||||
"contentRecommendations": "Создавайте образовательный контент, который помогает вашей целевой аудитории. Делитесь экспертными знаниями и инсайтами индустрии. Позиционируйте себя как эксперта в области.",
|
||||
"priorityPlatforms": ["LinkedIn", "Telegram"]
|
||||
},
|
||||
{
|
||||
"weekNumber": 4,
|
||||
"mainThemes": [
|
||||
"Призыв к действию",
|
||||
"Специальные предложения",
|
||||
"Завершение кампании"
|
||||
],
|
||||
"contentRecommendations": "Активно призывайте к действию. Предлагайте специальные условия или бонусы. Подводите итоги кампании и демонстрируйте достигнутые результаты.",
|
||||
"priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"]
|
||||
}
|
||||
],
|
||||
"postCalendar": [
|
||||
{
|
||||
"publishDate": "2025-01-21T10:00:00",
|
||||
"platform": "Instagram",
|
||||
"contentType": "пост",
|
||||
"theme": "Презентация продукта",
|
||||
"postText": "🚀 Представляем наш новый продукт! Мы создали решение, которое поможет вашему бизнесу достичь новых высот. Узнайте больше о ключевых преимуществах в нашем профиле. #бизнес #инновации #продукт",
|
||||
"hashtags": [
|
||||
"#бизнес",
|
||||
"#инновации",
|
||||
"#продукт",
|
||||
"#маркетинг",
|
||||
"#развитие"
|
||||
],
|
||||
"publishTime": "10:00"
|
||||
},
|
||||
{
|
||||
"publishDate": "2025-01-21T14:00:00",
|
||||
"platform": "LinkedIn",
|
||||
"contentType": "пост",
|
||||
"theme": "Ключевые преимущества",
|
||||
"postText": "Наш продукт предлагает уникальные преимущества для B2B клиентов: быстрая интеграция, масштабируемость и надежная поддержка. Свяжитесь с нами для консультации. #B2B #технологии #бизнес",
|
||||
"hashtags": [
|
||||
"#B2B",
|
||||
"#технологии",
|
||||
"#бизнес",
|
||||
"#решения",
|
||||
"#консультация"
|
||||
],
|
||||
"publishTime": "14:00"
|
||||
},
|
||||
{
|
||||
"publishDate": "2025-01-22T18:00:00",
|
||||
"platform": "Instagram",
|
||||
"contentType": "сторис",
|
||||
"theme": "Решение проблем клиентов",
|
||||
"postText": "Знаете ли вы, что 80% компаний сталкиваются с проблемой X? Наш продукт решает эту проблему эффективно и быстро. Swipe up для деталей! 👆",
|
||||
"hashtags": ["#решение", "#проблемы", "#эффективность"],
|
||||
"publishTime": "18:00"
|
||||
},
|
||||
{
|
||||
"publishDate": "2025-01-23T10:00:00",
|
||||
"platform": "Telegram",
|
||||
"contentType": "пост",
|
||||
"theme": "Кейс успешного клиента",
|
||||
"postText": "📊 Кейс: Как компания X увеличила эффективность на 150% с помощью нашего продукта. Читайте полную историю в нашем канале. #кейс #успех #результаты",
|
||||
"hashtags": ["#кейс", "#успех", "#результаты", "#бизнес"],
|
||||
"publishTime": "10:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Пример ответа (когда стратегия еще обрабатывается - 200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Операция выполнена успешно",
|
||||
"data": {
|
||||
"strategyId": "507f1f77bcf86cd799439012",
|
||||
"analysisId": "507f1f77bcf86cd799439011",
|
||||
"status": "processing",
|
||||
"createdAt": "2025-01-20T15:40:00",
|
||||
"completedAt": null,
|
||||
"durationWeeks": 4,
|
||||
"priorityPlatforms": ["Instagram", "LinkedIn", "Telegram"],
|
||||
"strategy": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Статусы стратегии
|
||||
|
||||
| Статус | Описание |
|
||||
| ------------ | ------------------------------- |
|
||||
| `queued` | Запрос в очереди на обработку |
|
||||
| `processing` | Стратегия генерируется |
|
||||
| `completed` | Стратегия завершена успешно |
|
||||
| `failed` | Стратегия завершилась с ошибкой |
|
||||
|
||||
#### Структура ответа
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
| ---------------------------------------------------- | ------- | ------------------------------------------------------ |
|
||||
| `success` | boolean | Флаг успешности операции |
|
||||
| `message` | string | Сообщение о результате операции |
|
||||
| `data.strategyId` | string | Уникальный идентификатор стратегии |
|
||||
| `data.analysisId` | string | ID маркетингового анализа |
|
||||
| `data.status` | string | Статус стратегии |
|
||||
| `data.createdAt` | string | ISO 8601 дата/время создания |
|
||||
| `data.completedAt` | string | ISO 8601 дата/время завершения (null если не завершен) |
|
||||
| `data.durationWeeks` | integer | Длительность стратегии в неделях |
|
||||
| `data.priorityPlatforms` | array | Список приоритетных платформ |
|
||||
| `data.strategy` | object | Объект со стратегией (null если не завершен) |
|
||||
| `data.strategy.weeklyPlans` | array | Список недельных планов |
|
||||
| `data.strategy.weeklyPlans[].weekNumber` | integer | Номер недели (1, 2, 3, ...) |
|
||||
| `data.strategy.weeklyPlans[].mainThemes` | array | Основные темы недели (массив строк) |
|
||||
| `data.strategy.weeklyPlans[].contentRecommendations` | string | Рекомендации по контенту для недели |
|
||||
| `data.strategy.weeklyPlans[].priorityPlatforms` | array | Приоритетные платформы для недели |
|
||||
| `data.strategy.postCalendar` | array | Календарь постов |
|
||||
| `data.strategy.postCalendar[].publishDate` | string | ISO 8601 дата/время публикации |
|
||||
| `data.strategy.postCalendar[].platform` | string | Платформа для публикации |
|
||||
| `data.strategy.postCalendar[].contentType` | string | Тип контента (пост, сторис, видео, баннер) |
|
||||
| `data.strategy.postCalendar[].theme` | string | Тема поста |
|
||||
| `data.strategy.postCalendar[].postText` | string | Полный текст поста (готовый к публикации) |
|
||||
| `data.strategy.postCalendar[].hashtags` | array | Список хештегов (массив строк) |
|
||||
| `data.strategy.postCalendar[].publishTime` | string | Время публикации в формате HH:mm |
|
||||
|
||||
#### Пример ошибки (404 Not Found)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Стратегия не найдена",
|
||||
"error": {
|
||||
"code": "NOT_FOUND",
|
||||
"message": "Стратегия с указанным ID не найдена"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Получение стратегии по ID анализа
|
||||
|
||||
**GET** `/api/marketing/analysis/{analysisId}/strategy`
|
||||
|
||||
Возвращает стратегию, связанную с указанным маркетинговым анализом.
|
||||
|
||||
#### Параметры пути
|
||||
|
||||
| Параметр | Тип | Описание |
|
||||
| ------------ | ------ | --------------------- |
|
||||
| `analysisId` | string | Идентификатор анализа |
|
||||
|
||||
#### Пример запроса
|
||||
|
||||
```
|
||||
GET /api/marketing/analysis/507f1f77bcf86cd799439011/strategy
|
||||
```
|
||||
|
||||
#### Пример ответа
|
||||
|
||||
Структура ответа идентична эндпоинту `GET /api/marketing/strategy/{strategyId}` (см. выше).
|
||||
|
||||
#### Пример ошибки (404 Not Found)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Стратегия не найдена",
|
||||
"error": {
|
||||
"code": "NOT_FOUND",
|
||||
"message": "Стратегия для указанного анализа не найдена"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
### Коды ошибок
|
||||
|
||||
| Код | HTTP статус | Описание |
|
||||
| ------------------------ | ----------- | ------------------------------- |
|
||||
| `VALIDATION_ERROR` | 400 | Ошибка валидации входных данных |
|
||||
| `INVALID_ANALYSIS` | 404 | Анализ не найден |
|
||||
| `ANALYSIS_NOT_COMPLETED` | 400 | Анализ еще не завершен |
|
||||
| `NOT_FOUND` | 404 | Стратегия не найдена |
|
||||
| `INTERNAL_SERVER_ERROR` | 500 | Внутренняя ошибка сервера |
|
||||
|
||||
### Формат ошибки
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Описание ошибки",
|
||||
"error": {
|
||||
"code": "ERROR_CODE",
|
||||
"message": "Детальное сообщение об ошибке",
|
||||
"details": {
|
||||
"field1": "Сообщение об ошибке для поля 1",
|
||||
"field2": "Сообщение об ошибке для поля 2"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Примечание**: Поле `details` присутствует только для ошибок валидации (`VALIDATION_ERROR`).
|
||||
|
||||
---
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### JavaScript/TypeScript (Fetch API)
|
||||
|
||||
#### Запуск генерации стратегии
|
||||
|
||||
```javascript
|
||||
async function generateStrategy(analysisId, options = {}) {
|
||||
const params = new URLSearchParams();
|
||||
params.append('analysisId', analysisId);
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.konturai.kz/api/marketing/strategy/generate?${params}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
durationWeeks: options.durationWeeks || 4,
|
||||
priorityPlatforms: options.priorityPlatforms || [],
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
console.log('Strategy ID:', result.data.strategyId);
|
||||
return result.data.strategyId;
|
||||
} else {
|
||||
console.error('Error:', result.error);
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Проверка статуса и получение результата
|
||||
|
||||
```javascript
|
||||
async function getStrategyResult(strategyId) {
|
||||
const response = await fetch(
|
||||
`https://api.konturai.kz/api/marketing/strategy/${strategyId}`
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const { status, strategy } = result.data;
|
||||
|
||||
if (status === 'completed' && strategy) {
|
||||
console.log('Strategy completed!');
|
||||
console.log('Weekly plans:', strategy.weeklyPlans);
|
||||
console.log('Post calendar:', strategy.postCalendar);
|
||||
return strategy;
|
||||
} else if (status === 'processing') {
|
||||
console.log('Strategy is still generating...');
|
||||
return null; // Повторить запрос позже
|
||||
} else if (status === 'failed') {
|
||||
throw new Error('Strategy generation failed');
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Получение стратегии по ID анализа
|
||||
|
||||
```javascript
|
||||
async function getStrategyByAnalysis(analysisId) {
|
||||
const response = await fetch(
|
||||
`https://api.konturai.kz/api/marketing/analysis/${analysisId}/strategy`
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
} else {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Полный цикл с polling
|
||||
|
||||
```javascript
|
||||
async function waitForStrategyCompletion(
|
||||
strategyId,
|
||||
maxAttempts = 60,
|
||||
intervalMs = 10000
|
||||
) {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const result = await getStrategyResult(strategyId);
|
||||
|
||||
if (result) {
|
||||
return result; // Стратегия завершена
|
||||
}
|
||||
|
||||
// Ждем перед следующей проверкой
|
||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||
}
|
||||
|
||||
throw new Error('Strategy generation timeout');
|
||||
}
|
||||
|
||||
// Использование
|
||||
async function runFullStrategyGeneration() {
|
||||
try {
|
||||
// 1. Получаем завершенный анализ (предполагается, что analysisId уже есть)
|
||||
const analysisId = '507f1f77bcf86cd799439011';
|
||||
|
||||
// 2. Запускаем генерацию стратегии
|
||||
const strategyId = await generateStrategy(analysisId, {
|
||||
durationWeeks: 4,
|
||||
priorityPlatforms: ['Instagram', 'LinkedIn', 'Telegram'],
|
||||
});
|
||||
|
||||
console.log(`Strategy generation started: ${strategyId}`);
|
||||
|
||||
// 3. Ждем завершения (проверяем каждые 10 секунд, максимум 10 минут)
|
||||
const strategy = await waitForStrategyCompletion(strategyId, 60, 10000);
|
||||
|
||||
// 4. Используем результаты
|
||||
console.log('Weekly plans:', strategy.weeklyPlans);
|
||||
console.log('Post calendar:', strategy.postCalendar);
|
||||
|
||||
// Отображаем календарь постов
|
||||
strategy.postCalendar.forEach((post) => {
|
||||
console.log(`${post.publishDate} - ${post.platform}: ${post.theme}`);
|
||||
});
|
||||
|
||||
return strategy;
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### React примеры
|
||||
|
||||
#### Компонент для отображения стратегии
|
||||
|
||||
```javascript
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
function StrategyView({ analysisId }) {
|
||||
const [strategy, setStrategy] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadStrategy() {
|
||||
try {
|
||||
// Сначала пытаемся получить существующую стратегию
|
||||
let response = await fetch(
|
||||
`/api/marketing/analysis/${analysisId}/strategy`
|
||||
);
|
||||
let result = await response.json();
|
||||
|
||||
if (result.success && result.data.status === 'completed') {
|
||||
setStrategy(result.data);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Если стратегии нет или она еще обрабатывается, запускаем генерацию
|
||||
if (!result.success || result.data.status === 'processing') {
|
||||
// Запускаем генерацию
|
||||
response = await fetch(
|
||||
`/api/marketing/strategy/generate?analysisId=${analysisId}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
durationWeeks: 4,
|
||||
priorityPlatforms: ['Instagram', 'LinkedIn', 'Telegram'],
|
||||
}),
|
||||
}
|
||||
);
|
||||
result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Polling для получения результата
|
||||
pollStrategy(result.data.strategyId);
|
||||
} else {
|
||||
setError(result.error.message);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function pollStrategy(strategyId) {
|
||||
const maxAttempts = 60;
|
||||
let attempts = 0;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
attempts++;
|
||||
try {
|
||||
const response = await fetch(`/api/marketing/strategy/${strategyId}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
if (result.data.status === 'completed') {
|
||||
setStrategy(result.data);
|
||||
setLoading(false);
|
||||
clearInterval(interval);
|
||||
} else if (result.data.status === 'failed') {
|
||||
setError('Strategy generation failed');
|
||||
setLoading(false);
|
||||
clearInterval(interval);
|
||||
}
|
||||
}
|
||||
|
||||
if (attempts >= maxAttempts) {
|
||||
setError('Strategy generation timeout');
|
||||
setLoading(false);
|
||||
clearInterval(interval);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
setLoading(false);
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 10000); // Проверяем каждые 10 секунд
|
||||
}
|
||||
|
||||
if (analysisId) {
|
||||
loadStrategy();
|
||||
}
|
||||
}, [analysisId]);
|
||||
|
||||
if (loading) {
|
||||
return <div>Генерация стратегии...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div>Ошибка: {error}</div>;
|
||||
}
|
||||
|
||||
if (!strategy || !strategy.strategy) {
|
||||
return <div>Стратегия не найдена</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='strategy-view'>
|
||||
<h2>Стратегия продвижения</h2>
|
||||
|
||||
{/* Недельный план */}
|
||||
<section className='weekly-plans'>
|
||||
<h3>Недельный план</h3>
|
||||
{strategy.strategy.weeklyPlans.map((plan) => (
|
||||
<div key={plan.weekNumber} className='week-plan'>
|
||||
<h4>Неделя {plan.weekNumber}</h4>
|
||||
<div className='themes'>
|
||||
<strong>Темы:</strong>
|
||||
<ul>
|
||||
{plan.mainThemes.map((theme, idx) => (
|
||||
<li key={idx}>{theme}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className='recommendations'>
|
||||
<strong>Рекомендации:</strong>
|
||||
<p>{plan.contentRecommendations}</p>
|
||||
</div>
|
||||
<div className='platforms'>
|
||||
<strong>Платформы:</strong>
|
||||
{plan.priorityPlatforms.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{/* Календарь постов */}
|
||||
<section className='post-calendar'>
|
||||
<h3>Календарь постов</h3>
|
||||
<div className='calendar-grid'>
|
||||
{strategy.strategy.postCalendar.map((post, idx) => (
|
||||
<div key={idx} className='post-item'>
|
||||
<div className='post-header'>
|
||||
<span className='date'>
|
||||
{new Date(post.publishDate).toLocaleDateString('ru-RU')}
|
||||
</span>
|
||||
<span className='time'>{post.publishTime}</span>
|
||||
<span className='platform'>{post.platform}</span>
|
||||
<span className='content-type'>{post.contentType}</span>
|
||||
</div>
|
||||
<div className='post-theme'>
|
||||
<strong>Тема:</strong> {post.theme}
|
||||
</div>
|
||||
<div className='post-text'>{post.postText}</div>
|
||||
<div className='post-hashtags'>
|
||||
{post.hashtags.map((tag, tagIdx) => (
|
||||
<span key={tagIdx} className='hashtag'>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StrategyView;
|
||||
```
|
||||
|
||||
#### Компонент для отображения календаря постов
|
||||
|
||||
```javascript
|
||||
import React from 'react';
|
||||
|
||||
function PostCalendar({ postCalendar }) {
|
||||
// Группируем посты по датам
|
||||
const postsByDate = postCalendar.reduce((acc, post) => {
|
||||
const date = new Date(post.publishDate).toLocaleDateString('ru-RU');
|
||||
if (!acc[date]) {
|
||||
acc[date] = [];
|
||||
}
|
||||
acc[date].push(post);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<div className='post-calendar'>
|
||||
<h3>Календарь публикаций</h3>
|
||||
{Object.entries(postsByDate).map(([date, posts]) => (
|
||||
<div key={date} className='date-group'>
|
||||
<h4>{date}</h4>
|
||||
{posts.map((post, idx) => (
|
||||
<div key={idx} className='post-card'>
|
||||
<div className='post-meta'>
|
||||
<span className='platform-badge'>{post.platform}</span>
|
||||
<span className='content-type-badge'>{post.contentType}</span>
|
||||
<span className='time'>{post.publishTime}</span>
|
||||
</div>
|
||||
<div className='post-content'>
|
||||
<h5>{post.theme}</h5>
|
||||
<p>{post.postText}</p>
|
||||
<div className='hashtags'>
|
||||
{post.hashtags.map((tag, tagIdx) => (
|
||||
<span key={tagIdx} className='hashtag'>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PostCalendar;
|
||||
```
|
||||
|
||||
### Vue.js примеры
|
||||
|
||||
```javascript
|
||||
// composable для работы со стратегией
|
||||
export function useMarketingStrategy() {
|
||||
const baseUrl = '';
|
||||
|
||||
const generateStrategy = async (analysisId, options = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
params.append('analysisId', analysisId);
|
||||
|
||||
const response = await fetch(
|
||||
`${baseUrl}/api/marketing/strategy/generate?${params}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
durationWeeks: options.durationWeeks || 4,
|
||||
priorityPlatforms: options.priorityPlatforms || [],
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const getStrategy = async (strategyId) => {
|
||||
const response = await fetch(
|
||||
`${baseUrl}/api/marketing/strategy/${strategyId}`
|
||||
);
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const getStrategyByAnalysis = async (analysisId) => {
|
||||
const response = await fetch(
|
||||
`${baseUrl}/api/marketing/analysis/${analysisId}/strategy`
|
||||
);
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
return result.data;
|
||||
};
|
||||
|
||||
return {
|
||||
generateStrategy,
|
||||
getStrategy,
|
||||
getStrategyByAnalysis,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Рекомендации по интеграции
|
||||
|
||||
### 1. Polling стратегия
|
||||
|
||||
Рекомендуется проверять статус стратегии каждые 10-15 секунд. Максимальное время ожидания - 5-7 минут.
|
||||
|
||||
### 2. Обработка ошибок
|
||||
|
||||
Всегда проверяйте поле `success` в ответе и обрабатывайте ошибки соответствующим образом. Особое внимание уделите случаям, когда анализ еще не завершен.
|
||||
|
||||
### 3. Валидация на клиенте
|
||||
|
||||
Перед отправкой запроса рекомендуется валидировать данные на клиенте:
|
||||
|
||||
- Проверка наличия `analysisId`
|
||||
- Проверка диапазона `durationWeeks` (1-12)
|
||||
- Проверка формата массива `priorityPlatforms`
|
||||
|
||||
### 4. UX рекомендации
|
||||
|
||||
- Показывайте индикатор загрузки во время генерации стратегии
|
||||
- Отображайте примерное время завершения (3-5 минут)
|
||||
- Предоставьте возможность отменить ожидание и проверить результат позже
|
||||
- Сохраняйте `strategyId` для последующей проверки статуса
|
||||
- Отображайте календарь постов в удобном формате (календарь, список, таблица)
|
||||
- Позвольте пользователю копировать текст постов и хештеги
|
||||
|
||||
### 5. Кэширование
|
||||
|
||||
После получения результатов можно кэшировать их локально, используя `strategyId` или `analysisId` как ключ.
|
||||
|
||||
### 6. Экспорт данных
|
||||
|
||||
Рассмотрите возможность экспорта стратегии в различных форматах:
|
||||
|
||||
- CSV для календаря постов
|
||||
- PDF для полной стратегии
|
||||
- iCal для импорта в календарные приложения
|
||||
|
||||
---
|
||||
|
||||
## Примечания
|
||||
|
||||
1. **Формат даты**: Все даты возвращаются в формате ISO 8601 без timezone (LocalDateTime)
|
||||
2. **Идентификаторы**: Используются MongoDB ObjectId (24 символа hex)
|
||||
3. **Асинхронность**: Генерация стратегии выполняется асинхронно, не блокируя запрос
|
||||
4. **Таймауты**: Рекомендуется устанавливать таймаут на запросы (минимум 30 секунд для запуска генерации)
|
||||
5. **Зависимость от анализа**: Стратегия может быть сгенерирована только для завершенного анализа
|
||||
6. **Повторная генерация**: Если стратегия уже существует для анализа, возвращается существующая стратегия
|
||||
7. **Платформы**: Поддерживаются все популярные платформы: Instagram, Facebook, LinkedIn, Telegram, TikTok, YouTube, 21MC и другие
|
||||
|
||||
---
|
||||
|
||||
## Поддержка
|
||||
|
||||
При возникновении проблем с API обращайтесь в техническую поддержку с указанием:
|
||||
|
||||
- `strategyId` (если есть)
|
||||
- `analysisId`
|
||||
- Время запроса
|
||||
- Описание проблемы
|
||||
- Код ошибки (если есть)
|
||||
Generated
+13
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@primeuix/themes": "^1.0.0",
|
||||
"chart.js": "3.3.2",
|
||||
"marked": "^17.0.1",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.3.1",
|
||||
"tailwindcss-primeui": "^0.5.0",
|
||||
@@ -2361,6 +2362,18 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "17.0.1",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz",
|
||||
"integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@primeuix/themes": "^1.0.0",
|
||||
"chart.js": "3.3.2",
|
||||
"marked": "^17.0.1",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.3.1",
|
||||
"tailwindcss-primeui": "^0.5.0",
|
||||
|
||||
+11
-1
@@ -48,7 +48,17 @@ export const API_CONFIG = {
|
||||
PUBLISHING_POST: '/publishing/post',
|
||||
|
||||
// AnalyticsController - Аналитика
|
||||
ANALYTICS_COLLECT: '/analytics/collect-now'
|
||||
ANALYTICS_COLLECT: '/analytics/collect-now',
|
||||
|
||||
// MarketingAnalysisController - Маркетинговый анализ
|
||||
MARKETING_ANALYSIS_START: '/api/marketing/analysis/start',
|
||||
MARKETING_ANALYSIS_GET: '/api/marketing/analysis',
|
||||
MARKETING_ANALYSIS_DOWNLOAD: '/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'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -223,6 +223,26 @@ const router = createRouter({
|
||||
name: 'landing',
|
||||
component: () => import('@/views/pages/Landing.vue')
|
||||
},
|
||||
{
|
||||
path: '/marketing-analysis',
|
||||
name: 'marketing-main',
|
||||
component: () => import('@/views/pages/marketing/MarketingMain.vue')
|
||||
},
|
||||
{
|
||||
path: '/marketing-analysis/analysis',
|
||||
name: 'marketing-analysis',
|
||||
component: () => import('@/views/pages/marketing/MarketingAnalysis.vue')
|
||||
},
|
||||
{
|
||||
path: '/marketing-analysis/promotion',
|
||||
name: 'marketing-promotion',
|
||||
component: () => import('@/views/pages/marketing/MarketingPromotion.vue')
|
||||
},
|
||||
{
|
||||
path: '/marketing-analysis/results',
|
||||
name: 'marketing-results',
|
||||
component: () => import('@/views/pages/marketing/MarketingResults.vue')
|
||||
},
|
||||
{
|
||||
path: '/pages/notfound',
|
||||
name: 'notfound',
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
import { API_CONFIG, DEFAULT_REQUEST_CONFIG } from '@/config/api';
|
||||
import AuthService from './AuthService';
|
||||
|
||||
const API_BASE_URL = API_CONFIG.BASE_URL;
|
||||
|
||||
class MarketingService {
|
||||
/**
|
||||
* Запуск маркетингового анализа
|
||||
* POST /api/marketing/analysis/start
|
||||
* @param {Object} data - Данные для анализа
|
||||
* @param {string} data.product - Название продукта или услуги
|
||||
* @param {string} data.location - Географическая локация работы
|
||||
* @param {string} data.client - Тип целевой аудитории
|
||||
* @param {string} data.differentiator - Уникальные особенности бизнеса
|
||||
* @returns {Promise<Object>} Объект с analysisId и статусом
|
||||
*/
|
||||
async startAnalysis(data) {
|
||||
try {
|
||||
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_ANALYSIS_START}`, {
|
||||
method: 'POST',
|
||||
...DEFAULT_REQUEST_CONFIG,
|
||||
body: JSON.stringify({
|
||||
product: data.product,
|
||||
location: data.location,
|
||||
client: data.client,
|
||||
differentiator: data.differentiator
|
||||
})
|
||||
});
|
||||
|
||||
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}
|
||||
* @param {string} analysisId - Идентификатор анализа
|
||||
* @returns {Promise<Object>} Объект с статусом и результатами анализа
|
||||
*/
|
||||
async getAnalysisResult(analysisId) {
|
||||
try {
|
||||
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_ANALYSIS_GET}/${analysisId}`);
|
||||
|
||||
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
|
||||
* @param {string} analysisId - Идентификатор анализа
|
||||
* @returns {Promise<Blob>} Blob с PDF файлом
|
||||
*/
|
||||
async downloadPdf(analysisId) {
|
||||
try {
|
||||
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_ANALYSIS_DOWNLOAD}/${analysisId}/download`);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || errorData.error?.message || 'Ошибка при скачивании PDF');
|
||||
}
|
||||
|
||||
return await response.blob();
|
||||
} catch (error) {
|
||||
console.error('Ошибка при скачивании PDF:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Вспомогательный метод для скачивания PDF в браузере
|
||||
* @param {string} analysisId - Идентификатор анализа
|
||||
*/
|
||||
async downloadPdfFile(analysisId) {
|
||||
try {
|
||||
const blob = await this.downloadPdf(analysisId);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `marketing_analysis_${analysisId}.pdf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при скачивании PDF файла:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск генерации стратегии продвижения
|
||||
* POST /api/marketing/strategy/generate?analysisId={analysisId}
|
||||
* @param {string} analysisId - ID завершенного маркетингового анализа
|
||||
* @param {Object} options - Опции генерации стратегии
|
||||
* @param {number} options.durationWeeks - Длительность стратегии в неделях (1-12)
|
||||
* @param {string[]} options.priorityPlatforms - Приоритетные платформы для продвижения
|
||||
* @returns {Promise<Object>} Объект с strategyId и статусом
|
||||
*/
|
||||
async generateStrategy(analysisId, options = {}) {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('analysisId', analysisId);
|
||||
|
||||
const body = {};
|
||||
if (options.durationWeeks) {
|
||||
body.durationWeeks = options.durationWeeks;
|
||||
}
|
||||
if (options.priorityPlatforms && options.priorityPlatforms.length > 0) {
|
||||
body.priorityPlatforms = options.priorityPlatforms;
|
||||
}
|
||||
|
||||
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_STRATEGY_GENERATE}?${params}`, {
|
||||
method: 'POST',
|
||||
...DEFAULT_REQUEST_CONFIG,
|
||||
body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение стратегии по ID
|
||||
* GET /api/marketing/strategy/{strategyId}
|
||||
* @param {string} strategyId - Идентификатор стратегии
|
||||
* @returns {Promise<Object>} Объект с статусом и результатами стратегии
|
||||
*/
|
||||
async getStrategy(strategyId) {
|
||||
try {
|
||||
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_STRATEGY_GET}/${strategyId}`);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение стратегии по ID анализа
|
||||
* GET /api/marketing/analysis/{analysisId}/strategy
|
||||
* @param {string} analysisId - Идентификатор анализа
|
||||
* @returns {Promise<Object>} Объект с статусом и результатами стратегии
|
||||
*/
|
||||
async getStrategyByAnalysis(analysisId) {
|
||||
try {
|
||||
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_STRATEGY_BY_ANALYSIS}/${analysisId}/strategy`);
|
||||
|
||||
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,598 @@
|
||||
<template>
|
||||
<div class="marketing-analysis-page bg-surface-50 dark:bg-surface-900 min-h-screen p-6">
|
||||
<Toast />
|
||||
<div class="max-w-[1200px] mx-auto">
|
||||
<!-- Форма запуска анализа -->
|
||||
<div v-if="!analysisId" class="card">
|
||||
<div class="card-header mb-4">
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Запуск анализа</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<form @submit.prevent="handleSubmit" class="space-y-4 max-w-2xl">
|
||||
<div class="field">
|
||||
<label for="product" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Что вы продаете? </label>
|
||||
<InputText id="product" v-model="formData.product" placeholder="Введите название продукта" class="w-full" :class="{ 'p-invalid': errors.product }" />
|
||||
<small v-if="errors.product" class="p-error">{{ errors.product }}</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="location" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Где вы работаете? </label>
|
||||
<InputText id="location" v-model="formData.location" placeholder="Введите локацию" class="w-full" :class="{ 'p-invalid': errors.location }" />
|
||||
<small v-if="errors.location" class="p-error">{{ errors.location }}</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="client" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Кто ваш клиент? </label>
|
||||
<Dropdown id="client" v-model="formData.client" :options="clientOptions" placeholder="Выберите тип клиента" class="w-full" :class="{ 'p-invalid': errors.client }" />
|
||||
<small v-if="errors.client" class="p-error">{{ errors.client }}</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="differentiator" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Чем ваш бизнес выделяется? </label>
|
||||
<InputText id="differentiator" v-model="formData.differentiator" placeholder="Опишите уникальность" class="w-full" :class="{ 'p-invalid': errors.differentiator }" />
|
||||
<small v-if="errors.differentiator" class="p-error">{{ errors.differentiator }}</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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Статус обработки -->
|
||||
<div v-else-if="status === 'processing' || status === 'queued'" class="card">
|
||||
<div class="card-header mb-4">
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Анализ выполняется</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="text-center py-6">
|
||||
<ProgressSpinner size="50" />
|
||||
<p class="mt-4 text-lg text-surface-700 dark:text-surface-300">Анализ обрабатывается...</p>
|
||||
<p class="mt-2 text-sm text-surface-500 dark:text-surface-400">Это может занять 5-10 минут. Пожалуйста, подождите.</p>
|
||||
<p v-if="estimatedCompletionTime" class="mt-2 text-sm text-surface-500 dark:text-surface-400">Ожидаемое время завершения: {{ formatDateTime(estimatedCompletionTime) }}</p>
|
||||
<Button label="Отменить" icon="pi pi-times" severity="secondary" class="mt-4" @click="resetAnalysis" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ошибка -->
|
||||
<div v-else-if="status === 'failed'" class="card">
|
||||
<div class="card-header mb-4">
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Ошибка анализа</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="text-center py-6">
|
||||
<i class="pi pi-exclamation-triangle text-6xl text-red-500 mb-3"></i>
|
||||
<p class="mt-4 text-lg text-surface-700 dark:text-surface-300">Анализ завершился с ошибкой</p>
|
||||
<p class="mt-2 text-sm text-surface-500 dark:text-surface-400">Попробуйте запустить анализ снова или обратитесь в поддержку.</p>
|
||||
<Button label="Попробовать снова" icon="pi pi-refresh" class="mt-4" @click="resetAnalysis" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Результаты анализа -->
|
||||
<div v-else-if="status === 'completed' && report" class="space-y-4">
|
||||
<!-- Заголовок с кнопкой скачивания -->
|
||||
<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="Скачать PDF" icon="pi pi-download" @click="handleDownloadPdf" :loading="downloadingPdf" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Краткое резюме -->
|
||||
<div class="card">
|
||||
<div class="card-header mb-4">
|
||||
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0">Краткое резюме</h3>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p class="text-surface-700 dark:text-surface-300 whitespace-pre-line">{{ report.summary }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Целевая аудитория -->
|
||||
<div class="card">
|
||||
<div class="card-header mb-4">
|
||||
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0">Целевая аудитория</h3>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p class="text-surface-700 dark:text-surface-300 mb-4 whitespace-pre-line">{{ report.targetAudience?.description }}</p>
|
||||
<div v-if="report.targetAudience?.channels && report.targetAudience.channels.length > 0">
|
||||
<p class="text-sm font-medium mb-2 text-surface-700 dark:text-surface-300">Рекомендуемые каналы:</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Tag v-for="channel in report.targetAudience.channels" :key="channel" :value="channel" severity="info" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Рекомендации -->
|
||||
<div v-if="report.recommendations && report.recommendations.length > 0" class="card">
|
||||
<div class="card-header mb-4">
|
||||
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0">Рекомендации</h3>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<ul class="list-none p-0 m-0">
|
||||
<li v-for="(recommendation, index) in report.recommendations" :key="index" class="flex align-items-start mb-3">
|
||||
<i class="pi pi-check-circle text-green-500 mr-2 mt-1"></i>
|
||||
<span class="text-surface-700 dark:text-surface-300">{{ recommendation }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Стратегия -->
|
||||
<div v-if="report.strategy" class="card">
|
||||
<div class="card-header mb-4">
|
||||
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0">Маркетинговая стратегия</h3>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="grid">
|
||||
<div v-if="report.strategy.duration" class="col-12 md:col-4 mb-3">
|
||||
<div class="p-3 border-round bg-blue-50 dark:bg-blue-900">
|
||||
<p class="text-sm font-medium text-surface-600 dark:text-surface-400 mb-1">Длительность</p>
|
||||
<p class="text-lg font-semibold text-surface-900 dark:text-surface-0">{{ report.strategy.duration }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="report.strategy.channels && (Array.isArray(report.strategy.channels) ? report.strategy.channels.length > 0 : report.strategy.channels.trim())" class="col-12 mb-3">
|
||||
<div class="p-4 border-round bg-surface-0 dark:bg-surface-800 border-1 border-surface-200 dark:border-surface-700">
|
||||
<p class="text-sm font-medium text-surface-600 dark:text-surface-400 mb-3">Каналы</p>
|
||||
<div class="markdown-content" v-html="renderMarkdown(Array.isArray(report.strategy.channels) ? report.strategy.channels.join('\n') : report.strategy.channels)"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="report.strategy.contentTypes && report.strategy.contentTypes.length > 0" class="col-12 md:col-4 mb-3">
|
||||
<div class="p-3 border-round bg-purple-50 dark:bg-purple-900">
|
||||
<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="type in report.strategy.contentTypes" :key="type" :value="type" severity="secondary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки действий -->
|
||||
<div class="card">
|
||||
<div class="card-content text-center">
|
||||
<div class="flex gap-3 justify-content-center flex-wrap">
|
||||
<Button label="Создать стратегию на основе анализа" icon="pi pi-magic" severity="success" @click="goToStrategy" />
|
||||
<Button label="Создать новый анализ" icon="pi pi-plus" severity="secondary" @click="resetAnalysis" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MarketingService from '@/service/MarketingService';
|
||||
import { marked } from 'marked';
|
||||
import Button from 'primevue/button';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
import InputText from 'primevue/inputtext';
|
||||
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';
|
||||
|
||||
const toast = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
// Form data
|
||||
const formData = ref({
|
||||
product: '',
|
||||
location: '',
|
||||
client: null,
|
||||
differentiator: ''
|
||||
});
|
||||
|
||||
const clientOptions = ['B2B клиенты', 'B2C клиенты', 'Частные лица', 'Корпорации', 'Малый бизнес'];
|
||||
|
||||
const submitting = ref(false);
|
||||
const errors = ref({});
|
||||
|
||||
// Analysis state
|
||||
const analysisId = ref(null);
|
||||
const status = ref(null);
|
||||
const report = ref(null);
|
||||
const estimatedCompletionTime = ref(null);
|
||||
const pollingInterval = ref(null);
|
||||
const pollingAttempts = ref(0);
|
||||
const maxPollingAttempts = 60; // 10 minutes with 10 second intervals
|
||||
const pollingIntervalMs = 10000; // 10 seconds
|
||||
const downloadingPdf = ref(false);
|
||||
|
||||
// Validation
|
||||
const validateForm = () => {
|
||||
errors.value = {};
|
||||
|
||||
// Product validation
|
||||
if (!formData.value.product || formData.value.product.trim().length < 3) {
|
||||
errors.value.product = 'Название продукта должно содержать минимум 3 символа';
|
||||
} else if (formData.value.product.trim().length > 200) {
|
||||
errors.value.product = 'Название продукта не должно превышать 200 символов';
|
||||
}
|
||||
|
||||
// Location validation
|
||||
if (!formData.value.location || formData.value.location.trim().length < 2) {
|
||||
errors.value.location = 'Локация должна содержать минимум 2 символа';
|
||||
} else if (formData.value.location.trim().length > 150) {
|
||||
errors.value.location = 'Локация не должна превышать 150 символов';
|
||||
}
|
||||
|
||||
// Client validation
|
||||
if (!formData.value.client) {
|
||||
errors.value.client = 'Пожалуйста, выберите тип клиента';
|
||||
} else if (!clientOptions.includes(formData.value.client)) {
|
||||
errors.value.client = 'Выбран недопустимый тип клиента';
|
||||
}
|
||||
|
||||
// Differentiator validation
|
||||
if (!formData.value.differentiator || formData.value.differentiator.trim().length < 10) {
|
||||
errors.value.differentiator = 'Описание уникальности должно содержать минимум 10 символов';
|
||||
} else if (formData.value.differentiator.trim().length > 500) {
|
||||
errors.value.differentiator = 'Описание уникальности не должно превышать 500 символов';
|
||||
}
|
||||
|
||||
return Object.keys(errors.value).length === 0;
|
||||
};
|
||||
|
||||
// Start polling for analysis status
|
||||
const startPolling = (id) => {
|
||||
pollingAttempts.value = 0;
|
||||
pollingInterval.value = setInterval(async () => {
|
||||
pollingAttempts.value++;
|
||||
|
||||
if (pollingAttempts.value > maxPollingAttempts) {
|
||||
stopPolling();
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Таймаут',
|
||||
detail: 'Превышено время ожидания анализа. Попробуйте проверить результат позже.',
|
||||
life: 5000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await MarketingService.getAnalysisResult(id);
|
||||
status.value = result.status;
|
||||
|
||||
if (result.status === 'completed' && result.report) {
|
||||
stopPolling();
|
||||
report.value = result.report;
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Анализ завершен',
|
||||
detail: 'Маркетинговый анализ успешно завершен',
|
||||
life: 3000
|
||||
});
|
||||
} else if (result.status === 'failed') {
|
||||
stopPolling();
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка анализа',
|
||||
detail: 'Анализ завершился с ошибкой',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при проверке статуса анализа:', error);
|
||||
// Continue polling on error, but log it
|
||||
}
|
||||
}, pollingIntervalMs);
|
||||
};
|
||||
|
||||
// Stop polling
|
||||
const stopPolling = () => {
|
||||
if (pollingInterval.value) {
|
||||
clearInterval(pollingInterval.value);
|
||||
pollingInterval.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle form submit
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка валидации',
|
||||
detail: 'Пожалуйста, исправьте ошибки в форме',
|
||||
life: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
try {
|
||||
const result = await MarketingService.startAnalysis({
|
||||
product: formData.value.product.trim(),
|
||||
location: formData.value.location.trim(),
|
||||
client: formData.value.client,
|
||||
differentiator: formData.value.differentiator.trim()
|
||||
});
|
||||
|
||||
analysisId.value = result.analysisId;
|
||||
status.value = result.status;
|
||||
estimatedCompletionTime.value = result.estimatedCompletionTime;
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Анализ запущен',
|
||||
detail: result.message || 'Анализ успешно запущен. Результаты будут готовы в течение 5-10 минут.',
|
||||
life: 5000
|
||||
});
|
||||
|
||||
// Start polling
|
||||
startPolling(result.analysisId);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при запуске анализа:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: error.message || 'Не удалось запустить анализ. Пожалуйста, попробуйте снова.',
|
||||
life: 5000
|
||||
});
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle PDF download
|
||||
const handleDownloadPdf = async () => {
|
||||
if (!analysisId.value) return;
|
||||
|
||||
downloadingPdf.value = true;
|
||||
|
||||
try {
|
||||
await MarketingService.downloadPdfFile(analysisId.value);
|
||||
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
|
||||
});
|
||||
} finally {
|
||||
downloadingPdf.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Reset analysis and form
|
||||
const resetAnalysis = () => {
|
||||
stopPolling();
|
||||
analysisId.value = null;
|
||||
status.value = null;
|
||||
report.value = null;
|
||||
estimatedCompletionTime.value = null;
|
||||
pollingAttempts.value = 0;
|
||||
formData.value = {
|
||||
product: '',
|
||||
location: '',
|
||||
client: null,
|
||||
differentiator: ''
|
||||
};
|
||||
errors.value = {};
|
||||
};
|
||||
|
||||
// Configure marked options
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true
|
||||
});
|
||||
|
||||
// Render markdown to HTML
|
||||
const renderMarkdown = (markdown) => {
|
||||
if (!markdown) return '';
|
||||
try {
|
||||
return marked.parse(markdown);
|
||||
} catch (error) {
|
||||
console.error('Error rendering markdown:', error);
|
||||
return markdown;
|
||||
}
|
||||
};
|
||||
|
||||
// Format date time
|
||||
const formatDateTime = (dateString) => {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
date.setHours(date.getHours() + 5);
|
||||
return date.toLocaleString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
// Navigate to strategy page
|
||||
const goToStrategy = () => {
|
||||
if (analysisId.value) {
|
||||
router.push({
|
||||
name: 'marketing-promotion',
|
||||
query: { analysisId: analysisId.value }
|
||||
});
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Предупреждение',
|
||||
detail: 'ID анализа не найден',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup on unmount
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.marketing-analysis-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;
|
||||
}
|
||||
|
||||
.markdown-content {
|
||||
line-height: 1.7;
|
||||
color: var(--text-color);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.markdown-content :deep(h1) {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-color);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.markdown-content :deep(h1:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.markdown-content :deep(h2) {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text-color);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.markdown-content :deep(h3) {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text-color);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.markdown-content :deep(h4) {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-color);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.markdown-content :deep(h5),
|
||||
.markdown-content :deep(h6) {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-color);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.markdown-content :deep(p) {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-color);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.markdown-content :deep(p:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown-content :deep(ul),
|
||||
.markdown-content :deep(ol) {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 2rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.markdown-content :deep(ul) {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.markdown-content :deep(ol) {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.markdown-content :deep(li) {
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.7;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.markdown-content :deep(li > p) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown-content :deep(li > ul),
|
||||
.markdown-content :deep(li > ol) {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown-content :deep(strong) {
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.markdown-content :deep(em) {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown-content :deep(code) {
|
||||
background-color: var(--surface-100);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875em;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.markdown-content :deep(pre) {
|
||||
background-color: var(--surface-100);
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.markdown-content :deep(pre code) {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div class="marketing-main-page bg-surface-50 dark:bg-surface-900 min-h-screen p-6">
|
||||
<div class="max-w-[1920px] mx-auto">
|
||||
<div class="flex justify-center">
|
||||
<!-- Navigation Menu -->
|
||||
<div class="w-full max-w-md">
|
||||
<div class="card h-full">
|
||||
<div class="card-header mb-4">
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Маркетинг</h2>
|
||||
</div>
|
||||
<div class="card-content space-y-3">
|
||||
<div
|
||||
v-for="item in menuItems"
|
||||
:key="item.id"
|
||||
class="menu-item p-4 rounded-lg cursor-pointer transition-colors"
|
||||
:class="{
|
||||
'bg-primary-50 dark:bg-primary-900': activeMenu === item.id,
|
||||
'hover:bg-surface-100 dark:hover:bg-surface-800': activeMenu !== item.id
|
||||
}"
|
||||
@click="navigateTo(item.route)"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<i :class="[item.icon, item.iconColor, 'text-xl']"></i>
|
||||
<span class="font-medium text-surface-900 dark:text-surface-0">{{ item.label }}</span>
|
||||
</div>
|
||||
<div class="w-2 h-2 rounded-full bg-surface-400"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Menu items
|
||||
const activeMenu = ref('main');
|
||||
const menuItems = [
|
||||
{
|
||||
id: 'analysis',
|
||||
label: 'Маркетинговый анализ',
|
||||
icon: 'pi pi-search',
|
||||
iconColor: 'text-primary-500',
|
||||
route: '/marketing-analysis/analysis'
|
||||
},
|
||||
{
|
||||
id: 'promotion',
|
||||
label: 'Продвижение',
|
||||
icon: 'pi pi-send',
|
||||
iconColor: 'text-primary-500',
|
||||
route: '/marketing-analysis/promotion'
|
||||
},
|
||||
{
|
||||
id: 'results',
|
||||
label: 'Результаты',
|
||||
icon: 'pi pi-chart-bar',
|
||||
iconColor: 'text-primary-500',
|
||||
route: '/marketing-analysis/results'
|
||||
}
|
||||
];
|
||||
|
||||
// Methods
|
||||
const navigateTo = (route) => {
|
||||
router.push(route);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.marketing-main-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;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,583 @@
|
||||
<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 v-if="!strategyId && !strategyData" class="card">
|
||||
<div class="card-header mb-4">
|
||||
<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-2">Создайте детальную стратегию продвижения на основе завершенного маркетингового анализа</p>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<form @submit.prevent="handleGenerateStrategy" class="space-y-4 max-w-2xl">
|
||||
<div class="field">
|
||||
<label for="analysisId" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> ID анализа <span class="text-red-500">*</span> </label>
|
||||
<InputText id="analysisId" v-model="formData.analysisId" placeholder="Введите ID завершенного анализа" class="w-full" :class="{ 'p-invalid': errors.analysisId }" />
|
||||
<small v-if="errors.analysisId" class="p-error">{{ errors.analysisId }}</small>
|
||||
<small class="text-surface-500 dark:text-surface-400"> ID можно получить из завершенного маркетингового анализа </small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="durationWeeks" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Длительность стратегии (недели) </label>
|
||||
<InputNumber id="durationWeeks" v-model="formData.durationWeeks" :min="1" :max="12" placeholder="4" class="w-full" :class="{ 'p-invalid': errors.durationWeeks }" />
|
||||
<small v-if="errors.durationWeeks" class="p-error">{{ errors.durationWeeks }}</small>
|
||||
<small class="text-surface-500 dark:text-surface-400">От 1 до 12 недель (по умолчанию: 4)</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="priorityPlatforms" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Приоритетные платформы </label>
|
||||
<MultiSelect id="priorityPlatforms" v-model="formData.priorityPlatforms" :options="platformOptions" placeholder="Выберите платформы (необязательно)" class="w-full" display="chip" />
|
||||
<small class="text-surface-500 dark:text-surface-400"> Если не указано, будут использованы все популярные платформы </small>
|
||||
</div>
|
||||
|
||||
<Button type="submit" label="Сгенерировать стратегию" icon="pi pi-magic" class="w-full p-button-primary" :loading="generating" />
|
||||
|
||||
<p class="text-sm text-surface-500 dark:text-surface-400 mt-3 text-center">Генерация стратегии занимает примерно 3-5 минут</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Статус обработки -->
|
||||
<div v-else-if="status === 'processing' || status === 'queued'" class="card">
|
||||
<div class="card-header mb-4">
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Генерация стратегии</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="text-center py-6">
|
||||
<ProgressSpinner size="50" />
|
||||
<p class="mt-4 text-lg text-surface-700 dark:text-surface-300">Стратегия генерируется...</p>
|
||||
<p class="mt-2 text-sm text-surface-500 dark:text-surface-400">Это может занять 3-5 минут. Пожалуйста, подождите.</p>
|
||||
<Button label="Отменить ожидание" icon="pi pi-times" severity="secondary" class="mt-4" @click="resetStrategy" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ошибка -->
|
||||
<div v-else-if="status === 'failed'" class="card">
|
||||
<div class="card-header mb-4">
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Ошибка генерации</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="text-center py-6">
|
||||
<i class="pi pi-exclamation-triangle text-6xl text-red-500 mb-3"></i>
|
||||
<p class="mt-4 text-lg text-surface-700 dark:text-surface-300">Генерация стратегии завершилась с ошибкой</p>
|
||||
<p class="mt-2 text-sm text-surface-500 dark:text-surface-400">Попробуйте запустить генерацию снова или обратитесь в поддержку.</p>
|
||||
<Button label="Попробовать снова" icon="pi pi-refresh" class="mt-4" @click="resetStrategy" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Результаты стратегии -->
|
||||
<div v-else-if="status === 'completed' && strategyData?.strategy" class="space-y-4">
|
||||
<!-- Заголовок -->
|
||||
<div class="card">
|
||||
<div class="card-header mb-4">
|
||||
<div class="flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Недельный план -->
|
||||
<div v-if="strategyData.strategy.weeklyPlans && strategyData.strategy.weeklyPlans.length > 0" class="card">
|
||||
<div class="card-header mb-4">
|
||||
<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> -->
|
||||
<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>
|
||||
<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>
|
||||
</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">
|
||||
{{ 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" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Календарь постов -->
|
||||
<div v-if="strategyData.strategy.postCalendar && strategyData.strategy.postCalendar.length > 0" 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>
|
||||
<div class="flex gap-2">
|
||||
<Button label="Экспорт CSV" icon="pi pi-download" severity="secondary" size="small" @click="exportToCsv" />
|
||||
<Button label="Группировать по датам" icon="pi pi-calendar" severity="secondary" size="small" @click="toggleGroupByDate" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<!-- Группировка по датам -->
|
||||
<div v-if="groupByDate">
|
||||
<div v-for="(posts, date) in postsByDate" :key="date" class="date-group mb-4 pb-4 border-bottom-1 border-surface-200 dark:border-surface-700">
|
||||
<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>
|
||||
<span class="text-sm 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>
|
||||
</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>
|
||||
<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 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">
|
||||
{{ 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<span class="text-sm 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>
|
||||
</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>
|
||||
<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 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">
|
||||
{{ 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MarketingService from '@/service/MarketingService';
|
||||
import Button from 'primevue/button';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import MultiSelect from 'primevue/multiselect';
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
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';
|
||||
|
||||
const toast = useToast();
|
||||
const route = useRoute();
|
||||
|
||||
// Form data
|
||||
const formData = ref({
|
||||
analysisId: '',
|
||||
durationWeeks: 4,
|
||||
priorityPlatforms: []
|
||||
});
|
||||
|
||||
const platformOptions = ['Instagram', 'Facebook', 'LinkedIn', 'Telegram', 'TikTok', 'YouTube', '21MC'];
|
||||
|
||||
const generating = ref(false);
|
||||
const errors = ref({});
|
||||
|
||||
// Strategy state
|
||||
const strategyId = ref(null);
|
||||
const status = ref(null);
|
||||
const strategyData = ref(null);
|
||||
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);
|
||||
|
||||
// Computed
|
||||
const postsByDate = computed(() => {
|
||||
if (!strategyData.value?.strategy?.postCalendar) return {};
|
||||
|
||||
return strategyData.value.strategy.postCalendar.reduce((acc, post) => {
|
||||
const date = new Date(post.publishDate).toISOString().split('T')[0];
|
||||
if (!acc[date]) {
|
||||
acc[date] = [];
|
||||
}
|
||||
acc[date].push(post);
|
||||
return acc;
|
||||
}, {});
|
||||
});
|
||||
|
||||
// Validation
|
||||
const validateForm = () => {
|
||||
errors.value = {};
|
||||
|
||||
if (!formData.value.analysisId || formData.value.analysisId.trim().length === 0) {
|
||||
errors.value.analysisId = 'ID анализа обязателен';
|
||||
}
|
||||
|
||||
if (formData.value.durationWeeks && (formData.value.durationWeeks < 1 || formData.value.durationWeeks > 12)) {
|
||||
errors.value.durationWeeks = 'Длительность должна быть от 1 до 12 недель';
|
||||
}
|
||||
|
||||
return Object.keys(errors.value).length === 0;
|
||||
};
|
||||
|
||||
// Start polling for strategy status
|
||||
const startPolling = (id) => {
|
||||
pollingAttempts.value = 0;
|
||||
pollingInterval.value = setInterval(async () => {
|
||||
pollingAttempts.value++;
|
||||
|
||||
if (pollingAttempts.value > maxPollingAttempts) {
|
||||
stopPolling();
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Таймаут',
|
||||
detail: 'Превышено время ожидания генерации стратегии. Попробуйте проверить результат позже.',
|
||||
life: 5000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await MarketingService.getStrategy(id);
|
||||
status.value = result.status;
|
||||
|
||||
if (result.status === 'completed' && result.strategy) {
|
||||
stopPolling();
|
||||
strategyData.value = result;
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Стратегия готова',
|
||||
detail: 'Стратегия продвижения успешно сгенерирована',
|
||||
life: 3000
|
||||
});
|
||||
} else if (result.status === 'failed') {
|
||||
stopPolling();
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка генерации',
|
||||
detail: 'Генерация стратегии завершилась с ошибкой',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при проверке статуса стратегии:', error);
|
||||
// Continue polling on error, but log it
|
||||
}
|
||||
}, pollingIntervalMs);
|
||||
};
|
||||
|
||||
// Stop polling
|
||||
const stopPolling = () => {
|
||||
if (pollingInterval.value) {
|
||||
clearInterval(pollingInterval.value);
|
||||
pollingInterval.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle form submit
|
||||
const handleGenerateStrategy = async () => {
|
||||
if (!validateForm()) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка валидации',
|
||||
detail: 'Пожалуйста, исправьте ошибки в форме',
|
||||
life: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
generating.value = true;
|
||||
|
||||
try {
|
||||
const options = {};
|
||||
if (formData.value.durationWeeks) {
|
||||
options.durationWeeks = formData.value.durationWeeks;
|
||||
}
|
||||
if (formData.value.priorityPlatforms && formData.value.priorityPlatforms.length > 0) {
|
||||
options.priorityPlatforms = formData.value.priorityPlatforms;
|
||||
}
|
||||
|
||||
const result = await MarketingService.generateStrategy(formData.value.analysisId.trim(), options);
|
||||
|
||||
strategyId.value = result.strategyId;
|
||||
status.value = result.status;
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Генерация запущена',
|
||||
detail: 'Генерация стратегии успешно запущена. Результаты будут готовы в течение 3-5 минут.',
|
||||
life: 5000
|
||||
});
|
||||
|
||||
// Start polling
|
||||
startPolling(result.strategyId);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при запуске генерации стратегии:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: error.message || 'Не удалось запустить генерацию стратегии. Пожалуйста, попробуйте снова.',
|
||||
life: 5000
|
||||
});
|
||||
} finally {
|
||||
generating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Reset strategy and form
|
||||
const resetStrategy = () => {
|
||||
stopPolling();
|
||||
strategyId.value = null;
|
||||
status.value = null;
|
||||
strategyData.value = null;
|
||||
pollingAttempts.value = 0;
|
||||
formData.value = {
|
||||
analysisId: '',
|
||||
durationWeeks: 4,
|
||||
priorityPlatforms: []
|
||||
};
|
||||
errors.value = {};
|
||||
};
|
||||
|
||||
// Handle copy post
|
||||
const handleCopyPost = (post) => {
|
||||
const textToCopy = `${post.postText}\n\n${post.hashtags ? post.hashtags.join(' ') : ''}`;
|
||||
navigator.clipboard
|
||||
.writeText(textToCopy)
|
||||
.then(() => {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Скопировано',
|
||||
detail: 'Текст поста скопирован в буфер обмена',
|
||||
life: 2000
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: 'Не удалось скопировать текст',
|
||||
life: 3000
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Export to CSV
|
||||
const exportToCsv = () => {
|
||||
if (!strategyData.value?.strategy?.postCalendar) return;
|
||||
|
||||
const headers = ['Дата', 'Время', 'Платформа', 'Тип контента', 'Тема', 'Текст поста', 'Хештеги'];
|
||||
const rows = strategyData.value.strategy.postCalendar.map((post) => {
|
||||
const date = new Date(post.publishDate);
|
||||
return [date.toLocaleDateString('ru-RU'), post.publishTime || '', post.platform || '', post.contentType || '', post.theme || '', `"${(post.postText || '').replace(/"/g, '""')}"`, (post.hashtags || []).join(' ')];
|
||||
});
|
||||
|
||||
const csvContent = [headers.join(','), ...rows.map((row) => row.join(','))].join('\n');
|
||||
|
||||
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `strategy_calendar_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Экспорт выполнен',
|
||||
detail: 'Календарь постов экспортирован в CSV',
|
||||
life: 3000
|
||||
});
|
||||
};
|
||||
|
||||
// Toggle group by date
|
||||
const toggleGroupByDate = () => {
|
||||
groupByDate.value = !groupByDate.value;
|
||||
};
|
||||
|
||||
// Format date
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long'
|
||||
});
|
||||
};
|
||||
|
||||
// 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 strategy from query params or try to get by analysisId
|
||||
onMounted(async () => {
|
||||
const queryAnalysisId = route.query.analysisId;
|
||||
const queryStrategyId = route.query.strategyId;
|
||||
|
||||
if (queryStrategyId) {
|
||||
// Try to load existing strategy
|
||||
try {
|
||||
const result = await MarketingService.getStrategy(queryStrategyId);
|
||||
strategyId.value = result.strategyId;
|
||||
status.value = result.status;
|
||||
if (result.status === 'completed' && result.strategy) {
|
||||
strategyData.value = result;
|
||||
} else if (result.status === 'processing' || result.status === 'queued') {
|
||||
startPolling(result.strategyId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке стратегии:', error);
|
||||
}
|
||||
} else if (queryAnalysisId) {
|
||||
// Try to get strategy by analysisId
|
||||
formData.value.analysisId = queryAnalysisId;
|
||||
try {
|
||||
const result = await MarketingService.getStrategyByAnalysis(queryAnalysisId);
|
||||
strategyId.value = result.strategyId;
|
||||
status.value = result.status;
|
||||
if (result.status === 'completed' && result.strategy) {
|
||||
strategyData.value = result;
|
||||
} else if (result.status === 'processing' || result.status === 'queued') {
|
||||
startPolling(result.strategyId);
|
||||
}
|
||||
} catch (error) {
|
||||
// Strategy doesn't exist yet, show form
|
||||
console.log('Стратегия для анализа не найдена, показываем форму');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup on unmount
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.marketing-promotion-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;
|
||||
}
|
||||
|
||||
.week-plan-card {
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.week-plan-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.post-card {
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.post-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.date-group:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="marketing-results-page bg-surface-50 dark:bg-surface-900 min-h-screen p-6">
|
||||
<div class="max-w-[1200px] mx-auto">
|
||||
<div class="card">
|
||||
<div class="card-header mb-4">
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Результаты кампании</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<!-- Metrics -->
|
||||
<div class="grid grid-cols-3 gap-4 mb-6">
|
||||
<div class="metric-card bg-surface-0 dark:bg-surface-800 rounded-lg p-4 text-center">
|
||||
<p class="text-sm text-surface-500 dark:text-surface-400 mb-1">Охват</p>
|
||||
<p class="text-2xl font-bold text-surface-900 dark:text-surface-0">15 000</p>
|
||||
</div>
|
||||
<div class="metric-card bg-surface-0 dark:bg-surface-800 rounded-lg p-4 text-center">
|
||||
<p class="text-sm text-surface-500 dark:text-surface-400 mb-1">Клики</p>
|
||||
<p class="text-2xl font-bold text-surface-900 dark:text-surface-0">1 200</p>
|
||||
</div>
|
||||
<div class="metric-card bg-surface-0 dark:bg-surface-800 rounded-lg p-4 text-center">
|
||||
<p class="text-sm text-surface-500 dark:text-surface-400 mb-1">Заявки</p>
|
||||
<p class="text-2xl font-bold text-surface-900 dark:text-surface-0">150</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart -->
|
||||
<div class="chart-container mb-6" style="height: 300px">
|
||||
<Chart type="line" :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
|
||||
<!-- By Channels -->
|
||||
<div class="mb-6">
|
||||
<h4 class="text-sm font-semibold mb-4 text-surface-700 dark:text-surface-300">По каналам</h4>
|
||||
<div class="flex gap-6 justify-center">
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center mb-2 mx-auto">
|
||||
<i class="pi pi-instagram text-primary-600 dark:text-primary-300 text-2xl"></i>
|
||||
</div>
|
||||
<p class="text-xl font-semibold text-surface-900 dark:text-surface-0">900</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center mb-2 mx-auto">
|
||||
<i class="pi pi-send text-primary-600 dark:text-primary-300 text-2xl"></i>
|
||||
</div>
|
||||
<p class="text-xl font-semibold text-surface-900 dark:text-surface-0">250</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center mb-2 mx-auto">
|
||||
<i class="pi pi-hashtag text-primary-600 dark:text-primary-300 text-2xl"></i>
|
||||
</div>
|
||||
<p class="text-xl font-semibold text-surface-900 dark:text-surface-0">50</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recommendation -->
|
||||
<div class="recommendation-card bg-primary-50 dark:bg-primary-900 rounded-lg p-4">
|
||||
<h4 class="text-sm font-semibold mb-2 text-surface-900 dark:text-surface-0">Рекомендация</h4>
|
||||
<p class="text-sm text-surface-700 dark:text-surface-300">Instagram стал ведущим каналом - вам стоит продолжать работу с визуальным контентом</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Chart from 'primevue/chart';
|
||||
import { ref } from 'vue';
|
||||
|
||||
// Chart data
|
||||
const chartData = ref({
|
||||
labels: ['Неделя 1', 'Неделя 2', 'Неделя 3', 'Неделя 4', 'Неделя 5', 'Неделя 6'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Охват',
|
||||
data: [5000, 8000, 10000, 12000, 13000, 15000],
|
||||
borderColor: '#3B82F6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.05)'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.marketing-results-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;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
border: 1px solid var(--surface-border);
|
||||
}
|
||||
|
||||
.recommendation-card {
|
||||
border: 1px solid var(--primary-200);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user