hoàn thành phần tạo theme
This commit is contained in:
@@ -4,7 +4,7 @@ PHẦN 1: CÁC CHỨC NĂNG TRÊN GISAO DIỆN ADMIN (UI MENUS)
|
||||
Đây là các chức năng mặc định xuất hiện ở thanh Menu bên trái (Sidebar) của trang quản trị:
|
||||
|
||||
Menu chính Menu con Chức năng chi tiết
|
||||
1. Dashboard (Bảng điều khiển) Home Xem tổng quan trạng thái website (Site Health, số lượng bài viết/trang, bình luận gần đây, hoạt động viết nháp nhanh).
|
||||
(done)1. Dashboard (Bảng điều khiển) Home Xem tổng quan trạng thái website (Site Health, số lượng bài viết/trang, bình luận gần đây, hoạt động viết nháp nhanh).
|
||||
Updates Cập nhật phiên bản WordPress Core, Themes, Plugins và các gói dịch vụ ngôn ngữ.
|
||||
(Done) 2. Posts (Bài viết) All Posts Quản lý toàn bộ bài viết tin tức, blog, lọc theo ngày/chuyên mục.
|
||||
Add New Soạn thảo bài viết mới bằng Block Editor (Gutenberg) hoặc Classic Editor.
|
||||
@@ -22,7 +22,7 @@ Theme File Editor Chỉnh sửa trực tiếp file code của giao diện (chỉ
|
||||
(done)7. Plugins (Gói mở rộng) Installed Plugins Quản lý, kích hoạt, vô hiệu hóa hoặc xóa các Plugin đang có trên hệ thống.
|
||||
Add New Tìm kiếm và cài đặt Plugin mới từ thư viện WordPress.org hoặc upload file .zip.
|
||||
Plugin File Editor Chỉnh sửa mã nguồn của các Plugin trực tiếp từ admin.
|
||||
8. Users (Thành viên) All Users Quản lý danh sách toàn bộ tài khoản đăng ký trên hệ thống.
|
||||
(done) 8. Users (Thành viên) All Users Quản lý danh sách toàn bộ tài khoản đăng ký trên hệ thống.
|
||||
Add New Tạo tài khoản người dùng mới và chỉ định vai trò (Role).
|
||||
Profile Chỉnh sửa thông tin cá nhân của tài khoản đang đăng nhập (đổi pass, ngôn ngữ hiển thị).
|
||||
9. Tools (Công cụ) Available Tools Các công cụ hỗ trợ chuyển đổi dữ liệu.
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
kill -9 $(lsof -t -i:8080) || true
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.sisvietnamvn.web.controller;
|
||||
|
||||
import com.sisvietnamvn.web.service.SettingService;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Provides global model attributes for all frontend controllers.
|
||||
*/
|
||||
@ControllerAdvice(basePackages = "com.sisvietnamvn.web.controller")
|
||||
public class GlobalControllerAdvice {
|
||||
|
||||
private final SettingService settingService;
|
||||
private final PathMatchingResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
|
||||
|
||||
public GlobalControllerAdvice(SettingService settingService) {
|
||||
this.settingService = settingService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the 'activeTheme' variable available to all Thymeleaf templates handled by controllers in this package.
|
||||
*/
|
||||
@ModelAttribute("activeTheme")
|
||||
public String getActiveTheme() {
|
||||
Map<String, String> settings = settingService.getAllAsMap();
|
||||
String activeTheme = settings.getOrDefault("active_theme", "default");
|
||||
|
||||
// Validate that the layout actually exists to prevent Thymeleaf TemplateInputException crashes
|
||||
try {
|
||||
Resource[] resources = resourceResolver.getResources("classpath*:templates/themes/" + activeTheme + "/layout.html");
|
||||
if (resources == null || resources.length == 0) {
|
||||
return "default"; // Fallback to safe layout
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return "default";
|
||||
}
|
||||
|
||||
return activeTheme;
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package com.sisvietnamvn.web.controller.manage;
|
||||
|
||||
import com.sisvietnamvn.web.service.SettingService;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
import com.sisvietnamvn.web.security.AuthoritiesConstants;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Controller for the admin Themes page.
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/manage/themes")
|
||||
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\")")
|
||||
public class ManageThemeController {
|
||||
|
||||
private final SettingService settingService;
|
||||
private final PathMatchingResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
|
||||
|
||||
public ManageThemeController(SettingService settingService) {
|
||||
this.settingService = settingService;
|
||||
}
|
||||
|
||||
private List<String> getAvailableLayouts() {
|
||||
List<String> layouts = new ArrayList<>();
|
||||
try {
|
||||
Resource[] resources = resourceResolver.getResources("classpath*:templates/themes/*/layout.html");
|
||||
for (Resource res : resources) {
|
||||
String path = res.getURL().toString();
|
||||
java.util.regex.Matcher m = java.util.regex.Pattern.compile("themes/([^/]+)/layout\\.html$").matcher(path);
|
||||
if (m.find()) {
|
||||
layouts.add(m.group(1));
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore if directory doesn't exist
|
||||
}
|
||||
|
||||
// Ensure we always have at least a fallback if empty
|
||||
if (layouts.isEmpty()) {
|
||||
layouts.add("default");
|
||||
}
|
||||
return layouts;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String listThemes(Model model) {
|
||||
Map<String, String> settings = settingService.getAllAsMap();
|
||||
String activeTheme = settings.getOrDefault("active_theme", "default");
|
||||
|
||||
model.addAttribute("activeTheme", activeTheme);
|
||||
model.addAttribute("availableThemes", getAvailableLayouts());
|
||||
return "manage/themes/list";
|
||||
}
|
||||
|
||||
private boolean isThemeComplete(String themeKey) {
|
||||
try {
|
||||
Resource[] layouts = resourceResolver.getResources("classpath*:templates/themes/" + themeKey + "/layout.html");
|
||||
Resource[] headers = resourceResolver.getResources("classpath*:templates/themes/" + themeKey + "/header.html");
|
||||
Resource[] footers = resourceResolver.getResources("classpath*:templates/themes/" + themeKey + "/footer.html");
|
||||
|
||||
return layouts.length > 0 && headers.length > 0 && footers.length > 0;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/activate")
|
||||
public String activateTheme(@RequestParam("themeKey") String themeKey, RedirectAttributes redirectAttributes) {
|
||||
List<String> validThemes = getAvailableLayouts();
|
||||
|
||||
if (!validThemes.contains(themeKey)) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Invalid theme selected.");
|
||||
return "redirect:/manage/themes";
|
||||
}
|
||||
|
||||
if (!isThemeComplete(themeKey)) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Cannot activate theme: Missing required files (layout.html, header.html, or footer.html).");
|
||||
return "redirect:/manage/themes";
|
||||
}
|
||||
|
||||
settingService.setValue("active_theme", themeKey);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "New theme activated successfully.");
|
||||
return "redirect:/manage/themes";
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,21 @@
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Nav Item - Appearance Collapse Menu -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseAppearance"
|
||||
aria-expanded="true" aria-controls="collapseAppearance">
|
||||
<i class="fas fa-fw fa-paint-brush"></i>
|
||||
<span>Appearance</span>
|
||||
</a>
|
||||
<div id="collapseAppearance" class="collapse" aria-labelledby="headingAppearance" data-parent="#accordionSidebar">
|
||||
<div class="bg-white py-2 collapse-inner rounded">
|
||||
<h6 class="collapse-header">Design Management:</h6>
|
||||
<a class="collapse-item" th:href="@{/manage/themes}">Themes</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Divider -->
|
||||
<hr class="sidebar-divider">
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<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>Themes</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div layout:fragment="content">
|
||||
<!-- Page Heading -->
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">Themes</h1>
|
||||
</div>
|
||||
|
||||
<!-- Success/Error Messages -->
|
||||
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<span th:text="${successMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<span th:text="${errorMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<!-- Dynamic Layout Cards -->
|
||||
<div class="col-lg-4 mb-4" th:each="theme : ${availableThemes}">
|
||||
<div class="card shadow mb-4 h-100" th:classappend="${activeTheme == theme} ? 'border-left-primary' : ''">
|
||||
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
|
||||
<h6 class="m-0 font-weight-bold text-primary" th:text="${#strings.capitalizeWords(#strings.replace(theme, '-', ' '))} + ' Layout'">Theme Name</h6>
|
||||
<span th:if="${activeTheme == theme}" class="badge badge-success">Active</span>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="bg-light p-5 text-center mb-3 border rounded">
|
||||
<i class="fas fa-file-code fa-4x text-gray-400"></i>
|
||||
</div>
|
||||
<p class="card-text flex-grow-1">A custom layout file loaded from the templates/themes directory.</p>
|
||||
|
||||
<form th:if="${activeTheme != theme}" th:action="@{/manage/themes/activate}" method="post" class="mt-auto">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<input type="hidden" name="themeKey" th:value="${theme}" />
|
||||
<button type="submit" class="btn btn-primary btn-block">Activate</button>
|
||||
</form>
|
||||
<button th:if="${activeTheme == theme}" class="btn btn-secondary btn-block mt-auto" disabled>Customize</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/layout}">
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{themes/__${activeTheme}__/layout}">
|
||||
|
||||
<head>
|
||||
<title th:text="${page.title}">Page Title</title>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<footer th:fragment="footer" style="background-color: #e9ecef; padding: 20px; text-align: center; margin-top: 40px;">
|
||||
<p>© 2026 Default Theme Footer. All rights reserved.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<header th:fragment="header" style="background-color: #e9ecef; padding: 20px; text-align: center;">
|
||||
<h2>Default Theme Header</h2>
|
||||
<nav>
|
||||
<a href="/">Home</a> |
|
||||
<a href="/about">About Us</a> |
|
||||
<a href="/tin-tuc">News</a> |
|
||||
<a href="/lien-he">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Default Theme</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Replace with Header Fragment -->
|
||||
<header th:replace="~{themes/__${activeTheme}__/header :: header}"></header>
|
||||
|
||||
<div style="padding: 20px; min-height: 400px;">
|
||||
<div layout:fragment="content"></div>
|
||||
</div>
|
||||
|
||||
<!-- Replace with Footer Fragment -->
|
||||
<footer th:replace="~{themes/__${activeTheme}__/footer :: footer}"></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<footer th:fragment="footer" style="background-color: #111; color: #888; padding: 40px 20px; text-align: center; margin-top: 50px;">
|
||||
<h4 style="color: #fff; margin-bottom: 10px;">Modern Theme Design</h4>
|
||||
<p style="font-size: 14px;">© 2026. Built with Spring Boot and Thymeleaf.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<header th:fragment="header" style="background-color: #000; color: #fff; padding: 25px; display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #007bff;">
|
||||
<h2 style="margin: 0; font-weight: 300; letter-spacing: 2px;">MODERN<span style="color: #007bff; font-weight: bold;">THEME</span></h2>
|
||||
<nav>
|
||||
<a href="/" style="color: #fff; text-decoration: none; margin-left: 20px; font-weight: bold;">HOME</a>
|
||||
<a href="/about" style="color: #fff; text-decoration: none; margin-left: 20px;">ABOUT</a>
|
||||
<a href="/tin-tuc" style="color: #fff; text-decoration: none; margin-left: 20px;">NEWS</a>
|
||||
<a href="/lien-he" style="color: #fff; text-decoration: none; margin-left: 20px;">CONTACT</a>
|
||||
</nav>
|
||||
</header>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Modern Theme</title>
|
||||
</head>
|
||||
<body style="background-color: #f8f9fa; font-family: sans-serif;">
|
||||
<!-- Replace with Header Fragment -->
|
||||
<header th:replace="~{themes/__${activeTheme}__/header :: header}"></header>
|
||||
|
||||
<div style="padding: 20px; min-height: 500px;" layout:fragment="content"></div>
|
||||
|
||||
<!-- Replace with Footer Fragment -->
|
||||
<footer th:replace="~{themes/__${activeTheme}__/footer :: footer}"></footer>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user