Files
sisvietnamvn_01/sisvietnamvn_main/fix_admin_form.py
T

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 = '&times;';
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>""")