Add Profile/Channel entities with CRUD and remove legacy parser builder.

Introduce reusable ParserProfile and ParseChannel, pair jobs with enqueue flatten, Events admin CRUD, and drop inline/legacy parser-builder UI and aliases.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-16 14:51:03 +03:00
co-authored by Cursor
parent e196de7320
commit 71f8cf169e
30 changed files with 2157 additions and 954 deletions
@@ -0,0 +1,273 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import {
createChannel,
deleteChannel,
fetchChannels,
updateChannel,
} from "../api/entities";
import type { ParseChannel } from "../types/admin";
const channels = ref<ParseChannel[]>([]);
const loading = ref(true);
const error = ref("");
const submitting = ref(false);
const form = ref({
name: "",
channel: "",
is_active: true,
});
const editModal = ref<{
open: boolean;
channel: ParseChannel | null;
name: string;
handle: string;
is_active: boolean;
}>({
open: false,
channel: null,
name: "",
handle: "",
is_active: true,
});
async function loadChannels() {
loading.value = true;
error.value = "";
try {
channels.value = await fetchChannels();
} catch (err) {
error.value = err instanceof Error ? err.message : "Не удалось загрузить каналы";
} finally {
loading.value = false;
}
}
async function handleCreate() {
if (!form.value.name.trim() || !form.value.channel.trim()) {
error.value = "Укажите название и handle канала";
return;
}
submitting.value = true;
error.value = "";
try {
await createChannel({
name: form.value.name.trim(),
channel: form.value.channel.trim(),
source_type: "telegram",
is_active: form.value.is_active,
});
form.value = { name: "", channel: "", is_active: true };
await loadChannels();
} catch (err) {
error.value = err instanceof Error ? err.message : "Не удалось создать канал";
} finally {
submitting.value = false;
}
}
function openEdit(ch: ParseChannel) {
editModal.value = {
open: true,
channel: ch,
name: ch.name,
handle: ch.channel,
is_active: ch.is_active,
};
}
function closeEdit() {
editModal.value.open = false;
editModal.value.channel = null;
}
async function handleSaveEdit() {
if (!editModal.value.channel) return;
if (!editModal.value.name.trim() || !editModal.value.handle.trim()) {
error.value = "Укажите название и handle";
return;
}
submitting.value = true;
error.value = "";
try {
await updateChannel(editModal.value.channel.id, {
name: editModal.value.name.trim(),
channel: editModal.value.handle.trim(),
is_active: editModal.value.is_active,
});
closeEdit();
await loadChannels();
} catch (err) {
error.value = err instanceof Error ? err.message : "Не удалось сохранить";
} finally {
submitting.value = false;
}
}
async function handleDelete(ch: ParseChannel) {
if (!window.confirm(`Удалить канал «${ch.name}» (@${ch.channel})?`)) return;
error.value = "";
try {
await deleteChannel(ch.id);
await loadChannels();
} catch (err) {
error.value = err instanceof Error ? err.message : "Не удалось удалить";
}
}
function formatDate(value: string): string {
return new Date(value).toLocaleString("ru-RU");
}
onMounted(loadChannels);
</script>
<template>
<div class="page">
<h2 class="page-heading">Каналы</h2>
<p class="intro">
Каналы для парсинга (Telegram). Связка канал + профиль создаётся на странице
<router-link to="/parsers">Парсеры</router-link>.
</p>
<section class="card">
<h3>Новый канал</h3>
<form class="admin-form" @submit.prevent="handleCreate">
<div class="form-row">
<label>
Название
<input v-model="form.name" type="text" placeholder="Сводки фронта" required />
</label>
<label>
Handle / id
<input v-model="form.channel" type="text" placeholder="example_channel" required />
</label>
<label class="checkbox-label">
<input v-model="form.is_active" type="checkbox" />
Активен
</label>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" :disabled="submitting">
{{ submitting ? "Создание..." : "Создать канал" }}
</button>
</div>
</form>
<p v-if="error" class="form-error">{{ error }}</p>
</section>
<section class="card">
<h3>Список <span v-if="loading" class="muted">(загрузка...)</span></h3>
<div class="table-wrap">
<table class="admin-table">
<thead>
<tr>
<th>ID</th>
<th>Название</th>
<th>Канал</th>
<th>Тип</th>
<th>Активен</th>
<th>Создан</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="ch in channels" :key="ch.id">
<td>{{ ch.id }}</td>
<td>{{ ch.name }}</td>
<td><code>@{{ ch.channel }}</code></td>
<td>{{ ch.source_type }}</td>
<td>{{ ch.is_active ? "да" : "нет" }}</td>
<td>{{ formatDate(ch.created_at) }}</td>
<td class="actions">
<button type="button" class="btn btn-sm" @click="openEdit(ch)">Изменить</button>
<button type="button" class="btn btn-sm btn-danger" @click="handleDelete(ch)">
Удалить
</button>
</td>
</tr>
<tr v-if="!loading && !channels.length">
<td colspan="7" class="muted">Пока нет каналов</td>
</tr>
</tbody>
</table>
</div>
</section>
<div v-if="editModal.open" class="modal-backdrop" @click.self="closeEdit">
<div class="modal">
<h3>Редактировать канал #{{ editModal.channel?.id }}</h3>
<form class="admin-form" @submit.prevent="handleSaveEdit">
<label>
Название
<input v-model="editModal.name" type="text" required />
</label>
<label>
Handle / id
<input v-model="editModal.handle" type="text" required />
</label>
<label class="checkbox-label">
<input v-model="editModal.is_active" type="checkbox" />
Активен
</label>
<div class="form-actions">
<button type="button" class="btn" @click="closeEdit">Отмена</button>
<button type="submit" class="btn btn-primary" :disabled="submitting">Сохранить</button>
</div>
</form>
</div>
</div>
</div>
</template>
<style scoped>
.intro {
margin: 0 0 1rem;
font-size: 0.9rem;
color: #555;
line-height: 1.5;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 1.5rem;
}
.actions {
display: flex;
gap: 0.35rem;
flex-wrap: wrap;
}
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.35);
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
}
.modal {
background: #fff;
border-radius: 8px;
padding: 1.25rem 1.5rem;
width: min(420px, 92vw);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.modal h3 {
margin: 0 0 1rem;
font-size: 1.05rem;
}
.modal .admin-form label {
display: block;
margin-bottom: 0.75rem;
}
</style>