459 lines
20 KiB
Python
459 lines
20 KiB
Python
with open("src/main/resources/templates/plugins/swiper-slider/admin-form.html", "w", encoding="utf-8") as f:
|
|
f.write("""<html
|
|
lang="en"
|
|
xmlns:th="http://www.thymeleaf.org"
|
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
|
layout:decorate="~{fragments/manage-layout}"
|
|
>
|
|
<head>
|
|
<title th:text="${pageTitle} + ' - SIS Vietnam'">Swiper Slider Settings</title>
|
|
<style>
|
|
.item-card {
|
|
border: 1px solid #ddd;
|
|
margin-bottom: 1rem;
|
|
padding: 1rem;
|
|
border-radius: 5px;
|
|
background: #fff;
|
|
}
|
|
.tab-field {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.tab-field label {
|
|
display: block;
|
|
font-weight: bold;
|
|
margin-bottom: 0.2rem;
|
|
}
|
|
.tab-field input,
|
|
.tab-field textarea,
|
|
.tab-field select {
|
|
width: 100%;
|
|
padding: 0.5rem;
|
|
border: 1px solid #ccc;
|
|
border-radius: 3px;
|
|
}
|
|
.tab-field textarea {
|
|
height: 80px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div layout:fragment="content">
|
|
<div class="container-fluid">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h1 class="h3 mb-0 text-gray-800" th:text="${pageTitle}">Sửa Nhóm Slider</h1>
|
|
<a th:href="@{/manage/plugins/swiper-slider}" class="btn btn-secondary shadow-sm"
|
|
><i class="fas fa-arrow-left fa-sm text-white-50"></i> Quay lại Danh sách</a
|
|
>
|
|
</div>
|
|
|
|
<div th:if="${errorMessage}" class="alert alert-danger" th:text="${errorMessage}"></div>
|
|
|
|
<form method="post" th:action="@{/manage/plugins/swiper-slider/save}" id="sliderForm">
|
|
<input type="hidden" name="groupData" id="groupData" />
|
|
<input type="hidden" name="oldSlug" th:value="${oldSlug}" />
|
|
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">Thông tin cơ bản</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row mb-3">
|
|
<div class="col-md-6 tab-field">
|
|
<label>Tên Nhóm (Chỉ để quản lý)</label>
|
|
<input
|
|
type="text"
|
|
class="form-control"
|
|
onchange="updateGroup('name', this.value)"
|
|
id="groupName"
|
|
placeholder="Ví dụ: Đào tạo - Chứng chỉ"
|
|
required
|
|
/>
|
|
</div>
|
|
<div class="col-md-6 tab-field">
|
|
<label>Slug (Dùng cho Shortcode)</label>
|
|
<input
|
|
type="text"
|
|
class="form-control"
|
|
onchange="updateGroup('slug', this.value)"
|
|
id="groupSlug"
|
|
placeholder="Ví dụ: chung-chi"
|
|
required
|
|
/>
|
|
</div>
|
|
<div class="col-md-6 tab-field mt-3">
|
|
<label>Ghi chú (Hiển thị ở danh sách)</label>
|
|
<input
|
|
type="text"
|
|
class="form-control"
|
|
onchange="updateGroup('note', this.value)"
|
|
id="groupNote"
|
|
placeholder="Ví dụ: Dùng cho trang chủ"
|
|
/>
|
|
</div>
|
|
<div class="col-md-6 tab-field mt-3">
|
|
<label>Mẫu Giao Diện (Layout Type)</label>
|
|
<select
|
|
class="form-control"
|
|
onchange="
|
|
updateGroup('layoutType', this.value);
|
|
toggleCustomTemplates(this.value);
|
|
"
|
|
id="groupLayoutType"
|
|
>
|
|
<option value="">Mặc định (Dựa theo Slug, hỗ trợ tương thích ngược)</option>
|
|
<option value="gallery">Mẫu 1: Thư viện ảnh (Gallery - ảnh chữ nhật nhỏ)</option>
|
|
<option value="card">Mẫu 2: Dạng thẻ (Khóa học/Chứng chỉ - ảnh vuông + tiêu đề)</option>
|
|
<option value="large-image">Mẫu 3: Hình ảnh lớn (Thực hành - ảnh chữ nhật lớn)</option>
|
|
<option value="custom">Tùy chỉnh (Nhập mã HTML bên dưới)</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6 tab-field mt-3">
|
|
<label>Nguồn Dữ Liệu</label>
|
|
<select
|
|
class="form-control"
|
|
onchange="
|
|
updateGroup('dataSource', this.value);
|
|
toggleDataSource(this.value);
|
|
"
|
|
id="groupDataSource"
|
|
>
|
|
<option value="MANUAL">Nhập thủ công</option>
|
|
<option value="POST">Bài viết (Posts)</option>
|
|
<option value="PAGE">Trang (Pages)</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6 tab-field mt-3 dynamic-source-container" style="display: none">
|
|
<div>
|
|
<label>Chuyên mục (Categories) (Tùy chọn)</label>
|
|
<select class="form-control" onchange="updateMultiGroup('categoryIds', this)" id="groupCategoryIds">
|
|
<option value="">-- Chọn chuyên mục --</option>
|
|
<option th:each="cat : ${allCategories}" th:value="${cat.id}" th:text="${cat.name}"></option>
|
|
</select>
|
|
</div>
|
|
<div id="selected_categoryIds"></div>
|
|
</div>
|
|
<div class="col-md-6 tab-field mt-3 dynamic-source-container" style="display: none">
|
|
<div>
|
|
<label>Thẻ (Tags) (Tùy chọn)</label>
|
|
<select class="form-control" onchange="updateMultiGroup('tagIds', this)" id="groupTagIds">
|
|
<option value="">-- Chọn thẻ --</option>
|
|
<option th:each="tag : ${allTags}" th:value="${tag.id}" th:text="${tag.name}"></option>
|
|
</select>
|
|
</div>
|
|
<div id="selected_tagIds"></div>
|
|
</div>
|
|
<div class="col-md-6 tab-field mt-3 dynamic-source-container" style="display: none">
|
|
<label>Số lượng hiển thị (Tùy chọn)</label>
|
|
<input
|
|
type="number"
|
|
class="form-control"
|
|
onchange="updateGroup('itemLimit', this.value ? parseInt(this.value) : null)"
|
|
id="groupItemLimit"
|
|
placeholder="Ví dụ: 10"
|
|
/>
|
|
</div>
|
|
<div class="col-md-6 tab-field mt-3 dynamic-source-container" style="display: none">
|
|
<div class="form-check mt-4">
|
|
<input
|
|
class="form-check-input"
|
|
type="checkbox"
|
|
id="groupUseExcerptAsDescription"
|
|
onchange="updateGroup('useExcerptAsDescription', this.checked)"
|
|
/>
|
|
<label class="form-check-label" for="groupUseExcerptAsDescription"> Dùng Excerpt làm Description </label>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-12 tab-field mt-3 custom-template-container" style="display: none">
|
|
<label>Outer Template (Cấu trúc bao ngoài Slider, tùy chọn)</label>
|
|
<textarea
|
|
class="form-control"
|
|
style="font-family: monospace; height: 120px"
|
|
onchange="updateGroup('outerTemplate', this.value)"
|
|
id="groupOuterTemplate"
|
|
placeholder="Ví dụ: <div class='swiper swiper-{{slug}}'><div class='swiper-wrapper'>{{#each items}}{{/each}}</div></div>"
|
|
></textarea>
|
|
<small class="text-muted"
|
|
>Dùng <code>{{slug}}</code> cho slug của nhóm và <code>{{#each items}}{{/each}}</code> để đánh dấu nơi đặt nội dung
|
|
slides.</small
|
|
>
|
|
</div>
|
|
<div class="col-md-12 tab-field mt-2 custom-template-container" style="display: none">
|
|
<label>Item Template (Cấu trúc 1 Slide con, tùy chọn)</label>
|
|
<textarea
|
|
class="form-control"
|
|
style="font-family: monospace; height: 100px"
|
|
onchange="updateGroup('itemTemplate', this.value)"
|
|
id="groupItemTemplate"
|
|
placeholder="Ví dụ: <div class='swiper-slide'><img src='{{imageUrl}}' alt='{{title}}'></div>"
|
|
></textarea>
|
|
<small class="text-muted"
|
|
>Các biến hỗ trợ: <code>{{title}}</code>, <code>{{imageUrl}}</code>, <code>{{linkUrl}}</code>,
|
|
<code>{{description}}</code>.</small
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
|
<h6 class="m-0 font-weight-bold text-primary">Danh sách Slides</h6>
|
|
<button type="button" class="btn btn-sm btn-info" onclick="addItem()">+ Thêm Slide Mới</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="itemsContainer"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary btn-lg mb-4" onclick="return saveGroup();">Lưu Nhóm Slider</button>
|
|
</form>
|
|
</div>
|
|
|
|
<script th:inline="javascript">
|
|
let group = /*[[${groupJson}]]*/ '{}';
|
|
if (typeof group === 'string') {
|
|
try {
|
|
group = JSON.parse(group);
|
|
} catch (e) {
|
|
group = { slug: '', name: '', layoutType: '', outerTemplate: '', itemTemplate: '', items: [] };
|
|
}
|
|
}
|
|
if (!group.items) group.items = [];
|
|
window.group = group;
|
|
|
|
function renderBadge(container, field, val, text) {
|
|
const badge = document.createElement('span');
|
|
badge.className = 'category-badge';
|
|
badge.setAttribute('data-value', val);
|
|
badge.setAttribute('data-field', field);
|
|
|
|
const badgeText = document.createElement('span');
|
|
badgeText.className = 'badge-text';
|
|
badgeText.innerText = text;
|
|
|
|
const removeBtn = document.createElement('button');
|
|
removeBtn.type = 'button';
|
|
removeBtn.className = 'remove_category-badge';
|
|
removeBtn.innerHTML = '×';
|
|
|
|
badge.appendChild(badgeText);
|
|
badge.appendChild(removeBtn);
|
|
container.appendChild(badge);
|
|
}
|
|
|
|
function renderGroup() {
|
|
document.getElementById('groupName').value = group.name || '';
|
|
document.getElementById('groupSlug').value = group.slug || '';
|
|
document.getElementById('groupNote').value = group.note || '';
|
|
document.getElementById('groupLayoutType').value = group.layoutType || '';
|
|
document.getElementById('groupOuterTemplate').value = group.outerTemplate || '';
|
|
document.getElementById('groupItemTemplate').value = group.itemTemplate || '';
|
|
|
|
toggleCustomTemplates(group.layoutType || '');
|
|
toggleDataSource(group.dataSource || 'MANUAL');
|
|
|
|
document.getElementById('groupDataSource').value = group.dataSource || 'MANUAL';
|
|
|
|
// Initialize categories badges
|
|
if (group.categoryIds && Array.isArray(group.categoryIds)) {
|
|
const selectEl = document.getElementById('groupCategoryIds');
|
|
const container = document.getElementById('selected_categoryIds');
|
|
container.innerHTML = '';
|
|
group.categoryIds.forEach(val => {
|
|
const optionEl = selectEl.querySelector(`option[value="${val}"]`);
|
|
const text = optionEl ? optionEl.innerText : val;
|
|
renderBadge(container, 'categoryIds', val, text);
|
|
});
|
|
}
|
|
|
|
// Initialize tag badges
|
|
if (group.tagIds && Array.isArray(group.tagIds)) {
|
|
const selectEl = document.getElementById('groupTagIds');
|
|
const container = document.getElementById('selected_tagIds');
|
|
container.innerHTML = '';
|
|
group.tagIds.forEach(val => {
|
|
const optionEl = selectEl.querySelector(`option[value="${val}"]`);
|
|
const text = optionEl ? optionEl.innerText : val;
|
|
renderBadge(container, 'tagIds', val, text);
|
|
});
|
|
}
|
|
|
|
document.getElementById('groupItemLimit').value = group.itemLimit || '';
|
|
document.getElementById('groupUseExcerptAsDescription').checked = group.useExcerptAsDescription || false;
|
|
|
|
const container = document.getElementById('itemsContainer');
|
|
container.innerHTML = '';
|
|
|
|
if (group.items.length === 0) {
|
|
container.innerHTML = '<p class="text-center text-muted my-3">Chưa có slide nào. Bấm "Thêm Slide Mới" để bắt đầu.</p>';
|
|
return;
|
|
}
|
|
|
|
group.items.forEach((item, iIndex) => {
|
|
const itemHtml = `
|
|
<div class="item-card shadow-sm">
|
|
<div class="d-flex justify-content-between mb-2">
|
|
<h6 class="m-0 font-weight-bold text-secondary">Slide ${iIndex + 1}</h6>
|
|
<button type="button" class="btn btn-danger btn-sm" onclick="removeItem(${iIndex})">Xóa Slide</button>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6 tab-field">
|
|
<label>Tiêu đề</label>
|
|
<input type="text" onchange="updateItem(${iIndex}, 'title', this.value)" value="${item.title || ''}" />
|
|
</div>
|
|
<div class="col-md-6 tab-field">
|
|
<label>Link đích (URL khi click)</label>
|
|
<input type="text" onchange="updateItem(${iIndex}, 'linkUrl', this.value)" value="${item.linkUrl || ''}" />
|
|
</div>
|
|
<div class="col-md-12 tab-field">
|
|
<label>Hình Ảnh (URL)</label>
|
|
<div class="d-flex">
|
|
<input type="text" id="img_${iIndex}" class="form-control" onchange="updateItem(${iIndex}, 'imageUrl', this.value)" value="${item.imageUrl || ''}" />
|
|
<button type="button" class="btn btn-secondary ml-2" onclick="document.getElementById('file_${iIndex}').click()" style="white-space: nowrap;">Tải Ảnh Lên</button>
|
|
</div>
|
|
<input type="file" id="file_${iIndex}" style="display:none;" onchange="uploadImage(${iIndex}, this.files[0])" accept="image/*"/>
|
|
</div>
|
|
<div class="col-md-12 tab-field">
|
|
<label>Mô tả (HTML)</label>
|
|
<textarea onchange="updateItem(${iIndex}, 'description', this.value)">${item.description || ''}</textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.insertAdjacentHTML('beforeend', itemHtml);
|
|
});
|
|
}
|
|
|
|
function toggleCustomTemplates(layoutType) {
|
|
const display = layoutType === 'custom' ? 'block' : 'none';
|
|
document.querySelectorAll('.custom-template-container').forEach(el => (el.style.display = display));
|
|
}
|
|
|
|
function toggleDataSource(dataSource) {
|
|
const display = dataSource === 'POST' || dataSource === 'PAGE' ? 'block' : 'none';
|
|
document.querySelectorAll('.dynamic-source-container').forEach(el => (el.style.display = display));
|
|
}
|
|
|
|
function updateGroup(field, value) {
|
|
group[field] = value;
|
|
}
|
|
|
|
window.updateMultiGroup = function (field, selectElement) {
|
|
const selectedValue = parseInt(selectElement.value, 10);
|
|
|
|
// Ignore default empty option selection
|
|
if (isNaN(selectedValue)) return;
|
|
|
|
// Initialize array state for this field if not present
|
|
if (!Array.isArray(window.group[field])) {
|
|
window.group[field] = [];
|
|
}
|
|
|
|
// 1. Add ID to array if not already selected (prevents duplicates)
|
|
if (!window.group[field].includes(selectedValue)) {
|
|
window.group[field].push(selectedValue);
|
|
}
|
|
|
|
// 2. Target the corresponding badge container
|
|
const container = document.getElementById('selected_' + field);
|
|
container.innerHTML = '';
|
|
|
|
// 3. Render badges from the window.group[field] array
|
|
window.group[field].forEach(val => {
|
|
const optionEl = selectElement.querySelector(`option[value="${val}"]`);
|
|
const text = optionEl ? optionEl.innerText : val;
|
|
renderBadge(container, field, val, text);
|
|
});
|
|
|
|
// 4. Reset select dropdown back to placeholder option
|
|
selectElement.selectedIndex = 0;
|
|
|
|
console.log(`Current group[${field}]:`, window.group[field]);
|
|
};
|
|
|
|
// Universal Remove Handler using Event Delegation in Vanilla JS
|
|
document.addEventListener('click', function (e) {
|
|
if (e.target && (e.target.matches('.remove_category-badge') || e.target.closest('.remove_category-badge'))) {
|
|
e.stopPropagation();
|
|
const badge = e.target.closest('.category-badge');
|
|
if (!badge) return;
|
|
|
|
const badgeVal = parseInt(badge.getAttribute('data-value'), 10);
|
|
const field = badge.getAttribute('data-field');
|
|
|
|
if (!isNaN(badgeVal) && field && Array.isArray(window.group[field])) {
|
|
// 1. Filter out deleted ID from the state array
|
|
window.group[field] = window.group[field].filter(val => val !== badgeVal);
|
|
console.log(`Updated group[${field}] after removal:`, window.group[field]);
|
|
}
|
|
|
|
// 2. Remove badge UI element
|
|
badge.remove();
|
|
}
|
|
});
|
|
|
|
function addItem() {
|
|
group.items.push({ title: '', imageUrl: '', linkUrl: '', description: '' });
|
|
renderGroup();
|
|
}
|
|
|
|
function removeItem(iIndex) {
|
|
if (confirm('Bạn có chắc muốn xóa slide này?')) {
|
|
group.items.splice(iIndex, 1);
|
|
renderGroup();
|
|
}
|
|
}
|
|
|
|
function updateItem(iIndex, field, value) {
|
|
group.items[iIndex][field] = value;
|
|
}
|
|
|
|
async function uploadImage(iIndex, file) {
|
|
if (!file) return;
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const btn = document.querySelector(`#file_${iIndex}`).previousElementSibling.querySelector('button');
|
|
const originalText = btn.innerText;
|
|
btn.innerText = 'Đang tải...';
|
|
btn.disabled = true;
|
|
|
|
try {
|
|
const response = await fetch('/api/manage/media/upload', {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
const result = await response.json();
|
|
if (result.success === 1 && result.file && result.file.url) {
|
|
group.items[iIndex]['imageUrl'] = result.file.url;
|
|
document.getElementById(`img_${iIndex}`).value = result.file.url;
|
|
} else {
|
|
alert('Lỗi tải ảnh lên!');
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert('Lỗi tải ảnh lên!');
|
|
} finally {
|
|
btn.innerText = originalText;
|
|
btn.disabled = false;
|
|
document.getElementById(`file_${iIndex}`).value = ''; // reset file input
|
|
}
|
|
}
|
|
|
|
function saveGroup() {
|
|
if (!group.name || !group.slug) {
|
|
alert('Vui lòng nhập Tên Nhóm và Slug!');
|
|
return false;
|
|
}
|
|
document.getElementById('groupData').value = JSON.stringify(group);
|
|
return true;
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
renderGroup();
|
|
});
|
|
</script>
|
|
</div>
|
|
</body>
|
|
</html>""")
|