.
This commit is contained in:
@@ -0,0 +1,752 @@
|
||||
# API для запуска стратегий продвижения
|
||||
|
||||
## Обзор
|
||||
|
||||
Данный документ описывает API для управления credentials социальных сетей и запуска маркетинговых стратегий с автоматической публикацией постов.
|
||||
|
||||
## Базовый URL
|
||||
|
||||
```
|
||||
http://your-server:port/api
|
||||
```
|
||||
|
||||
## Аутентификация
|
||||
|
||||
Все endpoints требуют JWT токен в заголовке `Authorization`:
|
||||
|
||||
```
|
||||
Authorization: Bearer <your-jwt-token>
|
||||
```
|
||||
|
||||
## Управление Credentials социальных сетей
|
||||
|
||||
### 1. Сохранение/обновление credentials
|
||||
|
||||
Сохраняет или обновляет credentials для указанной платформы. Credentials автоматически шифруются перед сохранением.
|
||||
|
||||
**Endpoint:** `POST /api/social-media/credentials`
|
||||
|
||||
**Headers:**
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"platform": "facebook",
|
||||
"credentials": "your-facebook-access-token"
|
||||
}
|
||||
```
|
||||
|
||||
**Параметры:**
|
||||
|
||||
- `platform` (string, required) - Название платформы (например: "facebook", "instagram")
|
||||
- `credentials` (string, required) - Access token или другие credentials для платформы
|
||||
|
||||
**Response 200 OK:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Credentials успешно сохранены для платформы facebook",
|
||||
"data": {
|
||||
"platform": "facebook",
|
||||
"hasCredentials": true,
|
||||
"createdAt": "2024-01-15T10:30:00",
|
||||
"updatedAt": "2024-01-15T10:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response 400 Bad Request:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Ошибка валидации",
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Platform is required",
|
||||
"details": {
|
||||
"platform": "Platform is required"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response 401 Unauthorized:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Не авторизован",
|
||||
"error": {
|
||||
"code": "UNAUTHORIZED",
|
||||
"message": "Требуется аутентификация. Пожалуйста, предоставьте валидный JWT токен."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Пример запроса (JavaScript):**
|
||||
|
||||
```javascript
|
||||
const saveCredentials = async (platform, accessToken) => {
|
||||
const response = await fetch('/api/social-media/credentials', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
platform: platform,
|
||||
credentials: accessToken,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
// Использование
|
||||
await saveCredentials('facebook', 'EAABwzLix...');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Получение информации о credentials
|
||||
|
||||
Проверяет наличие credentials для указанной платформы.
|
||||
|
||||
**Endpoint:** `GET /api/social-media/credentials/{platform}`
|
||||
|
||||
**Headers:**
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `platform` (string) - Название платформы
|
||||
|
||||
**Response 200 OK:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Операция выполнена успешно",
|
||||
"data": {
|
||||
"platform": "facebook",
|
||||
"hasCredentials": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Пример запроса:**
|
||||
|
||||
```javascript
|
||||
const checkCredentials = async (platform) => {
|
||||
const response = await fetch(`/api/social-media/credentials/${platform}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return data.data.hasCredentials;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Получение списка всех credentials
|
||||
|
||||
Возвращает список всех платформ, для которых у пользователя настроены credentials.
|
||||
|
||||
**Endpoint:** `GET /api/social-media/credentials`
|
||||
|
||||
**Headers:**
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**Response 200 OK:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Операция выполнена успешно",
|
||||
"data": [
|
||||
{
|
||||
"platform": "facebook",
|
||||
"hasCredentials": true,
|
||||
"createdAt": "2024-01-15T10:30:00",
|
||||
"updatedAt": "2024-01-15T10:30:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Пример запроса:**
|
||||
|
||||
```javascript
|
||||
const getAllCredentials = async () => {
|
||||
const response = await fetch('/api/social-media/credentials', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return data.data;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Удаление credentials
|
||||
|
||||
Удаляет credentials для указанной платформы.
|
||||
|
||||
**Endpoint:** `DELETE /api/social-media/credentials/{platform}`
|
||||
|
||||
**Headers:**
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `platform` (string) - Название платформы
|
||||
|
||||
**Response 200 OK:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Credentials для платформы facebook успешно удалены",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**Response 404 Not Found:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Credentials не найдены",
|
||||
"error": {
|
||||
"code": "NOT_FOUND",
|
||||
"message": "Credentials для платформы facebook не найдены"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Пример запроса:**
|
||||
|
||||
```javascript
|
||||
const deleteCredentials = async (platform) => {
|
||||
const response = await fetch(`/api/social-media/credentials/${platform}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Запуск стратегии продвижения
|
||||
|
||||
### Запуск стратегии
|
||||
|
||||
Запускает выполнение маркетинговой стратегии. Система автоматически создает задачи публикации из календаря постов стратегии и добавляет их в очередь для выполнения.
|
||||
|
||||
**Endpoint:** `POST /api/marketing/analysis/strategy/{strategyId}/start`
|
||||
|
||||
**Headers:**
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `strategyId` (string) - ID стратегии для запуска
|
||||
|
||||
**Request Body (опционально):**
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
**Response 200 OK:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Стратегия успешно запущена",
|
||||
"data": {
|
||||
"strategyId": "67890abcdef",
|
||||
"tasksCreated": 12,
|
||||
"platforms": ["facebook", "instagram"],
|
||||
"message": "Стратегия успешно запущена. Создано задач: 12"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response 400 Bad Request (стратегия не завершена):**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Стратегия не готова к запуску",
|
||||
"error": {
|
||||
"code": "INVALID_STATUS",
|
||||
"message": "Стратегия еще не завершена. Статус: processing"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response 400 Bad Request (нет credentials):**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Не удалось запустить стратегию",
|
||||
"error": {
|
||||
"code": "MISSING_CREDENTIALS",
|
||||
"message": "Credentials not found for platform: facebook. Please configure credentials first."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response 404 Not Found:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Стратегия не найдена",
|
||||
"error": {
|
||||
"code": "NOT_FOUND",
|
||||
"message": "Стратегия с указанным ID не найдена"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response 403 Forbidden:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Доступ запрещен",
|
||||
"error": {
|
||||
"code": "FORBIDDEN",
|
||||
"message": "У вас нет доступа к этой стратегии"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Пример запроса:**
|
||||
|
||||
```javascript
|
||||
const startStrategy = async (strategyId) => {
|
||||
const response = await fetch(
|
||||
`/api/marketing/analysis/strategy/${strategyId}/start`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
console.log(`Создано задач: ${data.data.tasksCreated}`);
|
||||
console.log(`Платформы: ${data.data.platforms.join(', ')}`);
|
||||
} else {
|
||||
console.error('Ошибка:', data.error.message);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Полный пример использования
|
||||
|
||||
### Шаг 1: Настройка credentials для Facebook
|
||||
|
||||
```javascript
|
||||
// Сохраняем Facebook Access Token
|
||||
const facebookToken = 'EAABwzLix...'; // Получить из Facebook Developer Console
|
||||
|
||||
const result = await saveCredentials('facebook', facebookToken);
|
||||
if (result.success) {
|
||||
console.log('Facebook credentials сохранены');
|
||||
}
|
||||
```
|
||||
|
||||
### Шаг 2: Получение стратегии
|
||||
|
||||
```javascript
|
||||
// Получаем список стратегий пользователя
|
||||
const response = await fetch('/api/marketing/analysis/strategy/my', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const strategies = data.data;
|
||||
|
||||
// Выбираем завершенную стратегию
|
||||
const completedStrategy = strategies.find((s) => s.status === 'completed');
|
||||
```
|
||||
|
||||
### Шаг 3: Запуск стратегии
|
||||
|
||||
```javascript
|
||||
if (completedStrategy) {
|
||||
// Проверяем наличие credentials для платформ стратегии
|
||||
const platforms = completedStrategy.priorityPlatforms || [];
|
||||
|
||||
for (const platform of platforms) {
|
||||
const hasCreds = await checkCredentials(platform);
|
||||
if (!hasCreds) {
|
||||
console.warn(`Необходимо настроить credentials для ${platform}`);
|
||||
// Показать пользователю форму для ввода credentials
|
||||
}
|
||||
}
|
||||
|
||||
// Запускаем стратегию
|
||||
const startResult = await startStrategy(completedStrategy.strategyId);
|
||||
|
||||
if (startResult.success) {
|
||||
console.log(
|
||||
`Стратегия запущена! Создано ${startResult.data.tasksCreated} задач`
|
||||
);
|
||||
// Показать уведомление пользователю
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
### Типичные ошибки и их обработка
|
||||
|
||||
1. **UNAUTHORIZED (401)**
|
||||
|
||||
- Причина: Невалидный или отсутствующий JWT токен
|
||||
- Решение: Обновить токен или перенаправить на страницу входа
|
||||
|
||||
2. **VALIDATION_ERROR (400)**
|
||||
|
||||
- Причина: Невалидные данные в запросе
|
||||
- Решение: Проверить обязательные поля и их формат
|
||||
|
||||
3. **MISSING_CREDENTIALS (400)**
|
||||
|
||||
- Причина: Не настроены credentials для платформы
|
||||
- Решение: Предложить пользователю настроить credentials
|
||||
|
||||
4. **INVALID_STATUS (400)**
|
||||
|
||||
- Причина: Стратегия еще не завершена
|
||||
- Решение: Дождаться завершения генерации стратегии
|
||||
|
||||
5. **NOT_FOUND (404)**
|
||||
|
||||
- Причина: Стратегия или credentials не найдены
|
||||
- Решение: Проверить правильность ID
|
||||
|
||||
6. **FORBIDDEN (403)**
|
||||
- Причина: Пользователь не имеет доступа к ресурсу
|
||||
- Решение: Проверить права доступа
|
||||
|
||||
### Пример обработки ошибок
|
||||
|
||||
```javascript
|
||||
const handleApiError = (error) => {
|
||||
switch (error.code) {
|
||||
case 'UNAUTHORIZED':
|
||||
// Перенаправить на страницу входа
|
||||
window.location.href = '/login';
|
||||
break;
|
||||
|
||||
case 'MISSING_CREDENTIALS':
|
||||
// Показать модальное окно для настройки credentials
|
||||
showCredentialsModal(error.message);
|
||||
break;
|
||||
|
||||
case 'INVALID_STATUS':
|
||||
// Показать сообщение о том, что стратегия еще не готова
|
||||
showNotification(
|
||||
'Стратегия еще не завершена. Пожалуйста, подождите.',
|
||||
'warning'
|
||||
);
|
||||
break;
|
||||
|
||||
case 'VALIDATION_ERROR':
|
||||
// Показать ошибки валидации
|
||||
showValidationErrors(error.details);
|
||||
break;
|
||||
|
||||
default:
|
||||
showNotification('Произошла ошибка. Попробуйте позже.', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// Использование
|
||||
try {
|
||||
const result = await startStrategy(strategyId);
|
||||
if (!result.success) {
|
||||
handleApiError(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Network error:', error);
|
||||
showNotification('Ошибка сети. Проверьте подключение.', 'error');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Статусы задач публикации
|
||||
|
||||
После запуска стратегии создаются задачи со следующими статусами:
|
||||
|
||||
- `pending` - Задача ожидает выполнения (дата публикации еще не наступила)
|
||||
- `processing` - Задача выполняется в данный момент
|
||||
- `completed` - Задача успешно выполнена
|
||||
- `failed` - Задача не выполнена из-за ошибки
|
||||
|
||||
**Примечание:** Задачи с датой публикации в прошлом выполняются сразу после создания.
|
||||
|
||||
---
|
||||
|
||||
## Планировщик задач
|
||||
|
||||
Система автоматически проверяет очередь задач каждую минуту и выполняет задачи, у которых наступило время публикации.
|
||||
|
||||
- Планировщик включен по умолчанию
|
||||
- Можно отключить через настройку `posting.scheduler.enabled=false`
|
||||
- Задачи выполняются асинхронно
|
||||
|
||||
---
|
||||
|
||||
## Поддерживаемые платформы
|
||||
|
||||
На данный момент поддерживается:
|
||||
|
||||
- **Facebook** - через Facebook Graph API v18.0
|
||||
|
||||
В будущем планируется поддержка:
|
||||
|
||||
- Instagram
|
||||
- LinkedIn
|
||||
- Telegram
|
||||
- TikTok
|
||||
- YouTube
|
||||
|
||||
---
|
||||
|
||||
## Получение Facebook Access Token
|
||||
|
||||
Для получения Facebook Access Token:
|
||||
|
||||
1. Перейдите на [Facebook Developers](https://developers.facebook.com/)
|
||||
2. Создайте приложение
|
||||
3. Добавьте продукт "Facebook Login"
|
||||
4. Настройте OAuth и получите Access Token
|
||||
5. Используйте полученный токен в API
|
||||
|
||||
**Важно:**
|
||||
|
||||
- Access Token имеет срок действия
|
||||
- Для долгосрочного использования рекомендуется использовать Long-Lived Token
|
||||
- Токен должен иметь разрешения `pages_manage_posts` для публикации
|
||||
|
||||
---
|
||||
|
||||
## Примеры React компонентов
|
||||
|
||||
### Компонент для настройки credentials
|
||||
|
||||
```jsx
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const CredentialsForm = ({ platform, onSave }) => {
|
||||
const [token, setToken] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/social-media/credentials', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
platform: platform,
|
||||
credentials: token,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
onSave();
|
||||
setToken('');
|
||||
} else {
|
||||
setError(data.error.message);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Ошибка сети');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label>Access Token для {platform}:</label>
|
||||
<input
|
||||
type='text'
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{error && <div className='error'>{error}</div>}
|
||||
<button type='submit' disabled={loading}>
|
||||
{loading ? 'Сохранение...' : 'Сохранить'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default CredentialsForm;
|
||||
```
|
||||
|
||||
### Компонент для запуска стратегии
|
||||
|
||||
```jsx
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const StartStrategyButton = ({ strategyId, onStart }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const handleStart = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/marketing/analysis/strategy/${strategyId}/start`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
onStart(data.data);
|
||||
} else {
|
||||
setError(data.error.message);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Ошибка сети');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={handleStart} disabled={loading}>
|
||||
{loading ? 'Запуск...' : 'Запустить стратегию'}
|
||||
</button>
|
||||
{error && <div className='error'>{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StartStrategyButton;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Часто задаваемые вопросы
|
||||
|
||||
### Q: Как часто проверяются задачи на выполнение?
|
||||
|
||||
A: Планировщик проверяет очередь каждую минуту.
|
||||
|
||||
### Q: Что происходит, если credentials истекли?
|
||||
|
||||
A: Задача получит статус `failed` с сообщением об ошибке. Необходимо обновить credentials и перезапустить стратегию.
|
||||
|
||||
### Q: Можно ли отменить выполнение стратегии?
|
||||
|
||||
A: На данный момент нет, но можно удалить credentials для платформы, что предотвратит выполнение будущих задач.
|
||||
|
||||
### Q: Как узнать статус выполнения задач?
|
||||
|
||||
A: Статусы задач можно получить через API (будет добавлено в будущих версиях).
|
||||
|
||||
### Q: Поддерживается ли публикация с изображениями?
|
||||
|
||||
A: На данный момент поддерживается только текстовая публикация. Поддержка изображений планируется в будущем.
|
||||
|
||||
---
|
||||
|
||||
## Версионирование API
|
||||
|
||||
Текущая версия: **v1**
|
||||
|
||||
Все endpoints могут изменяться в будущих версиях. При изменении API будет указана новая версия.
|
||||
|
||||
---
|
||||
|
||||
## Поддержка
|
||||
|
||||
При возникновении проблем:
|
||||
|
||||
1. Проверьте логи в консоли браузера
|
||||
2. Убедитесь, что JWT токен валиден
|
||||
3. Проверьте формат запросов согласно документации
|
||||
4. Обратитесь к разработчикам с описанием проблемы
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,411 @@
|
||||
# Обновление API: Виды анализа для маркетингового анализа (Frontend/AI Agent)
|
||||
|
||||
## Обзор изменений
|
||||
|
||||
В API маркетингового анализа добавлена поддержка **опциональных видов анализа**, которые могут быть переданы от фронтенда и будут включены в промпт для AI-генерации отчета.
|
||||
|
||||
**Дата обновления**: 2025-01-20
|
||||
|
||||
---
|
||||
|
||||
## Что изменилось
|
||||
|
||||
### Новые опциональные поля в запросе
|
||||
|
||||
В эндпоинт `POST /api/marketing/analysis/start` добавлены 5 новых опциональных полей для видов анализа:
|
||||
|
||||
1. **`market`** - Анализ рынка
|
||||
2. **`competitors`** - Анализ конкурентов
|
||||
3. **`targetAudienceAnalysis`** - Анализ целевой аудитории
|
||||
4. **`channels`** - Анализ каналов
|
||||
5. **`swot`** - SWOT анализ
|
||||
|
||||
Эти данные будут:
|
||||
|
||||
- ✅ Включены в контекст промпта для AI-генерации
|
||||
- ✅ Сохранены в отчете
|
||||
- ✅ Отображены в PDF-отчете в разделе "Виды анализа"
|
||||
|
||||
---
|
||||
|
||||
## Обновленная структура запроса
|
||||
|
||||
### Эндпоинт
|
||||
|
||||
**POST** `/api/marketing/analysis/start`
|
||||
|
||||
### Тело запроса (JSON)
|
||||
|
||||
#### Обязательные поля (без изменений)
|
||||
|
||||
| Поле | Тип | Обязательный | Описание | Пример значения |
|
||||
| ---------------- | ------ | ------------ | ------------------------------ | -------------------------------- |
|
||||
| `product` | string | ✅ | Название продукта или услуги | "Веб-разработка" |
|
||||
| `location` | string | ✅ | Географическая локация работы | "Алматы, Казахстан" |
|
||||
| `client` | string | ✅ | Тип целевой аудитории | "B2B клиенты" |
|
||||
| `differentiator` | string | ✅ | Уникальные особенности бизнеса | "Быстрая разработка за 2 недели" |
|
||||
|
||||
#### Новые опциональные поля
|
||||
|
||||
| Поле | Тип | Обязательный | Описание | Максимальная длина | Пример значения |
|
||||
| ------------------------ | ------ | ------------ | ----------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------- |
|
||||
| `market` | string | ❌ | Анализ рынка (размер, тренды, возможности) | 2000 символов | "Рынок веб-разработки в Казахстане растет на 15% ежегодно..." |
|
||||
| `competitors` | string | ❌ | Анализ конкурентов (основные игроки, их преимущества) | 2000 символов | "Основные конкуренты: Компания X, Компания Y..." |
|
||||
| `targetAudienceAnalysis` | string | ❌ | Детальный анализ целевой аудитории | 2000 символов | "Целевая аудитория: IT-директора средних компаний..." |
|
||||
| `channels` | string | ❌ | Анализ маркетинговых каналов | 2000 символов | "Рекомендуемые каналы: LinkedIn для B2B, Instagram для визуального контента..." |
|
||||
| `swot` | string | ❌ | SWOT анализ (Strengths, Weaknesses, Opportunities, Threats) | 2000 символов | "Strengths: Быстрая разработка... Weaknesses: Ограниченный бюджет..." |
|
||||
|
||||
### Валидация новых полей
|
||||
|
||||
Все новые поля имеют одинаковые правила валидации:
|
||||
|
||||
- **Тип**: `string`
|
||||
- **Обязательность**: ❌ Опциональное (можно не передавать)
|
||||
- **Максимальная длина**: 2000 символов
|
||||
- **Минимальная длина**: Нет ограничений (может быть пустой строкой)
|
||||
|
||||
---
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### Пример 1: Запрос только с обязательными полями (как раньше)
|
||||
|
||||
```javascript
|
||||
const response = await fetch(
|
||||
'https://api.konturai.kz/api/marketing/analysis/start',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product: 'Разработка мобильных приложений',
|
||||
location: 'Нур-Султан, Казахстан',
|
||||
client: 'B2B клиенты',
|
||||
differentiator: 'Специализируемся на быстрой разработке MVP за 4 недели',
|
||||
}),
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
**Результат**: Работает как раньше, без изменений в функциональности.
|
||||
|
||||
---
|
||||
|
||||
### Пример 2: Запрос с одним видом анализа
|
||||
|
||||
```javascript
|
||||
const response = await fetch(
|
||||
'https://api.konturai.kz/api/marketing/analysis/start',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product: 'Разработка мобильных приложений',
|
||||
location: 'Нур-Султан, Казахстан',
|
||||
client: 'B2B клиенты',
|
||||
differentiator: 'Специализируемся на быстрой разработке MVP за 4 недели',
|
||||
market:
|
||||
'Рынок мобильной разработки в Казахстане показывает стабильный рост на 20% в год. Основные драйверы: цифровизация бизнеса, рост числа стартапов, увеличение инвестиций в IT-сектор.',
|
||||
}),
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
**Результат**: Данные об анализе рынка будут включены в промпт и отображены в отчете.
|
||||
|
||||
---
|
||||
|
||||
### Пример 3: Запрос со всеми видами анализа
|
||||
|
||||
```javascript
|
||||
const response = await fetch(
|
||||
'https://api.konturai.kz/api/marketing/analysis/start',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${jwtToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product: 'Разработка мобильных приложений',
|
||||
location: 'Нур-Султан, Казахстан',
|
||||
client: 'B2B клиенты',
|
||||
differentiator: 'Специализируемся на быстрой разработке MVP за 4 недели',
|
||||
|
||||
// Виды анализа
|
||||
market:
|
||||
'Рынок мобильной разработки в Казахстане растет на 20% ежегодно. Основные сегменты: корпоративные приложения, e-commerce, финтех.',
|
||||
|
||||
competitors:
|
||||
'Основные конкуренты: TechCorp (лидер рынка, 30% доля), DevStudio (средний сегмент, 15% доля), StartupDev (нишевый игрок, 5% доля). Их преимущества: большая команда, долгосрочные контракты.',
|
||||
|
||||
targetAudienceAnalysis:
|
||||
'Целевая аудитория: IT-директора средних и крупных компаний (50-500 сотрудников), возраст 35-50 лет, техническое образование. Боли: долгие сроки разработки, высокие цены, отсутствие гибкости.',
|
||||
|
||||
channels:
|
||||
'Рекомендуемые каналы: LinkedIn (основной для B2B), Telegram-каналы для IT-сообщества, отраслевые конференции, контент-маркетинг через блог.',
|
||||
|
||||
swot: 'Strengths: Быстрая разработка MVP, опытная команда, гибкие методологии. Weaknesses: Ограниченный бюджет на маркетинг, небольшая команда. Opportunities: Рост спроса на мобильные решения, цифровизация госсектора. Threats: Конкуренция с крупными игроками, экономическая нестабильность.',
|
||||
}),
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
**Результат**: Все виды анализа будут включены в промпт для более точной генерации отчета и отображены в PDF.
|
||||
|
||||
---
|
||||
|
||||
## Как это влияет на генерацию отчета
|
||||
|
||||
### Включение в промпт
|
||||
|
||||
Когда вы передаете виды анализа, они автоматически добавляются в контекст промпта для AI:
|
||||
|
||||
```
|
||||
Продукт/услуга: Разработка мобильных приложений
|
||||
Локация: Нур-Султан, Казахстан
|
||||
Тип клиентов: B2B клиенты
|
||||
Уникальные особенности: Специализируемся на быстрой разработке MVP за 4 недели
|
||||
|
||||
Анализ рынка:
|
||||
Рынок мобильной разработки в Казахстане растет на 20% ежегодно...
|
||||
|
||||
Анализ конкурентов:
|
||||
Основные конкуренты: TechCorp...
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Это позволяет AI генерировать более точные и релевантные рекомендации, учитывая предоставленную аналитику.
|
||||
|
||||
### Отображение в PDF-отчете
|
||||
|
||||
Если виды анализа были переданы, они будут отображены в PDF-отчете в отдельном разделе:
|
||||
|
||||
```markdown
|
||||
## Виды анализа
|
||||
|
||||
### Анализ рынка
|
||||
|
||||
Рынок мобильной разработки в Казахстане растет на 20% ежегодно...
|
||||
|
||||
### Анализ конкурентов
|
||||
|
||||
Основные конкуренты: TechCorp...
|
||||
|
||||
### Анализ целевой аудитории
|
||||
|
||||
Целевая аудитория: IT-директора средних и крупных компаний...
|
||||
|
||||
### Анализ каналов
|
||||
|
||||
Рекомендуемые каналы: LinkedIn (основной для B2B)...
|
||||
|
||||
### SWOT анализ
|
||||
|
||||
Strengths: Быстрая разработка MVP...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Обратная совместимость
|
||||
|
||||
✅ **Полная обратная совместимость**: Старые запросы без новых полей продолжают работать без изменений.
|
||||
|
||||
✅ **Опциональные поля**: Все новые поля опциональны, их можно не передавать.
|
||||
|
||||
✅ **Валидация**: Если поля переданы, они валидируются (максимум 2000 символов).
|
||||
|
||||
---
|
||||
|
||||
## Рекомендации для фронтенда
|
||||
|
||||
### 1. UI/UX
|
||||
|
||||
Рекомендуется добавить в форму создания анализа:
|
||||
|
||||
- **Опциональные секции** для каждого вида анализа
|
||||
- **Текстовые поля** (textarea) с ограничением в 2000 символов
|
||||
- **Подсказки** о том, какую информацию следует включить в каждый вид анализа
|
||||
- **Индикатор прогресса** заполнения (опционально)
|
||||
|
||||
### 2. Валидация на фронтенде
|
||||
|
||||
```javascript
|
||||
// Пример валидации на фронтенде
|
||||
const validateAnalysisTypes = (data) => {
|
||||
const errors = {};
|
||||
|
||||
const analysisTypes = [
|
||||
'market',
|
||||
'competitors',
|
||||
'targetAudienceAnalysis',
|
||||
'channels',
|
||||
'swot',
|
||||
];
|
||||
|
||||
analysisTypes.forEach((field) => {
|
||||
if (data[field] && data[field].length > 2000) {
|
||||
errors[field] = `Поле "${field}" не должно превышать 2000 символов`;
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Структура данных
|
||||
|
||||
```typescript
|
||||
// TypeScript интерфейс для запроса
|
||||
interface MarketingAnalysisRequest {
|
||||
// Обязательные поля
|
||||
product: string; // 3-200 символов
|
||||
location: string; // 2-150 символов
|
||||
client: string; // Одно из допустимых значений
|
||||
differentiator: string; // 10-500 символов
|
||||
|
||||
// Опциональные виды анализа
|
||||
market?: string; // до 2000 символов
|
||||
competitors?: string; // до 2000 символов
|
||||
targetAudienceAnalysis?: string; // до 2000 символов
|
||||
channels?: string; // до 2000 символов
|
||||
swot?: string; // до 2000 символов
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Примеры ответов API
|
||||
|
||||
### Успешный ответ (без изменений)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Анализ запущен успешно",
|
||||
"data": {
|
||||
"analysisId": "507f1f77bcf86cd799439011",
|
||||
"status": "processing",
|
||||
"estimatedCompletionTime": "2025-01-20T15:38:00",
|
||||
"message": "Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ошибка валидации (если поле превышает лимит)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Ошибка валидации",
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Ошибка валидации входных данных",
|
||||
"details": {
|
||||
"market": "Поле 'market' должно содержать не более 2000 символов"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP статус**: `400 Bad Request`
|
||||
|
||||
---
|
||||
|
||||
## Часто задаваемые вопросы (FAQ)
|
||||
|
||||
### Q: Обязательно ли передавать все виды анализа?
|
||||
|
||||
**A**: Нет, все поля опциональны. Вы можете передать только те, которые у вас есть.
|
||||
|
||||
### Q: Что произойдет, если я передам пустую строку?
|
||||
|
||||
**A**: Пустые строки игнорируются, они не будут включены в промпт и отчет.
|
||||
|
||||
### Q: Можно ли передать только часть видов анализа?
|
||||
|
||||
**A**: Да, вы можете передать любое количество видов анализа (от 0 до 5).
|
||||
|
||||
### Q: Влияет ли это на время генерации отчета?
|
||||
|
||||
**A**: Нет, время генерации остается прежним (5-10 минут). Дополнительные данные могут улучшить качество отчета.
|
||||
|
||||
### Q: Будут ли виды анализа отображаться в JSON-ответе?
|
||||
|
||||
**A**: Виды анализа сохраняются в отчете и могут быть доступны через структуру `reportData`, но в текущей версии API они не возвращаются в стандартном JSON-ответе. Они включены в PDF-отчет.
|
||||
|
||||
---
|
||||
|
||||
## Миграция для существующих клиентов
|
||||
|
||||
### Для существующих интеграций
|
||||
|
||||
**Никаких изменений не требуется!** Старые запросы продолжают работать без изменений.
|
||||
|
||||
### Для новых интеграций
|
||||
|
||||
Если вы хотите использовать новые возможности:
|
||||
|
||||
1. Добавьте новые поля в форму запроса (опционально)
|
||||
2. Соблюдайте лимит в 2000 символов для каждого поля
|
||||
3. Передавайте только те виды анализа, которые у вас есть
|
||||
|
||||
---
|
||||
|
||||
## Технические детали
|
||||
|
||||
### Структура хранения
|
||||
|
||||
Виды анализа сохраняются в MongoDB в поле `reportData.analysisTypes`:
|
||||
|
||||
```json
|
||||
{
|
||||
"reportData": {
|
||||
"summary": "...",
|
||||
"targetAudience": {...},
|
||||
"recommendations": [...],
|
||||
"strategy": {...},
|
||||
"analysisTypes": {
|
||||
"market": "Рынок...",
|
||||
"competitors": "Конкуренты...",
|
||||
"targetAudienceAnalysis": "ЦА...",
|
||||
"channels": "Каналы...",
|
||||
"swot": "SWOT..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Включение в промпт
|
||||
|
||||
Виды анализа добавляются в контекст промпта в следующем формате:
|
||||
|
||||
```
|
||||
Анализ рынка:
|
||||
[содержимое поля market]
|
||||
|
||||
Анализ конкурентов:
|
||||
[содержимое поля competitors]
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Это позволяет AI учитывать предоставленную аналитику при генерации резюме, рекомендаций и стратегии.
|
||||
|
||||
---
|
||||
|
||||
## Поддержка
|
||||
|
||||
Если у вас возникли вопросы или проблемы с использованием новых полей, обратитесь к документации API или свяжитесь с командой разработки.
|
||||
|
||||
---
|
||||
|
||||
**Версия документа**: 1.0
|
||||
**Дата обновления**: 2025-01-20
|
||||
**Статус**: ✅ Актуально
|
||||
+6
-1
@@ -62,7 +62,12 @@ export const API_CONFIG = {
|
||||
MARKETING_STRATEGY_GET: '/api/marketing/analysis/strategy',
|
||||
MARKETING_STRATEGY_BY_ANALYSIS: '/api/marketing/analysis',
|
||||
MARKETING_STRATEGY_MY: '/api/marketing/analysis/strategy/my',
|
||||
MARKETING_STRATEGY_HISTORY: '/api/marketing/analysis/strategy'
|
||||
MARKETING_STRATEGY_HISTORY: '/api/marketing/analysis/strategy',
|
||||
MARKETING_STRATEGY_START: '/api/marketing/analysis/strategy',
|
||||
|
||||
// SocialMediaCredentialsController - Управление credentials социальных сетей
|
||||
SOCIAL_MEDIA_CREDENTIALS: '/api/social-media/credentials',
|
||||
SOCIAL_MEDIA_CREDENTIALS_BY_PLATFORM: '/api/social-media/credentials'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+10
-5
@@ -248,11 +248,16 @@ const router = createRouter({
|
||||
name: 'marketing-analyses-list',
|
||||
component: () => import('@/views/pages/marketing/MarketingAnalysesList.vue')
|
||||
},
|
||||
{
|
||||
path: '/marketing-analysis/strategies',
|
||||
name: 'marketing-strategies-list',
|
||||
component: () => import('@/views/pages/marketing/MarketingStrategiesList.vue')
|
||||
},
|
||||
{
|
||||
path: '/marketing-analysis/strategies',
|
||||
name: 'marketing-strategies-list',
|
||||
component: () => import('@/views/pages/marketing/MarketingStrategiesList.vue')
|
||||
},
|
||||
{
|
||||
path: '/marketing-analysis/credentials',
|
||||
name: 'marketing-credentials',
|
||||
component: () => import('@/views/pages/marketing/MarketingCredentials.vue')
|
||||
},
|
||||
{
|
||||
path: '/pages/notfound',
|
||||
name: 'notfound',
|
||||
|
||||
@@ -12,6 +12,7 @@ class MarketingService {
|
||||
* @param {string} data.location - Географическая локация работы
|
||||
* @param {string} data.client - Тип целевой аудитории
|
||||
* @param {string} data.differentiator - Уникальные особенности бизнеса
|
||||
* @param {string} data.analysisType - Тип анализа (РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT)
|
||||
* @returns {Promise<Object>} Объект с analysisId и статусом
|
||||
*/
|
||||
async startAnalysis(data) {
|
||||
@@ -23,7 +24,8 @@ class MarketingService {
|
||||
product: data.product,
|
||||
location: data.location,
|
||||
client: data.client,
|
||||
differentiator: data.differentiator
|
||||
differentiator: data.differentiator,
|
||||
analysisType: data.analysisType
|
||||
})
|
||||
});
|
||||
|
||||
@@ -318,6 +320,184 @@ class MarketingService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск стратегии продвижения
|
||||
* POST /api/marketing/analysis/strategy/{strategyId}/start
|
||||
* @param {string} strategyId - Идентификатор стратегии для запуска
|
||||
* @returns {Promise<Object>} Объект с информацией о запуске (tasksCreated, platforms)
|
||||
*/
|
||||
async startStrategy(strategyId) {
|
||||
try {
|
||||
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_STRATEGY_START}/${strategyId}/start`, {
|
||||
method: 'POST',
|
||||
...DEFAULT_REQUEST_CONFIG,
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
const error = {
|
||||
message: result.message || result.error?.message || 'Ошибка при запуске стратегии',
|
||||
code: result.error?.code,
|
||||
details: result.error?.details
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
const error = {
|
||||
message: result.message || 'Ошибка при запуске стратегии',
|
||||
code: result.error?.code,
|
||||
details: result.error?.details
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при запуске стратегии:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранение/обновление credentials для социальной сети
|
||||
* POST /api/social-media/credentials
|
||||
* @param {string} platform - Название платформы (facebook, instagram, etc.)
|
||||
* @param {string} credentials - Access token или другие credentials
|
||||
* @returns {Promise<Object>} Объект с информацией о сохраненных credentials
|
||||
*/
|
||||
async saveCredentials(platform, credentials) {
|
||||
try {
|
||||
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.SOCIAL_MEDIA_CREDENTIALS}`, {
|
||||
method: 'POST',
|
||||
...DEFAULT_REQUEST_CONFIG,
|
||||
body: JSON.stringify({
|
||||
platform: platform,
|
||||
credentials: credentials
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
const error = {
|
||||
message: result.message || result.error?.message || 'Ошибка при сохранении credentials',
|
||||
code: result.error?.code,
|
||||
details: result.error?.details
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
const error = {
|
||||
message: result.message || 'Ошибка при сохранении credentials',
|
||||
code: result.error?.code,
|
||||
details: result.error?.details
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при сохранении credentials:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение информации о credentials для платформы
|
||||
* GET /api/social-media/credentials/{platform}
|
||||
* @param {string} platform - Название платформы
|
||||
* @returns {Promise<Object>} Объект с информацией о наличии credentials
|
||||
*/
|
||||
async getCredentials(platform) {
|
||||
try {
|
||||
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.SOCIAL_MEDIA_CREDENTIALS_BY_PLATFORM}/${platform}`);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || result.error?.message || 'Ошибка при получении credentials');
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || 'Ошибка при получении credentials');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении credentials:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение списка всех credentials
|
||||
* GET /api/social-media/credentials
|
||||
* @returns {Promise<Array>} Массив объектов с информацией о credentials для каждой платформы
|
||||
*/
|
||||
async getAllCredentials() {
|
||||
try {
|
||||
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.SOCIAL_MEDIA_CREDENTIALS}`);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || result.error?.message || 'Ошибка при получении списка credentials');
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || 'Ошибка при получении списка credentials');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении списка credentials:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаление credentials для платформы
|
||||
* DELETE /api/social-media/credentials/{platform}
|
||||
* @param {string} platform - Название платформы
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async deleteCredentials(platform) {
|
||||
try {
|
||||
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.SOCIAL_MEDIA_CREDENTIALS_BY_PLATFORM}/${platform}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
const error = {
|
||||
message: result.message || result.error?.message || 'Ошибка при удалении credentials',
|
||||
code: result.error?.code,
|
||||
details: result.error?.details
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
const error = {
|
||||
message: result.message || 'Ошибка при удалении credentials',
|
||||
code: result.error?.code,
|
||||
details: result.error?.details
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при удалении credentials:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MarketingService();
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
:sortField="'createdAt'"
|
||||
:sortOrder="-1"
|
||||
filterDisplay="row"
|
||||
:globalFilterFields="['product', 'location', 'clientType', 'status']"
|
||||
:globalFilterFields="['product', 'location', 'clientType', 'status', 'analysisType']"
|
||||
v-model:filters="filters"
|
||||
:filters="filters"
|
||||
dataKey="analysisId"
|
||||
@@ -54,6 +54,13 @@
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="analysisType" header="Тип анализа" :sortable="true" style="min-width: 180px">
|
||||
<template #body="slotProps">
|
||||
<Tag v-if="slotProps.data.analysisType" :value="getAnalysisTypeLabel(slotProps.data.analysisType)" severity="secondary" />
|
||||
<span v-else class="text-surface-400">—</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="status" header="Статус" :sortable="true" style="min-width: 120px">
|
||||
<template #body="slotProps">
|
||||
<Tag :value="getStatusLabel(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
|
||||
@@ -309,6 +316,18 @@ const getStatusIcon = (status) => {
|
||||
return icons[status] || 'pi pi-circle';
|
||||
};
|
||||
|
||||
// Get analysis type label
|
||||
const getAnalysisTypeLabel = (type) => {
|
||||
const labels = {
|
||||
РЫНОК: 'Анализ рынка',
|
||||
КОНКУРЕНТЫ: 'Анализ конкурентов',
|
||||
ЦА: 'Анализ целевой аудитории',
|
||||
КАНАЛЫ: 'Анализ маркетинговых каналов',
|
||||
SWOT: 'SWOT-анализ'
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
|
||||
// Load on mount
|
||||
onMounted(() => {
|
||||
loadAnalyses();
|
||||
|
||||
@@ -33,6 +33,22 @@
|
||||
<small v-if="errors.differentiator" class="p-error">{{ errors.differentiator }}</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="analysisType" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Тип анализа </label>
|
||||
<Dropdown
|
||||
id="analysisType"
|
||||
v-model="formData.analysisType"
|
||||
:options="analysisTypeOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Выберите тип анализа"
|
||||
class="w-full"
|
||||
:class="{ 'p-invalid': errors.analysisType }"
|
||||
/>
|
||||
<small v-if="errors.analysisType" class="p-error">{{ errors.analysisType }}</small>
|
||||
<small class="text-surface-500 dark:text-surface-400 mt-1 block">Выберите тип анализа, который будет сгенерирован</small>
|
||||
</div>
|
||||
|
||||
<Button type="submit" label="Начать анализ" icon="pi pi-send" class="w-full p-button-primary" :loading="submitting" />
|
||||
|
||||
<p class="text-sm text-surface-500 dark:text-surface-400 mt-3 text-center">На основе ваших ответов будет создан подробный и полезный маркетинговый отчет</p>
|
||||
@@ -77,7 +93,12 @@
|
||||
<div class="card">
|
||||
<div class="card-header mb-4">
|
||||
<div class="flex justify-content-between align-items-center">
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Результаты анализа</h2>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Результаты анализа</h2>
|
||||
<div v-if="currentAnalysisType" class="mt-2">
|
||||
<Tag :value="getAnalysisTypeLabel(currentAnalysisType)" severity="info" />
|
||||
</div>
|
||||
</div>
|
||||
<Button label="Скачать PDF" icon="pi pi-download" @click="handleDownloadPdf" :loading="downloadingPdf" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,22 +200,32 @@ import ProgressSpinner from 'primevue/progressspinner';
|
||||
import Tag from 'primevue/tag';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { onBeforeUnmount, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const toast = useToast();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// Form data
|
||||
const formData = ref({
|
||||
product: '',
|
||||
location: '',
|
||||
client: null,
|
||||
differentiator: ''
|
||||
differentiator: '',
|
||||
analysisType: null
|
||||
});
|
||||
|
||||
const clientOptions = ['B2B клиенты', 'B2C клиенты', 'Частные лица', 'Корпорации', 'Малый бизнес'];
|
||||
|
||||
const analysisTypeOptions = [
|
||||
{ label: 'РЫНОК - Анализ рынка', value: 'РЫНОК' },
|
||||
{ label: 'КОНКУРЕНТЫ - Анализ конкурентов', value: 'КОНКУРЕНТЫ' },
|
||||
{ label: 'ЦА - Анализ целевой аудитории', value: 'ЦА' },
|
||||
{ label: 'КАНАЛЫ - Анализ маркетинговых каналов', value: 'КАНАЛЫ' },
|
||||
{ label: 'SWOT - SWOT-анализ', value: 'SWOT' }
|
||||
];
|
||||
|
||||
const submitting = ref(false);
|
||||
const errors = ref({});
|
||||
|
||||
@@ -202,6 +233,7 @@ const errors = ref({});
|
||||
const analysisId = ref(null);
|
||||
const status = ref(null);
|
||||
const report = ref(null);
|
||||
const currentAnalysisType = ref(null);
|
||||
const estimatedCompletionTime = ref(null);
|
||||
const pollingInterval = ref(null);
|
||||
const pollingAttempts = ref(0);
|
||||
@@ -241,6 +273,16 @@ const validateForm = () => {
|
||||
errors.value.differentiator = 'Описание уникальности не должно превышать 500 символов';
|
||||
}
|
||||
|
||||
// AnalysisType validation
|
||||
if (!formData.value.analysisType) {
|
||||
errors.value.analysisType = 'Пожалуйста, выберите тип анализа';
|
||||
} else {
|
||||
const validTypes = ['РЫНОК', 'КОНКУРЕНТЫ', 'ЦА', 'КАНАЛЫ', 'SWOT'];
|
||||
if (!validTypes.includes(formData.value.analysisType)) {
|
||||
errors.value.analysisType = 'Выбран недопустимый тип анализа';
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(errors.value).length === 0;
|
||||
};
|
||||
|
||||
@@ -268,6 +310,10 @@ const startPolling = (id) => {
|
||||
if (result.status === 'completed' && result.report) {
|
||||
stopPolling();
|
||||
report.value = result.report;
|
||||
// Сохраняем тип анализа из результата, если он есть
|
||||
if (result.analysisType) {
|
||||
currentAnalysisType.value = result.analysisType;
|
||||
}
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Анализ завершен',
|
||||
@@ -317,11 +363,13 @@ const handleSubmit = async () => {
|
||||
product: formData.value.product.trim(),
|
||||
location: formData.value.location.trim(),
|
||||
client: formData.value.client,
|
||||
differentiator: formData.value.differentiator.trim()
|
||||
differentiator: formData.value.differentiator.trim(),
|
||||
analysisType: formData.value.analysisType
|
||||
});
|
||||
|
||||
analysisId.value = result.analysisId;
|
||||
status.value = result.status;
|
||||
currentAnalysisType.value = formData.value.analysisType;
|
||||
estimatedCompletionTime.value = result.estimatedCompletionTime;
|
||||
|
||||
toast.add({
|
||||
@@ -379,13 +427,15 @@ const resetAnalysis = () => {
|
||||
analysisId.value = null;
|
||||
status.value = null;
|
||||
report.value = null;
|
||||
currentAnalysisType.value = null;
|
||||
estimatedCompletionTime.value = null;
|
||||
pollingAttempts.value = 0;
|
||||
formData.value = {
|
||||
product: '',
|
||||
location: '',
|
||||
client: null,
|
||||
differentiator: ''
|
||||
differentiator: '',
|
||||
analysisType: null
|
||||
};
|
||||
errors.value = {};
|
||||
};
|
||||
@@ -407,6 +457,18 @@ const renderMarkdown = (markdown) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Get analysis type label
|
||||
const getAnalysisTypeLabel = (type) => {
|
||||
const labels = {
|
||||
РЫНОК: 'Анализ рынка',
|
||||
КОНКУРЕНТЫ: 'Анализ конкурентов',
|
||||
ЦА: 'Анализ целевой аудитории',
|
||||
КАНАЛЫ: 'Анализ маркетинговых каналов',
|
||||
SWOT: 'SWOT-анализ'
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
|
||||
// Format date time
|
||||
const formatDateTime = (dateString) => {
|
||||
if (!dateString) return '';
|
||||
@@ -438,6 +500,39 @@ const goToStrategy = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Load analysis from query params
|
||||
onMounted(async () => {
|
||||
const queryAnalysisId = route.query.analysisId;
|
||||
if (queryAnalysisId) {
|
||||
// Try to load existing analysis
|
||||
try {
|
||||
analysisId.value = queryAnalysisId;
|
||||
const result = await MarketingService.getAnalysisResult(queryAnalysisId);
|
||||
status.value = result.status;
|
||||
|
||||
// Сохраняем тип анализа, если он есть
|
||||
if (result.analysisType) {
|
||||
currentAnalysisType.value = result.analysisType;
|
||||
}
|
||||
|
||||
if (result.status === 'completed' && result.report) {
|
||||
report.value = result.report;
|
||||
} else if (result.status === 'processing' || result.status === 'queued') {
|
||||
estimatedCompletionTime.value = result.estimatedCompletionTime;
|
||||
startPolling(queryAnalysisId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке анализа:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: error.message || 'Не удалось загрузить анализ',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup on unmount
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling();
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
<template>
|
||||
<div class="marketing-credentials-page bg-surface-50 dark:bg-surface-900 min-h-screen p-6">
|
||||
<Toast />
|
||||
<div class="max-w-[1200px] mx-auto">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header mb-4">
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Управление credentials социальных сетей</h2>
|
||||
<p class="text-sm text-surface-500 dark:text-surface-400 mt-2">Настройте access tokens для платформ, на которых вы хотите автоматически публиковать контент</p>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<form @submit.prevent="handleSaveCredentials" class="space-y-4">
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-4">
|
||||
<div class="field">
|
||||
<label for="platform" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300">
|
||||
Платформа <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<Dropdown
|
||||
id="platform"
|
||||
v-model="formData.platform"
|
||||
:options="platformOptions"
|
||||
placeholder="Выберите платформу"
|
||||
class="w-full"
|
||||
:class="{ 'p-invalid': errors.platform }"
|
||||
/>
|
||||
<small v-if="errors.platform" class="p-error">{{ errors.platform }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="field">
|
||||
<label for="credentials" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300">
|
||||
Access Token <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<InputText
|
||||
id="credentials"
|
||||
v-model="formData.credentials"
|
||||
type="password"
|
||||
placeholder="Введите access token"
|
||||
class="w-full"
|
||||
:class="{ 'p-invalid': errors.credentials }"
|
||||
/>
|
||||
<small v-if="errors.credentials" class="p-error">{{ errors.credentials }}</small>
|
||||
<small class="text-surface-500 dark:text-surface-400 mt-1 block">
|
||||
Токен будет зашифрован перед сохранением
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-2">
|
||||
<div class="field">
|
||||
<label class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> </label>
|
||||
<Button
|
||||
type="submit"
|
||||
label="Сохранить"
|
||||
icon="pi pi-save"
|
||||
class="w-full"
|
||||
:loading="saving"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header mb-4">
|
||||
<div class="flex justify-content-between align-items-center">
|
||||
<h3 class="text-xl font-semibold text-surface-900 dark:text-surface-0">Настроенные платформы</h3>
|
||||
<Button label="Обновить" icon="pi pi-refresh" severity="secondary" @click="loadCredentials" :loading="loading" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<DataTable
|
||||
:value="credentials"
|
||||
:loading="loading"
|
||||
paginator
|
||||
:rows="10"
|
||||
:rowsPerPageOptions="[5, 10, 20]"
|
||||
dataKey="platform"
|
||||
:emptyMessage="'Нет настроенных платформ'"
|
||||
>
|
||||
<Column field="platform" header="Платформа" :sortable="true" style="min-width: 150px">
|
||||
<template #body="slotProps">
|
||||
<Tag :value="slotProps.data.platform" severity="info" />
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="hasCredentials" header="Статус" :sortable="true" style="min-width: 120px">
|
||||
<template #body="slotProps">
|
||||
<Tag
|
||||
:value="slotProps.data.hasCredentials ? 'Настроено' : 'Не настроено'"
|
||||
:severity="slotProps.data.hasCredentials ? 'success' : 'warning'"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="createdAt" header="Создано" :sortable="true" style="min-width: 180px">
|
||||
<template #body="slotProps">
|
||||
<span v-if="slotProps.data.createdAt">{{ formatDateTime(slotProps.data.createdAt) }}</span>
|
||||
<span v-else class="text-surface-400">—</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="updatedAt" header="Обновлено" :sortable="true" style="min-width: 180px">
|
||||
<template #body="slotProps">
|
||||
<span v-if="slotProps.data.updatedAt">{{ formatDateTime(slotProps.data.updatedAt) }}</span>
|
||||
<span v-else class="text-surface-400">—</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Действия" style="min-width: 150px">
|
||||
<template #body="slotProps">
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
size="small"
|
||||
v-tooltip.top="'Удалить credentials'"
|
||||
@click="confirmDelete(slotProps.data.platform)"
|
||||
:disabled="!slotProps.data.hasCredentials"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Диалог подтверждения удаления -->
|
||||
<Dialog
|
||||
v-model:visible="deleteDialogVisible"
|
||||
:header="'Подтверждение удаления'"
|
||||
:style="{ width: '450px' }"
|
||||
:modal="true"
|
||||
>
|
||||
<div class="confirmation-content">
|
||||
<i class="pi pi-exclamation-triangle mr-3" style="font-size: 2rem" />
|
||||
<span>Вы уверены, что хотите удалить credentials для платформы <b>{{ platformToDelete }}</b>?</span>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="Отмена" icon="pi pi-times" @click="deleteDialogVisible = false" severity="secondary" />
|
||||
<Button label="Удалить" icon="pi pi-check" @click="handleDeleteCredentials" severity="danger" :loading="deleting" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MarketingService from '@/service/MarketingService';
|
||||
import Button from 'primevue/button';
|
||||
import Column from 'primevue/column';
|
||||
import DataTable from 'primevue/datatable';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Tag from 'primevue/tag';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
// Form data
|
||||
const formData = ref({
|
||||
platform: null,
|
||||
credentials: ''
|
||||
});
|
||||
|
||||
const platformOptions = ['facebook', 'instagram', 'linkedin', 'telegram', 'tiktok', 'youtube'];
|
||||
|
||||
const saving = ref(false);
|
||||
const errors = ref({});
|
||||
|
||||
// Data
|
||||
const credentials = ref([]);
|
||||
const loading = ref(false);
|
||||
const deleteDialogVisible = ref(false);
|
||||
const platformToDelete = ref(null);
|
||||
const deleting = ref(false);
|
||||
|
||||
// Validation
|
||||
const validateForm = () => {
|
||||
errors.value = {};
|
||||
|
||||
if (!formData.value.platform) {
|
||||
errors.value.platform = 'Пожалуйста, выберите платформу';
|
||||
}
|
||||
|
||||
if (!formData.value.credentials || formData.value.credentials.trim().length === 0) {
|
||||
errors.value.credentials = 'Access token обязателен';
|
||||
} else if (formData.value.credentials.trim().length < 10) {
|
||||
errors.value.credentials = 'Access token слишком короткий';
|
||||
}
|
||||
|
||||
return Object.keys(errors.value).length === 0;
|
||||
};
|
||||
|
||||
// Load credentials
|
||||
const loadCredentials = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await MarketingService.getAllCredentials();
|
||||
credentials.value = data || [];
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке credentials:', error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: error.message || 'Не удалось загрузить список credentials',
|
||||
life: 5000
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle save credentials
|
||||
const handleSaveCredentials = async () => {
|
||||
if (!validateForm()) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка валидации',
|
||||
detail: 'Пожалуйста, исправьте ошибки в форме',
|
||||
life: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
await MarketingService.saveCredentials(formData.value.platform, formData.value.credentials.trim());
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Успешно',
|
||||
detail: `Credentials для ${formData.value.platform} успешно сохранены`,
|
||||
life: 3000
|
||||
});
|
||||
|
||||
// Reset form
|
||||
formData.value = {
|
||||
platform: null,
|
||||
credentials: ''
|
||||
};
|
||||
errors.value = {};
|
||||
|
||||
// Reload credentials list
|
||||
await loadCredentials();
|
||||
} catch (error) {
|
||||
console.error('Ошибка при сохранении credentials:', error);
|
||||
|
||||
// Handle specific error codes
|
||||
if (error.code === 'VALIDATION_ERROR') {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка валидации',
|
||||
detail: error.message || 'Проверьте правильность введенных данных',
|
||||
life: 5000
|
||||
});
|
||||
} else if (error.code === 'UNAUTHORIZED') {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка авторизации',
|
||||
detail: 'Необходимо войти в систему',
|
||||
life: 5000
|
||||
});
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: error.message || 'Не удалось сохранить credentials',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Confirm delete
|
||||
const confirmDelete = (platform) => {
|
||||
platformToDelete.value = platform;
|
||||
deleteDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// Handle delete credentials
|
||||
const handleDeleteCredentials = async () => {
|
||||
if (!platformToDelete.value) return;
|
||||
|
||||
deleting.value = true;
|
||||
|
||||
try {
|
||||
await MarketingService.deleteCredentials(platformToDelete.value);
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Успешно',
|
||||
detail: `Credentials для ${platformToDelete.value} успешно удалены`,
|
||||
life: 3000
|
||||
});
|
||||
|
||||
deleteDialogVisible.value = false;
|
||||
platformToDelete.value = null;
|
||||
|
||||
// Reload credentials list
|
||||
await loadCredentials();
|
||||
} catch (error) {
|
||||
console.error('Ошибка при удалении credentials:', error);
|
||||
|
||||
if (error.code === 'NOT_FOUND') {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Не найдено',
|
||||
detail: `Credentials для ${platformToDelete.value} не найдены`,
|
||||
life: 5000
|
||||
});
|
||||
} else if (error.code === 'UNAUTHORIZED') {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка авторизации',
|
||||
detail: 'Необходимо войти в систему',
|
||||
life: 5000
|
||||
});
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: error.message || 'Не удалось удалить credentials',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Format date time
|
||||
const formatDateTime = (dateString) => {
|
||||
if (!dateString) return '—';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
// Load on mount
|
||||
onMounted(() => {
|
||||
loadCredentials();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.marketing-credentials-page {
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--surface-card);
|
||||
border-radius: 12px;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.12),
|
||||
0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.confirmation-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -72,6 +72,13 @@ const menuItems = [
|
||||
iconColor: 'text-primary-500',
|
||||
route: '/marketing-analysis/strategies'
|
||||
},
|
||||
{
|
||||
id: 'credentials',
|
||||
label: 'Credentials социальных сетей',
|
||||
icon: 'pi pi-key',
|
||||
iconColor: 'text-primary-500',
|
||||
route: '/marketing-analysis/credentials'
|
||||
},
|
||||
{
|
||||
id: 'results',
|
||||
label: 'Результаты',
|
||||
|
||||
@@ -77,7 +77,17 @@
|
||||
<h2 class="text-2xl font-bold text-surface-900 dark:text-surface-0">Стратегия продвижения</h2>
|
||||
<p class="text-sm text-surface-500 dark:text-surface-400 mt-1">Длительность: {{ strategyData.durationWeeks }} {{ pluralize(strategyData.durationWeeks, 'неделя', 'недели', 'недель') }}</p>
|
||||
</div>
|
||||
<Button label="Создать новую стратегию" icon="pi pi-plus" severity="secondary" @click="resetStrategy" />
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
label="Запустить стратегию"
|
||||
icon="pi pi-play"
|
||||
severity="success"
|
||||
@click="handleStartStrategy"
|
||||
:loading="startingStrategy"
|
||||
:disabled="startingStrategy"
|
||||
/>
|
||||
<Button label="Создать новую стратегию" icon="pi pi-plus" severity="secondary" @click="resetStrategy" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -215,12 +225,48 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Диалог для настройки credentials -->
|
||||
<Dialog
|
||||
v-model:visible="credentialsDialogVisible"
|
||||
:header="'Настройка credentials'"
|
||||
:style="{ width: '600px' }"
|
||||
:modal="true"
|
||||
>
|
||||
<div class="credentials-dialog-content">
|
||||
<p class="text-surface-700 dark:text-surface-300 mb-4">
|
||||
Для запуска стратегии необходимо настроить credentials для следующих платформ:
|
||||
</p>
|
||||
<div class="mb-4">
|
||||
<Tag
|
||||
v-for="platform in missingPlatforms"
|
||||
:key="platform"
|
||||
:value="platform"
|
||||
severity="warning"
|
||||
class="mr-2 mb-2"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-surface-600 dark:text-surface-400 text-sm">
|
||||
Перейдите на страницу управления credentials для настройки необходимых платформ.
|
||||
</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="Отмена" icon="pi pi-times" @click="credentialsDialogVisible = false" severity="secondary" />
|
||||
<Button
|
||||
label="Настроить credentials"
|
||||
icon="pi pi-cog"
|
||||
@click="goToCredentials"
|
||||
severity="info"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MarketingService from '@/service/MarketingService';
|
||||
import Button from 'primevue/button';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import MultiSelect from 'primevue/multiselect';
|
||||
@@ -229,10 +275,11 @@ import Tag from 'primevue/tag';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const toast = useToast();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// Form data
|
||||
const formData = ref({
|
||||
@@ -255,6 +302,9 @@ const pollingAttempts = ref(0);
|
||||
const maxPollingAttempts = 60; // 10 minutes with 10 second intervals
|
||||
const pollingIntervalMs = 10000; // 10 seconds
|
||||
const groupByDate = ref(true);
|
||||
const startingStrategy = ref(false);
|
||||
const credentialsDialogVisible = ref(false);
|
||||
const missingPlatforms = ref([]);
|
||||
|
||||
// Computed
|
||||
const postsByDate = computed(() => {
|
||||
@@ -462,6 +512,98 @@ const toggleGroupByDate = () => {
|
||||
groupByDate.value = !groupByDate.value;
|
||||
};
|
||||
|
||||
// Handle start strategy
|
||||
const handleStartStrategy = async () => {
|
||||
if (!strategyId.value) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: 'ID стратегии не найден',
|
||||
life: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
startingStrategy.value = true;
|
||||
|
||||
try {
|
||||
const result = await MarketingService.startStrategy(strategyId.value);
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Стратегия запущена',
|
||||
detail: `Стратегия успешно запущена. Создано задач: ${result.tasksCreated}. Платформы: ${result.platforms?.join(', ') || 'N/A'}`,
|
||||
life: 5000
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка при запуске стратегии:', error);
|
||||
|
||||
// Handle specific error codes
|
||||
if (error.code === 'MISSING_CREDENTIALS') {
|
||||
// Extract platform from error message (format: "Credentials not found for platform: facebook")
|
||||
const platformMatch = error.message?.match(/platform:\s*(\w+)/i) || error.message?.match(/for platform\s+(\w+)/i);
|
||||
if (platformMatch) {
|
||||
missingPlatforms.value = [platformMatch[1].toLowerCase()];
|
||||
} else {
|
||||
// Try to get platforms from strategy data
|
||||
const platforms = strategyData.value?.priorityPlatforms || strategyData.value?.strategy?.postCalendar?.map(p => p.platform).filter((v, i, a) => a.indexOf(v) === i) || [];
|
||||
missingPlatforms.value = platforms.length > 0 ? platforms : ['facebook']; // Default to facebook if unknown
|
||||
}
|
||||
credentialsDialogVisible.value = true;
|
||||
} else if (error.code === 'INVALID_STATUS') {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Стратегия не готова',
|
||||
detail: error.message || 'Стратегия еще не завершена. Дождитесь завершения генерации.',
|
||||
life: 5000
|
||||
});
|
||||
} else if (error.code === 'UNAUTHORIZED') {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка авторизации',
|
||||
detail: 'Необходимо войти в систему',
|
||||
life: 5000
|
||||
});
|
||||
} else if (error.code === 'NOT_FOUND') {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Не найдено',
|
||||
detail: 'Стратегия не найдена',
|
||||
life: 5000
|
||||
});
|
||||
} else if (error.code === 'FORBIDDEN') {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Доступ запрещен',
|
||||
detail: 'У вас нет доступа к этой стратегии',
|
||||
life: 5000
|
||||
});
|
||||
} else if (error.code === 'VALIDATION_ERROR') {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка валидации',
|
||||
detail: error.message || 'Проверьте правильность данных',
|
||||
life: 5000
|
||||
});
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка',
|
||||
detail: error.message || 'Не удалось запустить стратегию. Попробуйте позже.',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
startingStrategy.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Go to credentials page
|
||||
const goToCredentials = () => {
|
||||
credentialsDialogVisible.value = false;
|
||||
router.push({ name: 'marketing-credentials' });
|
||||
};
|
||||
|
||||
// Format date
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
|
||||
Reference in New Issue
Block a user