[CMS-VISUAL] E 激进清场: 下架 10 套伪主题(tag themes-before-cleanup-2026-09 + docs/主题备份/ 双备份)→ 保留 default/scholar-blue/campus-glow 三套;色皮档并入 default.colorScheme、克隆档并入 scholar-blue.colorScheme(ThemeParam.presets + ThemeVarsCss 变量组注入);别名表 R-46 映射(渲染端不断渲染 + 保存归一化 + 站点级 theme_params.colorScheme)

This commit is contained in:
chenxing
2026-09-28 14:58:31 +08:00
parent efd3cb30f9
commit a9e4240bb3
166 changed files with 435 additions and 20200 deletions
@@ -75,12 +75,24 @@ public class PortalQueryService {
return null; return null;
} }
PortalSiteSnapshot snapshot = PortalSiteSnapshot.of(site); PortalSiteSnapshot snapshot = PortalSiteSnapshot.of(site);
snapshot.setTheme(resolveTheme(snapshot.getTheme(), snapshot.getCode())); String originalTheme = snapshot.getTheme();
snapshot.setTheme(resolveTheme(originalTheme, snapshot.getCode()));
derivePublicBaseUrl(site, snapshot); derivePublicBaseUrl(site, snapshot);
// 微调参数注入(方案 §5.3):按已解析主题 schema 过滤,回退主题与参数过滤天然一致 // 微调参数注入(方案 §5.3):按已解析主题 schema 过滤,回退主题与参数过滤天然一致
com.yunhai.cms.theme.ThemeInfo themeInfo = themeRegistry.get(snapshot.getTheme()); com.yunhai.cms.theme.ThemeInfo themeInfo = themeRegistry.get(snapshot.getTheme());
java.util.Map<String, String> themeParams = java.util.Map<String, String> themeParams =
themeInfo.filterValidValues(rawParamsMap(snapshot)); themeInfo.filterValidValues(rawParamsMap(snapshot));
// docs/20 E(R-46):站点用下架主题 → 强制对应 colorScheme 预设(保持原视觉近似);
// 站点保存归一化后 theme 已是保留主题,改由 config.theme_params.colorScheme 站点级驱动
com.yunhai.cms.theme.ThemeRegistry.AliasInfo alias =
com.yunhai.cms.theme.ThemeRegistry.aliasOf(originalTheme);
String siteScheme = siteColorScheme(snapshot.getConfig());
if (alias != null && alias.colorScheme != null) {
themeParams.put("colorScheme", alias.colorScheme);
} else if (siteScheme != null && themeInfo.param("colorScheme") != null
&& themeInfo.param("colorScheme").isValidValue(siteScheme)) {
themeParams.put("colorScheme", siteScheme);
}
snapshot.setThemeParams(themeParams); snapshot.setThemeParams(themeParams);
snapshot.setThemeVarsCss(com.yunhai.cms.theme.ThemeVarsCss.build(themeInfo, themeParams)); snapshot.setThemeVarsCss(com.yunhai.cms.theme.ThemeVarsCss.build(themeInfo, themeParams));
@@ -280,15 +292,41 @@ public class PortalQueryService {
} }
} }
/** 皮肤包存在性检查:委托 ThemeRegistry(theme.json + 4 模板齐全校验,方案 §5.1 重构,缺省回退 default) */ /**
* 皮肤包存在性检查:委托 ThemeRegistry;docs/20 E 后下架主题按别名表映射到保留主题
* (R-46:门户不断渲染),未知主题回退 default。
*/
private String resolveTheme(String theme, String siteCode) { private String resolveTheme(String theme, String siteCode) {
if (themeRegistry.contains(theme)) { if (themeRegistry.contains(theme)) {
return theme; return theme;
} }
com.yunhai.cms.theme.ThemeRegistry.AliasInfo alias =
com.yunhai.cms.theme.ThemeRegistry.aliasOf(theme);
if (alias != null) {
log.warn("皮肤包 {} 已下架(docs/20 E 清场),映射到 {}(colorScheme={})site={}",
theme, alias.target, alias.colorScheme, siteCode);
return alias.target;
}
log.warn("皮肤包 {} 未注册(site={}),回退 default", theme, siteCode); log.warn("皮肤包 {} 未注册(site={}),回退 default", theme, siteCode);
return com.yunhai.cms.theme.ThemeRegistry.DEFAULT_THEME; return com.yunhai.cms.theme.ThemeRegistry.DEFAULT_THEME;
} }
/** 站点级 colorScheme(config_json.theme_params.colorScheme,仅归一化保存后的别名站点使用) */
private static String siteColorScheme(java.util.Map<String, Object> config) {
if (config == null) {
return null;
}
Object tp = config.get("theme_params");
if (tp instanceof Map) {
Object v = ((Map<?, ?>) tp).get("colorScheme");
if (v != null) {
String s = String.valueOf(v);
return s.isEmpty() ? null : s;
}
}
return null;
}
/** /**
* 参数原始值(工作项 T:参数微调上移为皮肤包级):取当前主题的包级 {@code paramValues} * 参数原始值(工作项 T:参数微调上移为皮肤包级):取当前主题的包级 {@code paramValues}
* ({@code themes/.meta/{name}.json} 覆盖层)。站点 {@code config_json.theme_params} 已退役、不再读取 * ({@code themes/.meta/{name}.json} 覆盖层)。站点 {@code config_json.theme_params} 已退役、不再读取
@@ -191,13 +191,27 @@ public class SiteService {
site.setSchId(req.getSchId()); site.setSchId(req.getSchId());
site.setSchName(req.getSchName()); site.setSchName(req.getSchName());
site.setName(req.getName()); site.setName(req.getName());
String resolvedTheme = StringUtils.hasText(req.getTheme()) String requestedTheme = StringUtils.hasText(req.getTheme())
? req.getTheme() : com.yunhai.cms.theme.ThemeRegistry.DEFAULT_THEME; ? req.getTheme() : com.yunhai.cms.theme.ThemeRegistry.DEFAULT_THEME;
// docs/20 E:下架主题名按别名表归一化为保留主题(保存后 theme 字段收敛到 3 套注册包)
com.yunhai.cms.theme.ThemeRegistry.AliasInfo alias =
com.yunhai.cms.theme.ThemeRegistry.aliasOf(requestedTheme);
String resolvedTheme = requestedTheme;
if (!themeRegistry.contains(resolvedTheme)) { if (!themeRegistry.contains(resolvedTheme)) {
if (alias != null) {
resolvedTheme = alias.target;
} else {
throw new BizException(ErrorCode.PARAM_ERROR, "皮肤包未注册:" + resolvedTheme); throw new BizException(ErrorCode.PARAM_ERROR, "皮肤包未注册:" + resolvedTheme);
} }
}
site.setTheme(resolvedTheme); site.setTheme(resolvedTheme);
validateConfigJson(req.getConfigJson(), site.getId()); String configJson = req.getConfigJson();
if (alias != null && alias.colorScheme != null) {
// 原配色写入站点级 theme_params.colorScheme(渲染端据此套用预设,保留原视觉近似)
configJson = withSiteColorScheme(configJson, alias.colorScheme);
req.setConfigJson(configJson);
}
validateConfigJson(configJson, site.getId());
// 工作项 T:参数微调上移为皮肤包级,站点不再持有参数值——themeParams 入参直接忽略 // 工作项 T:参数微调上移为皮肤包级,站点不再持有参数值——themeParams 入参直接忽略
// (config_json 原样入库;theme_params 仍是 RESERVED 键以容忍存量/老前端提交,不报错) // (config_json 原样入库;theme_params 仍是 RESERVED 键以容忍存量/老前端提交,不报错)
site.setConfigJson(req.getConfigJson()); site.setConfigJson(req.getConfigJson());
@@ -209,6 +223,26 @@ public class SiteService {
return SiteVo.of(site); return SiteVo.of(site);
} }
/**
* docs/20 E:站点级 colorScheme 写入 config_json.theme_params(仅 colorScheme 键,
* 其余 theme_params 内容原样保留;theme_params 为 RESERVED 键不参与常规校验)。
*/
private String withSiteColorScheme(String configJson, String colorScheme) {
java.util.Map<String, Object> config = ExtJsonUtils.parse(configJson);
Object tp = config.get("theme_params");
java.util.Map<String, Object> themeParams = new java.util.LinkedHashMap<>();
if (tp instanceof java.util.Map) {
((java.util.Map<?, ?>) tp).forEach((k, v) -> themeParams.put(String.valueOf(k), v));
}
themeParams.put("colorScheme", colorScheme);
config.put("theme_params", themeParams);
try {
return CONFIG_MAPPER.writeValueAsString(config);
} catch (Exception e) {
throw new BizException(ErrorCode.PARAM_ERROR, "站点配置序列化失败");
}
}
/** /**
* W4 门户 path 计算(唯一入口,前后端各一份纯函数语义:front/src/utils/sitePath.ts 同构): * W4 门户 path 计算(唯一入口,前后端各一份纯函数语义:front/src/utils/sitePath.ts 同构):
* 顶层(parentSiteId≤0)=code;子站=父站 path + '/' + code。 * 顶层(parentSiteId≤0)=code;子站=父站 path + '/' + code。
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data; import lombok.Data;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
@@ -47,6 +48,13 @@ public class ThemeParam {
private List<Option> options; private List<Option> options;
/**
* docs/20 E(colorScheme 配色预设):select 参数可携带「选项值 → CSS 变量组」映射——
* 选中某选项时整组变量并入 :root{}(值逐一过 COLOR_PATTERN,非法丢弃)。
* 供伪主题合并:色皮档 4 套并 default、骨架克隆档 6 套并 scholar-blue。
*/
private Map<String, Map<String, String>> presets;
/** 值是否合法(按参数类型;null/空一律不合法,方案 §5.3) */ /** 值是否合法(按参数类型;null/空一律不合法,方案 §5.3) */
public boolean isValidValue(String value) { public boolean isValidValue(String value) {
if (value == null || value.isEmpty()) { if (value == null || value.isEmpty()) {
@@ -44,6 +44,44 @@ public class ThemeRegistry {
public static final String SOURCE_BUILTIN = "builtin"; public static final String SOURCE_BUILTIN = "builtin";
public static final String SOURCE_EXTERNAL = "external"; public static final String SOURCE_EXTERNAL = "external";
/**
* docs/20 E(激进清场)下架主题别名表:原主题名 → [保留主题, colorScheme 预设值]。
* 存量站点 theme 仍写下架名时按本表解析(R-46:门户不断渲染,不 404);
* 站点下次保存时由 SiteService 归一化为保留主题 + config.theme_params.colorScheme。
*/
private static final java.util.Map<String, String[]> DEPRECATED_ALIASES = java.util.Collections.unmodifiableMap(
new java.util.LinkedHashMap<String, String[]>() {{
// 色皮档 4 套 → default 预设
put("blue-white", new String[] {DEFAULT_THEME, "blue-white"});
put("classic-red", new String[] {DEFAULT_THEME, "classic-red"});
put("fresh-mint", new String[] {DEFAULT_THEME, "fresh-mint"});
put("warm-kinder", new String[] {DEFAULT_THEME, "warm-kinder"});
// 骨架克隆档 6 套 → scholar-blue 预设
put("academic-navy", new String[] {"scholar-blue", "navy"});
put("campus-azure", new String[] {"scholar-blue", "azure"});
put("poly-blue", new String[] {"scholar-blue", "poly"});
put("agri-forest", new String[] {"scholar-blue", "agri"});
put("jade-normal", new String[] {"scholar-blue", "jade"});
put("mint-breeze", new String[] {"scholar-blue", "mint"});
}});
/** 下架别名解析结果:target=保留主题名;colorScheme=对应预设值(可能为 null) */
public static final class AliasInfo {
public final String target;
public final String colorScheme;
AliasInfo(String target, String colorScheme) {
this.target = target;
this.colorScheme = colorScheme;
}
}
/** 下架别名解析:非别名返回 null */
public static AliasInfo aliasOf(String name) {
String[] mapped = name == null ? null : DEPRECATED_ALIASES.get(name);
return mapped == null ? null : new AliasInfo(mapped[0], mapped[1]);
}
/** 包名与目录名约束(方案 §4.1) */ /** 包名与目录名约束(方案 §4.1) */
private static final Pattern NAME_PATTERN = Pattern.compile("^[a-z][a-z0-9-]{1,30}[a-z0-9]$"); private static final Pattern NAME_PATTERN = Pattern.compile("^[a-z][a-z0-9-]{1,30}[a-z0-9]$");
@@ -23,15 +23,23 @@ public final class ThemeVarsCss {
} }
StringBuilder sb = new StringBuilder(":root{"); StringBuilder sb = new StringBuilder(":root{");
for (ThemeParam p : theme.getParams()) { for (ThemeParam p : theme.getParams()) {
if (!p.isCssVarParam()) {
continue;
}
String value = params.get(p.getKey()); String value = params.get(p.getKey());
// 双重校验(方案 §9 注入面):入口已过滤,此处仍只拼通过 color 正则的值
if (value == null || !p.isValidValue(value)) { if (value == null || !p.isValidValue(value)) {
continue; continue;
} }
if (p.isCssVarParam()) {
// 双重校验(方案 §9 注入面):入口已过滤,此处仍只拼通过 color 正则的值
sb.append(p.getCssVar()).append(':').append(value).append(';'); sb.append(p.getCssVar()).append(':').append(value).append(';');
} else if (p.getPresets() != null && p.getPresets().containsKey(value)) {
// docs/20 E:select 参数携带 presets(colorScheme → 变量组)时,选中值对应的
// 整组变量并入输出(每个值仍过 COLOR_PATTERN,非法丢弃——注入面与 color 参数同标)
p.getPresets().get(value).forEach((cssVar, color) -> {
if (cssVar != null && cssVar.matches("^--[a-zA-Z][\\w-]*$")
&& color != null && ThemeParam.COLOR_PATTERN.matcher(color).matches()) {
sb.append(cssVar).append(':').append(color).append(';');
}
});
}
} }
return sb.length() == 6 ? "" : sb.append('}').toString(); return sb.length() == 6 ? "" : sb.append('}').toString();
} }
@@ -1,33 +0,0 @@
# academic-navy(学府藏蓝)
高校官网风皮肤包:以 `scholar-blue` 为基座派生,配色与首页区块形态按综合类大学官网(浙江理工大学一类)重做。
配了 `home_layout` 的站点,首页正文区块由本主题 `fragments/home-blocks.html` 渲染——派生包不自带区块形态时,
正文会落到 `themes/common` 的通用骨架,换肤就只剩页头页脚(线上事故根因,见《审计验收清单-20260919》)。
## 风格定位
- 主色藏蓝 `#0b4a8f` + 加深 `#063161` + 点缀金 `#b8934a`(标题左竖条与居中线脚走金线,区别于基座的全蓝语言)。
- 圆角收紧到 4/8px、页脚 `#0a2b4d`,行高压到 40px 一档,整体比基座更「机关报」而非「校园活动页」。
- 全部 11 个颜色参数可在站点 theme_params 微调(`primary/primaryDark/primaryLight/accent/navBg/footerBg/text/textLight/bg/cardBg/border`)。
## 首页区块形态(布局模式与回落模式同构)
| homeStyle | 呈现 | 类名 |
| -- | -- | -- |
| `news_grid` | 双栏要闻:左 3 份图文头条(16:7 封面 + 摘要 + 元信息)、右 2 份紧凑行;区块背景可取站点 `news_bg_image` | `an-split` / `an-feat-*` |
| `compact` | 日期方块行:46px 见方的「日 + 年月」方块,hover 反白 | `an-date-chip` |
| `media_focus` | 媒体聚焦:来源徽标 + 双列密行 | `an-src` / `an-block-list--two` |
| `image_wall` | 四列图片墙:封面 + 底部渐变遮罩标题,hover 抬图 | `an-wall` |
| (空/未知) | 整宽区块卡:头条 + 行式列表 | `an-block--flow` |
| `ticker` | 藏蓝要闻速递横条(单行不换行、超长省略,中文不塌列) | `an-ticker*` |
| `gallery` | 线上展厅:居中双语文区头 + 四列图墙 | `an-gallery` |
| `links` | 友情链接分组(胶囊标签行) | `an-links` |
## 契约要点
- 结构类前缀 `an-`,CSS 变量前缀 `--an-`;`:root` 把 `--an-*` 别名到皮肤契约变量(`--primary`/`--text`/`--card-bg`/`--border`…),
使 `themes/common` 兜底骨架也能跟随本皮肤着色。
- 空态文案「栏目暂无内容」只在 `block.items` 为空时出现;守卫必须写在 `th:block` 外层——
`th:replace`(优先级 100)先于 `th:if`(300)求值,写在同一标签上等于没写。
- 回归防线:`AcademicSkinContractTest`(自带区块 fragment + 中文不塌列 + 契约变量别名 + 主色必须学术蓝)、
`PortalHomeLayoutIT#academicNavy_layoutUsesOwnFragments`(布局模式不落通用骨架)、`ThemeContractIT`(7 条路由)。
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
<rect width="96" height="96" rx="20" fill="#0a5aa8"/>
<path d="M48 16l8.6 17.5 19.3 2.8-14 13.6 3.3 19.2L48 60.1 30.8 69.1l3.3-19.2-14-13.6 19.3-2.8L48 16z" fill="#ffffff"/>
</svg>

Before

Width:  |  Height:  |  Size: 246 B

@@ -1,190 +0,0 @@
/* academic-navy 主题脚本:顶栏日期 + 轮播(圆点/箭头/触摸) + 导航吸顶反蓝 + 移动端抽屉与二级手风琴。
与 default 同源逻辑,无任何外部依赖。 */
(function () {
'use strict';
/* ---------- 顶栏日期(本地时间,YYYY年M月D日 星期X) ---------- */
var dateEl = document.getElementById('siteDate');
if (dateEl) {
var now = new Date();
var week = ['日', '一', '二', '三', '四', '五', '六'][now.getDay()];
dateEl.textContent = now.getFullYear() + '年' + (now.getMonth() + 1) + '月'
+ now.getDate() + '日 星期' + week;
}
/* ---------- 导航吸顶反蓝(桌面端;≤768 不吸顶) ---------- */
var nav = document.getElementById('anNav');
var DESKTOP_MIN = 769;
var navBaseTop = 0;
var holder = null;
function measureNavTop() {
unlockNav();
navBaseTop = nav.getBoundingClientRect().top + window.pageYOffset;
}
function lockNav() {
if (!nav.classList.contains('an-nav--fixed')) {
holder = document.createElement('div');
holder.className = 'an-nav-holder';
holder.style.height = nav.offsetHeight + 'px';
nav.parentNode.insertBefore(holder, nav);
nav.classList.add('an-nav--fixed');
} else if (holder) {
holder.style.height = nav.offsetHeight + 'px';
}
}
function unlockNav() {
nav.classList.remove('an-nav--fixed');
if (holder && holder.parentNode) {
holder.parentNode.removeChild(holder);
}
holder = null;
}
function onScrollNav() {
if (window.innerWidth < DESKTOP_MIN) {
unlockNav();
return;
}
if (window.pageYOffset > navBaseTop) {
lockNav();
} else {
unlockNav();
}
}
if (nav) {
measureNavTop();
window.addEventListener('scroll', onScrollNav, { passive: true });
var resizeTimer = null;
window.addEventListener('resize', function () {
if (resizeTimer) { window.clearTimeout(resizeTimer); }
resizeTimer = window.setTimeout(function () {
measureNavTop();
onScrollNav();
}, 150);
});
}
/* ---------- 移动端抽屉 + 二级手风琴 ---------- */
var navToggle = document.querySelector('.an-nav-toggle');
var navList = document.querySelector('.an-nav-list');
if (navToggle && navList) {
navToggle.addEventListener('click', function () {
var open = navList.classList.toggle('an-open');
navToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
});
}
/* 窄屏下:点击带二级的项先展开面板,再点才跳转;宽屏交还给 CSS hover */
if (navList) {
navList.addEventListener('click', function (e) {
if (window.innerWidth >= DESKTOP_MIN) { return; }
var link = e.target.closest ? e.target.closest('a') : null;
if (!link) { return; }
var li = link.parentElement;
if (!li || !li.classList.contains('has-sub')) { return; }
var sub = li.querySelector('.an-sub');
if (!sub) { return; }
if (!li.classList.contains('an-sub-open')) {
e.preventDefault();
li.classList.add('an-sub-open');
}
});
}
/* ---------- 首页轮播 ---------- */
var root = document.querySelector('.an-banner');
var slides = document.querySelectorAll('.an-banner-slide');
var dots = document.querySelectorAll('.an-banner-dots button');
var prevBtn = document.querySelector('.an-banner-arrow.prev');
var nextBtn = document.querySelector('.an-banner-arrow.next');
if (!slides.length || !root) {
return;
}
var current = 0;
var timer = null;
var INTERVAL = 5000;
function show(index) {
slides[current].classList.remove('active');
if (dots[current]) {
dots[current].classList.remove('active');
}
current = (index + slides.length) % slides.length;
slides[current].classList.add('active');
if (dots[current]) {
dots[current].classList.add('active');
}
}
function start() {
stop();
if (slides.length > 1) {
timer = window.setInterval(function () {
show(current + 1);
}, INTERVAL);
}
}
function stop() {
if (timer) {
window.clearInterval(timer);
timer = null;
}
}
dots.forEach(function (dot, index) {
dot.addEventListener('click', function () {
show(index);
start();
});
});
if (prevBtn) {
prevBtn.addEventListener('click', function () {
show(current - 1);
start();
});
}
if (nextBtn) {
nextBtn.addEventListener('click', function () {
show(current + 1);
start();
});
}
root.addEventListener('mouseenter', stop);
root.addEventListener('mouseleave', start);
root.addEventListener('focusin', stop);
root.addEventListener('focusout', start);
root.setAttribute('tabindex', '-1');
root.addEventListener('keydown', function (e) {
if (e.key === 'ArrowLeft') {
show(current - 1);
start();
} else if (e.key === 'ArrowRight') {
show(current + 1);
start();
}
});
/* 触摸滑动切页 */
var touchX = null;
root.addEventListener('touchstart', function (e) {
touchX = e.changedTouches[0].clientX;
stop();
}, { passive: true });
root.addEventListener('touchend', function (e) {
if (touchX === null) { return; }
var dx = e.changedTouches[0].clientX - touchX;
if (Math.abs(dx) > 40) {
show(dx < 0 ? current + 1 : current - 1);
}
touchX = null;
start();
}, { passive: true });
start();
})();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

@@ -1,76 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="an-page an-container" id="main-content">
<div class="an-cols">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="an-side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="an-side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="an-side-nav-list">
<li class="an-side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="an-page-card">
<nav class="an-crumb" aria-label="面包屑">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node : ${categoryPath}">
<span class="an-crumb-sep" aria-hidden="true">/</span>
<a class="crumb-parent" th:href="${url.category(node.id)}" th:text="${node.name}">栏目</a>
</th:block>
<span class="an-crumb-sep" th:if="${currentCategoryId != null}" aria-hidden="true">/</span>
<span class="an-crumb-current">正文</span>
</nav>
<article class="an-article">
<h1 class="an-article-title" th:text="${article.title}">文章标题</h1>
<div class="an-article-meta">
<time th:if="${article.publishTime != null}"
th:text="${#temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
<span th:if="${!#strings.isEmpty(article.authorName)}"
th:text="'作者:' + ${article.authorName}">作者</span>
<span th:if="${!#strings.isEmpty(article.source)}"
th:text="'来源:' + ${article.source}">来源</span>
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
</div>
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
其余字段一律 th:text 转义输出 -->
<div class="an-article-content" th:utext="${article.content}">正文</div>
</article>
<nav class="an-sibling" aria-label="上下篇导航">
<a th:if="${prevArticle != null}" class="an-sib prev"
th:href="${url.article(prevArticle.id)}">
<span class="an-sib-label">上一篇</span>
<span class="an-sib-title" th:text="${prevArticle.title}">上一篇标题</span>
</a>
<span class="an-sib-none prev" th:if="${prevArticle == null}">
<span class="an-sib-label">上一篇</span>
<span class="an-sib-title">已经是最早一篇</span>
</span>
<a th:if="${nextArticle != null}" class="an-sib next"
th:href="${url.article(nextArticle.id)}">
<span class="an-sib-label">下一篇</span>
<span class="an-sib-title" th:text="${nextArticle.title}">下一篇标题</span>
</a>
<span class="an-sib-none next" th:if="${nextArticle == null}">
<span class="an-sib-label">下一篇</span>
<span class="an-sib-title">已是最新一篇</span>
</span>
</nav>
</div>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- academic-navy 公共页脚:深蓝渐变底 + 同心圆环纹理(CSS 绘制) + 三栏(品牌/联系备案/友链分组) + 底条校名。
数据绑定与 default 契约一致(linkGroups/contact/copyright/beian/logo/schName),仅换皮。 -->
<footer class="an-footer" th:fragment="footer">
<i class="an-ring an-ring--1" aria-hidden="true"></i>
<i class="an-ring an-ring--2" aria-hidden="true"></i>
<i class="an-ring an-ring--3" aria-hidden="true"></i>
<div class="an-footer-inner">
<!-- 品牌栏 -->
<div class="an-footer-brand">
<span class="an-footer-badge" th:if="${site.config['logo'] != null}">
<img class="an-footer-logo" th:src="${url.upload(site.config['logo'])}" th:alt="${site.name}">
</span>
<span class="an-footer-badge an-footer-badge--plain" th:if="${site.config['logo'] == null}" aria-hidden="true">
<svg viewBox="0 0 24 24" width="30" height="30" focusable="false">
<path d="M12 2l2.4 4.9 5.4.8-3.9 3.8.9 5.4L12 14.4 7.2 16.9l.9-5.4L4.2 7.7l5.4-.8L12 2z"
fill="currentColor"></path>
</svg>
</span>
<span class="an-footer-site" th:text="${site.name}">站点名称</span>
</div>
<!-- 联系/版权/备案 -->
<div class="an-footer-info">
<p class="an-footer-line" th:if="${site.config['contact'] != null}">
<i class="an-dot" aria-hidden="true"></i><span th:text="${site.config['contact']}">联系方式</span>
</p>
<p class="an-footer-line" th:if="${site.config['copyright'] != null}">
<i class="an-dot" aria-hidden="true"></i><span th:text="${site.config['copyright']}">版权信息</span>
</p>
<p class="an-footer-line" th:if="${site.config['beian'] != null}">
<i class="an-dot" aria-hidden="true"></i><span th:text="${site.config['beian']}">备案号</span>
</p>
</div>
<!-- 友情链接(分组渲染;无分组整栏不渲染) -->
<div class="an-footer-links" th:if="${!#lists.isEmpty(site.linkGroups)}">
<div class="an-links-group" th:each="group : ${site.linkGroups}"
th:if="${!#lists.isEmpty(group.links)}">
<span class="an-group-name" th:text="${group.groupName}">分组</span>
<p class="an-links-row">
<a th:each="link : ${group.links}" th:href="${link.linkUrl}" target="_blank" rel="noopener">
<img th:if="${link.logoUrl != null and !#strings.isEmpty(link.logoUrl)}" class="link-logo"
th:src="${url.upload(link.logoUrl)}" th:alt="${link.name}" loading="lazy">
<span th:text="${link.name}">链接</span>
</a>
</p>
</div>
</div>
</div>
<div class="an-footer-bottom">
<p th:if="${site.schName != null and !#strings.isEmpty(site.schName)}"
th:text="${site.schName}">学校名称</p>
</div>
</footer>
</html>
@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<!-- academic-navy 公共 head 片段:与 default 契约一致(pageTitle/seo/canonical/og/themeVarsCss/favicon 兜底) -->
<head th:fragment="head">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:text="${pageTitle}">页面标题</title>
<meta name="description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}"
th:content="${seoDescription}">
<link rel="canonical" th:if="${canonical != null}" th:href="${canonical}">
<meta property="og:title" th:if="${pageTitle != null}" th:content="${pageTitle}">
<meta property="og:description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}" th:content="${seoDescription}">
<meta property="og:image" th:if="${ogImage != null}" th:content="${ogImage}">
<link rel="stylesheet" th:href="${url.asset('css/main.css')}">
<style th:if="${not #strings.isEmpty(themeVarsCss)}" th:utext="${themeVarsCss}"></style>
<!-- 站点级自定义样式(B3):位于皮肤 CSS 与微调变量之后层叠覆盖;内容经服务端黑名单强校验后直出 -->
<style th:if="${not #strings.isEmpty(customCss)}" th:utext="${customCss}"></style>
<link rel="icon" type="image/png"
th:href="${site.config['favicon'] != null ? url.upload(site.config['favicon']) : url.asset('favicon.png')}">
</head>
</html>
@@ -1,86 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- academic-navy 公共头部:深蓝顶栏(日期+院系切换) + 深蓝品牌行(徽标+站名+圆角搜索) + 白底主导航(吸顶反蓝、
主蓝半透明下拉面板)。数据绑定与 default 契约一致,仅换皮。 -->
<header class="an-header" th:fragment="header">
<a class="skip-link" href="#main-content">跳到主要内容</a>
<!-- 顶栏 -->
<div class="an-topbar">
<div class="an-topbar-inner">
<span class="an-topbar-date" id="siteDate" th:aria-label="'今日日期'">加载日期…</span>
<div class="an-topbar-side" th:if="${!site.colleges.isEmpty()}"
th:with="collegesLabel=${site.config['colleges_label'] ?: '专业院系'}">
<div class="an-colleges">
<span class="an-colleges-toggle">
<a href="javascript:void(0)" th:text="${collegesLabel}">专业院系</a>
<i class="an-colleges-arrow" aria-hidden="true">▾</i>
</span>
<ul class="an-colleges-menu">
<li th:each="col : ${site.colleges}" th:classappend="${col.path == site.path} ? 'active'">
<a th:href="${url.siteHome(col.path, col.publicBaseUrl)}"
th:title="${col.name}" th:text="${col.name}">学院名称</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 品牌行 -->
<div class="an-brandbar">
<div class="an-brandbar-inner">
<div class="an-brand">
<span class="an-brand-badge" th:if="${site.config['logo'] != null}">
<img class="an-logo" th:src="${url.upload(site.config['logo'])}" th:alt="${site.name}">
</span>
<span class="an-brand-badge an-brand-badge--plain" th:if="${site.config['logo'] == null}" aria-hidden="true">
<svg viewBox="0 0 24 24" width="26" height="26" focusable="false">
<path d="M12 2l2.4 4.9 5.4.8-3.9 3.8.9 5.4L12 14.4 7.2 16.9l.9-5.4L4.2 7.7l5.4-.8L12 2z"
fill="currentColor"></path>
</svg>
</span>
<a class="an-site-name" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
</div>
<form class="an-search" th:action="${url.search('', null)}" method="get" role="search">
<input type="text" name="q" placeholder="请输入关键字…" aria-label="站内搜索">
<button type="submit" aria-label="搜索">
<svg viewBox="0 0 24 24" width="16" height="16" focusable="false" aria-hidden="true">
<path d="M10.5 3a7.5 7.5 0 015.9 12.1l4.3 4.3-1.4 1.4-4.3-4.3A7.5 7.5 0 1110.5 3zm0 2a5.5 5.5 0 100 11 5.5 5.5 0 000-11z"
fill="currentColor"></path>
</svg>
</button>
</form>
</div>
</div>
<!-- 主导航:白底深蓝字;下拉为主蓝半透明面板(.an-sub 展开动效见 main.css);吸顶反蓝由 main.js 处理 -->
<nav class="an-nav" id="anNav" aria-label="栏目导航">
<div class="an-nav-inner">
<button class="an-nav-toggle" type="button" aria-label="展开菜单" aria-expanded="false">
<span></span><span></span><span></span>
</button>
<ul class="an-nav-list">
<li th:classappend="${currentCategoryId == null and navServiceActive == null} ? 'active'">
<a th:href="${url.home()}">首页</a>
</li>
<li th:each="cat : ${navCategories}"
th:classappend="${((currentCategoryId == cat.id or navTopId == cat.id) ? 'active' : '') + (!cat.children.isEmpty() ? ' has-sub' : '')}">
<a th:if="${cat.type == 3}" th:href="${cat.linkUrl}" target="_blank" rel="noopener"
th:text="${cat.name}">栏目</a>
<a th:unless="${cat.type == 3}" th:href="${url.category(cat.id)}"><span th:text="${cat.name}">栏目</span><i th:if="${!cat.children.isEmpty()}" class="an-caret" aria-hidden="true">▾</i></a>
<!-- 二级下拉:主蓝半透明面板,顶部亮线,项 hover 加深+左亮条 -->
<ul class="an-sub" th:if="${!cat.children.isEmpty()}">
<li th:each="sub : ${cat.children}"
th:classappend="${sub.id == currentCategoryId} ? 'active'">
<a th:if="${sub.type == 3}" th:href="${sub.linkUrl}" target="_blank" rel="noopener"
th:text="${sub.name}">子栏目</a>
<a th:unless="${sub.type == 3}" th:href="${url.category(sub.id)}" th:text="${sub.name}">子栏目</a>
</li>
</ul>
</li>
</ul>
</div>
</nav>
</header>
</html>
@@ -1,205 +0,0 @@
<!--
academic-navy(学府藏蓝)首页区块 fragment:站点配了 home_layout 时,区块正文必须由本文件渲染,
不能落到 themes/common 的通用骨架——否则换肤只换了页头页脚,首页正文仍是骨架内置蓝灰卡(线上事故根因)。
结构复用主题 main.css 第 7/9 节既有语汇(an-block / an-block-head--bar / an-block-item / an-date-chip /
an-feat-* / an-top-badge / an-empty),仅布局模式特有的排布(双栏、速递条、图墙、链接行)另起第 16 节。
契约:block 为渲染端装配的同构 Map(category.name/title/homeStyle/items/moreUrl/background),
与回落模式 categoryBlocks 兼容;区块标题一律取 category.name(两种模式都有值)。
-->
<!-- 形态一:双栏要闻(homeStyle=news_grid)——左 3 份头条图文、右 2 份紧凑行(综合类高校官网 60/40) -->
<section class="an-section an-container" th:fragment="news_grid(block)" aria-label="栏目区块"
th:with="newsBg=${block['background'] != null ? (block['background'] ? site.config['news_bg_image'] : null) : site.config['news_bg_image']}">
<div class="an-sec-bg" th:if="${!#strings.isEmpty(newsBg)}"
th:style="|background-image:url(${url.upload(newsBg)})|"></div>
<div class="an-block an-block--flow"
th:with="lead=${!#lists.isEmpty(block.items) ? block.items[0] : null},
rest=${#lists.size(block.items) > 1 ? block.items.subList(1, block.items.size()) : null}">
<div class="an-block-head an-block-head--bar">
<h2 th:text="${block.category.name}">校园要闻</h2>
<a class="an-more" th:href="${block.moreUrl}">更多 ›</a>
</div>
<div class="an-split" th:if="${lead != null}">
<div class="an-block-featured">
<a th:href="${url.article(lead.id)}">
<span class="an-feat-cover" th:if="${lead.coverImage != null}">
<img th:src="${url.upload(lead.coverImage)}" th:alt="${lead.title}" loading="lazy">
</span>
<span class="an-feat-title" th:text="${lead.title}">头条标题</span>
<span class="an-feat-summary" th:if="${!#strings.isEmpty(lead.summary)}"
th:text="${lead.summary}">摘要</span>
<span class="an-feat-meta">
<span class="an-top-badge" th:if="${lead.topmost == 1}">置顶</span>
<time th:if="${lead.publishTime != null}"
th:text="${#temporals.format(lead.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</span>
</a>
</div>
<ul class="an-block-list" th:if="${rest != null}">
<li class="an-block-item" th:each="item : ${rest}">
<a class="an-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<span class="an-top-badge" th:if="${item.topmost == 1}">置顶</span>
<time class="an-item-date" th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'MM-dd')}">01-01</time>
</li>
</ul>
</div>
<th:block th:if="${#lists.isEmpty(block.items)}">
<div th:replace="~{'themes/academic-navy/fragments/home-blocks' :: empty(${block})}"></div>
</th:block>
</div>
</section>
<!-- 形态二:默认卡(homeStyle 未设/未知)——整宽头条 + 行式列表 -->
<section class="an-section an-container" th:fragment="card(block)" aria-label="栏目区块">
<div class="an-block an-block--flow">
<div class="an-block-head an-block-head--bar">
<h2 th:text="${block.category.name}">栏目名</h2>
<a class="an-more" th:href="${block.moreUrl}">更多 ›</a>
</div>
<div class="an-block-featured" th:if="${!#lists.isEmpty(block.items)}"
th:with="feat=${block.items[0]}">
<a th:href="${url.article(feat.id)}">
<span class="an-feat-cover an-feat-cover--row" th:if="${feat.coverImage != null}">
<img th:src="${url.upload(feat.coverImage)}" th:alt="${feat.title}" loading="lazy">
</span>
<span class="an-feat-title" th:text="${feat.title}">头条标题</span>
<span class="an-feat-summary" th:if="${!#strings.isEmpty(feat.summary)}" th:text="${feat.summary}">摘要</span>
<span class="an-feat-meta">
<span class="an-top-badge" th:if="${feat.topmost == 1}">置顶</span>
<time th:if="${feat.publishTime != null}"
th:text="${#temporals.format(feat.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</span>
</a>
</div>
<ul class="an-block-list" th:if="${#lists.size(block.items) > 1}">
<li class="an-block-item" th:each="item, stat : ${block.items}" th:if="${stat.index > 0}">
<a class="an-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<span class="an-top-badge" th:if="${item.topmost == 1}">置顶</span>
<time class="an-item-date" th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'MM-dd')}">01-01</time>
</li>
</ul>
<th:block th:if="${#lists.isEmpty(block.items)}">
<div th:replace="~{'themes/academic-navy/fragments/home-blocks' :: empty(${block})}"></div>
</th:block>
</div>
</section>
<!-- 形态三:日期方块列表(homeStyle=compact)——官网最典型的「方块日期 + 标题」行式 -->
<section class="an-section an-container" th:fragment="compact(block)" aria-label="栏目区块">
<div class="an-block an-block--flow">
<div class="an-block-head an-block-head--bar">
<h2 th:text="${block.category.name}">通知公告</h2>
<a class="an-more" th:href="${block.moreUrl}">更多 ›</a>
</div>
<ul class="an-block-list" th:if="${!#lists.isEmpty(block.items)}">
<li class="an-block-item" th:each="item : ${block.items}">
<span class="an-date-chip" th:if="${item.publishTime != null}" th:with="d=${item.publishTime}">
<b th:text="${#temporals.format(d, 'dd')}">01</b>
<i th:text="${#temporals.format(d, 'yyyy-MM')}">2026-01</i>
</span>
<a class="an-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<span class="an-top-badge" th:if="${item.topmost == 1}">置顶</span>
</li>
</ul>
<th:block th:if="${#lists.isEmpty(block.items)}">
<div th:replace="~{'themes/academic-navy/fragments/home-blocks' :: empty(${block})}"></div>
</th:block>
</div>
</section>
<!-- 形态四:媒体聚焦(homeStyle=media_focus)——来源徽标 + 双列行式 -->
<section class="an-section an-container" th:fragment="media_focus(block)" aria-label="栏目区块">
<div class="an-block an-block--flow">
<div class="an-block-head an-block-head--bar">
<h2 th:text="${block.category.name}">媒体聚焦</h2>
<a class="an-more" th:href="${block.moreUrl}">更多 ›</a>
</div>
<ul class="an-block-list an-block-list--two" th:if="${!#lists.isEmpty(block.items)}">
<li class="an-block-item" th:each="item : ${block.items}">
<span class="an-src" th:if="${!#strings.isEmpty(item.source)}" th:text="${item.source}">媒体来源</span>
<a class="an-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time class="an-item-date" th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'MM-dd')}">01-01</time>
</li>
</ul>
<th:block th:if="${#lists.isEmpty(block.items)}">
<div th:replace="~{'themes/academic-navy/fragments/home-blocks' :: empty(${block})}"></div>
</th:block>
</div>
</section>
<!-- 形态五:图片墙(homeStyle=image_wall)——仅取有封面的条目,四列格 -->
<section class="an-section an-container" th:fragment="image_wall(block)" aria-label="栏目区块">
<div class="an-block-head an-block-head--bar">
<h2 th:text="${block.category.name}">图片新闻</h2>
<a class="an-more" th:href="${block.moreUrl}">更多 ›</a>
</div>
<ul class="an-wall" th:if="${!#lists.isEmpty(block.items)}">
<li class="an-wall-item" th:each="item : ${block.items}" th:if="${item.coverImage != null}">
<a th:href="${url.article(item.id)}" th:title="${item.title}">
<img th:src="${url.upload(item.coverImage)}" th:alt="${item.title}" loading="lazy">
<span class="an-wall-cap" th:text="${item.title}">图片标题</span>
</a>
</li>
</ul>
<th:block th:if="${#lists.isEmpty(block.items)}">
<div th:replace="~{'themes/academic-navy/fragments/home-blocks' :: empty(${block})}"></div>
</th:block>
</section>
<!-- 布局模式:要闻速递横条(block.items 由渲染端按置顶+最新装配;单行不换行,中文不塌列) -->
<section class="an-ticker" th:fragment="ticker(block)" aria-label="要闻速递"
th:if="${!#lists.isEmpty(block.items)}">
<div class="an-container an-ticker-inner">
<span class="an-ticker-label" th:text="${block.title}">要闻速递</span>
<ul class="an-ticker-list">
<li th:each="item : ${block.items}">
<a class="an-ticker-item" th:href="${url.article(item.id)}" th:text="${item.title}">要闻标题</a>
</li>
</ul>
</div>
</section>
<!-- 布局模式:线上展厅(block.items = 轮播位 gallery) -->
<section class="an-section an-container an-gallery" th:fragment="gallery(block)" aria-label="线上展厅"
th:if="${!#lists.isEmpty(block.items)}">
<div class="an-block-head">
<h2 th:text="${block.title}">线上展厅</h2>
<i class="an-block-tick" aria-hidden="true"></i>
</div>
<p class="an-gallery-sub" th:if="${!#strings.isEmpty(block.subtitle)}" th:text="${block.subtitle}">
Online Exhibition Hall
</p>
<ul class="an-wall an-wall--4">
<li class="an-wall-item" th:each="g : ${block.items}">
<a th:href="${g.linkUrl}" th:title="${g.title}">
<img th:src="${g.imageUrl}" th:alt="${g.title}" loading="lazy">
<span class="an-wall-cap" th:if="${!#strings.isEmpty(g.title)}" th:text="${g.title}">作品标题</span>
</a>
</li>
</ul>
</section>
<!-- 布局模式:友情链接分组(block.linkGroups 由渲染端按 group 过滤) -->
<section class="an-section an-container" th:fragment="links(block)" aria-label="友情链接">
<div class="an-block an-block--flow">
<div class="an-block-head an-block-head--bar">
<h2 th:text="${block.title}">友情链接</h2>
</div>
<th:block th:each="grp : ${block.linkGroups}">
<p class="an-links-group" th:if="${!#strings.isEmpty(grp.groupName)}" th:text="${grp.groupName}">分组</p>
<ul class="an-links">
<li th:each="l : ${grp.links}">
<a th:href="${l.linkUrl}" th:text="${l.name}" target="_blank" rel="noopener">链接名</a>
</li>
</ul>
</th:block>
</div>
</section>
<!-- 空态(各形态引用时守卫必须包在 th:block 外层:th:replace 优先级 100 先于 th:if 300 求值) -->
<div class="an-empty" th:fragment="empty(block)">
<p>栏目暂无内容</p>
<p class="an-empty-hint">内容发布后将在此展示</p>
</div>
@@ -1,149 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="an-home" id="main-content">
<!-- 首页轮播位(cms_banner position=home;设计尺寸见 theme.json bannerSize) -->
<section class="an-banner" th:if="${!#lists.isEmpty(banners)}" aria-label="焦点图片">
<div class="an-banner-slides">
<a class="an-banner-slide" th:each="b, stat : ${banners}"
th:classappend="${stat.first} ? 'active'"
th:href="${b.linkUrl}" th:title="${b.title}">
<img th:src="${b.imageUrl}" th:alt="${b.title}">
<span class="an-banner-title" th:text="${b.title}">标题</span>
</a>
</div>
<button class="an-banner-arrow prev" type="button" aria-label="上一张" th:if="${#lists.size(banners) > 1}">‹</button>
<button class="an-banner-arrow next" type="button" aria-label="下一张" th:if="${#lists.size(banners) > 1}">›</button>
<div class="an-banner-dots" th:if="${#lists.size(banners) > 1}">
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
th:aria-label="${'第' + (stat.count) + '张'}"></button>
</div>
</section>
<!-- 首页布局模式(docs/16 §4.4):区块形态由本主题 fragments/home-blocks 承载——换肤必须连首页正文一起换,
只换页头页脚会被判「皮肤不生效」。未知类型仍回落 common 骨架(渲染端已过滤未知 type,正常不触发)。
注意:th:replace(优先级100)先于 th:case(300)执行——case 必须放父元素、replace 放子元素 -->
<th:block th:if="${layoutBlocks != null}">
<th:block th:each="block : ${layoutBlocks}">
<th:block th:switch="${block.type}">
<th:block th:case="'article_list'">
<th:block th:switch="${block.homeStyle}">
<th:block th:case="'news_grid'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: news_grid(${block})}" /></th:block>
<th:block th:case="'media_focus'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: media_focus(${block})}" /></th:block>
<th:block th:case="'compact'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: compact(${block})}" /></th:block>
<th:block th:case="'image_wall'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: image_wall(${block})}" /></th:block>
<th:block th:case="*"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: card(${block})}" /></th:block>
</th:block>
</th:block>
<th:block th:case="'ticker'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: ticker(${block})}" /></th:block>
<th:block th:case="'gallery'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: gallery(${block})}" /></th:block>
<th:block th:case="'links'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: links(${block})}" /></th:block>
<th:block th:case="*"><th:block th:replace="~{'themes/common/fragments/home-blocks' :: render(${block})}" /></th:block>
</th:block>
</th:block>
</th:block>
<!-- 回落分支(docs/16 D2):无 home_layout 时按现状渲染 -->
<th:block th:unless="${layoutBlocks != null}">
<section class="an-section an-container" aria-label="头条与图片新闻">
<div class="an-headline">
<div class="an-headline-card" th:if="${!#lists.isEmpty(topArticles)}">
<div class="an-block-head">
<h2>头条推荐</h2>
<i class="an-block-tick" aria-hidden="true"></i>
</div>
<ul class="an-headline-list">
<li class="an-headline-lead" th:with="lead=${topArticles[0]}">
<a class="an-lead-title" th:href="${url.article(lead.id)}" th:text="${lead.title}">头条标题</a>
<div class="an-lead-meta">
<span class="an-top-badge" th:if="${lead.topmost == 1}">置顶</span>
<time th:if="${lead.publishTime != null}"
th:text="${#temporals.format(lead.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</div>
</li>
<li th:each="item, stat : ${topArticles}" th:if="${stat.index > 0}">
<span class="an-top-badge" th:if="${item.topmost == 1}">置顶</span>
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
</div>
<div class="an-focus-card" th:if="${!#lists.isEmpty(focusArticles)}">
<div class="an-block-head">
<h2>图片新闻</h2>
<i class="an-block-tick" aria-hidden="true"></i>
</div>
<a class="an-focus-lead" th:with="cover=${focusArticles[0]}"
th:href="${url.article(cover.id)}">
<span class="an-focus-cover">
<img th:src="${url.upload(cover.coverImage)}" th:alt="${cover.title}">
</span>
<span class="an-focus-title" th:text="${cover.title}">图片新闻标题</span>
</a>
<ul class="an-focus-rows">
<li th:each="row, stat : ${focusArticles}" th:if="${stat.index > 0}">
<a th:href="${url.article(row.id)}">
<span class="an-row-cover"><img th:src="${url.upload(row.coverImage)}" th:alt="${row.title}"></span>
<span class="an-row-title" th:text="${row.title}">图片新闻标题</span>
</a>
</li>
</ul>
</div>
</div>
</section>
<!-- 一级栏目区块(聚合列表型子孙栏目 top 8) -->
<section class="an-section an-container" aria-label="栏目区块">
<div class="an-blocks">
<div class="an-block" th:each="block : ${categoryBlocks}">
<div class="an-block-head an-block-head--bar">
<h2 th:text="${block.category.name}">栏目名</h2>
<a class="an-more" th:href="${block.moreUrl}">更多 ›</a>
</div>
<ul class="an-block-list" th:if="${!#lists.isEmpty(block.items)}">
<!-- 区块头条:有封面则图文,无封面则带摘要纯文 -->
<li class="an-block-featured" th:with="feat=${block.items[0]}">
<a th:href="${url.article(feat.id)}">
<span class="an-feat-cover" th:if="${feat.coverImage != null}">
<img th:src="${url.upload(feat.coverImage)}" th:alt="${feat.title}">
</span>
<span class="an-feat-body">
<span class="an-feat-title" th:text="${feat.title}">头条标题</span>
<span class="an-feat-summary" th:if="${!#strings.isEmpty(feat.summary)}"
th:text="${feat.summary}">摘要</span>
<span class="an-feat-meta">
<span class="an-top-badge" th:if="${feat.topmost == 1}">置顶</span>
<time th:if="${feat.publishTime != null}"
th:text="${#temporals.format(feat.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</span>
</span>
</a>
</li>
<li class="an-block-item" th:each="item, stat : ${block.items}" th:if="${stat.index > 0}">
<span class="an-date-chip" th:if="${item.publishTime != null}"
th:with="d=${item.publishTime}">
<b th:text="${#temporals.format(d, 'dd')}">01</b>
<i th:text="${#temporals.format(d, 'yyyy-MM')}">2026-01</i>
</span>
<a class="an-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<span class="an-top-badge" th:if="${item.topmost == 1}">置顶</span>
</li>
</ul>
<div class="an-empty" th:if="${#lists.isEmpty(block.items)}">
<p>栏目暂无内容</p>
<p class="an-empty-hint">内容发布后将在此展示</p>
</div>
</div>
</div>
</section>
</th:block>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,105 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="an-page an-container" id="main-content">
<div class="an-cols">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="an-side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="an-side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="an-side-nav-list">
<li class="an-side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="an-page-card">
<nav class="an-crumb" aria-label="面包屑" th:if="${searchMode != true}">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node, st : ${categoryPath}">
<span class="an-crumb-sep" aria-hidden="true">/</span>
<a th:if="${!st.last}" class="crumb-parent" th:href="${url.category(node.id)}"
th:text="${node.name}">父栏目</a>
<span class="an-crumb-current" th:if="${st.last}" th:text="${node.name}">栏目</span>
</th:block>
</nav>
<nav class="an-crumb" aria-label="面包屑" th:if="${searchMode == true}">
<a th:href="${url.home()}">首页</a>
<span class="an-crumb-sep" aria-hidden="true">/</span>
<span class="an-crumb-current">搜索结果</span>
</nav>
<h1 class="an-page-title" th:if="${searchMode != true}" th:text="${category.name}">栏目名</h1>
<h1 class="an-page-title" th:if="${searchMode == true}" th:text="'搜索:' + ${q}">搜索词</h1>
<ul class="an-article-list">
<!-- 有封面:图文行;无封面:行式(符号 + 标题 + 右侧日期) -->
<li th:each="item : ${listData.items}">
<div class="an-article-row" th:if="${item.coverImage != null}">
<a class="an-item-cover" th:href="${url.article(item.id)}">
<img th:src="${url.upload(item.coverImage)}" th:alt="${item.title}">
</a>
<div class="an-item-body">
<div class="an-item-title">
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
</div>
<p class="an-item-summary" th:if="${!#strings.isEmpty(item.summary)}"
th:text="${item.summary}">摘要</p>
<div class="an-item-meta">
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
<span class="an-top-badge" th:if="${item.topmost == 1}">置顶</span>
</div>
</div>
</div>
<div class="an-article-line" th:if="${item.coverImage == null}">
<i class="an-line-tick" aria-hidden="true"></i>
<div class="an-line-main">
<a class="an-line-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<p class="an-item-summary" th:if="${!#strings.isEmpty(item.summary)}"
th:text="${item.summary}">摘要</p>
</div>
<div class="an-line-side">
<span class="an-top-badge" th:if="${item.topmost == 1}">置顶</span>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</div>
</div>
</li>
</ul>
<div class="an-empty" th:if="${#lists.isEmpty(listData.items)}">
<p th:if="${searchMode == true}">未找到相关内容</p>
<p th:if="${searchMode != true}">栏目暂无内容</p>
<p class="an-empty-hint" th:if="${searchMode == true}">换个关键词试试</p>
<p class="an-empty-hint" th:if="${searchMode != true}">内容发布后将在此展示</p>
</div>
<!-- 分页(列表页/搜索页共用) -->
<nav class="an-pagination" th:if="${listData.totalPages > 1}"
th:with="cur=${listData.page}, total=${listData.totalPages}" aria-label="分页">
<a th:classappend="${cur <= 1} ? 'disabled'"
th:href="${searchMode == true} ? ${url.search(q, cur - 1)} : ${url.category(listData.categoryId) + '?page=' + (cur - 1)}"
th:aria-disabled="${cur <= 1}">上一页</a>
<a th:each="p : ${#numbers.sequence(1, total)}"
th:classappend="${p == cur} ? 'active'"
th:href="${searchMode == true} ? ${url.search(q, p)} : ${url.category(listData.categoryId) + '?page=' + p}"
th:aria-label="${'第' + p + '页'}"
th:text="${p}">1</a>
<a th:classappend="${cur >= total} ? 'disabled'"
th:href="${searchMode == true} ? ${url.search(q, cur + 1)} : ${url.category(listData.categoryId) + '?page=' + (cur + 1)}"
th:aria-disabled="${cur >= total}">下一页</a>
</nav>
</div>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,50 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="an-page an-container" id="main-content">
<div class="an-cols">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="an-side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="an-side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="an-side-nav-list">
<li class="an-side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="an-page-card">
<nav class="an-crumb" aria-label="面包屑">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node, st : ${categoryPath}">
<span class="an-crumb-sep" aria-hidden="true">/</span>
<a th:if="${!st.last}" class="crumb-parent" th:href="${url.category(node.id)}"
th:text="${node.name}">父栏目</a>
<span class="an-crumb-current" th:if="${st.last}" th:text="${node.name}">栏目</span>
</th:block>
</nav>
<article class="an-article">
<h1 class="an-article-title" th:text="${category.name}">栏目名</h1>
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
<div class="an-article-content" th:if="${!#strings.isEmpty(category.content)}"
th:utext="${category.content}">单页内容</div>
<div class="an-empty" th:if="${#strings.isEmpty(category.content)}">
<p>页面暂无内容</p>
<p class="an-empty-hint">内容编辑后将在此展示</p>
</div>
</article>
</div>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,23 +0,0 @@
{
"specVersion": 1,
"name": "academic-navy",
"title": "学府藏蓝(academic-navy)",
"version": "1.0.0",
"author": "yunhai",
"description": "学府藏蓝·高校官网门户:藏蓝顶栏+白底主导航+金线标题+日期方块列表+双栏要闻(左图右列)+深色三段页脚。基座 scholar-blue,配色与首页区块形态按综合类高校官网重做。",
"screenshot": "assets/screenshot.png",
"bannerSize": "1920x560",
"params": [
{ "key": "primary", "cssVar": "--an-primary", "label": "主色", "type": "color", "default": "#0b4a8f" },
{ "key": "primaryDark", "cssVar": "--an-primary-dark", "label": "主色加深", "type": "color", "default": "#063161" },
{ "key": "primaryLight", "cssVar": "--an-primary-light","label": "主色提亮", "type": "color", "default": "#1a6cb5" },
{ "key": "accent", "cssVar": "--an-accent", "label": "点缀金", "type": "color", "default": "#b8934a" },
{ "key": "navBg", "cssVar": "--an-nav-bg", "label": "导航条底色", "type": "color", "default": "#ffffff" },
{ "key": "footerBg", "cssVar": "--an-footer-bg", "label": "页脚底色", "type": "color", "default": "#0a2b4d" },
{ "key": "text", "cssVar": "--an-text", "label": "正文文字色", "type": "color", "default": "#2b3644" },
{ "key": "textLight", "cssVar": "--an-text-light", "label": "次要文字色", "type": "color", "default": "#6b7889" },
{ "key": "bg", "cssVar": "--an-bg", "label": "页面底色", "type": "color", "default": "#f1f4f9" },
{ "key": "cardBg", "cssVar": "--an-card-bg", "label": "卡片底色", "type": "color", "default": "#ffffff" },
{ "key": "border", "cssVar": "--an-border", "label": "边框色", "type": "color", "default": "#dbe3ee" }
]
}
@@ -1,58 +0,0 @@
# agri-forest · 农林绿
云海 CMS 皮肤包。fork 自 `default`(数据契约、模板 th:* 绑定与 default 完全一致),视觉层按农林院校门户语言全部重写,设计语言参考信阳农林学院官网(仅学语言,不复制实现)。
## 风格定位
- **气质词**:庄重、质朴、农林学府。
- **配色**:墨绿 `#1C6E19` 主导(通栏导航/日期方块/页脚主底),加深端 `#14510F`(页脚/标题条),提亮端 `#3FA43A`(当前导航项/hover),麦穗金 `#C8A45C` 只做装饰线与角标(栏目标题下划线、导航当前项下杠、日期方块 hover 金线、置顶角标)。
- **结构**:白色页头(左校徽+校名,右工具行:今日日期 + 校园直通车下拉 + 站内搜索)→ 墨绿通栏主导航(白字,当前栏目提亮绿底+金下杠,导航下方与 banner 无缝)→ 通栏轮播(左下墨绿标题条带金边)→ 首页区块 → 墨绿多列页脚(更深底条)。
- **工艺**:小圆角 4px;间距刻度 4/8/12/16/24/32/48;字号阶梯 12/13/14/15/16/18/20/24/28/32;hover 过渡 0.2s ease;正文行高 1.75;无渐变堆砌、无粉紫、无玻璃拟态;图标一律内联 SVG。
## 参数取值建议(theme.json,13 项)
| key | cssVar | 默认 | 建议 |
| --- | --- | --- | --- |
| primary | `--af-primary` | `#1C6E19` | 主色。换季校庆可微调为 `#1F7A1B`,勿低于 `#14510F` 明度(导航白字对比) |
| primaryDark | `--af-primary-dark` | `#14510F` | 页脚主底/轮播标题条。与 primary 保持同色相 1~2 档差 |
| primaryLight | `--af-primary-light` | `#3FA43A` | 当前导航项/hover。需要更醒目可用 `#4DB548`,过亮会刺眼 |
| accent | `--af-accent` | `#C8A45C` | 麦穗金装饰线/角标。庄重场合可降饱和为 `#B8934A` |
| navBg | `--af-nav-bg` | `#1C6E19` | 主导航底色。默认与 primary 一致;要"导航深一档"配 `#14510F` |
| footerBg | `--af-footer-bg` | `#14510F` | 页脚主底;底条固定更深一档(`--af-primary-deep`,非参数) |
| text / textLight | `--af-text` / `--af-text-light` | `#2A3327` / `#6C7A67` | 正文/次要文字,偏绿灰 |
| bg | `--af-bg` | `#F5F8F3` | 页面底色,带一点绿调的暖白 |
| cardBg | `--af-card-bg` | `#FFFFFF` | 卡片底,建议保持纯白 |
| border | `--af-border` | `#DDE8DA` | 边框/分隔线,绿灰调 |
| linkGreen | `--af-link` | `#277428` | 正文链接绿,介于主色深浅之间 |
| listLayout | —(select) | `date` | 列表页形态:`date`=46px 日期方块行(农林官网典型);`plain`=通栏行式(日期右置),信息密度更高 |
微调优先级:站点 `theme_params`(本表参数)→ 站点 `custom_css` → 本包 main.css(契约 §6)。
## 首页九段形态表(fragments/home-blocks.html)
站点配 `home_layout` 时首页正文由本包 fragment 渲染(换肤必须连正文一起换):
| fragment | 触发 | 形态 |
| --- | --- | --- |
| `news_grid` | article_list + homeStyle=news_grid | 三图焦点(横排 3 封面卡)+ 双列要闻行式列表;支持站点背景带(news_bg_image,画在卡片之下) |
| `media_focus` | article_list + homeStyle=media_focus | 学术动态:双列密行 + 墨绿来源徽标 |
| `compact` | article_list + homeStyle=compact | 通知公告:46px 墨绿日期方块(日大号+年月小号),行 hover 出金线 |
| `image_wall` | article_list + homeStyle=image_wall | 四列图片墙(仅有封面条目),悬浮标题 |
| `card` | article_list 其他/未设 | 默认卡:头条图文(有封面则图文)+ 行式列表 |
| `ticker` | type=ticker | 要闻速递横条:墨绿底、金色分隔、单行不塌列 |
| `gallery` | type=gallery | 校园映像:居中标题+副题+四列图墙+悬浮标题 |
| `links` | type=links | 专题网站:4 列缩略卡(绿标题条+金下杠+圆徽),分组拉平 |
| `empty` | 各形态 items 为空 | 空态「栏目暂无内容」(守卫包在外层 th:block,th:replace 优先级 100 先于 th:if 300) |
未知类型回落 `themes/common` 通用骨架;`:root` 已把 `--af-*` 别名到契约变量(`--primary/--text/--card-bg/--border/--bg/--radius`),兜底骨架与 themeVarsCss 换色可穿透。
## 纪律要点
- URL 一律 `url.*` 生成,无硬编码绝对地址;非转义输出仅契约七处白名单。
- 中文防塌列:`.af-ticker-list{min-width:0;white-space:nowrap}`、`.af-ticker-item{flex:none;text-overflow:ellipsis}`、`.af-block-item-title{min-width:0;overflow:hidden;text-overflow:ellipsis}`。
- 网格一律 `minmax(0,1fr)`;≤768px 单列、导航横滚、子菜单静态平铺。
- 背景带层序:`.af-sec-bg` z-index:0,其后 `.af-block` z-index:1。
## 截图说明
`assets/screenshot.png` 为按本皮肤设计语言绘制的门户结构示意图(非浏览器实拍)。管理端选择器如需实拍图,起服后截首页替换即可(记入遗留项)。
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

@@ -1,87 +0,0 @@
/* agri-forest 皮肤包脚本:页头日期 + 首页轮播(自动播放/圆点/箭头/悬停暂停/键盘),无任何外部依赖 */
(function () {
/* 页头工具行日期(本地时间,YYYY年M月D日 星期X) */
var dateEl = document.getElementById('siteDate');
if (dateEl) {
var now = new Date();
var week = ['日', '一', '二', '三', '四', '五', '六'][now.getDay()];
dateEl.textContent = now.getFullYear() + '年' + (now.getMonth() + 1) + '月'
+ now.getDate() + '日 星期' + week;
}
/* 轮播 */
var root = document.querySelector('.af-banner');
var slides = document.querySelectorAll('.af-banner-slide');
var dots = document.querySelectorAll('.af-banner-dots button');
var prevBtn = document.querySelector('.af-banner-arrow.prev');
var nextBtn = document.querySelector('.af-banner-arrow.next');
if (!slides.length || !root) {
return;
}
var current = 0;
var timer = null;
var INTERVAL = 5000;
function show(index) {
slides[current].classList.remove('active');
if (dots[current]) {
dots[current].classList.remove('active');
}
current = (index + slides.length) % slides.length;
slides[current].classList.add('active');
if (dots[current]) {
dots[current].classList.add('active');
}
}
function start() {
stop();
if (slides.length > 1) {
timer = window.setInterval(function () {
show(current + 1);
}, INTERVAL);
}
}
function stop() {
if (timer) {
window.clearInterval(timer);
timer = null;
}
}
dots.forEach(function (dot, index) {
dot.addEventListener('click', function () {
show(index);
start();
});
});
if (prevBtn) {
prevBtn.addEventListener('click', function () {
show(current - 1);
start();
});
}
if (nextBtn) {
nextBtn.addEventListener('click', function () {
show(current + 1);
start();
});
}
root.addEventListener('mouseenter', stop);
root.addEventListener('mouseleave', start);
root.addEventListener('focusin', stop);
root.addEventListener('focusout', start);
root.setAttribute('tabindex', '-1');
root.addEventListener('keydown', function (e) {
if (e.key === 'ArrowLeft') {
show(current - 1);
start();
} else if (e.key === 'ArrowRight') {
show(current + 1);
start();
}
});
start();
})();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

@@ -1,76 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="af-page af-container" id="main-content">
<div class="af-cols">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="af-side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="af-side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="af-side-nav-list">
<li class="af-side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="af-page-card">
<nav class="af-crumb" aria-label="面包屑">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node : ${categoryPath}">
<span class="af-crumb-sep" aria-hidden="true">/</span>
<a class="af-crumb-parent" th:href="${url.category(node.id)}" th:text="${node.name}">栏目</a>
</th:block>
<span class="af-crumb-sep" th:if="${currentCategoryId != null}" aria-hidden="true">/</span>
<span class="af-crumb-current">正文</span>
</nav>
<article class="af-article">
<h1 class="af-article-title" th:text="${article.title}">文章标题</h1>
<div class="af-article-meta">
<time th:if="${article.publishTime != null}"
th:text="${'发布时间:' + #temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
<span th:if="${!#strings.isEmpty(article.authorName)}"
th:text="'作者:' + ${article.authorName}">作者</span>
<span th:if="${!#strings.isEmpty(article.source)}"
th:text="'来源:' + ${article.source}">来源</span>
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
</div>
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
其余字段一律 th:text 转义输出 -->
<div class="af-article-content" th:utext="${article.content}">正文</div>
</article>
<nav class="af-sibling" aria-label="上下篇导航">
<a th:if="${prevArticle != null}" class="prev"
th:href="${url.article(prevArticle.id)}">
<span class="sib-label">上一篇</span>
<span class="sib-title" th:text="${prevArticle.title}">上一篇标题</span>
</a>
<span class="sib-none prev" th:if="${prevArticle == null}">
<span class="sib-label">上一篇</span>
<span class="sib-title">已经是最早一篇</span>
</span>
<a th:if="${nextArticle != null}" class="next"
th:href="${url.article(nextArticle.id)}">
<span class="sib-label">下一篇</span>
<span class="sib-title" th:text="${nextArticle.title}">下一篇标题</span>
</a>
<span class="sib-none next" th:if="${nextArticle == null}">
<span class="sib-label">下一篇</span>
<span class="sib-title">已是最新一篇</span>
</span>
</nav>
</div>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,92 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共页脚(农林绿·墨绿多列):品牌区(校徽+站名+社媒图标) + 地址/联系/版权/备案 + 二维码位
+ 友链分组(浅色带)+ 更深底条(校名)。
数据契约与 default 一致(linkGroups/contact/copyright/beian/logo/schName),
增强键(docs/12):footerAddressLines / qr_wechat / qr_weibo / social_*(有值才渲染,空站点优雅降级)。 -->
<footer class="af-footer" th:fragment="footer">
<!-- 主区:墨绿多列 -->
<div class="af-footer-main">
<div class="af-container af-footer-cols">
<!-- 品牌列 -->
<div class="af-footer-brand">
<div class="af-footer-logo-row">
<img th:if="${site.config['logo'] != null}" class="af-footer-logo"
th:src="${url.upload(site.config['logo'])}" th:alt="${site.name}">
<span class="af-footer-site" th:text="${site.name}">站点名称</span>
</div>
<!-- 社媒主页(url 校验,有值才渲染对应图标;图标内联 SVG,契约 §4.6 不引外链) -->
<div class="af-social" th:if="${site.config['social_wechat'] != null
or site.config['social_weibo'] != null
or site.config['social_douyin'] != null
or site.config['social_bilibili'] != null}">
<a th:if="${site.config['social_wechat'] != null}" class="af-social-icon"
th:href="${site.config['social_wechat']}" target="_blank" rel="noopener" aria-label="微信公众号" th:title="微信公众号">
<svg viewBox="0 0 24 24" width="20" height="20" focusable="false" aria-hidden="true"><path fill="currentColor" d="M9.3 4C5.8 4 3 6.4 3 9.4c0 1.7.9 3.2 2.4 4.2l-.6 2 2.2-1.1c.6.2 1.3.3 2 .3h.4a5 5 0 0 1-.2-1.4c0-2.9 2.7-5.2 6-5.2h.3C14.9 5.9 12.3 4 9.3 4zM7 7.5a.9.9 0 1 1 0 1.8.9.9 0 0 1 0-1.8zm4.7 0a.9.9 0 1 1 0 1.8.9.9 0 0 1 0-1.8zM15 9.6c-3 0-5.4 2-5.4 4.5S12 18.5 15 18.5c.6 0 1.2-.1 1.7-.3l1.9 1-.5-1.8c1.3-.9 2.2-2.2 2.2-3.7 0-2.5-2.4-4.1-5.3-4.1zm-1.9 2.5a.8.8 0 1 1 0 1.6.8.8 0 0 1 0-1.6zm3.8 0a.8.8 0 1 1 0 1.6.8.8 0 0 1 0-1.6z"/></svg>
</a>
<a th:if="${site.config['social_weibo'] != null}" class="af-social-icon"
th:href="${site.config['social_weibo']}" target="_blank" rel="noopener" aria-label="微博" th:title="微博">
<svg viewBox="0 0 24 24" width="20" height="20" focusable="false" aria-hidden="true"><path fill="currentColor" d="M10.1 19.6c-3.5.3-6.6-1.3-6.9-3.6-.3-2.2 2.3-4.4 5.8-4.8 3.5-.3 6.6 1.3 6.9 3.6.3 2.3-2.3 4.5-5.8 4.8zm3.2-4.9c-.4-1.3-2-2-3.8-1.8-1.9.3-3.2 1.6-2.9 3 .3 1.3 2 2.1 3.9 1.8 1.8-.3 3.1-1.7 2.8-3zm-3.1 1.9a.7.7 0 0 1-.9.3.6.6 0 0 1-.2-.9c.2-.3.6-.4.9-.2.3.1.4.5.2.8zm.8-1a.3.3 0 0 1-.4.1c-.2 0-.3-.2-.2-.4.1-.1.3-.2.4-.1.2 0 .3.2.2.4zm3.6-7.3a3.9 3.9 0 0 0-3.6-1.1.5.5 0 0 0-.4.6c.1.3.4.5.6.4a2.9 2.9 0 0 1 3.3 3.7c-.1.3.1.5.4.6.2 0 .5-.1.6-.4.4-1.4.1-2.8-.9-3.8zm2.6-1a6.8 6.8 0 0 0-6.3-2 .5.5 0 0 0-.4.6c0 .3.3.5.6.4a5.8 5.8 0 0 1 6.7 7.4c-.1.3.1.5.3.6.3.1.6 0 .7-.3.8-2.3.3-4.9-1.6-6.7z"/></svg>
</a>
<a th:if="${site.config['social_douyin'] != null}" class="af-social-icon"
th:href="${site.config['social_douyin']}" target="_blank" rel="noopener" aria-label="抖音" th:title="抖音">
<svg viewBox="0 0 24 24" width="20" height="20" focusable="false" aria-hidden="true"><path fill="currentColor" d="M16.6 3c.4 1.8 1.6 3.2 3.4 3.5v2.8c-1.3 0-2.5-.4-3.5-1.1v5.6a5.6 5.6 0 1 1-5.6-5.6c.3 0 .6 0 .9.1v2.9a2.7 2.7 0 1 0 1.9 2.6V3h2.9z"/></svg>
</a>
<a th:if="${site.config['social_bilibili'] != null}" class="af-social-icon"
th:href="${site.config['social_bilibili']}" target="_blank" rel="noopener" aria-label="哔哩哔哩" th:title="哔哩哔哩">
<svg viewBox="0 0 24 24" width="20" height="20" focusable="false" aria-hidden="true"><path fill="currentColor" d="M7.2 4.3 9 6.4h6l1.8-2.1c.3-.4.9-.4 1.2-.1.3.3.4.8.1 1.2l-1 1h1.4A3.5 3.5 0 0 1 22 9.9v6.6a3.5 3.5 0 0 1-3.5 3.5h-13A3.5 3.5 0 0 1 2 16.5V9.9a3.5 3.5 0 0 1 3.5-3.5h1.4l-1-1A.9.9 0 0 1 6 4.2c.3-.3.9-.3 1.2.1zM5.5 8.2A1.7 1.7 0 0 0 3.8 9.9v6.6c0 .9.8 1.7 1.7 1.7h13c.9 0 1.7-.8 1.7-1.7V9.9c0-.9-.8-1.7-1.7-1.7h-13zm3 3.2c.5 0 .9.4.9.9v.9a.9.9 0 1 1-1.8 0v-.9c0-.5.4-.9.9-.9zm7 0c.5 0 .9.4.9.9v.9a.9.9 0 1 1-1.8 0v-.9c0-.5.4-.9.9-.9z"/></svg>
</a>
</div>
</div>
<!-- 地址/联系列(多校区地址按行直出;contact/copyright/beian 可空,空块收敛) -->
<div class="af-footer-info">
<ul class="af-footer-addr" th:if="${!#lists.isEmpty(site.footerAddressLines)}">
<li th:each="line : ${site.footerAddressLines}" th:text="${line}">校区地址</li>
</ul>
<p class="af-footer-line" th:if="${site.config['contact'] != null}" th:text="${site.config['contact']}">联系方式</p>
<p class="af-footer-line" th:if="${site.config['copyright'] != null}" th:text="${site.config['copyright']}">版权信息</p>
<p class="af-footer-line" th:if="${site.config['beian'] != null}" th:text="${site.config['beian']}">备案号</p>
</div>
<!-- 二维码位(asset 校验,有值才渲染) -->
<div class="af-footer-qr" th:if="${site.config['qr_wechat'] != null or site.config['qr_weibo'] != null}">
<figure class="af-qr" th:if="${site.config['qr_wechat'] != null}">
<img th:src="${url.upload(site.config['qr_wechat'])}" alt="微信二维码">
<figcaption>官方微信</figcaption>
</figure>
<figure class="af-qr" th:if="${site.config['qr_weibo'] != null}">
<img th:src="${url.upload(site.config['qr_weibo'])}" alt="微博二维码">
<figcaption>官方微博</figcaption>
</figure>
</div>
</div>
</div>
<!-- 友链区(浅绿带;无分组的站整段不渲染;分组无链接时防御性跳过——数据上分组由链接行物化,不可达) -->
<div class="af-footer-links" th:if="${!#lists.isEmpty(site.linkGroups)}">
<div class="af-container af-footer-links-inner">
<div class="af-links-group" th:each="group : ${site.linkGroups}"
th:if="${!#lists.isEmpty(group.links)}">
<span class="af-group-name" th:text="${group.groupName}">分组</span>
<p class="af-links-row">
<a th:each="link : ${group.links}" th:href="${link.linkUrl}" target="_blank" rel="noopener">
<!-- B4 友链 Logo:有 logo 图片+名称并排(渲染期补基准),无 logo 纯文字 -->
<img th:if="${link.logoUrl != null and !#strings.isEmpty(link.logoUrl)}" class="af-link-logo"
th:src="${url.upload(link.logoUrl)}" th:alt="${link.name}" loading="lazy">
<span th:text="${link.name}">链接</span>
</a>
</p>
</div>
</div>
</div>
<!-- 底条(更深墨绿):全站无任何配置信息时仍保留结构完整(空块由 CSS 收敛) -->
<div class="af-footer-bottom">
<div class="af-container af-footer-bottom-inner">
<p th:if="${site.schName != null and !#strings.isEmpty(site.schName)}"
th:text="${site.schName}">学校名称</p>
</div>
</div>
</footer>
</html>
@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<!-- 公共 head 片段:SEO(title/description/canonical)+ 主题资产(方案 §8.3/8.6),与 default 数据契约一致 -->
<head th:fragment="head">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:text="${pageTitle}">页面标题</title>
<meta name="description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}"
th:content="${seoDescription}">
<link rel="canonical" th:if="${canonical != null}" th:href="${canonical}">
<!-- og 标签基本三件(C-7,方案 §8.6 canonical/og 同基准) -->
<meta property="og:title" th:if="${pageTitle != null}" th:content="${pageTitle}">
<meta property="og:description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}" th:content="${seoDescription}">
<meta property="og:image" th:if="${ogImage != null}" th:content="${ogImage}">
<link rel="stylesheet" th:href="${url.asset('css/main.css')}">
<!-- 微调参数 CSS 变量覆盖(方案 §5.3):位于皮肤 CSS 之后层叠覆盖;值由服务端预构建并校验 -->
<style th:if="${not #strings.isEmpty(themeVarsCss)}" th:utext="${themeVarsCss}"></style>
<!-- 站点级自定义样式(B3):位于皮肤 CSS 与微调变量之后层叠覆盖;内容经服务端黑名单强校验后直出 -->
<style th:if="${not #strings.isEmpty(customCss)}" th:utext="${customCss}"></style>
<!-- favicon:管理端配置了 favicon 键用上传图,未配置输出主题内置兜底 icon(assets/favicon.png) -->
<link rel="icon" type="image/png"
th:href="${site.config['favicon'] != null ? url.upload(site.config['favicon']) : url.asset('favicon.png')}">
</head>
</html>
@@ -1,66 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共头部(农林绿):白色页头(左校徽+校名 / 右工具行:日期+校园直通车下拉+搜索)
+ 墨绿通栏主导航(白字,当前栏目 primaryLight 底),导航下方与 banner 无缝衔接。
数据契约与 default 完全一致:topbar 日期由 main.js 填充、专业院系下拉(site.colleges 空则不渲染)。 -->
<header class="af-header" th:fragment="header">
<a class="af-skip-link" href="#main-content">跳到主要内容</a>
<!-- 白色页头 -->
<div class="af-head">
<div class="af-head-inner">
<!-- 左:校徽 + 校名 -->
<div class="af-brand">
<img th:if="${site.config['logo'] != null}" class="af-logo"
th:src="${url.upload(site.config['logo'])}" th:alt="${site.name}">
<a class="af-sitename" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
</div>
<!-- 右:工具行(今日日期 + 校园直通车 + 站内搜索);无子站站点下拉整块不渲染 -->
<div class="af-head-side">
<span class="af-date" id="siteDate" th:aria-label="'今日日期'">加载日期…</span>
<div class="af-colleges" th:if="${!site.colleges.isEmpty()}"
th:with="collegesLabel=${site.config['colleges_label'] ?: '校园直通车'}">
<span class="af-colleges-toggle">
<a href="javascript:void(0)" th:text="${collegesLabel}">校园直通车</a>
<i class="af-colleges-arrow" aria-hidden="true">▾</i>
</span>
<ul class="af-colleges-menu">
<li th:each="col : ${site.colleges}" th:classappend="${col.path == site.path} ? 'active'">
<a th:href="${url.siteHome(col.path, col.publicBaseUrl)}"
th:title="${col.name}" th:text="${col.name}">学院名称</a>
</li>
</ul>
</div>
<form class="af-search" th:action="${url.search('', null)}" method="get" role="search">
<input type="text" name="q" placeholder="站内搜索" aria-label="站内搜索">
<button type="submit">搜索</button>
</form>
</div>
</div>
</div>
<!-- 墨绿通栏主导航:外链栏目直接外跳;其余进栏目页(方案 §8.1) -->
<nav class="af-nav" aria-label="栏目导航">
<ul>
<li th:classappend="${currentCategoryId == null and navServiceActive == null} ? 'active'">
<a th:href="${url.home()}">首页</a>
</li>
<li th:each="cat : ${navCategories}"
th:classappend="${((currentCategoryId == cat.id or navTopId == cat.id) ? 'active' : '') + (!cat.children.isEmpty() ? ' has-sub' : '')}">
<a th:if="${cat.type == 3}" th:href="${cat.linkUrl}" target="_blank" rel="noopener"
th:text="${cat.name}">栏目</a>
<a th:unless="${cat.type == 3}" th:href="${url.category(cat.id)}"><span th:text="${cat.name}">栏目</span><i th:if="${!cat.children.isEmpty()}" class="af-caret" aria-hidden="true">▾</i></a>
<!-- 二级下拉:白底面板(.af-sub-nav 样式见 assets/css/main.css);当前项高亮(docs/11) -->
<ul class="af-sub-nav" th:if="${!cat.children.isEmpty()}">
<li th:each="sub : ${cat.children}"
th:classappend="${sub.id == currentCategoryId} ? 'active'">
<a th:if="${sub.type == 3}" th:href="${sub.linkUrl}" target="_blank" rel="noopener"
th:text="${sub.name}">子栏目</a>
<a th:unless="${sub.type == 3}" th:href="${url.category(sub.id)}" th:text="${sub.name}">子栏目</a>
</li>
</ul>
</li>
</ul>
</nav>
</header>
</html>
@@ -1,257 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!--
agri-forest(农林绿)首页区块 fragment:站点配了 home_layout 时,区块正文必须由本文件渲染,
不能落到 themes/common 的通用骨架——否则换肤只换了页头页脚,首页正文仍是骨架内置蓝灰卡(线上事故根因)。
九段形态:news_grid(三图焦点+要闻列表)/ media_focus(来源徽标密行)/ compact(46px 日期方块行)/
image_wall(四列图片墙)/ card(默认卡:头条+行式)/ ticker(速递横条)/ gallery(校园映像图廊)/
links(专题网站缩略卡)/ empty(空态)。
契约:block 为渲染端装配的同构 Map(type/title/category.name/homeStyle/items/moreUrl/subtitle/
background/source/linkGroups),与回落模式 categoryBlocks 兼容;区块标题一律取 category.name。
空态守卫必须包在 th:block 外层:th:replace(优先级 100)先于 th:if(300)求值,写在同一标签上等于没写。
-->
<!-- 形态一:三图焦点 + 要闻列表(homeStyle=news_grid)——横排 3 张封面卡 + 双列行式列表 -->
<section class="af-section af-section--news" th:fragment="news_grid(block)" aria-label="栏目区块"
th:with="newsBg=${block['background'] != null ? (block['background'] ? site.config['news_bg_image'] : null) : site.config['news_bg_image']}">
<div class="af-sec-bg" th:if="${!#strings.isEmpty(newsBg)}"
th:style="|background-image:url(${url.upload(newsBg)})|"></div>
<div class="af-block af-container">
<div class="af-block-head">
<div class="af-block-title">
<span class="af-block-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M4 5h11a1 1 0 0 1 1 1v13H6a2 2 0 0 1-2-2V5z"/><path d="M16 9h3a1 1 0 0 1 1 1v7a2 2 0 0 1-2 2"/><path d="M7 9h6M7 13h6M7 17h4"/></svg>
</span>
<h2 th:text="${block.category.name}">学校要闻</h2>
</div>
<a class="af-more" th:href="${block.moreUrl}">更多 &gt;</a>
</div>
<div class="af-news-body" th:if="${!#lists.isEmpty(block.items)}"
th:with="rest=${#lists.size(block.items) > 3 ? block.items.subList(3, block.items.size()) : null}">
<ul class="af-trio">
<li class="af-trio-item" th:each="item, stat : ${block.items}" th:if="${stat.index &lt; 3}">
<a th:href="${url.article(item.id)}" th:title="${item.title}">
<span class="af-trio-cover" th:if="${item.coverImage != null}">
<img th:src="${url.upload(item.coverImage)}" th:alt="${item.title}" loading="lazy">
</span>
<span class="af-trio-cover af-trio-ph" th:if="${item.coverImage == null}" aria-hidden="true">
<svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M5 19C5 9 12 4 20 4c0 9-5 15-12 15-1.5 0-3-.4-3-1z"/><path d="M5 19c3-5 6-8 10-10"/></svg>
</span>
<span class="af-trio-cap">
<span class="af-trio-title" th:text="${item.title}">焦点标题</span>
<span class="af-trio-meta">
<span class="af-top-badge" th:if="${item.topmost == 1}">置顶</span>
<time class="af-item-date" th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</span>
</span>
</a>
</li>
</ul>
<ul class="af-block-list af-block-list--two" th:if="${rest != null}">
<li class="af-line" th:each="item : ${rest}">
<a class="af-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<span class="af-top-badge" th:if="${item.topmost == 1}">置顶</span>
<time class="af-item-date" th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'MM-dd')}">01-01</time>
</li>
</ul>
</div>
<th:block th:if="${#lists.isEmpty(block.items)}">
<div th:replace="~{'themes/agri-forest/fragments/home-blocks' :: empty(${block})}"></div>
</th:block>
</div>
</section>
<!-- 形态二:来源徽标密行(homeStyle=media_focus)——学术动态,双列紧凑行 + 来源徽标 -->
<section class="af-section af-container" th:fragment="media_focus(block)" aria-label="栏目区块">
<div class="af-block">
<div class="af-block-head">
<div class="af-block-title">
<span class="af-block-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M9 3h6M10 3v6L4.6 18A2 2 0 0 0 6.3 21h11.4a2 2 0 0 0 1.7-3L14 9V3"/><path d="M7.5 15h9"/></svg>
</span>
<h2 th:text="${block.category.name}">学术动态</h2>
</div>
<a class="af-more" th:href="${block.moreUrl}">更多 &gt;</a>
</div>
<ul class="af-block-list af-block-list--two" th:if="${!#lists.isEmpty(block.items)}">
<li class="af-line af-line--media" th:each="item : ${block.items}">
<span class="af-src" th:if="${!#strings.isEmpty(item.source)}" th:text="${item.source}">来源</span>
<a class="af-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<span class="af-top-badge" th:if="${item.topmost == 1}">置顶</span>
<time class="af-item-date" th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'MM-dd')}">01-01</time>
</li>
</ul>
<th:block th:if="${#lists.isEmpty(block.items)}">
<div th:replace="~{'themes/agri-forest/fragments/home-blocks' :: empty(${block})}"></div>
</th:block>
</div>
</section>
<!-- 形态三:46px 日期方块列表(homeStyle=compact)——通知公告:日大号 + 年月小号,墨绿底白字,hover 金线 -->
<section class="af-section af-container" th:fragment="compact(block)" aria-label="栏目区块">
<div class="af-block">
<div class="af-block-head">
<div class="af-block-title">
<span class="af-block-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M3 10v4l6 1 9 5V4L9 9l-6 1z"/><path d="M7 15l1.5 5h3L10 16"/></svg>
</span>
<h2 th:text="${block.category.name}">通知公告</h2>
</div>
<a class="af-more" th:href="${block.moreUrl}">更多 &gt;</a>
</div>
<ul class="af-block-list af-notice-list" th:if="${!#lists.isEmpty(block.items)}">
<li class="af-line af-line--date" th:each="item : ${block.items}">
<span class="af-date-chip" th:if="${item.publishTime != null}" th:with="d=${item.publishTime}">
<b th:text="${#temporals.format(d, 'dd')}">01</b>
<i th:text="${#temporals.format(d, 'yyyy-MM')}">2026-01</i>
</span>
<a class="af-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<span class="af-top-badge" th:if="${item.topmost == 1}">置顶</span>
</li>
</ul>
<th:block th:if="${#lists.isEmpty(block.items)}">
<div th:replace="~{'themes/agri-forest/fragments/home-blocks' :: empty(${block})}"></div>
</th:block>
</div>
</section>
<!-- 形态四:四列图片墙(homeStyle=image_wall)——仅取有封面的条目,悬浮标题 -->
<section class="af-section af-container" th:fragment="image_wall(block)" aria-label="栏目区块">
<div class="af-block-head">
<div class="af-block-title">
<span class="af-block-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><rect x="3" y="5" width="18" height="14" rx="1"/><circle cx="8.5" cy="10" r="1.5"/><path d="M21 15.5l-4.5-4.5-9 8"/></svg>
</span>
<h2 th:text="${block.category.name}">图片新闻</h2>
</div>
<a class="af-more" th:href="${block.moreUrl}">更多 &gt;</a>
</div>
<ul class="af-wall" th:if="${!#lists.isEmpty(block.items)}">
<li class="af-wall-item" th:each="item : ${block.items}" th:if="${item.coverImage != null}">
<a th:href="${url.article(item.id)}" th:title="${item.title}">
<img th:src="${url.upload(item.coverImage)}" th:alt="${item.title}" loading="lazy">
<span class="af-wall-cap" th:text="${item.title}">图片标题</span>
</a>
</li>
</ul>
<th:block th:if="${#lists.isEmpty(block.items)}">
<div th:replace="~{'themes/agri-forest/fragments/home-blocks' :: empty(${block})}"></div>
</th:block>
</section>
<!-- 形态五:默认卡(homeStyle 未设/未知)——整宽头条(有封面则图文)+ 行式列表 -->
<section class="af-section af-container" th:fragment="card(block)" aria-label="栏目区块">
<div class="af-block">
<div class="af-block-head">
<div class="af-block-title">
<span class="af-block-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M6 3h8l4 4v14H6V3z"/><path d="M14 3v4h4M9 12h6M9 16h6"/></svg>
</span>
<h2 th:text="${block.category.name}">栏目名</h2>
</div>
<a class="af-more" th:href="${block.moreUrl}">更多 &gt;</a>
</div>
<div class="af-feat" th:if="${!#lists.isEmpty(block.items)}" th:with="feat=${block.items[0]}">
<a th:href="${url.article(feat.id)}">
<span class="af-feat-cover" th:if="${feat.coverImage != null}">
<img th:src="${url.upload(feat.coverImage)}" th:alt="${feat.title}" loading="lazy">
</span>
<span class="af-feat-body">
<span class="af-feat-title" th:text="${feat.title}">头条标题</span>
<span class="af-feat-summary" th:if="${!#strings.isEmpty(feat.summary)}"
th:text="${feat.summary}">摘要</span>
<span class="af-feat-meta">
<span class="af-top-badge" th:if="${feat.topmost == 1}">置顶</span>
<time class="af-item-date" th:if="${feat.publishTime != null}"
th:text="${#temporals.format(feat.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</span>
</span>
</a>
</div>
<ul class="af-block-list" th:if="${#lists.size(block.items) > 1}">
<li class="af-line" th:each="item, stat : ${block.items}" th:if="${stat.index > 0}">
<a class="af-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<span class="af-top-badge" th:if="${item.topmost == 1}">置顶</span>
<time class="af-item-date" th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'MM-dd')}">01-01</time>
</li>
</ul>
<th:block th:if="${#lists.isEmpty(block.items)}">
<div th:replace="~{'themes/agri-forest/fragments/home-blocks' :: empty(${block})}"></div>
</th:block>
</div>
</section>
<!-- 布局模式:要闻速递横条(block.items 由渲染端按置顶+最新装配;单行不换行,中文不塌列) -->
<section class="af-ticker" th:fragment="ticker(block)" aria-label="要闻速递"
th:if="${!#lists.isEmpty(block.items)}">
<div class="af-container af-ticker-inner">
<span class="af-ticker-label">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 10v4l6 1 9 5V4L9 9l-6 1z"/><path d="M7 15l1.5 5h3L10 16"/></svg>
<span th:text="${block.title}">要闻速递</span>
</span>
<ul class="af-ticker-list">
<li th:each="item : ${block.items}">
<a class="af-ticker-item" th:href="${url.article(item.id)}" th:text="${item.title}">要闻标题</a>
</li>
</ul>
</div>
</section>
<!-- 布局模式:校园映像图廊(block.items = 轮播位 gallery,四列图片墙 + 悬浮标题) -->
<section class="af-section af-container af-gallery" th:fragment="gallery(block)" aria-label="校园映像"
th:if="${!#lists.isEmpty(block.items)}">
<div class="af-gallery-head">
<div class="af-block-title af-gallery-title">
<span class="af-block-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><rect x="3" y="5" width="18" height="14" rx="1"/><circle cx="8.5" cy="10" r="1.5"/><path d="M21 15.5l-4.5-4.5-9 8"/></svg>
</span>
<h2 th:text="${block.title}">校园映像</h2>
</div>
<p class="af-gallery-sub" th:if="${!#strings.isEmpty(block.subtitle)}" th:text="${block.subtitle}">
Campus Gallery
</p>
</div>
<ul class="af-wall af-wall--4">
<li class="af-wall-item" th:each="g : ${block.items}">
<a th:href="${g.linkUrl}" th:title="${g.title}">
<img th:src="${g.imageUrl}" th:alt="${g.title}" loading="lazy">
<span class="af-wall-cap" th:if="${!#strings.isEmpty(g.title)}" th:text="${g.title}">作品标题</span>
</a>
</li>
</ul>
</section>
<!-- 布局模式:专题网站缩略卡(block.linkGroups 由渲染端按 group 过滤;4 列卡片含标题条) -->
<section class="af-section af-container" th:fragment="links(block)" aria-label="专题网站">
<div class="af-block-head">
<div class="af-block-title">
<span class="af-block-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
</span>
<h2 th:text="${block.title}">专题网站</h2>
</div>
</div>
<ul class="af-topic-grid">
<th:block th:each="grp : ${block.linkGroups}">
<li class="af-topic" th:each="l : ${grp.links}">
<a th:href="${l.linkUrl}" target="_blank" rel="noopener" th:title="${l.name}">
<span class="af-topic-bar" aria-hidden="true"></span>
<span class="af-topic-glyph" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c3 3.5 3 14 0 18M12 3c-3 3.5-3 14 0 18"/></svg>
</span>
<span class="af-topic-name" th:text="${l.name}">专题名称</span>
</a>
</li>
</th:block>
</ul>
</section>
<!-- 空态(各形态引用时守卫必须包在 th:block 外层:th:replace 优先级 100 先于 th:if 300 求值) -->
<div class="af-empty" th:fragment="empty(block)">
<p>栏目暂无内容</p>
<p class="af-empty-hint">内容发布后将在此展示</p>
</div>
</html>
@@ -1,133 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<!-- 事项指南页(事项库 §6.2 契约):item / attachments[] / relatedItems[] /
faqCategoryUrl·noticeCategoryUrl·ruleCategoryUrl(可空)/ applyUrl(可空,空则不渲染"我要办理")。
三个富文本字段已在保存期过 jsoup 白名单、渲染期补 URL 基准,此处允许 utext。
svcUnavailable=true(P1-4 停机降级):只渲染骨架+空态,不渲染事项内容与"我要办理" -->
<main class="af-page af-container" id="main-content">
<div class="af-page-card af-svc-guide">
<nav class="af-crumb" aria-label="面包屑">
<a th:href="${url.home()}">首页</a>
<span class="af-crumb-sep" aria-hidden="true">/</span>
<a th:href="${url.serviceList()}">服务中心</a>
<span class="af-crumb-sep" aria-hidden="true">/</span>
<span class="af-crumb-current" th:text="${svcUnavailable} ? '服务中心' : ${item.title}">事项名称</span>
</nav>
<!-- P1-4 停机空态:svc-hall 不可达时降级渲染(200),不出 500 错误页 -->
<div class="af-empty af-svc-unavailable" th:if="${svcUnavailable}">
<p>事项服务暂时不可用,请稍后再试</p>
<p class="af-empty-hint">事项数据服务暂不可达,稍后刷新即可恢复</p>
</div>
<article class="af-svc-guide-body" th:if="${!svcUnavailable}">
<h1 class="af-article-title" th:text="${item.title}">事项名称</h1>
<div class="af-article-meta">
<span th:if="${!#strings.isEmpty(item.deptName)}"
th:text="${'办理部门:' + item.deptName}">办理部门</span>
<span th:text="${item.handleMode == 1 ? '线上办理' : (item.handleMode == 2 ? '线下办理' : '线上线下均可办理')}">办理方式</span>
<time th:if="${item.publishTime != null}"
th:text="${'发布时间:' + #temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
<span th:text="${'浏览量:' + item.viewCount}">浏览量</span>
</div>
<!-- 我要办理:三态(手填外链 > svc_hall_base 生成 > 不渲染),按钮外链新窗口打开 -->
<div class="af-svc-apply" th:if="${applyUrl != null}">
<a class="af-svc-apply-btn" th:href="${applyUrl}" target="_blank" rel="noopener">我要办理</a>
<span class="af-svc-apply-hint">进入服务大厅在线办理</span>
</div>
<p class="af-svc-summary" th:if="${!#strings.isEmpty(item.summary)}" th:text="${item.summary}">简介</p>
<table class="af-svc-info">
<caption class="af-skip-link">事项基本信息</caption>
<tbody>
<tr th:if="${!#strings.isEmpty(item.objectName)}">
<th scope="row">服务对象</th>
<td th:text="${item.objectName}">服务对象</td>
</tr>
<tr th:if="${!#strings.isEmpty(item.categoryName)}">
<th scope="row">事项类别</th>
<td th:text="${item.categoryName}">事项类别</td>
</tr>
<tr th:if="${!#strings.isEmpty(item.handlePlace)}">
<th scope="row">办理地点</th>
<td th:text="${item.handlePlace}">办理地点</td>
</tr>
<tr th:if="${!#strings.isEmpty(item.handleTimeLimit)}">
<th scope="row">办理时限</th>
<td th:text="${item.handleTimeLimit}">办理时限</td>
</tr>
<tr th:if="${!#strings.isEmpty(item.promiseTimeLimit)}">
<th scope="row">承诺时限</th>
<td th:text="${item.promiseTimeLimit}">承诺时限</td>
</tr>
<tr th:if="${!#strings.isEmpty(item.contactPhone)}">
<th scope="row">咨询电话</th>
<td th:text="${item.contactPhone}">咨询电话</td>
</tr>
<tr th:if="${!#strings.isEmpty(item.supervisePhone)}">
<th scope="row">监督投诉</th>
<td th:text="${item.supervisePhone}">监督投诉电话</td>
</tr>
</tbody>
</table>
<section class="af-svc-section" th:if="${!#strings.isEmpty(item.processHtml)}">
<h2>办理流程</h2>
<div class="af-article-content" th:utext="${item.processHtml}">办理流程</div>
</section>
<section class="af-svc-section" th:if="${!#strings.isEmpty(item.materialHtml)}">
<h2>所需材料</h2>
<div class="af-article-content" th:utext="${item.materialHtml}">所需材料</div>
</section>
<section class="af-svc-section" th:if="${!#strings.isEmpty(item.noticeHtml)}">
<h2>注意事项</h2>
<div class="af-article-content" th:utext="${item.noticeHtml}">注意事项</div>
</section>
<section class="af-svc-section af-svc-attachments" th:if="${!#lists.isEmpty(attachments)}">
<h2>相关材料下载</h2>
<ul>
<li th:each="att : ${attachments}">
<a th:href="${url.upload(att.webUrl)}" th:download
th:text="${att.fileName}">材料文件</a>
<span class="af-svc-att-size" th:if="${att.fileSize != null}"
th:text="${#numbers.formatDecimal(att.fileSize / 1024.0, 1, 1) + ' KB'}">12.3 KB</span>
</li>
</ul>
</section>
<nav class="af-svc-links" th:if="${faqCategoryUrl != null or noticeCategoryUrl != null or ruleCategoryUrl != null}">
<a th:if="${faqCategoryUrl != null}" th:href="${faqCategoryUrl}">常见问题</a>
<a th:if="${noticeCategoryUrl != null}" th:href="${noticeCategoryUrl}">公告信息</a>
<a th:if="${ruleCategoryUrl != null}" th:href="${ruleCategoryUrl}">文件规定</a>
</nav>
</article>
<aside class="af-svc-related" th:if="${!svcUnavailable and !#lists.isEmpty(relatedItems)}">
<h2 class="af-section-title">相关服务</h2>
<ul class="af-svc-related-list">
<li th:each="rel : ${relatedItems}">
<a th:href="${rel.url}">
<span class="af-svc-related-name" th:text="${rel.title}">相关事项</span>
<span class="af-svc-related-meta">
<span th:if="${!#strings.isEmpty(rel.deptName)}" th:text="${rel.deptName}">部门</span>
<span class="af-svc-tag"
th:text="${rel.handleMode == 1 ? '线上' : (rel.handleMode == 2 ? '线下' : '线上线下')}">方式</span>
</span>
</a>
</li>
</ul>
</aside>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,169 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="af-home" id="main-content">
<!-- 首页轮播位(cms_banner position=home;设计尺寸 1920×560,见 theme.json bannerSize):
干净切图 + 左下墨绿标题条 -->
<section class="af-banner" th:if="${!#lists.isEmpty(banners)}" aria-label="焦点图片">
<div class="af-banner-slides">
<a class="af-banner-slide" th:each="b, stat : ${banners}"
th:classappend="${stat.first} ? 'active'"
th:href="${b.linkUrl}" th:title="${b.title}">
<img th:src="${b.imageUrl}" th:alt="${b.title}">
<span class="af-banner-title" th:text="${b.title}">标题</span>
</a>
</div>
<button class="af-banner-arrow prev" type="button" aria-label="上一张" th:if="${#lists.size(banners) > 1}">‹</button>
<button class="af-banner-arrow next" type="button" aria-label="下一张" th:if="${#lists.size(banners) > 1}">›</button>
<div class="af-banner-dots" th:if="${#lists.size(banners) > 1}">
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
th:aria-label="${'第' + (stat.count) + '张'}"></button>
</div>
</section>
<!-- 首页布局模式(docs/16 §4.4):区块形态由本主题 fragments/home-blocks 承载——换肤必须连首页正文一起换,
只换页头页脚会被判「皮肤不生效」。未知类型仍回落 common 骨架(渲染端已过滤未知 type,正常不触发)。
注意:th:replace(优先级100)先于 th:case(300)执行——case 必须放父元素、replace 放子元素 -->
<th:block th:if="${layoutBlocks != null}">
<th:block th:each="block : ${layoutBlocks}">
<th:block th:switch="${block.type}">
<th:block th:case="'article_list'">
<th:block th:switch="${block.homeStyle}">
<th:block th:case="'news_grid'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: news_grid(${block})}" /></th:block>
<th:block th:case="'media_focus'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: media_focus(${block})}" /></th:block>
<th:block th:case="'compact'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: compact(${block})}" /></th:block>
<th:block th:case="'image_wall'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: image_wall(${block})}" /></th:block>
<th:block th:case="*"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: card(${block})}" /></th:block>
</th:block>
</th:block>
<th:block th:case="'ticker'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: ticker(${block})}" /></th:block>
<th:block th:case="'gallery'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: gallery(${block})}" /></th:block>
<th:block th:case="'links'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: links(${block})}" /></th:block>
<th:block th:case="*"><th:block th:replace="~{'themes/common/fragments/home-blocks' :: render(${block})}" /></th:block>
</th:block>
</th:block>
</th:block>
<!-- 回落分支(docs/16 D2):无 home_layout 时按现状渲染 -->
<th:block th:unless="${layoutBlocks != null}">
<!-- 三图焦点:图片新闻(focusArticles 必有封面)横排 3 卡 -->
<section class="af-section af-container" aria-label="图片新闻" th:if="${!#lists.isEmpty(focusArticles)}">
<div class="af-block-head">
<div class="af-block-title">
<span class="af-block-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><rect x="3" y="5" width="18" height="14" rx="1"/><circle cx="8.5" cy="10" r="1.5"/><path d="M21 15.5l-4.5-4.5-9 8"/></svg>
</span>
<h2>图片新闻</h2>
</div>
</div>
<ul class="af-trio">
<li class="af-trio-item" th:each="item : ${focusArticles}">
<a th:href="${url.article(item.id)}" th:title="${item.title}">
<span class="af-trio-cover">
<img th:src="${url.upload(item.coverImage)}" th:alt="${item.title}">
</span>
<span class="af-trio-cap">
<span class="af-trio-title" th:text="${item.title}">图片新闻标题</span>
<span class="af-trio-meta">
<time class="af-item-date" th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</span>
</span>
</a>
</li>
</ul>
</section>
<!-- 头条区:头条推荐(置顶优先)大卡 + 右侧密行 -->
<section class="af-section af-container" aria-label="头条推荐">
<div class="af-headline" th:if="${!#lists.isEmpty(topArticles)}">
<div class="af-block">
<div class="af-block-head">
<div class="af-block-title">
<span class="af-block-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M4 5h11a1 1 0 0 1 1 1v13H6a2 2 0 0 1-2-2V5z"/><path d="M16 9h3a1 1 0 0 1 1 1v7a2 2 0 0 1-2 2"/><path d="M7 9h6M7 13h6M7 17h4"/></svg>
</span>
<h2>头条推荐</h2>
</div>
</div>
<div class="af-headline-body"
th:with="rest=${#lists.size(topArticles) > 1 ? topArticles.subList(1, topArticles.size()) : null}">
<div class="af-headline-lead" th:with="lead=${topArticles[0]}">
<a class="af-lead-title" th:href="${url.article(lead.id)}" th:text="${lead.title}">头条标题</a>
<div class="af-lead-meta">
<span class="af-top-badge" th:if="${lead.topmost == 1}">置顶</span>
<time class="af-item-date" th:if="${lead.publishTime != null}"
th:text="${#temporals.format(lead.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</div>
</div>
<ul class="af-block-list" th:if="${rest != null}">
<li class="af-line" th:each="item : ${rest}">
<a class="af-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<span class="af-top-badge" th:if="${item.topmost == 1}">置顶</span>
<time class="af-item-date" th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'MM-dd')}">01-01</time>
</li>
</ul>
</div>
</div>
</div>
</section>
<!-- 一级栏目区块(聚合列表型子孙栏目 top 8,D1-2):通知公告形态(46px 日期方块) -->
<section class="af-section af-container" aria-label="栏目区块">
<div class="af-blocks">
<div class="af-block" th:each="block : ${categoryBlocks}">
<div class="af-block-head">
<div class="af-block-title">
<span class="af-block-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M6 3h8l4 4v14H6V3z"/><path d="M14 3v4h4M9 12h6M9 16h6"/></svg>
</span>
<h2 th:text="${block.category.name}">栏目名</h2>
</div>
<a class="af-more" th:href="${block.moreUrl}">更多 &gt;</a>
</div>
<ul class="af-block-list af-notice-list" th:if="${!#lists.isEmpty(block.items)}">
<!-- 区块头条:有封面则图文,无封面则带摘要纯文 -->
<li class="af-feat-row" th:with="feat=${block.items[0]}">
<a th:href="${url.article(feat.id)}">
<span class="af-feat-cover" th:if="${feat.coverImage != null}">
<img th:src="${url.upload(feat.coverImage)}" th:alt="${feat.title}">
</span>
<span class="af-feat-body">
<span class="af-feat-title" th:text="${feat.title}">头条标题</span>
<span class="af-feat-summary" th:if="${!#strings.isEmpty(feat.summary)}"
th:text="${feat.summary}">摘要</span>
<span class="af-feat-meta">
<span class="af-top-badge" th:if="${feat.topmost == 1}">置顶</span>
<time class="af-item-date" th:if="${feat.publishTime != null}"
th:text="${#temporals.format(feat.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</span>
</span>
</a>
</li>
<li class="af-line af-line--date" th:each="item, stat : ${block.items}" th:if="${stat.index > 0}">
<span class="af-date-chip" th:if="${item.publishTime != null}" th:with="d=${item.publishTime}">
<b th:text="${#temporals.format(d, 'dd')}">01</b>
<i th:text="${#temporals.format(d, 'yyyy-MM')}">2026-01</i>
</span>
<a class="af-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<span class="af-top-badge" th:if="${item.topmost == 1}">置顶</span>
</li>
</ul>
<div class="af-empty" th:if="${#lists.isEmpty(block.items)}">
<p>栏目暂无内容</p>
<p class="af-empty-hint">内容发布后将在此展示</p>
</div>
</div>
</div>
</section>
</th:block>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,111 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<!-- 列表页(含搜索结果页两用):themeParams['listLayout'] 切换形态——
date=46px 日期方块行(默认,农林官网典型)/ plain=通栏行式(隐藏方块、日期右置) -->
<main class="af-page af-container" id="main-content"
th:classappend="${themeParams['listLayout'] == 'plain'} ? 'af-page--plainlist'">
<div class="af-cols">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="af-side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="af-side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="af-side-nav-list">
<li class="af-side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="af-page-card">
<nav class="af-crumb" aria-label="面包屑" th:if="${searchMode != true}">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node, st : ${categoryPath}">
<span class="af-crumb-sep" aria-hidden="true">/</span>
<a th:if="${!st.last}" class="af-crumb-parent" th:href="${url.category(node.id)}"
th:text="${node.name}">父栏目</a>
<span class="af-crumb-current" th:if="${st.last}" th:text="${node.name}">栏目</span>
</th:block>
</nav>
<nav class="af-crumb" aria-label="面包屑" th:if="${searchMode == true}">
<a th:href="${url.home()}">首页</a>
<span class="af-crumb-sep" aria-hidden="true">/</span>
<span class="af-crumb-current">搜索结果</span>
</nav>
<h1 class="af-page-title" th:if="${searchMode != true}" th:text="${category.name}">栏目名</h1>
<h1 class="af-page-title" th:if="${searchMode == true}" th:text="'搜索:' + ${q}">搜索词</h1>
<ul class="af-article-list">
<!-- 有封面:图文行;无封面:日期方块行(plain 形态下自动切通栏行式),不造占位图 -->
<li th:each="item : ${listData.items}">
<div class="af-article-row" th:if="${item.coverImage != null}">
<a class="af-item-cover" th:href="${url.article(item.id)}">
<img th:src="${url.upload(item.coverImage)}" th:alt="${item.title}">
</a>
<div class="af-item-body">
<div class="af-item-title">
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
</div>
<p class="af-item-summary" th:if="${!#strings.isEmpty(item.summary)}"
th:text="${item.summary}">摘要</p>
<div class="af-item-meta">
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
<span class="af-top-badge" th:if="${item.topmost == 1}">置顶</span>
</div>
</div>
</div>
<div class="af-article-line" th:if="${item.coverImage == null}">
<span class="af-date-chip" th:if="${item.publishTime != null}" th:with="d=${item.publishTime}">
<b th:text="${#temporals.format(d, 'dd')}">01</b>
<i th:text="${#temporals.format(d, 'yyyy-MM')}">2026-01</i>
</span>
<div class="af-line-main">
<a class="af-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<p class="af-item-summary" th:if="${!#strings.isEmpty(item.summary)}"
th:text="${item.summary}">摘要</p>
</div>
<div class="af-line-side">
<span class="af-top-badge" th:if="${item.topmost == 1}">置顶</span>
<time class="af-line-date" th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</div>
</div>
</li>
</ul>
<div class="af-empty" th:if="${#lists.isEmpty(listData.items)}">
<p th:if="${searchMode == true}">未找到相关内容</p>
<p th:if="${searchMode != true}">栏目暂无内容</p>
<p class="af-empty-hint" th:if="${searchMode == true}">换个关键词试试</p>
<p class="af-empty-hint" th:if="${searchMode != true}">内容发布后将在此展示</p>
</div>
<!-- 分页(列表页/搜索页共用) -->
<nav class="af-pagination" th:if="${listData.totalPages > 1}"
th:with="cur=${listData.page}, total=${listData.totalPages}" aria-label="分页">
<a th:classappend="${cur <= 1} ? 'disabled'"
th:href="${searchMode == true} ? ${url.search(q, cur - 1)} : ${url.category(listData.categoryId) + '?page=' + (cur - 1)}"
th:aria-disabled="${cur <= 1}">上一页</a>
<a th:each="p : ${#numbers.sequence(1, total)}"
th:classappend="${p == cur} ? 'active'"
th:href="${searchMode == true} ? ${url.search(q, p)} : ${url.category(listData.categoryId) + '?page=' + p}"
th:aria-label="${'第' + p + '页'}"
th:text="${p}">1</a>
<a th:classappend="${cur >= total} ? 'disabled'"
th:href="${searchMode == true} ? ${url.search(q, cur + 1)} : ${url.category(listData.categoryId) + '?page=' + (cur + 1)}"
th:aria-disabled="${cur >= total}">下一页</a>
</nav>
</div>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,111 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<!-- 服务中心(事项库 §6.1 契约):serviceList / serviceFilters / activeFilters / q;
筛选与分页 URL 全部后端预生成(facets[].url / pageUrls[].url),模板禁止手拼 query -->
<main class="af-page af-container" id="main-content">
<div class="af-page-card af-svc-page">
<nav class="af-crumb" aria-label="面包屑">
<a th:href="${url.home()}">首页</a>
<span class="af-crumb-sep" aria-hidden="true">/</span>
<span class="af-crumb-current">服务中心</span>
</nav>
<h1 class="af-page-title">服务中心</h1>
<!-- P1-4 停机空态:svc-hall 不可达时降级渲染(200),不出 500 错误页 -->
<div class="af-empty af-svc-unavailable" th:if="${svcUnavailable}">
<p>事项服务暂时不可用,请稍后再试</p>
<p class="af-empty-hint">事项数据服务暂不可达,稍后刷新即可恢复</p>
</div>
<div class="af-svc-filters"
th:if="${!svcUnavailable and (!#lists.isEmpty(serviceFilters.objects) or !#lists.isEmpty(serviceFilters.categories) or !#lists.isEmpty(serviceFilters.depts))}">
<div class="af-svc-filter-group" th:if="${!#lists.isEmpty(serviceFilters.objects)}">
<span class="af-svc-filter-label">服务对象</span>
<div class="af-svc-filter-options">
<a class="af-svc-chip" th:each="opt : ${serviceFilters.objects}"
th:classappend="${opt.active} ? 'active'" th:href="${opt.url}">
<span th:text="${opt.name}">对象</span><em th:text="${opt.count}">0</em>
</a>
</div>
</div>
<div class="af-svc-filter-group" th:if="${!#lists.isEmpty(serviceFilters.categories)}">
<span class="af-svc-filter-label">事项类别</span>
<div class="af-svc-filter-options">
<a class="af-svc-chip" th:each="opt : ${serviceFilters.categories}"
th:classappend="${opt.active} ? 'active'" th:href="${opt.url}">
<span th:text="${opt.name}">类别</span><em th:text="${opt.count}">0</em>
</a>
</div>
</div>
<div class="af-svc-filter-group" th:if="${!#lists.isEmpty(serviceFilters.depts)}">
<span class="af-svc-filter-label">服务部门</span>
<div class="af-svc-filter-options">
<a class="af-svc-chip" th:each="opt : ${serviceFilters.depts}"
th:classappend="${opt.active} ? 'active'" th:href="${opt.url}">
<span th:text="${opt.name}">部门</span><em th:text="${opt.count}">0</em>
</a>
</div>
</div>
</div>
<div class="af-svc-toolbar" th:if="${!svcUnavailable}">
<form class="af-svc-search" th:action="${url.serviceList()}" method="get" role="search">
<input type="search" name="q" th:value="${q}" placeholder="搜索事项名称 / 简介" maxlength="64">
<button type="submit">搜索</button>
</form>
<a class="af-svc-clear" th:if="${activeFilters.hasAny}" th:href="${activeFilters.clearUrl}">清除筛选</a>
</div>
<ul class="af-svc-list" th:if="${!svcUnavailable}">
<li th:each="it : ${serviceList.items}">
<a class="af-svc-item" th:href="${url.serviceItem(it.id)}">
<span class="af-svc-item-cover" th:if="${it.coverImage != null}">
<img th:src="${url.upload(it.coverImage)}" th:alt="${it.title}">
</span>
<span class="af-svc-item-body">
<span class="af-svc-item-title">
<span class="af-svc-item-name" th:text="${it.title}">事项名称</span>
<span class="af-svc-tag online" th:if="${it.onlineFlag == 1}">可在线办理</span>
<span class="af-svc-tag"
th:text="${it.handleMode == 1 ? '线上办理' : (it.handleMode == 2 ? '线下办理' : '线上线下')}">办理方式</span>
</span>
<span class="af-svc-item-summary" th:if="${!#strings.isEmpty(it.summary)}"
th:text="${it.summary}">简介</span>
<span class="af-svc-item-meta">
<span th:if="${!#strings.isEmpty(it.deptName)}" th:text="${it.deptName}">部门</span>
<span th:if="${!#strings.isEmpty(it.categoryName)}" th:text="${it.categoryName}">类别</span>
<time th:if="${it.publishTime != null}"
th:text="${#temporals.format(it.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</span>
</span>
</a>
</li>
</ul>
<div class="af-empty" th:if="${!svcUnavailable and #lists.isEmpty(serviceList.items)}">
<p th:if="${activeFilters.hasAny}">未找到符合条件的服务事项</p>
<p th:if="${!activeFilters.hasAny}">服务中心暂未上架事项</p>
<p class="af-empty-hint" th:if="${activeFilters.hasAny}">换个筛选条件或关键词试试</p>
<p class="af-empty-hint" th:if="${!activeFilters.hasAny}">事项发布后将在此展示</p>
</div>
<!-- 分页:URL 由后端预生成(携带当前筛选,pageUrls[].active 标记当前页);停机态 totalPages=0 自然隐藏 -->
<nav class="af-pagination" th:if="${!svcUnavailable and serviceList.totalPages > 1}" aria-label="分页">
<a th:each="p : ${serviceList.pageUrls}"
th:classappend="${p.active} ? 'active'"
th:href="${p.url}"
th:aria-label="${'第' + p.page + '页'}"
th:aria-current="${p.active} ? 'page'"
th:text="${p.page}">1</a>
</nav>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,50 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="af-page af-container" id="main-content">
<div class="af-cols">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="af-side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="af-side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="af-side-nav-list">
<li class="af-side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="af-page-card">
<nav class="af-crumb" aria-label="面包屑">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node : ${categoryPath}">
<span class="af-crumb-sep" aria-hidden="true">/</span>
<a class="af-crumb-parent" th:href="${url.category(node.id)}" th:text="${node.name}">栏目</a>
</th:block>
<span class="af-crumb-sep" th:if="${currentCategoryId != null}" aria-hidden="true">/</span>
<span class="af-crumb-current">正文</span>
</nav>
<article class="af-article">
<h1 class="af-article-title" th:text="${category.name}">栏目名</h1>
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
<div class="af-article-content" th:if="${!#strings.isEmpty(category.content)}"
th:utext="${category.content}">单页内容</div>
<div class="af-empty" th:if="${#strings.isEmpty(category.content)}">
<p>页面暂无内容</p>
<p class="af-empty-hint">内容编辑后将在此展示</p>
</div>
</article>
</div>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,30 +0,0 @@
{
"specVersion": 1,
"name": "agri-forest",
"title": "农林绿(agri-forest)",
"version": "1.0.0",
"author": "yunhai",
"description": "墨绿+麦穗金的农林院校风(设计语言参考信阳农林学院官网)。",
"screenshot": "assets/screenshot.png",
"bannerSize": "1920x560",
"params": [
{ "key": "primary", "cssVar": "--af-primary", "label": "主色(墨绿)", "type": "color", "default": "#1C6E19" },
{ "key": "primaryDark", "cssVar": "--af-primary-dark", "label": "主色加深", "type": "color", "default": "#14510F" },
{ "key": "primaryLight", "cssVar": "--af-primary-light","label": "主色提亮", "type": "color", "default": "#3FA43A" },
{ "key": "accent", "cssVar": "--af-accent", "label": "麦穗金", "type": "color", "default": "#C8A45C" },
{ "key": "navBg", "cssVar": "--af-nav-bg", "label": "主导航底色", "type": "color", "default": "#1C6E19" },
{ "key": "footerBg", "cssVar": "--af-footer-bg", "label": "页脚底色", "type": "color", "default": "#14510F" },
{ "key": "text", "cssVar": "--af-text", "label": "正文文字色", "type": "color", "default": "#2A3327" },
{ "key": "textLight", "cssVar": "--af-text-light", "label": "次要文字色", "type": "color", "default": "#6C7A67" },
{ "key": "bg", "cssVar": "--af-bg", "label": "页面底色", "type": "color", "default": "#F5F8F3" },
{ "key": "cardBg", "cssVar": "--af-card-bg", "label": "卡片底色", "type": "color", "default": "#FFFFFF" },
{ "key": "border", "cssVar": "--af-border", "label": "边框色", "type": "color", "default": "#DDE8DA" },
{ "key": "linkGreen", "cssVar": "--af-link", "label": "链接绿", "type": "color", "default": "#277428" },
{ "key": "listLayout", "label": "列表页形态", "type": "select", "default": "date",
"options": [
{ "value": "date", "label": "日期方块" },
{ "value": "plain", "label": "通栏行式" }
]
}
]
}
@@ -1,23 +0,0 @@
# blue-white 皮肤包
由 default 派生的演示皮肤(方案 §7)。**升级随版本发布,不在站点间复制目录**。
## 数据契约(模板可取用的 model 属性全集,§4.3)
| 页面 | 属性 |
| --- | --- |
| 全部 | `site`(快照:config/分类树/轮播/友链组/院系子站)、`url`(PortalUrlHelper:home/category/article/asset/search/siteHome)、`themeBase`(=`themes/blue-white`)、`navCategories`、`pageTitle`、`seoDescription`、`canonical`、`themeParams`(微调参数,仅 theme.json 声明过的 key)、`themeVarsCss`(服务端预构建 `:root{}` 覆盖串,无参数为空) |
| home | `banners`(时间窗内 home 位)、`topArticles`(置顶+最新 6)、`categoryBlocks`(LIST 型根栏目块,各 top 8) |
| list / search | `listData`(分页)、`category`(list)、`q` + `searchMode`(search) |
| detail | `article`、`prevArticle`、`nextArticle`、`currentCategoryId` |
| single | `category`(含 `content`)、`currentCategoryId` |
原则:皮肤不得假设"首页一定有轮播",对空集合优雅降级。
## 轮播图尺寸约定(bannerSize)
1920x560(管理端上传端按此引导裁剪,D2 决策点)。
## 可调参数
见 `theme.json` 的 `params`(color 进 CSS 变量;select 供模板 `th:if` 分支布局)。
@@ -1,370 +0,0 @@
/* ============================================================
* blue-white 皮肤包样式(default 派生)(方案 §8.2)
* 换肤 = 拷贝 default 目录改 CSS 变量与布局,零后端代码。
* 轮播图设计尺寸约定:1920×560(管理端上传端按此引导裁剪,D2 决策点确认)。
* ============================================================ */
:root {
--primary: #2f6bd8;
--primary-dark: #1d4fae;
--text: #333;
--text-light: #777;
--bg: #f7f9fc;
--card-bg: #fff;
--border: #e3e9f2;
--max-width: 1200px;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
color: var(--text);
background: var(--bg);
}
a { color: inherit; text-decoration: none; }
a:hover { color: var(--primary); }
img { max-width: 100%; }
time, .item-meta { color: var(--text-light); font-size: 13px; }
.empty { color: var(--text-light); text-align: center; padding: 32px 0; }
/* ===== 头部 ===== */
.site-header { background: var(--card-bg); border-bottom: 1px solid var(--border); }
.header-inner {
max-width: var(--max-width);
margin: 0 auto;
padding: 18px 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.site-brand { display: flex; align-items: center; gap: 12px; }
.site-logo { height: 44px; }
.site-name { font-size: 26px; font-weight: 700; color: var(--primary); }
.site-search { display: flex; }
.site-search input {
border: 1px solid var(--border);
border-right: none;
padding: 8px 12px;
width: 220px;
outline: none;
}
.site-search button {
border: none;
background: var(--primary);
color: #fff;
padding: 8px 18px;
cursor: pointer;
}
/* ===== 专业院系下拉(院系子站切换,《院系子站方案》) ===== */
/* 悬停展开白底纵向列表,右对齐 + 超高滚动(院系数十个子站场景) */
.site-colleges { position: relative; }
.site-colleges .colleges-toggle {
display: flex;
align-items: center;
gap: 4px;
border: 1px solid var(--border);
padding: 8px 14px;
cursor: pointer;
color: var(--primary);
font-weight: 600;
white-space: nowrap;
}
.site-colleges .colleges-arrow { font-style: normal; font-size: 12px; color: var(--text-light); }
.site-colleges .colleges-menu {
display: none;
position: absolute;
top: 100%;
right: 0;
min-width: 200px;
max-height: 420px;
overflow-y: auto;
margin: 0;
padding: 6px 0;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-top: 2px solid var(--primary);
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
z-index: 110;
}
.site-colleges:hover .colleges-menu { display: block; }
.site-colleges .colleges-menu li a {
display: block;
padding: 9px 16px;
color: var(--text);
white-space: nowrap;
}
.site-colleges .colleges-menu li a:hover { background: #f2f6fc; color: var(--primary); }
.site-colleges .colleges-menu li.active a { color: var(--primary); font-weight: 600; }
.site-nav { background: var(--primary); }
.site-nav ul {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 16px;
list-style: none;
display: flex;
flex-wrap: wrap;
}
.site-nav li a {
display: block;
padding: 12px 22px;
color: #fff;
}
.site-nav li:hover a, .site-nav li.active a { background: var(--primary-dark); }
/* ===== 二级下拉导航 ===== */
/* 面板为白底纵向列表:需整体覆盖 .site-nav ul 的横向 flex 与内边距 */
.site-nav > ul > li { position: relative; }
.site-nav li.has-sub > a::after { content: " ▾"; font-size: 12px; opacity: .85; }
.site-nav ul.sub-nav {
display: none;
position: absolute;
top: 100%;
left: 0;
min-width: 168px;
max-width: none;
margin: 0;
padding: 6px 0;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-top: 2px solid var(--primary);
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
z-index: 100;
}
.site-nav li:hover > .sub-nav { display: block; }
.site-nav .sub-nav li a {
padding: 9px 16px;
color: var(--text);
white-space: nowrap;
background: none;
}
.site-nav .sub-nav li a:hover { background: #f2f6fc; color: var(--primary); }
.site-nav .sub-nav li.active a { color: var(--primary); font-weight: 600; }
/* ===== 首页轮播 ===== */
.banner { position: relative; max-width: var(--max-width); margin: 16px auto; overflow: hidden; }
.banner-slides { position: relative; height: 420px; }
.banner-slide { display: none; position: absolute; inset: 0; }
.banner-slide.active { display: block; }
.banner-slide img { width: 100%; height: 100%; object-fit: cover; }
.banner-title {
position: absolute;
left: 0; right: 0; bottom: 0;
padding: 12px 20px;
color: #fff;
background: linear-gradient(transparent, rgba(0, 0, 0, .6));
}
.banner-dots {
position: absolute;
bottom: 12px; right: 20px;
display: flex; gap: 8px;
}
.banner-dots button {
width: 10px; height: 10px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, .6);
cursor: pointer;
}
.banner-dots button.active { background: #fff; }
/* ===== 通用区块 ===== */
.top-strip, .category-block {
max-width: var(--max-width);
margin: 16px auto;
padding: 20px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.section-title, .block-head h2 {
margin: 0 0 12px;
font-size: 20px;
border-left: 4px solid var(--primary);
padding-left: 10px;
}
.block-head { display: flex; align-items: baseline; justify-content: space-between; }
.more { font-size: 13px; color: var(--text-light); }
.top-strip ul, .block-list { list-style: none; margin: 0; padding: 0; }
.top-strip li, .block-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 9px 0;
border-bottom: 1px dashed var(--border);
}
.top-strip li:last-child, .block-list li:last-child { border-bottom: none; }
.top-strip li a, .block-list li a {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.top-badge {
flex: none;
color: #fff;
background: var(--primary);
font-size: 12px;
padding: 1px 6px;
border-radius: 2px;
}
/* ===== 列表页 ===== */
.list-main {
max-width: var(--max-width);
margin: 16px auto;
padding: 24px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.breadcrumb { color: var(--text-light); font-size: 13px; margin-bottom: 16px; }
.breadcrumb a { margin-right: 8px; }
.breadcrumb span { margin-left: 8px; }
.page-title {
font-size: 22px;
border-left: 4px solid var(--primary);
padding-left: 10px;
margin: 0 0 18px;
}
.article-list { list-style: none; margin: 0; padding: 0; }
.article-list li { padding: 14px 0; border-bottom: 1px dashed var(--border); }
.item-title { font-size: 17px; }
.item-summary {
color: var(--text-light);
font-size: 14px;
margin: 6px 0;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.item-meta { display: flex; gap: 10px; align-items: center; }
/* ===== 分页 ===== */
.pagination { display: flex; gap: 6px; justify-content: center; padding: 24px 0 4px; }
.pagination a {
min-width: 34px;
text-align: center;
padding: 6px 10px;
border: 1px solid var(--border);
background: var(--card-bg);
}
.pagination a.active { background: var(--primary); color: #fff; border-color: var(--primary); }
/* ===== 详情/单页 ===== */
.detail-main {
max-width: var(--max-width);
margin: 16px auto;
padding: 32px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.article-title { margin: 8px 0 12px; font-size: 26px; text-align: center; }
.article-meta {
display: flex;
gap: 18px;
justify-content: center;
flex-wrap: wrap;
color: var(--text-light);
font-size: 13px;
padding-bottom: 18px;
border-bottom: 1px solid var(--border);
}
.article-content { line-height: 1.9; font-size: 16px; padding-top: 20px; word-break: break-word; }
.article-content img { height: auto; }
.article-content table { border-collapse: collapse; width: 100%; }
.article-content table td, .article-content table th { border: 1px solid var(--border); padding: 6px 10px; }
.article-sibling { display: flex; flex-direction: column; gap: 8px; margin-top: 28px; font-size: 14px; }
/* ===== 页脚 ===== */
.site-footer { margin-top: 32px; background: var(--primary-dark); color: rgba(255, 255, 255, .85); }
.footer-links {
max-width: var(--max-width);
margin: 0 auto;
padding: 18px 16px 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.links-group { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; font-size: 13px; }
.group-name { font-weight: 600; margin-right: 4px; }
.footer-info {
max-width: var(--max-width);
margin: 0 auto;
padding: 12px 16px 20px;
text-align: center;
font-size: 13px;
}
.footer-info p { margin: 4px 0; }
/* ===== 移动端适配 ===== */
@media (max-width: 768px) {
.header-inner { flex-direction: column; align-items: stretch; }
.site-search input { flex: 1; width: auto; }
.site-colleges { align-self: flex-start; }
.site-colleges .colleges-menu { left: 0; right: auto; min-width: 100%; }
.banner-slides { height: 200px; }
.detail-main, .list-main { padding: 18px 14px; }
.article-title { font-size: 22px; }
/* 触屏无 hover:子菜单静态平铺(缩进列表),不做浮层 */
.site-nav ul.sub-nav {
display: block;
position: static;
border: none;
box-shadow: none;
background: transparent;
min-width: 0;
padding: 0 0 6px;
}
.site-nav .sub-nav li a { color: rgba(255, 255, 255, .9); padding: 8px 16px 8px 34px; }
.site-nav .sub-nav li a:hover { background: var(--primary-dark); color: #fff; }
}
/* ===== blue-white 主题差异层(方案 §7:差异仅限 CSS + fragments)===== */
/* 细线顶栏 + 大留白 + 卡片化列表 */
.site-header { border-bottom: 1px solid var(--border); box-shadow: none; }
.home-main, .list-main, .detail-main { padding-top: 36px; padding-bottom: 56px; }
.article-list .article-title, .block-head, .page-title { letter-spacing: .02em; }
.article-list li, .banner, .banner-title { border-radius: 4px; }
/* B4 友链 Logo:与名称并排小图(高度 ~20px,垂直居中;过宽截断不撑破页脚) */
.links-group a .link-logo,
.sp-links-row a .link-logo {
height: 20px;
width: auto;
max-width: 64px;
object-fit: contain;
vertical-align: middle;
margin-right: 4px;
border-radius: 2px;
}
/* ===== 左侧栏目导航(docs/11):有二级节点的模块,栏目/文章页左侧展示兄弟目录 ===== */
.list-main.has-sidenav, .detail-main.has-sidenav { display: flex; align-items: flex-start; gap: 24px; }
.has-sidenav .list-body { flex: 1; min-width: 0; }
.side-nav { flex: none; width: 220px; background: var(--card-bg, #fff); border: 1px solid var(--border); overflow: hidden; }
.side-nav-head { background: var(--primary); color: #fff; font-size: 20px; font-weight: 700; text-align: center; padding: 18px 12px; letter-spacing: 2px; }
.side-nav-list { list-style: none; margin: 0; padding: 0; }
.side-nav-item + .side-nav-item { border-top: 1px dashed var(--border); }
.side-nav-item a { display: block; padding: 12px 16px; color: var(--text); text-decoration: none; font-size: 15px; transition: background .15s ease, color .15s ease; }
.side-nav-item a:hover { color: var(--primary); background: var(--bg); }
.side-nav-item.active a { background: var(--primary); color: #fff; font-weight: 600; text-decoration: none; }
/* 顶导航二级下拉当前项高亮(docs/11 顺带) */
.site-nav ul.sub-nav li.active a { color: var(--primary); }
@media (max-width: 768px) {
.list-main.has-sidenav, .detail-main.has-sidenav { flex-direction: column; gap: 12px; }
.side-nav { width: 100%; }
.side-nav-list { display: flex; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.side-nav-item { flex: none; }
.side-nav-item + .side-nav-item { border-top: none; border-left: 1px dashed var(--border); }
.side-nav-item a { white-space: nowrap; }
}
@media print {
.side-nav { display: none; }
.list-main.has-sidenav, .detail-main.has-sidenav { display: block; }
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs><linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#5486df"/><stop offset="1" stop-color="#214b97"/>
</linearGradient></defs>
<rect width="64" height="64" fill="url(#bg)"/>
<circle cx="32" cy="25" r="10" fill="#ffffff"/>
<path d="M 16 49.3 Q 32 30.7 48 49.3 Z" fill="#ffffff" opacity="0.92"/>
</svg>

Before

Width:  |  Height:  |  Size: 434 B

@@ -1,40 +0,0 @@
/* default 皮肤包脚本:仅首页轮播自动播放与圆点切换,无任何外部依赖 */
(function () {
var slides = document.querySelectorAll('.banner-slide');
var dots = document.querySelectorAll('.banner-dots button');
if (!slides.length) {
return;
}
var current = 0;
var timer = null;
function show(index) {
slides[current].classList.remove('active');
if (dots[current]) {
dots[current].classList.remove('active');
}
current = (index + slides.length) % slides.length;
slides[current].classList.add('active');
if (dots[current]) {
dots[current].classList.add('active');
}
}
function start() {
if (slides.length > 1) {
timer = window.setInterval(function () {
show(current + 1);
}, 5000);
}
}
dots.forEach(function (dot, index) {
dot.addEventListener('click', function () {
window.clearInterval(timer);
show(index);
start();
});
});
start();
})();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

@@ -1,58 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main" th:classappend="${sideNav != null} ? 'has-sidenav'">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="side-nav-list">
<li class="side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="list-body">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node : ${categoryPath}">
<a class="crumb-parent" th:href="${url.category(node.id)}" th:text="${node.name}">栏目</a>
</th:block>
<span>正文</span>
</nav>
<article class="article">
<h1 class="article-title" th:text="${article.title}">文章标题</h1>
<div class="article-meta">
<time th:if="${article.publishTime != null}"
th:text="${'发布时间:' + #temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
<span th:if="${!#strings.isEmpty(article.authorName)}"
th:text="'作者:' + ${article.authorName}">作者</span>
<span th:if="${!#strings.isEmpty(article.source)}"
th:text="'来源:' + ${article.source}">来源</span>
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
</div>
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
其余字段一律 th:text 转义输出 -->
<div class="article-content" th:utext="${article.content}">正文</div>
</article>
<nav class="article-sibling">
<a th:if="${prevArticle != null}" class="prev"
th:href="${url.article(prevArticle.id)}"
th:text="'上一篇:' + ${prevArticle.title}">上一篇</a>
<a th:if="${nextArticle != null}" class="next"
th:href="${url.article(nextArticle.id)}"
th:text="'下一篇:' + ${nextArticle.title}">下一篇</a>
</nav>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共页脚:联系信息 + 友情链接(分组渲染,方案 §8.1:友链不设独立路由) -->
<footer class="site-footer" th:fragment="footer">
<div class="footer-links" th:if="${!#lists.isEmpty(site.linkGroups)}">
<div class="links-group" th:each="group : ${site.linkGroups}">
<span class="group-name" th:text="${group.groupName}">分组</span>
<a th:each="link : ${group.links}" th:href="${link.linkUrl}" target="_blank" rel="noopener">
<img th:if="${link.logoUrl != null and !#strings.isEmpty(link.logoUrl)}" class="link-logo"
th:src="${url.upload(link.logoUrl)}" th:alt="${link.name}" loading="lazy">
<span th:text="${link.name}">链接</span>
</a>
</div>
</div>
<div class="footer-info">
<p th:if="${site.config['contact'] != null}" th:text="${site.config['contact']}">联系方式</p>
<p th:if="${site.config['copyright'] != null}" th:text="${site.config['copyright']}">版权信息</p>
<p th:if="${site.config['beian'] != null}" th:text="${site.config['beian']}">备案号</p>
</div>
</footer>
</html>
@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<!-- 公共 head 片段:SEO(title/description/canonical)+ 主题资产(方案 §8.3/§8.6) -->
<head th:fragment="head">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:text="${pageTitle}">页面标题</title>
<meta name="description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}"
th:content="${seoDescription}">
<link rel="canonical" th:if="${canonical != null}" th:href="${canonical}">
<!-- og 标签基本三件(C-7,方案 §8.6 canonical/og 同基准) -->
<meta property="og:title" th:if="${pageTitle != null}" th:content="${pageTitle}">
<meta property="og:description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}" th:content="${seoDescription}">
<meta property="og:image" th:if="${ogImage != null}" th:content="${ogImage}">
<link rel="stylesheet" th:href="${url.asset('css/main.css')}">
<!-- 微调参数 CSS 变量覆盖(方案 §5.3):位于皮肤 CSS 之后层叠覆盖;值由服务端预构建并校验 -->
<style th:if="${not #strings.isEmpty(themeVarsCss)}" th:utext="${themeVarsCss}"></style>
<!-- 站点级自定义样式(B3):位于皮肤 CSS 与微调变量之后层叠覆盖;内容经服务端黑名单强校验后直出 -->
<style th:if="${not #strings.isEmpty(customCss)}" th:utext="${customCss}"></style>
<!-- favicon:管理端配置了 favicon 键用上传图,未配置输出主题内置兜底 icon(assets/favicon.png) -->
<link rel="icon" type="image/png"
th:href="${site.config['favicon'] != null ? url.upload(site.config['favicon']) : url.asset('favicon.png')}">
</head>
</html>
@@ -1,54 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共头部:站点名 + 栏目导航 + 站内搜索 + 专业院系下拉(院系子站切换,《院系子站方案》) -->
<header class="site-header" th:fragment="header">
<div class="header-inner">
<div class="site-brand">
<img th:if="${site.config['logo'] != null}" class="site-logo"
th:src="${url.upload(site.config['logo'])}" th:alt="${site.name}">
<a class="site-name" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
</div>
<form class="site-search" th:action="${url.search('', null)}" method="get">
<input type="text" name="q" placeholder="站内搜索" aria-label="站内搜索">
<button type="submit">搜索</button>
</form>
<!-- 专业院系下拉:顶层站取其子站、院系子站取兄弟站(含自身);无子站不渲染;文案经 config_json.colleges_label 覆盖 -->
<div class="site-colleges" th:if="${!site.colleges.isEmpty()}"
th:with="collegesLabel=${site.config['colleges_label'] ?: '专业院系'}">
<span class="colleges-toggle">
<a href="javascript:void(0)" th:text="${collegesLabel}">专业院系</a>
<i class="colleges-arrow">▾</i>
</span>
<ul class="colleges-menu">
<li th:each="col : ${site.colleges}" th:classappend="${col.path == site.path} ? 'active'">
<a th:href="${url.siteHome(col.path, col.publicBaseUrl)}"
th:title="${col.name}" th:text="${col.name}">学院名称</a>
</li>
</ul>
</div>
</div>
<nav class="site-nav">
<ul>
<li th:classappend="${currentCategoryId == null and navServiceActive == null} ? 'active'">
<a th:href="${url.home()}">首页</a>
</li>
<li th:each="cat : ${navCategories}"
th:classappend="${((currentCategoryId == cat.id or navTopId == cat.id) ? 'active' : '') + (!cat.children.isEmpty() ? ' has-sub' : '')}">
<!-- 外链栏目直接外跳;其余进栏目页(方案 §8.1) -->
<a th:if="${cat.type == 3}" th:href="${cat.linkUrl}" target="_blank" rel="noopener"
th:text="${cat.name}">栏目</a>
<a th:unless="${cat.type == 3}" th:href="${url.category(cat.id)}" th:text="${cat.name}">栏目</a>
<!-- 二级下拉:悬停展开(样式见 assets/css/main.css .sub-nav) -->
<ul class="sub-nav" th:if="${!cat.children.isEmpty()}">
<li th:each="sub : ${cat.children}"
th:classappend="${sub.id == currentCategoryId} ? 'active'">
<a th:if="${sub.type == 3}" th:href="${sub.linkUrl}" target="_blank" rel="noopener"
th:text="${sub.name}">子栏目</a>
<a th:unless="${sub.type == 3}" th:href="${url.category(sub.id)}" th:text="${sub.name}">子栏目</a>
</li>
</ul>
</li>
</ul>
</nav>
</header>
</html>
@@ -1,68 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="home-main">
<!-- 首页轮播位(cms_banner position=home;设计尺寸约定 1920×560,见 assets/css 注释) -->
<section class="banner" th:if="${!#lists.isEmpty(banners)}">
<div class="banner-slides">
<a class="banner-slide" th:each="b, stat : ${banners}"
th:classappend="${stat.first} ? 'active'"
th:href="${b.linkUrl}" th:title="${b.title}">
<img th:src="${b.imageUrl}" th:alt="${b.title}">
<span class="banner-title" th:text="${b.title}">标题</span>
</a>
</div>
<div class="banner-dots" th:if="${#lists.size(banners) > 1}">
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
th:aria-label="${'第' + (stat.count) + '张'}"></button>
</div>
</section>
<!-- 首页布局模式(docs/16 §4.4):home_layout 落库后按区块序列渲染(common 兜底骨架,本主题不另设形态) -->
<th:block th:if="${layoutBlocks != null}">
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: style}" />
<th:block th:each="block : ${layoutBlocks}">
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: render(${block})}" />
</th:block>
</th:block>
<!-- 回落分支(docs/16 D2):无 home_layout 时按现状渲染 -->
<th:block th:unless="${layoutBlocks != null}">
<!-- 置顶/头条区(cms_article topmost 优先) -->
<section class="top-strip" th:if="${!#lists.isEmpty(topArticles)}">
<h2 class="section-title">头条推荐</h2>
<ul>
<li th:each="item : ${topArticles}">
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
</section>
<!-- 一级栏目区块(每栏目 top 8) -->
<section class="category-block" th:each="block : ${categoryBlocks}">
<div class="block-head">
<h2 th:text="${block.category.name}">栏目名</h2>
<a class="more" th:href="${block.moreUrl}">更多 &gt;</a>
</div>
<ul class="block-list" th:if="${!#lists.isEmpty(block.items)}">
<li th:each="item : ${block.items}">
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(block.items)}">暂无内容</p>
</section>
</th:block>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,68 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="list-main" th:classappend="${sideNav != null} ? 'has-sidenav'">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="side-nav-list">
<li class="side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="list-body">
<nav class="breadcrumb" th:if="${searchMode != true}">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node, st : ${categoryPath}">
<a th:if="${!st.last}" class="crumb-parent" th:href="${url.category(node.id)}"
th:text="${node.name}">父栏目</a>
<span th:if="${st.last}" th:text="${node.name}">栏目</span>
</th:block>
</nav>
<nav class="breadcrumb" th:if="${searchMode == true}">
<a th:href="${url.home()}">首页</a>
<span>搜索结果</span>
</nav>
<h1 class="page-title" th:if="${searchMode != true}" th:text="${category.name}">栏目名</h1>
<h1 class="page-title" th:if="${searchMode == true}" th:text="'搜索:' + ${q}">搜索词</h1>
<ul class="article-list">
<li th:each="item : ${listData.items}">
<a class="item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<p class="item-summary" th:if="${!#strings.isEmpty(item.summary)}" th:text="${item.summary}">摘要</p>
<div class="item-meta">
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
</div>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(listData.items)}">暂无内容</p>
<!-- 分页(列表页/搜索页共用) -->
<nav class="pagination" th:if="${listData.totalPages > 1}"
th:with="cur=${listData.page}, total=${listData.totalPages}">
<a th:if="${cur > 1}"
th:href="${searchMode == true} ? ${url.search(q, cur - 1)} : ${url.category(listData.categoryId) + '?page=' + (cur - 1)}">上一页</a>
<a th:each="p : ${#numbers.sequence(1, total)}"
th:classappend="${p == cur} ? 'active'"
th:href="${searchMode == true} ? ${url.search(q, p)} : ${url.category(listData.categoryId) + '?page=' + p}"
th:text="${p}">1</a>
<a th:if="${cur < total}"
th:href="${searchMode == true} ? ${url.search(q, cur + 1)} : ${url.category(listData.categoryId) + '?page=' + (cur + 1)}">下一页</a>
</nav>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,41 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main" th:classappend="${sideNav != null} ? 'has-sidenav'">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="side-nav-list">
<li class="side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="list-body">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node, st : ${categoryPath}">
<a th:if="${!st.last}" class="crumb-parent" th:href="${url.category(node.id)}"
th:text="${node.name}">父栏目</a>
<span th:if="${st.last}" th:text="${node.name}">栏目</span>
</th:block>
</nav>
<article class="article">
<h1 class="article-title" th:text="${category.name}">栏目名</h1>
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
<div class="article-content" th:utext="${category.content}">单页内容</div>
<p class="empty" th:if="${#strings.isEmpty(category.content)}">暂无内容</p>
</article>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -1,16 +0,0 @@
{
"specVersion": 1,
"name": "blue-white",
"title": "蓝白简约",
"version": "1.0.0",
"author": "yunhai",
"description": "细线顶栏 + 卡片化列表 + 大留白,适合高校与职业院校(方案 §7)。",
"screenshot": "assets/screenshot.png",
"bannerSize": "1920x560",
"params": [
{ "key": "primary", "cssVar": "--primary", "label": "主色", "type": "color", "default": "#2f6bd8" },
{ "key": "primaryDark", "cssVar": "--primary-dark", "label": "主色加深", "type": "color", "default": "#1d4fae" },
{ "key": "bg", "cssVar": "--bg", "label": "页面底色", "type": "color", "default": "#f7f9fc" },
{ "key": "border", "cssVar": "--border", "label": "边框色", "type": "color", "default": "#e3e9f2" }
]
}
@@ -1,33 +0,0 @@
# campus-azure(青蓝校园)
高校官网风皮肤包:以 `scholar-blue` 为基座派生,配色与首页区块形态按师范/农林类院校官网(信阳师范学院、
信阳农林学院一类)重做。配了 `home_layout` 的站点,首页正文区块由本主题 `fragments/home-blocks.html` 渲染,
不落 `themes/common` 通用骨架(否则换肤只剩页头页脚)。
## 风格定位
- 主色青蓝 `#0d6a9e` + 加深 `#084c73` + 点缀青绿 `#2b9c8f`(列表圆点、标题线脚、速递标签走青绿)。
- 圆角放宽到 6/12px、页面底 `#eef3f6`,比 academic-navy 更松更「校园」;区块头统一走居中双线 + 右置「更多」。
- 全部 11 个颜色参数可在站点 theme_params 微调(`primary/primaryDark/primaryLight/accent/navBg/footerBg/text/textLight/bg/cardBg/border`)。
## 首页区块形态(布局模式与回落模式同构)
| homeStyle | 呈现 | 类名 |
| -- | -- | -- |
| `news_grid` | 图文要闻:全宽封面头条(底部渐变遮罩标题 + 元信息)+ 双列圆点密行;区块背景可取站点 `news_bg_image` | `cz-hero*` / `cz-block-list--two` |
| `compact` | 圆点密行:青绿圆点 + 标题 + 右对齐完整日期 | `cz-item-bullet` / `cz-item-date` |
| `media_focus` | 媒体聚焦:胶囊来源徽标 + 双列密行 | `cz-src` |
| `image_wall` | 三列大格图墙:圆角卡 + 底部遮罩标题 | `cz-wall` |
| (空/未知) | 整宽区块卡:左 2 份图文头条 + 右 3 份圆点行 | `cz-block--flow` / `cz-split` |
| `ticker` | 青蓝要闻速递横条,标签为青绿胶囊(单行不换行、超长省略,中文不塌列) | `cz-ticker*` |
| `gallery` | 线上展厅:居中双语文区头 + 四列图墙 | `cz-gallery` |
| `links` | 友情链接分组(胶囊标签行) | `cz-links` |
## 契约要点
- 结构类前缀 `cz-`,CSS 变量前缀 `--cz-`;`:root` 把 `--cz-*` 别名到皮肤契约变量(`--primary`/`--text`/`--card-bg`/`--border`…),
使 `themes/common` 兜底骨架也能跟随本皮肤着色。
- 空态文案「栏目暂无内容」只在 `block.items` 为空时出现;守卫必须写在 `th:block` 外层——
`th:replace`(优先级 100)先于 `th:if`(300)求值,写在同一标签上等于没写。
- 回归防线:`AcademicSkinContractTest`(自带区块 fragment + 中文不塌列 + 契约变量别名 + 主色必须学术蓝)、
`PortalHomeLayoutIT#campusAzure_fallbackAndAssets`(回落分支 + 资产通道)、`ThemeContractIT`(7 条路由)。
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
<rect width="96" height="96" rx="20" fill="#0a5aa8"/>
<path d="M48 16l8.6 17.5 19.3 2.8-14 13.6 3.3 19.2L48 60.1 30.8 69.1l3.3-19.2-14-13.6 19.3-2.8L48 16z" fill="#ffffff"/>
</svg>

Before

Width:  |  Height:  |  Size: 246 B

@@ -1,190 +0,0 @@
/* campus-azure 主题脚本:顶栏日期 + 轮播(圆点/箭头/触摸) + 导航吸顶反蓝 + 移动端抽屉与二级手风琴。
与 default 同源逻辑,无任何外部依赖。 */
(function () {
'use strict';
/* ---------- 顶栏日期(本地时间,YYYY年M月D日 星期X) ---------- */
var dateEl = document.getElementById('siteDate');
if (dateEl) {
var now = new Date();
var week = ['日', '一', '二', '三', '四', '五', '六'][now.getDay()];
dateEl.textContent = now.getFullYear() + '年' + (now.getMonth() + 1) + '月'
+ now.getDate() + '日 星期' + week;
}
/* ---------- 导航吸顶反蓝(桌面端;≤768 不吸顶) ---------- */
var nav = document.getElementById('czNav');
var DESKTOP_MIN = 769;
var navBaseTop = 0;
var holder = null;
function measureNavTop() {
unlockNav();
navBaseTop = nav.getBoundingClientRect().top + window.pageYOffset;
}
function lockNav() {
if (!nav.classList.contains('cz-nav--fixed')) {
holder = document.createElement('div');
holder.className = 'cz-nav-holder';
holder.style.height = nav.offsetHeight + 'px';
nav.parentNode.insertBefore(holder, nav);
nav.classList.add('cz-nav--fixed');
} else if (holder) {
holder.style.height = nav.offsetHeight + 'px';
}
}
function unlockNav() {
nav.classList.remove('cz-nav--fixed');
if (holder && holder.parentNode) {
holder.parentNode.removeChild(holder);
}
holder = null;
}
function onScrollNav() {
if (window.innerWidth < DESKTOP_MIN) {
unlockNav();
return;
}
if (window.pageYOffset > navBaseTop) {
lockNav();
} else {
unlockNav();
}
}
if (nav) {
measureNavTop();
window.addEventListener('scroll', onScrollNav, { passive: true });
var resizeTimer = null;
window.addEventListener('resize', function () {
if (resizeTimer) { window.clearTimeout(resizeTimer); }
resizeTimer = window.setTimeout(function () {
measureNavTop();
onScrollNav();
}, 150);
});
}
/* ---------- 移动端抽屉 + 二级手风琴 ---------- */
var navToggle = document.querySelector('.cz-nav-toggle');
var navList = document.querySelector('.cz-nav-list');
if (navToggle && navList) {
navToggle.addEventListener('click', function () {
var open = navList.classList.toggle('cz-open');
navToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
});
}
/* 窄屏下:点击带二级的项先展开面板,再点才跳转;宽屏交还给 CSS hover */
if (navList) {
navList.addEventListener('click', function (e) {
if (window.innerWidth >= DESKTOP_MIN) { return; }
var link = e.target.closest ? e.target.closest('a') : null;
if (!link) { return; }
var li = link.parentElement;
if (!li || !li.classList.contains('has-sub')) { return; }
var sub = li.querySelector('.cz-sub');
if (!sub) { return; }
if (!li.classList.contains('cz-sub-open')) {
e.preventDefault();
li.classList.add('cz-sub-open');
}
});
}
/* ---------- 首页轮播 ---------- */
var root = document.querySelector('.cz-banner');
var slides = document.querySelectorAll('.cz-banner-slide');
var dots = document.querySelectorAll('.cz-banner-dots button');
var prevBtn = document.querySelector('.cz-banner-arrow.prev');
var nextBtn = document.querySelector('.cz-banner-arrow.next');
if (!slides.length || !root) {
return;
}
var current = 0;
var timer = null;
var INTERVAL = 5000;
function show(index) {
slides[current].classList.remove('active');
if (dots[current]) {
dots[current].classList.remove('active');
}
current = (index + slides.length) % slides.length;
slides[current].classList.add('active');
if (dots[current]) {
dots[current].classList.add('active');
}
}
function start() {
stop();
if (slides.length > 1) {
timer = window.setInterval(function () {
show(current + 1);
}, INTERVAL);
}
}
function stop() {
if (timer) {
window.clearInterval(timer);
timer = null;
}
}
dots.forEach(function (dot, index) {
dot.addEventListener('click', function () {
show(index);
start();
});
});
if (prevBtn) {
prevBtn.addEventListener('click', function () {
show(current - 1);
start();
});
}
if (nextBtn) {
nextBtn.addEventListener('click', function () {
show(current + 1);
start();
});
}
root.addEventListener('mouseenter', stop);
root.addEventListener('mouseleave', start);
root.addEventListener('focusin', stop);
root.addEventListener('focusout', start);
root.setAttribute('tabindex', '-1');
root.addEventListener('keydown', function (e) {
if (e.key === 'ArrowLeft') {
show(current - 1);
start();
} else if (e.key === 'ArrowRight') {
show(current + 1);
start();
}
});
/* 触摸滑动切页 */
var touchX = null;
root.addEventListener('touchstart', function (e) {
touchX = e.changedTouches[0].clientX;
stop();
}, { passive: true });
root.addEventListener('touchend', function (e) {
if (touchX === null) { return; }
var dx = e.changedTouches[0].clientX - touchX;
if (Math.abs(dx) > 40) {
show(dx < 0 ? current + 1 : current - 1);
}
touchX = null;
start();
}, { passive: true });
start();
})();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

@@ -1,76 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="cz-page cz-container" id="main-content">
<div class="cz-cols">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="cz-side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="cz-side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="cz-side-nav-list">
<li class="cz-side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="cz-page-card">
<nav class="cz-crumb" aria-label="面包屑">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node : ${categoryPath}">
<span class="cz-crumb-sep" aria-hidden="true">/</span>
<a class="crumb-parent" th:href="${url.category(node.id)}" th:text="${node.name}">栏目</a>
</th:block>
<span class="cz-crumb-sep" th:if="${currentCategoryId != null}" aria-hidden="true">/</span>
<span class="cz-crumb-current">正文</span>
</nav>
<article class="cz-article">
<h1 class="cz-article-title" th:text="${article.title}">文章标题</h1>
<div class="cz-article-meta">
<time th:if="${article.publishTime != null}"
th:text="${#temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
<span th:if="${!#strings.isEmpty(article.authorName)}"
th:text="'作者:' + ${article.authorName}">作者</span>
<span th:if="${!#strings.isEmpty(article.source)}"
th:text="'来源:' + ${article.source}">来源</span>
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
</div>
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
其余字段一律 th:text 转义输出 -->
<div class="cz-article-content" th:utext="${article.content}">正文</div>
</article>
<nav class="cz-sibling" aria-label="上下篇导航">
<a th:if="${prevArticle != null}" class="cz-sib prev"
th:href="${url.article(prevArticle.id)}">
<span class="cz-sib-label">上一篇</span>
<span class="cz-sib-title" th:text="${prevArticle.title}">上一篇标题</span>
</a>
<span class="cz-sib-none prev" th:if="${prevArticle == null}">
<span class="cz-sib-label">上一篇</span>
<span class="cz-sib-title">已经是最早一篇</span>
</span>
<a th:if="${nextArticle != null}" class="cz-sib next"
th:href="${url.article(nextArticle.id)}">
<span class="cz-sib-label">下一篇</span>
<span class="cz-sib-title" th:text="${nextArticle.title}">下一篇标题</span>
</a>
<span class="cz-sib-none next" th:if="${nextArticle == null}">
<span class="cz-sib-label">下一篇</span>
<span class="cz-sib-title">已是最新一篇</span>
</span>
</nav>
</div>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- campus-azure 公共页脚:深蓝渐变底 + 同心圆环纹理(CSS 绘制) + 三栏(品牌/联系备案/友链分组) + 底条校名。
数据绑定与 default 契约一致(linkGroups/contact/copyright/beian/logo/schName),仅换皮。 -->
<footer class="cz-footer" th:fragment="footer">
<i class="cz-ring cz-ring--1" aria-hidden="true"></i>
<i class="cz-ring cz-ring--2" aria-hidden="true"></i>
<i class="cz-ring cz-ring--3" aria-hidden="true"></i>
<div class="cz-footer-inner">
<!-- 品牌栏 -->
<div class="cz-footer-brand">
<span class="cz-footer-badge" th:if="${site.config['logo'] != null}">
<img class="cz-footer-logo" th:src="${url.upload(site.config['logo'])}" th:alt="${site.name}">
</span>
<span class="cz-footer-badge cz-footer-badge--plain" th:if="${site.config['logo'] == null}" aria-hidden="true">
<svg viewBox="0 0 24 24" width="30" height="30" focusable="false">
<path d="M12 2l2.4 4.9 5.4.8-3.9 3.8.9 5.4L12 14.4 7.2 16.9l.9-5.4L4.2 7.7l5.4-.8L12 2z"
fill="currentColor"></path>
</svg>
</span>
<span class="cz-footer-site" th:text="${site.name}">站点名称</span>
</div>
<!-- 联系/版权/备案 -->
<div class="cz-footer-info">
<p class="cz-footer-line" th:if="${site.config['contact'] != null}">
<i class="cz-dot" aria-hidden="true"></i><span th:text="${site.config['contact']}">联系方式</span>
</p>
<p class="cz-footer-line" th:if="${site.config['copyright'] != null}">
<i class="cz-dot" aria-hidden="true"></i><span th:text="${site.config['copyright']}">版权信息</span>
</p>
<p class="cz-footer-line" th:if="${site.config['beian'] != null}">
<i class="cz-dot" aria-hidden="true"></i><span th:text="${site.config['beian']}">备案号</span>
</p>
</div>
<!-- 友情链接(分组渲染;无分组整栏不渲染) -->
<div class="cz-footer-links" th:if="${!#lists.isEmpty(site.linkGroups)}">
<div class="cz-links-group" th:each="group : ${site.linkGroups}"
th:if="${!#lists.isEmpty(group.links)}">
<span class="cz-group-name" th:text="${group.groupName}">分组</span>
<p class="cz-links-row">
<a th:each="link : ${group.links}" th:href="${link.linkUrl}" target="_blank" rel="noopener">
<img th:if="${link.logoUrl != null and !#strings.isEmpty(link.logoUrl)}" class="link-logo"
th:src="${url.upload(link.logoUrl)}" th:alt="${link.name}" loading="lazy">
<span th:text="${link.name}">链接</span>
</a>
</p>
</div>
</div>
</div>
<div class="cz-footer-bottom">
<p th:if="${site.schName != null and !#strings.isEmpty(site.schName)}"
th:text="${site.schName}">学校名称</p>
</div>
</footer>
</html>
@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<!-- campus-azure 公共 head 片段:与 default 契约一致(pageTitle/seo/canonical/og/themeVarsCss/favicon 兜底) -->
<head th:fragment="head">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:text="${pageTitle}">页面标题</title>
<meta name="description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}"
th:content="${seoDescription}">
<link rel="canonical" th:if="${canonical != null}" th:href="${canonical}">
<meta property="og:title" th:if="${pageTitle != null}" th:content="${pageTitle}">
<meta property="og:description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}" th:content="${seoDescription}">
<meta property="og:image" th:if="${ogImage != null}" th:content="${ogImage}">
<link rel="stylesheet" th:href="${url.asset('css/main.css')}">
<style th:if="${not #strings.isEmpty(themeVarsCss)}" th:utext="${themeVarsCss}"></style>
<!-- 站点级自定义样式(B3):位于皮肤 CSS 与微调变量之后层叠覆盖;内容经服务端黑名单强校验后直出 -->
<style th:if="${not #strings.isEmpty(customCss)}" th:utext="${customCss}"></style>
<link rel="icon" type="image/png"
th:href="${site.config['favicon'] != null ? url.upload(site.config['favicon']) : url.asset('favicon.png')}">
</head>
</html>
@@ -1,86 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- campus-azure 公共头部:深蓝顶栏(日期+院系切换) + 深蓝品牌行(徽标+站名+圆角搜索) + 白底主导航(吸顶反蓝、
主蓝半透明下拉面板)。数据绑定与 default 契约一致,仅换皮。 -->
<header class="cz-header" th:fragment="header">
<a class="skip-link" href="#main-content">跳到主要内容</a>
<!-- 顶栏 -->
<div class="cz-topbar">
<div class="cz-topbar-inner">
<span class="cz-topbar-date" id="siteDate" th:aria-label="'今日日期'">加载日期…</span>
<div class="cz-topbar-side" th:if="${!site.colleges.isEmpty()}"
th:with="collegesLabel=${site.config['colleges_label'] ?: '专业院系'}">
<div class="cz-colleges">
<span class="cz-colleges-toggle">
<a href="javascript:void(0)" th:text="${collegesLabel}">专业院系</a>
<i class="cz-colleges-arrow" aria-hidden="true">▾</i>
</span>
<ul class="cz-colleges-menu">
<li th:each="col : ${site.colleges}" th:classappend="${col.path == site.path} ? 'active'">
<a th:href="${url.siteHome(col.path, col.publicBaseUrl)}"
th:title="${col.name}" th:text="${col.name}">学院名称</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 品牌行 -->
<div class="cz-brandbar">
<div class="cz-brandbar-inner">
<div class="cz-brand">
<span class="cz-brand-badge" th:if="${site.config['logo'] != null}">
<img class="cz-logo" th:src="${url.upload(site.config['logo'])}" th:alt="${site.name}">
</span>
<span class="cz-brand-badge cz-brand-badge--plain" th:if="${site.config['logo'] == null}" aria-hidden="true">
<svg viewBox="0 0 24 24" width="26" height="26" focusable="false">
<path d="M12 2l2.4 4.9 5.4.8-3.9 3.8.9 5.4L12 14.4 7.2 16.9l.9-5.4L4.2 7.7l5.4-.8L12 2z"
fill="currentColor"></path>
</svg>
</span>
<a class="cz-site-name" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
</div>
<form class="cz-search" th:action="${url.search('', null)}" method="get" role="search">
<input type="text" name="q" placeholder="请输入关键字…" aria-label="站内搜索">
<button type="submit" aria-label="搜索">
<svg viewBox="0 0 24 24" width="16" height="16" focusable="false" aria-hidden="true">
<path d="M10.5 3a7.5 7.5 0 015.9 12.1l4.3 4.3-1.4 1.4-4.3-4.3A7.5 7.5 0 1110.5 3zm0 2a5.5 5.5 0 100 11 5.5 5.5 0 000-11z"
fill="currentColor"></path>
</svg>
</button>
</form>
</div>
</div>
<!-- 主导航:白底深蓝字;下拉为主蓝半透明面板(.cz-sub 展开动效见 main.css);吸顶反蓝由 main.js 处理 -->
<nav class="cz-nav" id="czNav" aria-label="栏目导航">
<div class="cz-nav-inner">
<button class="cz-nav-toggle" type="button" aria-label="展开菜单" aria-expanded="false">
<span></span><span></span><span></span>
</button>
<ul class="cz-nav-list">
<li th:classappend="${currentCategoryId == null and navServiceActive == null} ? 'active'">
<a th:href="${url.home()}">首页</a>
</li>
<li th:each="cat : ${navCategories}"
th:classappend="${((currentCategoryId == cat.id or navTopId == cat.id) ? 'active' : '') + (!cat.children.isEmpty() ? ' has-sub' : '')}">
<a th:if="${cat.type == 3}" th:href="${cat.linkUrl}" target="_blank" rel="noopener"
th:text="${cat.name}">栏目</a>
<a th:unless="${cat.type == 3}" th:href="${url.category(cat.id)}"><span th:text="${cat.name}">栏目</span><i th:if="${!cat.children.isEmpty()}" class="cz-caret" aria-hidden="true">▾</i></a>
<!-- 二级下拉:主蓝半透明面板,顶部亮线,项 hover 加深+左亮条 -->
<ul class="cz-sub" th:if="${!cat.children.isEmpty()}">
<li th:each="sub : ${cat.children}"
th:classappend="${sub.id == currentCategoryId} ? 'active'">
<a th:if="${sub.type == 3}" th:href="${sub.linkUrl}" target="_blank" rel="noopener"
th:text="${sub.name}">子栏目</a>
<a th:unless="${sub.type == 3}" th:href="${url.category(sub.id)}" th:text="${sub.name}">子栏目</a>
</li>
</ul>
</li>
</ul>
</div>
</nav>
</header>
</html>
@@ -1,209 +0,0 @@
<!--
campus-azure(青蓝校园)首页区块 fragment:站点配了 home_layout 时,区块正文必须由本文件渲染,
不能落到 themes/common 的通用骨架——否则换肤只换了页头页脚,首页正文仍是骨架内置蓝灰卡(线上事故根因)。
结构复用主题 main.css 第 7/9 节既有语汇(cz-block / cz-block-head / cz-block-item / cz-feat-* /
cz-top-badge / cz-empty),布局模式特有的排布(封面头条、圆点行、图墙、速递条、链接行)另起第 16 节。
与 academic-navy 的分野:区块头用居中双线(师范/农林官网语言)、要闻用全宽封面头条、列表用圆点密行。
契约:block 为渲染端装配的同构 Map(category.name/title/homeStyle/items/moreUrl/background)。
-->
<!-- 形态一:图文要闻(homeStyle=news_grid)——全宽封面头条(遮罩标题)+ 双列密行 -->
<section class="cz-section cz-container" th:fragment="news_grid(block)" aria-label="栏目区块"
th:with="newsBg=${block['background'] != null ? (block['background'] ? site.config['news_bg_image'] : null) : site.config['news_bg_image']}">
<div class="cz-sec-bg" th:if="${!#strings.isEmpty(newsBg)}"
th:style="|background-image:url(${url.upload(newsBg)})|"></div>
<div class="cz-block cz-block--flow"
th:with="lead=${!#lists.isEmpty(block.items) ? block.items[0] : null},
rest=${#lists.size(block.items) > 1 ? block.items.subList(1, block.items.size()) : null}">
<div class="cz-block-head">
<h2 th:text="${block.category.name}">校园要闻</h2>
<i class="cz-block-tick" aria-hidden="true"></i>
</div>
<a class="cz-hero" th:if="${lead != null}" th:href="${url.article(lead.id)}">
<img th:if="${lead.coverImage != null}" th:src="${url.upload(lead.coverImage)}"
th:alt="${lead.title}" loading="lazy">
<span class="cz-hero-plain" th:if="${lead.coverImage == null}" aria-hidden="true"></span>
<span class="cz-hero-mask">
<span class="cz-hero-title" th:text="${lead.title}">头条标题</span>
<span class="cz-hero-meta">
<span class="cz-top-badge" th:if="${lead.topmost == 1}">置顶</span>
<time th:if="${lead.publishTime != null}"
th:text="${#temporals.format(lead.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</span>
</span>
</a>
<ul class="cz-block-list cz-block-list--two" th:if="${rest != null}">
<li class="cz-block-item" th:each="item : ${rest}">
<i class="cz-item-bullet" aria-hidden="true"></i>
<a class="cz-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<span class="cz-top-badge" th:if="${item.topmost == 1}">置顶</span>
<time class="cz-item-date" th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
<th:block th:if="${#lists.isEmpty(block.items)}">
<div th:replace="~{'themes/campus-azure/fragments/home-blocks' :: empty(${block})}"></div>
</th:block>
</div>
</section>
<!-- 形态二:默认卡(homeStyle 未设/未知)——左图文头条 + 右圆点密行 -->
<section class="cz-section cz-container" th:fragment="card(block)" aria-label="栏目区块">
<div class="cz-block cz-block--flow">
<div class="cz-block-head">
<h2 th:text="${block.category.name}">栏目名</h2>
<a class="cz-more" th:href="${block.moreUrl}">更多 ›</a>
<i class="cz-block-tick" aria-hidden="true"></i>
</div>
<div class="cz-split" th:if="${!#lists.isEmpty(block.items)}"
th:with="feat=${block.items[0]}, rest=${#lists.size(block.items) > 1 ? block.items.subList(1, block.items.size()) : null}">
<div class="cz-block-featured">
<a th:href="${url.article(feat.id)}">
<span class="cz-feat-cover" th:if="${feat.coverImage != null}">
<img th:src="${url.upload(feat.coverImage)}" th:alt="${feat.title}" loading="lazy">
</span>
<span class="cz-feat-title" th:text="${feat.title}">头条标题</span>
<span class="cz-feat-summary" th:if="${!#strings.isEmpty(feat.summary)}" th:text="${feat.summary}">摘要</span>
<span class="cz-feat-meta">
<span class="cz-top-badge" th:if="${feat.topmost == 1}">置顶</span>
<time th:if="${feat.publishTime != null}"
th:text="${#temporals.format(feat.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</span>
</a>
</div>
<ul class="cz-block-list" th:if="${rest != null}">
<li class="cz-block-item" th:each="item : ${rest}">
<i class="cz-item-bullet" aria-hidden="true"></i>
<a class="cz-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<span class="cz-top-badge" th:if="${item.topmost == 1}">置顶</span>
<time class="cz-item-date" th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'MM-dd')}">01-01</time>
</li>
</ul>
</div>
<th:block th:if="${#lists.isEmpty(block.items)}">
<div th:replace="~{'themes/campus-azure/fragments/home-blocks' :: empty(${block})}"></div>
</th:block>
</div>
</section>
<!-- 形态三:圆点密行(homeStyle=compact)——官网最密的「圆点 + 标题 + 右日期」单行 -->
<section class="cz-section cz-container" th:fragment="compact(block)" aria-label="栏目区块">
<div class="cz-block cz-block--flow">
<div class="cz-block-head">
<h2 th:text="${block.category.name}">通知公告</h2>
<a class="cz-more" th:href="${block.moreUrl}">更多 ›</a>
<i class="cz-block-tick" aria-hidden="true"></i>
</div>
<ul class="cz-block-list" th:if="${!#lists.isEmpty(block.items)}">
<li class="cz-block-item" th:each="item : ${block.items}">
<i class="cz-item-bullet" aria-hidden="true"></i>
<a class="cz-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<span class="cz-top-badge" th:if="${item.topmost == 1}">置顶</span>
<time class="cz-item-date" th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
<th:block th:if="${#lists.isEmpty(block.items)}">
<div th:replace="~{'themes/campus-azure/fragments/home-blocks' :: empty(${block})}"></div>
</th:block>
</div>
</section>
<!-- 形态四:媒体聚焦(homeStyle=media_focus)——来源徽标 + 双列密行 -->
<section class="cz-section cz-container" th:fragment="media_focus(block)" aria-label="栏目区块">
<div class="cz-block cz-block--flow">
<div class="cz-block-head">
<h2 th:text="${block.category.name}">媒体聚焦</h2>
<a class="cz-more" th:href="${block.moreUrl}">更多 ›</a>
<i class="cz-block-tick" aria-hidden="true"></i>
</div>
<ul class="cz-block-list cz-block-list--two" th:if="${!#lists.isEmpty(block.items)}">
<li class="cz-block-item" th:each="item : ${block.items}">
<span class="cz-src" th:if="${!#strings.isEmpty(item.source)}" th:text="${item.source}">媒体来源</span>
<a class="cz-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time class="cz-item-date" th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'MM-dd')}">01-01</time>
</li>
</ul>
<th:block th:if="${#lists.isEmpty(block.items)}">
<div th:replace="~{'themes/campus-azure/fragments/home-blocks' :: empty(${block})}"></div>
</th:block>
</div>
</section>
<!-- 形态五:图片墙(homeStyle=image_wall)——仅取有封面的条目,三列大格 -->
<section class="cz-section cz-container" th:fragment="image_wall(block)" aria-label="栏目区块">
<div class="cz-block-head">
<h2 th:text="${block.category.name}">图片新闻</h2>
<a class="cz-more" th:href="${block.moreUrl}">更多 ›</a>
<i class="cz-block-tick" aria-hidden="true"></i>
</div>
<ul class="cz-wall" th:if="${!#lists.isEmpty(block.items)}">
<li class="cz-wall-item" th:each="item : ${block.items}" th:if="${item.coverImage != null}">
<a th:href="${url.article(item.id)}" th:title="${item.title}">
<img th:src="${url.upload(item.coverImage)}" th:alt="${item.title}" loading="lazy">
<span class="cz-wall-cap" th:text="${item.title}">图片标题</span>
</a>
</li>
</ul>
<th:block th:if="${#lists.isEmpty(block.items)}">
<div th:replace="~{'themes/campus-azure/fragments/home-blocks' :: empty(${block})}"></div>
</th:block>
</section>
<!-- 布局模式:要闻速递横条(block.items 由渲染端按置顶+最新装配;单行不换行,中文不塌列) -->
<section class="cz-ticker" th:fragment="ticker(block)" aria-label="要闻速递"
th:if="${!#lists.isEmpty(block.items)}">
<div class="cz-container cz-ticker-inner">
<span class="cz-ticker-label" th:text="${block.title}">要闻速递</span>
<ul class="cz-ticker-list">
<li th:each="item : ${block.items}">
<a class="cz-ticker-item" th:href="${url.article(item.id)}" th:text="${item.title}">要闻标题</a>
</li>
</ul>
</div>
</section>
<!-- 布局模式:线上展厅(block.items = 轮播位 gallery) -->
<section class="cz-section cz-container cz-gallery" th:fragment="gallery(block)" aria-label="线上展厅"
th:if="${!#lists.isEmpty(block.items)}">
<div class="cz-block-head">
<h2 th:text="${block.title}">线上展厅</h2>
<i class="cz-block-tick" aria-hidden="true"></i>
</div>
<p class="cz-gallery-sub" th:if="${!#strings.isEmpty(block.subtitle)}" th:text="${block.subtitle}">
Online Exhibition Hall
</p>
<ul class="cz-wall cz-wall--4">
<li class="cz-wall-item" th:each="g : ${block.items}">
<a th:href="${g.linkUrl}" th:title="${g.title}">
<img th:src="${g.imageUrl}" th:alt="${g.title}" loading="lazy">
<span class="cz-wall-cap" th:if="${!#strings.isEmpty(g.title)}" th:text="${g.title}">作品标题</span>
</a>
</li>
</ul>
</section>
<!-- 布局模式:友情链接分组(block.linkGroups 由渲染端按 group 过滤) -->
<section class="cz-section cz-container" th:fragment="links(block)" aria-label="友情链接">
<div class="cz-block cz-block--flow">
<div class="cz-block-head">
<h2 th:text="${block.title}">友情链接</h2>
<i class="cz-block-tick" aria-hidden="true"></i>
</div>
<th:block th:each="grp : ${block.linkGroups}">
<p class="cz-links-group" th:if="${!#strings.isEmpty(grp.groupName)}" th:text="${grp.groupName}">分组</p>
<ul class="cz-links">
<li th:each="l : ${grp.links}">
<a th:href="${l.linkUrl}" th:text="${l.name}" target="_blank" rel="noopener">链接名</a>
</li>
</ul>
</th:block>
</div>
</section>
<!-- 空态(各形态引用时守卫必须包在 th:block 外层:th:replace 优先级 100 先于 th:if 300 求值) -->
<div class="cz-empty" th:fragment="empty(block)">
<p>栏目暂无内容</p>
<p class="cz-empty-hint">内容发布后将在此展示</p>
</div>
@@ -1,149 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="cz-home" id="main-content">
<!-- 首页轮播位(cms_banner position=home;设计尺寸见 theme.json bannerSize) -->
<section class="cz-banner" th:if="${!#lists.isEmpty(banners)}" aria-label="焦点图片">
<div class="cz-banner-slides">
<a class="cz-banner-slide" th:each="b, stat : ${banners}"
th:classappend="${stat.first} ? 'active'"
th:href="${b.linkUrl}" th:title="${b.title}">
<img th:src="${b.imageUrl}" th:alt="${b.title}">
<span class="cz-banner-title" th:text="${b.title}">标题</span>
</a>
</div>
<button class="cz-banner-arrow prev" type="button" aria-label="上一张" th:if="${#lists.size(banners) > 1}">‹</button>
<button class="cz-banner-arrow next" type="button" aria-label="下一张" th:if="${#lists.size(banners) > 1}">›</button>
<div class="cz-banner-dots" th:if="${#lists.size(banners) > 1}">
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
th:aria-label="${'第' + (stat.count) + '张'}"></button>
</div>
</section>
<!-- 首页布局模式(docs/16 §4.4):区块形态由本主题 fragments/home-blocks 承载——换肤必须连首页正文一起换,
只换页头页脚会被判「皮肤不生效」。未知类型仍回落 common 骨架(渲染端已过滤未知 type,正常不触发)。
注意:th:replace(优先级100)先于 th:case(300)执行——case 必须放父元素、replace 放子元素 -->
<th:block th:if="${layoutBlocks != null}">
<th:block th:each="block : ${layoutBlocks}">
<th:block th:switch="${block.type}">
<th:block th:case="'article_list'">
<th:block th:switch="${block.homeStyle}">
<th:block th:case="'news_grid'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: news_grid(${block})}" /></th:block>
<th:block th:case="'media_focus'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: media_focus(${block})}" /></th:block>
<th:block th:case="'compact'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: compact(${block})}" /></th:block>
<th:block th:case="'image_wall'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: image_wall(${block})}" /></th:block>
<th:block th:case="*"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: card(${block})}" /></th:block>
</th:block>
</th:block>
<th:block th:case="'ticker'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: ticker(${block})}" /></th:block>
<th:block th:case="'gallery'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: gallery(${block})}" /></th:block>
<th:block th:case="'links'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: links(${block})}" /></th:block>
<th:block th:case="*"><th:block th:replace="~{'themes/common/fragments/home-blocks' :: render(${block})}" /></th:block>
</th:block>
</th:block>
</th:block>
<!-- 回落分支(docs/16 D2):无 home_layout 时按现状渲染 -->
<th:block th:unless="${layoutBlocks != null}">
<section class="cz-section cz-container" aria-label="头条与图片新闻">
<div class="cz-headline">
<div class="cz-headline-card" th:if="${!#lists.isEmpty(topArticles)}">
<div class="cz-block-head">
<h2>头条推荐</h2>
<i class="cz-block-tick" aria-hidden="true"></i>
</div>
<ul class="cz-headline-list">
<li class="cz-headline-lead" th:with="lead=${topArticles[0]}">
<a class="cz-lead-title" th:href="${url.article(lead.id)}" th:text="${lead.title}">头条标题</a>
<div class="cz-lead-meta">
<span class="cz-top-badge" th:if="${lead.topmost == 1}">置顶</span>
<time th:if="${lead.publishTime != null}"
th:text="${#temporals.format(lead.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</div>
</li>
<li th:each="item, stat : ${topArticles}" th:if="${stat.index > 0}">
<span class="cz-top-badge" th:if="${item.topmost == 1}">置顶</span>
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
</div>
<div class="cz-focus-card" th:if="${!#lists.isEmpty(focusArticles)}">
<div class="cz-block-head">
<h2>图片新闻</h2>
<i class="cz-block-tick" aria-hidden="true"></i>
</div>
<a class="cz-focus-lead" th:with="cover=${focusArticles[0]}"
th:href="${url.article(cover.id)}">
<span class="cz-focus-cover">
<img th:src="${url.upload(cover.coverImage)}" th:alt="${cover.title}">
</span>
<span class="cz-focus-title" th:text="${cover.title}">图片新闻标题</span>
</a>
<ul class="cz-focus-rows">
<li th:each="row, stat : ${focusArticles}" th:if="${stat.index > 0}">
<a th:href="${url.article(row.id)}">
<span class="cz-row-cover"><img th:src="${url.upload(row.coverImage)}" th:alt="${row.title}"></span>
<span class="cz-row-title" th:text="${row.title}">图片新闻标题</span>
</a>
</li>
</ul>
</div>
</div>
</section>
<!-- 一级栏目区块(聚合列表型子孙栏目 top 8) -->
<section class="cz-section cz-container" aria-label="栏目区块">
<div class="cz-blocks">
<div class="cz-block" th:each="block : ${categoryBlocks}">
<div class="cz-block-head cz-block-head--bar">
<h2 th:text="${block.category.name}">栏目名</h2>
<a class="cz-more" th:href="${block.moreUrl}">更多 ›</a>
</div>
<ul class="cz-block-list" th:if="${!#lists.isEmpty(block.items)}">
<!-- 区块头条:有封面则图文,无封面则带摘要纯文 -->
<li class="cz-block-featured" th:with="feat=${block.items[0]}">
<a th:href="${url.article(feat.id)}">
<span class="cz-feat-cover" th:if="${feat.coverImage != null}">
<img th:src="${url.upload(feat.coverImage)}" th:alt="${feat.title}">
</span>
<span class="cz-feat-body">
<span class="cz-feat-title" th:text="${feat.title}">头条标题</span>
<span class="cz-feat-summary" th:if="${!#strings.isEmpty(feat.summary)}"
th:text="${feat.summary}">摘要</span>
<span class="cz-feat-meta">
<span class="cz-top-badge" th:if="${feat.topmost == 1}">置顶</span>
<time th:if="${feat.publishTime != null}"
th:text="${#temporals.format(feat.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</span>
</span>
</a>
</li>
<li class="cz-block-item" th:each="item, stat : ${block.items}" th:if="${stat.index > 0}">
<span class="cz-date-chip" th:if="${item.publishTime != null}"
th:with="d=${item.publishTime}">
<b th:text="${#temporals.format(d, 'dd')}">01</b>
<i th:text="${#temporals.format(d, 'yyyy-MM')}">2026-01</i>
</span>
<a class="cz-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<span class="cz-top-badge" th:if="${item.topmost == 1}">置顶</span>
</li>
</ul>
<div class="cz-empty" th:if="${#lists.isEmpty(block.items)}">
<p>栏目暂无内容</p>
<p class="cz-empty-hint">内容发布后将在此展示</p>
</div>
</div>
</div>
</section>
</th:block>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,105 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="cz-page cz-container" id="main-content">
<div class="cz-cols">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="cz-side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="cz-side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="cz-side-nav-list">
<li class="cz-side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="cz-page-card">
<nav class="cz-crumb" aria-label="面包屑" th:if="${searchMode != true}">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node, st : ${categoryPath}">
<span class="cz-crumb-sep" aria-hidden="true">/</span>
<a th:if="${!st.last}" class="crumb-parent" th:href="${url.category(node.id)}"
th:text="${node.name}">父栏目</a>
<span class="cz-crumb-current" th:if="${st.last}" th:text="${node.name}">栏目</span>
</th:block>
</nav>
<nav class="cz-crumb" aria-label="面包屑" th:if="${searchMode == true}">
<a th:href="${url.home()}">首页</a>
<span class="cz-crumb-sep" aria-hidden="true">/</span>
<span class="cz-crumb-current">搜索结果</span>
</nav>
<h1 class="cz-page-title" th:if="${searchMode != true}" th:text="${category.name}">栏目名</h1>
<h1 class="cz-page-title" th:if="${searchMode == true}" th:text="'搜索:' + ${q}">搜索词</h1>
<ul class="cz-article-list">
<!-- 有封面:图文行;无封面:行式(符号 + 标题 + 右侧日期) -->
<li th:each="item : ${listData.items}">
<div class="cz-article-row" th:if="${item.coverImage != null}">
<a class="cz-item-cover" th:href="${url.article(item.id)}">
<img th:src="${url.upload(item.coverImage)}" th:alt="${item.title}">
</a>
<div class="cz-item-body">
<div class="cz-item-title">
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
</div>
<p class="cz-item-summary" th:if="${!#strings.isEmpty(item.summary)}"
th:text="${item.summary}">摘要</p>
<div class="cz-item-meta">
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
<span class="cz-top-badge" th:if="${item.topmost == 1}">置顶</span>
</div>
</div>
</div>
<div class="cz-article-line" th:if="${item.coverImage == null}">
<i class="cz-line-tick" aria-hidden="true"></i>
<div class="cz-line-main">
<a class="cz-line-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<p class="cz-item-summary" th:if="${!#strings.isEmpty(item.summary)}"
th:text="${item.summary}">摘要</p>
</div>
<div class="cz-line-side">
<span class="cz-top-badge" th:if="${item.topmost == 1}">置顶</span>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</div>
</div>
</li>
</ul>
<div class="cz-empty" th:if="${#lists.isEmpty(listData.items)}">
<p th:if="${searchMode == true}">未找到相关内容</p>
<p th:if="${searchMode != true}">栏目暂无内容</p>
<p class="cz-empty-hint" th:if="${searchMode == true}">换个关键词试试</p>
<p class="cz-empty-hint" th:if="${searchMode != true}">内容发布后将在此展示</p>
</div>
<!-- 分页(列表页/搜索页共用) -->
<nav class="cz-pagination" th:if="${listData.totalPages > 1}"
th:with="cur=${listData.page}, total=${listData.totalPages}" aria-label="分页">
<a th:classappend="${cur <= 1} ? 'disabled'"
th:href="${searchMode == true} ? ${url.search(q, cur - 1)} : ${url.category(listData.categoryId) + '?page=' + (cur - 1)}"
th:aria-disabled="${cur <= 1}">上一页</a>
<a th:each="p : ${#numbers.sequence(1, total)}"
th:classappend="${p == cur} ? 'active'"
th:href="${searchMode == true} ? ${url.search(q, p)} : ${url.category(listData.categoryId) + '?page=' + p}"
th:aria-label="${'第' + p + '页'}"
th:text="${p}">1</a>
<a th:classappend="${cur >= total} ? 'disabled'"
th:href="${searchMode == true} ? ${url.search(q, cur + 1)} : ${url.category(listData.categoryId) + '?page=' + (cur + 1)}"
th:aria-disabled="${cur >= total}">下一页</a>
</nav>
</div>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,50 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="cz-page cz-container" id="main-content">
<div class="cz-cols">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="cz-side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="cz-side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="cz-side-nav-list">
<li class="cz-side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="cz-page-card">
<nav class="cz-crumb" aria-label="面包屑">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node, st : ${categoryPath}">
<span class="cz-crumb-sep" aria-hidden="true">/</span>
<a th:if="${!st.last}" class="crumb-parent" th:href="${url.category(node.id)}"
th:text="${node.name}">父栏目</a>
<span class="cz-crumb-current" th:if="${st.last}" th:text="${node.name}">栏目</span>
</th:block>
</nav>
<article class="cz-article">
<h1 class="cz-article-title" th:text="${category.name}">栏目名</h1>
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
<div class="cz-article-content" th:if="${!#strings.isEmpty(category.content)}"
th:utext="${category.content}">单页内容</div>
<div class="cz-empty" th:if="${#strings.isEmpty(category.content)}">
<p>页面暂无内容</p>
<p class="cz-empty-hint">内容编辑后将在此展示</p>
</div>
</article>
</div>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,23 +0,0 @@
{
"specVersion": 1,
"name": "campus-azure",
"title": "青蓝校园(campus-azure)",
"version": "1.0.0",
"author": "yunhai",
"description": "青蓝校园·师范/农林类官网门户:青蓝顶栏+白底主导航+青绿描边标题+大日期数字列表+图文左列右栏+圆角卡片区块+深色页脚带二维码位。基座 scholar-blue,配色与首页区块形态按师范/农林类高校官网重做。",
"screenshot": "assets/screenshot.png",
"bannerSize": "1920x560",
"params": [
{ "key": "primary", "cssVar": "--cz-primary", "label": "主色", "type": "color", "default": "#0d6a9e" },
{ "key": "primaryDark", "cssVar": "--cz-primary-dark", "label": "主色加深", "type": "color", "default": "#084c73" },
{ "key": "primaryLight", "cssVar": "--cz-primary-light","label": "主色提亮", "type": "color", "default": "#1d87bd" },
{ "key": "accent", "cssVar": "--cz-accent", "label": "点缀青绿", "type": "color", "default": "#2b9c8f" },
{ "key": "navBg", "cssVar": "--cz-nav-bg", "label": "导航条底色", "type": "color", "default": "#ffffff" },
{ "key": "footerBg", "cssVar": "--cz-footer-bg", "label": "页脚底色", "type": "color", "default": "#0b3348" },
{ "key": "text", "cssVar": "--cz-text", "label": "正文文字色", "type": "color", "default": "#303c46" },
{ "key": "textLight", "cssVar": "--cz-text-light", "label": "次要文字色", "type": "color", "default": "#6e7d88" },
{ "key": "bg", "cssVar": "--cz-bg", "label": "页面底色", "type": "color", "default": "#eef3f6" },
{ "key": "cardBg", "cssVar": "--cz-card-bg", "label": "卡片底色", "type": "color", "default": "#ffffff" },
{ "key": "border", "cssVar": "--cz-border", "label": "边框色", "type": "color", "default": "#d9e5ec" }
]
}
@@ -1,23 +0,0 @@
# classic-red 皮肤包
由 default 派生的演示皮肤(方案 §7)。**升级随版本发布,不在站点间复制目录**。
## 数据契约(模板可取用的 model 属性全集,§4.3)
| 页面 | 属性 |
| --- | --- |
| 全部 | `site`(快照:config/分类树/轮播/友链组/院系子站)、`url`(PortalUrlHelper:home/category/article/asset/search/siteHome)、`themeBase`(=`themes/classic-red`)、`navCategories`、`pageTitle`、`seoDescription`、`canonical`、`themeParams`(微调参数,仅 theme.json 声明过的 key)、`themeVarsCss`(服务端预构建 `:root{}` 覆盖串,无参数为空) |
| home | `banners`(时间窗内 home 位)、`topArticles`(置顶+最新 6)、`categoryBlocks`(LIST 型根栏目块,各 top 8) |
| list / search | `listData`(分页)、`category`(list)、`q` + `searchMode`(search) |
| detail | `article`、`prevArticle`、`nextArticle`、`currentCategoryId` |
| single | `category`(含 `content`)、`currentCategoryId` |
原则:皮肤不得假设"首页一定有轮播",对空集合优雅降级。
## 轮播图尺寸约定(bannerSize)
1920x560(管理端上传端按此引导裁剪,D2 决策点)。
## 可调参数
见 `theme.json` 的 `params`(color 进 CSS 变量;select 供模板 `th:if` 分支布局)。
@@ -1,372 +0,0 @@
/* ============================================================
* classic-red 皮肤包样式(default 派生)(方案 §8.2)
* 换肤 = 拷贝 default 目录改 CSS 变量与布局,零后端代码。
* 轮播图设计尺寸约定:1920×560(管理端上传端按此引导裁剪,D2 决策点确认)。
* ============================================================ */
:root {
--primary: #b01a2e;
--primary-dark: #7f1224;
--text: #333;
--text-light: #777;
--bg: #f7f5f2;
--card-bg: #fff;
--border: #e5e6eb;
--max-width: 1200px;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
color: var(--text);
background: var(--bg);
}
a { color: inherit; text-decoration: none; }
a:hover { color: var(--primary); }
img { max-width: 100%; }
time, .item-meta { color: var(--text-light); font-size: 13px; }
.empty { color: var(--text-light); text-align: center; padding: 32px 0; }
/* ===== 头部 ===== */
.site-header { background: var(--card-bg); border-bottom: 1px solid var(--border); }
.header-inner {
max-width: var(--max-width);
margin: 0 auto;
padding: 18px 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.site-brand { display: flex; align-items: center; gap: 12px; }
.site-logo { height: 44px; }
.site-name { font-size: 26px; font-weight: 700; color: var(--primary); }
.site-search { display: flex; }
.site-search input {
border: 1px solid var(--border);
border-right: none;
padding: 8px 12px;
width: 220px;
outline: none;
}
.site-search button {
border: none;
background: var(--primary);
color: #fff;
padding: 8px 18px;
cursor: pointer;
}
/* ===== 专业院系下拉(院系子站切换,《院系子站方案》) ===== */
/* 悬停展开白底纵向列表,右对齐 + 超高滚动(院系数十个子站场景) */
.site-colleges { position: relative; }
.site-colleges .colleges-toggle {
display: flex;
align-items: center;
gap: 4px;
border: 1px solid var(--border);
padding: 8px 14px;
cursor: pointer;
color: var(--primary);
font-weight: 600;
white-space: nowrap;
}
.site-colleges .colleges-arrow { font-style: normal; font-size: 12px; color: var(--text-light); }
.site-colleges .colleges-menu {
display: none;
position: absolute;
top: 100%;
right: 0;
min-width: 200px;
max-height: 420px;
overflow-y: auto;
margin: 0;
padding: 6px 0;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-top: 2px solid var(--primary);
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
z-index: 110;
}
.site-colleges:hover .colleges-menu { display: block; }
.site-colleges .colleges-menu li a {
display: block;
padding: 9px 16px;
color: var(--text);
white-space: nowrap;
}
.site-colleges .colleges-menu li a:hover { background: #f2f6fc; color: var(--primary); }
.site-colleges .colleges-menu li.active a { color: var(--primary); font-weight: 600; }
.site-nav { background: var(--primary); }
.site-nav ul {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 16px;
list-style: none;
display: flex;
flex-wrap: wrap;
}
.site-nav li a {
display: block;
padding: 12px 22px;
color: #fff;
}
.site-nav li:hover a, .site-nav li.active a { background: var(--primary-dark); }
/* ===== 二级下拉导航 ===== */
/* 面板为白底纵向列表:需整体覆盖 .site-nav ul 的横向 flex 与内边距 */
.site-nav > ul > li { position: relative; }
.site-nav li.has-sub > a::after { content: " ▾"; font-size: 12px; opacity: .85; }
.site-nav ul.sub-nav {
display: none;
position: absolute;
top: 100%;
left: 0;
min-width: 168px;
max-width: none;
margin: 0;
padding: 6px 0;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-top: 2px solid var(--primary);
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
z-index: 100;
}
.site-nav li:hover > .sub-nav { display: block; }
.site-nav .sub-nav li a {
padding: 9px 16px;
color: var(--text);
white-space: nowrap;
background: none;
}
.site-nav .sub-nav li a:hover { background: #f2f6fc; color: var(--primary); }
.site-nav .sub-nav li.active a { color: var(--primary); font-weight: 600; }
/* ===== 首页轮播 ===== */
.banner { position: relative; max-width: var(--max-width); margin: 16px auto; overflow: hidden; }
.banner-slides { position: relative; height: 420px; }
.banner-slide { display: none; position: absolute; inset: 0; }
.banner-slide.active { display: block; }
.banner-slide img { width: 100%; height: 100%; object-fit: cover; }
.banner-title {
position: absolute;
left: 0; right: 0; bottom: 0;
padding: 12px 20px;
color: #fff;
background: linear-gradient(transparent, rgba(0, 0, 0, .6));
}
.banner-dots {
position: absolute;
bottom: 12px; right: 20px;
display: flex; gap: 8px;
}
.banner-dots button {
width: 10px; height: 10px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, .6);
cursor: pointer;
}
.banner-dots button.active { background: #fff; }
/* ===== 通用区块 ===== */
.top-strip, .category-block {
max-width: var(--max-width);
margin: 16px auto;
padding: 20px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.section-title, .block-head h2 {
margin: 0 0 12px;
font-size: 20px;
border-left: 4px solid var(--primary);
padding-left: 10px;
}
.block-head { display: flex; align-items: baseline; justify-content: space-between; }
.more { font-size: 13px; color: var(--text-light); }
.top-strip ul, .block-list { list-style: none; margin: 0; padding: 0; }
.top-strip li, .block-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 9px 0;
border-bottom: 1px dashed var(--border);
}
.top-strip li:last-child, .block-list li:last-child { border-bottom: none; }
.top-strip li a, .block-list li a {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.top-badge {
flex: none;
color: #fff;
background: var(--primary);
font-size: 12px;
padding: 1px 6px;
border-radius: 2px;
}
/* ===== 列表页 ===== */
.list-main {
max-width: var(--max-width);
margin: 16px auto;
padding: 24px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.breadcrumb { color: var(--text-light); font-size: 13px; margin-bottom: 16px; }
.breadcrumb a { margin-right: 8px; }
.breadcrumb span { margin-left: 8px; }
.page-title {
font-size: 22px;
border-left: 4px solid var(--primary);
padding-left: 10px;
margin: 0 0 18px;
}
.article-list { list-style: none; margin: 0; padding: 0; }
.article-list li { padding: 14px 0; border-bottom: 1px dashed var(--border); }
.item-title { font-size: 17px; }
.item-summary {
color: var(--text-light);
font-size: 14px;
margin: 6px 0;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.item-meta { display: flex; gap: 10px; align-items: center; }
/* ===== 分页 ===== */
.pagination { display: flex; gap: 6px; justify-content: center; padding: 24px 0 4px; }
.pagination a {
min-width: 34px;
text-align: center;
padding: 6px 10px;
border: 1px solid var(--border);
background: var(--card-bg);
}
.pagination a.active { background: var(--primary); color: #fff; border-color: var(--primary); }
/* ===== 详情/单页 ===== */
.detail-main {
max-width: var(--max-width);
margin: 16px auto;
padding: 32px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.article-title { margin: 8px 0 12px; font-size: 26px; text-align: center; }
.article-meta {
display: flex;
gap: 18px;
justify-content: center;
flex-wrap: wrap;
color: var(--text-light);
font-size: 13px;
padding-bottom: 18px;
border-bottom: 1px solid var(--border);
}
.article-content { line-height: 1.9; font-size: 16px; padding-top: 20px; word-break: break-word; }
.article-content img { height: auto; }
.article-content table { border-collapse: collapse; width: 100%; }
.article-content table td, .article-content table th { border: 1px solid var(--border); padding: 6px 10px; }
.article-sibling { display: flex; flex-direction: column; gap: 8px; margin-top: 28px; font-size: 14px; }
/* ===== 页脚 ===== */
.site-footer { margin-top: 32px; background: var(--primary-dark); color: rgba(255, 255, 255, .85); }
.footer-links {
max-width: var(--max-width);
margin: 0 auto;
padding: 18px 16px 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.links-group { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; font-size: 13px; }
.group-name { font-weight: 600; margin-right: 4px; }
.footer-info {
max-width: var(--max-width);
margin: 0 auto;
padding: 12px 16px 20px;
text-align: center;
font-size: 13px;
}
.footer-info p { margin: 4px 0; }
/* ===== 移动端适配 ===== */
@media (max-width: 768px) {
.header-inner { flex-direction: column; align-items: stretch; }
.site-search input { flex: 1; width: auto; }
.site-colleges { align-self: flex-start; }
.site-colleges .colleges-menu { left: 0; right: auto; min-width: 100%; }
.banner-slides { height: 200px; }
.detail-main, .list-main { padding: 18px 14px; }
.article-title { font-size: 22px; }
/* 触屏无 hover:子菜单静态平铺(缩进列表),不做浮层 */
.site-nav ul.sub-nav {
display: block;
position: static;
border: none;
box-shadow: none;
background: transparent;
min-width: 0;
padding: 0 0 6px;
}
.site-nav .sub-nav li a { color: rgba(255, 255, 255, .9); padding: 8px 16px 8px 34px; }
.site-nav .sub-nav li a:hover { background: var(--primary-dark); color: #fff; }
}
/* ===== classic-red 主题差异层(方案 §7:差异仅限 CSS + fragments)===== */
/* 宋体标题(中小学/党政风) */
.site-name, .banner-title, .page-title, .section-title, .article-title, .item-title {
font-family: "SimSun", "STSong", "Songti SC", serif;
}
/* 头部样式参数(theme.json headerStyle=plain → 简洁顶栏) */
.site-header.header-plain { background: #fff; border-bottom: 2px solid var(--primary); }
.site-header.header-plain .site-name { color: var(--primary-dark); }
/* B4 友链 Logo:与名称并排小图(高度 ~20px,垂直居中;过宽截断不撑破页脚) */
.links-group a .link-logo,
.sp-links-row a .link-logo {
height: 20px;
width: auto;
max-width: 64px;
object-fit: contain;
vertical-align: middle;
margin-right: 4px;
border-radius: 2px;
}
/* ===== 左侧栏目导航(docs/11):有二级节点的模块,栏目/文章页左侧展示兄弟目录 ===== */
.list-main.has-sidenav, .detail-main.has-sidenav { display: flex; align-items: flex-start; gap: 24px; }
.has-sidenav .list-body { flex: 1; min-width: 0; }
.side-nav { flex: none; width: 220px; background: var(--card-bg, #fff); border: 1px solid var(--border); overflow: hidden; }
.side-nav-head { background: var(--primary); color: #fff; font-size: 20px; font-weight: 700; text-align: center; padding: 18px 12px; letter-spacing: 2px; }
.side-nav-list { list-style: none; margin: 0; padding: 0; }
.side-nav-item + .side-nav-item { border-top: 1px dashed var(--border); }
.side-nav-item a { display: block; padding: 12px 16px; color: var(--text); text-decoration: none; font-size: 15px; transition: background .15s ease, color .15s ease; }
.side-nav-item a:hover { color: var(--primary); background: var(--bg); }
.side-nav-item.active a { background: var(--primary); color: #fff; font-weight: 600; text-decoration: none; }
/* 顶导航二级下拉当前项高亮(docs/11 顺带) */
.site-nav ul.sub-nav li.active a { color: var(--primary); }
@media (max-width: 768px) {
.list-main.has-sidenav, .detail-main.has-sidenav { flex-direction: column; gap: 12px; }
.side-nav { width: 100%; }
.side-nav-list { display: flex; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.side-nav-item { flex: none; }
.side-nav-item + .side-nav-item { border-top: none; border-left: 1px dashed var(--border); }
.side-nav-item a { white-space: nowrap; }
}
@media print {
.side-nav { display: none; }
.list-main.has-sidenav, .detail-main.has-sidenav { display: block; }
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs><linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#be4354"/><stop offset="1" stop-color="#7b1220"/>
</linearGradient></defs>
<rect width="64" height="64" fill="url(#bg)"/>
<circle cx="32" cy="25" r="10" fill="#ffffff"/>
<path d="M 16 49.3 Q 32 30.7 48 49.3 Z" fill="#ffffff" opacity="0.92"/>
</svg>

Before

Width:  |  Height:  |  Size: 434 B

@@ -1,40 +0,0 @@
/* default 皮肤包脚本:仅首页轮播自动播放与圆点切换,无任何外部依赖 */
(function () {
var slides = document.querySelectorAll('.banner-slide');
var dots = document.querySelectorAll('.banner-dots button');
if (!slides.length) {
return;
}
var current = 0;
var timer = null;
function show(index) {
slides[current].classList.remove('active');
if (dots[current]) {
dots[current].classList.remove('active');
}
current = (index + slides.length) % slides.length;
slides[current].classList.add('active');
if (dots[current]) {
dots[current].classList.add('active');
}
}
function start() {
if (slides.length > 1) {
timer = window.setInterval(function () {
show(current + 1);
}, 5000);
}
}
dots.forEach(function (dot, index) {
dot.addEventListener('click', function () {
window.clearInterval(timer);
show(index);
start();
});
});
start();
})();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

@@ -1,58 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main" th:classappend="${sideNav != null} ? 'has-sidenav'">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="side-nav-list">
<li class="side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="list-body">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node : ${categoryPath}">
<a class="crumb-parent" th:href="${url.category(node.id)}" th:text="${node.name}">栏目</a>
</th:block>
<span>正文</span>
</nav>
<article class="article">
<h1 class="article-title" th:text="${article.title}">文章标题</h1>
<div class="article-meta">
<time th:if="${article.publishTime != null}"
th:text="${'发布时间:' + #temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
<span th:if="${!#strings.isEmpty(article.authorName)}"
th:text="'作者:' + ${article.authorName}">作者</span>
<span th:if="${!#strings.isEmpty(article.source)}"
th:text="'来源:' + ${article.source}">来源</span>
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
</div>
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
其余字段一律 th:text 转义输出 -->
<div class="article-content" th:utext="${article.content}">正文</div>
</article>
<nav class="article-sibling">
<a th:if="${prevArticle != null}" class="prev"
th:href="${url.article(prevArticle.id)}"
th:text="'上一篇:' + ${prevArticle.title}">上一篇</a>
<a th:if="${nextArticle != null}" class="next"
th:href="${url.article(nextArticle.id)}"
th:text="'下一篇:' + ${nextArticle.title}">下一篇</a>
</nav>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共页脚:联系信息 + 友情链接(分组渲染,方案 §8.1:友链不设独立路由) -->
<footer class="site-footer" th:fragment="footer">
<div class="footer-links" th:if="${!#lists.isEmpty(site.linkGroups)}">
<div class="links-group" th:each="group : ${site.linkGroups}">
<span class="group-name" th:text="${group.groupName}">分组</span>
<a th:each="link : ${group.links}" th:href="${link.linkUrl}" target="_blank" rel="noopener">
<img th:if="${link.logoUrl != null and !#strings.isEmpty(link.logoUrl)}" class="link-logo"
th:src="${url.upload(link.logoUrl)}" th:alt="${link.name}" loading="lazy">
<span th:text="${link.name}">链接</span>
</a>
</div>
</div>
<div class="footer-info">
<p th:if="${site.config['contact'] != null}" th:text="${site.config['contact']}">联系方式</p>
<p th:if="${site.config['copyright'] != null}" th:text="${site.config['copyright']}">版权信息</p>
<p th:if="${site.config['beian'] != null}" th:text="${site.config['beian']}">备案号</p>
</div>
</footer>
</html>
@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<!-- 公共 head 片段:SEO(title/description/canonical)+ 主题资产(方案 §8.3/§8.6) -->
<head th:fragment="head">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:text="${pageTitle}">页面标题</title>
<meta name="description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}"
th:content="${seoDescription}">
<link rel="canonical" th:if="${canonical != null}" th:href="${canonical}">
<!-- og 标签基本三件(C-7,方案 §8.6 canonical/og 同基准) -->
<meta property="og:title" th:if="${pageTitle != null}" th:content="${pageTitle}">
<meta property="og:description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}" th:content="${seoDescription}">
<meta property="og:image" th:if="${ogImage != null}" th:content="${ogImage}">
<link rel="stylesheet" th:href="${url.asset('css/main.css')}">
<!-- 微调参数 CSS 变量覆盖(方案 §5.3):位于皮肤 CSS 之后层叠覆盖;值由服务端预构建并校验 -->
<style th:if="${not #strings.isEmpty(themeVarsCss)}" th:utext="${themeVarsCss}"></style>
<!-- 站点级自定义样式(B3):位于皮肤 CSS 与微调变量之后层叠覆盖;内容经服务端黑名单强校验后直出 -->
<style th:if="${not #strings.isEmpty(customCss)}" th:utext="${customCss}"></style>
<!-- favicon:管理端配置了 favicon 键用上传图,未配置输出主题内置兜底 icon(assets/favicon.png) -->
<link rel="icon" type="image/png"
th:href="${site.config['favicon'] != null ? url.upload(site.config['favicon']) : url.asset('favicon.png')}">
</head>
</html>
@@ -1,55 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共头部:站点名 + 栏目导航 + 站内搜索 + 专业院系下拉(院系子站切换,《院系子站方案》) -->
<header class="site-header" th:fragment="header"
th:classappend="${themeParams['headerStyle'] == 'plain'} ? 'header-plain'">
<div class="header-inner">
<div class="site-brand">
<img th:if="${site.config['logo'] != null}" class="site-logo"
th:src="${url.upload(site.config['logo'])}" th:alt="${site.name}">
<a class="site-name" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
</div>
<form class="site-search" th:action="${url.search('', null)}" method="get">
<input type="text" name="q" placeholder="站内搜索" aria-label="站内搜索">
<button type="submit">搜索</button>
</form>
<!-- 专业院系下拉:顶层站取其子站、院系子站取兄弟站(含自身);无子站不渲染;文案经 config_json.colleges_label 覆盖 -->
<div class="site-colleges" th:if="${!site.colleges.isEmpty()}"
th:with="collegesLabel=${site.config['colleges_label'] ?: '专业院系'}">
<span class="colleges-toggle">
<a href="javascript:void(0)" th:text="${collegesLabel}">专业院系</a>
<i class="colleges-arrow">▾</i>
</span>
<ul class="colleges-menu">
<li th:each="col : ${site.colleges}" th:classappend="${col.path == site.path} ? 'active'">
<a th:href="${url.siteHome(col.path, col.publicBaseUrl)}"
th:title="${col.name}" th:text="${col.name}">学院名称</a>
</li>
</ul>
</div>
</div>
<nav class="site-nav">
<ul>
<li th:classappend="${currentCategoryId == null and navServiceActive == null} ? 'active'">
<a th:href="${url.home()}">首页</a>
</li>
<li th:each="cat : ${navCategories}"
th:classappend="${((currentCategoryId == cat.id or navTopId == cat.id) ? 'active' : '') + (!cat.children.isEmpty() ? ' has-sub' : '')}">
<!-- 外链栏目直接外跳;其余进栏目页(方案 §8.1) -->
<a th:if="${cat.type == 3}" th:href="${cat.linkUrl}" target="_blank" rel="noopener"
th:text="${cat.name}">栏目</a>
<a th:unless="${cat.type == 3}" th:href="${url.category(cat.id)}" th:text="${cat.name}">栏目</a>
<!-- 二级下拉:悬停展开(样式见 assets/css/main.css .sub-nav) -->
<ul class="sub-nav" th:if="${!cat.children.isEmpty()}">
<li th:each="sub : ${cat.children}"
th:classappend="${sub.id == currentCategoryId} ? 'active'">
<a th:if="${sub.type == 3}" th:href="${sub.linkUrl}" target="_blank" rel="noopener"
th:text="${sub.name}">子栏目</a>
<a th:unless="${sub.type == 3}" th:href="${url.category(sub.id)}" th:text="${sub.name}">子栏目</a>
</li>
</ul>
</li>
</ul>
</nav>
</header>
</html>
@@ -1,68 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="home-main">
<!-- 首页轮播位(cms_banner position=home;设计尺寸约定 1920×560,见 assets/css 注释) -->
<section class="banner" th:if="${!#lists.isEmpty(banners)}">
<div class="banner-slides">
<a class="banner-slide" th:each="b, stat : ${banners}"
th:classappend="${stat.first} ? 'active'"
th:href="${b.linkUrl}" th:title="${b.title}">
<img th:src="${b.imageUrl}" th:alt="${b.title}">
<span class="banner-title" th:text="${b.title}">标题</span>
</a>
</div>
<div class="banner-dots" th:if="${#lists.size(banners) > 1}">
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
th:aria-label="${'第' + (stat.count) + '张'}"></button>
</div>
</section>
<!-- 首页布局模式(docs/16 §4.4):home_layout 落库后按区块序列渲染(common 兜底骨架,本主题不另设形态) -->
<th:block th:if="${layoutBlocks != null}">
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: style}" />
<th:block th:each="block : ${layoutBlocks}">
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: render(${block})}" />
</th:block>
</th:block>
<!-- 回落分支(docs/16 D2):无 home_layout 时按现状渲染 -->
<th:block th:unless="${layoutBlocks != null}">
<!-- 置顶/头条区(cms_article topmost 优先) -->
<section class="top-strip" th:if="${!#lists.isEmpty(topArticles)}">
<h2 class="section-title">头条推荐</h2>
<ul>
<li th:each="item : ${topArticles}">
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
</section>
<!-- 一级栏目区块(每栏目 top 8) -->
<section class="category-block" th:each="block : ${categoryBlocks}">
<div class="block-head">
<h2 th:text="${block.category.name}">栏目名</h2>
<a class="more" th:href="${block.moreUrl}">更多 &gt;</a>
</div>
<ul class="block-list" th:if="${!#lists.isEmpty(block.items)}">
<li th:each="item : ${block.items}">
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(block.items)}">暂无内容</p>
</section>
</th:block>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,68 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="list-main" th:classappend="${sideNav != null} ? 'has-sidenav'">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="side-nav-list">
<li class="side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="list-body">
<nav class="breadcrumb" th:if="${searchMode != true}">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node, st : ${categoryPath}">
<a th:if="${!st.last}" class="crumb-parent" th:href="${url.category(node.id)}"
th:text="${node.name}">父栏目</a>
<span th:if="${st.last}" th:text="${node.name}">栏目</span>
</th:block>
</nav>
<nav class="breadcrumb" th:if="${searchMode == true}">
<a th:href="${url.home()}">首页</a>
<span>搜索结果</span>
</nav>
<h1 class="page-title" th:if="${searchMode != true}" th:text="${category.name}">栏目名</h1>
<h1 class="page-title" th:if="${searchMode == true}" th:text="'搜索:' + ${q}">搜索词</h1>
<ul class="article-list">
<li th:each="item : ${listData.items}">
<a class="item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<p class="item-summary" th:if="${!#strings.isEmpty(item.summary)}" th:text="${item.summary}">摘要</p>
<div class="item-meta">
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
</div>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(listData.items)}">暂无内容</p>
<!-- 分页(列表页/搜索页共用) -->
<nav class="pagination" th:if="${listData.totalPages > 1}"
th:with="cur=${listData.page}, total=${listData.totalPages}">
<a th:if="${cur > 1}"
th:href="${searchMode == true} ? ${url.search(q, cur - 1)} : ${url.category(listData.categoryId) + '?page=' + (cur - 1)}">上一页</a>
<a th:each="p : ${#numbers.sequence(1, total)}"
th:classappend="${p == cur} ? 'active'"
th:href="${searchMode == true} ? ${url.search(q, p)} : ${url.category(listData.categoryId) + '?page=' + p}"
th:text="${p}">1</a>
<a th:if="${cur < total}"
th:href="${searchMode == true} ? ${url.search(q, cur + 1)} : ${url.category(listData.categoryId) + '?page=' + (cur + 1)}">下一页</a>
</nav>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,41 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main" th:classappend="${sideNav != null} ? 'has-sidenav'">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="side-nav-list">
<li class="side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="list-body">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node, st : ${categoryPath}">
<a th:if="${!st.last}" class="crumb-parent" th:href="${url.category(node.id)}"
th:text="${node.name}">父栏目</a>
<span th:if="${st.last}" th:text="${node.name}">栏目</span>
</th:block>
</nav>
<article class="article">
<h1 class="article-title" th:text="${category.name}">栏目名</h1>
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
<div class="article-content" th:utext="${category.content}">单页内容</div>
<p class="empty" th:if="${#strings.isEmpty(category.content)}">暂无内容</p>
</article>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -1,17 +0,0 @@
{
"specVersion": 1,
"name": "classic-red",
"title": "红蓝经典",
"version": "1.0.0",
"author": "yunhai",
"description": "深红横幅头图 + 通栏轮播 + 宋体标题,适合中小学与党政风格的学校(方案 §7)。",
"screenshot": "assets/screenshot.png",
"bannerSize": "1920x560",
"params": [
{ "key": "primary", "cssVar": "--primary", "label": "主色", "type": "color", "default": "#b01a2e" },
{ "key": "primaryDark", "cssVar": "--primary-dark", "label": "主色加深", "type": "color", "default": "#7f1224" },
{ "key": "bg", "cssVar": "--bg", "label": "页面底色", "type": "color", "default": "#f7f5f2" },
{ "key": "headerStyle", "label": "头部样式", "type": "select", "default": "banner",
"options": [ { "value": "banner", "label": "横幅头图" }, { "value": "plain", "label": "简洁顶栏" } ] }
]
}
@@ -4,7 +4,7 @@
"title": "高校蓝(默认)", "title": "高校蓝(默认)",
"version": "1.0.0", "version": "1.0.0",
"author": "yunhai", "author": "yunhai",
"description": "高校蓝主题:顶栏 + 通栏轮播布局,现行基准皮肤(方案 §7)。", "description": "高校蓝主题:顶栏 + 通栏轮播布局,现行基准皮肤(方案 §7)。 docs/20 E:色皮档 4 套(blue-white/classic-red/fresh-mint/warm-kinder)并入为 colorScheme 预设。",
"screenshot": "assets/screenshot.png", "screenshot": "assets/screenshot.png",
"bannerSize": "1920x560", "bannerSize": "1920x560",
"params": [ "params": [
@@ -128,6 +128,60 @@
"label": "全屏搜索浮层(右上放大镜唤起)" "label": "全屏搜索浮层(右上放大镜唤起)"
} }
] ]
},
{
"key": "colorScheme",
"label": "配色方案",
"type": "select",
"default": "custom",
"options": [
{
"value": "custom",
"label": "自定义(上方色号自由调)"
},
{
"value": "blue-white",
"label": "蓝白经典(原 blue-white)"
},
{
"value": "classic-red",
"label": "典雅红(原 classic-red)"
},
{
"value": "fresh-mint",
"label": "清新薄荷(原 fresh-mint)"
},
{
"value": "warm-kinder",
"label": "暖阳幼教(原 warm-kinder)"
}
],
"presets": {
"blue-white": {
"--primary": "#2f6bd8",
"--primary-dark": "#1d4fae",
"--bg": "#f7f9fc",
"--border": "#e3e9f2"
},
"classic-red": {
"--primary": "#b01a2e",
"--primary-dark": "#7f1224",
"--bg": "#f7f5f2"
},
"fresh-mint": {
"--primary": "#17a2a2",
"--primary-dark": "#0f7c7c",
"--bg": "#f5fbfa",
"--border": "#dcedeb"
},
"warm-kinder": {
"--primary": "#e8722a",
"--primary-dark": "#c25a1a",
"--bg": "#fff8f0",
"--card-bg": "#fffdf9",
"--border": "#f3e3d3"
}
}
} }
] ]
} }
@@ -1,36 +0,0 @@
# fresh-mint 皮肤包
由 blue-white 派生的清新薄荷风皮肤(**卡片封面版**,docs/10 契约 §5 工作流)。**升级随版本发布,不在站点间复制目录**。
## 风格定位
清爽、亲和,适合职业院校门户:主色薄荷青、圆角 12px、卡片纯白+柔和阴影、细线吸顶顶栏+居中 logo、列表卡片网格(一行 3 张)支持封面图、几何圆点装饰与大留白;无纹理、无通栏大图、无复杂动效。
与 `fresh-mint`(同色系基础版)的差异:列表为带封面的卡片网格(`listLayout=grid`,`decoration=dots`),header 为 3 列网格(左:专业院系 / 中:品牌 / 右:搜索);`fresh-mint` 采用 top-bar + brand-row 顶栏与纯文字列表卡片。
## 数据契约(模板可取用的 model 属性全集,契约 §3)
| 页面 | 属性 |
| --- | --- |
| 全部 | `site`(快照:config/分类树/轮播/友链组/院系子站)、`url`(PortalUrlHelper:home/category/article/upload/asset/search/siteHome)、`themeBase`(=`themes/fresh-mint`)、`navCategories`、`pageTitle`、`seoDescription`、`canonical`、`themeParams`(微调参数,仅 theme.json 声明过的 key)、`themeVarsCss`(服务端预构建 `:root{}` 覆盖串,无参数为空) |
| home | `banners`(时间窗内 home 位)、`topArticles`(置顶+最新 6)、`categoryBlocks`(LIST 型根栏目块,各 top 8) |
| list / search | `listData`(分页 + `items[].coverImage`)、`category`(list)、`q` + `searchMode`(search) |
| detail | `article`、`prevArticle`、`nextArticle`、`currentCategoryId` |
| single | `category`(含 `content`)、`currentCategoryId` |
原则:皮肤不得假设"首页一定有轮播"、"列表一定有封面",对空集合优雅降级。
## 轮播图尺寸约定(bannerSize)
1920x560(管理端上传端按此引导裁剪)。
## 可调参数
见 `theme.json` 的 `params`:
- **color**(进 CSS 变量):`primary` 主色 / `primaryDark` 主色加深(用于品牌文字、悬停加深)/ `primarySoft` 主色浅调(装饰、点缀背景)/ `bg` 页面底色 / `border` 边框色。
- **select**:`listLayout` = `grid`(默认,卡片网格一行 3 张,支持封面)或 `list`(简约列表,封面靠左缩略);`decoration` = `dots`(默认,背景几何圆点)或 `plain`(纯净无装饰)。
换色建议:主色系三值同步调整(primary / primaryDark / primarySoft),bg 与 border、primarySoft 保持同族浅色调,避免底色与卡片对比失衡。
优先级:`custom_css` > `themeVarsCss` > `main.css`。
@@ -1,650 +0,0 @@
/* ============================================================
* fresh-mint 皮肤包样式(blue-white 派生,docs/10 皮肤包契约 §5)
* 清新薄荷风(卡片封面版):主色 #17A2A2、浅薄荷底、白卡 12px 圆角 + 柔和阴影;
* 细线吸顶顶栏 + 居中 logo;列表为带封面的卡片网格(一行 3 张);几何圆点装饰。
* 无纹理、无通栏大图、无复杂动效。
* 轮播图设计尺寸约定:1920×560(管理端上传端按此引导裁剪)。
* ============================================================ */
:root {
--primary: #17A2A2;
--primary-dark: #0F7C7C;
--primary-soft: #E6F6F6;
--bg: #F5FBFA;
--card-bg: #FFFFFF;
--border: #DCEDEB;
--text: #1F2D2D;
--text-light: #6B8382;
--radius: 12px;
--radius-sm: 8px;
--max-width: 1200px;
--shadow-card: 0 6px 18px rgba(15, 124, 124, .06);
--shadow-hover: 0 14px 30px rgba(15, 124, 124, .13);
--shadow-pop: 0 10px 26px rgba(15, 124, 124, .12);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
font-size: 15px;
line-height: 1.7;
color: var(--text);
background-color: var(--bg);
background-image: radial-gradient(rgba(23, 162, 162, .10) 1.6px, transparent 1.6px);
background-size: 28px 28px;
background-attachment: fixed;
}
body.decor-plain { background-image: none; }
h1, h2, h3 { font-weight: 600; letter-spacing: .01em; }
a { color: inherit; text-decoration: none; }
a:hover { color: var(--primary-dark); }
img { max-width: 100%; }
time, .item-meta { color: var(--text-light); font-size: 13px; }
.empty {
color: var(--text-light);
text-align: center;
padding: 36px 0;
background: var(--card-bg);
border: 1px dashed var(--border);
border-radius: var(--radius);
}
/* ===== 头部:细线顶栏 + 居中 logo + 吸顶 ===== */
.site-header {
position: sticky;
top: 0;
z-index: 200;
background: rgba(255, 255, 255, .94);
border-bottom: 1px solid var(--border);
box-shadow: 0 2px 12px rgba(15, 124, 124, .05);
}
.header-inner {
max-width: var(--max-width);
margin: 0 auto;
padding: 12px 16px;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 16px;
}
.header-aside { display: flex; align-items: center; gap: 10px; min-width: 0; }
.header-aside-left { justify-self: start; }
.header-aside-right { justify-self: end; }
.site-brand { display: flex; align-items: center; gap: 12px; justify-self: center; }
.site-logo { height: 40px; width: auto; }
.site-name {
font-size: 22px;
font-weight: 600;
color: var(--primary-dark);
letter-spacing: .02em;
white-space: nowrap;
}
.site-search { display: flex; gap: 8px; }
.site-search input {
border: 1px solid var(--border);
background: var(--primary-soft);
border-radius: 999px;
padding: 8px 16px;
width: 190px;
font-size: 14px;
color: var(--text);
outline: none;
}
.site-search input::placeholder { color: #9bb6b5; }
.site-search input:focus { border-color: var(--primary); background: #fff; }
.site-search button {
border: none;
border-radius: 999px;
background: var(--primary);
color: #fff;
font-size: 14px;
padding: 8px 20px;
cursor: pointer;
}
.site-search button:hover { background: var(--primary-dark); color: #fff; }
/* ===== 专业院系下拉(院系子站切换,《院系子站方案》) ===== */
.site-colleges { position: relative; }
.site-colleges .colleges-toggle {
display: flex;
align-items: center;
gap: 4px;
border: 1px solid var(--border);
border-radius: 999px;
background: #fff;
padding: 7px 16px;
cursor: pointer;
color: var(--primary-dark);
font-size: 14px;
font-weight: 500;
white-space: nowrap;
}
.site-colleges .colleges-toggle:hover { border-color: var(--primary); background: var(--primary-soft); }
.site-colleges .colleges-arrow { font-style: normal; font-size: 12px; color: var(--text-light); }
.site-colleges .colleges-menu {
display: none;
position: absolute;
top: calc(100% + 8px);
right: 0;
min-width: 200px;
max-height: 420px;
overflow-y: auto;
margin: 0;
padding: 6px;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-pop);
z-index: 210;
}
.site-colleges:hover .colleges-menu { display: block; }
.site-colleges .colleges-menu li a {
display: block;
padding: 9px 14px;
border-radius: var(--radius-sm);
color: var(--text);
white-space: nowrap;
}
.site-colleges .colleges-menu li a:hover { background: var(--primary-soft); color: var(--primary-dark); }
.site-colleges .colleges-menu li.active a { color: var(--primary-dark); font-weight: 600; background: var(--primary-soft); }
/* ===== 主导航:细线文字导航 + 薄荷下划线激活态 ===== */
.site-nav { background: transparent; border-top: 1px solid var(--border); }
.site-nav ul {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 16px;
list-style: none;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.site-nav > ul > li { position: relative; }
.site-nav li > a {
display: block;
padding: 13px 22px;
color: var(--text);
font-weight: 500;
position: relative;
}
.site-nav > ul > li > a::before {
content: "";
position: absolute;
left: 22px;
right: 22px;
bottom: 8px;
height: 2px;
border-radius: 2px;
background: var(--primary);
transform: scaleX(0);
transition: transform .18s ease;
}
.site-nav > ul > li:hover > a::before,
.site-nav > ul > li.active > a::before { transform: scaleX(1); }
.site-nav > ul > li:hover > a,
.site-nav > ul > li.active > a { color: var(--primary-dark); }
.site-nav li.has-sub > a::after { content: " ▾"; font-size: 12px; opacity: .7; }
/* ===== 二级下拉导航 ===== */
.site-nav ul.sub-nav {
display: none;
position: absolute;
top: 100%;
left: 0;
min-width: 178px;
max-width: none;
margin: 0;
padding: 6px;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-pop);
z-index: 205;
}
.site-nav li:hover > .sub-nav { display: block; }
.site-nav .sub-nav li a {
padding: 9px 16px;
border-radius: var(--radius-sm);
color: var(--text);
white-space: nowrap;
background: none;
}
.site-nav .sub-nav li a::before { display: none; }
.site-nav .sub-nav li a:hover { background: var(--primary-soft); color: var(--primary-dark); }
.site-nav .sub-nav li.active a { color: var(--primary-dark); font-weight: 600; }
/* ===== 首页轮播(收窄 + 圆角,不做通栏大图) ===== */
.banner {
position: relative;
max-width: var(--max-width);
margin: 24px auto 4px;
padding: 0 16px;
}
.banner-slides {
position: relative;
height: 380px;
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow-card);
}
.banner-slide { display: none; position: absolute; inset: 0; }
.banner-slide.active { display: block; }
.banner-slide img { width: 100%; height: 100%; object-fit: cover; }
.banner-title {
position: absolute;
left: 16px;
right: auto;
bottom: 16px;
max-width: 68%;
padding: 8px 18px;
border-radius: 999px;
background: rgba(255, 255, 255, .92);
color: var(--text);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.banner-dots {
position: absolute;
bottom: 24px;
right: 32px;
display: flex;
gap: 8px;
}
.banner-dots button {
width: 9px;
height: 9px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, .75);
cursor: pointer;
}
.banner-dots button.active { background: var(--primary); transform: scale(1.25); }
/* ===== 通用区块:白卡 + 柔和阴影 ===== */
.top-strip, .category-block {
max-width: var(--max-width);
margin: 20px auto;
padding: 22px 24px 24px;
background: var(--card-bg);
border: none;
border-radius: var(--radius);
box-shadow: var(--shadow-card);
}
.section-title, .block-head h2 {
margin: 0 0 16px;
font-size: 19px;
font-weight: 600;
display: flex;
align-items: center;
}
.section-title::before, .block-head h2::before {
content: "";
flex: none;
width: 7px;
height: 7px;
margin-right: 26px;
border-radius: 50%;
background: var(--primary);
box-shadow: 12px 0 0 rgba(23, 162, 162, .32), 24px 0 0 rgba(23, 162, 162, .14);
}
.block-head { display: flex; align-items: baseline; justify-content: space-between; }
.more {
font-size: 13px;
color: var(--primary-dark);
background: var(--primary-soft);
border-radius: 999px;
padding: 4px 14px;
flex: none;
}
/* 头条推荐:卡片网格(listLayout=list 时回落为行列表) */
.top-grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.top-grid li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 18px;
background: var(--primary-soft);
border-radius: var(--radius-sm);
}
.top-grid li a {
flex: 1;
font-weight: 500;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.top-grid li time { flex: none; }
.top-grid.is-list { display: block; }
.top-grid.is-list li { background: transparent; padding: 11px 0; border-bottom: 1px solid var(--border); }
.top-grid.is-list li:last-child { border-bottom: none; }
/* 栏目区块:行列表 + 圆点标记 */
.block-list { list-style: none; margin: 0; padding: 0; }
.block-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 11px 0;
border-bottom: 1px solid var(--border);
}
.block-list li:last-child { border-bottom: none; }
.block-list li a {
flex: 1;
display: flex;
align-items: center;
gap: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.block-list li a::before {
content: "";
flex: none;
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--primary);
opacity: .5;
}
.top-badge {
flex: none;
color: #fff;
background: var(--primary);
font-size: 12px;
font-weight: 500;
padding: 2px 10px;
border-radius: 999px;
}
/* ===== 列表页:卡片网格(一行三张,支持封面) ===== */
.list-main {
max-width: var(--max-width);
margin: 0 auto;
padding: 24px 16px 48px;
background: transparent;
border: none;
}
.home-main { padding-bottom: 8px; }
.breadcrumb { color: var(--text-light); font-size: 13px; margin: 0 0 14px; }
.breadcrumb a { margin-right: 8px; }
.breadcrumb span { margin-left: 8px; color: var(--primary-dark); }
.page-title {
font-size: 24px;
font-weight: 600;
margin: 0 0 22px;
padding-bottom: 14px;
border-bottom: 1px solid var(--border);
}
.article-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.article-list li {
background: var(--card-bg);
border-radius: var(--radius);
box-shadow: var(--shadow-card);
overflow: hidden;
transition: transform .2s ease, box-shadow .2s ease;
}
.article-list li:hover { transform: translateY(-4px); box-shadow: var(--shadow-hover); }
.item-card { display: flex; flex-direction: column; height: 100%; }
.item-cover {
display: block;
aspect-ratio: 16 / 9;
overflow: hidden;
background: var(--primary-soft);
}
.item-cover img { display: block; width: 100%; height: 100%; object-fit: cover; }
.item-body { display: flex; flex-direction: column; gap: 8px; padding: 16px 18px 18px; flex: 1; }
.item-title {
font-size: 16px;
font-weight: 600;
line-height: 1.5;
color: var(--text);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.item-summary {
color: var(--text-light);
font-size: 14px;
line-height: 1.7;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.item-meta { display: flex; gap: 10px; align-items: center; margin-top: auto; padding-top: 4px; }
.article-list.is-list { display: block; }
.article-list.is-list li { margin-bottom: 14px; }
.article-list.is-list .item-card { flex-direction: row; align-items: center; gap: 18px; padding: 16px 20px; }
.article-list.is-list .item-cover { width: 190px; flex: none; aspect-ratio: 16 / 10; border-radius: var(--radius-sm); }
.article-list.is-list .item-body { padding: 0; }
.article-list.is-list .item-meta { margin-top: 4px; }
/* ===== 分页 ===== */
.pagination { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; padding: 36px 0 8px; }
.pagination a {
min-width: 36px;
text-align: center;
padding: 7px 14px;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--card-bg);
color: var(--text-light);
}
.pagination a:hover { border-color: var(--primary); color: var(--primary-dark); }
.pagination a.active { background: var(--primary); color: #fff; border-color: var(--primary); }
/* ===== 详情 / 单页 ===== */
.detail-main {
max-width: 1000px;
margin: 24px auto 48px;
padding: 36px 40px 40px;
background: var(--card-bg);
border: none;
border-radius: var(--radius);
box-shadow: var(--shadow-card);
}
.article-title { margin: 8px 0 14px; font-size: 27px; font-weight: 600; text-align: center; line-height: 1.45; }
.article-meta {
display: flex;
gap: 0;
justify-content: center;
flex-wrap: wrap;
color: var(--text-light);
font-size: 13px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border);
}
.article-meta > * { padding: 0 12px; position: relative; }
.article-meta > * + *::before {
content: "";
position: absolute;
left: 0;
top: 50%;
width: 4px;
height: 4px;
margin-top: -2px;
border-radius: 50%;
background: var(--primary);
opacity: .5;
}
.article-content { line-height: 1.95; font-size: 16px; padding-top: 24px; word-break: break-word; color: var(--text); }
.article-content p { margin: 0 0 16px; }
.article-content h1, .article-content h2, .article-content h3 { margin: 26px 0 12px; }
.article-content img { height: auto; border-radius: var(--radius-sm); }
.article-content a { color: var(--primary-dark); text-decoration: underline; text-underline-offset: 3px; }
.article-content blockquote {
margin: 18px 0;
padding: 12px 18px;
background: var(--primary-soft);
border-left: 3px solid var(--primary);
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.article-content table { border-collapse: collapse; width: 100%; }
.article-content table td, .article-content table th { border: 1px solid var(--border); padding: 8px 12px; }
.article-sibling { display: flex; flex-direction: column; gap: 10px; margin-top: 32px; font-size: 14px; }
.article-sibling a {
padding: 12px 18px;
border-radius: var(--radius-sm);
background: var(--card-bg);
border: 1px solid var(--border);
color: var(--text-light);
}
.article-sibling a:hover { border-color: var(--primary); color: var(--primary-dark); }
/* ===== 页脚(浅薄荷底) ===== */
.site-footer {
margin-top: 40px;
background: var(--primary-soft);
color: var(--text-light);
border-top: 1px solid var(--border);
}
.footer-decor {
height: 26px;
display: flex;
justify-content: center;
align-items: center;
}
.footer-decor::before {
content: "";
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--primary);
box-shadow: 14px 0 0 rgba(23, 162, 162, .32), -14px 0 0 rgba(23, 162, 162, .32);
}
.footer-links {
max-width: var(--max-width);
margin: 0 auto;
padding: 8px 16px 4px;
display: flex;
flex-direction: column;
gap: 10px;
}
.links-group { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; font-size: 13px; }
.group-name {
font-weight: 600;
color: var(--primary-dark);
margin-right: 6px;
padding-right: 16px;
position: relative;
}
.group-name::after {
content: "";
position: absolute;
right: 0;
top: 50%;
width: 4px;
height: 4px;
margin-top: -2px;
border-radius: 50%;
background: var(--primary);
opacity: .5;
}
.links-group a { color: var(--text-light); }
.links-group a:hover { color: var(--primary-dark); }
.footer-info {
max-width: var(--max-width);
margin: 0 auto;
padding: 14px 16px 26px;
text-align: center;
font-size: 13px;
}
.footer-info p { margin: 4px 0; }
.links-group a .link-logo,
.sp-links-row a .link-logo {
height: 20px;
width: auto;
max-width: 64px;
object-fit: contain;
vertical-align: middle;
margin-right: 4px;
border-radius: 4px;
}
/* ===== 移动端适配 ===== */
@media (max-width: 768px) {
.header-inner { grid-template-columns: 1fr; justify-items: center; gap: 12px; padding: 12px 14px; }
.site-brand { order: 1; justify-self: center; }
.header-aside-right { order: 2; justify-self: stretch; }
.header-aside-left { order: 3; justify-self: center; }
.site-search { flex: 1; }
.site-search input { flex: 1; width: auto; min-width: 0; }
.site-colleges .colleges-menu { left: 50%; right: auto; transform: translateX(-50%); min-width: 200px; }
.site-nav ul { flex-wrap: nowrap; overflow-x: auto; justify-content: flex-start; }
.site-nav > ul > li > a { padding: 12px 16px; white-space: nowrap; }
.site-nav > ul > li > a::before { left: 16px; right: 16px; }
.banner { margin-top: 16px; }
.banner-slides { height: 190px; }
.banner-title { font-size: 13px; padding: 6px 14px; }
.banner-dots { bottom: 14px; right: 20px; }
.top-grid { grid-template-columns: 1fr; }
.article-list { grid-template-columns: minmax(0, 1fr); }
.article-list.is-list .item-card { flex-direction: column; align-items: stretch; }
.article-list.is-list .item-cover { width: 100%; aspect-ratio: 16 / 9; }
.top-strip, .category-block { padding: 18px 16px 20px; margin: 16px auto; }
.detail-main { margin: 16px auto 32px; padding: 22px 18px 26px; }
.article-title { font-size: 22px; }
.article-content { font-size: 15px; }
.site-nav ul.sub-nav {
display: block;
position: static;
border: none;
border-radius: var(--radius-sm);
box-shadow: none;
background: var(--primary-soft);
min-width: 0;
padding: 6px;
}
.site-nav .sub-nav li a { color: var(--text); padding: 8px 14px; }
}
/* ===== 左侧栏目导航(docs/11):有二级节点的模块,栏目/文章页左侧展示兄弟目录 ===== */
.list-main.has-sidenav, .detail-main.has-sidenav { display: flex; align-items: flex-start; gap: 24px; }
.has-sidenav .list-body { flex: 1; min-width: 0; }
.side-nav { flex: none; width: 220px; background: var(--card-bg, #fff); border: 1px solid var(--border); overflow: hidden; }
.side-nav-head { background: var(--primary); color: #fff; font-size: 20px; font-weight: 700; text-align: center; padding: 18px 12px; letter-spacing: 2px; }
.side-nav-list { list-style: none; margin: 0; padding: 0; }
.side-nav-item + .side-nav-item { border-top: 1px dashed var(--border); }
.side-nav-item a { display: block; padding: 12px 16px; color: var(--text); text-decoration: none; font-size: 15px; transition: background .15s ease, color .15s ease; }
.side-nav-item a:hover { color: var(--primary); background: var(--bg); }
.side-nav-item.active a { background: var(--primary); color: #fff; font-weight: 600; text-decoration: none; }
/* 顶导航二级下拉当前项高亮(docs/11 顺带) */
.site-nav ul.sub-nav li.active a { color: var(--primary); }
@media (max-width: 768px) {
.list-main.has-sidenav, .detail-main.has-sidenav { flex-direction: column; gap: 12px; }
.side-nav { width: 100%; }
.side-nav-list { display: flex; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.side-nav-item { flex: none; }
.side-nav-item + .side-nav-item { border-top: none; border-left: 1px dashed var(--border); }
.side-nav-item a { white-space: nowrap; }
}
@media print {
.side-nav { display: none; }
.list-main.has-sidenav, .detail-main.has-sidenav { display: block; }
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs><linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#5486df"/><stop offset="1" stop-color="#214b97"/>
</linearGradient></defs>
<rect width="64" height="64" fill="url(#bg)"/>
<circle cx="32" cy="25" r="10" fill="#ffffff"/>
<path d="M 16 49.3 Q 32 30.7 48 49.3 Z" fill="#ffffff" opacity="0.92"/>
</svg>

Before

Width:  |  Height:  |  Size: 434 B

@@ -1,40 +0,0 @@
/* fresh-mint 皮肤包脚本:仅首页轮播自动播放与圆点切换,无任何外部依赖 */
(function () {
var slides = document.querySelectorAll('.banner-slide');
var dots = document.querySelectorAll('.banner-dots button');
if (!slides.length) {
return;
}
var current = 0;
var timer = null;
function show(index) {
slides[current].classList.remove('active');
if (dots[current]) {
dots[current].classList.remove('active');
}
current = (index + slides.length) % slides.length;
slides[current].classList.add('active');
if (dots[current]) {
dots[current].classList.add('active');
}
}
function start() {
if (slides.length > 1) {
timer = window.setInterval(function () {
show(current + 1);
}, 5000);
}
}
dots.forEach(function (dot, index) {
dot.addEventListener('click', function () {
window.clearInterval(timer);
show(index);
start();
});
});
start();
})();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

@@ -1,58 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body th:classappend="${themeParams['decoration'] == 'plain'} ? 'decor-plain'">
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main" th:classappend="${sideNav != null} ? 'has-sidenav'">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="side-nav-list">
<li class="side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="list-body">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node : ${categoryPath}">
<a class="crumb-parent" th:href="${url.category(node.id)}" th:text="${node.name}">栏目</a>
</th:block>
<span>正文</span>
</nav>
<article class="article">
<h1 class="article-title" th:text="${article.title}">文章标题</h1>
<div class="article-meta">
<time th:if="${article.publishTime != null}"
th:text="${'发布时间:' + #temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
<span th:if="${!#strings.isEmpty(article.authorName)}"
th:text="'作者:' + ${article.authorName}">作者</span>
<span th:if="${!#strings.isEmpty(article.source)}"
th:text="'来源:' + ${article.source}">来源</span>
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
</div>
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
其余字段一律 th:text 转义输出 -->
<div class="article-content" th:utext="${article.content}">正文</div>
</article>
<nav class="article-sibling">
<a th:if="${prevArticle != null}" class="prev"
th:href="${url.article(prevArticle.id)}"
th:text="'上一篇:' + ${prevArticle.title}">上一篇</a>
<a th:if="${nextArticle != null}" class="next"
th:href="${url.article(nextArticle.id)}"
th:text="'下一篇:' + ${nextArticle.title}">下一篇</a>
</nav>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共页脚:浅薄荷底 + 圆点装饰 + 友情链接(分组渲染,方案 §8.1:友链不设独立路由) -->
<footer class="site-footer" th:fragment="footer">
<!-- 纯装饰几何圆点(无数据绑定,aria-hidden) -->
<div class="footer-decor" aria-hidden="true"></div>
<div class="footer-links" th:if="${!#lists.isEmpty(site.linkGroups)}">
<div class="links-group" th:each="group : ${site.linkGroups}">
<span class="group-name" th:text="${group.groupName}">分组</span>
<a th:each="link : ${group.links}" th:href="${link.linkUrl}" target="_blank" rel="noopener">
<img th:if="${link.logoUrl != null and !#strings.isEmpty(link.logoUrl)}" class="link-logo"
th:src="${url.upload(link.logoUrl)}" th:alt="${link.name}" loading="lazy">
<span th:text="${link.name}">链接</span>
</a>
</div>
</div>
<div class="footer-info">
<p th:if="${site.config['contact'] != null}" th:text="${site.config['contact']}">联系方式</p>
<p th:if="${site.config['copyright'] != null}" th:text="${site.config['copyright']}">版权信息</p>
<p th:if="${site.config['beian'] != null}" th:text="${site.config['beian']}">备案号</p>
</div>
</footer>
</html>
@@ -1,25 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<!-- 公共 head 片段:SEO(title/description/canonical)+ 主题资产(方案 §8.3/§8.6) -->
<head th:fragment="head">
<meta name="theme-color" content="#17A2A2">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:text="${pageTitle}">页面标题</title>
<meta name="description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}"
th:content="${seoDescription}">
<link rel="canonical" th:if="${canonical != null}" th:href="${canonical}">
<!-- og 标签基本三件(C-7,方案 §8.6 canonical/og 同基准) -->
<meta property="og:title" th:if="${pageTitle != null}" th:content="${pageTitle}">
<meta property="og:description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}" th:content="${seoDescription}">
<meta property="og:image" th:if="${ogImage != null}" th:content="${ogImage}">
<link rel="stylesheet" th:href="${url.asset('css/main.css')}">
<!-- 微调参数 CSS 变量覆盖(方案 §5.3):位于皮肤 CSS 之后层叠覆盖;值由服务端预构建并校验 -->
<style th:if="${not #strings.isEmpty(themeVarsCss)}" th:utext="${themeVarsCss}"></style>
<!-- 站点级自定义样式(B3):位于皮肤 CSS 与微调变量之后层叠覆盖;内容经服务端黑名单强校验后直出 -->
<style th:if="${not #strings.isEmpty(customCss)}" th:utext="${customCss}"></style>
<!-- favicon:管理端配置了 favicon 键用上传图,未配置输出主题内置兜底 icon(assets/favicon.png) -->
<link rel="icon" type="image/png"
th:href="${site.config['favicon'] != null ? url.upload(site.config['favicon']) : url.asset('favicon.png')}">
</head>
</html>
@@ -1,62 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共头部:细线顶栏 + 居中品牌行(左:专业院系 / 中:品牌 / 右:站内搜索)+ 栏目导航(《院系子站方案》) -->
<header class="site-header" th:fragment="header">
<div class="header-inner">
<!-- 左:专业院系下拉;顶层站取其子站、院系子站取兄弟站(含自身);无子站不渲染;文案经 config_json.colleges_label 覆盖 -->
<div class="header-aside header-aside-left">
<div class="site-colleges" th:if="${!site.colleges.isEmpty()}"
th:with="collegesLabel=${site.config['colleges_label'] ?: '专业院系'}">
<span class="colleges-toggle">
<a href="javascript:void(0)" th:text="${collegesLabel}">专业院系</a>
<i class="colleges-arrow">▾</i>
</span>
<ul class="colleges-menu">
<li th:each="col : ${site.colleges}" th:classappend="${col.path == site.path} ? 'active'">
<a th:href="${url.siteHome(col.path, col.publicBaseUrl)}"
th:title="${col.name}" th:text="${col.name}">学院名称</a>
</li>
</ul>
</div>
</div>
<!-- 中:居中品牌(logo + 站名) -->
<div class="site-brand">
<img th:if="${site.config['logo'] != null}" class="site-logo"
th:src="${url.upload(site.config['logo'])}" th:alt="${site.name}">
<a class="site-name" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
</div>
<!-- 右:站内搜索 -->
<div class="header-aside header-aside-right">
<form class="site-search" th:action="${url.search('', null)}" method="get">
<input type="text" name="q" placeholder="站内搜索" aria-label="站内搜索">
<button type="submit">搜索</button>
</form>
</div>
</div>
<nav class="site-nav">
<ul>
<li th:classappend="${currentCategoryId == null and navServiceActive == null} ? 'active'">
<a th:href="${url.home()}">首页</a>
</li>
<li th:each="cat : ${navCategories}"
th:classappend="${((currentCategoryId == cat.id or navTopId == cat.id) ? 'active' : '') + (!cat.children.isEmpty() ? ' has-sub' : '')}">
<!-- 外链栏目直接外跳;其余进栏目页(方案 §8.1) -->
<a th:if="${cat.type == 3}" th:href="${cat.linkUrl}" target="_blank" rel="noopener"
th:text="${cat.name}">栏目</a>
<a th:unless="${cat.type == 3}" th:href="${url.category(cat.id)}" th:text="${cat.name}">栏目</a>
<!-- 二级下拉:悬停展开(样式见 assets/css/main.css .sub-nav) -->
<ul class="sub-nav" th:if="${!cat.children.isEmpty()}">
<li th:each="sub : ${cat.children}"
th:classappend="${sub.id == currentCategoryId} ? 'active'">
<a th:if="${sub.type == 3}" th:href="${sub.linkUrl}" target="_blank" rel="noopener"
th:text="${sub.name}">子栏目</a>
<a th:unless="${sub.type == 3}" th:href="${url.category(sub.id)}" th:text="${sub.name}">子栏目</a>
</li>
</ul>
</li>
</ul>
</nav>
</header>
</html>
@@ -1,68 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body th:classappend="${themeParams['decoration'] == 'plain'} ? 'decor-plain'">
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="home-main">
<!-- 首页轮播位(cms_banner position=home;设计尺寸约定 1920×560,见 assets/css 注释) -->
<section class="banner" th:if="${!#lists.isEmpty(banners)}">
<div class="banner-slides">
<a class="banner-slide" th:each="b, stat : ${banners}"
th:classappend="${stat.first} ? 'active'"
th:href="${b.linkUrl}" th:title="${b.title}">
<img th:src="${b.imageUrl}" th:alt="${b.title}">
<span class="banner-title" th:text="${b.title}">标题</span>
</a>
</div>
<div class="banner-dots" th:if="${#lists.size(banners) > 1}">
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
th:aria-label="${'第' + (stat.count) + '张'}"></button>
</div>
</section>
<!-- 首页布局模式(docs/16 §4.4):home_layout 落库后按区块序列渲染(common 兜底骨架,本主题不另设形态) -->
<th:block th:if="${layoutBlocks != null}">
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: style}" />
<th:block th:each="block : ${layoutBlocks}">
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: render(${block})}" />
</th:block>
</th:block>
<!-- 回落分支(docs/16 D2):无 home_layout 时按现状渲染 -->
<th:block th:unless="${layoutBlocks != null}">
<!-- 置顶/头条区(cms_article topmost 优先) -->
<section class="top-strip" th:if="${!#lists.isEmpty(topArticles)}">
<h2 class="section-title">头条推荐</h2>
<ul class="top-grid" th:classappend="${themeParams['listLayout'] == 'list'} ? 'is-list'">
<li th:each="item : ${topArticles}">
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
</section>
<!-- 一级栏目区块(每栏目 top 8) -->
<section class="category-block" th:each="block : ${categoryBlocks}">
<div class="block-head">
<h2 th:text="${block.category.name}">栏目名</h2>
<a class="more" th:href="${block.moreUrl}">更多 &gt;</a>
</div>
<ul class="block-list" th:if="${!#lists.isEmpty(block.items)}">
<li th:each="item : ${block.items}">
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(block.items)}">暂无内容</p>
</section>
</th:block>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,76 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body th:classappend="${themeParams['decoration'] == 'plain'} ? 'decor-plain'">
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="list-main" th:classappend="${sideNav != null} ? 'has-sidenav'">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="side-nav-list">
<li class="side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="list-body">
<nav class="breadcrumb" th:if="${searchMode != true}">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node, st : ${categoryPath}">
<a th:if="${!st.last}" class="crumb-parent" th:href="${url.category(node.id)}"
th:text="${node.name}">父栏目</a>
<span th:if="${st.last}" th:text="${node.name}">栏目</span>
</th:block>
</nav>
<nav class="breadcrumb" th:if="${searchMode == true}">
<a th:href="${url.home()}">首页</a>
<span>搜索结果</span>
</nav>
<h1 class="page-title" th:if="${searchMode != true}" th:text="${category.name}">栏目名</h1>
<h1 class="page-title" th:if="${searchMode == true}" th:text="'搜索:' + ${q}">搜索词</h1>
<ul class="article-list" th:classappend="${themeParams['listLayout'] == 'list'} ? 'is-list'">
<li th:each="item : ${listData.items}">
<a class="item-card" th:href="${url.article(item.id)}">
<!-- 封面(契约 §3.3 listData.items[].coverImage;上传图走 url.upload) -->
<span class="item-cover" th:if="${item.coverImage != null and !#strings.isEmpty(item.coverImage)}">
<img th:src="${url.upload(item.coverImage)}" th:alt="${item.title}" loading="lazy">
</span>
<span class="item-body">
<span class="item-title" th:text="${item.title}">文章标题</span>
<span class="item-summary" th:if="${!#strings.isEmpty(item.summary)}" th:text="${item.summary}">摘要</span>
<span class="item-meta">
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
</span>
</span>
</a>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(listData.items)}">暂无内容</p>
<!-- 分页(列表页/搜索页共用) -->
<nav class="pagination" th:if="${listData.totalPages > 1}"
th:with="cur=${listData.page}, total=${listData.totalPages}">
<a th:if="${cur > 1}"
th:href="${searchMode == true} ? ${url.search(q, cur - 1)} : ${url.category(listData.categoryId) + '?page=' + (cur - 1)}">上一页</a>
<a th:each="p : ${#numbers.sequence(1, total)}"
th:classappend="${p == cur} ? 'active'"
th:href="${searchMode == true} ? ${url.search(q, p)} : ${url.category(listData.categoryId) + '?page=' + p}"
th:text="${p}">1</a>
<a th:if="${cur < total}"
th:href="${searchMode == true} ? ${url.search(q, cur + 1)} : ${url.category(listData.categoryId) + '?page=' + (cur + 1)}">下一页</a>
</nav>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -1,41 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body th:classappend="${themeParams['decoration'] == 'plain'} ? 'decor-plain'">
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main" th:classappend="${sideNav != null} ? 'has-sidenav'">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="side-nav-list">
<li class="side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="list-body">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node, st : ${categoryPath}">
<a th:if="${!st.last}" class="crumb-parent" th:href="${url.category(node.id)}"
th:text="${node.name}">父栏目</a>
<span th:if="${st.last}" th:text="${node.name}">栏目</span>
</th:block>
</nav>
<article class="article">
<h1 class="article-title" th:text="${category.name}">栏目名</h1>
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
<div class="article-content" th:utext="${category.content}">单页内容</div>
<p class="empty" th:if="${#strings.isEmpty(category.content)}">暂无内容</p>
</article>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -1,21 +0,0 @@
{
"specVersion": 1,
"name": "fresh-mint",
"title": "清新薄荷",
"version": "1.0.0",
"author": "yunhai",
"description": "清新薄荷风(卡片封面版):主色 #17A2A2、浅薄荷底、白卡 12px 圆角;细线吸顶顶栏 + 居中 logo;列表为带封面的卡片网格(一行 3 张);几何圆点装饰与大留白;适合职业院校。",
"screenshot": "assets/screenshot.png",
"bannerSize": "1920x560",
"params": [
{ "key": "primary", "cssVar": "--primary", "label": "主色(薄荷绿)", "type": "color", "default": "#17A2A2" },
{ "key": "primaryDark", "cssVar": "--primary-dark", "label": "主色加深", "type": "color", "default": "#0F7C7C" },
{ "key": "primarySoft", "cssVar": "--primary-soft", "label": "主色浅调(装饰)", "type": "color", "default": "#E6F6F6" },
{ "key": "bg", "cssVar": "--bg", "label": "页面底色", "type": "color", "default": "#F5FBFA" },
{ "key": "border", "cssVar": "--border", "label": "边框色", "type": "color", "default": "#DCEDEB" },
{ "key": "listLayout", "label": "列表形态", "type": "select", "default": "grid",
"options": [ { "value": "grid", "label": "卡片网格(一行三张)" }, { "value": "list", "label": "简约列表" } ] },
{ "key": "decoration", "label": "背景装饰", "type": "select", "default": "dots",
"options": [ { "value": "dots", "label": "几何圆点" }, { "value": "plain", "label": "纯净无装饰" } ] }
]
}
@@ -1,54 +0,0 @@
# jade-normal(师大青)
师范院校风皮肤包:以 `default` 为基座派生,设计语言参考信阳师范大学官网(www.xynu.edu.cn,仅学语言不复制实现)——
翡翠青主色 + 暖橙点缀,人文师范气质、温润轻盈。
配了 `home_layout` 的站点,首页正文区块由本主题 `fragments/home-blocks.html` 渲染——派生包不自带区块形态时,
正文会落到 `themes/common` 的通用骨架,换肤就只剩页头页脚(线上事故根因,见《审计验收清单-20260919》)。
## 风格定位
- 主色翡翠青 `#008D3F` / 加深 `#04682E` / 提亮 `#35B371`,点缀暖橙 `#F18D00`(日期徽标数字、强调角标、"我要办理"按钮)。
- 页头三段式:深青顶条(日期+专业院系)→ 白色品牌行(校徽+校名居左,服务中心入口+圆角搜索居右)→
翡翠青通栏主导航(白字,当前/悬停项 primaryLight 底条,≤768px checkbox 纯 CSS 折叠)。
- banner 标题用青色底条(非渐变遮罩);列表主打"46px 橙色日期徽标(日大号+年月小号)"语言。
- 圆角 8/10px、正文行高 1.8、hover 过渡 0.2s ease;无渐变堆砌、无玻璃拟态、无粉紫。
- 页脚:薄荷友链带 + 翡翠深青 `#04682E` 多列(品牌+社媒图标 / 多校区地址+联系版权备案 / 二维码)+ 深色底条。
- 全部 12 个颜色参数可在站点 theme_params 微调,另有 1 个 select 布局参数(见下)。
## 参数取值建议(theme_params)
| key | 默认 | 说明 |
| -- | -- | -- |
| `primary` | `#008D3F` | 翡翠青主色(导航/标题/链接) |
| `primaryDark` | `#04682E` | 加深(顶条/页脚/hover) |
| `primaryLight` | `#35B371` | 提亮(当前栏目底条/描边) |
| `accent` | `#F18D00` | 暖橙点缀(日期徽标/角标) |
| `navBg` | `#008D3F` | 主导航条底色(可与主色分离调) |
| `footerBg` | `#04682E` | 页脚主底色 |
| `text` / `textLight` | `#2E3A33` / `#6E7F75` | 正文/次要文字 |
| `bg` / `cardBg` / `border` | `#F4FAF6` / `#FFFFFF` / `#DFEEE5` | 页面底/卡片/边框 |
| `mintBg` | `#E7F6EE` | 薄荷底(背景带/友链区/hover 面) |
| `listLayout` | `badge` | 列表页形态:`badge` 橙色日期徽标行 / `plain` 通栏行式(日期右置) |
## 首页区块形态(布局模式与回落模式同构)
| homeStyle | 呈现 | 类名 |
| -- | -- | -- |
| `news_grid` | 校园要闻:左图文头条 + 右橙色日期徽标行 + 四图横排;区块背景可取站点 `news_bg_image` | `jn-split` / `jn-date-badge` / `jn-shots` |
| `compact` | 通知公告:46px「日大号+年月小号」橙色徽标行 | `jn-row-badge` |
| `media_focus` | 学术交流/媒体聚焦:来源徽标 + 双列密行 | `jn-src` / `jn-block-list--two` |
| `image_wall` | 教学科研:四列图片墙,青底标题条,hover 抬图 | `jn-wall` |
| (空/未知) | 整宽区块卡:头条 + 行式列表 | `jn-block--flow` |
| `ticker` | 翡翠青要闻速递横条(单行不换行、超长省略,中文不塌列) | `jn-ticker*` |
| `gallery` | 校园映像四图墙 + 「融媒师大」图标行(微信/微博/抖音/B站走 `social_*` 配置,校报/广播台常驻) | `jn-gallery` / `jn-fusion` |
| `links` | 快捷通道图标宫格(6 列内联 SVG 图标卡,青色描边 hover 青底白字) | `jn-quick` |
## 契约要点
- 结构类前缀 `jn-`,CSS 变量前缀 `--jn-`;`:root` 把 `--jn-*` 别名到皮肤契约变量(`--primary`/`--text`/`--card-bg`/`--border`…),
使 `themes/common` 兜底骨架也能跟随本皮肤着色。
- 空态文案「栏目暂无内容」只在 `block.items` 为空时出现;守卫必须写在 `th:block` 外层——
`th:replace`(优先级 100)先于 `th:if`(300)求值,写在同一标签上等于没写。
- 页脚消费 docs/12 增强键:`footer_addresses`(经 `site.footerAddressLines` 逐行渲染)、`qr_wechat`/`qr_weibo`、
`social_wechat`/`social_weibo`/`social_douyin`/`social_bilibili`——均有值才渲染,空站点优雅降级。
- 回归防线:`ThemeContractIT`(7 条路由);带 home_layout 的站点首页不落通用骨架(本包 home.html 分发到自家 fragment)。
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

@@ -1,88 +0,0 @@
/* jade-normal(师大青)皮肤包脚本:顶条日期 + 首页轮播(自动播放/圆点/箭头/悬停暂停/键盘),无任何外部依赖。
选择器与 main.css / home.html 的 jn-* 类名一一对应;导航折叠由 CSS(checkbox + label)承担,无需 JS。 */
(function () {
/* 顶条日期(本地时间,YYYY年M月D日 星期X) */
var dateEl = document.getElementById('siteDate');
if (dateEl) {
var now = new Date();
var week = ['日', '一', '二', '三', '四', '五', '六'][now.getDay()];
dateEl.textContent = now.getFullYear() + '年' + (now.getMonth() + 1) + '月'
+ now.getDate() + '日 星期' + week;
}
/* 轮播(.jn-banner,见 home.html) */
var root = document.querySelector('.jn-banner');
var slides = document.querySelectorAll('.jn-banner-slide');
var dots = document.querySelectorAll('.jn-banner-dots button');
var prevBtn = document.querySelector('.jn-banner-arrow.prev');
var nextBtn = document.querySelector('.jn-banner-arrow.next');
if (!slides.length || !root) {
return;
}
var current = 0;
var timer = null;
var INTERVAL = 5000;
function show(index) {
slides[current].classList.remove('active');
if (dots[current]) {
dots[current].classList.remove('active');
}
current = (index + slides.length) % slides.length;
slides[current].classList.add('active');
if (dots[current]) {
dots[current].classList.add('active');
}
}
function start() {
stop();
if (slides.length > 1) {
timer = window.setInterval(function () {
show(current + 1);
}, INTERVAL);
}
}
function stop() {
if (timer) {
window.clearInterval(timer);
timer = null;
}
}
dots.forEach(function (dot, index) {
dot.addEventListener('click', function () {
show(index);
start();
});
});
if (prevBtn) {
prevBtn.addEventListener('click', function () {
show(current - 1);
start();
});
}
if (nextBtn) {
nextBtn.addEventListener('click', function () {
show(current + 1);
start();
});
}
root.addEventListener('mouseenter', stop);
root.addEventListener('mouseleave', start);
root.addEventListener('focusin', stop);
root.addEventListener('focusout', start);
root.setAttribute('tabindex', '-1');
root.addEventListener('keydown', function (e) {
if (e.key === 'ArrowLeft') {
show(current - 1);
start();
} else if (e.key === 'ArrowRight') {
show(current + 1);
start();
}
});
start();
})();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

@@ -1,76 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="jn-detail-main jn-container" id="main-content">
<div class="jn-page-cols">
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
<aside class="jn-side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
<div class="jn-side-nav-head" th:text="${sideNav.top.name}">模块名</div>
<ul class="jn-side-nav-list">
<li class="jn-side-nav-item" th:each="item : ${sideNav.items}"
th:classappend="${item.id == sideNav.activeId} ? 'active'">
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
th:text="${item.name}">子栏目</a>
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
</li>
</ul>
</aside>
<div class="jn-page-card">
<nav class="jn-breadcrumb" aria-label="面包屑">
<a th:href="${url.home()}">首页</a>
<th:block th:each="node : ${categoryPath}">
<span class="crumb-sep" aria-hidden="true">/</span>
<a class="crumb-parent" th:href="${url.category(node.id)}" th:text="${node.name}">栏目</a>
</th:block>
<span class="crumb-sep" th:if="${currentCategoryId != null}" aria-hidden="true">/</span>
<span class="current">正文</span>
</nav>
<article class="jn-article">
<h1 class="jn-article-title" th:text="${article.title}">文章标题</h1>
<div class="jn-article-meta">
<time th:if="${article.publishTime != null}"
th:text="${'发布时间:' + #temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
<span th:if="${!#strings.isEmpty(article.authorName)}"
th:text="'作者:' + ${article.authorName}">作者</span>
<span th:if="${!#strings.isEmpty(article.source)}"
th:text="'来源:' + ${article.source}">来源</span>
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
</div>
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
其余字段一律 th:text 转义输出 -->
<div class="jn-article-content" th:utext="${article.content}">正文</div>
</article>
<nav class="jn-sibling" aria-label="上下篇导航">
<a th:if="${prevArticle != null}" class="prev"
th:href="${url.article(prevArticle.id)}">
<span class="jn-sib-label">上一篇</span>
<span class="jn-sib-title" th:text="${prevArticle.title}">上一篇标题</span>
</a>
<span class="jn-sib-none prev" th:if="${prevArticle == null}">
<span class="jn-sib-label">上一篇</span>
<span class="jn-sib-title">已经是最早一篇</span>
</span>
<a th:if="${nextArticle != null}" class="next"
th:href="${url.article(nextArticle.id)}">
<span class="jn-sib-label">下一篇</span>
<span class="jn-sib-title" th:text="${nextArticle.title}">下一篇标题</span>
</a>
<span class="jn-sib-none next" th:if="${nextArticle == null}">
<span class="jn-sib-label">下一篇</span>
<span class="jn-sib-title">已是最新一篇</span>
</span>
</nav>
</div>
</div>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More