.
This commit is contained in:
@@ -0,0 +1,700 @@
|
||||
# API Документация: Обновленные поля для генерации бизнес-анализа (Frontend/AI Agent)
|
||||
|
||||
## Обзор изменений
|
||||
|
||||
API для генерации маркетингового анализа был обновлен с новыми полями, которые более точно отражают требования бизнес-анализа. Все старые поля были заменены новыми для улучшения качества анализа.
|
||||
|
||||
---
|
||||
|
||||
## Изменения в структуре запроса
|
||||
|
||||
### Удаленные поля (больше не используются)
|
||||
|
||||
Следующие поля были **удалены** из API и больше не принимаются:
|
||||
|
||||
- ❌ `location` - заменено на `region`
|
||||
- ❌ `client` - заменено на `targetAudience`
|
||||
- ❌ `differentiator` - заменено на комбинацию `businessNiche`, `strongSide`, `weakSide`
|
||||
|
||||
### Новые обязательные поля
|
||||
|
||||
| Поле | Тип | Обязательный | Описание | Пример значения |
|
||||
| ---------------- | ------ | ------------ | -------------------------------------- | ------------------------------------------------------ |
|
||||
| `businessNiche` | string | ✅ | Ниша бизнеса | "E-commerce платформы" |
|
||||
| `product` | string | ✅ | Продукт или услуга | "Разработка мобильных приложений" |
|
||||
| `targetAudience` | string | ✅ | Целевая аудитория (детальное описание) | "Малый и средний бизнес, владельцы интернет-магазинов" |
|
||||
| `region` | string | ✅ | Регион (город Казахстана) | "Алматы" |
|
||||
| `goal` | string | ✅ | Цель на 6-12 месяцев | "Увеличить количество клиентов на 50%" |
|
||||
| `detailLevel` | string | ✅ | Уровень детализации анализа | "СТАНДАРТНО" |
|
||||
| `analysisType` | string | ✅ | Тип анализа | "РЫНОК" |
|
||||
|
||||
### Новые опциональные поля
|
||||
|
||||
| Поле | Тип | Обязательный | Описание | Пример значения |
|
||||
| ------------ | ------ | ------------ | ----------------------- | ------------------------------------------------- |
|
||||
| `strongSide` | string | ❌ | Сильная сторона бизнеса | "Опытная команда разработчиков, быстрая доставка" |
|
||||
| `weakSide` | string | ❌ | Слабая сторона бизнеса | "Ограниченный маркетинговый бюджет" |
|
||||
|
||||
---
|
||||
|
||||
## Детальное описание полей
|
||||
|
||||
### 1. `businessNiche` (обязательное)
|
||||
|
||||
**Описание**: Ниша бизнеса, в которой работает компания.
|
||||
|
||||
**Валидация**:
|
||||
|
||||
- Минимальная длина: 3 символа
|
||||
- Максимальная длина: 200 символов
|
||||
- Разрешены: буквы, цифры, пробелы, дефисы, запятые
|
||||
- Паттерн: `^[\p{L}\p{N}\s\-,]+$`
|
||||
|
||||
**Примеры**:
|
||||
|
||||
```json
|
||||
"businessNiche": "E-commerce платформы"
|
||||
"businessNiche": "Образовательные технологии"
|
||||
"businessNiche": "Финансовые услуги для малого бизнеса"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `product` (обязательное)
|
||||
|
||||
**Описание**: Конкретный продукт или услуга, которую предоставляет компания.
|
||||
|
||||
**Валидация**:
|
||||
|
||||
- Минимальная длина: 3 символа
|
||||
- Максимальная длина: 200 символов
|
||||
- Разрешены: буквы, цифры, пробелы, дефисы, запятые
|
||||
- Паттерн: `^[\p{L}\p{N}\s\-,]+$`
|
||||
|
||||
**Примеры**:
|
||||
|
||||
```json
|
||||
"product": "Разработка мобильных приложений"
|
||||
"product": "Консультации по маркетингу"
|
||||
"product": "Веб-разработка и дизайн"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. `targetAudience` (обязательное)
|
||||
|
||||
**Описание**: Детальное описание целевой аудитории. Это поле заменяет старое поле `client` и должно содержать более подробную информацию.
|
||||
|
||||
**Валидация**:
|
||||
|
||||
- Минимальная длина: 3 символа
|
||||
- Максимальная длина: 300 символов
|
||||
- Разрешены любые символы
|
||||
|
||||
**Примеры**:
|
||||
|
||||
```json
|
||||
"targetAudience": "Малый и средний бизнес, владельцы интернет-магазинов в возрасте 30-50 лет"
|
||||
"targetAudience": "Стартапы и технологические компании, нуждающиеся в быстрой разработке MVP"
|
||||
"targetAudience": "Частные лица, желающие создать личный бренд в социальных сетях"
|
||||
```
|
||||
|
||||
**Примечание**: В отличие от старого поля `client`, которое принимало только фиксированные значения, `targetAudience` принимает свободный текст для более гибкого описания.
|
||||
|
||||
---
|
||||
|
||||
### 4. `region` (обязательное)
|
||||
|
||||
**Описание**: Регион (город Казахстана), в котором работает бизнес. Это поле заменяет старое поле `location`.
|
||||
|
||||
**Валидация**:
|
||||
|
||||
- Должно быть одним из допустимых городов Казахстана
|
||||
- Проверка выполняется через валидатор `@ValidRegion`
|
||||
|
||||
**Допустимые значения** (точное совпадение):
|
||||
|
||||
- `"Алматы"`
|
||||
- `"Астана"`
|
||||
- `"Шымкент"`
|
||||
- `"Караганда"`
|
||||
- `"Актобе"`
|
||||
- `"Тараз"`
|
||||
- `"Павлодар"`
|
||||
- `"Усть-Каменогорск"`
|
||||
- `"Семей"`
|
||||
- `"Костанай"`
|
||||
- `"Кызылорда"`
|
||||
- `"Уральск"`
|
||||
- `"Петропавловск"`
|
||||
- `"Атырау"`
|
||||
- `"Актау"`
|
||||
- `"Туркестан"`
|
||||
- `"Кокшетау"`
|
||||
- `"Талдыкорган"`
|
||||
- `"Экибастуз"`
|
||||
- `"Рудный"`
|
||||
|
||||
**Примеры**:
|
||||
|
||||
```json
|
||||
"region": "Алматы"
|
||||
"region": "Астана"
|
||||
"region": "Шымкент"
|
||||
```
|
||||
|
||||
**Важно**: Значение должно точно совпадать с одним из допустимых городов (регистр важен).
|
||||
|
||||
---
|
||||
|
||||
### 5. `goal` (обязательное)
|
||||
|
||||
**Описание**: Цель бизнеса на период 6-12 месяцев. Это новое поле, которое помогает AI лучше понять приоритеты бизнеса.
|
||||
|
||||
**Валидация**:
|
||||
|
||||
- Минимальная длина: 10 символов
|
||||
- Максимальная длина: 500 символов
|
||||
- Разрешены любые символы
|
||||
|
||||
**Примеры**:
|
||||
|
||||
```json
|
||||
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев"
|
||||
"goal": "Выйти на рынок соседних регионов и открыть 3 новых филиала"
|
||||
"goal": "Повысить узнаваемость бренда и увеличить продажи через онлайн-каналы на 30%"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. `detailLevel` (обязательное)
|
||||
|
||||
**Описание**: Уровень детализации анализа. Влияет на объем и глубину генерируемого анализа.
|
||||
|
||||
**Валидация**:
|
||||
|
||||
- Должно быть одним из допустимых значений
|
||||
- Проверка выполняется через валидатор `@ValidDetailLevel`
|
||||
|
||||
**Допустимые значения** (точное совпадение, регистр важен):
|
||||
|
||||
- `"КРАТКО"` - Краткий анализ (1-2 абзаца)
|
||||
- `"СТАНДАРТНО"` - Стандартный анализ (3-5 абзацев) - **рекомендуется по умолчанию**
|
||||
- `"ПОДРОБНО"` - Подробный анализ (5-8 абзацев)
|
||||
|
||||
**Влияние на анализ**:
|
||||
|
||||
| Уровень | Длина ответов | Количество рекомендаций | Детализация стратегии |
|
||||
| ------------ | ------------- | ----------------------- | ------------------------ |
|
||||
| `КРАТКО` | 1-2 абзаца | 3-4 рекомендации | Краткая |
|
||||
| `СТАНДАРТНО` | 3-5 абзацев | 4-6 рекомендаций | Стандартная |
|
||||
| `ПОДРОБНО` | 5-8 абзацев | 6-8 рекомендаций | Детальная с обоснованием |
|
||||
|
||||
**Примеры**:
|
||||
|
||||
```json
|
||||
"detailLevel": "КРАТКО"
|
||||
"detailLevel": "СТАНДАРТНО"
|
||||
"detailLevel": "ПОДРОБНО"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. `strongSide` (опциональное)
|
||||
|
||||
**Описание**: Сильная сторона бизнеса. Помогает AI лучше понять конкурентные преимущества.
|
||||
|
||||
**Валидация**:
|
||||
|
||||
- Максимальная длина: 500 символов
|
||||
- Разрешены любые символы
|
||||
- Может быть пустым или отсутствовать
|
||||
|
||||
**Примеры**:
|
||||
|
||||
```json
|
||||
"strongSide": "Опытная команда разработчиков с 10+ летним опытом, быстрая доставка проектов"
|
||||
"strongSide": "Уникальная технология, низкие цены, отличная поддержка клиентов"
|
||||
```
|
||||
|
||||
**Примечание**: Если поле не указано, AI будет анализировать сильные стороны на основе других данных.
|
||||
|
||||
---
|
||||
|
||||
### 8. `weakSide` (опциональное)
|
||||
|
||||
**Описание**: Слабая сторона бизнеса. Помогает AI лучше понять области для улучшения.
|
||||
|
||||
**Валидация**:
|
||||
|
||||
- Максимальная длина: 500 символов
|
||||
- Разрешены любые символы
|
||||
- Может быть пустым или отсутствовать
|
||||
|
||||
**Примеры**:
|
||||
|
||||
```json
|
||||
"weakSide": "Ограниченный маркетинговый бюджет, недостаточная узнаваемость бренда"
|
||||
"weakSide": "Небольшая команда, ограниченные ресурсы для масштабирования"
|
||||
```
|
||||
|
||||
**Примечание**: Если поле не указано, AI будет анализировать слабые стороны на основе других данных.
|
||||
|
||||
---
|
||||
|
||||
### 9. `analysisType` (обязательное)
|
||||
|
||||
**Описание**: Тип анализа, который необходимо провести. Поле осталось без изменений.
|
||||
|
||||
**Допустимые значения**:
|
||||
|
||||
- `"РЫНОК"` - Анализ рынка
|
||||
- `"КОНКУРЕНТЫ"` - Анализ конкурентов
|
||||
- `"ЦА"` - Анализ целевой аудитории
|
||||
- `"КАНАЛЫ"` - Анализ маркетинговых каналов
|
||||
- `"SWOT"` - SWOT-анализ
|
||||
|
||||
---
|
||||
|
||||
## Полный пример запроса
|
||||
|
||||
### POST `/api/marketing/analysis/start`
|
||||
|
||||
```json
|
||||
{
|
||||
"businessNiche": "E-commerce платформы",
|
||||
"product": "Разработка мобильных приложений для интернет-магазинов",
|
||||
"targetAudience": "Малый и средний бизнес, владельцы интернет-магазинов в возрасте 30-50 лет, нуждающиеся в мобильных решениях",
|
||||
"region": "Алматы",
|
||||
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев и выйти на рынок соседних регионов",
|
||||
"detailLevel": "СТАНДАРТНО",
|
||||
"strongSide": "Опытная команда разработчиков с 10+ летним опытом, быстрая доставка проектов, использование современных технологий",
|
||||
"weakSide": "Ограниченный маркетинговый бюджет, недостаточная узнаваемость бренда в новых регионах",
|
||||
"analysisType": "РЫНОК"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Примеры использования на фронтенде
|
||||
|
||||
### JavaScript/TypeScript
|
||||
|
||||
```typescript
|
||||
interface MarketingAnalysisRequest {
|
||||
businessNiche: string;
|
||||
product: string;
|
||||
targetAudience: string;
|
||||
region: string;
|
||||
goal: string;
|
||||
detailLevel: 'КРАТКО' | 'СТАНДАРТНО' | 'ПОДРОБНО';
|
||||
strongSide?: string;
|
||||
weakSide?: string;
|
||||
analysisType: 'РЫНОК' | 'КОНКУРЕНТЫ' | 'ЦА' | 'КАНАЛЫ' | 'SWOT';
|
||||
}
|
||||
|
||||
// Список допустимых регионов
|
||||
const VALID_REGIONS = [
|
||||
'Алматы',
|
||||
'Астана',
|
||||
'Шымкент',
|
||||
'Караганда',
|
||||
'Актобе',
|
||||
'Тараз',
|
||||
'Павлодар',
|
||||
'Усть-Каменогорск',
|
||||
'Семей',
|
||||
'Костанай',
|
||||
'Кызылорда',
|
||||
'Уральск',
|
||||
'Петропавловск',
|
||||
'Атырау',
|
||||
'Актау',
|
||||
'Туркестан',
|
||||
'Кокшетау',
|
||||
'Талдыкорган',
|
||||
'Экибастуз',
|
||||
'Рудный',
|
||||
];
|
||||
|
||||
// Список уровней детализации
|
||||
const DETAIL_LEVELS = ['КРАТКО', 'СТАНДАРТНО', 'ПОДРОБНО'] as const;
|
||||
|
||||
// Функция для отправки запроса
|
||||
async function startMarketingAnalysis(
|
||||
data: MarketingAnalysisRequest
|
||||
): Promise<string> {
|
||||
const response = await fetch(
|
||||
'https://api.konturai.kz/api/marketing/analysis/start',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer YOUR_JWT_TOKEN', // Если требуется
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error?.message || 'Failed to start analysis');
|
||||
}
|
||||
|
||||
return result.data.analysisId;
|
||||
}
|
||||
|
||||
// Пример использования
|
||||
const analysisId = await startMarketingAnalysis({
|
||||
businessNiche: 'E-commerce платформы',
|
||||
product: 'Разработка мобильных приложений',
|
||||
targetAudience: 'Малый и средний бизнес, владельцы интернет-магазинов',
|
||||
region: 'Алматы',
|
||||
goal: 'Увеличить количество клиентов на 50% за следующие 6 месяцев',
|
||||
detailLevel: 'СТАНДАРТНО',
|
||||
strongSide: 'Опытная команда, быстрая доставка',
|
||||
weakSide: 'Ограниченный маркетинговый бюджет',
|
||||
analysisType: 'РЫНОК',
|
||||
});
|
||||
```
|
||||
|
||||
### React компонент с формой
|
||||
|
||||
```tsx
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const MarketingAnalysisForm: React.FC = () => {
|
||||
const [formData, setFormData] = useState<MarketingAnalysisRequest>({
|
||||
businessNiche: '',
|
||||
product: '',
|
||||
targetAudience: '',
|
||||
region: '',
|
||||
goal: '',
|
||||
detailLevel: 'СТАНДАРТНО',
|
||||
strongSide: '',
|
||||
weakSide: '',
|
||||
analysisType: 'РЫНОК',
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const analysisId = await startMarketingAnalysis(formData);
|
||||
console.log('Analysis started:', analysisId);
|
||||
// Перенаправление на страницу с результатами
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label>Ниша бизнеса *</label>
|
||||
<input
|
||||
type='text'
|
||||
value={formData.businessNiche}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, businessNiche: e.target.value })
|
||||
}
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={200}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Продукт / услуга *</label>
|
||||
<input
|
||||
type='text'
|
||||
value={formData.product}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, product: e.target.value })
|
||||
}
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={200}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Целевая аудитория *</label>
|
||||
<textarea
|
||||
value={formData.targetAudience}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, targetAudience: e.target.value })
|
||||
}
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={300}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Регион *</label>
|
||||
<select
|
||||
value={formData.region}
|
||||
onChange={(e) => setFormData({ ...formData, region: e.target.value })}
|
||||
required
|
||||
>
|
||||
<option value=''>Выберите регион</option>
|
||||
{VALID_REGIONS.map((region) => (
|
||||
<option key={region} value={region}>
|
||||
{region}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Цель на 6-12 месяцев *</label>
|
||||
<textarea
|
||||
value={formData.goal}
|
||||
onChange={(e) => setFormData({ ...formData, goal: e.target.value })}
|
||||
required
|
||||
minLength={10}
|
||||
maxLength={500}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Уровень детализации анализа *</label>
|
||||
<div>
|
||||
{DETAIL_LEVELS.map((level) => (
|
||||
<label key={level}>
|
||||
<input
|
||||
type='radio'
|
||||
name='detailLevel'
|
||||
value={level}
|
||||
checked={formData.detailLevel === level}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
detailLevel: e.target.value as any,
|
||||
})
|
||||
}
|
||||
/>
|
||||
{level === 'КРАТКО' && ' Кратко'}
|
||||
{level === 'СТАНДАРТНО' && ' Стандартно'}
|
||||
{level === 'ПОДРОБНО' && ' Подробно'}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Сильная сторона</label>
|
||||
<textarea
|
||||
value={formData.strongSide}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, strongSide: e.target.value })
|
||||
}
|
||||
maxLength={500}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Слабая сторона</label>
|
||||
<textarea
|
||||
value={formData.weakSide}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, weakSide: e.target.value })
|
||||
}
|
||||
maxLength={500}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Тип анализа *</label>
|
||||
<select
|
||||
value={formData.analysisType}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, analysisType: e.target.value as any })
|
||||
}
|
||||
required
|
||||
>
|
||||
<option value='РЫНОК'>Рынок</option>
|
||||
<option value='КОНКУРЕНТЫ'>Конкуренты</option>
|
||||
<option value='ЦА'>Целевая аудитория</option>
|
||||
<option value='КАНАЛЫ'>Каналы</option>
|
||||
<option value='SWOT'>SWOT</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type='submit'>Сгенерировать анализ бизнеса</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Валидация на клиенте
|
||||
|
||||
Рекомендуется выполнять валидацию на клиенте перед отправкой запроса:
|
||||
|
||||
```typescript
|
||||
function validateMarketingAnalysisRequest(data: MarketingAnalysisRequest): {
|
||||
valid: boolean;
|
||||
errors: Record<string, string>;
|
||||
} {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
// Валидация businessNiche
|
||||
if (!data.businessNiche || data.businessNiche.trim().length < 3) {
|
||||
errors.businessNiche = 'Ниша бизнеса должна содержать минимум 3 символа';
|
||||
}
|
||||
if (data.businessNiche && data.businessNiche.length > 200) {
|
||||
errors.businessNiche = 'Ниша бизнеса не должна превышать 200 символов';
|
||||
}
|
||||
|
||||
// Валидация product
|
||||
if (!data.product || data.product.trim().length < 3) {
|
||||
errors.product = 'Продукт должен содержать минимум 3 символа';
|
||||
}
|
||||
if (data.product && data.product.length > 200) {
|
||||
errors.product = 'Продукт не должен превышать 200 символов';
|
||||
}
|
||||
|
||||
// Валидация targetAudience
|
||||
if (!data.targetAudience || data.targetAudience.trim().length < 3) {
|
||||
errors.targetAudience =
|
||||
'Целевая аудитория должна содержать минимум 3 символа';
|
||||
}
|
||||
if (data.targetAudience && data.targetAudience.length > 300) {
|
||||
errors.targetAudience =
|
||||
'Целевая аудитория не должна превышать 300 символов';
|
||||
}
|
||||
|
||||
// Валидация region
|
||||
if (!data.region || !VALID_REGIONS.includes(data.region)) {
|
||||
errors.region = 'Выберите допустимый регион из списка';
|
||||
}
|
||||
|
||||
// Валидация goal
|
||||
if (!data.goal || data.goal.trim().length < 10) {
|
||||
errors.goal = 'Цель должна содержать минимум 10 символов';
|
||||
}
|
||||
if (data.goal && data.goal.length > 500) {
|
||||
errors.goal = 'Цель не должна превышать 500 символов';
|
||||
}
|
||||
|
||||
// Валидация detailLevel
|
||||
if (!data.detailLevel || !DETAIL_LEVELS.includes(data.detailLevel)) {
|
||||
errors.detailLevel = 'Выберите допустимый уровень детализации';
|
||||
}
|
||||
|
||||
// Валидация strongSide (опциональное)
|
||||
if (data.strongSide && data.strongSide.length > 500) {
|
||||
errors.strongSide = 'Сильная сторона не должна превышать 500 символов';
|
||||
}
|
||||
|
||||
// Валидация weakSide (опциональное)
|
||||
if (data.weakSide && data.weakSide.length > 500) {
|
||||
errors.weakSide = 'Слабая сторона не должна превышать 500 символов';
|
||||
}
|
||||
|
||||
// Валидация analysisType
|
||||
const validAnalysisTypes = ['РЫНОК', 'КОНКУРЕНТЫ', 'ЦА', 'КАНАЛЫ', 'SWOT'];
|
||||
if (!data.analysisType || !validAnalysisTypes.includes(data.analysisType)) {
|
||||
errors.analysisType = 'Выберите допустимый тип анализа';
|
||||
}
|
||||
|
||||
return {
|
||||
valid: Object.keys(errors).length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Миграция со старого API
|
||||
|
||||
Если у вас есть код, использующий старые поля, необходимо обновить его следующим образом:
|
||||
|
||||
### Старый формат (больше не работает):
|
||||
|
||||
```json
|
||||
{
|
||||
"product": "Разработка мобильных приложений",
|
||||
"location": "Алматы, Казахстан",
|
||||
"client": "B2B клиенты",
|
||||
"differentiator": "Быстрая разработка за 2 недели",
|
||||
"analysisType": "РЫНОК"
|
||||
}
|
||||
```
|
||||
|
||||
### Новый формат:
|
||||
|
||||
```json
|
||||
{
|
||||
"businessNiche": "Разработка программного обеспечения",
|
||||
"product": "Разработка мобильных приложений",
|
||||
"targetAudience": "B2B клиенты, технологические компании, стартапы",
|
||||
"region": "Алматы",
|
||||
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев",
|
||||
"detailLevel": "СТАНДАРТНО",
|
||||
"strongSide": "Быстрая разработка за 2 недели, опытная команда",
|
||||
"weakSide": "",
|
||||
"analysisType": "РЫНОК"
|
||||
}
|
||||
```
|
||||
|
||||
### Маппинг старых полей на новые:
|
||||
|
||||
| Старое поле | Новое поле(я) | Примечание |
|
||||
| ---------------- | ----------------------------------------- | ------------------------------------------------ |
|
||||
| `location` | `region` | Только название города из списка |
|
||||
| `client` | `targetAudience` | Более детальное описание (свободный текст) |
|
||||
| `differentiator` | `businessNiche`, `strongSide`, `weakSide` | Разделено на несколько полей для лучшего анализа |
|
||||
|
||||
---
|
||||
|
||||
## Обработка ошибок валидации
|
||||
|
||||
При ошибках валидации API возвращает следующий формат:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Ошибка валидации",
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Ошибка валидации входных данных",
|
||||
"details": {
|
||||
"businessNiche": "Поле 'businessNiche' должно содержать от 3 до 200 символов",
|
||||
"region": "Поле 'region' должно быть одним из допустимых городов Казахстана",
|
||||
"detailLevel": "Поле 'detailLevel' должно быть одним из: КРАТКО, СТАНДАРТНО, ПОДРОБНО"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Важные замечания
|
||||
|
||||
1. **Регистр важен**: Значения `region`, `detailLevel` и `analysisType` чувствительны к регистру. Используйте точные значения из списка допустимых.
|
||||
|
||||
2. **Все новые поля передаются в OpenAI**: Все указанные поля включаются в контекст для генерации анализа, что улучшает качество и релевантность результатов.
|
||||
|
||||
3. **Уровень детализации влияет на результат**: Выбор `detailLevel` напрямую влияет на объем и глубину генерируемого анализа.
|
||||
|
||||
4. **Опциональные поля улучшают анализ**: Хотя `strongSide` и `weakSide` опциональны, их указание помогает AI лучше понять бизнес и дать более точные рекомендации.
|
||||
|
||||
5. **Обратная совместимость**: Старые поля больше не поддерживаются. Необходимо обновить все клиентские приложения.
|
||||
|
||||
---
|
||||
|
||||
## Поддержка
|
||||
|
||||
При возникновении проблем с API обращайтесь в техническую поддержку с указанием:
|
||||
|
||||
- `analysisId` (если есть)
|
||||
- Время запроса
|
||||
- Описание проблемы
|
||||
- Код ошибки (если есть)
|
||||
- Пример запроса (без чувствительных данных)
|
||||
+24
-17
@@ -226,38 +226,45 @@ const router = createRouter({
|
||||
{
|
||||
path: '/marketing-analysis',
|
||||
name: 'marketing-main',
|
||||
component: () => import('@/views/pages/marketing/MarketingMain.vue')
|
||||
component: () => import('@/views/pages/marketing/MarketingMain.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/marketing-analysis/analysis',
|
||||
name: 'marketing-analysis',
|
||||
component: () => import('@/views/pages/marketing/MarketingAnalysis.vue')
|
||||
component: () => import('@/views/pages/marketing/MarketingAnalysis.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/marketing-analysis/promotion',
|
||||
name: 'marketing-promotion',
|
||||
component: () => import('@/views/pages/marketing/MarketingPromotion.vue')
|
||||
component: () => import('@/views/pages/marketing/MarketingPromotion.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/marketing-analysis/results',
|
||||
name: 'marketing-results',
|
||||
component: () => import('@/views/pages/marketing/MarketingResults.vue')
|
||||
component: () => import('@/views/pages/marketing/MarketingResults.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/marketing-analysis/analyses',
|
||||
name: 'marketing-analyses-list',
|
||||
component: () => import('@/views/pages/marketing/MarketingAnalysesList.vue')
|
||||
component: () => import('@/views/pages/marketing/MarketingAnalysesList.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/marketing-analysis/strategies',
|
||||
name: 'marketing-strategies-list',
|
||||
component: () => import('@/views/pages/marketing/MarketingStrategiesList.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/marketing-analysis/credentials',
|
||||
name: 'marketing-credentials',
|
||||
component: () => import('@/views/pages/marketing/MarketingCredentials.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
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',
|
||||
@@ -284,13 +291,13 @@ const router = createRouter({
|
||||
|
||||
router.beforeEach(async (to) => {
|
||||
const isAuthenticated = !!AuthService.getAccessToken();
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
if (to.meta?.requiresAuth && !isAuthenticated) {
|
||||
return { name: 'login', query: { redirect: to.fullPath } };
|
||||
}
|
||||
if (to.name === 'login' && isAuthenticated) {
|
||||
return { path: '/' };
|
||||
}
|
||||
if (to.meta.requiresAdmin) {
|
||||
if (to.meta?.requiresAdmin) {
|
||||
try {
|
||||
const me = await IdentityService.getCurrentUser();
|
||||
const roles = Array.isArray(me?.roles) ? me.roles : [];
|
||||
|
||||
@@ -8,38 +8,54 @@ class MarketingService {
|
||||
* Запуск маркетингового анализа
|
||||
* POST /api/marketing/analysis/start
|
||||
* @param {Object} data - Данные для анализа
|
||||
* @param {string} data.businessNiche - Ниша бизнеса
|
||||
* @param {string} data.product - Название продукта или услуги
|
||||
* @param {string} data.location - Географическая локация работы
|
||||
* @param {string} data.client - Тип целевой аудитории
|
||||
* @param {string} data.differentiator - Уникальные особенности бизнеса
|
||||
* @param {string} data.targetAudience - Целевая аудитория (детальное описание)
|
||||
* @param {string} data.region - Регион (город Казахстана)
|
||||
* @param {string} data.goal - Цель на 6-12 месяцев
|
||||
* @param {string} data.detailLevel - Уровень детализации анализа (КРАТКО, СТАНДАРТНО, ПОДРОБНО)
|
||||
* @param {string} data.analysisType - Тип анализа (РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT)
|
||||
* @param {string} [data.strongSide] - Сильная сторона бизнеса (опционально)
|
||||
* @param {string} [data.weakSide] - Слабая сторона бизнеса (опционально)
|
||||
* @returns {Promise<Object>} Объект с analysisId и статусом
|
||||
*/
|
||||
async startAnalysis(data) {
|
||||
try {
|
||||
const requestBody = {
|
||||
businessNiche: data.businessNiche,
|
||||
product: data.product,
|
||||
targetAudience: data.targetAudience,
|
||||
region: data.region,
|
||||
goal: data.goal,
|
||||
detailLevel: data.detailLevel,
|
||||
analysisType: data.analysisType
|
||||
};
|
||||
|
||||
// Добавляем опциональные поля только если они указаны
|
||||
if (data.strongSide) {
|
||||
requestBody.strongSide = data.strongSide;
|
||||
}
|
||||
if (data.weakSide) {
|
||||
requestBody.weakSide = data.weakSide;
|
||||
}
|
||||
|
||||
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,
|
||||
analysisType: data.analysisType
|
||||
})
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const result = await response.json().catch(() => null);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || result.error?.message || 'Ошибка при запуске анализа');
|
||||
throw new Error(result?.message || result?.error?.message || 'Ошибка при запуске анализа');
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || 'Ошибка при запуске анализа');
|
||||
if (!result || !result.success) {
|
||||
throw new Error(result?.message || 'Ошибка при запуске анализа');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
return result.data || null;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при запуске маркетингового анализа:', error);
|
||||
throw error;
|
||||
@@ -56,17 +72,17 @@ class MarketingService {
|
||||
try {
|
||||
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_ANALYSIS_GET}/${analysisId}`);
|
||||
|
||||
const result = await response.json();
|
||||
const result = await response.json().catch(() => null);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || result.error?.message || 'Ошибка при получении результата анализа');
|
||||
throw new Error(result?.message || result?.error?.message || 'Ошибка при получении результата анализа');
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || 'Ошибка при получении результата анализа');
|
||||
if (!result || !result.success) {
|
||||
throw new Error(result?.message || 'Ошибка при получении результата анализа');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
return result.data || null;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении результата анализа:', error);
|
||||
throw error;
|
||||
@@ -76,23 +92,25 @@ class MarketingService {
|
||||
/**
|
||||
* Получение списка всех анализов текущего пользователя
|
||||
* GET /api/marketing/analysis/my
|
||||
* @returns {Promise<Array>} Массив анализов с полями analysisId, product, location, clientType, differentiator, status, userId, createdAt, completedAt, statusHistory
|
||||
* @returns {Promise<Array>} Массив анализов. Поля могут различаться в зависимости от версии API:
|
||||
* Старые анализы: analysisId, product, location, clientType, differentiator, status, userId, createdAt, completedAt, statusHistory
|
||||
* Новые анализы: analysisId, product, businessNiche, targetAudience, region, goal, detailLevel, strongSide, weakSide, status, userId, createdAt, completedAt, statusHistory
|
||||
*/
|
||||
async getMyAnalyses() {
|
||||
try {
|
||||
const response = await AuthService.authFetch(`${API_BASE_URL}${API_CONFIG.ENDPOINTS.MARKETING_ANALYSIS_MY}`);
|
||||
|
||||
const result = await response.json();
|
||||
const result = await response.json().catch(() => null);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || result.error?.message || 'Ошибка при получении списка анализов');
|
||||
throw new Error(result?.message || result?.error?.message || 'Ошибка при получении списка анализов');
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || 'Ошибка при получении списка анализов');
|
||||
if (!result || !result.success) {
|
||||
throw new Error(result?.message || 'Ошибка при получении списка анализов');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
return result.data || [];
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении списка анализов:', error);
|
||||
throw error;
|
||||
|
||||
@@ -11,17 +11,17 @@
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<DataTable
|
||||
:value="analyses"
|
||||
:value="analyses || []"
|
||||
:loading="loading"
|
||||
paginator
|
||||
:rows="10"
|
||||
:rowsPerPageOptions="[5, 10, 20, 50]"
|
||||
sortMode="multiple"
|
||||
sortMode="single"
|
||||
removableSort
|
||||
:sortField="'createdAt'"
|
||||
:sortOrder="-1"
|
||||
filterDisplay="row"
|
||||
:globalFilterFields="['product', 'location', 'clientType', 'status', 'analysisType']"
|
||||
filterDisplay="menu"
|
||||
:globalFilterFields="['product', 'location', 'region', 'clientType', 'status', 'analysisType', 'businessNiche']"
|
||||
v-model:filters="filters"
|
||||
:filters="filters"
|
||||
dataKey="analysisId"
|
||||
@@ -30,7 +30,7 @@
|
||||
<div class="flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search" />
|
||||
<InputText v-model="filters['global'].value" placeholder="Поиск анализов..." />
|
||||
<InputText v-model="filters.global.value" placeholder="Поиск анализов..." />
|
||||
</span>
|
||||
<Button label="Обновить" icon="pi pi-refresh" severity="secondary" @click="loadAnalyses" :loading="loading" />
|
||||
</div>
|
||||
@@ -38,19 +38,20 @@
|
||||
|
||||
<Column field="product" header="Продукт" :sortable="true" style="min-width: 200px">
|
||||
<template #body="slotProps">
|
||||
<span class="font-semibold">{{ slotProps.data.product }}</span>
|
||||
<span class="font-semibold">{{ slotProps.data.product || '—' }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="location" header="Локация" :sortable="true" style="min-width: 150px">
|
||||
<template #body="slotProps">
|
||||
<span>{{ slotProps.data.location }}</span>
|
||||
<span>{{ slotProps.data.location || '—' }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="clientType" header="Тип клиента" :sortable="true" style="min-width: 150px">
|
||||
<template #body="slotProps">
|
||||
<Tag :value="slotProps.data.clientType" severity="info" />
|
||||
<Tag v-if="slotProps.data.clientType && slotProps.data.clientType !== '—'" :value="slotProps.data.clientType" severity="info" />
|
||||
<span v-else class="text-surface-400">—</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
@@ -66,7 +67,7 @@
|
||||
<Tag :value="getStatusLabel(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<Dropdown v-model="filterModel.value" :options="statusOptions" placeholder="Все статусы" class="p-column-filter" @change="filterCallback()">
|
||||
<Dropdown v-if="filterModel" v-model="filterModel.value" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="Все статусы" class="p-column-filter" @change="filterCallback && filterCallback()">
|
||||
<template #option="slotProps">
|
||||
<Tag :value="slotProps.option.label" :severity="slotProps.option.severity" />
|
||||
</template>
|
||||
@@ -161,11 +162,21 @@ const statusHistory = ref([]);
|
||||
const historyDialogVisible = ref(false);
|
||||
const currentAnalysisId = ref(null);
|
||||
|
||||
// Filters
|
||||
const filters = ref({
|
||||
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
||||
status: { value: null, matchMode: FilterMatchMode.EQUALS }
|
||||
});
|
||||
// Filters - initialize with all possible filter fields
|
||||
const initFilters = () => {
|
||||
return {
|
||||
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
||||
product: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
||||
location: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
||||
region: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
||||
clientType: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
||||
status: { value: null, matchMode: FilterMatchMode.EQUALS },
|
||||
analysisType: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
||||
businessNiche: { value: null, matchMode: FilterMatchMode.CONTAINS }
|
||||
};
|
||||
};
|
||||
|
||||
const filters = ref(initFilters());
|
||||
|
||||
// Status options for filter
|
||||
const statusOptions = [
|
||||
@@ -175,12 +186,53 @@ const statusOptions = [
|
||||
{ label: 'Ошибка', value: 'failed', severity: 'danger' }
|
||||
];
|
||||
|
||||
// Normalize analysis data to ensure all required fields exist
|
||||
const normalizeAnalysis = (analysis) => {
|
||||
if (!analysis) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure analysisId exists - generate one if missing
|
||||
const analysisId = analysis.analysisId || `temp-${Date.now()}-${Math.random()}`;
|
||||
|
||||
// Ensure createdAt exists for sorting - use current date if missing
|
||||
const createdAt = analysis.createdAt || new Date().toISOString();
|
||||
|
||||
return {
|
||||
...analysis,
|
||||
analysisId,
|
||||
product: analysis.product || analysis.businessNiche || '',
|
||||
location: analysis.location || analysis.region || '',
|
||||
region: analysis.region || analysis.location || '',
|
||||
businessNiche: analysis.businessNiche || analysis.product || '',
|
||||
clientType: analysis.clientType || '',
|
||||
analysisType: analysis.analysisType || null,
|
||||
status: analysis.status || 'unknown',
|
||||
createdAt: createdAt,
|
||||
completedAt: analysis.completedAt || null
|
||||
};
|
||||
};
|
||||
|
||||
// Load analyses
|
||||
const loadAnalyses = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await MarketingService.getMyAnalyses();
|
||||
analyses.value = data || [];
|
||||
console.log('Raw data from API:', data);
|
||||
|
||||
// Ensure data is an array
|
||||
if (!Array.isArray(data)) {
|
||||
console.warn('Data is not an array:', data);
|
||||
analyses.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize all analyses to ensure they have all required fields
|
||||
// Filter out any null values from normalization
|
||||
const normalized = data.map(normalizeAnalysis).filter((analysis) => analysis !== null && analysis !== undefined);
|
||||
|
||||
console.log('Normalized analyses:', normalized);
|
||||
analyses.value = normalized;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке списка анализов:', error);
|
||||
toast.add({
|
||||
@@ -189,6 +241,7 @@ const loadAnalyses = async () => {
|
||||
detail: error.message || 'Не удалось загрузить список анализов',
|
||||
life: 5000
|
||||
});
|
||||
analyses.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -10,31 +10,63 @@
|
||||
<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 }" />
|
||||
<label for="businessNiche" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Ниша бизнеса * </label>
|
||||
<InputText id="businessNiche" v-model="formData.businessNiche" placeholder="Например: E-commerce платформы" class="w-full" :class="{ 'p-invalid': errors.businessNiche }" />
|
||||
<small v-if="errors.businessNiche" class="p-error">{{ errors.businessNiche }}</small>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<label for="targetAudience" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Целевая аудитория * </label>
|
||||
<Textarea id="targetAudience" v-model="formData.targetAudience" placeholder="Опишите вашу целевую аудиторию детально" rows="3" class="w-full" :class="{ 'p-invalid': errors.targetAudience }" />
|
||||
<small v-if="errors.targetAudience" class="p-error">{{ errors.targetAudience }}</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>
|
||||
<label for="region" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Регион (город Казахстана) * </label>
|
||||
<Dropdown id="region" v-model="formData.region" :options="VALID_REGIONS" placeholder="Выберите город" class="w-full" :class="{ 'p-invalid': errors.region }" />
|
||||
<small v-if="errors.region" class="p-error">{{ errors.region }}</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>
|
||||
<label for="goal" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Цель на 6-12 месяцев * </label>
|
||||
<Textarea id="goal" v-model="formData.goal" placeholder="Опишите вашу цель на ближайшие 6-12 месяцев" rows="3" class="w-full" :class="{ 'p-invalid': errors.goal }" />
|
||||
<small v-if="errors.goal" class="p-error">{{ errors.goal }}</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="analysisType" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Тип анализа </label>
|
||||
<label class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Уровень детализации анализа * </label>
|
||||
<div class="flex gap-4">
|
||||
<div v-for="level in DETAIL_LEVELS" :key="level" class="flex align-items-center">
|
||||
<RadioButton :inputId="`detailLevel-${level}`" v-model="formData.detailLevel" :value="level" :class="{ 'p-invalid': errors.detailLevel }" />
|
||||
<label :for="`detailLevel-${level}`" class="ml-2 cursor-pointer">
|
||||
{{ level === 'КРАТКО' ? 'Кратко' : level === 'СТАНДАРТНО' ? 'Стандартно' : 'Подробно' }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<small v-if="errors.detailLevel" class="p-error">{{ errors.detailLevel }}</small>
|
||||
<small class="text-surface-500 dark:text-surface-400 mt-1 block">Выберите уровень детализации анализа</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="strongSide" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Сильная сторона бизнеса </label>
|
||||
<Textarea id="strongSide" v-model="formData.strongSide" placeholder="Опишите сильные стороны вашего бизнеса (опционально)" rows="2" class="w-full" :class="{ 'p-invalid': errors.strongSide }" />
|
||||
<small v-if="errors.strongSide" class="p-error">{{ errors.strongSide }}</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="weakSide" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Слабая сторона бизнеса </label>
|
||||
<Textarea id="weakSide" v-model="formData.weakSide" placeholder="Опишите слабые стороны вашего бизнеса (опционально)" rows="2" class="w-full" :class="{ 'p-invalid': errors.weakSide }" />
|
||||
<small v-if="errors.weakSide" class="p-error">{{ errors.weakSide }}</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"
|
||||
@@ -197,7 +229,9 @@ import Button from 'primevue/button';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
import RadioButton from 'primevue/radiobutton';
|
||||
import Tag from 'primevue/tag';
|
||||
import Textarea from 'primevue/textarea';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
@@ -207,17 +241,45 @@ const toast = useToast();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// Constants
|
||||
const VALID_REGIONS = [
|
||||
'Алматы',
|
||||
'Астана',
|
||||
'Шымкент',
|
||||
'Караганда',
|
||||
'Актобе',
|
||||
'Тараз',
|
||||
'Павлодар',
|
||||
'Усть-Каменогорск',
|
||||
'Семей',
|
||||
'Костанай',
|
||||
'Кызылорда',
|
||||
'Уральск',
|
||||
'Петропавловск',
|
||||
'Атырау',
|
||||
'Актау',
|
||||
'Туркестан',
|
||||
'Кокшетау',
|
||||
'Талдыкорган',
|
||||
'Экибастуз',
|
||||
'Рудный'
|
||||
];
|
||||
|
||||
const DETAIL_LEVELS = ['КРАТКО', 'СТАНДАРТНО', 'ПОДРОБНО'];
|
||||
|
||||
// Form data
|
||||
const formData = ref({
|
||||
businessNiche: '',
|
||||
product: '',
|
||||
location: '',
|
||||
client: null,
|
||||
differentiator: '',
|
||||
targetAudience: '',
|
||||
region: null,
|
||||
goal: '',
|
||||
detailLevel: 'СТАНДАРТНО',
|
||||
strongSide: '',
|
||||
weakSide: '',
|
||||
analysisType: null
|
||||
});
|
||||
|
||||
const clientOptions = ['B2B клиенты', 'B2C клиенты', 'Частные лица', 'Корпорации', 'Малый бизнес'];
|
||||
|
||||
const analysisTypeOptions = [
|
||||
{ label: 'РЫНОК - Анализ рынка', value: 'РЫНОК' },
|
||||
{ label: 'КОНКУРЕНТЫ - Анализ конкурентов', value: 'КОНКУРЕНТЫ' },
|
||||
@@ -245,6 +307,13 @@ const downloadingPdf = ref(false);
|
||||
const validateForm = () => {
|
||||
errors.value = {};
|
||||
|
||||
// BusinessNiche validation
|
||||
if (!formData.value.businessNiche || formData.value.businessNiche.trim().length < 3) {
|
||||
errors.value.businessNiche = 'Ниша бизнеса должна содержать минимум 3 символа';
|
||||
} else if (formData.value.businessNiche.trim().length > 200) {
|
||||
errors.value.businessNiche = 'Ниша бизнеса не должна превышать 200 символов';
|
||||
}
|
||||
|
||||
// Product validation
|
||||
if (!formData.value.product || formData.value.product.trim().length < 3) {
|
||||
errors.value.product = 'Название продукта должно содержать минимум 3 символа';
|
||||
@@ -252,25 +321,40 @@ const validateForm = () => {
|
||||
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 символов';
|
||||
// TargetAudience validation
|
||||
if (!formData.value.targetAudience || formData.value.targetAudience.trim().length < 3) {
|
||||
errors.value.targetAudience = 'Целевая аудитория должна содержать минимум 3 символа';
|
||||
} else if (formData.value.targetAudience.trim().length > 300) {
|
||||
errors.value.targetAudience = 'Целевая аудитория не должна превышать 300 символов';
|
||||
}
|
||||
|
||||
// Client validation
|
||||
if (!formData.value.client) {
|
||||
errors.value.client = 'Пожалуйста, выберите тип клиента';
|
||||
} else if (!clientOptions.includes(formData.value.client)) {
|
||||
errors.value.client = 'Выбран недопустимый тип клиента';
|
||||
// Region validation
|
||||
if (!formData.value.region) {
|
||||
errors.value.region = 'Пожалуйста, выберите регион';
|
||||
} else if (!VALID_REGIONS.includes(formData.value.region)) {
|
||||
errors.value.region = 'Выбран недопустимый регион';
|
||||
}
|
||||
|
||||
// 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 символов';
|
||||
// Goal validation
|
||||
if (!formData.value.goal || formData.value.goal.trim().length < 10) {
|
||||
errors.value.goal = 'Цель должна содержать минимум 10 символов';
|
||||
} else if (formData.value.goal.trim().length > 500) {
|
||||
errors.value.goal = 'Цель не должна превышать 500 символов';
|
||||
}
|
||||
|
||||
// DetailLevel validation
|
||||
if (!formData.value.detailLevel || !DETAIL_LEVELS.includes(formData.value.detailLevel)) {
|
||||
errors.value.detailLevel = 'Пожалуйста, выберите уровень детализации';
|
||||
}
|
||||
|
||||
// StrongSide validation (optional)
|
||||
if (formData.value.strongSide && formData.value.strongSide.trim().length > 500) {
|
||||
errors.value.strongSide = 'Сильная сторона не должна превышать 500 символов';
|
||||
}
|
||||
|
||||
// WeakSide validation (optional)
|
||||
if (formData.value.weakSide && formData.value.weakSide.trim().length > 500) {
|
||||
errors.value.weakSide = 'Слабая сторона не должна превышать 500 символов';
|
||||
}
|
||||
|
||||
// AnalysisType validation
|
||||
@@ -359,13 +443,25 @@ const handleSubmit = async () => {
|
||||
submitting.value = true;
|
||||
|
||||
try {
|
||||
const result = await MarketingService.startAnalysis({
|
||||
const requestData = {
|
||||
businessNiche: formData.value.businessNiche.trim(),
|
||||
product: formData.value.product.trim(),
|
||||
location: formData.value.location.trim(),
|
||||
client: formData.value.client,
|
||||
differentiator: formData.value.differentiator.trim(),
|
||||
targetAudience: formData.value.targetAudience.trim(),
|
||||
region: formData.value.region,
|
||||
goal: formData.value.goal.trim(),
|
||||
detailLevel: formData.value.detailLevel,
|
||||
analysisType: formData.value.analysisType
|
||||
});
|
||||
};
|
||||
|
||||
// Добавляем опциональные поля только если они заполнены
|
||||
if (formData.value.strongSide && formData.value.strongSide.trim()) {
|
||||
requestData.strongSide = formData.value.strongSide.trim();
|
||||
}
|
||||
if (formData.value.weakSide && formData.value.weakSide.trim()) {
|
||||
requestData.weakSide = formData.value.weakSide.trim();
|
||||
}
|
||||
|
||||
const result = await MarketingService.startAnalysis(requestData);
|
||||
|
||||
analysisId.value = result.analysisId;
|
||||
status.value = result.status;
|
||||
@@ -431,10 +527,14 @@ const resetAnalysis = () => {
|
||||
estimatedCompletionTime.value = null;
|
||||
pollingAttempts.value = 0;
|
||||
formData.value = {
|
||||
businessNiche: '',
|
||||
product: '',
|
||||
location: '',
|
||||
client: null,
|
||||
differentiator: '',
|
||||
targetAudience: '',
|
||||
region: null,
|
||||
goal: '',
|
||||
detailLevel: 'СТАНДАРТНО',
|
||||
strongSide: '',
|
||||
weakSide: '',
|
||||
analysisType: null
|
||||
};
|
||||
errors.value = {};
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// vite.config.mjs
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
import { PrimeVueResolver } from "file:///Users/magzhanzhumabaev/Desktop/kt%20cloud/sakai-vue/node_modules/@primevue/auto-import-resolver/index.mjs";
|
||||
import vue from "file:///Users/magzhanzhumabaev/Desktop/kt%20cloud/sakai-vue/node_modules/@vitejs/plugin-vue/dist/index.mjs";
|
||||
import Components from "file:///Users/magzhanzhumabaev/Desktop/kt%20cloud/sakai-vue/node_modules/unplugin-vue-components/dist/vite.js";
|
||||
import { defineConfig } from "file:///Users/magzhanzhumabaev/Desktop/kt%20cloud/sakai-vue/node_modules/vite/dist/node/index.js";
|
||||
var __vite_injected_original_import_meta_url = "file:///Users/magzhanzhumabaev/Desktop/kt%20cloud/sakai-vue/vite.config.mjs";
|
||||
var vite_config_default = defineConfig({
|
||||
optimizeDeps: {
|
||||
noDiscovery: true
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
Components({
|
||||
resolvers: [PrimeVueResolver()]
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", __vite_injected_original_import_meta_url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "https://api.konturai.kz",
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
configure: (proxy, _options) => {
|
||||
proxy.on("error", (err, _req, _res) => {
|
||||
console.log("proxy error", err);
|
||||
});
|
||||
proxy.on("proxyReq", (proxyReq, req, _res) => {
|
||||
console.log("Sending Request to the Target:", req.method, req.url);
|
||||
});
|
||||
proxy.on("proxyRes", (proxyRes, req, _res) => {
|
||||
console.log("Received Response from the Target:", proxyRes.statusCode, req.url);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
export {
|
||||
vite_config_default as default
|
||||
};
|
||||
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcubWpzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL21hZ3poYW56aHVtYWJhZXYvRGVza3RvcC9rdCBjbG91ZC9zYWthaS12dWVcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy9tYWd6aGFuemh1bWFiYWV2L0Rlc2t0b3Ava3QgY2xvdWQvc2FrYWktdnVlL3ZpdGUuY29uZmlnLm1qc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvbWFnemhhbnpodW1hYmFldi9EZXNrdG9wL2t0JTIwY2xvdWQvc2FrYWktdnVlL3ZpdGUuY29uZmlnLm1qc1wiO2ltcG9ydCB7IGZpbGVVUkxUb1BhdGgsIFVSTCB9IGZyb20gJ25vZGU6dXJsJztcblxuaW1wb3J0IHsgUHJpbWVWdWVSZXNvbHZlciB9IGZyb20gJ0BwcmltZXZ1ZS9hdXRvLWltcG9ydC1yZXNvbHZlcic7XG5pbXBvcnQgdnVlIGZyb20gJ0B2aXRlanMvcGx1Z2luLXZ1ZSc7XG5pbXBvcnQgQ29tcG9uZW50cyBmcm9tICd1bnBsdWdpbi12dWUtY29tcG9uZW50cy92aXRlJztcbmltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gJ3ZpdGUnO1xuXG4vLyBodHRwczovL3ZpdGVqcy5kZXYvY29uZmlnL1xuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKHtcbiAgICBvcHRpbWl6ZURlcHM6IHtcbiAgICAgICAgbm9EaXNjb3Zlcnk6IHRydWVcbiAgICB9LFxuICAgIHBsdWdpbnM6IFtcbiAgICAgICAgdnVlKCksXG4gICAgICAgIENvbXBvbmVudHMoe1xuICAgICAgICAgICAgcmVzb2x2ZXJzOiBbUHJpbWVWdWVSZXNvbHZlcigpXVxuICAgICAgICB9KVxuICAgIF0sXG4gICAgcmVzb2x2ZToge1xuICAgICAgICBhbGlhczoge1xuICAgICAgICAgICAgJ0AnOiBmaWxlVVJMVG9QYXRoKG5ldyBVUkwoJy4vc3JjJywgaW1wb3J0Lm1ldGEudXJsKSlcbiAgICAgICAgfVxuICAgIH0sXG4gICAgc2VydmVyOiB7XG4gICAgICAgIHByb3h5OiB7XG4gICAgICAgICAgICAnL2FwaSc6IHtcbiAgICAgICAgICAgICAgICB0YXJnZXQ6ICdodHRwczovL2FwaS5rb250dXJhaS5reicsXG4gICAgICAgICAgICAgICAgY2hhbmdlT3JpZ2luOiB0cnVlLFxuICAgICAgICAgICAgICAgIHNlY3VyZTogdHJ1ZSxcbiAgICAgICAgICAgICAgICBjb25maWd1cmU6IChwcm94eSwgX29wdGlvbnMpID0+IHtcbiAgICAgICAgICAgICAgICAgICAgcHJveHkub24oJ2Vycm9yJywgKGVyciwgX3JlcSwgX3JlcykgPT4ge1xuICAgICAgICAgICAgICAgICAgICAgICAgY29uc29sZS5sb2coJ3Byb3h5IGVycm9yJywgZXJyKTtcbiAgICAgICAgICAgICAgICAgICAgfSk7XG4gICAgICAgICAgICAgICAgICAgIHByb3h5Lm9uKCdwcm94eVJlcScsIChwcm94eVJlcSwgcmVxLCBfcmVzKSA9PiB7XG4gICAgICAgICAgICAgICAgICAgICAgICBjb25zb2xlLmxvZygnU2VuZGluZyBSZXF1ZXN0IHRvIHRoZSBUYXJnZXQ6JywgcmVxLm1ldGhvZCwgcmVxLnVybCk7XG4gICAgICAgICAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgICAgICAgICBwcm94eS5vbigncHJveHlSZXMnLCAocHJveHlSZXMsIHJlcSwgX3JlcykgPT4ge1xuICAgICAgICAgICAgICAgICAgICAgICAgY29uc29sZS5sb2coJ1JlY2VpdmVkIFJlc3BvbnNlIGZyb20gdGhlIFRhcmdldDonLCBwcm94eVJlcy5zdGF0dXNDb2RlLCByZXEudXJsKTtcbiAgICAgICAgICAgICAgICAgICAgfSk7XG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgfVxufSk7XG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQTRVLFNBQVMsZUFBZSxXQUFXO0FBRS9XLFNBQVMsd0JBQXdCO0FBQ2pDLE9BQU8sU0FBUztBQUNoQixPQUFPLGdCQUFnQjtBQUN2QixTQUFTLG9CQUFvQjtBQUxnTCxJQUFNLDJDQUEyQztBQVE5UCxJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUN4QixjQUFjO0FBQUEsSUFDVixhQUFhO0FBQUEsRUFDakI7QUFBQSxFQUNBLFNBQVM7QUFBQSxJQUNMLElBQUk7QUFBQSxJQUNKLFdBQVc7QUFBQSxNQUNQLFdBQVcsQ0FBQyxpQkFBaUIsQ0FBQztBQUFBLElBQ2xDLENBQUM7QUFBQSxFQUNMO0FBQUEsRUFDQSxTQUFTO0FBQUEsSUFDTCxPQUFPO0FBQUEsTUFDSCxLQUFLLGNBQWMsSUFBSSxJQUFJLFNBQVMsd0NBQWUsQ0FBQztBQUFBLElBQ3hEO0FBQUEsRUFDSjtBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ0osT0FBTztBQUFBLE1BQ0gsUUFBUTtBQUFBLFFBQ0osUUFBUTtBQUFBLFFBQ1IsY0FBYztBQUFBLFFBQ2QsUUFBUTtBQUFBLFFBQ1IsV0FBVyxDQUFDLE9BQU8sYUFBYTtBQUM1QixnQkFBTSxHQUFHLFNBQVMsQ0FBQyxLQUFLLE1BQU0sU0FBUztBQUNuQyxvQkFBUSxJQUFJLGVBQWUsR0FBRztBQUFBLFVBQ2xDLENBQUM7QUFDRCxnQkFBTSxHQUFHLFlBQVksQ0FBQyxVQUFVLEtBQUssU0FBUztBQUMxQyxvQkFBUSxJQUFJLGtDQUFrQyxJQUFJLFFBQVEsSUFBSSxHQUFHO0FBQUEsVUFDckUsQ0FBQztBQUNELGdCQUFNLEdBQUcsWUFBWSxDQUFDLFVBQVUsS0FBSyxTQUFTO0FBQzFDLG9CQUFRLElBQUksc0NBQXNDLFNBQVMsWUFBWSxJQUFJLEdBQUc7QUFBQSxVQUNsRixDQUFDO0FBQUEsUUFDTDtBQUFBLE1BQ0o7QUFBQSxJQUNKO0FBQUEsRUFDSjtBQUNKLENBQUM7IiwKICAibmFtZXMiOiBbXQp9Cg==
|
||||
Reference in New Issue
Block a user