作为一个博客系统,更换主题的功能几乎是必不可少的。该功能的实现参考了tale开源项目,非常感谢!
具体实现
项目结构
├── java
│ └── cc
│ └── ryanc
│ └── halo
│ ├── Application.java
│ ├── model
│ ├── repository
│ ├── service
│ ├── util
│ └── web
│ ├── controller //控制器
│ └── interceptor //拦截器
└── resources
├── static
└── templates //模板目录
└── themes //主题目录
├── anatole
├── halo
└── material
更换主题
由于使用了freemarker模板引擎,换主题这个功能就变得非常简单了,在Controller里面渲染页面的时候,只需要修改主题存在路径就可以了,具体实现方式如下:
BaseController:
public abstract class BaseController {
/**
* 定义默认主题
*/
public static String THEME = "halo";
/**
* 根据主题名称渲染页面
*
* @param pageName pageName
* @return 返回拼接好的模板路径
*/
public String render(String pageName){
StringBuffer themeStr = new StringBuffer("themes/");
themeStr.append(THEME);
themeStr.append("/");
return themeStr.append(pageName).toString();
}
}
IndexController(页面控制器),继承BaseController:
@GetMapping(value = "page/{page}")
public String index(Model model,
@PathVariable(value = "page") Integer page){
Sort sort = new Sort(Sort.Direction.DESC,"postDate");
//默认显示10条
Integer size = 10;
//所有文章数据,分页
Pageable pageable = new PageRequest(page-1,size,sort);
Page<Post> posts = postService.findPostByStatus(0,pageable);
model.addAttribute("posts",posts);
return this.render("index");
}
仔细看上面所示代码可知,在BaseController里面定义了一个静态变量作为主题名称(和主题文件夹名一致),然后在rander方法里面拼接好主题完整路径返回即可,如/themes/halo
,需要注意的是:render方法是有一个参数的,这个参数就是freemarker的模板文件名称,完整拼接如:/themes/halo/index
,然后在IndexController的方法里面就可以调用该方法,并传入响应的模板文件名,就可以完成渲染了。
如果需要切换主题,那么只需要在后台管理对BaseController里面的THEME变量重新赋值便可以实现切换主题了。
主题管理界面
上面说到了在后台管理对BaseController里面的THEME变量重新赋值,那么既然要切换主题,那就得把所有主题展示出来吧!实现的方法也不是太难,只需要扫描theme文件夹下的所有目录就行了。
具体代码:
Theme实体类:
public class Theme implements Serializable {
/**
* 主题名称
*/
private String themeName;
/**
* 是否支持设置
*/
private boolean hasOptions;
}
本来是不需要创建这个实体类的,但考虑到要确定该主题是否支持设置,所有建一个实体类来传输数据会比较方便。
HaloUtil(工具类):
/**
* 获取所有主题
* @return list
*/
public static List<Theme> getThemes(){
List<Theme> themes = new ArrayList<>();
try {
//获取项目根路径
File basePath = new File(ResourceUtils.getURL("classpath:").getPath());
//获取主题路径
File themesPath = new File(basePath.getAbsolutePath(),"templates/themes");
File[] files = themesPath.listFiles();
if(null!=files) {
Theme theme = null;
for (File file : files) {
if (file.isDirectory()) {
theme = new Theme();
theme.setThemeName(file.getName());
File optionsPath = new File(themesPath.getAbsolutePath(), file.getName() + "/module/options.ftl");
//判断是否存在options.ftl模板
if (optionsPath.exists()) {
theme.setHasOptions(true);
} else {
theme.setHasOptions(false);
}
themes.add(theme);
}
}
}
}catch (Exception e){
log.error("主题获取失败:"+e.getMessage());
}
return themes;
}
这里返回的themes就是所有主题的List集合了。
ThemeController:
/**
* 渲染主题设置页面
*
* @return String
*/
@GetMapping
public String themes(Model model){
model.addAttribute("activeTheme",BaseController.THEME);
if(null!=HaloConst.THEMES){
model.addAttribute("themes",HaloUtil.getThemes());
}
return "admin/admin_theme";
}
页面上:
<#list themes as theme>
<div class="col-md-3">
<div class="box box-solid">
<div class="box-body theme-thumbnail" style="background-image: url(/${theme.themeName?if_exists}/screenshot.png)"></div>
<div class="box-footer">
<span class="theme-title">${theme.themeName?if_exists?upper_case}</span>
<#if theme.hasOptions==true>
<button class="btn btn-primary btn-sm pull-right btn-flat" onclick="openSetting('${theme.themeName?if_exists}')">设置</button>
</#if>
<#if activeTheme == "${theme.themeName}">
<button class="btn btn-primary btn-sm pull-right btn-flat" disabled>已启用</button>
<#else>
<button onclick="setTheme('${theme.themeName?if_exists}')" class="btn btn-primary btn-sm pull-right btn-flat">启用</button>
</#if>
</div>
</div>
</div>
</#list>
效果:
后台上传主题
这个功能可以实现,在后台管理可以上传主题压缩包并解压到themes目录,实现起来也不是太难,实现代码如下:
ThemeController:
@RequestMapping(value = "/upload", method = RequestMethod.POST)
@ResponseBody
public boolean uploadTheme(@RequestParam("file") MultipartFile file,
HttpServletRequest request){
try {
if(!file.isEmpty()) {
//获取项目根路径
File basePath = new File(ResourceUtils.getURL("classpath:").getPath());
File themePath = new File(basePath.getAbsolutePath(), new StringBuffer("templates/themes/").append(file.getOriginalFilename()).toString());
file.transferTo(themePath);
log.info("上传主题成功,路径:" + themePath.getAbsolutePath());
logsService.saveByLogs(
new Logs(LogsRecord.UPLOAD_THEME,file.getOriginalFilename(),HaloUtil.getIpAddr(request),HaloUtil.getDate())
);
//调用方法解压该压缩包到themes目录
HaloUtil.unZip(themePath.getAbsolutePath(),new File(basePath.getAbsolutePath(),"templates/themes/").getAbsolutePath());
//移除压缩包
HaloUtil.removeFile(themePath.getAbsolutePath());
HaloConst.THEMES.clear();
HaloConst.THEMES = HaloUtil.getThemes();
return true;
}else{
log.error("上传失败,没有选择文件");
}
}catch (Exception e){
log.error("上传失败:"+e.getMessage());
}
return false;
}
unZip:
public static void unZip(String zipFilePath,String descDir){
File zipFile=new File(zipFilePath);
File pathFile=new File(descDir);
if(!pathFile.exists()){
pathFile.mkdirs();
}
ZipFile zip=null;
InputStream in=null;
OutputStream out=null;
try {
zip=new ZipFile(zipFile);
Enumeration<?> entries=zip.entries();
while(entries.hasMoreElements()){
ZipEntry entry=(ZipEntry) entries.nextElement();
String zipEntryName=entry.getName();
in=zip.getInputStream(entry);
String outPath=(descDir+"/"+zipEntryName).replace("\\*", "/");
File file=new File(outPath.substring(0, outPath.lastIndexOf('/')));
if(!file.exists()){
file.mkdirs();
}
if(new File(outPath).isDirectory()){
continue;
}
out=new FileOutputStream(outPath);
byte[] buf=new byte[4*1024];
int len;
while((len=in.read(buf))>=0){
out.write(buf, 0, len);
}
in.close();
}
} catch (Exception e) {
log.error("解压失败:"+e.getMessage());
}finally{
try {
if(zip!=null)
zip.close();
if(in!=null)
in.close();
if(out!=null)
out.close();
} catch (IOException e) {
log.error("未知错误:"+e.getMessage());
}
}
}
主题的设置
在整个主题系统中,在某些情况下是需要对主题进行单独设置的。比如社交选项,样式选项等,其实要存储这些设置是非常简单的,和上一篇文章其实是一样的,将各个设置选项和值以key,value的方式存储在数据表中就可以了,在这里就不多讲了。
效果图:
注:这个弹出层是使用的layer实现的,非常感谢该框架!
总结
整个主题系统的完善还是花了不少时间的,这里只是讲了核心的实现方法,如果有朋友对此感兴趣的话,可以去github上看具体实现的代码:https://github.com/ruibaby/halo。如果对你有帮助的话,请给个Star,也欢迎大家提pull request。