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:
@@ -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>
|
||||
Reference in New Issue
Block a user