This commit is contained in:
root
2025-12-03 09:05:00 +05:00
parent 24cc534fed
commit 0b074d6281
6 changed files with 1023 additions and 98 deletions
@@ -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;
}
+140 -40
View File
@@ -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 = {};