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
+2 -2
View File
@@ -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.
+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>