feat(theme): 新增三套高校官网风皮肤 poly-blue/agri-forest/jade-normal
build / build-push (push) Successful in 51s

以 default 为基座(文件最全:含事项库 service/guide 两模板),设计语言与配色取自
真实高校官网 CSS(浙理工 #0558A5+橙、信阳农林 #1C6E19+麦穗金、信阳师大 #008D3F+暖橙)。
三包均自带九段 home-blocks、:root 契约变量别名、中文防塌列三件套、真实实拍截图
(1920 无头截图缩样)。

- UniversitySkinContractTest(7 用例):九段 fragment/homeStyle 分发/防塌列/变量别名/
  层序/自有截图/主色色域(蓝或绿通道主导,粉紫禁入)
- ThemeRegistryTest +哨兵:三包注册成功且主色与 default/scholar-blue 及彼此互异
- jade-normal 修复:news_grid 全宽封面变体把正文列压成 0 宽致 390px 溢出 47px,
  增 .jn-feat-a--stack 纵向变体,复测 overflowX=0
- 门禁:ThemeContractIT 14 主题×7 路由全绿;全量 mvn test 393/393 绿
This commit is contained in:
chenxing
2026-09-20 02:31:53 +08:00
parent d2df12b91e
commit 82d6ff4602
50 changed files with 9289 additions and 0 deletions
@@ -0,0 +1,257 @@
<!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>