400 lines
14 KiB
Vue
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>
|