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,148 @@
<!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="pb-home" id="main-content">
<!-- 首页轮播位(cms_banner position=home;设计尺寸 1920×560,见 theme.json bannerSize);
图上左下角标题渐变遮罩(assets/css/main.css) -->
<section class="pb-banner" th:if="${!#lists.isEmpty(banners)}" aria-label="焦点图片">
<div class="pb-banner-slides">
<a class="pb-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="pb-banner-title" th:text="${b.title}">标题</span>
</a>
</div>
<button class="pb-banner-arrow prev" type="button" aria-label="上一张" th:if="${#lists.size(banners) > 1}">‹</button>
<button class="pb-banner-arrow next" type="button" aria-label="下一张" th:if="${#lists.size(banners) > 1}">›</button>
<div class="pb-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 时按现状渲染(数据绑定与 default 同构,仅换皮) -->
<th:block th:unless="${layoutBlocks != null}">
<!-- 头条区:头条推荐(置顶优先) + 图片新闻(有封面文章) -->
<section class="pb-section pb-container" aria-label="头条与图片新闻">
<div class="pb-headline">
<div class="pb-headline-card" th:if="${!#lists.isEmpty(topArticles)}">
<div class="pb-block-head">
<h2>头条推荐</h2>
</div>
<ul class="pb-headline-list">
<li class="pb-headline-lead" th:with="lead=${topArticles[0]}">
<a class="pb-lead-title" th:href="${url.article(lead.id)}" th:text="${lead.title}">头条标题</a>
<div class="pb-lead-meta">
<span class="pb-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="pb-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="pb-focus-card" th:if="${!#lists.isEmpty(focusArticles)}">
<div class="pb-block-head">
<h2>图片新闻</h2>
</div>
<a class="pb-focus-lead" th:with="cover=${focusArticles[0]}"
th:href="${url.article(cover.id)}">
<span class="pb-focus-cover">
<img th:src="${url.upload(cover.coverImage)}" th:alt="${cover.title}">
</span>
<span class="pb-focus-title" th:text="${cover.title}">图片新闻标题</span>
</a>
<ul class="pb-focus-rows">
<li th:each="row, stat : ${focusArticles}" th:if="${stat.index > 0}">
<a th:href="${url.article(row.id)}">
<span class="pb-row-cover"><img th:src="${url.upload(row.coverImage)}" th:alt="${row.title}"></span>
<span class="pb-row-title" th:text="${row.title}">图片新闻标题</span>
</a>
</li>
</ul>
</div>
</div>
</section>
<!-- 一级栏目区块(聚合列表型子孙栏目 top 8,D1-2) -->
<section class="pb-section pb-container" aria-label="栏目区块">
<div class="pb-blocks">
<div class="pb-block" th:each="block : ${categoryBlocks}">
<div class="pb-block-head">
<h2 th:text="${block.category.name}">栏目名</h2>
<a class="pb-more" th:href="${block.moreUrl}">更多 +</a>
</div>
<ul class="pb-notice-list" th:if="${!#lists.isEmpty(block.items)}">
<!-- 区块头条:有封面则图文,无封面则带摘要纯文 -->
<li class="pb-feat" th:with="feat=${block.items[0]}">
<a th:href="${url.article(feat.id)}">
<span class="pb-feat-cover" th:if="${feat.coverImage != null}">
<img th:src="${url.upload(feat.coverImage)}" th:alt="${feat.title}">
</span>
<span class="pb-feat-body">
<span class="pb-feat-title" th:text="${feat.title}">头条标题</span>
<span class="pb-feat-summary" th:if="${!#strings.isEmpty(feat.summary)}"
th:text="${feat.summary}">摘要</span>
<span class="pb-news-meta">
<span class="pb-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="pb-notice-item" th:each="item, stat : ${block.items}" th:if="${stat.index > 0}">
<span class="pb-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="pb-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<span class="pb-top-badge" th:if="${item.topmost == 1}">置顶</span>
</li>
</ul>
<div class="pb-empty" th:if="${#lists.isEmpty(block.items)}">
<p>栏目暂无内容</p>
<p class="pb-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>