Add sorting for chapters in book.
This commit is contained in:
parent
e1a5403936
commit
70b75f8426
|
|
@ -220,7 +220,40 @@ class ChapterController extends BaseController {
|
|||
]);
|
||||
}
|
||||
|
||||
// Добавьте эту функцию в начало файла
|
||||
public function updateOrder($book_id) {
|
||||
$this->requireLogin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
return $this->jsonResponse(['success' => false, 'error' => 'Неверный метод запроса']);
|
||||
}
|
||||
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
return $this->jsonResponse(['success' => false, 'error' => 'Ошибка безопасности']);
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$chapterModel = new Chapter($this->pdo);
|
||||
$bookModel = new Book($this->pdo);
|
||||
|
||||
// Проверяем права доступа к книге
|
||||
if (!$bookModel->userOwnsBook($book_id, $user_id)) {
|
||||
return $this->jsonResponse(['success' => false, 'error' => 'У вас нет доступа к этой книге']);
|
||||
}
|
||||
|
||||
$order_data = $_POST['order'] ?? [];
|
||||
|
||||
if (empty($order_data)) {
|
||||
return $this->jsonResponse(['success' => false, 'error' => 'Нет данных для обновления']);
|
||||
}
|
||||
|
||||
// Обновляем порядок глав
|
||||
if ($chapterModel->updateChaptersOrder($book_id, $order_data)) {
|
||||
return $this->jsonResponse(['success' => true]);
|
||||
} else {
|
||||
return $this->jsonResponse(['success' => false, 'error' => 'Ошибка при обновлении порядка глав']);
|
||||
}
|
||||
}
|
||||
|
||||
function cleanChapterContent($content) {
|
||||
// Удаляем лишние пробелы в начале и конце
|
||||
$content = trim($content);
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ $router->add('/books/{book_id}/chapters/create', 'ChapterController@create');
|
|||
$router->add('/chapters/{id}/edit', 'ChapterController@edit');
|
||||
$router->add('/chapters/{id}/delete', 'ChapterController@delete');
|
||||
$router->add('/chapters/preview', 'ChapterController@preview');
|
||||
$router->add('/books/{id}/chapters/update-order', 'ChapterController@updateOrder');
|
||||
|
||||
// Серии
|
||||
$router->add('/series', 'SeriesController@index');
|
||||
|
|
|
|||
|
|
@ -20,33 +20,31 @@ class Chapter {
|
|||
}
|
||||
|
||||
public function findByBook($book_id) {
|
||||
$stmt = $this->pdo->prepare("
|
||||
SELECT * FROM chapters
|
||||
WHERE book_id = ?
|
||||
ORDER BY sort_order, created_at
|
||||
");
|
||||
$stmt = $this->pdo->prepare("SELECT * FROM chapters WHERE book_id = ? ORDER BY sort_order ASC, id ASC");
|
||||
$stmt->execute([$book_id]);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
public function create($data) {
|
||||
// Получаем максимальный порядковый номер для этой книги
|
||||
$stmt = $this->pdo->prepare("SELECT MAX(sort_order) as max_order FROM chapters WHERE book_id = ?");
|
||||
$stmt->execute([$data['book_id']]);
|
||||
$result = $stmt->fetch();
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$next_order = ($result['max_order'] ?? 0) + 1;
|
||||
|
||||
$word_count = $this->countWords($data['content']);
|
||||
|
||||
|
||||
$stmt = $this->pdo->prepare("
|
||||
INSERT INTO chapters (book_id, title, content, sort_order, word_count, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO chapters (book_id, title, content, word_count, sort_order, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
");
|
||||
|
||||
$word_count = str_word_count(strip_tags($data['content']));
|
||||
|
||||
return $stmt->execute([
|
||||
$data['book_id'],
|
||||
$data['title'],
|
||||
$data['content'],
|
||||
$next_order,
|
||||
$word_count,
|
||||
$next_order,
|
||||
$data['status'] ?? 'draft'
|
||||
]);
|
||||
}
|
||||
|
|
@ -108,21 +106,24 @@ class Chapter {
|
|||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
// private function getAllChapters($book_id) {
|
||||
// $stmt = $this->pdo->prepare("SELECT id, content FROM chapters WHERE book_id = ?");
|
||||
// $stmt->execute([$book_id]);
|
||||
// return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
// }
|
||||
|
||||
// private function updateChapterContent($chapter_id, $content) {
|
||||
// $word_count = $this->countWords($content);
|
||||
// $stmt = $this->pdo->prepare("
|
||||
// UPDATE chapters
|
||||
// SET content = ?, word_count = ?, updated_at = CURRENT_TIMESTAMP
|
||||
// WHERE id = ?
|
||||
// ");
|
||||
// return $stmt->execute([$content, $word_count, $chapter_id]);
|
||||
// }
|
||||
public function updateChaptersOrder($book_id, $chapter_ids) {
|
||||
try {
|
||||
$this->pdo->beginTransaction();
|
||||
|
||||
// Обновляем порядок для каждой главы
|
||||
foreach ($chapter_ids as $index => $chapter_id) {
|
||||
$stmt = $this->pdo->prepare("UPDATE chapters SET sort_order = ? WHERE id = ? AND book_id = ?");
|
||||
$stmt->execute([$index + 1, $chapter_id, $book_id]);
|
||||
}
|
||||
|
||||
$this->pdo->commit();
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$this->pdo->rollBack();
|
||||
error_log("Error updating chapters order: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
|
|
@ -41,58 +41,76 @@ include 'views/layouts/header.php';
|
|||
</div>
|
||||
<?php else: ?>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 5%;">№</th>
|
||||
<th style="width: 40%;">Название главы</th>
|
||||
<th style="width: 15%;">Статус</th>
|
||||
<th style="width: 10%;">Слов</th>
|
||||
<th style="width: 20%;">Обновлено</th>
|
||||
<th style="width: 10%;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($chapters as $index => $chapter): ?>
|
||||
<tr>
|
||||
<td><?= $index + 1 ?></td>
|
||||
<td>
|
||||
<strong><?= e($chapter['title']) ?></strong>
|
||||
<?php if ($chapter['content']): ?>
|
||||
<br><small class="text-muted"><?= e(mb_strimwidth($chapter['content'], 0, 100, '...')) ?></small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge <?= $chapter['status'] == 'published' ? 'bg-success' : 'bg-warning' ?>">
|
||||
<?= $chapter['status'] == 'published' ? 'Опубликована' : 'Черновик' ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?= $chapter['word_count'] ?></td>
|
||||
<td>
|
||||
<small><?= date('d.m.Y H:i', strtotime($chapter['updated_at'])) ?></small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="<?= SITE_URL ?>/chapters/<?= $chapter['id'] ?>/edit" class="btn btn-outline-primary" title="Редактировать">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form method="post" action="<?= SITE_URL ?>/chapters/<?= $chapter['id'] ?>/delete"
|
||||
onsubmit="return confirm('Вы уверены, что хотите удалить эту главу? Это действие нельзя отменить.');">
|
||||
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
|
||||
<button type="submit" class="btn btn-outline-danger" title="Удалить">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">Список глав</h5>
|
||||
<div>
|
||||
<small class="text-muted me-3">Перетащите для изменения порядка</small>
|
||||
<button type="button" id="save-order-btn" class="btn btn-success btn-sm" style="display: none;">
|
||||
<i class="bi bi-check-circle"></i> Сохранить порядок
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="chapters-order-form" method="post" action="<?= SITE_URL ?>/books/<?= $book['id'] ?>/chapters/update-order">
|
||||
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover" id="chapters-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 5%;"></th>
|
||||
<th style="width: 5%;">№</th>
|
||||
<th style="width: 35%;">Название главы</th>
|
||||
<th style="width: 15%;">Статус</th>
|
||||
<th style="width: 10%;">Слов</th>
|
||||
<th style="width: 20%;">Обновлено</th>
|
||||
<th style="width: 10%;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="chapters-list">
|
||||
<?php foreach ($chapters as $index => $chapter): ?>
|
||||
<tr data-chapter-id="<?= $chapter['id'] ?>" class="chapter-item">
|
||||
<td class="drag-handle text-muted" style="cursor: move;">
|
||||
<i class="bi bi-grip-vertical"></i>
|
||||
</td>
|
||||
<td class="chapter-order"><?= $index + 1 ?></td>
|
||||
<td>
|
||||
<strong><?= e($chapter['title']) ?></strong>
|
||||
<?php if ($chapter['content']): ?>
|
||||
<br><small class="text-muted"><?= e(mb_strimwidth($chapter['content'], 0, 100, '...')) ?></small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge <?= $chapter['status'] == 'published' ? 'bg-success' : 'bg-warning' ?>">
|
||||
<?= $chapter['status'] == 'published' ? 'Опубликована' : 'Черновик' ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?= $chapter['word_count'] ?></td>
|
||||
<td>
|
||||
<small><?= date('d.m.Y H:i', strtotime($chapter['updated_at'])) ?></small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="<?= SITE_URL ?>/chapters/<?= $chapter['id'] ?>/edit" class="btn btn-outline-primary" title="Редактировать">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form method="post" action="<?= SITE_URL ?>/chapters/<?= $chapter['id'] ?>/delete"
|
||||
onsubmit="return confirm('Вы уверены, что хотите удалить эту главу? Это действие нельзя отменить.');">
|
||||
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
|
||||
<button type="submit" class="btn btn-outline-danger" title="Удалить">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
<input type="hidden" name="order[]" value="<?= $chapter['id'] ?>">
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 p-3 bg-light rounded">
|
||||
|
|
@ -114,4 +132,90 @@ include 'views/layouts/header.php';
|
|||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($chapters)): ?>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const chaptersList = document.getElementById('chapters-list');
|
||||
const saveOrderBtn = document.getElementById('save-order-btn');
|
||||
const orderForm = document.getElementById('chapters-order-form');
|
||||
|
||||
if (chaptersList) {
|
||||
const sortable = new Sortable(chaptersList, {
|
||||
handle: '.drag-handle',
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
animation: 150,
|
||||
onUpdate: function() {
|
||||
saveOrderBtn.style.display = 'block';
|
||||
updateChapterNumbers();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для обновления номеров глав
|
||||
function updateChapterNumbers() {
|
||||
const orderNumbers = document.querySelectorAll('.chapter-order');
|
||||
orderNumbers.forEach((element, index) => {
|
||||
element.textContent = index + 1;
|
||||
});
|
||||
}
|
||||
|
||||
// Обработчик сохранения порядка
|
||||
saveOrderBtn.addEventListener('click', function() {
|
||||
const formData = new FormData(orderForm);
|
||||
|
||||
fetch(orderForm.action, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
saveOrderBtn.innerHTML = '<i class="bi bi-check-circle"></i> Порядок сохранен';
|
||||
saveOrderBtn.classList.remove('btn-success');
|
||||
saveOrderBtn.classList.add('btn-secondary');
|
||||
|
||||
setTimeout(() => {
|
||||
saveOrderBtn.style.display = 'none';
|
||||
saveOrderBtn.innerHTML = '<i class="bi bi-check-circle"></i> Сохранить порядок';
|
||||
saveOrderBtn.classList.remove('btn-secondary');
|
||||
saveOrderBtn.classList.add('btn-success');
|
||||
}, 2000);
|
||||
} else {
|
||||
alert('Ошибка: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Произошла ошибка при сохранении порядка');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.chapter-item {
|
||||
transition: background-color 0.2s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.chapter-item:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.chapter-item.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.chapter-item.sortable-chosen {
|
||||
background: #e3f2fd;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
</style>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php include 'views/layouts/footer.php'; ?>
|
||||
Loading…
Reference in New Issue