Add sorting for chapters in book.

This commit is contained in:
mirivlad 2025-11-28 03:47:39 +08:00
parent e1a5403936
commit 70b75f8426
4 changed files with 217 additions and 78 deletions

View File

@ -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) { function cleanChapterContent($content) {
// Удаляем лишние пробелы в начале и конце // Удаляем лишние пробелы в начале и конце
$content = trim($content); $content = trim($content);

View File

@ -133,6 +133,7 @@ $router->add('/books/{book_id}/chapters/create', 'ChapterController@create');
$router->add('/chapters/{id}/edit', 'ChapterController@edit'); $router->add('/chapters/{id}/edit', 'ChapterController@edit');
$router->add('/chapters/{id}/delete', 'ChapterController@delete'); $router->add('/chapters/{id}/delete', 'ChapterController@delete');
$router->add('/chapters/preview', 'ChapterController@preview'); $router->add('/chapters/preview', 'ChapterController@preview');
$router->add('/books/{id}/chapters/update-order', 'ChapterController@updateOrder');
// Серии // Серии
$router->add('/series', 'SeriesController@index'); $router->add('/series', 'SeriesController@index');

View File

@ -20,33 +20,31 @@ class Chapter {
} }
public function findByBook($book_id) { public function findByBook($book_id) {
$stmt = $this->pdo->prepare(" $stmt = $this->pdo->prepare("SELECT * FROM chapters WHERE book_id = ? ORDER BY sort_order ASC, id ASC");
SELECT * FROM chapters
WHERE book_id = ?
ORDER BY sort_order, created_at
");
$stmt->execute([$book_id]); $stmt->execute([$book_id]);
return $stmt->fetchAll(PDO::FETCH_ASSOC); return $stmt->fetchAll(PDO::FETCH_ASSOC);
} }
public function create($data) { public function create($data) {
// Получаем максимальный порядковый номер для этой книги
$stmt = $this->pdo->prepare("SELECT MAX(sort_order) as max_order FROM chapters WHERE book_id = ?"); $stmt = $this->pdo->prepare("SELECT MAX(sort_order) as max_order FROM chapters WHERE book_id = ?");
$stmt->execute([$data['book_id']]); $stmt->execute([$data['book_id']]);
$result = $stmt->fetch(); $result = $stmt->fetch(PDO::FETCH_ASSOC);
$next_order = ($result['max_order'] ?? 0) + 1; $next_order = ($result['max_order'] ?? 0) + 1;
$word_count = $this->countWords($data['content']);
$stmt = $this->pdo->prepare(" $stmt = $this->pdo->prepare("
INSERT INTO chapters (book_id, title, content, sort_order, word_count, status) INSERT INTO chapters (book_id, title, content, word_count, sort_order, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW())
"); ");
$word_count = str_word_count(strip_tags($data['content']));
return $stmt->execute([ return $stmt->execute([
$data['book_id'], $data['book_id'],
$data['title'], $data['title'],
$data['content'], $data['content'],
$next_order,
$word_count, $word_count,
$next_order,
$data['status'] ?? 'draft' $data['status'] ?? 'draft'
]); ]);
} }
@ -108,21 +106,24 @@ class Chapter {
return $stmt->fetchAll(PDO::FETCH_ASSOC); return $stmt->fetchAll(PDO::FETCH_ASSOC);
} }
// private function getAllChapters($book_id) { public function updateChaptersOrder($book_id, $chapter_ids) {
// $stmt = $this->pdo->prepare("SELECT id, content FROM chapters WHERE book_id = ?"); try {
// $stmt->execute([$book_id]); $this->pdo->beginTransaction();
// return $stmt->fetchAll(PDO::FETCH_ASSOC);
// }
// private function updateChapterContent($chapter_id, $content) { // Обновляем порядок для каждой главы
// $word_count = $this->countWords($content); foreach ($chapter_ids as $index => $chapter_id) {
// $stmt = $this->pdo->prepare(" $stmt = $this->pdo->prepare("UPDATE chapters SET sort_order = ? WHERE id = ? AND book_id = ?");
// UPDATE chapters $stmt->execute([$index + 1, $chapter_id, $book_id]);
// SET content = ?, word_count = ?, updated_at = CURRENT_TIMESTAMP }
// WHERE id = ?
// "); $this->pdo->commit();
// return $stmt->execute([$content, $word_count, $chapter_id]); return true;
// } } catch (Exception $e) {
$this->pdo->rollBack();
error_log("Error updating chapters order: " . $e->getMessage());
return false;
}
}
} }
?> ?>

View File

@ -41,23 +41,39 @@ include 'views/layouts/header.php';
</div> </div>
<?php else: ?> <?php else: ?>
<div class="card"> <div class="card">
<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"> <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"> <div class="table-responsive">
<table class="table table-hover"> <table class="table table-hover" id="chapters-table">
<thead> <thead>
<tr> <tr>
<th style="width: 5%;"></th>
<th style="width: 5%;"></th> <th style="width: 5%;"></th>
<th style="width: 40%;">Название главы</th> <th style="width: 35%;">Название главы</th>
<th style="width: 15%;">Статус</th> <th style="width: 15%;">Статус</th>
<th style="width: 10%;">Слов</th> <th style="width: 10%;">Слов</th>
<th style="width: 20%;">Обновлено</th> <th style="width: 20%;">Обновлено</th>
<th style="width: 10%;">Действия</th> <th style="width: 10%;">Действия</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="chapters-list">
<?php foreach ($chapters as $index => $chapter): ?> <?php foreach ($chapters as $index => $chapter): ?>
<tr> <tr data-chapter-id="<?= $chapter['id'] ?>" class="chapter-item">
<td><?= $index + 1 ?></td> <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> <td>
<strong><?= e($chapter['title']) ?></strong> <strong><?= e($chapter['title']) ?></strong>
<?php if ($chapter['content']): ?> <?php if ($chapter['content']): ?>
@ -87,11 +103,13 @@ include 'views/layouts/header.php';
</form> </form>
</div> </div>
</td> </td>
<input type="hidden" name="order[]" value="<?= $chapter['id'] ?>">
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div> </div>
</form>
</div> </div>
</div> </div>
@ -114,4 +132,90 @@ include 'views/layouts/header.php';
<?php endif; ?> <?php endif; ?>
</div> </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'; ?> <?php include 'views/layouts/footer.php'; ?>