Files
clipperia/src/routes/videos/index.vue
2025-11-03 16:31:28 -03:00

407 lines
8.8 KiB
Vue

<template>
<div class="videos-page">
<section class="app-page__header">
<div>
<h2 class="app-page__title">Biblioteca de vídeos</h2>
<p class="app-page__subtitle">
Filtre os vídeos processados pela IA, acompanhe o status dos recortes e inicie novas captações
em poucos cliques.
</p>
</div>
<div class="app-actions-bar">
<Button
variant="ghost"
icon="sym_o_refresh"
label="Atualizar"
@click="handleSearch(pagination)"
/>
<Button icon="sym_o_add" label="Novo vídeo" @click="handleAddVideo" />
</div>
</section>
<q-card flat bordered class="videos-page__filters app-card">
<div class="videos-page__filters-head">
<div>
<h3>Filtrar resultados</h3>
<span>Refine a busca por título, identificador ou situação.</span>
</div>
<div class="videos-page__filters-actions">
<Button
variant="ghost"
color="secondary"
text-color="primary"
icon="sym_o_close"
label="Limpar filtros"
:disabled="!hasFilters"
@click="resetFilters"
/>
</div>
</div>
<div class="videos-page__filters-grid">
<TextField
v-model="filters.title"
label="Título"
placeholder="Ex.: Cortes da live com convidados"
>
<template #prepend>
<q-icon name="sym_o_title" color="primary" />
</template>
</TextField>
<TextField
v-model="filters.videoId"
label="Video ID"
placeholder="Ex.: x9-YRAYhesI"
>
<template #prepend>
<q-icon name="sym_o_confirmation_number" color="primary" />
</template>
</TextField>
<Dropdown
v-model="filters.situations"
label="Situação"
clearable
:options="situations"
:loading="situationsLoading"
multiple
emit-value
map-options
/>
</div>
<div class="videos-page__filters-footer">
<Button
label="Buscar vídeos"
icon="sym_o_search"
:loading="loading"
:disabled="loading"
@click="handleSearch({ ...pagination, page: 1 })"
/>
</div>
</q-card>
<Table
key="videos-table"
class="videos-page__table"
title="Resultados"
subtitle="Veja os vídeos cadastrados, a quantidade de clipes e o status de processamento."
:columns="columns"
:rows="rows"
:pagination="pagination"
:loading="loading"
@update:pagination="updatePagination"
>
<template #actions>
<Button
variant="ghost"
icon="sym_o_download"
label="Exportar CSV"
@click="handleExport"
/>
</template>
<template #empty-message>
Nenhum vídeo encontrado para os filtros selecionados.
</template>
<template #body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
<div class="videos-page__cell">
<div class="videos-page__cell-label">{{ col.label }}</div>
<div class="videos-page__cell-value">{{ col.value }}</div>
</div>
</q-td>
</q-tr>
</template>
</Table>
</div>
</template>
<script>
import Button from "@components/Button";
import Table from "@components/Table";
import TextField from "@components/TextField";
import Dropdown from "@components/Dropdown";
import { API } from "@config/axios";
import { getErrorMessage } from "@utils/axios";
const columns = [
{
name: "id",
label: "Identificador",
align: "left",
field: "id",
},
{
name: "title",
label: "Título",
align: "left",
field: "title",
},
{
name: "clips_quantity",
label: "Clipes",
align: "center",
field: "clips_quantity",
},
{
name: "videoid",
label: "Video ID",
field: "videoid",
},
{
name: "situation",
label: "Situação",
field: "situation",
},
];
export default {
name: "VideosList",
components: {
Button,
Table,
TextField,
Dropdown,
},
data() {
return {
columns,
rows: [],
loading: false,
pagination: {
page: 1,
direction: "desc",
perPage: 10,
total: 0,
totalPages: 1,
hasNext: false,
hasPrev: false,
},
situations: [],
situationsLoading: false,
filters: {
title: "",
videoId: "",
situations: [],
},
};
},
computed: {
hasFilters() {
return (
!!this.filters.title ||
!!this.filters.videoId ||
(Array.isArray(this.filters.situations) && this.filters.situations.length > 0)
);
},
},
created() {
this.initializePage();
},
methods: {
async initializePage() {
await this.getSituation();
await this.handleSearch(this.pagination);
},
async handleSearch(pagination) {
try {
this.loading = true;
const baseParams = {};
if (this.filters.title) {
baseParams.title = this.filters.title;
}
if (this.filters.videoId) {
baseParams.videoId = this.filters.videoId;
}
if (Array.isArray(this.filters.situations) && this.filters.situations.length) {
baseParams.situation = this.filters.situations;
}
const { data } = await API.get("/videos", {
params: {
perPage: pagination.perPage,
page: pagination.page,
...baseParams,
},
paramsSerializer: {
indexes: null,
},
});
this.rows = data.content;
this.pagination = data.pagination;
} catch (error) {
this.$q.notify({
type: "negative",
message: getErrorMessage(error, "Erro ao buscar vídeos"),
});
} finally {
this.loading = false;
}
},
updatePagination(pagination) {
this.handleSearch(pagination);
},
async getSituation() {
try {
this.situationsLoading = true;
const { data } = await API.get("/videos/situacoes");
this.situations = data;
} catch (error) {
this.$q.notify({
type: "negative",
message: getErrorMessage(error, "Erro ao buscar situações"),
});
} finally {
this.situationsLoading = false;
}
},
handleAddVideo() {
this.$router.push("/videos/new");
},
resetFilters() {
this.filters = {
title: "",
videoId: "",
situations: [],
};
this.handleSearch({ ...this.pagination, page: 1 });
},
handleExport() {
if (!this.rows.length) {
this.$q.notify({
type: "warning",
message: "Não há dados para exportar. Ajuste os filtros e tente novamente.",
});
return;
}
this.$q.notify({
type: "info",
message: "Exportação em CSV disponível em breve.",
});
},
},
};
</script>
<style scoped lang="scss">
.videos-page {
display: flex;
flex-direction: column;
gap: 32px;
}
.videos-page__filters h3 {
margin: 0;
font-size: 18px;
font-weight: 700;
color: #101828;
}
.videos-page__filters span {
color: #475467;
font-size: 14px;
}
.videos-page__filters-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 24px;
}
.videos-page__filters-actions {
display: flex;
align-items: center;
}
.videos-page__filters-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
}
.videos-page__filters-footer {
display: flex;
justify-content: flex-end;
margin-top: 24px;
}
.videos-page__table {
margin-bottom: 32px;
}
.videos-page__cell {
display: flex;
flex-direction: column;
gap: 4px;
}
.videos-page__cell-label {
display: none;
font-size: 12px;
font-weight: 600;
color: #98a2b3;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.videos-page__cell-value {
font-size: 15px;
font-weight: 600;
color: #101828;
}
body.body--dark .videos-page__filters h3 {
color: rgba(255, 255, 255, 0.92);
}
body.body--dark .videos-page__filters span {
color: rgba(255, 255, 255, 0.7);
}
body.body--dark .videos-page__cell-value {
color: rgba(255, 255, 255, 0.9);
}
@media (max-width: 768px) {
.videos-page__filters-head {
flex-direction: column;
align-items: flex-start;
}
.app-actions-bar {
width: 100%;
justify-content: flex-start;
}
.videos-page__filters-footer {
justify-content: stretch;
}
.videos-page__filters-footer > * {
width: 100%;
}
.videos-page__cell-label {
display: block;
}
}
</style>