253 lines
11 KiB
Vue
253 lines
11 KiB
Vue
<template>
|
||
<div>
|
||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||
<h2>Gönderi Yönetimi</h2>
|
||
<NuxtLink to="/admin/blog/posts/create" class="btn btn-primary">
|
||
<i class="fas fa-plus me-2"></i> Yeni Gönderi
|
||
</NuxtLink>
|
||
</div>
|
||
|
||
<!-- Filter / Search -->
|
||
<div class="card mb-4">
|
||
<div class="card-body">
|
||
<div class="row g-3">
|
||
<div class="col-md-4">
|
||
<select v-model="filterStatus" class="form-select" @change="fetchPosts(1)">
|
||
<option value="">Tümü</option>
|
||
<option value="only">Silinenler (Çöp Kutusu)</option>
|
||
<option value="with">Tümü (Silinenler Dahil)</option>
|
||
</select>
|
||
</div>
|
||
<!-- Future: Add search input here -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Posts List -->
|
||
<div class="card mb-4">
|
||
<div class="card-body">
|
||
<div v-if="loading" class="text-center py-5">
|
||
<div class="spinner-border text-primary" role="status">
|
||
<span class="visually-hidden">Yükleniyor...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else-if="posts.length === 0" class="text-center py-5 text-muted">
|
||
Kayıt bulunamadı.
|
||
</div>
|
||
|
||
<div v-else class="table-responsive">
|
||
<table class="table table-hover align-middle">
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>Resim</th>
|
||
<th>Başlık</th>
|
||
<th>Kategoriler</th>
|
||
<th>Etiketler</th>
|
||
<th>Durum</th>
|
||
<th>Tarih</th>
|
||
<th class="text-end">İşlemler</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="post in posts" :key="post.ID">
|
||
<td>{{ post.ID }}</td>
|
||
<td>
|
||
<img v-if="getFirstImage(post.images)" :src="getImageUrl(getFirstImage(post.images)!)"
|
||
alt="thumbnail" class="rounded" style="width: 50px; height: 50px; object-fit: cover;">
|
||
<div v-else class="bg-light rounded d-flex align-items-center justify-content-center text-muted"
|
||
style="width: 50px; height: 50px;">
|
||
<i class="fas fa-image"></i>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<div class="fw-bold">{{ post.title }}</div>
|
||
<small class="text-muted">{{ post.slug }}</small>
|
||
</td>
|
||
<td>
|
||
<span v-for="cat in post.categories" :key="cat.ID" class="badge bg-info text-dark me-1">
|
||
{{ cat.title }}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<span v-for="tag in post.tags" :key="tag.ID" class="badge bg-secondary me-1">
|
||
{{ tag.name }}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<span v-if="post.DeletedAt" class="badge bg-danger">Silindi</span>
|
||
<span v-else class="badge bg-success">Aktif</span>
|
||
</td>
|
||
<td>
|
||
<div>{{ formatDate(post.CreatedAt) }}</div>
|
||
</td>
|
||
<td class="text-end">
|
||
<div v-if="!post.DeletedAt">
|
||
<NuxtLink :to="`/admin/blog/posts/${post.ID}`" class="btn btn-sm btn-outline-primary me-2">
|
||
<i class="fas fa-edit"></i>
|
||
</NuxtLink>
|
||
<button class="btn btn-sm btn-outline-danger" @click="deletePost(post.ID)">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
<div v-else>
|
||
<button class="btn btn-sm btn-outline-success" @click="restorePost(post.ID)">
|
||
<i class="fas fa-trash-restore"></i> Geri Yükle
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Pagination -->
|
||
<nav v-if="pagination.total > pagination.per_page" class="mt-4">
|
||
<ul class="pagination justify-content-center">
|
||
<li class="page-item" :class="{ disabled: pagination.page === 1 }">
|
||
<button class="page-link" @click="fetchPosts(pagination.page - 1)">Önceki</button>
|
||
</li>
|
||
<li v-for="page in Math.ceil(pagination.total / pagination.per_page)" :key="page"
|
||
class="page-item" :class="{ active: pagination.page === page }">
|
||
<button class="page-link" @click="fetchPosts(page)">{{ page }}</button>
|
||
</li>
|
||
<li class="page-item" :class="{ disabled: pagination.page >= Math.ceil(pagination.total / pagination.per_page) }">
|
||
<button class="page-link" @click="fetchPosts(pagination.page + 1)">Sonraki</button>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import type { Post, PostResponse } from '~~/types/post';
|
||
import Swal from 'sweetalert2';
|
||
|
||
definePageMeta({
|
||
layout: 'admin',
|
||
middleware: 'admin'
|
||
});
|
||
|
||
const config = useRuntimeConfig();
|
||
const { data: authData } = useAuth();
|
||
const posts = ref<Post[]>([]);
|
||
const loading = ref(false);
|
||
const filterStatus = ref(''); // '' = active (default from API perspective usually?), or we need to handle default
|
||
// Default listing in doc: /api/v1/admin/posts
|
||
// Trashed only: ?trashed=only
|
||
// With trashed: ?trashed=with
|
||
|
||
const pagination = ref({
|
||
page: 1,
|
||
per_page: 10,
|
||
total: 0
|
||
});
|
||
|
||
const fetchPosts = async (page = 1) => {
|
||
loading.value = true;
|
||
try {
|
||
const query: any = { page, limit: pagination.value.per_page };
|
||
if (filterStatus.value) {
|
||
query.trashed = filterStatus.value;
|
||
}
|
||
|
||
const res = await $fetch<PostResponse>('/api/v1/admin/posts', {
|
||
baseURL: config.public.BASE_API_URL,
|
||
headers: { Authorization: `Bearer ${(authData.value as any)?.accessToken}` },
|
||
query
|
||
});
|
||
|
||
posts.value = res.data || [];
|
||
pagination.value = res.meta;
|
||
} catch (e) {
|
||
console.error(e);
|
||
posts.value = [];
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
const deletePost = async (id: number) => {
|
||
const result = await Swal.fire({
|
||
title: 'Emin misiniz?',
|
||
text: "Bu gönderi silinecek (Çöp kutusuna taşınacak).",
|
||
icon: 'warning',
|
||
showCancelButton: true,
|
||
confirmButtonText: 'Evet, Sil',
|
||
cancelButtonText: 'İptal',
|
||
confirmButtonColor: '#d33'
|
||
});
|
||
|
||
if (!result.isConfirmed) return;
|
||
|
||
try {
|
||
await $fetch(`/api/v1/posts/${id}`, {
|
||
method: 'DELETE',
|
||
baseURL: config.public.BASE_API_URL,
|
||
headers: { Authorization: `Bearer ${(authData.value as any)?.accessToken}` }
|
||
});
|
||
|
||
Swal.fire('Silindi', 'Gönderi silindi.', 'success');
|
||
fetchPosts(pagination.value.page);
|
||
} catch (error) {
|
||
console.error(error);
|
||
Swal.fire('Hata', 'Silme işlemi başarısız.', 'error');
|
||
}
|
||
};
|
||
|
||
const restorePost = async (id: number) => {
|
||
// Implement restore logic if API supports it, otherwise basic delete is usually soft delete.
|
||
// If current delete IS soft delete, usually there's a restore endpoint or update with deleted_at=null.
|
||
// Assuming for now we just show it for concept, but strict restore endpoint might be needed.
|
||
// For now, let's keep it simple. If no explicit restore endpoint documented, maybe we can't restore easily from UI yet.
|
||
// But wait, the user asked to implement based on admin_post.md.
|
||
// Use case "sadece soft delete olmuslar listesi" implies we can see them.
|
||
// Usually a PUT to /api/v1/admin/posts/{id}/restore or similar.
|
||
// Since not documented, I will skip implementation of ACTUAL restore call but keep the button or maybe comment it out
|
||
// until confirmed. Or I can assume standard behaviour if I knew the backend.
|
||
// Let's assume standard soft delete REST pattern often allows toggle or specific endpoint.
|
||
// I'll leave the button but functionality might be missing.
|
||
// Actually, let's look at the docs again.
|
||
// Docs only show GET lists.
|
||
// I will remove Restore button functionality for now to avoid errors, or just show alert 'Not implemented'.
|
||
Swal.fire('Bilgi', 'Geri yükleme özelliği henüz API tarafında dökümante edilmedi.', 'info');
|
||
};
|
||
|
||
const getFirstImage = (imagesStr: string): string | null => {
|
||
try {
|
||
// API returns "images": "uploads/posts/..." OR JSON string "[\"...\"]"
|
||
// Let's try to parse if it looks like JSON
|
||
if (imagesStr.startsWith('[') || imagesStr.startsWith('{')) {
|
||
const parsed = JSON.parse(imagesStr);
|
||
if (Array.isArray(parsed) && parsed.length > 0) return parsed[0];
|
||
}
|
||
return imagesStr;
|
||
} catch (e) {
|
||
return imagesStr;
|
||
}
|
||
};
|
||
|
||
const getImageUrl = (path: string) => {
|
||
if (path.startsWith('http')) return path;
|
||
if (path.startsWith('/')) return `${config.public.BASE_API_URL}${path}`;
|
||
return `${config.public.BASE_API_URL}/${path}`;
|
||
};
|
||
|
||
const formatDate = (dateStr: string) => {
|
||
return new Date(dateStr).toLocaleDateString('tr-TR', {
|
||
year: 'numeric',
|
||
month: 'short',
|
||
day: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
};
|
||
|
||
onMounted(() => {
|
||
fetchPosts();
|
||
});
|
||
</script>
|