.
This commit is contained in:
+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 = {};
|
||||
|
||||
Reference in New Issue
Block a user