Files
marketing/src/views/smm/CampaignList.vue
T
2025-09-24 10:10:42 +05:00

400 lines
14 KiB
Vue

<template>
<div class="card">
<div class="card-header">
<h5>Управление кампаниями</h5>
<Button icon="pi pi-plus" label="Создать кампанию" @click="showCreateDialog = true" class="p-button-success" />
</div>
<div class="card-content">
<DataTable
:value="campaigns"
:loading="loading"
paginator
:rows="10"
:rowsPerPageOptions="[5, 10, 20]"
sortMode="multiple"
removableSort
filterDisplay="row"
:globalFilterFields="['name', 'goal', 'status']"
v-model:filters="filters"
:filters="filters"
>
<template #header>
<div class="flex justify-content-between">
<span class="p-input-icon-left">
<i class="pi pi-search" />
<InputText v-model="filters['global'].value" placeholder="Поиск кампаний..." />
</span>
</div>
</template>
<Column field="name" header="Название" :sortable="true">
<template #body="slotProps">
<span class="font-semibold">{{ slotProps.data.name }}</span>
</template>
</Column>
<Column field="goal" header="Цель" :sortable="true">
<template #body="slotProps">
<span>{{ slotProps.data.goal }}</span>
</template>
</Column>
<Column field="budget" header="Бюджет" :sortable="true">
<template #body="slotProps">
<span class="font-semibold text-green-600">
{{ formatCurrency(slotProps.data.budget) }}
</span>
</template>
</Column>
<Column field="startAt" header="Начало" :sortable="true">
<template #body="slotProps">
<span>{{ formatDate(slotProps.data.startAt) }}</span>
</template>
</Column>
<Column field="endAt" header="Окончание" :sortable="true">
<template #body="slotProps">
<span>{{ formatDate(slotProps.data.endAt) }}</span>
</template>
</Column>
<Column field="status" header="Статус" :sortable="true">
<template #body="slotProps">
<Tag :value="getStatusLabel(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
</template>
</Column>
<Column header="Действия" :exportable="false">
<template #body="slotProps">
<div class="flex gap-2">
<Button icon="pi pi-pencil" class="p-button-rounded p-button-text p-button-sm" @click="editCampaign(slotProps.data)" v-tooltip.top="'Редактировать'" />
<Button icon="pi pi-trash" class="p-button-rounded p-button-text p-button-danger p-button-sm" @click="confirmDelete(slotProps.data)" v-tooltip.top="'Удалить'" />
</div>
</template>
</Column>
</DataTable>
</div>
</div>
<!-- Диалог создания/редактирования кампании -->
<Dialog v-model:visible="showCreateDialog" :header="editingCampaign ? 'Редактировать кампанию' : 'Создать кампанию'" :modal="true" :style="{ width: '50vw' }" :closable="false">
<div class="p-fluid">
<div class="field">
<label for="name">Название кампании *</label>
<InputText id="name" v-model="campaignForm.name" :class="{ 'p-invalid': !campaignForm.name }" />
<small v-if="!campaignForm.name" class="p-error">Название обязательно</small>
</div>
<div class="field">
<label for="goal">Цель кампании</label>
<Textarea id="goal" v-model="campaignForm.goal" rows="3" />
</div>
<div class="field">
<label for="budget">Бюджет</label>
<InputNumber id="budget" v-model="campaignForm.budget" mode="currency" currency="KZT" locale="ru-RU" :min="0" />
</div>
<div class="grid">
<div class="col-6">
<div class="field">
<label for="startAt">Дата начала</label>
<Calendar id="startAt" v-model="campaignForm.startAt" dateFormat="yy-mm-dd" showTime hourFormat="24" />
</div>
</div>
<div class="col-6">
<div class="field">
<label for="endAt">Дата окончания</label>
<Calendar id="endAt" v-model="campaignForm.endAt" dateFormat="yy-mm-dd" showTime hourFormat="24" />
</div>
</div>
</div>
<div class="field">
<label for="status">Статус</label>
<Dropdown id="status" v-model="campaignForm.status" :options="statusOptions" optionLabel="label" optionValue="value" />
</div>
</div>
<template #footer>
<Button label="Отмена" icon="pi pi-times" @click="cancelEdit" class="p-button-text" />
<Button :label="editingCampaign ? 'Обновить' : 'Создать'" icon="pi pi-check" @click="saveCampaign" :loading="saving" />
</template>
</Dialog>
<!-- Диалог подтверждения удаления -->
<Dialog v-model:visible="showDeleteDialog" header="Подтверждение удаления" :modal="true" :style="{ width: '25vw' }">
<p>Вы уверены, что хотите удалить кампанию "{{ campaignToDelete?.name }}"?</p>
<template #footer>
<Button label="Отмена" icon="pi pi-times" @click="showDeleteDialog = false" class="p-button-text" />
<Button label="Удалить" icon="pi pi-trash" @click="deleteCampaign" class="p-button-danger" :loading="deleting" />
</template>
</Dialog>
</template>
<script>
import CampaignService from '@/service/CampaignService.js';
import { CampaignStatus } from '@/types/smm.js';
import { useToast } from 'primevue/usetoast';
import { onMounted, reactive, ref } from 'vue';
export default {
name: 'CampaignList',
setup() {
const toast = useToast();
// Reactive data
const campaigns = ref([]);
const loading = ref(false);
const saving = ref(false);
const deleting = ref(false);
const showCreateDialog = ref(false);
const showDeleteDialog = ref(false);
const editingCampaign = ref(null);
const campaignToDelete = ref(null);
const filters = ref({
global: { value: null }
});
// Campaign form
const campaignForm = reactive({
name: '',
goal: '',
budget: 0,
startAt: null,
endAt: null,
status: CampaignStatus.PLANNED
});
// Status options
const statusOptions = [
{ label: 'Запланирована', value: CampaignStatus.PLANNED },
{ label: 'Активна', value: CampaignStatus.ACTIVE },
{ label: 'Завершена', value: CampaignStatus.COMPLETED }
];
// Methods
const loadCampaigns = async () => {
loading.value = true;
try {
campaigns.value = await CampaignService.getAllCampaigns();
} catch (error) {
toast.add({
severity: 'error',
summary: 'Ошибка',
detail: 'Не удалось загрузить кампании: ' + error.message,
life: 3000
});
} finally {
loading.value = false;
}
};
const editCampaign = (campaign) => {
editingCampaign.value = campaign;
Object.assign(campaignForm, {
name: campaign.name,
goal: campaign.goal,
budget: campaign.budget,
startAt: campaign.startAt ? new Date(campaign.startAt) : null,
endAt: campaign.endAt ? new Date(campaign.endAt) : null,
status: campaign.status
});
showCreateDialog.value = true;
};
const saveCampaign = async () => {
if (!campaignForm.name) {
toast.add({
severity: 'error',
summary: 'Ошибка валидации',
detail: 'Название кампании обязательно',
life: 3000
});
return;
}
saving.value = true;
try {
const data = {
...campaignForm,
startAt: campaignForm.startAt ? campaignForm.startAt.toISOString() : null,
endAt: campaignForm.endAt ? campaignForm.endAt.toISOString() : null
};
if (editingCampaign.value) {
await CampaignService.updateCampaign(editingCampaign.value.id, data);
toast.add({
severity: 'success',
summary: 'Успех',
detail: 'Кампания обновлена',
life: 3000
});
} else {
await CampaignService.createCampaign(data);
toast.add({
severity: 'success',
summary: 'Успех',
detail: 'Кампания создана',
life: 3000
});
}
await loadCampaigns();
cancelEdit();
} catch (error) {
toast.add({
severity: 'error',
summary: 'Ошибка',
detail: 'Не удалось сохранить кампанию: ' + error.message,
life: 3000
});
} finally {
saving.value = false;
}
};
const cancelEdit = () => {
editingCampaign.value = null;
showCreateDialog.value = false;
Object.assign(campaignForm, {
name: '',
goal: '',
budget: 0,
startAt: null,
endAt: null,
status: CampaignStatus.PLANNED
});
};
const confirmDelete = (campaign) => {
campaignToDelete.value = campaign;
showDeleteDialog.value = true;
};
const deleteCampaign = async () => {
deleting.value = true;
try {
await CampaignService.deleteCampaign(campaignToDelete.value.id);
toast.add({
severity: 'success',
summary: 'Успех',
detail: 'Кампания удалена',
life: 3000
});
await loadCampaigns();
showDeleteDialog.value = false;
campaignToDelete.value = null;
} catch (error) {
toast.add({
severity: 'error',
summary: 'Ошибка',
detail: 'Не удалось удалить кампанию: ' + error.message,
life: 3000
});
} finally {
deleting.value = false;
}
};
const formatCurrency = (value) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'KZT'
}).format(value || 0);
};
const formatDate = (dateString) => {
if (!dateString) return '-';
return new Date(dateString).toLocaleDateString('ru-RU', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const getStatusLabel = (status) => {
const labels = {
[CampaignStatus.PLANNED]: 'Запланирована',
[CampaignStatus.ACTIVE]: 'Активна',
[CampaignStatus.COMPLETED]: 'Завершена'
};
return labels[status] || status;
};
const getStatusSeverity = (status) => {
const severities = {
[CampaignStatus.PLANNED]: 'info',
[CampaignStatus.ACTIVE]: 'success',
[CampaignStatus.COMPLETED]: 'secondary'
};
return severities[status] || 'info';
};
// Lifecycle
onMounted(() => {
loadCampaigns();
});
return {
campaigns,
loading,
saving,
deleting,
showCreateDialog,
showDeleteDialog,
editingCampaign,
campaignToDelete,
filters,
campaignForm,
statusOptions,
loadCampaigns,
editCampaign,
saveCampaign,
cancelEdit,
confirmDelete,
deleteCampaign,
formatCurrency,
formatDate,
getStatusLabel,
getStatusSeverity
};
}
};
</script>
<style scoped>
.card {
background: var(--surface-card);
border-radius: 12px;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.24);
}
.card-header {
padding: 1.5rem;
border-bottom: 1px solid var(--surface-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.card-content {
padding: 1.5rem;
}
.field {
margin-bottom: 1rem;
}
.p-error {
color: var(--red-500);
font-size: 0.875rem;
}
</style>