hoàn thành phần tạo theme

This commit is contained in:
2026-07-02 18:30:12 +07:00
parent 986bd08e5f
commit e91e607d14
13 changed files with 313 additions and 3 deletions
+1
View File
@@ -0,0 +1 @@
kill -9 $(lsof -t -i:8080) || true
@@ -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;
}
}
@@ -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">&times;</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">&times;</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>&copy; 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;">&copy; 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>